TensorFlow 2.0 Beta : 上級 Tutorials : テキストとシークエンス :- ニューラル機械翻訳 with Attention (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 07/06/2019
* 本ページは、TensorFlow の本家サイトの TF 2.0 Beta – Advanced Tutorials – Text and sequences の以下のページを翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
テキストとシークエンス :- ニューラル機械翻訳 with Attention
このノートブックは西英翻訳のための sequence to sequence (seq2seq) モデルを訓練します。これは 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, unicode_literals !pip install -q tensorflow-gpu==2.0.0-beta1 import tensorflow as tf import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split import unicodedata import re import numpy as np import os import io import time
データセットをダウンロードして準備する
私達は 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://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip', extract=True) path_to_file = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip 2646016/2638744 [==============================] - 0s 0us/step
# 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
en_sentence = u"May I borrow this book?" sp_sentence = u"¿Puedo tomar prestado este libro?" print(preprocess_sentence(en_sentence)) print(preprocess_sentence(sp_sentence).encode('utf-8'))
<start> may i borrow this book ? <end> b'<start> \xc2\xbf puedo tomar prestado este libro ? <end>'
# 1. Remove the accents # 2. Clean the sentences # 3. Return word pairs in the format: [ENGLISH, SPANISH] def create_dataset(path, num_examples): lines = io.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 zip(*word_pairs)
en, sp = create_dataset(path_to_file, None) print(en[-1]) print(sp[-1])
<start> if you want to sound like a native speaker , you must be willing to practice saying the same sentence over and over in the same way that banjo players practice the same phrase over and over until they can play it correctly and at the desired tempo . <end> <start> si quieres sonar como un hablante nativo , debes estar dispuesto a practicar diciendo la misma frase una y otra vez de la misma manera en que un musico de banjo practica el mismo fraseo una y otra vez hasta que lo puedan tocar correctamente y en el tiempo esperado . <end>
def max_length(tensor): return max(len(t) for t in tensor)
def tokenize(lang): lang_tokenizer = tf.keras.preprocessing.text.Tokenizer( filters='') lang_tokenizer.fit_on_texts(lang) tensor = lang_tokenizer.texts_to_sequences(lang) tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding='post') return tensor, lang_tokenizer
def load_dataset(path, num_examples=None): # creating cleaned input, output pairs targ_lang, inp_lang = create_dataset(path, num_examples) input_tensor, inp_lang_tokenizer = tokenize(inp_lang) target_tensor, targ_lang_tokenizer = tokenize(targ_lang) return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer
実験をより速くするためにデータセットのサイズを制限する (オプション)
センテンス > 100,000 の完全なデータセット上で訓練するのは時間がかかります。より速く訓練するために、データセットのサイズを 30,000 センテンスに制限することができます (もちろん、翻訳品質はより少ないデータでは劣化します) :
# Try experimenting with the size of that dataset num_examples = 30000 input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples) # Calculate max_length of the target tensors max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)
# 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)
(24000, 24000, 6000, 6000)
def convert(lang, tensor): for t in tensor: if t!=0: print ("%d ----> %s" % (t, lang.index_word[t]))
print ("Input Language; index to word mapping") convert(inp_lang, input_tensor_train[0]) print () print ("Target Language; index to word mapping") convert(targ_lang, target_tensor_train[0])
Input Language; index to word mapping 1 ----> <start> 30 ----> esto 7 ----> es 1109 ----> emocionante 3 ----> . 2 ----> <end> Target Language; index to word mapping 1 ----> <start> 19 ----> this 8 ----> is 1239 ----> exciting 3 ----> . 2 ----> <end>
tf.data データセットを作成する
BUFFER_SIZE = len(input_tensor_train) BATCH_SIZE = 64 steps_per_epoch = len(input_tensor_train)//BATCH_SIZE embedding_dim = 256 units = 1024 vocab_inp_size = len(inp_lang.word_index)+1 vocab_tar_size = len(targ_lang.word_index)+1 dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE) dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
example_input_batch, example_target_batch = next(iter(dataset)) example_input_batch.shape, example_target_batch.shape
(TensorShape([64, 16]), TensorShape([64, 11]))
エンコーダとデコーダ・モデルを書く
attention を持つエンコーダ-デコーダ・モデルを実装します、これについて TensorFlow Neural Machine Translation (seq2seq) チュートリアル で読むことができます。このサンプルは API のより新しいセットを使用しています。このノートブックは seq2seq チュートリアルからの attention 式 を実装しています。次の図は attention メカニズムにより各入力単語に重みが割り当てられて、それからそれがセンテンスの次の単語を予測するためにデコーダにより使用されることを示しています。下の図と式は Luong のペーパー からの 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 が最後の軸上に適用されますがここではそれを最初の軸上で適用することを望みます、何故ならばスコアの shape は (batch_size, max_length, hidden_size) だからです。Max_length は入力の長さです。各入力に重みを割り当てようとしていますので、softmax はその軸上で適用されるべきです。
- context vector = sum(attention weights * EO, axis = 1)。上と同じ理由で軸として 1 を選択します。
- embedding output = デコーダへの入力 X は埋め込み層を通されます。
- merged vector = concat(embedding output, context vector)
- それからこのマージされたベクトルが GRU に与えられます。
各ステップにおける総てのベクトルの shape はコードのコメントで指定されます :
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 = tf.keras.layers.GRU(self.enc_units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform') 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))
encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE) # sample input sample_hidden = encoder.initialize_hidden_state() sample_output, sample_hidden = encoder(example_input_batch, sample_hidden) print ('Encoder output shape: (batch size, sequence length, units) {}'.format(sample_output.shape)) print ('Encoder Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))
Encoder output shape: (batch size, sequence length, units) (64, 16, 1024) Encoder Hidden state shape: (batch size, units) (64, 1024)
class BahdanauAttention(tf.keras.Model): def __init__(self, units): super(BahdanauAttention, self).__init__() self.W1 = tf.keras.layers.Dense(units) self.W2 = tf.keras.layers.Dense(units) self.V = tf.keras.layers.Dense(1) def call(self, query, values): # 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(query, 1) # score shape == (batch_size, max_length, 1) # we get 1 at the last axis because we are applying score to self.V # the shape of the tensor before applying self.V is (batch_size, max_length, units) score = self.V(tf.nn.tanh( self.W1(values) + self.W2(hidden_with_time_axis))) # attention_weights shape == (batch_size, max_length, 1) attention_weights = tf.nn.softmax(score, axis=1) # context_vector shape after sum == (batch_size, hidden_size) context_vector = attention_weights * values context_vector = tf.reduce_sum(context_vector, axis=1) return context_vector, attention_weights
attention_layer = BahdanauAttention(10) attention_result, attention_weights = attention_layer(sample_hidden, sample_output) print("Attention result shape: (batch size, units) {}".format(attention_result.shape)) print("Attention weights shape: (batch_size, sequence_length, 1) {}".format(attention_weights.shape))
Attention result shape: (batch size, units) (64, 1024) Attention weights shape: (batch_size, sequence_length, 1) (64, 16, 1)
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 = tf.keras.layers.GRU(self.dec_units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform') self.fc = tf.keras.layers.Dense(vocab_size) # used for attention self.attention = BahdanauAttention(self.dec_units) def call(self, x, hidden, enc_output): # enc_output shape == (batch_size, max_length, hidden_size) context_vector, attention_weights = self.attention(hidden, enc_output) # 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 * 1, hidden_size) output = tf.reshape(output, (-1, output.shape[2])) # output shape == (batch_size, vocab) x = self.fc(output) return x, state, attention_weights
decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE) sample_decoder_output, _, _ = decoder(tf.random.uniform((64, 1)), sample_hidden, sample_output) print ('Decoder output shape: (batch_size, vocab size) {}'.format(sample_decoder_output.shape))
Decoder output shape: (batch_size, vocab size) (64, 4935)
optimizer と損失関数を定義する
optimizer = tf.keras.optimizers.Adam() loss_object = tf.keras.losses.SparseCategoricalCrossentropy( from_logits=True, reduction='none') def loss_function(real, pred): mask = tf.math.logical_not(tf.math.equal(real, 0)) loss_ = loss_object(real, pred) mask = tf.cast(mask, dtype=loss_.dtype) loss_ *= mask return tf.reduce_mean(loss_)
チェックポイント (オブジェクトベースのセーブ)
checkpoint_dir = './training_checkpoints' checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt") checkpoint = tf.train.Checkpoint(optimizer=optimizer, encoder=encoder, decoder=decoder)
訓練
- 入力をエンコーダを通します、これはエンコーダ出力とエンコーダ隠れ状態を返します。
- エンコーダ出力、エンコーダ隠れ状態とデコーダ入力 (これは start トークンです) がデコーダに渡されます。
- デコーダは予測とデコーダ隠れ状態を返します。
- それからデコーダ隠れ状態はモデルに渡し返されて予測は損失を計算するために使用されます。
- デコーダへの次の入力を決めるために teacher forcing を使用します。
- teacher forcing はターゲット単語がデコーダへの次の入力として渡されるテクニックです。
- 最後のステップは勾配を計算してそれを optimizer に適用して backpropagate します。
@tf.function def train_step(inp, targ, enc_hidden): loss = 0 with tf.GradientTape() as tape: enc_output, enc_hidden = encoder(inp, enc_hidden) dec_hidden = enc_hidden dec_input = tf.expand_dims([targ_lang.word_index['']] * 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) batch_loss = (loss / int(targ.shape[1])) variables = encoder.trainable_variables + decoder.trainable_variables gradients = tape.gradient(loss, variables) optimizer.apply_gradients(zip(gradients, variables)) return batch_loss
EPOCHS = 10 for epoch in range(EPOCHS): start = time.time() enc_hidden = encoder.initialize_hidden_state() total_loss = 0 for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)): batch_loss = train_step(inp, targ, enc_hidden) total_loss += batch_loss if batch % 100 == 0: print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1, batch, batch_loss.numpy())) # saving (checkpoint) the model every 2 epochs if (epoch + 1) % 2 == 0: checkpoint.save(file_prefix = checkpoint_prefix) print('Epoch {} Loss {:.4f}'.format(epoch + 1, total_loss / steps_per_epoch)) print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
Epoch 1 Batch 0 Loss 4.5541 Epoch 1 Batch 100 Loss 2.2291 Epoch 1 Batch 200 Loss 1.8727 Epoch 1 Batch 300 Loss 1.7644 Epoch 1 Loss 2.0230 Time taken for 1 epoch 56.17962884902954 sec Epoch 2 Batch 0 Loss 1.5572 Epoch 2 Batch 100 Loss 1.5255 Epoch 2 Batch 200 Loss 1.3902 Epoch 2 Batch 300 Loss 1.2984 Epoch 2 Loss 1.3868 Time taken for 1 epoch 21.577687740325928 sec Epoch 3 Batch 0 Loss 1.1589 Epoch 3 Batch 100 Loss 1.0942 Epoch 3 Batch 200 Loss 1.0069 Epoch 3 Batch 300 Loss 0.8360 Epoch 3 Loss 0.9756 Time taken for 1 epoch 20.880752086639404 sec Epoch 4 Batch 0 Loss 0.7405 Epoch 4 Batch 100 Loss 0.7607 Epoch 4 Batch 200 Loss 0.6250 Epoch 4 Batch 300 Loss 0.5633 Epoch 4 Loss 0.6589 Time taken for 1 epoch 21.411985635757446 sec Epoch 5 Batch 0 Loss 0.4929 Epoch 5 Batch 100 Loss 0.5100 Epoch 5 Batch 200 Loss 0.4399 Epoch 5 Batch 300 Loss 0.4001 Epoch 5 Loss 0.4457 Time taken for 1 epoch 21.400570154190063 sec Epoch 6 Batch 0 Loss 0.3381 Epoch 6 Batch 100 Loss 0.3662 Epoch 6 Batch 200 Loss 0.2639 Epoch 6 Batch 300 Loss 0.2613 Epoch 6 Loss 0.3043 Time taken for 1 epoch 21.35978865623474 sec Epoch 7 Batch 0 Loss 0.2534 Epoch 7 Batch 100 Loss 0.2906 Epoch 7 Batch 200 Loss 0.2128 Epoch 7 Batch 300 Loss 0.1770 Epoch 7 Loss 0.2178 Time taken for 1 epoch 21.304248571395874 sec Epoch 8 Batch 0 Loss 0.1710 Epoch 8 Batch 100 Loss 0.2109 Epoch 8 Batch 200 Loss 0.1711 Epoch 8 Batch 300 Loss 0.1444 Epoch 8 Loss 0.1609 Time taken for 1 epoch 21.66683864593506 sec Epoch 9 Batch 0 Loss 0.1276 Epoch 9 Batch 100 Loss 0.1570 Epoch 9 Batch 200 Loss 0.1437 Epoch 9 Batch 300 Loss 0.1098 Epoch 9 Loss 0.1195 Time taken for 1 epoch 21.08802342414856 sec Epoch 10 Batch 0 Loss 0.0835 Epoch 10 Batch 100 Loss 0.1123 Epoch 10 Batch 200 Loss 0.0953 Epoch 10 Batch 300 Loss 0.0959 Epoch 10 Loss 0.0929 Time taken for 1 epoch 21.302005767822266 sec
翻訳する
- evaluate 関数は訓練ループに似ています、ここでは teacher forcing を使用しないことを除いて。各時間ステップでのデコーダへの入力は隠れ状態とエンコーダ出力と共にその前の予測になります。
- モデルが end トークンを予測するとき予測を停止します。
- そして総ての時間ステップのために attention 重みをストアします。
Note: エンコーダ出力は一つの入力に対して一度だけ計算されます。
def evaluate(sentence): attention_plot = np.zeros((max_length_targ, max_length_inp)) sentence = preprocess_sentence(sentence) inputs = [inp_lang.word_index[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.word_index['']], 0) for t in range(max_length_targ): predictions, dec_hidden, attention_weights = decoder(dec_input, dec_hidden, enc_out) # storing the attention weights to plot later on attention_weights = tf.reshape(attention_weights, (-1, )) attention_plot[t] = attention_weights.numpy() predicted_id = tf.argmax(predictions[0]).numpy() result += targ_lang.index_word[predicted_id] + ' ' if targ_lang.index_word[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): result, sentence, attention_plot = evaluate(sentence) print('Input: %s' % (sentence)) print('Predicted translation: {}'.format(result)) attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))] plot_attention(attention_plot, sentence.split(' '), result.split(' '))
最新のチェックポイントを復元してテストする
# restoring the latest checkpoint in checkpoint_dir checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
<tensorflow.python.training.tracking.util.CheckpointLoadStatus at 0x7f83b4381470>
translate(u'hace mucho frio aqui.')
Input: <start> hace mucho frio aqui . <end> Predicted translation: it s too cold here . <end>
translate(u'esta es mi vida.')
Input: <start> esta es mi vida . <end> Predicted translation: this is my life . <end>
translate(u'¿todavia estan en casa?')
Input: <start> ¿ todavia estan en casa ? <end> Predicted translation: are you still at home ? <end>
# wrong translation translate(u'trata de averiguarlo.')
Input: <start> trata de averiguarlo . <end> Predicted translation: try to figure it out . <end>
Next steps
- 翻訳による実験をするために 異なるデータセットをダウンロード します、例えば、英語 to 独語、あるいは英語 to 仏語です。
- より巨大なデータセット上の訓練で実験します、あるいはより多いエポックを使用します。
以上