Python for NLP: KerasによるディープラーニングのためのWord Embeddings

Python for NLPの連載は今回で16回目です。

前回の記事では、Pythonで簡単な自動テキストフィラーを開発するためにN-Gramsの技術をどのように使用できるかを説明しました。

N-Gramモデルは基本的にテキストデータを統計的アルゴリズムで利用できるように数値化する方法です。

N-Gramの前に、Bag of WordやTF-IDFのアプローチを説明しましたが、これらもテキストデータから数値特徴ベクトルを生成するのに利用できます。

これまで、我々は機械学習を用いて、テキストの分類、トピックモデリング、感傷分析、テキストの要約など、様々な自然言語処理タスクを行ってきた。

この記事では、NLPのための深層学習技術についての議論を開始します。

ディープラーニングは、様々な種類の密結合ニューラルネットワークから構成されています。

これらのアプローチは、自動運転車、画像生成、画像分割など、いくつかの複雑なタスクを解決するのに有効であることが証明されています。

また、ディープラーニングアプローチは自然言語処理タスクにも有効であることが証明されている。

この記事では、ディープラーニングを用いた自然言語処理タスクのための単語埋め込みを研究します。

PythonのKerasライブラリでディープニューラルネットワークを使って簡単な分類タスクを実行するために、どのように単語埋め込みを使用できるかを見ていきます。

一熱符号化特徴ベクトルアプローチの問題点

N-Grams、bag of words、TF-IDFなどの一次元化特徴ベクトルの欠点は、各文書に対する特徴ベクトルが巨大になることである。

例えば、コーパスに50万語の単語があり、10個の単語を含む文を表現したい場合、特徴ベクトルは50万次元の一発符号化ベクトルとなり、10個のインデックスだけが1を持つことになる。

これは空間の浪費であり、アルゴリズムの複雑さを指数関数的に増大させるので、次元の呪いと呼ばれる。

ワードエンベッディング

単語埋め込みでは、すべての単語はn次元の密なベクトルとして表現される。

似たような単語は似たようなベクトルを持つ。

GloVeやWord2Vecのような単語埋め込み技術は、単語を対応する密なベクトルに変換するのに非常に効率的であることが証明されています。

ベクトルサイズは小さく、ベクトル内のインデックスも実際には一つも空ではない。

KerasのシーケンシャルモデルでWord Embeddingsを実装する。

Kerasライブラリは、TensorFlowの上に構築されたPython用の最も有名でよく使われる深層学習ライブラリの1つです。

Kerasは2種類のAPIをサポートしています。

SequentialとFunctionalです。

このセクションでは、KerasのSequential APIを使用して単語埋め込みがどのように使用されるかを見ていきます。

次の章では、KerasのFunctional APIで同じモデルを実装する方法を説明します。

単語埋め込みを実装するために、KerasライブラリにはEmbedding()というレイヤーがあります。

埋め込み層はKerasのクラスの形で実装され、通常NLPタスクの逐次モデルの最初の層として使用される。

埋め込み層はKerasの3つのタスクに使用することができます。

  • 単語の埋め込みを学習し、その結果を保存する。
  • テキスト分類、感情分析などの自然言語処理タスクに加え、単語埋め込みを学習することができます。
  • 事前に学習した単語埋め込みをロードして、新しいモデルで利用することができます。

今回は、埋め込み層の2つ目と3つ目のユースケースを紹介します。

最初の使用例は、2番目の使用例のサブセットです。

それでは、エンベッディングレイヤーがどのように見えるか見てみましょう。

embedding_layer = Embedding(200, 32, input_length=50)


embeddig層の最初のパラメータは、語彙の大きさ、つまりコーパスに含まれるユニークな単語の総数です。

2番目のパラメータは、各単語ベクトルの次元数です。

例えば、各単語ベクトルを32次元にしたい場合は、2番目のパラメータに32を指定します。

そして最後に、第3のパラメータは入力文の長さである。

単語埋め込みの出力は、単語が行に、対応する次元が列に表現された2次元ベクトルである。

最後に、単語埋め込み層と高密度接続層を直接接続したい場合は、まず2Dの単語埋め込みを1Dに平坦化する必要があります。

これらの概念は、実際に単語の埋め込みを見れば、より理解できるようになるでしょう。

カスタムワードエンベッディング

先ほど、Kerasはカスタムの単語埋め込みを学習するために使うことも、事前に学習した単語埋め込みを読み込むために使うこともできると言いました。

ここでは、KerasのEmbedding Layerを使って、どのようにカスタムの単語埋め込みを学習することができるかを見ていきます。

