TensorFlow : Tutorials : 生成モデル : ニューラル機械翻訳 with Attention (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 09/02/2018
* TensorFlow 1.10 で更に改訂されています。
* TensorFlow 1.9 でドキュメント構成が変更され、数篇が新規に追加されましたので再翻訳しました。
* 本ページは、TensorFlow の本家サイトの Tutorials – Generative Models の以下のページを翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
ニューラル機械翻訳 with Attention
このノートブックは西英翻訳のための sequence to sequence (seq2seq) モデルを tf.keras と eager execution を使用して訓練します。これは sequence to sequence モデルの何某かの知識を仮定した上級サンプルです。
このノートブックでモデルを訓練した後、”¿todavia estan en casa?” のようなスペイン語のセンテンスを入力することができ、英語翻訳: “are you still at home?” を返すことができます。
翻訳品質は toy サンプルに対しては妥当ですが、生成された attention プロットは多分より興味深いです。これは翻訳の間に入力センテンスのどの部分がモデルの attention を持つかを示します :
Note: このサンプルは単一の P100 GPU 上で実行するためにおよそ 10 分かかります。
from __future__ import absolute_import, division, print_function # Import TensorFlow >= 1.9 and enable eager execution import tensorflow as tf tf.enable_eager_execution() import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split import unicodedata import re import numpy as np import os import time print(tf.__version__)
データセットをダウンロードして準備する
私達は http://www.manythings.org/anki/ で提供される言語データセットを使用します。このデータセットは次のフォーマットの言語翻訳ペアを含みます :
May I borrow this book? ¿Puedo tomar prestado este libro?
利用可能な様々な言語がありますが、英語-スペイン語データセットを使用します。便宜上、このデータセットのコピーを Google Cloud 上にホストしましたが、貴方自身のコピーをダウンロードすることもできます。データセットをダウンロードした後、データを準備するために取るステップがここにあります :
- 各センテンスに start と end トークンを追加します。
- 特殊文字を除去してセンテンスをクリーンアップします。
- 単語インデックスとリバース単語インデックス (単語 → id と id → 単語のマッピングをする辞書) を作成します。
- 各センテンスを最大長にパッドします。
# Download the file path_to_zip = tf.keras.utils.get_file( 'spa-eng.zip', origin='http://download.tensorflow.org/data/spa-eng.zip', extract=True) path_to_file = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"
# Converts the unicode file to ascii def unicode_to_ascii(s): return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') def preprocess_sentence(w): w = unicode_to_ascii(w.lower().strip()) # creating a space between a word and the punctuation following it # eg: "he is a boy." => "he is a boy ." # Reference:- https://stackoverflow.com/questions/3645931/python-padding-punctuation-with-white-spaces-keeping-punctuation w = re.sub(r"([?.!,¿])", r" \1 ", w) w = re.sub(r'[" "]+', " ", w) # replacing everything with space except (a-z, A-Z, ".", "?", "!", ",") w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w) w = w.rstrip().strip() # adding a start and an end token to the sentence # so that the model know when to start and stop predicting. w = '' + w + ' ' return w
# 1. Remove the accents # 2. Clean the sentences # 3. Return word pairs in the format: [ENGLISH, SPANISH] def create_dataset(path, num_examples): lines = open(path, encoding='UTF-8').read().strip().split('\n') word_pairs = [[preprocess_sentence(w) for w in l.split('\t')] for l in lines[:num_examples]] return word_pairs
# This class creates a word -> index mapping (e.g,. "dad" -> 5) and vice-versa # (e.g., 5 -> "dad") for each language, class LanguageIndex(): def __init__(self, lang): self.lang = lang self.word2idx = {} self.idx2word = {} self.vocab = set() self.create_index() def create_index(self): for phrase in self.lang: self.vocab.update(phrase.split(' ')) self.vocab = sorted(self.vocab) self.word2idx[''] = 0 for index, word in enumerate(self.vocab): self.word2idx[word] = index + 1 for word, index in self.word2idx.items(): self.idx2word[index] = word
def max_length(tensor): return max(len(t) for t in tensor) def load_dataset(path, num_examples): # creating cleaned input, output pairs pairs = create_dataset(path, num_examples) # index language using the class defined above inp_lang = LanguageIndex(sp for en, sp in pairs) targ_lang = LanguageIndex(en for en, sp in pairs) # Vectorize the input and target languages # Spanish sentences input_tensor = [[inp_lang.word2idx[s] for s in sp.split(' ')] for en, sp in pairs] # English sentences target_tensor = [[targ_lang.word2idx[s] for s in en.split(' ')] for en, sp in pairs] # Calculate max_length of input and output tensor # Here, we'll set those to the longest sentence in the dataset max_length_inp, max_length_tar = max_length(input_tensor), max_length(target_tensor) # Padding the input and output tensor to the maximum length input_tensor = tf.keras.preprocessing.sequence.pad_sequences(input_tensor, maxlen=max_length_inp, padding='post') target_tensor = tf.keras.preprocessing.sequence.pad_sequences(target_tensor, maxlen=max_length_tar, padding='post') return input_tensor, target_tensor, inp_lang, targ_lang, max_length_inp, max_length_tar
実験をより速くするためにデータセットのサイズを制限する (オプション)
> 100,000 センテンスの完全なデータセット上で訓練するのは時間がかかります。より速く訓練するために、データセットのサイズを 30,000 センテンスに制限することができます (もちろん、翻訳品質はより少ないデータでは劣化します) :
# Try experimenting with the size of that dataset num_examples = 30000 input_tensor, target_tensor, inp_lang, targ_lang, max_length_inp, max_length_targ = load_dataset(path_to_file, num_examples)
# Creating training and validation sets using an 80-20 split input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2) # Show length len(input_tensor_train), len(target_tensor_train), len(input_tensor_val), len(target_tensor_val)
tf.data データセットを作成する
BUFFER_SIZE = len(input_tensor_train) BATCH_SIZE = 64 embedding_dim = 256 units = 1024 vocab_inp_size = len(inp_lang.word2idx) vocab_tar_size = len(targ_lang.word2idx) dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE) dataset = dataset.apply(tf.contrib.data.batch_and_drop_remainder(BATCH_SIZE))
エンコーダとデコーダ・モデルを書く
ここで、attention を持つエンコーダ-デコーダ・モデルを実装します、これについては TensorFlow Neural Machine Translation (seq2seq) チュートリアル で読むことができます。このサンプルは API のより新しいセットを使用しています。このノートブックは seq2seq チュートリアルからの attention 等式を実装しています。次のダイアグラムは attention メカニズムにより各入力単語に重みが割り当てられて、それからそれがセンテンスの次の単語を予測するためにデコーダにより使用されることを示します。
入力はエンコーダ・モデルの中を通され、これは shape (batch_size, max_length, hidden_size) のエンコーダ出力と shape (batch_size, hidden_size) のエンコーダ隠れ状態を与えます。
ここに実装される等式があります :
私達は Bahdanau attention を使用しています。単純化された形式を書く前に記法を決めましょう :
- FC = 完全結合 (dense) 層
- EO = エンコーダ出力
- H = 隠れ状態
- X = デコーダへの入力
そして擬似コード :
- score = FC(tanh(FC(EO) + FC(H)))
- attention weights = softmax(score, axis = 1)。デフォルトでは softmax は最後の axis 上に適用されますがここではそれを 1st axis 上で適用することを望みます、何故ならばスコアの shape は (batch_size, max_length, hidden_size) だからです。Max_length は入力の長さです。各入力に重みを割り当てようとしていますので、softmax はその軸上で適用されるべきです。
- context vector = sum(attention weights * EO, axis = 1)。上と同じ理由で axis として 1 を選択します。
- embedding output = デコーダへの入力 X は埋め込み層を通されます。
- merged vector = concat(embedding output, context vector)
- そしてこのマージされたベクトルが GRU に与えられます。
各ステップにおける総てのベクトルの shape はコードのコメントで指定されます :
def gru(units): # If you have a GPU, we recommend using CuDNNGRU(provides a 3x speedup than GRU) # the code automatically does that. if tf.test.is_gpu_available(): return tf.keras.layers.CuDNNGRU(units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform') else: return tf.keras.layers.GRU(units, return_sequences=True, return_state=True, recurrent_activation='sigmoid', recurrent_initializer='glorot_uniform')
class Encoder(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz): super(Encoder, self).__init__() self.batch_sz = batch_sz self.enc_units = enc_units self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim) self.gru = gru(self.enc_units) def call(self, x, hidden): x = self.embedding(x) output, state = self.gru(x, initial_state = hidden) return output, state def initialize_hidden_state(self): return tf.zeros((self.batch_sz, self.enc_units))
class Decoder(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz): super(Decoder, self).__init__() self.batch_sz = batch_sz self.dec_units = dec_units self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim) self.gru = gru(self.dec_units) self.fc = tf.keras.layers.Dense(vocab_size) # used for attention self.W1 = tf.keras.layers.Dense(self.dec_units) self.W2 = tf.keras.layers.Dense(self.dec_units) self.V = tf.keras.layers.Dense(1) def call(self, x, hidden, enc_output): # enc_output shape == (batch_size, max_length, hidden_size) # hidden shape == (batch_size, hidden size) # hidden_with_time_axis shape == (batch_size, 1, hidden size) # we are doing this to perform addition to calculate the score hidden_with_time_axis = tf.expand_dims(hidden, 1) # score shape == (batch_size, max_length, hidden_size) score = tf.nn.tanh(self.W1(enc_output) + self.W2(hidden_with_time_axis)) # attention_weights shape == (batch_size, max_length, 1) # we get 1 at the last axis because we are applying score to self.V attention_weights = tf.nn.softmax(self.V(score), axis=1) # context_vector shape after sum == (batch_size, hidden_size) context_vector = attention_weights * enc_output context_vector = tf.reduce_sum(context_vector, axis=1) # x shape after passing through embedding == (batch_size, 1, embedding_dim) x = self.embedding(x) # x shape after concatenation == (batch_size, 1, embedding_dim + hidden_size) x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1) # passing the concatenated vector to the GRU output, state = self.gru(x) # output shape == (batch_size * max_length, hidden_size) output = tf.reshape(output, (-1, output.shape[2])) # output shape == (batch_size * max_length, vocab) x = self.fc(output) return x, state, attention_weights def initialize_hidden_state(self): return tf.zeros((self.batch_sz, self.dec_units))
encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE) decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)
optimizer と損失関数を定義する
optimizer = tf.train.AdamOptimizer() def loss_function(real, pred): mask = 1 - np.equal(real, 0) loss_ = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=real, logits=pred) * mask return tf.reduce_mean(loss_)
訓練
- 入力をエンコーダを通します、これはエンコーダ出力とエンコーダ隠れ状態を返します。
- エンコーダ出力、エンコーダ隠れ状態とデコーダ入力 (これは start トークンです) がデコーダに渡されます。
- デコーダは予測とデコーダ隠れ状態を返します。
- そしてデコーダ隠れ状態はモデルに渡し返されて予測は損失を計算するために使用されます。
- デコーダへの次の入力を決めるために teacher forcing を使用します。
- teacher forcing はターゲット単語がデコーダへの次の入力として渡されるテクニックです。
- 最後のステップは勾配を計算してそれを optimizer に適用して backpropagate します。
EPOCHS = 10 for epoch in range(EPOCHS): start = time.time() hidden = encoder.initialize_hidden_state() total_loss = 0 for (batch, (inp, targ)) in enumerate(dataset): loss = 0 with tf.GradientTape() as tape: enc_output, enc_hidden = encoder(inp, hidden) dec_hidden = enc_hidden dec_input = tf.expand_dims([targ_lang.word2idx['']] * BATCH_SIZE, 1) # Teacher forcing - feeding the target as the next input for t in range(1, targ.shape[1]): # passing enc_output to the decoder predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output) loss += loss_function(targ[:, t], predictions) # using teacher forcing dec_input = tf.expand_dims(targ[:, t], 1) total_loss += (loss / int(targ.shape[1])) variables = encoder.variables + decoder.variables gradients = tape.gradient(loss, variables) optimizer.apply_gradients(zip(gradients, variables), tf.train.get_or_create_global_step()) if batch % 100 == 0: print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1, batch, loss.numpy() / int(targ.shape[1]))) print('Epoch {} Loss {:.4f}'.format(epoch + 1, total_loss/len(input_tensor))) print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
翻訳する
- evaluate 関数は訓練ループに似ています、ここでは teacher forcing を使用しないことを除いて。各時間ステップでのデコーダへの入力は隠れ状態とエンコーダ出力と一緒にその前の予測になります。
- モデルが end トークンを予測するとき予測を停止します。
- そして総ての時間ステップのために attention 重みをストアします。
Note: エンコーダ出力は一つの入力に対して一度だけ計算されます。
def evaluate(sentence, encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ): attention_plot = np.zeros((max_length_targ, max_length_inp)) sentence = preprocess_sentence(sentence) inputs = [inp_lang.word2idx[i] for i in sentence.split(' ')] inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs], maxlen=max_length_inp, padding='post') inputs = tf.convert_to_tensor(inputs) result = '' hidden = [tf.zeros((1, units))] enc_out, enc_hidden = encoder(inputs, hidden) dec_hidden = enc_hidden dec_input = tf.expand_dims([targ_lang.word2idx['']], 0) for t in range(max_length_targ): predictions, dec_hidden, attention_weights = decoder(dec_input, dec_hidden, enc_out) # storing the attention weigths to plot later on attention_weights = tf.reshape(attention_weights, (-1, )) attention_plot[t] = attention_weights.numpy() predicted_id = tf.multinomial(tf.exp(predictions), num_samples=1)[0][0].numpy() result += targ_lang.idx2word[predicted_id] + ' ' if targ_lang.idx2word[predicted_id] == ' ': return result, sentence, attention_plot # the predicted ID is fed back into the model dec_input = tf.expand_dims([predicted_id], 0) return result, sentence, attention_plot
# function for plotting the attention weights def plot_attention(attention, sentence, predicted_sentence): fig = plt.figure(figsize=(10,10)) ax = fig.add_subplot(1, 1, 1) ax.matshow(attention, cmap='viridis') fontdict = {'fontsize': 14} ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90) ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict) plt.show()
def translate(sentence, encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ): result, sentence, attention_plot = evaluate(sentence, encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ) print('Input: {}'.format(sentence)) print('Predicted translation: {}'.format(result)) attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))] plot_attention(attention_plot, sentence.split(' '), result.split(' '))
translate('hace mucho frio aqui.', encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ)
translate('esta es mi vida.', encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ)
translate('¿todavia estan en casa?', encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ)
# wrong translation translate('trata de averiguarlo.', encoder, decoder, inp_lang, targ_lang, max_length_inp, max_length_targ)
Next steps
- 翻訳の実験をするために 異なるデータセットをダウンロード します、例えば、英語 to 独語、あるいは英語 to 仏語です。
- より巨大なデータセット上の訓練で実験します、あるいはより多いエポックを使用します。
以上