英文の自然言語処理で利用する前処理チートシート
概要
最近自然言語処理を扱うことになり、基本的な前処理を調べて実行する機会があったので、その際に利用したコードをまとめておく。 手法の理論とかの具体的な説明は各専門家に任せて、似たような処理を行いたい時のチートシート的な感じでまとめておく。
実際に利用したノートブックは以下の通り。
全体像
自然言語処理を機械学習するための前処理として、一般的に以下のことが実施される。
- クリーニング
- 単語のベクトル化
- 次元削減
1. クリーニング
クリーニングでは文章に含まれる、学習に不要・または悪影響を及ぼすような文字を取り除く作業が行われる。例えば、文中の「,(カンマ)」や「.(ピリオド)」といった記号や、英語であれば(I,at,of,a,an)
のような文章に多用される単語を元データから除去する。
1-1. TextHero
TextHeroは、英語の前処理を簡単に行えるようにしたものである。一般的に英語の前処理には、大文字を小文字に統一したり、空白の除去、StopWordsと呼ばれる(I,at,of,a,an)
を除去する方法が用いられる。
TextHeroはそういった面倒な前処理を簡単に実装できるライブラリとなっている。
公式ドキュメントは以下の撮り。
import texthero as hero import pandas as pd df = pd.read_csv( "https://github.com/jbesomi/texthero/raw/master/dataset/bbcsport.csv" ) def texthero_clean(input_df: pd.Series): ''' 英語の自然言語が含まれるDataFrameに対して、TextHeroを用いたクリーニング処理を行う。 ''' input_df = input_df.fillna('missing').astype(str) # 空白をmissingで埋める custom_pipeline = [ hero.preprocessing.fillna, # null埋め hero.preprocessing.lowercase, # 全単語を小文字 hero.preprocessing.remove_digits, # 数字を除去 hero.preprocessing.remove_punctuation, # 句読点を除去 hero.preprocessing.remove_diacritics, # 分音記号を除去 hero.preprocessing.remove_stopwords, # 一般的であるという単語(I,at,of,a,an) hero.preprocessing.remove_whitespace, # 空白 hero.preprocessing.stem # 改行文字を取り除く. https://texthero.org/docs/api/texthero.preprocessing.stem ] texts = hero.clean(input_df, custom_pipeline) del input_df, custom_pipeline return texts df['clean_text'] = texthero_clean(df['text'])
結果を出力して確認する。↓原文
df['text'] 'Claxton hunting first major medal\n\nBritish hurdler Sarah Claxton is confident she can win her first major medal at next month\'s European Indoor Championships in Madrid.\n\nThe 25-year-old has already smashed the British record over 60m hurdles twice this season, setting a new mark of 7.96 seconds to win the AAAs title. "I am quite confident," said Claxton. "But I take each race as it comes. "As long as I keep up my training but not do too much I think there is a chance of a medal." Claxton has won the national 60m hurdles title for the past three years but has struggled to translate her domestic success to the international stage. Now, the Scotland-born athlete owns the equal fifth-fastest time in the world this year. And at last week\'s Birmingham Grand Prix, Claxton left European medal favourite Russian Irina Shevchenko trailing in sixth spot.\n\nFor the first time, Claxton has only been preparing for a campaign over the hurdles - which could explain her leap in form. In previous seasons, the 25-year-old also contested the long jump but since moving from Colchester to London she has re-focused her attentions. Claxton will see if her new training regime pays dividends at the European Indoors which take place on 5-6 March.\n'
前処理後のテキスト
df['clean_text'][0] 'claxton hunt first major medal british hurdler sarah claxton is confid she can win her first major medal at next month s european indoor championship in madrid the year old has alreadi smash the british record over 60m hurdl twice this season set a new mark of second to win the aaa titl i am quit confid said claxton but i take each race as it come as long as i keep up my train but not do too much i think there is a chanc of a medal claxton has won the nation 60m hurdl titl for the past three year but has struggl to translat her domest success to the intern stage now the scotland born athlet own the equal fifth fastest time in the world this year and at last week s birmingham grand prix claxton left european medal favourit russian irina shevchenko trail in sixth spot for the first time claxton has onli been prepar for a campaign over the hurdl which could explain her leap in form in previous season the year old also contest the long jump but sinc move from colchest to london she has re focus her attent claxton will see if her new train regim pay dividend at the european indoor which take place on march'
基本的な前処理を簡単に実現できるので、ほとんどの場合の前処理に関してはTextHeroで事足りそう。
2. 単語のベクトル化
文章に対して機械学習を行うために、テキストをベクトルの数値表現へ変換する。
Word2Vec
Word2Vecは単語のベクトル表現のうち、SkipGramという手法で学習された単語のベクトル表現の1つである。SkipGramは、単語を前後の単語の出現頻度を測定することで、その単語のベクトル表現を獲得する手法である。
今回は自分で新規に学習してWord2Vecのモデルを作成するのではなく、公開されている学習済み重みを利用するパターンで実装を行う。広く利用されているものとして、Googleが公開しているGoogleNews-vectors-negative300
がある。こちらはGoogle News Datasetに含まれる100億単語を学習して作成されたもので、入力した単語を300次元のベクトルに変換できる。
英語のベクトル表現は、以下のリンクからダウンロード可能である。ダウンロードしたファイルを./GoogleNews-vectors-negative300.bin
として、以降に示すスクリプトと同じパスに保存する。
各言語のベクトル表現は以下のリンクから取得できるので、他の言語を利用したい場合は、こちらから利用したい言語のものを探す。
上記のGoogleNewsと後で紹介するFastTextの、どちらかを用いて単語のベクトル表現を獲得するクラスを以下に示す。初期化の時にmodel_name
で指定した重みを、単語のベクトル化の際に利用する。FastTextもGoogleNewsのモデルもベクトル化の手順は同じなので、1つのクラスで使い分けられるようにした。
# Word2Vec用のクラス import gensim import numpy as np import pandas as pd class SentenceVectorizer(): def __init__(self, model_name="fasttext"): if model_name == "fasttext": self._model = gensim.models.KeyedVectors.load_word2vec_format( "../signate-paper/Fasttext-vectors300.vec", binary=False ) elif model_name == "googlenews": self._model = gensim.models.KeyedVectors.load_word2vec_format( "../signate-paper/GoogleNews-vectors-negative300.bin", binary=True ) else: exit() self.out_dim = 300 self.model_name = model_name def vectorize(self, sentence: str) -> pd.Series: """ 1つの文章に対するWord2Vecの処理の実行 """ func = np.median return pd.Series(func( [self._model[w] for w in sentence.split(" ") if w in self._model], axis=0, )) def fit_transform(self, input_df: pd.DataFrame, col_prefix: str = ''): """ 入力されたデータフレームに対してWord2Vecを実行する。 col_prefixに任意の文字列を指定することで、カラムに任意の接頭詞付与する。 """ _prefix = self.model_name if col_prefix: _prefix = col_prefix + '_' + _prefix columns=[_prefix + f'_{i:03}' for i in range(self.out_dim)] vectorized_np = input_df.apply(self.vectorize) resoponse_df = pd.DataFrame(vectorized_np.values, columns=columns) del columns, vectorized_np, col_prefix, input_df return resoponse_df
こちらのクラスを用いて、先ほどのTextHeroのサンプルのベクトル化を実施する。
g_model_vectorizer = SentenceVectorizer('googlenews') gnews_text_df = g_model_vectorizer.fit_transform(df['clean_text'], 'gnews_text')
出力すると以下のように、文章が300次元のベクトルに変換されていることが確認できる。
gnews_text_df.head(1) herosample_googlenews_000 herosample_googlenews_001 herosample_googlenews_002 herosample_googlenews_003 herosample_googlenews_004 herosample_googlenews_005 herosample_googlenews_006 herosample_googlenews_007 herosample_googlenews_008 herosample_googlenews_009 ... herosample_googlenews_290 herosample_googlenews_291 herosample_googlenews_292 herosample_googlenews_293 herosample_googlenews_294 herosample_googlenews_295 herosample_googlenews_296 herosample_googlenews_297 herosample_googlenews_298 herosample_googlenews_299 0 0.010895 0.028931 0.015381 0.07373 0.032227 -0.080322 -0.020935 -0.099121 0.076172 0.121338 ... -0.031494 0.023804 -0.098633 -0.010895 -0.026733 -0.063232 -0.023193 -0.002747 0.00325 -0.045532 1 rows × 300 columns
FastText
FastTextは、Word2Vecの中でも人間の感覚に近い活用形をまとめられるようなモデルとなっており、近年注目されている手法である。
詳細に関しては以下のリンクを参照。
FastTextの有用性に関しては、1つ目の記事の以下の箇所で説明されている。
fastTextでは、Word2Vecとその類型のモデルでそれまで考慮されていなかった、「活用形」をまとめられるようなモデルになっています。具体的には、goとgoes、そしてgoing、これらは全て「go」ですが、字面的にはすべて異なるのでこれまでの手法では別々の単語として扱われてしまいます。そこで、単語を構成要素に分解したもの(イメージ的には、goesなら「go」「es」)を考慮することで、字面の近しい単語同士により意味のまとまりをもたせるという手法を提案しています(Subword model)。
このように、確かにFastTextの方が単語のベクトル表現としてはWord2Vecよりは優れていそうである。しかしケースバイケースということで、どちらの方が全ての場合において優れている、ということではなさそう。
FastTextを利用し文章のベクトル化を実施する。以下の記事のダウンロードリンクから学習済みモデルをダウンロードし、Fasttext-vectors300.vec
という名前で上記のクラスと同じディレクトリに配置する。
その後、先ほどのSentenceVectorizer()
を利用して単語の分散表現を獲得する処理を実行する。
fasttext_vectorizer = SentenceVectorizer('fasttext') fasttext_text_df = fasttext_vectorizer.fit_transform(df['clean_text'], 'fasttext_text')
こちらもWord2Vecの場合と同様に、単語が300次元のベクトルに変換されていることが確認できた。
fasttext_text_df.head(1) herosample_googlenews_000 herosample_googlenews_001 herosample_googlenews_002 herosample_googlenews_003 herosample_googlenews_004 herosample_googlenews_005 herosample_googlenews_006 herosample_googlenews_007 herosample_googlenews_008 herosample_googlenews_009 ... herosample_googlenews_290 herosample_googlenews_291 herosample_googlenews_292 herosample_googlenews_293 herosample_googlenews_294 herosample_googlenews_295 herosample_googlenews_296 herosample_googlenews_297 herosample_googlenews_298 herosample_googlenews_299 0 0.010895 0.028931 0.015381 0.07373 0.032227 -0.080322 -0.020935 -0.099121 0.076172 0.121338 ... -0.031494 0.023804 -0.098633 -0.010895 -0.026733 -0.063232 -0.023193 -0.002747 0.00325 -0.045532 1 rows × 300 columns
BERT
BERTはGoogleが2018年に発表した自然言語処理モデルの1つで、自然言語処理に関するほとんどのタスクで当時の最高の性能を出したモデルである。単語のベクトル表現の取得に関しても高い精度を誇っているため、多くの場合で上記のWord2VecではなくBERTが使われることが多い。
BERTに関する詳細は以下の記事を参照。
今回はPytorchで実装する。
import torch import transformers class BertVectorizer: """ 事前学習済み BERT モデルを使った文章のベクトル化 """ def __init__(self, model_name='bert-base-uncased', max_len=128): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' self.model_name = model_name self.tokenizer = transformers.AutoTokenizer.from_pretrained(self.model_name) self.model = transformers.AutoModel.from_pretrained(self.model_name) self.model = self.model.to(self.device) self.max_len = max_len self.out_dim = 768 # 出力の次元数。利用するモデルに依存する。 def vectorize(self, sentence : str) -> np.array: inp = self.tokenizer.encode(sentence) len_inp = len(inp) if len_inp >= self.max_len: inputs = inp[:self.max_len] masks = [1] * self.max_len else: inputs = inp + [0] * (self.max_len - len_inp) masks = [1] * len_inp + [0] * (self.max_len - len_inp) inputs_tensor = torch.tensor([inputs], dtype=torch.long).to(self.device) masks_tensor = torch.tensor([masks], dtype=torch.long).to(self.device) output = self.model(inputs_tensor, masks_tensor) seq_out, pooled_out = output['last_hidden_state'], output['pooler_output'] if torch.cuda.is_available(): return seq_out[0][0].cpu().detach().numpy() # 0番目は [CLS] token, 768 dim の文章特徴量 else: return seq_out[0][0].detach().numpy() def fit_transform(self, df: pd.Series, col_prefix: str = ''): """ 入力されたデータフレームに対してBERTを実行する。 col_prefixに任意の文字列を指定することで、カラムに任意の接頭詞付与する。 """ _prefix = self.model_name if col_prefix: _prefix = col_prefix + '_' + _prefix columns=[_prefix + f'_{i:03}' for i in range(self.out_dim)] vectorized_np = np.array([self.vectorize(x) for x in df]) resoponse_df = pd.DataFrame(vectorized_np, columns=columns) del columns, vectorized_np, col_prefix return resoponse_df
同様に実行してみる。DeepLearningの手法の1つでCPU実行だと時間がかかるので、実際にコンペで利用する場合はGoogle Colabなどを利用した方が良さそう。
bert_vectorizer = BertVectorizer() bert_text_df = bert_vectorizer.fit_transform(df['clean_text']) bert-base-uncased_000 bert-base-uncased_001 bert-base-uncased_002 bert-base-uncased_003 bert-base-uncased_004 bert-base-uncased_005 bert-base-uncased_006 bert-base-uncased_007 bert-base-uncased_008 bert-base-uncased_009 ... bert-base-uncased_758 bert-base-uncased_759 bert-base-uncased_760 bert-base-uncased_761 bert-base-uncased_762 bert-base-uncased_763 bert-base-uncased_764 bert-base-uncased_765 bert-base-uncased_766 bert-base-uncased_767 0 -0.049896 -0.053156 0.439427 -0.175278 0.102648 -0.09097 0.156063 -0.070803 -0.012865 -0.195102 ... 0.381639 -0.290457 -0.07601 -0.035404 0.126096 -0.341293 -0.095096 -0.226657 -0.085968 -0.037578
次元削減
上記の手順で得られた単語のベクトル表現は、多くの場合が高次元のベクトルのため、機械学習などで用いられる場合、次元削減により次元圧縮した後に利用されることが多い。
3-1. 主成分分析(PCA)
次元削減の代表といえば、主成分分析(PCA)がある。こちらはsklearnで実装済みのものがあるので、簡単に利用可能である。
from sklearn.decomposition import PCA def pca_reduce(input_df: pd.Series, col_prefix: str=None, n_components: int=2): """PCAで圧縮 """ _prefix = "pca" if col_prefix: _prefix = col_prefix + '_' + _prefix columns=[_prefix + f'_{i:03}' for i in range(n_components)] return pd.DataFrame( PCA(n_components=n_components).fit_transform(input_df), columns=columns, )
FastTextを用いてベクトル化した結果に対して、PCAで次元削減を行う。結果を出力してみると、300次元のベクトルが2次元に削減されていることが確認できる。
fasttext_pca = pca_reduce(fasttext_text_df, 'herosample_fasttext') fasttext_pca.head(1) herosample_fasttext_pca_000 herosample_fasttext_pca_001 0 0.019008 -0.099318
3-2. t-SNE
PCAは、多次元かつ非線形構造をもつデータに対して弱いという欠点があり、代わりに利用されるものとしてt-SNEがある。こちらは圧縮後の次元が、原則2次元、または3次元という制限はあるが、高速に学習可能だったり、PCAより性能が良くなるケースが多かったりという理由から使われることが多い。
t-SNEはsklearnよりもbhtsneが利用されることが多いため、そちらで実装する。
import bhtsne def tsne_reduce(input_df: pd.Series, col_prefix: str=None, n_components: int=2): """t-SNEで圧縮 """ _prefix = "tsne" if col_prefix: _prefix = col_prefix + '_' + _prefix _bhtsne = bhtsne.tsne(input_df.astype(np.float64), dimensions=n_components, rand_seed=42) columns=[_prefix + f'_{i:03}' for i in range(n_components)] return pd.DataFrame(_bhtsne, columns=columns)
PCAと同様にFastTextを用いてベクトル化した結果に対してtsneで次元削減を行う。
fasttext_tnse = tsne_reduce(fasttext_text_df, 'herosample_fasttext') fasttext_tnse.head(1) herosample_fasttext_tsne_000 herosample_fasttext_tsne_001 0 1.509905 -7.385899
3-3. Umap
Umapは、t-SNEより高速・高性能に次元削減を行える手法である。最近は次元削減の方法としてt-SNEよりもこちらが利用されることも多い。
Umapはsklearnに実装済みのものがないため、追加でUmapライブラリをインストールする必要がある。
import umap def umap_reduce(input_df, col_prefix=None, n_components=10): """umapで圧縮 """ _prefix = "umap" if col_prefix: _prefix = col_prefix + '_' + _prefix columns=[_prefix + f'_{i:03}' for i in range(n_components)] return pd.DataFrame( umap.UMAP(n_components=n_components).fit_transform(input_df), columns=columns, )
PCAと同様にFastTextを用いてベクトル化した結果に対してUmapで次元削減を行う。
fasttext_umap = umap_reduce(fasttext_text_df, 'herosample_fasttext') fasttext_umap.head(1) herosample_fasttext_umap_000 herosample_fasttext_umap_001 herosample_fasttext_umap_002 herosample_fasttext_umap_003 herosample_fasttext_umap_004 herosample_fasttext_umap_005 herosample_fasttext_umap_006 herosample_fasttext_umap_007 herosample_fasttext_umap_008 herosample_fasttext_umap_009 0 10.521688 7.048684 2.234146 3.02162 4.69678 4.38614 5.731061 5.076957 1.378522 4.385059