ここでは、単語埋め込みを使用する簡単なテキスト分類タスクを実行します。

以下のスクリプトを実行し、必要なライブラリをダウンロードします。

from numpy import array
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.embeddings import Embedding


次に、データセットを定義する必要があります。

今回は、映画のレビューを含む非常にシンプルなカスタムデータセットを使用します。

以下のスクリプトでデータセットを作成します。

corpus = [
    # Positive Reviews


'This is an excellent movie',
    'The move was fantastic I like it',
    'You should watch it is brilliant',
    'Exceptionally good',
    'Wonderfully directed and executed I like it',
    'Its a fantastic series',
    'Never watched such a brillent movie',
    'It is a Wonderful movie',


# Negtive Reviews


"horrible acting",
    'waste of money',
    'pathetic picture',
    'It was very boring',
    'I did not like the movie',
    'The movie was horrible',
    'I will not recommend',
    'The acting is pathetic'
]


このコーパスには、8つの肯定的なレビューと8つの否定的なレビューが含まれています。

次に、ラベルセットを作成する。

sentiments = array([1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0])


sentiment配列の最初の8項目は1を含み、肯定的な感情に対応していることがわかります。

最後の8つの項目は0であり、否定的な感情に対応します。

先ほど、Embedding() 層の最初のパラメータは語彙、つまりコーパスに含まれるユニークな単語の数であると述べましたが、この語彙の総数を求めてみましょう。

まず、コーパスに含まれる単語の総数を求めてみよう。

from nltk.tokenize import word_tokenize


all_words = []
for sent in corpus:
    tokenize_word = word_tokenize(sent)
    for word in tokenize_word:
        all_words.append(word)


上のスクリプトでは、コーパスに含まれる各文を繰り返し処理し、トークン化して単語にする。

次に、すべての単語のリストを繰り返し、その単語を all_words リストに追加する。

上記のスクリプトを実行すると、all_wordsの辞書に全ての単語が登録される。

しかし、重複している単語は要らない。

以下のように、set関数にリストを渡すことで、ユニークな単語をすべて取り出すことができる。

unique_words = set(all_words)
print(len(unique_words))


出力には “45 “と表示されるが、これはこのコーパスに含まれるユニークな単語の数である。

ここで、語彙のサイズに5個のバッファを追加し、vocab_length の値を50に設定する。

埋め込み層は単語が数値形式であることを期待する。

そのため、コーパスに含まれる文章を数値に変換する必要がある。

テキストを数値に変換する一つの方法として、 keras.preprocessing.text ライブラリの one_hot 関数を使用する方法があります。

この関数は、文と語彙の長さを受け取り、文を数値で返す。

embedded_sentences = [one_hot(sent, vocab_length) for sent in corpus]
print(embedded_sentences )


上のスクリプトでは、コーパスに含まれるすべての文を数値に変換し、コンソールに表示している。

出力は以下のようなものである。

[[31, 12, 31, 14, 9], [20, 3, 20, 16, 18, 45, 14], [16, 26, 29, 14, 12, 1], [16, 23], [32, 41, 13, 20, 18, 45, 14], [15, 28, 16, 43], [7, 9, 31, 28, 31, 9], [14, 12, 28, 46, 9], [4, 22], [5, 4, 9], [23, 46], [14, 20, 32, 14], [18, 1, 26, 45, 20, 9], [20, 9, 20, 4], [18, 8, 26, 34], [20, 22, 12, 23]]


最初の文には5つの単語が含まれているので、最初のリスト項目には5つの整数が含まれていることがわかる。

また、最初の文の最後の単語は、最初のリスト項目の “movie “であり、結果の2次元配列の5番目の場所に9という数字があることに注意してください。

埋め込み層は、文が同じ大きさであることを期待しています。

しかし、私たちが符号化した文の大きさはバラバラです。

すべての文を同じ大きさにする一つの方法は、すべての文の長さを長くして、最大の文の長さと同じにすることです。

まず、コーパスの中で最大の文を見つけ、すべての文の長さをその最大の文の長さにすることにしよう。

そのために、以下のスクリプトを実行する。

word_count = lambda sentence: len(word_tokenize(sentence))
longest_sentence = max(corpus, key=word_count)
length_long_sentence = len(word_tokenize(longest_sentence))


上の文では、ラムダ式を使ってすべての文の長さを求めている。

次に、max関数を使って、最も長い文を返す。

最後に、最長の文を単語にトークン化し、len関数を用いて単語の数を数える。

次に、すべての文が同じサイズになるように、文の長さを増やした結果作成される空のインデックスにゼロを追加します。

