Python for NLPの連載は今回で22回目です。
以前、Kerasでシーケンス問題を解くという記事で、入力と出力の両方が複数のタイムステップに分かれている多対多のシーケンス問題を解く方法について説明しました。
seq2seqアーキテクチャは多対多のシーケンスモデリングの一種で、Text-Summarization、チャットボット開発、会話モデリング、ニューラル機械翻訳など、様々なタスクでよく利用されています。
今回は、ニューラル機械翻訳の応用として非常に有名な言語翻訳モデルの作り方を見ていきましょう。
PythonのKerasライブラリを使って、seq2seqアーキテクチャで言語翻訳モデルを作成します。
リカレントニューラルネットワーク、特にLSTMの知識があることが前提です。
この記事のコードは、PythonでKerasライブラリを用いて書かれています。
したがって、Kerasライブラリだけでなく、Python言語に関する十分な知識を持っていることが前提です。
それでは、さっそく始めましょう。
ライブラリおよびコンフィギュレーション設定
最初のステップとして、必要なライブラリをインポートし、コード内で使用する様々なパラメータの値を設定します。
まず、必要なライブラリをインポートします。
import os, sys
from keras.models import Model
from keras.layers import Input, LSTM, GRU, Dense, Embedding
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
以下のスクリプトを実行し、各パラメータの値を設定します。
BATCH_SIZE = 64
EPOCHS = 20
LSTM_NODES =256
NUM_SENTENCES = 20000
MAX_SENTENCE_LENGTH = 50
MAX_NUM_WORDS = 20000
EMBEDDING_SIZE = 100
データセット
この記事で開発する言語翻訳モデルは、英語の文章をフランス語の文章に翻訳するものです。
このようなモデルを開発するためには、英語の文章とそのフランス語の翻訳を含むデータセットが必要です。
幸いなことに、そのようなデータセットは以下のリンクから自由に利用することができる。
fra-eng.zipをダウンロードし、解凍してください。
すると、fra.txtというファイルが現れる。
各行には、英語の文章とそのフランス語訳がタブで区切られています。
fra.txtファイルの最初の20行は次のような感じです。
Go. Va !
Hi. Salut !
Hi. Salut.
Run! Cours !
Run! Courez !
Who? Qui ?
Wow! Ça alors !
Fire! Au feu !
Help! À l'aide !
Jump. Saute.
Stop! Ça suffit !
Stop! Stop !
Stop! Arrête-toi !
Wait! Attends !
Wait! Attendez !
Go on. Poursuis.
Go on. Continuez.
Go on. Poursuivez.
Hello! Bonjour !
Hello! Salut !
このモデルには170,000以上のレコードが含まれているが、ここでは最初の20,000レコードのみを用いてモデルを学習する。
必要であれば、もっと多くのレコードを使用することができます。
データ前処理
ニューラル機械翻訳モデルは、しばしばseq2seqアーキテクチャをベースにしている。
seq2seqアーキテクチャはエンコーダ・デコーダのアーキテクチャであり、エンコーダLSTMとデコーダLSTMという2つのLSTMネットワークから構成される。
エンコーダLSTMへの入力は原語の文、デコーダLSTMへの入力は文頭トークンを持つ翻訳語の文である。
出力は文末トークンを持つ実際のターゲット文である。
今回のデータセットでは、入力を処理する必要はないが、翻訳文のコピーを2つ生成する必要がある:1つは文頭トークン、もう1つは文末トークンである。
以下は、そのためのスクリプトである。
input_sentences = []
output_sentences = []
output_sentences_inputs = []
count = 0
for line in open(r'/content/drive/My Drive/datasets/fra.txt', encoding="utf-8"):
count += 1
if count > NUM_SENTENCES:
break
if ' ' not in line:
continue
input_sentence, output = line.rstrip().split(' ')
output_sentence = output + ' <eos'
output_sentence_input = '<sos ' + output
input_sentences.append(input_sentence)
output_sentences.append(output_sentence)
output_sentences_inputs.append(output_sentence_input)
print("num samples input:", len(input_sentences))
print("num samples output:", len(output_sentences))
print("num samples output input:", len(output_sentences_inputs))
注意:このスクリプトを実行するためには、あなたのコンピュータの fra.txt
ファイルのパスを変更する必要があるかもしれません。
上のスクリプトでは、3つのリスト input_sentences[]
、output_sentences[]
、output_sentences_inputs[]
を作成します。
次に、for
ループで fra.txt
ファイルを一行ずつ読み込んでいきます。
各行は、タブが発生した位置で2つの部分文字列に分割される。
左側の部分文字列(英文)は input_sentences[]
のリストに挿入される。
タブの右側の部分文字列は、対応する翻訳されたフランス語の文章です。
文の終わりを示す <eos
トークンが翻訳された文の前に置かれ、結果としての文が output_sentences[]
リストに追加されます。
同様に、「文の始まり」を表す <sos
トークンを翻訳された文の先頭に連結し、その結果を output_sentences_inputs[]
リストに追加する。
リストに追加された文の数が変数 NUM_SENTENCES
よりも大きい場合、つまり20,000の場合、ループは終了する。
最後に、3つのリストのサンプル数が出力に表示されます。
num samples input: 20000
num samples output: 20000
num samples output input: 20000
それでは、input_sentences[]
、output_sentences[]
、output_sentences_inputs[]
の各リストからランダムに文章を出力してみましょう。
print(input_sentences[172])
print(output_sentences[172])
print(output_sentences_inputs[172])
以下はその出力です。
I'm ill.
Je suis malade. <eos
<sos Je suis malade.
元の文は I'm ill
で、出力にはそれに対応する翻訳、すなわち Je suis malade' が表示されます。
<eos です。
ここでは、文末に「」というトークンがあることに注意してください。
同様に、デコーダへの入力は、 Je suis malade.`となる。
トークン化とパディング
次のステップは、原文と訳文をトークン化し、ある長さより長いか短い文にパディングを適用することである。
また、出力の場合は、出力中の最も長い文の長さになる。
トークン化には、 keras.preprocessing.text
ライブラリの Tokenizer
クラスを利用することができる。
トーケナイザーは2つのタスクを実行します。
- 文章を対応する単語のリストに分割します。
- 次に、単語を整数に変換します。
これは、深層学習や機械学習のアルゴリズムが数字を扱うため、非常に重要です。
以下のスクリプトは入力された文章をトークン化するために使用されます。
input_tokenizer = Tokenizer(num_words=MAX_NUM_WORDS)
input_tokenizer.fit_on_texts(input_sentences)
input_integer_seq = input_tokenizer.texts_to_sequences(input_sentences)
word2idx_inputs = input_tokenizer.word_index
print('Total unique words in the input: %s' % len(word2idx_inputs))
max_input_len = max(len(sen) for sen in input_integer_seq)
print("Length of longest sentence in input: %g" % max_input_len)
トークン化と整数変換に加えて、Tokenizer
クラスの word_index
属性は、単語をキー、対応する整数を値とする word-to-index 辞書を返す。
上のスクリプトでは、辞書に含まれるユニークな単語の数と、入力の中で最も長い文の長さも表示される。
Total unique words in the input: 3523
Length of longest sentence in input: 6
同様に、出力文も以下のようにトークン化することができる。
output_tokenizer = Tokenizer(num_words=MAX_NUM_WORDS, filters='')
output_tokenizer.fit_on_texts(output_sentences + output_sentences_inputs)
output_integer_seq = output_tokenizer.texts_to_sequences(output_sentences)
output_input_integer_seq = output_tokenizer.texts_to_sequences(output_sentences_inputs)
word2idx_outputs = output_tokenizer.word_index
print('Total unique words in the output: %s' % len(word2idx_outputs))
num_words_output = len(word2idx_outputs) + 1
max_out_len = max(len(sen) for sen in output_integer_seq)
print("Length of longest sentence in the output: %g" % max_out_len)
以下はその出力である。
Total unique words in the output: 9561
Length of longest sentence in the output: 13
入力と出力の固有単語数の比較から、翻訳されたフランス語の文に比べて、英語の文は通常短く、平均してより少ない単語数を含むと結論付けることができる。
次に、入力のパディングが必要である。
入力と出力を詰める理由は、テキストの文の長さは様々ですが、LSTM(モデルを学習させるアルゴリズム)は同じ長さの入力インスタンスを想定しているからです。
そのため、文章を固定長のベクトルに変換する必要があります。
そのための一つの方法がパディングである。
パディングでは、文に一定の長さを定義する。
この例では、入力文と出力文のそれぞれで最も長い文の長さがパディングに使われる。
入力文の最長の文は6語である。
6語未満の文には、空のインデックスにゼロが追加される。
以下のスクリプトは入力文にパディングを施す。
encoder_input_sequences = pad_sequences(input_integer_seq, maxlen=max_input_len)
print("encoder_input_sequences.shape:", encoder_input_sequences.shape)
print("encoder_input_sequences[172]:", encoder_input_sequences[172])
上のスクリプトはパディングされた入力文の形状を表示する。
インデックス172の文に対するパディングされた整数列も表示される。
以下はその出力である。
encoder_input_sequences.shape: (20000, 6)
encoder_input_sequences[172]: [ 0 0 0 0 6 539]
入力文は20000個あり、各入力文の長さは6であるから、入力の形状は (20000, 6) となる。
入力文のインデックス172の文の整数列を見ると、0が3つあり、その後に6と539という値が続いていることがわかる。
インデックス172の元の文はI'm ill
であることを思い出すかもしれません。
トークナイザーはこの文を I’mと
ill` という二つの単語に分割して整数に変換し、入力リストのインデックス172にある文に対応する整数列の先頭に三つのゼロを追加してプリパディングを適用しています。
i’mと
illの整数値がそれぞれ 6 と 539 であることを確認するために、以下のように
word2index_inputs` 辞書に単語を渡すことができる。
print(word2idx_inputs["i'm"])
print(word2idx_inputs["ill"])
出力する。
6
539
同様に、デコーダの出力とデコーダの入力は以下のようにパディングされる。
decoder_input_sequences = pad_sequences(output_input_integer_seq, maxlen=max_out_len, padding='post')
print("decoder_input_sequences.shape:", decoder_input_sequences.shape)
print("decoder_input_sequences[172]:", decoder_input_sequences[172])
出力
decoder_input_sequences.shape: (20000, 13)
decoder_input_sequences[172]: [ 2 3 6 188 0 0 0 0 0 0 0 0 0]
デコーダ入力のインデックス172にある文は <sos je suis malade.
である。
word2idx_outputs` 辞書から対応する整数を出力すると、以下のようにコンソールに 2, 3, 6, 188 と表示されるはずである。
print(word2idx_outputs["<sos"])
print(word2idx_outputs["je"])
print(word2idx_outputs["suis"])
print(word2idx_outputs["malade."])
出力されます。
2
3
6
188
さらに重要なことは、デコーダの場合、ポストパディングが適用され、文末にゼロが付加されていることである。
エンコーダでは、文頭にゼロを付加している。
これは、エンコーダの出力は文末に出現する単語に基づいているため、元の単語を文末に残し、文頭にゼロをパディングしたためです。
一方、デコーダーの場合は、文の先頭から処理を開始するため、デコーダーの入出力に対してポストパディングを行う。
ワードエンベッディング
単語埋め込みについては、Kerasの単語埋め込みを理解するために詳しい記事を書きましたので、そちらをご覧ください。
ここでは、ニューラル機械翻訳のための単語埋め込みの実装のみを説明します。
しかし、基本的な考え方は変わりません。
我々は深層学習モデルを使用しており、深層学習モデルは数値で動作するため、我々は単語を対応する数値ベクトル表現に変換する必要があります。
しかし、すでに単語は整数に変換しています。
整数表現と単語埋め込みの違いは、大きく分けて2つあります。
整数表現では、単語は1つの整数だけで表現されます。
ベクトル表現では、単語は50、100、200など好きな次元のベクトルで表現されます。
従って、単語埋め込みは単語の情報をより多く取り込むことができます。
第二に、単一整数表現では、異なる単語間の関係を捉えることができません。
逆に、単語埋め込みは単語間の関係を保持します。
単語埋め込みを利用するには、カスタム単語埋め込みを利用することもできますし、事前に学習させた単語埋め込みを利用することもできます。
今回は、入力である英語の文章には、GloVeの単語埋め込みを使うことにします。
出力である翻訳されたフランス語の文には、カスタムの単語埋め込みを使います。
まず、入力用の単語埋め込みを作成しましょう。
まず、GloVeの単語ベクトルをメモリにロードします。
そして、以下のように、単語をキー、対応するベクトルを値とする辞書を作成します。
from numpy import array
from numpy import asarray
from numpy import zeros
embeddings_dictionary = dict()
glove_file = open(r'/content/drive/My Drive/datasets/glove.6B.100d.txt', encoding="utf8")
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()
ここで、入力された単語は3523個である。
行番号は単語の整数値を表し、列は単語の次元に対応する行列を作成する。
この行列は入力文の単語を埋め込むものです。
num_words = min(MAX_NUM_WORDS, len(word2idx_inputs) + 1)
embedding_matrix = zeros((num_words, EMBEDDING_SIZE))
for word, index in word2idx_inputs.items():
embedding_vector = embeddings_dictionary.get(word)
if embedding_vector is not None:
embedding_matrix[index] = embedding_vector
まず、GloVeの単語埋め込み辞書を使って、単語ill
の単語埋め込みを表示してみましょう。
print(embeddings_dictionary["ill"])
出力してみましょう。
[ 0.12648 0.1366 0.22192 -0.025204 -0.7197 0.66147
0.48509 0.057223 0.13829 -0.26375 -0.23647 0.74349
0.46737 -0.462 0.20031 -0.26302 0.093948 -0.61756
-0.28213 0.1353 0.28213 0.21813 0.16418 0.22547
-0.98945 0.29624 -0.62476 -0.29535 0.21534 0.92274
0.38388 0.55744 -0.14628 -0.15674 -0.51941 0.25629
-0.0079678 0.12998 -0.029192 0.20868 -0.55127 0.075353
0.44746 -0.71046 0.75562 0.010378 0.095229 0.16673
0.22073 -0.46562 -0.10199 -0.80386 0.45162 0.45183
0.19869 -1.6571 0.7584 -0.40298 0.82426 -0.386
0.0039546 0.61318 0.02701 -0.3308 -0.095652 -0.082164
0.7858 0.13394 -0.32715 -0.31371 -0.20247 -0.73001
-0.49343 0.56445 0.61038 0.36777 -0.070182 0.44859
-0.61774 -0.18849 0.65592 0.44797 -0.10469 0.62512
-1.9474 -0.60622 0.073874 0.50013 -1.1278 -0.42066
-0.37322 -0.50538 0.59171 0.46534 -0.42482 0.83265
0.081548 -0.44147 -0.084311 -1.2304 ]
前節で、単語ill
の整数表現は539であることがわかりました。
ここで、単語埋め込み行列の539番目のインデックスを確認してみましょう。
print(embedding_matrix[539])
を出力する。
[ 0.12648 0.1366 0.22192 -0.025204 -0.7197 0.66147
0.48509 0.057223 0.13829 -0.26375 -0.23647 0.74349
0.46737 -0.462 0.20031 -0.26302 0.093948 -0.61756
-0.28213 0.1353 0.28213 0.21813 0.16418 0.22547
-0.98945 0.29624 -0.62476 -0.29535 0.21534 0.92274
0.38388 0.55744 -0.14628 -0.15674 -0.51941 0.25629
-0.0079678 0.12998 -0.029192 0.20868 -0.55127 0.075353
0.44746 -0.71046 0.75562 0.010378 0.095229 0.16673
0.22073 -0.46562 -0.10199 -0.80386 0.45162 0.45183
0.19869 -1.6571 0.7584 -0.40298 0.82426 -0.386
0.0039546 0.61318 0.02701 -0.3308 -0.095652 -0.082164
0.7858 0.13394 -0.32715 -0.31371 -0.20247 -0.73001
-0.49343 0.56445 0.61038 0.36777 -0.070182 0.44859
-0.61774 -0.18849 0.65592 0.44797 -0.10469 0.62512
-1.9474 -0.60622 0.073874 0.50013 -1.1278 -0.42066
-0.37322 -0.50538 0.59171 0.46534 -0.42482 0.83265
0.081548 -0.44147 -0.084311 -1.2304 ]
埋め込み行列の539行目の値が、GloVe辞書の単語ill
のベクトル表現に似ていることがわかります。
このことから、埋め込み行列の行はGloVe単語埋め込み辞書の対応する単語の埋め込みを表していることが確認できます。
この単語埋め込み行列を用いて、LSTMモデルの埋め込み層を作成する。
以下のスクリプトは、入力に対する埋め込み層を作成するものです。
embedding_layer = Embedding(num_words, EMBEDDING_SIZE, weights=[embedding_matrix], input_length=max_input_len)
モデルの作成
さて、いよいよモデルを作成します。
出力が単語の列であることが分かっているので、最初にしなければならないことは、出力を定義することです。
出力に含まれるユニークな単語の総数は9562であることを思い出してください。
したがって、出力の各単語は9562語のうちのどれでもよいことになる。
出力文の長さは13である。
そして、各入力文に対して、対応する出力文が必要である。
したがって、出力の最終的な形はこうなる。
(number of inputs, length of the output sentence, the number of words in the output)
次のスクリプトは空の出力配列を作成する。
decoder_targets_one_hot = np.zeros((
len(input_sentences),
max_out_len,
num_words_output
),
dtype='float32'
)
次のスクリプトはデコーダーの形状を表示する。
decoder_targets_one_hot.shape
出力する。
(20000, 13, 9562)
予測を行うために、モデルの最終層は密な層になる。
したがって、密な層でソフトマックス活性化関数を使うので、1ホットエンコードされたベクトルの形で出力が必要である。
このようなワンホットエンコーディングされた出力を作成するために、次のステップは、単語の整数表現に対応する列番号に1を代入することである。
例えば、<sos je suis malade
の整数表現は [ 2 3 6 188 0 0 0 0 0 0 ]
となる。
出力配列 decoder_targets_one_hot
には、1行目の2列目に1が挿入されます。
同様に、2行目の3番目のインデックスに、さらに1が挿入され、以下同様となります。
次のスクリプトを見てください。
for i, d in enumerate(decoder_output_sequences):
for t, word in enumerate(d):
decoder_targets_one_hot[i, t, word] = 1
次に、エンコーダーとデコーダーを作る必要がある。
エンコーダの入力は英語の文であり、出力はLSTMの隠れ状態とセルの状態である。
以下のスクリプトはエンコーダを定義する。
encoder_inputs_placeholder = Input(shape=(max_input_len,))
x = embedding_layer(encoder_inputs_placeholder)
encoder = LSTM(LSTM_NODES, return_state=True)
encoder_outputs, h, c = encoder(x)
encoder_states = [h, c]
次に、デコーダを定義する。
デコーダは2つの入力を持つ。
エンコーダからの隠れ状態とセル状態、そして入力文である。
以下のスクリプトはデコーダのLSTMを作成する。
decoder_inputs_placeholder = Input(shape=(max_out_len,))
decoder_embedding = Embedding(num_words_output, LSTM_NODES)
decoder_inputs_x = decoder_embedding(decoder_inputs_placeholder)
decoder_lstm = LSTM(LSTM_NODES, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs_x, initial_state=encoder_states)
最後に、デコーダLSTMの出力は密な層に渡され、デコーダの出力を予測する。
decoder_dense = Dense(num_words_output, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
次のステップは、モデルをコンパイルすることである。
model = Model([encoder_inputs_placeholder,
decoder_inputs_placeholder], decoder_outputs)
model.compile(
optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy']
)
このモデルがどのように見えるか、プロットしてみよう。
from keras.utils import plot_model
plot_model(model, to_file='model_plot4a.png', show_shapes=True, show_layer_names=True)
出力。
出力から、2種類の入力があることがわかるでしょう。
input_1はエンコーダの入力プレースホルダーで、これは
lstm_1層に埋め込まれて渡されます。
lstm_1 層からの出力は、出力、隠れ層、セルの状態の 3 つである。
しかし、デコーダにはセルの状態と隠れ層の状態のみが渡される。
ここで、lstm_2
層はデコーダのLSTMである。
input_2には文頭に
トークンを付加した出力文が格納される。
input_2 は埋め込み層にも渡され、デコーダLSTMである lstm_2
の入力として用いられる。
最後に、デコーダ LSTM の出力は密な層に渡され、予測を行う。
次のステップは fit()
メソッドを用いてモデルを学習することである。
r = model.fit(
[encoder_input_sequences, decoder_input_sequences],
decoder_targets_one_hot,
batch_size=BATCH_SIZE,
epochs=EPOCHS,
validation_split=0.1,
)
このモデルは 18,000 レコードで学習され、残りの 2,000 レコードでテストされる。
モデルは20エポック学習されますが、より良い結果が得られるかどうか、エポック数を変更することができます。
20エポック後、学習精度は90.99%、検証精度は79.11%となり、モデルがオーバーフィットしていることがわかります。
オーバーフィッティングを抑えるには、ドロップアウトを追加するか、レコード数を増やすとよいでしょう。
今回は20,0000レコードで学習しているので、オーバーフィットを減らすためにレコードを追加することができます。
予測のためのモデルの修正
学習中は、シーケンス中のすべての出力語について、デコーダへの実際の入力を知ることができる。
学習中に起こることの一例を以下に挙げる。
例えば、i'm ill
という文があるとする。
この文は次のように翻訳される。
// Inputs on the left of Encoder/Decoder, outputs on the right.
Step 1:
I'm ill -> Encoder -> enc(h1,c1)
enc(h1,c1) + <sos -> Decoder -> je + dec(h1,c1)
step 2:
enc(h1,c1) + je -> Decoder -> suis + dec(h2,c2)
step 3:
enc(h2,c2) + suis -> Decoder -> malade. + dec(h3,c3)
step 3:
enc(h3,c3) + malade. -> Decoder -> <eos + dec(h4,c4)
デコーダへの入力とデコーダからの出力は既知であり、これらの入力と出力を基にモデルが学習されることがわかる。
しかし、予測時には、前の単語を基に次の単語が予測され、その単語も前の時間ステップで予測される。
これで <sos
と <eos
トークンの目的が理解できたと思います。
実際の予測時には、出力されるすべての単語は利用できませんが、実はその単語を予測しなければなりません。
すべての出力文は <sos
で始まっているので、予測中に利用できる単語は <sos
だけです。
予測中に起こることの例として、次のようなものがある。
再びi'm ill
という文を翻訳してみましょう。
// Inputs on the left of Encoder/Decoder, outputs on the right.
Step 1:
I'm ill -> Encoder -> enc(h1,c1)
enc(h1,c1) + <sos -> Decoder -> y1(je) + dec(h1,c1)
step 2:
enc(h1,c1) + y1 -> Decoder -> y2(suis) + dec(h2,c2)
step 3:
enc(h2,c2) + y2 -> Decoder -> y3(malade.) + dec(h3,c3)
step 3:
enc(h3,c3) + y3 -> Decoder -> y4(<eos) + dec(h4,c4)
エンコーダの機能が同じであることがわかると思います。
原語の文はエンコーダと隠れ状態を通過し、セルの状態はエンコーダからの出力である。
ステップ1では、エンコーダの隠れ状態とセル状態、そして<sos
がデコーダの入力として使われる。
デコーダは単語 y1
を予測するが、これは正しいかもしれないし、正しくないかもしれない。
しかし、我々のモデルに従って、正しい予測の確率は0.7911である。
ステップ 2 では、ステップ 1 のデコーダの隠れ状態とセルの状態が、y1と共にデコーダの入力として使われ、デコーダは y2
を予測する。
このプロセスは <eos
トークンに遭遇するまで続けられる。
デコーダから予測された出力はすべて連結され、最終的な出力文となる。
このロジックを実現するために、我々のモデルを変更してみよう。
エンコーダのモデルはそのままである。
encoder_model = Model(encoder_inputs_placeholder, encoder_states)
各ステップでデコーダの隠れ状態とセルの状態が必要なので、以下のようにモデルを修正して隠れ状態とセルの状態を受け入れるようにする。
decoder_state_input_h = Input(shape=(LSTM_NODES,))
decoder_state_input_c = Input(shape=(LSTM_NODES,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
各ステップにおいて、デコーダーの入力は一単語だけなので、デコーダーの埋め込み層を次のように修正する。
decoder_inputs_single = Input(shape=(1,))
decoder_inputs_single_x = decoder_embedding(decoder_inputs_single)
次に、デコーダ出力のプレースホルダーを作成する必要がある。
decoder_outputs, h, c = decoder_lstm(decoder_inputs_single_x, initial_state=decoder_states_inputs)
予測を行うために、デコーダ出力は密な層に通される。
decoder_states = [h, c]
decoder_outputs = decoder_dense(decoder_outputs)
最後のステップは、更新されたデコーダ・モデルを定義することで、次のようになる。
decoder_model = Model(
[decoder_inputs_single] + decoder_states_inputs,
[decoder_outputs] + decoder_states
)
ここで、予測を行う修正されたデコーダLSTMをプロットしてみよう。
from keras.utils import plot_model
plot_model(decoder_model, to_file='model_plot_dec.png', show_shapes=True, show_layer_names=True)
出力
上の画像で lstm_2
は修正されたデコーダLSTMである。
input_5に示されるように1つの単語を含む文と、前の出力(
input_3と
input_4)のhiddenとセルの状態を受け取っていることがわかる。
入力文の形状は(none,1)となり、デコーダの入力は1単語だけであることが分かる。
逆に、学習時には入力文の形状は(None,6)` であった。
なぜなら、入力文には最大長 6 の完全な文が含まれていたからである。
予想すること
このステップでは、英語の文章を入力として、予測を行う方法を説明します。
トークン化のステップでは、単語を整数に変換した。
デコーダからの出力も整数になる。
しかし、出力はフランス語の単語の並びであることが望ましい。
そのためには、整数を単語に戻す必要がある。
入力と出力の両方に新しい辞書を作り、キーは整数、対応する値は単語とする。
idx2word_input = {v:k for k, v in word2idx_inputs.items()}
idx2word_target = {v:k for k, v in word2idx_outputs.items()}
次に、 translate_sentence()
というメソッドを作成します。
このメソッドは、入力された一連の英文(整数型)を受け取り、翻訳された仏文を返します。
translate_sentence()` メソッドを見てみましょう。
def translate_sentence(input_seq):
states_value = encoder_model.predict(input_seq)
target_seq = np.zeros((1, 1))
target_seq[0, 0] = word2idx_outputs['<sos']
eos = word2idx_outputs['<eos']
output_sentence = []
for _ in range(max_out_len):
output_tokens, h, c = decoder_model.predict([target_seq] + states_value)
idx = np.argmax(output_tokens[0, 0, :])
if eos == idx:
break
word = ''
if idx > 0:
word = idx2word_target[idx]
output_sentence.append(word)
target_seq[0, 0] = idx
states_value = [h, c]
return ' '.join(output_sentence)
上のスクリプトでは、入力配列を encoder_model
に渡しています。
このモデルにより、隠された状態とセルの状態が予測され、変数 states_value
に格納されます。
次に、変数 target_seq
を定義する。
この変数は 1 x 1
のすべてゼロの行列である。
変数 target_seq
には、デコーダモデルへの最初のワードである <sos
が格納されます。
その後、eos
変数が初期化され、<eos
トークンに対する整数値が格納されます。
次の行では output_sentence
リストが定義され、予測された翻訳が格納される。
次に、for
ループを実行する。
forループの実行サイクル数は、出力文の中で最も長い文の長さに等しくなります。
ループの内部では、最初の反復でdecoder_modelがエンコーダの隠れ状態とセル状態、および入力トークン、すなわち
を用いて、出力と隠れ状態、セル状態を予測する。
予測された単語のインデックスが変数idxに格納される。
もし予測されたインデックスがトークンと等しければ、ループは終了する。
予測されたインデックスが0より大きい場合は、対応する単語をidx2wordから取得して
word変数に格納し、
output_sentenceリストに追加する。
変数states_valueはデコーダの新しい隠れ状態とセルの状態で更新され、予測された単語のインデックスが変数
target_seqに格納される。
次のループサイクルでは、更新された隠れ家とセルの状態と、前に予測されたワードのインデックスが、新しい予測を行うために使用される。
ループは最大出力シーケンス長に達するか、あるいは` トークンに遭遇するまで続けられる。
最後に、output_sentence
リスト中の単語がスペースで連結され、結果の文字列が呼び出し元の関数に返される。
モデルのテスト
コードをテストするために、input_sentences
リストからランダムに文を選び、その文に対応するパディングされたシーケンスを取得し、 translate_sentence()
メソッドに渡します。
このメソッドは、以下のように翻訳された文章を返します。
以下は、このモデルの機能をテストするためのスクリプトです。
i = np.random.choice(len(input_sentences))
input_seq = encoder_input_sequences[i:i+1]
translation = translate_sentence(input_seq)
print('-')
print('Input:', input_sentences[i])
print('Response:', translation)
出力は以下の通りです。
-
Input: You're not fired.
Response: vous n'êtes pas viré.
見事でしょう?このモデルは、「あなたは解雇されていない」という文章をフランス語に翻訳することに成功した。
Google翻訳でも確認することができます。
もう1つ試してみましょう。
注:文章はランダムに選択されるので、おそらく異なる英語の文章がフランス語に翻訳されるでしょう。
フランス語に翻訳されたいくつかの他の英語の文章を見るために、もう一度上記のスクリプトを実行します。
次のような結果が得られました。
-
Input: I'm not a lawyer.
Response: je ne suis pas avocat.
このモデルは、別の英文をフランス語に翻訳することに成功しました。
結論と展望
ニューラル機械翻訳は、自然言語処理のかなり進んだ応用であり、非常に複雑なアーキテクチャを含んでいます。
この記事では、エンコーダ・デコーダモデルをベースとしたseq2seqアーキテクチャによるニューラル機械翻訳の実行方法を説明します。
エンコーダはLSTMで入力文を符号化し、デコーダはその入力を復号して対応する出力を生成する。
この記事で説明した手法は、データセットがこの記事で使用したものと同様の形式であれば、あらゆる機械翻訳モデルの作成に使用することができます。
また、seq2seqアーキテクチャを利用してチャットボットを開発することも可能です。
seq2seqアーキテクチャは、入力関係を出力にマッピングすることに関しては、かなり成功しています。
しかし、seq2seqアーキテクチャには1つの制限があります。
この記事で説明するバニラseq2seqアーキテクチャは、コンテキストを捉えることができないのです。
それは単に単体の入力を単体の出力に対応付けることを学習するだけである。
リアルタイムの会話はコンテキストに基づいており、2人以上のユーザー間の対話は、過去に何が語られたかに基づいている。
したがって、かなり高度なチャットボットを作成したい場合は、単純なエンコーダ-デコーダベースのseq2seqモデルを使用すべきではありません。
</sos