Keras : Ex-Tutorials : GloVe 単語埋め込みの活用 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/23/2018 (2.2.0)
* 本ページは Keras が提供しているサンプル examples/pretrained_word_embeddings.py とその解説記事をベースに
翻訳した上でまとめ直して、適宜、補足説明したものです:
- https://github.com/keras-team/keras/blob/master/examples/pretrained_word_embeddings.py
- https://blog.keras.io/using-pre-trained-word-embeddings-in-a-keras-model.html
* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
単語埋め込み
概要
本ページでは Stanford NLP の GloVe: Global Vectors for Word Representation が提供する、事前訓練された単語埋め込みと畳込みニューラルネットを使用して Keras 実装のテキスト分類問題の解法を示します。
「単語埋め込み」は意味論的意味を幾何学的空間にマップすることを意図した自然言語処理テクニッの一つで、任意の 2 つのベクトルの間の距離 (e.g. コサイン距離) が 2 つの関連単語の間の意味関係を部分的に捕捉するように、数値ベクトルを辞書の総ての単語に関連付けることにより遂行されます。これらのベクトルで形成される幾何学的空間は 埋め込み空間 と呼ばれます。
「単語埋め込み」はコーパスの単語間の共起統計情報のデータセットに次元削減技術を適用することで計算され、これはニューラルネット (word2vec) や行列分解 (= matrix factorization) を通して遂行されます。
噛み砕いて説明するならば、例えば、”ヤシの実” と “ネコ” は意味的に非常に異なる単語ですから合理的な埋め込み空間であればそれらを非常に遠く離れたベクトルとして表現するでしょう。けれども “kitchen” と “dinner” は関連単語なのでそれらは互いに近く埋め込まれるはずです。
理想的には、優れた埋め込み空間では “kitchen” から “dinner” へのベクトル (パス) はこれら 2 つの概念の間の意味関係を正確に捕捉します。この場合の関係性は “where x occurs” と考えられますから、ベクトル kitchen – (マイナス) dinner (= 2 つの埋め込みベクトルの差、i.e. dinner から kitchen へのパス) にはこの関係性 “where x occurs” を捕捉することが期待されます。
基本的には、以下のベクトルの恒等式が少なくとも近似的に成立すべきです :
dinner + (where x occurs) = kitchen
実際に成立すればその関係性ベクトルを質問に答えるために使用できます。
例えば、新しいベクトル e.g. “work” から初めてこの関係性ベクトルを適用すれば、意味を持つ, e.g. work + (where x occurs) = office を得ることができて “where does work occur?” に答えられるはずです。
事前訓練済みの単語埋め込み
事前訓練済みの単語埋め込みは幾つかが知られています :
GloVe 単語埋め込み
ここでは GloVe 埋め込みを使用します。GloVe は “Global Vectors for Word Representation” を表しますが、これは単語共起統計情報の行列の分解に基づく良く知られた埋め込みテクニックです。
GloVe Web サイト の説明によれば :
GloVe は単語のためのベクトル表現を得るための教師なし学習アルゴリズムです。
訓練はコーパスから集約された大域単語間 (= global word-word) 共起統計情報上で遂行されて、結果の表現は単語ベクトル空間の興味深い線形基礎構造を見せます。
特に、英語 Wikipedia 2014 + Gigaword 5 のダンプ上で計算された 400 K 語彙の100-次元 GloVe 埋め込みを使用します。それは ここ からダウンロードできますが、 822MB ダウンロードを開始します ので注意してください。
タスク
CMU : 20 ニュースグループ・ データセット
ここで解法が望まれるタスクは、20 の異なるニュースグループからの投稿をそれらの元の 20 カテゴリーに分類することです。
データセットは CMU Text Learning Group Data Archives が提供する “20 Newsgroup dataset” です。
このデータセットは 20,000 メッセージのコレクションで、20 の異なるネットニュース・ニュースグループから集められました。20 ニュースグループの各々からの 1,000 メッセージはランダムに選択されてニュースグループ名で分割されています。
ここ でデータセットについて読み生テキストデータをダウンロードできます。
ニュースグループのリストは次のようなものです :
alt.atheism talk.politics.guns talk.politics.mideast talk.politics.misc talk.religion.misc soc.religion.christian comp.sys.ibm.pc.hardware comp.graphics comp.os.ms-windows.misc comp.sys.mac.hardware comp.windows.x rec.autos rec.motorcycles rec.sport.baseball rec.sport.hockey sci.crypt sci.electronics sci.space sci.med misc.forsale
解法
この分類問題を解くためのアプローチは以下のようなものです :
- データセットの総てのテキストサンプルを単語インデックスのシークエンスに変換します。”単語インデックス” は単純に単語のための整数 ID です。データセット 20,000 で最も一般的に現れる単語だけを考え、シークエンスは 1000 単語の最大長 (MAX_SEQUENCE_LENGTH) に切り捨てます。
- “埋め込み行列” を準備します、これは (単語インデックス内における) インデックス i の単語のための埋め込みベクトルを含みます。
- この埋め込み行列を Keras 埋め込み層にロードします、これは frozen (凍結) として設定されます (その重みや埋め込みベクトルは訓練時に更新されません)。
- そのトップ上に 1D 畳み込みニューラルネットを構築して、20 カテゴリーに渡る softmax 出力で終えます。
テキストデータの準備
最初に、テキストサンプルがストアされているフォルダに渡り iterate し、それらをサンプルのリストにフォーマットします。サンプルに適合するクラス・インデックスのリストも同時に準備します :
texts = [] # テキストサンプルのリスト labels_index = {} # ラベル名を数値 id にマップする辞書 labels = [] # ラベル id のリスト for name in sorted(os.listdir(TEXT_DATA_DIR)): path = os.path.join(TEXT_DATA_DIR, name) if os.path.isdir(path): label_id = len(labels_index) labels_index[name] = label_id for fname in sorted(os.listdir(path)): if fname.isdigit(): fpath = os.path.join(path, fname) if sys.version_info < (3,): f = open(fpath) else: f = open(fpath, encoding='latin-1') t = f.read() i = t.find('\n\n') # skip header if 0 < i: t = t[i:] texts.append(t) f.close() labels.append(label_id) print('Found %s texts.' % len(texts))
それからテキストサンプルとラベルをニューラルネットに供給可能な tensor にフォーマットします。そのために、Keras ユティリティ keras.preprocessing.text.Tokenizer と keras.preprocessing.sequence.pad_sequences を利用します。
from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences tokenizer = Tokenizer(nb_words=MAX_NB_WORDS) tokenizer.fit_on_texts(texts) sequences = tokenizer.texts_to_sequences(texts) word_index = tokenizer.word_index print('Found %s unique tokens.' % len(word_index)) data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH) labels = to_categorical(np.asarray(labels)) print('Shape of data tensor:', data.shape) print('Shape of label tensor:', labels.shape) # データを訓練セットと検証セットに分割します。 indices = np.arange(data.shape[0]) np.random.shuffle(indices) data = data[indices] labels = labels[indices] nb_validation_samples = int(VALIDATION_SPLIT * data.shape[0]) x_train = data[:-nb_validation_samples] y_train = labels[:-nb_validation_samples] x_val = data[-nb_validation_samples:] y_val = labels[-nb_validation_samples:]
埋め込み層の準備
次に、事前訓練された埋め込みのデータダンプを解析することによって、単語を既知の埋め込みにマップするインデックスを計算します :
embeddings_index = {} f = open(os.path.join(GLOVE_DIR, 'glove.6B.100d.txt')) for line in f: values = line.split() word = values[0] coefs = np.asarray(values[1:], dtype='float32') embeddings_index[word] = coefs f.close() print('Found %s word vectors.' % len(embeddings_index))
この時点で embedding_index 辞書を word_index を埋め込み行列を計算するために活用できます :
embedding_matrix = np.zeros((len(word_index) + 1, EMBEDDING_DIM)) for word, i in word_index.items(): embedding_vector = embeddings_index.get(word) if embedding_vector is not None: # words not found in embedding index will be all-zeros. embedding_matrix[i] = embedding_vector
この埋め込み行列を Embedding 層にロードします。訓練中に重みが更新されることを回避するために trainable=False を設定することに注意してください。
from keras.layers import Embedding embedding_layer = Embedding(len(word_index) + 1, EMBEDDING_DIM, weights=[embedding_matrix], input_length=MAX_SEQUENCE_LENGTH, trainable=False)
Embedding 層は整数のシークエンス i.e. shape (samples, indices) の 2D 入力が供給されます。これらの入力シーケンスはそれらが総て入力データのバッチで同じ長さを持つようにパディングされるべきです (層に明示的に input_length 引数を渡さない場合には、Embedding 層は不均一の長さのシークエンスを処理することが可能ですが)。
Embedding 層が行なうことの総ては整数入力を埋め込み行列の対応するインデックスで見つかるベクトルにマップすることです、i.e. シークエンス [1, 2] は [embeddings[1], embeddings[2]] に変換されます。これは Embedding 層の出力が shape (samples, sequence_length, embedding_dim) の 3D tensor になることを意味します。
1D 畳み込みニューラルネットの訓練
最後に分類問題を解くために小さい 1D 畳み込みニューラルネットを構築できます :
sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32') embedded_sequences = embedding_layer(sequence_input) x = Conv1D(128, 5, activation='relu')(embedded_sequences) x = MaxPooling1D(5)(x) x = Conv1D(128, 5, activation='relu')(x) x = MaxPooling1D(5)(x) x = Conv1D(128, 5, activation='relu')(x) x = MaxPooling1D(35)(x) # global max pooling x = Flatten()(x) x = Dense(128, activation='relu')(x) preds = Dense(len(labels_index), activation='softmax')(x) model = Model(sequence_input, preds) model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['acc']) # happy learning! model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=2, batch_size=128)
このモデルは 2 エポックだけの後で検証セット上で 95 % 分類精度に達します。(dropout のような) 何某かの正則化メカニズムを伴いより長く訓練するか Embedding 層を再調整することによってより高い精度にさえ多分到達できるでしょう。
事前訓練された単語埋め込みを使用せずに代わりに Embedding 層をスクラッチから初期化して訓練の間にその重みを学習することによってどれくらい上手く遂行するかをテストすることもできます。次のようにして Embedding 層を置き換える必要があるだけです :
embedding_layer = Embedding(len(word_index) + 1, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH)
2 エポック後、このアプローチは、前のモデルが 1 エポックで到達するものよりも低い 90% 検証精度に達するだけです。事前訓練された埋め込みは明らかに私達に何某かを与えています。一般的に、事前訓練された埋め込みの使用は少ない訓練データ利用可能であるような自然言語タスクに適します (機能的には埋め込みはモデルに取って有用であると判明するかもしれない外部情報の注入として作用します)。
フルソースコード
'''This script loads pre-trained word embeddings (GloVe embeddings) into a frozen Keras Embedding layer, and uses it to train a text classification model on the 20 Newsgroup dataset (classification of newsgroup messages into 20 different categories). GloVe embedding data can be found at: http://nlp.stanford.edu/data/glove.6B.zip (source page: http://nlp.stanford.edu/projects/glove/) 20 Newsgroup data can be found at: http://www.cs.cmu.edu/afs/cs.cmu.edu/project/theo-20/www/data/news20.html ''' from __future__ import print_function import os import sys import numpy as np from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.utils import to_categorical from keras.layers import Dense, Input, GlobalMaxPooling1D from keras.layers import Conv1D, MaxPooling1D, Embedding from keras.models import Model BASE_DIR = '' GLOVE_DIR = os.path.join(BASE_DIR, 'glove.6B') TEXT_DATA_DIR = os.path.join(BASE_DIR, '20_newsgroup') MAX_SEQUENCE_LENGTH = 1000 MAX_NUM_WORDS = 20000 EMBEDDING_DIM = 100 VALIDATION_SPLIT = 0.2 # first, build index mapping words in the embeddings set # to their embedding vector print('Indexing word vectors.') embeddings_index = {} with open(os.path.join(GLOVE_DIR, 'glove.6B.100d.txt')) as f: for line in f: values = line.split() word = values[0] coefs = np.asarray(values[1:], dtype='float32') embeddings_index[word] = coefs print('Found %s word vectors.' % len(embeddings_index)) # second, prepare text samples and their labels print('Processing text dataset') texts = [] # list of text samples labels_index = {} # dictionary mapping label name to numeric id labels = [] # list of label ids for name in sorted(os.listdir(TEXT_DATA_DIR)): path = os.path.join(TEXT_DATA_DIR, name) if os.path.isdir(path): label_id = len(labels_index) labels_index[name] = label_id for fname in sorted(os.listdir(path)): if fname.isdigit(): fpath = os.path.join(path, fname) args = {} if sys.version_info < (3,) else {'encoding': 'latin-1'} with open(fpath, **args) as f: t = f.read() i = t.find('\n\n') # skip header if 0 < i: t = t[i:] texts.append(t) labels.append(label_id) print('Found %s texts.' % len(texts)) # finally, vectorize the text samples into a 2D integer tensor tokenizer = Tokenizer(num_words=MAX_NUM_WORDS) tokenizer.fit_on_texts(texts) sequences = tokenizer.texts_to_sequences(texts) word_index = tokenizer.word_index print('Found %s unique tokens.' % len(word_index)) data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH) labels = to_categorical(np.asarray(labels)) print('Shape of data tensor:', data.shape) print('Shape of label tensor:', labels.shape) # split the data into a training set and a validation set indices = np.arange(data.shape[0]) np.random.shuffle(indices) data = data[indices] labels = labels[indices] num_validation_samples = int(VALIDATION_SPLIT * data.shape[0]) x_train = data[:-num_validation_samples] y_train = labels[:-num_validation_samples] x_val = data[-num_validation_samples:] y_val = labels[-num_validation_samples:] print('Preparing embedding matrix.') # prepare embedding matrix num_words = min(MAX_NUM_WORDS, len(word_index) + 1) embedding_matrix = np.zeros((num_words, EMBEDDING_DIM)) for word, i in word_index.items(): if i >= MAX_NUM_WORDS: continue embedding_vector = embeddings_index.get(word) if embedding_vector is not None: # words not found in embedding index will be all-zeros. embedding_matrix[i] = embedding_vector # load pre-trained word embeddings into an Embedding layer # note that we set trainable = False so as to keep the embeddings fixed embedding_layer = Embedding(num_words, EMBEDDING_DIM, weights=[embedding_matrix], input_length=MAX_SEQUENCE_LENGTH, trainable=False) print('Training model.') # train a 1D convnet with global maxpooling sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32') embedded_sequences = embedding_layer(sequence_input) x = Conv1D(128, 5, activation='relu')(embedded_sequences) x = MaxPooling1D(5)(x) x = Conv1D(128, 5, activation='relu')(x) x = MaxPooling1D(5)(x) x = Conv1D(128, 5, activation='relu')(x) x = GlobalMaxPooling1D()(x) x = Dense(128, activation='relu')(x) preds = Dense(len(labels_index), activation='softmax')(x) model = Model(sequence_input, preds) model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['acc']) model.fit(x_train, y_train, batch_size=128, epochs=10, validation_data=(x_val, y_val))
以上