文の末尾にゼロを追加するには、 pad_sequences メソッドを使用します。

最初のパラメータは不等間隔でエンコードされた文のリスト、2番目のパラメータは最長文のサイズまたはパディングインデックス、最後のパラメータは padding で、文末にパディングを追加する場合は post を指定する。

以下のスクリプトを実行する。

padded_sentences = pad_sequences(embedded_sentences, length_long_sentence, padding='post')
print(padded_sentences)


出力には、パディングされた文が表示されるはずである。

[[31 12 31 14  9  0  0]
 [20  3 20 16 18 45 14]
 [16 26 29 14 12  1  0]
 [16 23  0  0  0  0  0]
 [32 41 13 20 18 45 14]
 [15 28 16 43  0  0  0]
 [ 7  9 31 28 31  9  0]
 [14 12 28 46  9  0  0]
 [ 4 22  0  0  0  0  0]
 [ 5  4  9  0  0  0  0]
 [23 46  0  0  0  0  0]
 [14 20 32 14  0  0  0]
 [18  1 26 45 20  9  0]
 [20  9 20  4  0  0  0]
 [18  8 26 34  0  0  0]
 [20 22 12 23  0  0  0]]


パディングされた文の末尾にゼロが見えるでしょう。

これで、単語埋め込みを使った感情分類モデルを作るのに必要なものが揃いました。

ここでは、埋め込み層と隠れ層のない非常にシンプルなテキスト分類モデルを作成します。

次のスクリプトを見てください。

model = Sequential()
model.add(Embedding(vocab_length, 20, input_length=length_long_sentence))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))


上のスクリプトでは、Sequentialモデルを作成し、Embedding層をモデルの最初の層として追加しています。

語彙の長さは vocab_length パラメータで指定する。

各単語ベクトルの次元は20、input_lengthは最長文の長さである7とする。

次に、Embedding層は密結合層と直接利用できるように平坦化される。

二値分類問題なので、密な層での損失関数として sigmoid 関数を用いる。

次に、以下のようにモデルをコンパイルして、モデルの概要を出力します。

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
print(model.summary())


モデルの要約は以下の通りである。

Layer (type)                 Output Shape              Param #=================================================================
embedding_1 (Embedding)      (None, 7, 20)             1000
_________________________________________________________________
flatten_1 (Flatten)          (None, 140)               0
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 141
=================================================================
Total params: 1,141
Trainable params: 1,141
Non-trainable params: 0


第1層は1000の学習可能なパラメータを持っていることがわかります。

前処理された単語の埋め込みを読み込む

前節では、カスタムの単語埋め込みを学習させました。

しかし、事前に学習させた単語埋め込みを利用することもできます。

事前学習済みの単語埋め込みはいくつかありますが、ここでは最も有名でよく使われているStanford NLPのGloVe単語埋め込みを使用します。

GloVeの単語埋め込みはこのリンクからダウンロードできます。

一番小さいファイルは “Glove.6B.zip “という名前です。

ファイルサイズは822MBです。

このファイルには、400k語の50次元、100次元、200次元、300次元の単語ベクトルが含まれています。

今回は100次元のベクトルを使用する。

手順はよく似ている。

まず、必要なライブラリをインポートする必要があります。

model.fit(padded_sentences, sentiments, epochs=100, verbose=1)


次に、コーパスを作成し、ラベルを作成する。

loss, accuracy = model.evaluate(padded_sentences, sentiments, verbose=0)
print('Accuracy: %f' % (accuracy*100))


from numpy import array
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.embeddings import Embedding


前節では、テキストをベクトルに変換するために one_hot 関数を使いました。

もう一つの方法は、 keras.preprocessing.text ライブラリにある Tokenizer 関数を使うことです。

この場合、コーパスを Tokenizerfit_on_text メソッドに渡せばよい。

corpus = [
    # Positive Reviews


'This is an excellent movie',
    'The move was fantastic I like it',
    'You should watch it is brilliant',
    'Exceptionally good',
    'Wonderfully directed and executed I like it',
    'Its a fantastic series',
    'Never watched such a brillent movie',
    'It is a Wonderful movie',


# Negtive Reviews


"horrible acting",
    'waste of money',
    'pathetic picture',
    'It was very boring',
    'I did not like the movie',
    'The movie was horrible',
    'I will not recommend',
    'The acting is pathetic'
]


テキストに含まれるユニークな単語の数は、 word_tokenizer オブジェクトの word_index 辞書の長さを数えることで得ることができます。

このとき、語彙のサイズに 1 を加えることを忘れないように。

