TensorFlow 機械学習ガイド : テキスト分類 (3) データの準備 (前処理) (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 08/22/2018
* 本ページは、developers.google.com サイトの Machine Learning Guides : Text classification の以下のページを
翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
Step 3: データを準備する
データがモデルに供給できる前に、それはモデルが理解できるフォーマットに変換される必要があります。
最初に、集めたデータ・サンプルは特別な順序にあるかもしれません。テキストとラベル間の関係に影響するサンプルの順序に関連するどのような情報も望みません。例えば、データセットがクラスでソートされてから訓練/検証セットに分割されている場合、これらのセットはデータの全体的な分布の見本にはなりません。
モデルがデータ順序に影響されないことを確実にする単純なベストプラクティスは他に何かする前にデータを必ずシャッフルすることです。もしデータが既に訓練と検証セットに分割されている場合には、訓練データを変換するのと同じ方法で検証データを変換することを確実にしてください。分離した訓練と検証セットをまだ持たない場合には、シャッフル後にサンプルを分割することもできます ; 典型的には訓練のためにサンプルの 80% をそして検証のために 20% を使用します。
2 番目に、機械学習アルゴリズムは入力として数字を取ります。これはテキストを数値ベクトルに変換する必要があることを意味します。このプロセスには 2 つのステップがあります :
- トークン化 (=Tokenization): テキストを単語かより小さい部分テキストに分割します、これはテキストとラベル間の関係の良い一般化を可能にします。これはデータセットの「語彙 (= vocabulary)」を決定します (データに現れる一意なトークンの集合)。
- ベクトル化: これらのテキストを特性化するために良い数値尺度を定義します。
n-gram ベクトルとシークエンス・ベクトルの両者のためのこれら 2 つのステップをどのように遂行するか、そして特徴選択と正規化技術を使用してどのようにベクトル表現を最適化するかを見ましょう。
N-gram ベクトル [オプション A]
次のパラグラフでは、n-gram モデルのためのトークン化とベクトル化をどのように行なうかを見ます。特徴選択と正規化技術を使用して n-gram 表現をどのように最適化できるかもまたカバーします。
n-gram ベクトルでは、テキストは一意な n-gram: n 隣接トークン (典型的には、単語) のコレクションとして表わされます。テキスト The mouse ran up the clock を考えます。ここで、単語 unigram (n=1) は [‘the’, ‘mouse’, ‘ran’, ‘up’, ‘clock’] で、単語 bigram (n = 2) は [‘the mouse’, ‘mouse ran’, ‘ran up’, ‘up the’, ‘the clock’]、等々。
トークン化
単語 unigram + bigram へのトークン化が良い精度を提供する一方で、より少ない計算時間しかかからないことを私達は見出しました。
ベクトル化
テキスト・サンプルを n-gram にひとたび分割したならば、これらの n-gram を機械学習モデルが処理できる数値ベクトルに変える必要があります。下のサンプルは 2 つのテキストのために生成された unigram と bigram に割り当てられたインデックスを示します。
Texts: 'The mouse ran up the clock' and 'The mouse ran down' Index assigned for every token: {'the': 7, 'mouse': 2, 'ran': 4, 'up': 10, 'clock': 0, 'the mouse': 9, 'mouse ran': 3, 'ran up': 6, 'up the': 11, 'the clock': 8, 'down': 1, 'ran down': 5}
ひとたびインデックスが n-gram に割り当てられれば、典型的には次のオプションを使用してベクトル化します。
One-hot エンコーディング: 総てのサンプルテキストはテキスト内のトークンの存在か欠如を示すベクトルとして現れます。
'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]
カウント・エンコーディング: 総てのサンプルテキストはテキスト内のトークンのカウントを示すベクトルとして表わされます。unigram ‘the’ に対応する要素は 2 として表わされることに注意してください、何故ならば単語 “the” はテキスト内に 2 度現れるからです。
'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1]
Tf-idf エンコーディング: 上の 2 つのアプローチが持つ問題は総ての文書で同様の頻度で現れる一般的な単語 (i.e., データセットのテキストサンプルに特に固有ではない単語) にペナルティが与えられないことです。例えば、”a” のような単語は総てのテキストで非常に頻繁に現れます。そのため他の意味のある単語よりもより高い “the” に対するトークンカウントは大して有用ではありません。
'The mouse ran up the clock' = [0.33, 0, 0.23, 0.23, 0.23, 0, 0.33, 0.47, 0.33, 0.23, 0.33, 0.33] (Scikit-learn TdidfTransformer 参照)
多くの他のベクトル表現がありますが、上の 3 つは最も一般的に使用されます。
tf-idf エンコーディングは他の 2 つよりも精度 (平均で: 0.25-15% higher) の点で僅かにより良いことを私達は観察し、そして n-gram のベクトル化にこのメソッドを使用することを勧めます。けれども、それはより多くのメモリを占有して (何故ならばそれは浮動小数点表現を使用しますので) 計算するためにより多くの時間がかかることに留意してください、特に巨大なデータセットについて (ある場合には 2 倍の長さがかかるかもしれません)。
特徴選択
データセットのテキストの総てを単語 uni+bigram トークンに変換するとき、数万のトークンという結果で終わるかもしれません。これらのトークン/特徴の総てがラベル予測に寄与するわけではありません。従って、特定のトークンを破棄することができます、例えばデータセットに渡り極めて稀にしか出現しないものです。特徴の重要性を測定することもでき (各トークンがラベル予測にどの程度寄与するか)、そして最も情報を与える (= informative) トークンだけを含むことができます。
特徴と対応するラベルを取り特徴重要性スコアを出力する多くの統計関数があります。2 つの一般に使用される関数は f_classif と chi2 です。私達の実験はこれらの関数の両者が同等に良く遂行することを示します。
より重要なことは、多くのデータセットに対して精度は 20,000 特徴あたりでピークに達します (Figure 6 参照)。この閾値を越えてより多くの特徴を追加しても殆ど寄与せずそして時に overfitting に繋がってパフォーマンスが低下さえします。
★ ここでのテストでは英語テキストだけを使用しました。特徴の理想的な数は言語により変化するかもしれません ; これは補足解析で調査されるでしょう。
Figure 6: Top K 特徴 vs 精度。データセットに渡り、精度は top 20K 特徴あたりで平坦化します。
正規化
正規化は総ての特徴/サンプルの値を小さくて類似の値に変換します。これは学習アルゴリズムにおける勾配降下の収束を単純化します。私達が見たことからは、データ前処理の間の正規化はテキスト分類問題において大した値を追加しないように見えます ; このステップをスキップすることを勧めます。
◆ 次のコードは上のステップの総てをまとめています :
- テキスト・サンプルを単語 uni+bigram にトークン化し、
- tf-idf エンコーディングでベクトル化して、
- 2 回より少なく出現するトークンを捨てて特徴重要性を計算するために f_classif を使用することによりトークンのベクトルから top 20,000 特徴だけを選択します。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.feature_selection import SelectKBest from sklearn.feature_selection import f_classif # Vectorization parameters # Range (inclusive) of n-gram sizes for tokenizing text. NGRAM_RANGE = (1, 2) # Limit on the number of features. We use the top 20K features. TOP_K = 20000 # Whether text should be split into word or character n-grams. # One of 'word', 'char'. TOKEN_MODE = 'word' # Minimum document/corpus frequency below which a token will be discarded. MIN_DOCUMENT_FREQUENCY = 2 def ngram_vectorize(train_texts, train_labels, val_texts): """Vectorizes texts as n-gram vectors. 1 text = 1 tf-idf vector the length of vocabulary of unigrams + bigrams. # Arguments train_texts: list, training text strings. train_labels: np.ndarray, training labels. val_texts: list, validation text strings. # Returns x_train, x_val: vectorized training and validation texts """ # Create keyword arguments to pass to the 'tf-idf' vectorizer. kwargs = { 'ngram_range': NGRAM_RANGE, # Use 1-grams + 2-grams. 'dtype': 'int32', 'strip_accents': 'unicode', 'decode_error': 'replace', 'analyzer': TOKEN_MODE, # Split text into word tokens. 'min_df': MIN_DOCUMENT_FREQUENCY, } vectorizer = TfidfVectorizer(**kwargs) # Learn vocabulary from training texts and vectorize training texts. x_train = vectorizer.fit_transform(train_texts) # Vectorize validation texts. x_val = vectorizer.transform(val_texts) # Select top 'k' of the vectorized features. selector = SelectKBest(f_classif, k=min(TOP_K, x_train.shape[1])) selector.fit(x_train, train_labels) x_train = selector.transform(x_train).astype('float32') x_val = selector.transform(x_val).astype('float32') return x_train, x_val
n-gram ベクトル表現では、単語順序と文法について多くの情報を捨てます (せいぜい、n > 1 の時にある程度の部分的な順序情報を維持できるだけです)。これは bag-of-words アプローチと呼ばれます。この表現はロジスティック回帰、多層パーセプトロン、勾配ブースティング・マシン、サポートベクターマシンのような、順序を考慮しないモデルと連携して使用されます。
シークエンス・ベクトル [オプション B]
次のパラグラフでは、シークエンス・モデルのためのトークン化とベクトル化をどのように行なうかを見ます。特徴選択と正規化技術を使用してシークエンス表現をどのように最適化できるかもまたカバーします。
あるテキスト・サンプルについては、単語順序はテキストの意味に対して重要です。例えば、センテンス “I used to hate my commute. My new bike changed that completely” は順序正しく読むことによってのみ理解可能です。CNN/RNN のようなモデルはサンプルの単語の順序から意味を推論できます。これらのモデルのためには、テキストを順序を保持して、トークンのシークエンスとして表します。
トークン化
テキストは文字のシークエンスか、単語のシークエンスとして表わされます。単語レベル表現の使用が文字トークンよりもより良いパフォーマンスを提供することを見出しました。これはまた業界で追随される一般的な規範です。文字トークンの使用はテキストが多くのタイポを持つ場合に限り意味があり、これは通常は当てはまりません。
ベクトル化
ひとたびテキスト・サンプルを単語のシークエンスに変換したならば、これらのシークエンスを数値ベクトルに変える必要があります。下の例は 2 つのテキストのために生成された unigram に割り当てられたインデックスと、それから最初のテキストが変換されたトークン・インデックスのシークエンスです。
Texts: 'The mouse ran up the clock' and 'The mouse ran down' Index assigned for every token: {'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}. NOTE: 'the' occurs most frequently, so the index value of 1 is assigned to it. Some libraries reserve index 0 for unknown tokens, as is the case here. Sequence of token indexes: 'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]
トークン・シークエンスをベクトル化するために利用可能な 2 つのオプションがあります。
One-hot エンコーディング: シークエンスは n-次元空間の単語ベクトルを使用して表わされます、ここでn = 語彙のサイズです。この表現は文字としてトークン化しているとき、そして従って語彙が小さいときに素晴らしく動作します。単語としてトークン化しているときは、語彙は通常は数万のトークンを持ち、one-hot ベクトルを非常にスパースで非効率にします。例えば :
'The mouse ran up the clock' = [ [0, 1, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0] ]
単語埋め込み (= Word embeddings): 単語はそれらに関係する意味を持ちます。結果として、単語トークンを密ベクトル空間 (~ 数百の実数) 内で表わすことができます、そこでは位置と単語間の距離はそれらが意味的にどのように類似しているかを示します (Figure 7 参照)。この表現は単語埋め込み (= word embeddings)と呼ばれます。
Figure 7: 単語埋め込み (= Word embeddings)
シークエンス・モデルは最初の層としてそのような埋め込み層をしばしば持ちます。この層は訓練プロセスの間に単語インデックス・シークエンスを単語埋め込みベクトルに変えることを学習します、各単語インデックスが意味空間の単語の位置を表わす実数の密ベクトルにマップされるようにです (Figure 8 参照)。
Figure 8: 埋め込み層 (= Embedding layer)
特徴選択
データの総ての単語がラベル予測に寄与するわけではありません。語彙から稀だったり重要でない単語を捨てることで学習プロセスを最適化できます。実際に、最も頻度の高い 20,000 特徴の使用が一般に十分であることを私達は観察しました。これは n-gram モデルに対しても事実です (Figure 6 参照)。
シークエンス・ベクトル化の上のステップの総てをまとめましょう。次のコードはこれらのタスクを遂行します :
- テキストを単語にトークン化する
- top 20,000 トークンを使用して語彙を作成する
- トークンをシークエンス・ベクトルに変換する
- シークエンスを固定されたシークエンス長にパッドする
from tensorflow.python.keras.preprocessing import sequence from tensorflow.python.keras.preprocessing import text # Vectorization parameters # Limit on the number of features. We use the top 20K features. TOP_K = 20000 # Limit on the length of text sequences. Sequences longer than this # will be truncated. MAX_SEQUENCE_LENGTH = 500 def sequence_vectorize(train_texts, val_texts): """Vectorizes texts as sequence vectors. 1 text = 1 sequence vector with fixed length. # Arguments train_texts: list, training text strings. val_texts: list, validation text strings. # Returns x_train, x_val, word_index: vectorized training and validation texts and word index dictionary. """ # Create vocabulary with training texts. tokenizer = text.Tokenizer(num_words=TOP_K) tokenizer.fit_on_texts(train_texts) # Vectorize training and validation texts. x_train = tokenizer.texts_to_sequences(train_texts) x_val = tokenizer.texts_to_sequences(val_texts) # Get max sequence length. max_length = len(max(x_train, key=len)) if max_length > MAX_SEQUENCE_LENGTH: max_length = MAX_SEQUENCE_LENGTH # Fix sequence length to max value. Sequences shorter than the length are # padded in the beginning and sequences longer are truncated # at the beginning. x_train = sequence.pad_sequences(x_train, maxlen=max_length) x_val = sequence.pad_sequences(x_val, maxlen=max_length) return x_train, x_val, tokenizer.word_index
ラベルのベクトル化
サンプルのテキストデータをどのように数値ベクトルに変換するかを見ました。同様のプロセスはラベルに適用されなければなりません。ラベルを範囲 [0, num_classes – 1] の値に単純に変換できます。例えば、3 クラスがあれば、それらを表わすために値 0, 1 と 2 を単に使用できます。内部的には、ネットワークはこれらの値を表わすために one-hot ベクトルを使用します (ラベル間の間違った関係を推定することを回避するため)。この表現はネットワークで使用する損失関数と最後の層の活性化関数に依拠します。これらについての次のセクションで更に学習します。
以上