Keras : Ex-Tutorials : GloVe 単語埋め込みの活用

Keras : Ex-Tutorials : GloVe 単語埋め込みの活用 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/23/2018 (2.2.0)

* 本ページは Keras が提供しているサンプル examples/pretrained_word_embeddings.py とその解説記事をベースに
翻訳した上でまとめ直して、適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

単語埋め込み

概要

本ページでは Stanford NLPGloVe: 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))
 

以上