これは、事前に学習した単語埋め込みが存在しない単語の次元を格納するためである。

sentiments = array([1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0])


最後に、文章を数値に変換するために、 texts_to_sequences 関数を呼び出して、コーパス全体を渡します。

word_tokenizer = Tokenizer()
word_tokenizer.fit_on_texts(corpus)


出力では、文章が数値化された形で表示される。

vocab_length = len(word_tokenizer.word_index) + 1


次のステップは、最も長い文の単語数を求め、最も長い文の長さより短い文にパディングを適用することである。

embedded_sentences = word_tokenizer.texts_to_sequences(corpus)
print(embedded_sentences)


水増しされた文は次のようになる。

[[14, 3, 15, 16, 1], [4, 17, 6, 9, 5, 7, 2], [18, 19, 20, 2, 3, 21], [22, 23], [24, 25, 26, 27, 5, 7, 2], [28, 8, 9, 29], [30, 31, 32, 8, 33, 1], [2, 3, 8, 34, 1], [10, 11], [35, 36, 37], [12, 38], [2, 6, 39, 40], [5, 41, 13, 7, 4, 1], [4, 1, 6, 10], [5, 42, 13, 43], [4, 11, 3, 12]]


これで、文が数字の羅列に変換された。

次に、GloVeの単語埋め込みをロードし、コーパスの単語とGloVeの埋め込み値からなる埋め込み行列を作成します。

以下のスクリプトを実行します。

from nltk.tokenize import word_tokenize


word_count = lambda sentence: len(word_tokenize(sentence))
longest_sentence = max(corpus, key=word_count)
length_long_sentence = len(word_tokenize(longest_sentence))


padded_sentences = pad_sequences(embedded_sentences, length_long_sentence, padding='post')


print(padded_sentences)


上のスクリプトでは、GloVeの埋め込みをロードするだけでなく、いくつかのライブラリもインポートしています。

これらのライブラリの使い方は次節で説明します。

ここで、「glove.6B.100d.txt」ファイルを読み込んでいることに注目してください。

このファイルには100次元の単語埋め込みが含まれています。

また、単語埋め込みを格納するための空の辞書も作成しました。

このファイルを開くと、各行の最初に単語があり、その後に100個の数字が並んでいることがわかります。

この数字は、各行の先頭にある単語の100次元のベクトルを形成しています。

ここでは、単語をキー、それに対応する100次元ベクトルを値とする辞書を、配列形式で作成する。

以下のスクリプトを実行する。

[[14  3 15 16  1  0  0]
 [ 4 17  6  9  5  7  2]
 [18 19 20  2  3 21  0]
 [22 23  0  0  0  0  0]
 [24 25 26 27  5  7  2]
 [28  8  9 29  0  0  0]
 [30 31 32  8 33  1  0]
 [ 2  3  8 34  1  0  0]
 [10 11  0  0  0  0  0]
 [35 36 37  0  0  0  0]
 [12 38  0  0  0  0  0]
 [ 2  6 39 40  0  0  0]
 [ 5 41 13  7  4  1  0]
 [ 4  1  6 10  0  0  0]
 [ 5 42 13 43  0  0  0]
 [ 4 11  3 12  0  0  0]]


辞書 embeddings_dictionary には、単語とそれに対応するGloVeの埋め込み値が格納されています。

ここで、コーパスに存在する単語だけの埋め込みが欲しい。

44(語彙のサイズ)行、100列の2次元のnumpy配列を作成します。

配列の初期値は0である。

配列の名前は embedding_matrix とする。

次に、単語とそれに対応するインデックスを含む word_tokenizer.word_index 辞書を走査して、コーパスの各単語を繰り返し処理する。

各単語は embedding_dictionary にキーとして渡され、その単語に対応する100次元のベクトルを取得する。

そして、その100次元ベクトルは embedding_matrix の中の対応する単語のインデックスに格納されます。

以下のスクリプトを見てほしい。

from numpy import array
from numpy import asarray
from numpy import zeros


embeddings_dictionary = dict()
glove_file = open('E:/Datasets/Word Embeddings/glove.6B.100d.txt', encoding="utf8")


これで embedding_matrix にはコーパスに含まれる単語に対する事前学習された埋め込みが格納されました。

これで逐次モデルを作成する準備が整いました。

次のスクリプトを見てください。

for line in glove_file:
    records = line.split()
    word = records[0]
    vector_dimensions = asarray(records[1:], dtype='float32')
    embeddings_dictionary [word] = vector_dimensions


glove_file.close()


スクリプトは埋め込み層を除いて同じです。

ここで、埋め込み層では、第一パラメータとして、空孔の大きさを指定します。

第二パラメータは、出力ベクトルの次元です。

ここでは、100次元のベクトルを含む学習済みの単語埋め込みを使うので、ベクトル次元を100に設定しています。

Embedding()層のもう一つの重要な属性は、前節で使わなかった weights です。

この weights パラメータには、事前に学習させた埋め込み行列をデフォルトの重みとして渡すことができます。

また、今回は埋め込み層を学習しないので、 trainable 属性は False に設定されています。

それでは、モデルをコンパイルして、その結果を見てみましょう。

embedding_matrix = zeros((vocab_length, 100))
for word, index in word_tokenizer.word_index.items():
    embedding_vector = embeddings_dictionary.get(word)
    if embedding_vector is not None:
        embedding_matrix[index] = embedding_vector


損失を最小化するためのオプティマイザとして、今回も adam を使用します。

使用する損失関数は binary_crossentropy です。

また、結果を精度という形で見たいので、metrics 属性の値として acc が渡されます。

モデルの概要は以下の通りです。

model = Sequential()
embedding_layer = Embedding(vocab_length, 100, weights=[embedding_matrix], input_length=length_long_sentence, trainable=False)
model.add(embedding_layer)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))


語彙が44個あり、各単語は100次元のベクトルで表現されるので、埋め込み層のパラメータ数は「44 x 100 = 4400」であることがわかります。

埋め込み層の出力は、7行(文中の各単語に1行)、100列の2次元ベクトルとなります。

Kerasの関数型APIによる単語埋め込み機能

前節では、Kerasの逐次APIで単語埋め込みを利用する方法を見ました。

逐次APIは深層学習モデルを素早く作成できるため、初心者には良いスタート地点ですが、Keras Functional APIの仕組みを知っておくことは非常に重要です。

複数の入出力を伴う高度な深層学習モデルのほとんどは、Functional APIを使用します。

このセクションでは、Keras Functional APIを使用してエンベッディングレイヤーを実装する方法を見ていきます。

スクリプトの残りの部分は、最後のセクションと同様に残ります。

唯一の変更は、ディープラーニングモデルの開発でしょう。

前節で実装したのと同じディープラーニングモデルをKeras Functional APIで実装してみましょう。

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
print(model.summary())


Keras Functional APIでは、エンベッディング層の前に入力層を別途定義する必要があります。

入力層では、単純に入力ベクトルの長さを渡します。

その前のレイヤーを次のレイヤーの入力として指定するには、次のレイヤーの最後に、前のレイヤーを括弧内のパラメータとして渡します。

例えば、上のスクリプトでは、埋め込み層の最後に deep_inputs がパラメータとして渡されているのがわかる。

同様に、embeddingFlatten() レイヤーの最後で入力として渡され、以下同様です。

最後に、Model() の中で、入力層と、最終的な出力層を渡す必要があります。

では、モデルをコンパイルして、その概要を見てみましょう。

Layer (type)                 Output Shape              Param #=================================================================
embedding_1 (Embedding)      (None, 7, 100)            4400
_________________________________________________________________
flatten_1 (Flatten)          (None, 700)               0
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 701
=================================================================
Total params: 5,101
Trainable params: 701
Non-trainable params: 4,400
_________________________________________________________________


出力はこのようになります。

model.fit(padded_sentences, sentiments, epochs=100, verbose=1)


モデルの概要では、入力層が埋め込み層の前に独立した層としてあるのがわかります。

残りのモデルは同じままです。

最後に、モデルの適合と評価のプロセスは、Sequential APIで使われたものと同じです。

loss, accuracy = model.evaluate(padded_sentences, sentiments, verbose=0)
print('Accuracy: %f' % (accuracy*100))


出力では、精度が1.000、つまり100%であることがわかります。

結論

テキストデータをディープラーニングモデルの入力として利用するためには、テキストを数値に変換する必要があります。

しかし、機械学習モデルと異なり、巨大なサイズのスパースベクトルを渡すと、ディープラーニングモデルに大きな影響を与える。

そのため、テキストを小さな密なベクトルに変換する必要がある。

単語埋め込みは、テキストを密なベクトルに変換するのに役立ちます。

この記事では、Kerasディープラーニングライブラリでどのようにワードエンベッディングを実装できるかを見てきました。

我々は、簡単な分類タスクを解決するために、事前学習された単語埋め込みを使用するだけでなく、カスタムの単語埋め込みを実装しました。

最後に、KerasのFunctional APIを使って単語埋め込みを実装する方法も見てきました。

タイトルとURLをコピーしました