ホーム » 機械翻訳

機械翻訳」カテゴリーアーカイブ

TensorFlow 2.0 : 上級 Tutorials : テキスト :- ニューラル機械翻訳 with Attention

TensorFlow 2.0 : 上級 Tutorials : テキスト :- ニューラル機械翻訳 with Attention (翻訳/解説)

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

* 本ページは、TensorFlow org サイトの TF 2.0 – Advanced Tutorials – Text の以下のページを翻訳した上で
適宜、補足説明したものです:

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

 

無料セミナー開催中 クラスキャット主催 人工知能 & ビジネス Web セミナー

人工知能とビジネスをテーマにウェビナー (WEB セミナー) を定期的に開催しています。スケジュールは弊社 公式 Web サイト でご確認頂けます。
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
  • Windows PC のブラウザからご参加が可能です。スマートデバイスもご利用可能です。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/
Facebook: https://www.facebook.com/ClassCatJP/

 

テキスト :- ニューラル機械翻訳 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

import tensorflow as tf

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
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 上にホストしましたが、貴方自身のコピーをダウンロードすることもできます。データセットをダウンロードした後、データを準備するために取るステップがここにあります :

  1. 各センテンスに start と end トークンを追加します。
  2. 特殊文字を除去してセンテンスをクリーンアップします。
  3. 単語インデックスとリバース単語インデックス (単語 → id と id → 単語のマッピングを行なう辞書) を作成します。
  4. 各センテンスを最大長にパッドします。
# 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
print(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>
22 ----> por
50 ----> favor
456 ----> escucha
3 ----> .
2 ----> <end>

Target Language; index to word mapping
1 ----> <start>
56 ----> please
279 ----> listen
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.layers.Layer):
  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)

 

訓練

  1. 入力をエンコーダを通します、これはエンコーダ出力とエンコーダ隠れ状態を返します。
  2. エンコーダ出力、エンコーダ隠れ状態とデコーダ入力 (これは開始トークンです) がデコーダに渡されます。
  3. デコーダは予測とデコーダ隠れ状態を返します。
  4. それからデコーダ隠れ状態はモデルに渡し戻されて予測は損失を計算するために使用されます。
  5. デコーダへの次の入力を決めるために teacher forcing を使用します。
  6. teacher forcing は、そこではターゲット単語がデコーダへの次の入力として渡されるテクニックです。
  7. 最後のステップは勾配を計算してそれを 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.5782
Epoch 1 Batch 100 Loss 2.3247
Epoch 1 Batch 200 Loss 1.8299
Epoch 1 Batch 300 Loss 1.7017
Epoch 1 Loss 2.0273
Time taken for 1 epoch 32.79620003700256 sec

Epoch 2 Batch 0 Loss 1.5939
Epoch 2 Batch 100 Loss 1.4579
Epoch 2 Batch 200 Loss 1.4302
Epoch 2 Batch 300 Loss 1.3060
Epoch 2 Loss 1.3856
Time taken for 1 epoch 17.484558582305908 sec

Epoch 3 Batch 0 Loss 0.9645
Epoch 3 Batch 100 Loss 0.9450
Epoch 3 Batch 200 Loss 0.9626
Epoch 3 Batch 300 Loss 1.0414
Epoch 3 Loss 0.9681
Time taken for 1 epoch 17.044427633285522 sec

Epoch 4 Batch 0 Loss 0.6285
Epoch 4 Batch 100 Loss 0.7940
Epoch 4 Batch 200 Loss 0.5498
Epoch 4 Batch 300 Loss 0.6397
Epoch 4 Loss 0.6515
Time taken for 1 epoch 17.429885387420654 sec

Epoch 5 Batch 0 Loss 0.4643
Epoch 5 Batch 100 Loss 0.4660
Epoch 5 Batch 200 Loss 0.4049
Epoch 5 Batch 300 Loss 0.4017
Epoch 5 Loss 0.4392
Time taken for 1 epoch 17.022470474243164 sec

Epoch 6 Batch 0 Loss 0.2925
Epoch 6 Batch 100 Loss 0.2970
Epoch 6 Batch 200 Loss 0.2859
Epoch 6 Batch 300 Loss 0.2650
Epoch 6 Loss 0.3011
Time taken for 1 epoch 17.285199642181396 sec

Epoch 7 Batch 0 Loss 0.2012
Epoch 7 Batch 100 Loss 0.1468
Epoch 7 Batch 200 Loss 0.2198
Epoch 7 Batch 300 Loss 0.2109
Epoch 7 Loss 0.2155
Time taken for 1 epoch 16.99945044517517 sec

Epoch 8 Batch 0 Loss 0.1343
Epoch 8 Batch 100 Loss 0.1683
Epoch 8 Batch 200 Loss 0.1547
Epoch 8 Batch 300 Loss 0.1345
Epoch 8 Loss 0.1589
Time taken for 1 epoch 17.30172872543335 sec

Epoch 9 Batch 0 Loss 0.1193
Epoch 9 Batch 100 Loss 0.1181
Epoch 9 Batch 200 Loss 0.1104
Epoch 9 Batch 300 Loss 0.1278
Epoch 9 Loss 0.1278
Time taken for 1 epoch 17.062464952468872 sec

Epoch 10 Batch 0 Loss 0.0915
Epoch 10 Batch 100 Loss 0.0890
Epoch 10 Batch 200 Loss 0.1234
Epoch 10 Batch 300 Loss 0.1449
Epoch 10 Loss 0.1016
Time taken for 1 epoch 17.3432514667511 sec

 

翻訳する

  • evaluate 関数は訓練ループに似ています、ここでは teacher forcing を使用しないことを除いて。各時間ステップでのデコーダへの入力は隠れ状態とエンコーダ出力と共にその前の予測になります。
  • モデルが終了トークンを予測するとき予測を停止します。
  • そして総ての時間ステップのために 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)

    ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

    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 0x7f687502d9e8>
translate(u'hace mucho frio aqui.')
Input: <start> hace mucho frio aqui . <end>
Predicted translation: it s very 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 we still at home now ? <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 仏語です。
  • より巨大なデータセット上の訓練で実験します、あるいはより多いエポックを使用します。
 

以上






TensorFlow 2.0 Beta : 上級 Tutorials :- テキストとシークエンス : ニューラル機械翻訳 with Attention

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 上にホストしましたが、貴方自身のコピーをダウンロードすることもできます。データセットをダウンロードした後、データを準備するために取るステップがここにあります :

  1. 各センテンスに start と end トークンを追加します。
  2. 特殊文字を除去してセンテンスをクリーンアップします。
  3. 単語インデックスとリバース単語インデックス (単語 → id と id → 単語のマッピングを行なう辞書) を作成します。
  4. 各センテンスを最大長にパッドします。
# 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)

 

訓練

  1. 入力をエンコーダを通します、これはエンコーダ出力とエンコーダ隠れ状態を返します。
  2. エンコーダ出力、エンコーダ隠れ状態とデコーダ入力 (これは start トークンです) がデコーダに渡されます。
  3. デコーダは予測とデコーダ隠れ状態を返します。
  4. それからデコーダ隠れ状態はモデルに渡し返されて予測は損失を計算するために使用されます。
  5. デコーダへの次の入力を決めるために teacher forcing を使用します。
  6. teacher forcing はターゲット単語がデコーダへの次の入力として渡されるテクニックです。
  7. 最後のステップは勾配を計算してそれを 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 仏語です。
  • より巨大なデータセット上の訓練で実験します、あるいはより多いエポックを使用します。
 

以上






TensorFlow 2.0 Alpha : 上級 Tutorials :- テキストとシークエンス : ニューラル機械翻訳 with Attention

TensorFlow 2.0 Alpha : 上級 Tutorials : テキストとシークエンス :- ニューラル機械翻訳 with Attention (翻訳/解説)

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

* 本ページは、TensorFlow の本家サイトの TF 2.0 Alpha – 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

!pip install -q tensorflow-gpu==2.0.0-alpha0
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 上にホストしましたが、貴方自身のコピーをダウンロードすることもできます。データセットをダウンロードした後、データを準備するために取るステップがここにあります :

  1. 各センテンスに start と end トークンを追加します。
  2. 特殊文字を除去してセンテンスをクリーンアップします。
  3. 単語インデックスとリバース単語インデックス (単語 → id と id → 単語のマッピングをする辞書) を作成します。
  4. 各センテンスを最大長にパッドします。
# 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"
# 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>
<start> ¿ 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>
431 ----> vuestro
92 ----> perro
7 ----> es
36 ----> muy
189 ----> grande
3 ----> .
2 ----> <end>

Target Language; index to word mapping
1 ----> <start>
31 ----> your
104 ----> dog
8 ----> is
48 ----> very
155 ----> big
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 メカニズムにより各入力単語に重みが割り当てられて、それからそれがセンテンスの次の単語を予測するためにデコーダにより使用されることを示しています。

入力はエンコーダ・モデルの中を通され、これは 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 は最後の軸上に適用されますがここではそれを 1st 軸上で適用することを望みます、何故ならばスコアの 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, hidden_size)
    score = self.V(tf.nn.tanh(
        self.W1(values) + 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(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)

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)

 

訓練

  1. 入力をエンコーダを通します、これはエンコーダ出力とエンコーダ隠れ状態を返します。
  2. エンコーダ出力、エンコーダ隠れ状態とデコーダ入力 (これは start トークンです) がデコーダに渡されます。
  3. デコーダは予測とデコーダ隠れ状態を返します。
  4. それからデコーダ隠れ状態はモデルに渡し返されて予測は損失を計算するために使用されます。
  5. デコーダへの次の入力を決めるために teacher forcing を使用します。
  6. teacher forcing はターゲット単語がデコーダへの次の入力として渡されるテクニックです。
  7. 最後のステップは勾配を計算してそれを 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.5657
Epoch 1 Batch 100 Loss 2.1792
Epoch 1 Batch 200 Loss 1.7583
Epoch 1 Batch 300 Loss 1.7384
Epoch 1 Loss 1.9949
Time taken for 1 epoch 49.0501441956 sec

Epoch 2 Batch 0 Loss 1.5181
Epoch 2 Batch 100 Loss 1.4801
Epoch 2 Batch 200 Loss 1.2197
Epoch 2 Batch 300 Loss 1.2561
Epoch 2 Loss 1.3435
Time taken for 1 epoch 34.9393060207 sec

Epoch 3 Batch 0 Loss 1.0709
Epoch 3 Batch 100 Loss 1.0403
Epoch 3 Batch 200 Loss 0.7950
Epoch 3 Batch 300 Loss 0.8709
Epoch 3 Loss 0.9174
Time taken for 1 epoch 34.288944006 sec

Epoch 4 Batch 0 Loss 0.7071
Epoch 4 Batch 100 Loss 0.6888
Epoch 4 Batch 200 Loss 0.4978
Epoch 4 Batch 300 Loss 0.5841
Epoch 4 Loss 0.6134
Time taken for 1 epoch 35.33512187 sec

Epoch 5 Batch 0 Loss 0.4685
Epoch 5 Batch 100 Loss 0.4926
Epoch 5 Batch 200 Loss 0.3000
Epoch 5 Batch 300 Loss 0.3866
Epoch 5 Loss 0.4136
Time taken for 1 epoch 34.1087429523 sec

Epoch 6 Batch 0 Loss 0.3144
Epoch 6 Batch 100 Loss 0.3042
Epoch 6 Batch 200 Loss 0.1984
Epoch 6 Batch 300 Loss 0.2658
Epoch 6 Loss 0.2875
Time taken for 1 epoch 35.1170010567 sec

Epoch 7 Batch 0 Loss 0.2284
Epoch 7 Batch 100 Loss 0.2275
Epoch 7 Batch 200 Loss 0.1428
Epoch 7 Batch 300 Loss 0.1976
Epoch 7 Loss 0.2032
Time taken for 1 epoch 33.9758181572 sec

Epoch 8 Batch 0 Loss 0.1724
Epoch 8 Batch 100 Loss 0.1635
Epoch 8 Batch 200 Loss 0.0983
Epoch 8 Batch 300 Loss 0.1408
Epoch 8 Loss 0.1461
Time taken for 1 epoch 35.9068500996 sec

Epoch 9 Batch 0 Loss 0.1353
Epoch 9 Batch 100 Loss 0.1123
Epoch 9 Batch 200 Loss 0.0888
Epoch 9 Batch 300 Loss 0.0953
Epoch 9 Loss 0.1107
Time taken for 1 epoch 34.0839440823 sec

Epoch 10 Batch 0 Loss 0.1103
Epoch 10 Batch 100 Loss 0.0954
Epoch 10 Batch 200 Loss 0.0654
Epoch 10 Batch 300 Loss 0.0824
Epoch 10 Loss 0.0885
Time taken for 1 epoch 35.4287509918 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).encode('utf-8'))
    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))
translate(u'hace mucho frio aqui.')
Input: <start> hace mucho frio aqui . <end>
Predicted translation: it s very 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 <end>
Predicted translation: are you still 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 仏語です。
  • より巨大なデータセット上の訓練で実験します、あるいはより多いエポックを使用します。
 

以上






TensorFlow 実装、マルチクラウド対応のニューラル機械翻訳ソリューションを提供開始

Press Release

cc_logo_square

2018年09月17日 
TensorFlow 実装、マルチクラウド対応のニューラル機械翻訳ソリューション
「ClassCat® NMT TK v1.0」を2018年10月から提供開始
 
– 英日・独英・仏英・西英翻訳の各種事前訓練モデルも併せて提供 –

 

クラスキャット AI リサーチ (株式会社クラスキャット、代表取締役社長:佐々木規行、茨城県取手市)は、最新のニューラルネット翻訳 AI 技術を Amazon EC2 を始めとするマルチクラウド上で手軽に利用できるソリューション新製品「ClassCat® NMT TK v1.0」を2018年10月から提供開始することを発表致しました。コンサルティング・サービスも併せて提供致します。

機械翻訳技術はニューラルネットワークの導入により目覚ましい進歩を遂げましたが、その技術の利用は必ずしも容易ではありません。本ソリューションは対訳コーパスを保持するユーザが最新のニューラルネット機械翻訳技術を手軽に利用して翻訳モデルを構築することを可能にします。

本ソリューションは深層学習フレームワークのデファクト・スタンダードである TensorFlow で実装されています。優れた可視化ツール TensorBoard でトレーニングの進捗が視覚的に監視可能です。

本ソリューションはマルチクラウドに対応しています。GPU (マルチ GPU 推奨) を装備するインスタンスやベアメタルが利用可能な各種パブリッククラウド Amazon EC2、Microsoft Azure、IBM Cloud 上でサービスが提供されます。

本ソリューションでは事前訓練モデルとして以下が提供可能です :

  • 英語 – 日本語 翻訳
  • ドイツ語 – 英語 翻訳
  • フランス語 – 英語 翻訳
  • スペイン語 – 英語 翻訳

事前訓練モデルは順次増やしていきます。


【販売概要】

製品名  : ClassCat® NMT ツールキット v1.0
販売時期 : 2018年10月1日
販売形態 : 直接販売・販売パートナー経由・OEM
販売価格 : オープンプライス

【動作環境】

製品名  : ClassCat® NMT ツールキット v1.0
OS    : Ubuntu Server 16.04 LTS
ハードウェア : 各種パブリッククラウドの仮想サーバ、ベアメタルサーバ。
GPU 装備必須、マルチGPU推奨。

お問合せ
本件に関するお問い合わせ先は下記までお願いいたします。

クラスキャット AI リサーチ (株式会社クラスキャット)
〒300-1525 茨城県取手市桜ヶ丘 4-48-7(AI 研究所)
セールス・マーケティング本部 セールス・インフォメーション
E-Mail:sales-info@classcat.com
WebSite: http://www.classcat.com/

※ ClassCat は株式会社クラスキャットの登録商標です。
※ TensorFlow, the TensorFlow logo and any related marks are trademarks of Google Inc.
※ AWS は米国その他の諸国における Amazon.com, Inc. またはその関連会社の商標です。
※ Microsoft Azure は米国 Microsoft Corporation の米国およびその他の国における登録商標または商標です。
※ その他、記載されている会社名・製品名は各社の登録商標または商標です。

TensorFlow : Tutorials : 生成モデル : ニューラル機械翻訳 with Attention

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 上にホストしましたが、貴方自身のコピーをダウンロードすることもできます。データセットをダウンロードした後、データを準備するために取るステップがここにあります :

  1. 各センテンスに start と end トークンを追加します。
  2. 特殊文字を除去してセンテンスをクリーンアップします。
  3. 単語インデックスとリバース単語インデックス (単語 → id と id → 単語のマッピングをする辞書) を作成します。
  4. 各センテンスを最大長にパッドします。
# 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_)

 

訓練

  1. 入力をエンコーダを通します、これはエンコーダ出力とエンコーダ隠れ状態を返します。
  2. エンコーダ出力、エンコーダ隠れ状態とデコーダ入力 (これは start トークンです) がデコーダに渡されます。
  3. デコーダは予測とデコーダ隠れ状態を返します。
  4. そしてデコーダ隠れ状態はモデルに渡し返されて予測は損失を計算するために使用されます。
  5. デコーダへの次の入力を決めるために teacher forcing を使用します。
  6. teacher forcing はターゲット単語がデコーダへの次の入力として渡されるテクニックです。
  7. 最後のステップは勾配を計算してそれを 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 仏語です。
  • より巨大なデータセット上の訓練で実験します、あるいはより多いエポックを使用します。
 

以上



TensorFlow : Tutorials : Sequences : ニューラル機械翻訳 (seq2seq) チュートリアル

TensorFlow : Tutorials : Sequences : ニューラル機械翻訳 (seq2seq) チュートリアル (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 09/02, 07/16/2018
作成日時 : 06/07/2018

* TensorFlow 1.9 でドキュメント構成が変わりましたので調整しました。
* 本ページは、TensorFlow 本家サイトの Tutorials – Sequences – Neural Machine Translation (seq2seq) Tutorial を
翻訳した上で適宜、補足説明したものです:

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

 


 

1 章. イントロダクション

 
Sequence-to-sequence (seq2seq) モデル (Sutskever et al., 2014, Cho et al., 2014) は機械翻訳、音声認識、そしてテキスト要約のような様々なタスクで大きな成功を楽しんでいます。このチュートリアルは読者に seq2seq モデルの完全な理解を与えて競争力のある seq2seq モデルをスクラッチからどのように構築するかを示します。ニューラル機械翻訳 (NMT) のタスクにフォーカスします、これは大成功 (= wild success) した seq2seq モデルのための本当に最初のテストベッドでした。含まれるコードは軽量で、高品質で、プロダクション・レディで、そして最新の研究アイデアが組み込まれています。この目標を以下により獲得します :

  1. 最近のデコーダ / attention ラッパー API、TensorFlow 1.2 データ iterator を使用する。
  2. 私達の強い専門技術をリカレントと seq2seq モデルの構築に組み入れる。
  3. 非常にベストな NMT モデルの構築と Google の NMT (GNMT) システム のレプリカのためのティップスとトリックを提供する。

私達は簡単にレプリカが作成できるベンチマークを提供することが重要であると信じます。その結果、完全な実験結果を提供して次の公に利用可能なデータセット上でモデルを事前訓練しました :

  1. Small-スケール: IWSLT Evaluation Campaign により提供される、英越 (= English-Vietnamese) パラレルコーパス of TED talks (133K センテンス・ペア)
  2. Large-スケール: WMT Evaluation Campaign により提供される、独英パラレルコーパス (4.5M センテンス・ペア)

最初に NMT のための seq2seq モデルについての何某かの基本知識を築き上げて、vanilla NMT モデルをどのように構築して訓練するかを説明します。2 番目のパートは attention メカニズムを持つ競争力のある NMT モデルの構築の詳細に入ります。それから、TensorFlow ベストプラクティス、bidirectional (双方向) RNN、ビームサーチ、更に GNMT attention を使用してマルチ GPU へスケールアップするというような、(速度と翻訳品質の両者で) ベストな可能な NMT モデルを構築するためにティップスとトリックを議論します。

 

2 章. 基本

ニューラル機械翻訳の背景

昔に戻れば、伝統的なフレーズ・ベースの翻訳システムはソース・センテンスを複数のチャンクに分解することによりタスクを遂行して、それからそれらをフレーズ・バイ・フレーズに翻訳しました。これは翻訳出力において流暢ではなく私達、人間が、翻訳するようなものではありませんでした。私達はソース・センテンス全体を読み、その意味を理解し、そしてそれから翻訳を生成します。ニューラル機械翻訳 (NMT) はそれを模倣します!

Figure 1. エンコーダ-デコーダ・アーキテクチャ – NMT のための一般的なアプローチの例です。エンコーダはソース・センテンスを “意味” ベクトルに変換します、これは翻訳を生成するためにデコーダを通して渡されます。

 
具体的には、NMT システムは最初に “thought” ベクトル を構築するためにエンコーダを使用して、ソース・センテンスを読みます、それは数字のシークエンスでセンテンスの意味を表します ; それからデコーダは、Figure 1 で示されるように翻訳を吐くためにセンテンス・ベクトルを処理します。これはしばしばエンコーダ-デコーダ・アーキテクチャとして参照されます。この方法で、NMT は伝統的なフレーズ・ベースのアプローチの局所翻訳問題に対処します : それは言語の long-range dependencies, e.g., gender agreements; シンタクス構造; etc., を捕捉することができて、そして Google Neural Machine Translation systems でデモされるような遥かにより流暢な翻訳を生成します。

NMT モデルはそれらの正確なアーキテクチャの観点から様々です。シーケンシャル・データのための自然な選択はリカレント・ニューラルネットワーク (RNN) で、殆どの NMT モデルで使用されます。通常は RNN はエンコーダとデコーダの両者のために使用されます。RNN モデルは、けれども、以下の観点から異なります : (a) 方向性 – 単方向 or 双方向 ; (b) 深さ – シングル- or マルチ層 ; そして (c) タイプ – しばしば vanilla RNN、Long Short-term Memory (LSTM)、または gated recurrent unit (GRU)。興味ある読者は RNN と LSTM についての更なる情報はこの ブログ投稿 で見つけることができます。

このチュートリアルでは、サンプルとして深層マルチ層 RNN を考えます、これは単方向でリカレント・ユニットとして LSTM を使用します。そのようなモデルのサンプルを Figure 2 で示します。このサンプルでは、ソース・センテンス “I am a student” をターゲット・センテンス “Je suis étudiant” に翻訳するモデルを構築します。高いレベルでは、NMT モデルは 2 つのリカレント・ニューラルネットワークから成ります : エンコーダ RNN はどのような予測をすることもなく単純に入力ソース単語を消費します ; デコーダは、一方で、次の単語を予測しながらターゲット・センテンスを処理します。

更なる情報のためには、読者は Luong (2016) を参照してください、これはこのチュートリアルがベースとしているものです。

Figure 2. ニューラル機械翻訳 – ソース・センテンス “I am a student” をターゲット・センテンス “Je suis étudiant” に翻訳するために提案された深層リカレント・アーキテクチャのサンプルです。ここで、”<s>” はデコーディング・プロセスの開始をマークしてその一方で “</s>” はデコーダに停止を伝えます。

 

チュートリアルをインストールする

このチュートリアルをインストールするためには、TensorFlow を貴方のシステム上にインストールする必要があります。このチュートリアルは TensorFlow Nightly を必要とします。TensorFlow をインストールするためには、ここのインストール手順 に従ってください。

ひとたび TensorFlow がインストールされれば、次を実行することでこのチュートリアルのソースコードをダウンロードできます :

git clone https://github.com/tensorflow/nmt/

 

訓練 – 最初の NMT システムをどのようにビルドするか

最初に具体的なコード・スニペットで NMT モデルの構築の中心へと飛び込みましょう、それを通して Figure 2 をより詳細に説明します。データ準備とフルコードは後に回します。このパートはファイル model.py を参照します。

ボトム層では、エンコーダとデコーダ RNN は入力として次を受け取ります : 最初に、ソース・センテンス、それから境界マーカー “\<s>”、これはエンコーディングからデコーディング・モードへの移行を示します、そしてターゲット・センテンスです。訓練のためには、次の tensor をシステムに供給します、これらは time-major 形式で単語インデックスを含みます :

  • encoder_inputs [max_encoder_time, batch_size]: ソース入力単語。
  • decoder_inputs [max_decoder_time, batch_size]: ターゲット入力単語。.
  • decoder_outputs [max_decoder_time, batch_size]: ターゲット出力単語、これらは右に追加された end-of-sentence タグを伴う左に 1 時間ステップシフトされた decoder_inputs です。

ここでは効率のために、複数のセンテンス (batch_size) で一度に訓練します。テスティングは少し異なりまるので、それは後で議論しましょう。

 

埋め込み

単語のカテゴリー的な性質が与えられたとき、モデルは対応する単語表現を取得するために最初にソースとターゲット埋め込みを調べなければなりません。この埋め込み層を動作させるために、最初に各言語のために語彙が選択されます。通常は、語彙サイズ V が選択され、そして最も頻度の高い V 単語だけが一意に扱われます。総ての他の単語は “unknown (未知)” トークンに変換されて総て同じ埋め込みを得ます。埋め込み重み、言語毎に 1 セット、は通常は訓練の間に学習されます。

# Embedding
embedding_encoder = variable_scope.get_variable(
    "embedding_encoder", [src_vocab_size, embedding_size], ...)
# Look up embedding:
#   encoder_inputs: [max_time, batch_size]
#   encoder_emb_inp: [max_time, batch_size, embedding_size]
encoder_emb_inp = embedding_ops.embedding_lookup(
    embedding_encoder, encoder_inputs)

同様に、embedding_decoder と decoder_emb_inp を構築することができます。word2vec や Glove ベクトルのような事前訓練された単語表現で埋め込み重みを初期化することを選択できることに注意してください。一般に、訓練データの巨大な総量が与えられた場合にはスクラッチからこれらの埋め込みを学習できます。

 

エンコーダ

ひとたび取得されれば、単語埋め込みはそれから入力としてメイン・ネットワークに供給されます、これは 2 つのマルチ層 RNN から成ります – ソース言語のためのエンコーダとターゲット言語のためのデコーダです。これらの 2 つの RNN は、原理的には、同じ重みを共有できます ; けれども、実際には、2 つの異なる RNN パラメータをしばしば使用します (そのようなモデルは巨大な訓練データセットにフィットするときより良いジョブを行ないます)。エンコーダ RNN はその開始状態としてゼロ・ベクトルを使用して次のように構築されます :

# Build RNN cell
encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)

# Run Dynamic RNN
#   encoder_outputs: [max_time, batch_size, num_units]
#   encoder_state: [batch_size, num_units]
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
    encoder_cell, encoder_emb_inp,
    sequence_length=source_sequence_length, time_major=True)

センテンスは異なる長さを持つことに注意してください、無駄な計算を回避するために、dynamic_rnn に正確なソース・センテンスの長さを source_sequence_length を通して伝えます。私達の入力は time major ですので、time_major=True を設定します。ここでは、シングル層 LSTM, encoder_cell だけを構築します。どのようにマルチ層 LSTM を構築し、dropout を追加し、そして attention を使用するかについては後のセクションで説明します。

 

デコーダ

デコーダもまたソース情報へのアクセスを持つ必要があり、そしてそれを達成する一つの単純な方法はエンコーダの最後の隠れ状態, encoder_state でそれを初期化することです。Figure 2 で、ソース単語 “student” における隠れ状態をデコーダ側に渡します。

# Build RNN cell
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Helper
helper = tf.contrib.seq2seq.TrainingHelper(
    decoder_emb_inp, decoder_lengths, time_major=True)
# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
    decoder_cell, helper, encoder_state,
    output_layer=projection_layer)
# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)
logits = outputs.rnn_output

ここで、このコードの中心パートは BasicDecoder, decoder, で、これは入力として (encoder_cell と同様の) decoder_cell、ヘルパー、そして前の encoder_state を受け取ります。デコーダとヘルパーを分離することにより、異なるコードベースを再利用することができます、e.g., TrainingHelper は greedy デコーディングを行なうために GreedyEmbeddingHelper で置き換えられます。それ以上は helper.py を見てださい。

最後に、projection_layer に言及していませんでしたが、これはトップの隠れ状態を次元 V のロジット・ベクトルに変える dense 行列です。このプロセスを Figure 2 のトップに示します。

projection_layer = layers_core.Dense(
    tgt_vocab_size, use_bias=False)

 

損失

上でロジットが与えられ、訓練損失を計算する用意ができました :

crossent = tf.nn.sparse_softmax_cross_entropy_with_logits(
    labels=decoder_outputs, logits=logits)
train_loss = (tf.reduce_sum(crossent * target_weights) /
    batch_size)

ここで、target_weights は decoder_outputs と同じサイズの 0-1 (= zero-one) 行列です。それはターゲット・シークエンス長の外側の位置のパディングを値 0 でマスクします。

重要なノート: 損失を batch_size で除算するので、ハイパーパラメータは batch_size に対して “不変 (= invariant)” であることは指摘する価値があります。人によっては損失を (batch_size * num_time_steps) で分割しますが、これは短いセンテンス上のエラーを軽視します。より微妙なことに、(前者に適用された) 私達のハイパーパラメータは後者の方法のためには使用できません。例えば、両者のアプローチが 1.0 の学習率で SGD を使用する場合、後者のアプローチは事実上 1 / num_time_steps の遥かに小さい学習率を使用します。

 

勾配計算 & 最適化

私達は今 NMT モデルの forward パスを定義しました。backpropagation パスを計算することは単に数行のコードの事柄です :

# Calculate and clip gradients
params = tf.trainable_variables()
gradients = tf.gradients(train_loss, params)
clipped_gradients, _ = tf.clip_by_global_norm(
    gradients, max_gradient_norm)

RNN の訓練における重要なステップの一つは勾配クリッピングです。ここで、global norm でクリップします。max value, max_gradient_norm はしばしば 5 or 1 のような値に設定されます。最後のステップは optimizer の選択です。Adam optimizer は一般的な選択です。学習率もまた選択します。learning_rate の値は通常は 0.0001 から 0.001 の範囲にあります ; そして訓練が進むにつれて減少するように設定できます。

# Optimization
optimizer = tf.train.AdamOptimizer(learning_rate)
update_step = optimizer.apply_gradients(
    zip(clipped_gradients, params))

私達自身の実験では、標準的な SGD (tf.train.GradientDescentOptimizer) を低下する学習率スケジュールで使用し、これはより良いパフォーマンスを生成します。ベンチマーク を見てください。

 

ハンズオン – NMT モデルを訓練しましょう

ベトナム語から英語に翻訳する、私達の本当に最初の NMT モデルを訓練しましょう、コードのエントリ・ポイントは nmt.py です。

この課題のために TED talks の small-スケールのパラレルコーパス ((133K 訓練サンプル) を使用します。ここで使用されるデータの総ては https://nlp.stanford.edu/projects/nmt/ で見つかります。tst2012 を dev データセットとして、そして tst2013 をテスト・データセットとして使用します。

NMT を訓練するためのデータをダンロードする次のコマンドを実行します :\
nmt/scripts/download_iwslt15.sh /tmp/nmt_data

訓練を開始するためには次のコマンドを実行します :

mkdir /tmp/nmt_model
python -m nmt.nmt \
    --src=vi --tgt=en \
    --vocab_prefix=/tmp/nmt_data/vocab  \
    --train_prefix=/tmp/nmt_data/train \
    --dev_prefix=/tmp/nmt_data/tst2012  \
    --test_prefix=/tmp/nmt_data/tst2013 \
    --out_dir=/tmp/nmt_model \
    --num_train_steps=12000 \
    --steps_per_stats=100 \
    --num_layers=2 \
    --num_units=128 \
    --dropout=0.2 \
    --metrics=bleu

上のコマンドは 128-dim 隠れユニットと埋め込みを持つ 2-層 LSTM seq2seq モデルを 12 エポック訓練します。0.2 の dropout 値 (確率 0.8 を保つ) を使用します。もしエラーがないのであれば、訓練につれての perplexity の減少とともに 下に似たログを見るはずです。

# First evaluation, global step 0
  eval dev: perplexity 17193.66
  eval test: perplexity 17193.27
# Start epoch 0, step 0, lr 1, Tue Apr 25 23:17:41 2017
  sample train data:
    src_reverse:   Điều đó , dĩ nhiên , là câu chuyện trích ra từ học thuyết của Karl Marx .
    ref: That , of course , was the  distilled from the theories of Karl Marx .   
  epoch 0 step 100 lr 1 step-time 0.89s wps 5.78K ppl 1568.62 bleu 0.00
  epoch 0 step 200 lr 1 step-time 0.94s wps 5.91K ppl 524.11 bleu 0.00
  epoch 0 step 300 lr 1 step-time 0.96s wps 5.80K ppl 340.05 bleu 0.00
  epoch 0 step 400 lr 1 step-time 1.02s wps 6.06K ppl 277.61 bleu 0.00
  epoch 0 step 500 lr 1 step-time 0.95s wps 5.89K ppl 205.85 bleu 0.00

より詳細は train.py を見てください。

訓練の間にモデルの要約を見るために TensorBoard を開始することもできます :

tensorboard --port 22222 --logdir /tmp/nmt_model/

英語からベトナム語への反対の方向の訓練は単純に次の変更で成されます:\ –src=en –tgt=vi

 

推論 – 翻訳をどのように生成するか

NMT モデルを訓練している一方で (そしてひとたびモデルを訓練したのであれば)、以前に見ていないソース・センテンスが与えられたときに翻訳を得ることができます。このプロセスは推論と呼ばれます。訓練と推論 (テスティング) の間には明確な区別があります : 推論時には、ソース・センテンス, i.e., encoder_inputsへのアクセスを持つのみです。デコーディングを遂行するためには多くの方法があります。デコーディング・メソッドは greedy、サンプリングそしてビームサーチ・デコーディングを含みます。ここでは、greedy デコーディング・ストラテジーを議論します。

アイデアは単純でそれを Figure 3 で示します :

  1. encoder_state を得るために訓練の間と同じ方法でソース・センテンスを依然としてエンコードします、そしてこの encoder_state はデコーダを初期化するために使用されます。
  2. デコーディング (翻訳) プロセスはデコーダが開始シンボル “\<s>” (コードでは tgt_sos_id として参照) を受け取るとすぐに開始されます;
  3. デコーダ側の各 time ステップに対して、RNN 出力をロジットのセットとして扱います。最も尤もらしい単語、最大ロジット値に関連する id、を出力された単語として選択します (これは “greedy” の挙動です)。Figure 3 のサンプルでは、最初のデコーディング・ステップで単語 “moi” が最も高い翻訳確率を持ちます。それからこの単語を次の time ステップへの入力として供給します。
  4. このプロセスが出力シンボルとして文末マーカー “\</s>” (コードでは tgt_eos_id として参照) が生成されるまで続きます。

Figure 3. Greedy デコーディング – 訓練された NMT モデルが greedy サーチを使用してソース・センテンス “je suis étudiant” のためにどのように翻訳を生成するかのサンプルです。

ステップ 3 は推論を訓練とは異なるものにするものです。入力として常に正しいターゲット単語を供給する代わりに、推論はモデルにより推測される単語を使用します。ここに greedy デコーディングを成し遂げるコードがあります。それは訓練デコーダに非常に似ています。

# Helper
helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(
    embedding_decoder,
    tf.fill([batch_size], tgt_sos_id), tgt_eos_id)

# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
    decoder_cell, helper, encoder_state,
    output_layer=projection_layer)
# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(
    decoder, maximum_iterations=maximum_iterations)
translations = outputs.sample_id

ここで、TrainingHelper の代わりに GreedyEmbeddingHelper を使用します。ターゲット・シークエンス長を前もって知らないので、翻訳の長さを制限するために maximum_iterations を使用します。一つの経験則はソース・センテンス長の 2 倍までデコードすることです。

maximum_iterations = tf.round(tf.reduce_max(source_sequence_length) * 2)

モデルを訓練したら、今では推論ファイルを作成して幾つかのセンテンスを翻訳することができます :

cat > /tmp/my_infer_file.vi
# (copy and paste some sentences from /tmp/nmt_data/tst2013.vi)

python -m nmt.nmt \
    --out_dir=/tmp/nmt_model \
    --inference_input_file=/tmp/my_infer_file.vi \
    --inference_output_file=/tmp/nmt_model/output_infer

cat /tmp/nmt_model/output_infer # To view the inference as output

訓練チェックポイントが存在する限りはモデルがまだ訓練中でも上のコマンドはまた実行できることに注意してください。より詳細は inference.py を見てください。

 

3 章. Intermediate

 
最も基本的な seq2seq モデルを通り抜けました、更に進みましょう!先端技術のニューラル翻訳システムを構築するためには、更なる “秘密のソース” が必要です : attention メカニズム、これは最初に Bahdanau et al., 2015 により導入され、後に Luong et al., 2015 他により洗練されました。attention メカニズムの鍵となるアイデアは、翻訳するときに関係するソースコンテンツに “注意 (attention)” を払うことによって、ターゲットとソース間の直接的なショートカット接続を確立することです。attention メカニズムの素晴らしい副産物は (Figure 4 で示されるような) ソースとターゲット・センテンス間の可視化しやすいアラインメント行列です。

Figure 4. Attention 可視化 – ソースとターゲット・センテンス間のアラインメントの例です。画像は (Bahdanau et al., 2015) から取られています。

vanilla seq2seq モデルでは、デコーディング・プロセスが始まるときエンコードからの最後のソース状態をデコーダに渡すことを思い出してください。これは短いあるいは中ぐらいの長さのセンテンスについては上手く動作します ; けれども、長いセンテンスに対しては、単一の固定長の隠れ状態は情報ボトルネックになります。ソース RNN で計算された隠れ状態の総てを捨てる代わりに、attention メカニズムはデコーダにそれらを覗き見ることを可能にします (それらをソース情報の動的メモリとして扱います)。
そのようにすることで、attention メカニズムはより長いセンテンスの翻訳を改善します。最近では、attention メカニズムはデファクト・スタンダードで多くの他のタスクに成功的に適用されてきています (画像キャプション生成、音声認識、そしてテキスト要約)。

 

Attention メカニズムの背景

(Luong et al., 2015) で提案された attention メカニズムの実例を今記述します、これはオープンソースのツールキット含む幾つかの先端技術システムとこのチュートリアルの TF seq2seq API で使用されているものです。attention メカニズムの他の変形へのコネクションもまた提供します。

Figure 5. Attention メカニズム – (Luong et al., 2015) で説明されている attention ベース NMT システムのサンプルです。attention 計算の最初のステップに詳細にハイライトしています。明確化のため、埋め込みと投射層 (= projection layer) は Figure (2) で示していません。

Figure 5 で図示されるように、attention 計算は総てのデコーダ time ステップで発生します。それは次の段階から成ります :

  1. (Figure 4 内で可視化される) attention 重みを導出するために現在のターゲット隠れ状態を総てのソース状態と比較します。
  2. attention 重みに基づいてソース状態の重み付けられた平均としてコンテキスト・ベクトルを計算します。
  3. 最終的な attention ベクトルを生成するためにコンテキスト・ベクトルを現在のターゲット隠れ状態と結合します。
  4. attention ベクトルは次の time ステップへの入力として供給されます (input feeding)。最初の 3 つのステップは下の等式によって要約できます :

 
ここで、関数 score はターゲット隠れ状態 $h_t$ をソース隠れ状態 $\overline{h}_s$ の各々と比較するために使用されて、結果は attention 重み (ソース位置に渡る分布) を生成するために正規化されます。scoring 関数の様々な選択があります ; ポピュラーな scoreing 関数は Eq. (4) で与えられる multiplicative and additive 形式を含みます。ひとたび計算されれば、attention ベクトルは softmax ロジットと損失を導出するために使用されます。これは vanilla seq2seq モデルのトップ層におけるターゲット隠れ状態に類似しています。関数 f はまた他の形式を取ることもできます。

attention メカニズムの様々な実装は attention_wrapper.py で見つけられます。

 

attention メカニズムでは何が重要でしょう?

上の等式でヒントが与えられているように、多くの異なる attention 変種があります。これらの変種は scoring 関数と attention 関数の形式、そして (Bahdanau et al., 2015) で元々提案されているように scoring 関数内で代わりに前の状態が使用されるか否かに依拠します。実証的に、私達は特定の選択だけが重要であることを見出しました。1 番目に、attention の基本的な形式、i.e. ターゲットとソースの直接コネクションが存在している必要があります。2 番目に、(Luong et al., 2015) で説明されているように過去の attention 決定についてネットワークに伝えるために attention ベクトルを次の timestep に供給することは重要です。最後に、scoring 関数の選択はしばしば異なるパフォーマンスの結果になります。詳細は (後述の) ベンチマーク結果のセクションを見てください。

 

Attention ラッパー API

AttentionWrapper の私達の実装では、(Weston et al., 2015) からメモリ・ネットワークのワークの幾つかの用語を借りています。読み書き可能なメモリを持つ代わりに、このチュートリアルで提案される attention メカニズムは read-only メモリです。特に、ソース隠れ状態 (あるいはそれらの変換されたバージョン、i.e. $W\overline{h}_s$ in Luong’s scoring スタイルまたは $W_2\overline{h}_s$ in Bahdanau’s scoring スタイル) のセットは “メモリ” として参照されます。各 time ステップで、メモリのどの部分を読むかを決定するために現在のターゲット隠れ状態を “query (問合せ)” として使用します。通常は、query は個々のメモリスロットに対応するキーと比較される必要があります。attention メカニズムの上の提示では、ソース隠れ状態 (あるいはそれらの変換されたバージョン、$W_1h_t$ i.e. in Bahdanau’s scoring スタイル) のセットを “キー” として偶々使用しています。他の attention 形式を導出するためにこのメモリ・ネットワーク用語によりインスパイアされるかもしれません!

attention ラッパーのおかげで、vanilla seq2seq コードを attention で拡張することは自明です。この部分はファイル attention_model.py に該当します。

最初に、attention メカニズム, e.g., from (Luong et al., 2015) を定義する必要があります :

# attention_states: [batch_size, max_time, num_units]
attention_states = tf.transpose(encoder_outputs, [1, 0, 2])

# Create an attention mechanism
attention_mechanism = tf.contrib.seq2seq.LuongAttention(
    num_units, attention_states,
    memory_sequence_length=source_sequence_length)

前のエンコーダのセクションでは、encoder_outputs はトップ層における総てのソース隠れ状態のセットで [max_time, batch_size, num_units] の shape を持ちます (何故ならば効率のために time_major を True に設定して dynamic_rnn を使用するからです)。attention メカニズムのためには、渡される “メモリ” が batch major であることを確かなものにする必要があります、そして attention_states を transpose する必要があります。 (non-padding 位置だけに渡り) attention 重みが適切に正規化されることを確かなものにするために source_sequence_length を attention メカニズムに渡します。attention メカニズムを定義しましたので、デコーディング・セルをラップするために AttentionWrapper を使用します :

decoder_cell = tf.contrib.seq2seq.AttentionWrapper(
    decoder_cell, attention_mechanism,
    attention_layer_size=num_units)

コードの残りはデコーダのセクション内のものと殆ど同じです!

 

ハンズオン – attention ベースの NMT モデルを構築する

attention を有効にするには、訓練の間 attention フラグとして luong, scaled_luong, bahdanau または normed_bahdanau の一つを使用する必要があります。フラグはどの attention メカニズムを使用するかを指定します。更に、attention モデルのための新しい辞書を作成する必要がありますので、前に訓練した基本 NMT モデルは再利用しません。

訓練を開始するためには次のコマンドを実行します :

mkdir /tmp/nmt_attention_model

python -m nmt.nmt \
    --attention=scaled_luong \
    --src=vi --tgt=en \
    --vocab_prefix=/tmp/nmt_data/vocab  \
    --train_prefix=/tmp/nmt_data/train \
    --dev_prefix=/tmp/nmt_data/tst2012  \
    --test_prefix=/tmp/nmt_data/tst2013 \
    --out_dir=/tmp/nmt_attention_model \
    --num_train_steps=12000 \
    --steps_per_stats=100 \
    --num_layers=2 \
    --num_units=128 \
    --dropout=0.2 \
    --metrics=bleu

訓練後、推論のための新しい out_dir を伴う同じ推論コマンドを利用できます :

python -m nmt.nmt \
    --out_dir=/tmp/nmt_attention_model \
    --inference_input_file=/tmp/my_infer_file.vi \
    --inference_output_file=/tmp/nmt_attention_model/output_infer

 

4 章. ティップス & トリック

 

訓練、評価、そして推論グラフを構築する

TensorFlow で機械学習モデルを構築するとき、3 つの分離したグラフを構築することがしばしば最善です :

  • 訓練グラフ、これは :
    • ファイル/外部入力のセットから入力データをバッチ処理し、バケットに入れ、そしておそらくはサブサンプリングします。
    • forward と backprop ops を含みます。
    • optimizer を構築して、訓練 op を追加します。
  • 評価グラフ、これは :
    • ファイル/外部入力のセットから入力データをバッチ処理し、バケットに入れます。
    • 訓練 forward ops と訓練では使用されない追加の評価 ops を含みます。
  • 推論グラフ、これは :
    • 入力データのバッチ処理はしないかもしれません。
    • 入力データのサブサンプリングやバケット処理は行ないません。
    • プレースホルダーから入力データを読みます (データは feed_dict を通して、あるいは C++ TensorFlow serving バイナリからデータはグラフに直接供給できます)。
    • モデル forward ops のサブセットと、おそらくは session.run 呼び出し間の状態をストアするため追加の特別な入出力を含みます。

分離したグラフの構築は幾つかの利点があります :

  • 推論グラフは通常は他の 2 つと非常に異なりますので、それを別に構築するのは意味があります。
  • 評価グラフはより単純になります、何故ならばそれはもはや追加の逆伝播 ops の総てを持たないからです。
  • データ供給は各グラフに対して別々に実装可能です。
  • 変数 reuse は遥かにより単純です。例えば、評価グラフにおいて reuse=True を持つ変数スコープを再オープンする必要はありません、それは単に訓練モデルはこれらの変数を既に作成しているからです。従って reuse= 引数を至るところに撒き散らすことなしに同じコードが再利用できます。
  • 分散訓練では、別々のワーカーに訓練、評価、そして推論を遂行させることはよくあります。これらはそれら自身のグラフを構築する必要がいずれにせよあります。従ってシステムをこの方法で構築することは分散訓練のために貴方を準備させることになります。

複雑さの主な源は単一のマシン設定で 3 つのグラフに渡り変数をどのように共有するかとなるでしょう。これは各グラフのために別々のセッションを使用することで解決されます。訓練セッションは定期的にチェックポイントをセーブし、そして評価セッションと推論セッションはチェックポイントからパラメータをレストアします。下のサンプルは 2 つのアプローチ間の主な違いを示します。

Before: 単一グラフ内の 3 つのモデルと単一セッションの共有

with tf.variable_scope('root'):
  train_inputs = tf.placeholder()
  train_op, loss = BuildTrainModel(train_inputs)
  initializer = tf.global_variables_initializer()

with tf.variable_scope('root', reuse=True):
  eval_inputs = tf.placeholder()
  eval_loss = BuildEvalModel(eval_inputs)

with tf.variable_scope('root', reuse=True):
  infer_inputs = tf.placeholder()
  inference_output = BuildInferenceModel(infer_inputs)

sess = tf.Session()

sess.run(initializer)

for i in itertools.count():
  train_input_data = ...
  sess.run([loss, train_op], feed_dict={train_inputs: train_input_data})

  if i % EVAL_STEPS == 0:
    while data_to_eval:
      eval_input_data = ...
      sess.run([eval_loss], feed_dict={eval_inputs: eval_input_data})

  if i % INFER_STEPS == 0:
    sess.run(inference_output, feed_dict={infer_inputs: infer_input_data})

After: 3 つのグラフ内の 3 つのモデル、3 つのセッションが同じ変数を共有する

train_graph = tf.Graph()
eval_graph = tf.Graph()
infer_graph = tf.Graph()

with train_graph.as_default():
  train_iterator = ...
  train_model = BuildTrainModel(train_iterator)
  initializer = tf.global_variables_initializer()

with eval_graph.as_default():
  eval_iterator = ...
  eval_model = BuildEvalModel(eval_iterator)

with infer_graph.as_default():
  infer_iterator, infer_inputs = ...
  infer_model = BuildInferenceModel(infer_iterator)

checkpoints_path = "/tmp/model/checkpoints"

train_sess = tf.Session(graph=train_graph)
eval_sess = tf.Session(graph=eval_graph)
infer_sess = tf.Session(graph=infer_graph)

train_sess.run(initializer)
train_sess.run(train_iterator.initializer)

for i in itertools.count():

  train_model.train(train_sess)

  if i % EVAL_STEPS == 0:
    checkpoint_path = train_model.saver.save(train_sess, checkpoints_path, global_step=i)
    eval_model.saver.restore(eval_sess, checkpoint_path)
    eval_sess.run(eval_iterator.initializer)
    while data_to_eval:
      eval_model.eval(eval_sess)

  if i % INFER_STEPS == 0:
    checkpoint_path = train_model.saver.save(train_sess, checkpoints_path, global_step=i)
    infer_model.saver.restore(infer_sess, checkpoint_path)
    infer_sess.run(infer_iterator.initializer, feed_dict={infer_inputs: infer_input_data})
    while data_to_infer:
      infer_model.infer(infer_sess)

後者のアプローチは分散バージョンに変換されるための「準備」ができていることに注意してください。

新しいアプローチの一つの他の違いは各 session.run 呼び出しでデータを供給するために feed_dicts を使用する (そして関連してバッチ処理、バケット処理、そしてデータの操作を遂行する) 代わりに、stateful iterator オブジェクトを使用します。これらの iterator は入力パイプラインを単一マシンと分散設定の両者において遥かにより簡単にします。次のセクションで (TensorFlow 1.2 で導入された) 新しい入力データパイプラインをカバーします。

 

データ入力パイプライン

TensorFlow 1.2 の前には、ユーザは TensorFlow 訓練と評価パイプラインにデータを供給するために 2 つのオプションを持ちました :

  1. 各訓練 session.run 呼び出しで feed_dict を通してデータを直接供給する。
  2. tf.train (e.g. tf.train.batch) と tf.contrib.train の queueing メカニズムを使用する。
  3. tf.contrib.learn or tf.contrib.slim のような高位フレームワークからのヘルパーを使用する (これは事実上 #2 を使用しています)。

最初のアプローチは TensorFlow に精通していないか、Python でのみ行える変わった入力変更を行なう (i.e. 彼ら自身のミニバッチ queueing) 必要があるユーザのためにより容易です。2 番目と 3 番目のアプローチはより標準的ですが少し柔軟性に欠けます : それらは複数の python スレッドを開始することも要求します (queue runners)。更に、誤って使用される場合、キューはデッドロックか不明瞭なエラーメッセージに繋がります。それにもかかわらず、キューは feed_dict を使用するよりも著しくより効率的で単一マシンと分散訓練の両者に対して標準的です。

TensorFlow 1.2 からは、データを TensorFlow モデルに読み込むために利用可能な新しいシステムがあります : tf.data モジュールで見つかる、データセット iterator です。データ iterator は柔軟で、それについて考えて操作することが容易で、TensorFlow C++ ランタイムを利用して効率性とマルチスレッドを提供します。

dataset はバッチデータ Tensor、ファイル名、または複数のファイル名を含む Tensor から作成できます。幾つかのサンプルです :

# Training dataset consists of multiple files.
train_dataset = tf.data.TextLineDataset(train_files)

# Evaluation dataset uses a single file, but we may
# point to a different file for each evaluation round.
eval_file = tf.placeholder(tf.string, shape=())
eval_dataset = tf.data.TextLineDataset(eval_file)

# For inference, feed input data to the dataset directly via feed_dict.
infer_batch = tf.placeholder(tf.string, shape=(num_infer_examples,))
infer_dataset = tf.data.Dataset.from_tensor_slices(infer_batch)

総ての dataset は入力処理を通して同様に扱えます。これはデータの読み込みとクリーンアップ、(訓練と評価の場合) バケット処理、フィルタリング、そしてバッチ処理を含みます。

各センテンスを単語文字列のベクトルに変換するため、例えばですが、dataset map 変換を使用します :

dataset = dataset.map(lambda string: tf.string_split([string]).values)

それから各センテンス・ベクトルをベクトルとその動的長さを含むタプルに切り替えることができます :

dataset = dataset.map(lambda words: (words, tf.size(words))

最後に、各センテンス上で語彙検索を遂行することができます。検索テーブル・オブジェクト・テーブルが与えられる場合、この map は最初のタプル要素を文字列ベクトルから整数ベクトルに変換します。

dataset = dataset.map(lambda words, size: (table.lookup(words), size))

2 つのデータセットの結合もまた簡単です。もし 2 つのファイルが互いの line-by-line 変換を含みそして各々一つがそれ自身のデータセットに読み込まれる場合、zipped lines のタプルを含む新しい dataset が以下を通して作成されます :

source_target_dataset = tf.data.Dataset.zip((source_dataset, target_dataset))

可変長のセンテンスのバッチ処理もストレートです。次の変換は source_target_dataset から batch_size 要素をバッチ処理し、各バッチでソースとターゲット・ベクトルをそれぞれ最も長いソースとターゲット・ベクトルの長さにパディングします。

batched_dataset = source_target_dataset.padded_batch(
        batch_size,
        padded_shapes=((tf.TensorShape([None]),  # source vectors of unknown size
                        tf.TensorShape([])),     # size(source)
                       (tf.TensorShape([None]),  # target vectors of unknown size
                        tf.TensorShape([]))),    # size(target)
        padding_values=((src_eos_id,  # source vectors padded on the right with src_eos_id
                         0),          # size(source) -- unused
                        (tgt_eos_id,  # target vectors padded on the right with tgt_eos_id
                         0)))         # size(target) -- unused

この dataset から吐かれた値はネストされたタプルでその tensor はサイズ batch_size の一番左の次元を持ちます。その構造は :

  • iterator[0][0] はバッチ化されてパディングされたソース・センテンス行列を持ちます。
  • iterator[0][1] はバッチ化されたソース・サイズ・ベクトルを持ちます。
  • iterator[1][0] はバッチ化されてパディングされたターゲット・センテンス行列を持ちます。
  • iterator[1][1] はターゲット・サイズ・ベクトルを持ちます。

最後に、同じようなサイズのソース・センテンスを一緒にバッチ化するバケット処理もまた可能です。
より詳細と完全な実装のためにはファイル utils/iterator_utils.py を見てください。

Dataset からデータを読むにはコードの 3 行が必要です : iterator を作成し、その値を取得して、そしてそれを初期化します。

batched_iterator = batched_dataset.make_initializable_iterator()

((source, source_lengths), (target, target_lengths)) = batched_iterator.get_next()

# At initialization time.
session.run(batched_iterator.initializer, feed_dict={...})

iterator がひとたび初期化されれば、ソースまたはターゲット・テンソルにアクセスする総ての session.run 呼び出しは基礎的な dataset から次のミニバッチを要求するでしょう。

 

より良い NMT モデルのための他の詳細

Bidirectional RNN

エンコーダ側の双方向性は一般的に (より多くの層が使用されるので何某かの速度の低下を伴い) 良いパフォーマンスを与えます。ここでは単一の bidirectional 層を持つエンコーダをどのように構築するかの単純化されたサンプルを与えます :

# Construct forward and backward cells
forward_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
backward_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)

bi_outputs, encoder_state = tf.nn.bidirectional_dynamic_rnn(
    forward_cell, backward_cell, encoder_emb_inp,
    sequence_length=source_sequence_length, time_major=True)
encoder_outputs = tf.concat(bi_outputs, -1)

変数 encoder_outputs と encoder_state はエンコーダのセクションと同じ方法で使用できます。マルチ bidirectional 層のためには、encoder_state を少し操作する必要があることに注意してください、より詳細のためには model.py, メソッド _build_bidirectional_rnn() を見てください。

 

ビーム・サーチ

greedy デコーディングが非常に合理的な翻訳品質を与える一方で、ビーム・サーチ・デコーダはパフォーマンスを更にブーストします。ビーム・サーチの考えは、翻訳時にトップ候補の小さなセット回りを保持することにより総ての可能な翻訳の検索空間をより良く探検することです。ビームのサイズはビーム幅 (= beam width) と呼ばれます : 例えばサイズ 10 の最小のビーム幅で一般的には十分です。更なる情報のためには、読者は Neubig, (2017) のセクション 7.2.3 を参照してください。ここにはビーム・サーチがどのように成されるかのサンプルがあります :

# Replicate encoder infos beam_width times
decoder_initial_state = tf.contrib.seq2seq.tile_batch(
    encoder_state, multiplier=hparams.beam_width)

# Define a beam-search decoder
decoder = tf.contrib.seq2seq.BeamSearchDecoder(
        cell=decoder_cell,
        embedding=embedding_decoder,
        start_tokens=start_tokens,
        end_token=end_token,
        initial_state=decoder_initial_state,
        beam_width=beam_width,
        output_layer=projection_layer,
        length_penalty_weight=0.0)

# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)

デコーダのセクションと同様に、同じ dynamic_decode() API 呼び出しが使用されることに注意してください。ひとたびデコードされれば、次のように翻訳にアクセスできます :

translations = outputs.predicted_ids
# Make sure translations shape is [batch_size, beam_width, time]
if self.time_major:
   translations = tf.transpose(translations, perm=[1, 2, 0])

更なる詳細のためには model.py, メソッド _build_decoder() を見てください。

 

ハイパーパラメータ

追加のパフォーマンスに繋がることができる幾つかのハイパーパラメータがあります。ここでは、私達獅子にの経験に基づいて幾つかをリストします [ Disclaimers: 他の人は私達が書いたことに同意しないかもしれません! ]。

Optimizer: Adam が “unfamiliar” アーキテクチャに対して合理的な結果に繋がることができる一方で、もし SGD で訓練可能であればスケジューリングを伴う SGD は一般的により良いパフォーマンスに繋がるでしょう。

Attention: Bahdanau-スタイル attention は、上手く動作するためにはしばしばエンコーダ側で双方向性を要求します ; その一方で Luong-スタイル attention は異なる設定のために上手く動作する傾向があります。このチュートリアル・コードのためには、Luong & Bahdanau スタイル attention の 2 つの改良種を使用することを推奨します : scaled_luong & normed bahdanau です。

 

マルチ-GPU 訓練

NMT モデルの訓練は数日間かかるかもしれません。異なる RNN 層を異なる GPU 上に置けば訓練スピードを改善できます。マルチ GPU 上に RNN 層を作成するサンプルがここにあります。

cells = []
for i in range(num_layers):
  cells.append(tf.contrib.rnn.DeviceWrapper(
      tf.contrib.rnn.LSTMCell(num_units),
      "/gpu:%d" % (num_layers % num_gpus)))
cell = tf.contrib.rnn.MultiRNNCell(cells)

更に、勾配計算を並列化するためには tf.gradients で colocate_gradients_with_ops オプションを有効にする必要があります。attention ベースの NMT モデルのスピード改良は GPU 数の増加につれて非常に小さいことに気がつくかもしれません。標準的な attention アーキテクチャの一つの大きな欠点は各 time ステップで attention に query するためにトップ (最終) 層の出力を使用することです。これは各デコーディング・ステップはその前のステップが完全に終了するのを待たなければならないことを意味します : それ故に、RNN 層をマルチ GPU 上に単純に置くことではデコーディング・プロセスを並列化できません。

GNMT attention アーキテクチャ は attention に query するためにボトム (最初の) 層の出力を使用してデコーダの計算を並列化します。従って、各デコーディング・ステップはその前の最初の層と attention 計算が終了すればすぐに開始できます。そのアーキテクチャを tf.contrib.rnn.MultiRNNCell のサブクラス、GNMTAttentionMultiCell で実装しました。GNMTAttentionMultiCell でどのようにデコーダ・セルを作成するかのサンプルがここにあります。

cells = []
for i in range(num_layers):
  cells.append(tf.contrib.rnn.DeviceWrapper(
      tf.contrib.rnn.LSTMCell(num_units),
      "/gpu:%d" % (num_layers % num_gpus)))
attention_cell = cells.pop(0)
attention_cell = tf.contrib.seq2seq.AttentionWrapper(
    attention_cell,
    attention_mechanism,
    attention_layer_size=None,  # don't add an additional dense layer.
    output_attention=False,)
cell = GNMTAttentionMultiCell(attention_cell, cells)

 

5 章. ベンチマーク

 
(訳注: ベンチマークについては原文の該当セクション: Benchmarks を参照してください。)

 
以上






AI導入支援 #2 ウェビナー

スモールスタートを可能としたAI導入支援   Vol.2
[無料 WEB セミナー] [詳細]
「画像認識 AI PoC スターターパック」の紹介
既に AI 技術を実ビジネスで活用し、成果を上げている日本企業も多く存在しており、競争優位なビジネスを展開しております。
しかしながら AI を導入したくとも PoC (概念実証) だけでも高額な費用がかかり取組めていない企業も少なくないようです。A I導入時には欠かせない PoC を手軽にしかも短期間で認知度を確認可能とするサービの紹介と共に、AI 技術の特性と具体的な導入プロセスに加え運用時のポイントについても解説いたします。
日時:2021年10月13日(水)
会場:WEBセミナー
共催:クラスキャット、日本FLOW(株)
後援:働き方改革推進コンソーシアム
参加費: 無料 (事前登録制)
人工知能開発支援
◆ クラスキャットは 人工知能研究開発支援 サービスを提供しています :
  • テクニカルコンサルティングサービス
  • 実証実験 (プロトタイプ構築)
  • アプリケーションへの実装
  • 人工知能研修サービス
◆ お問合せ先 ◆
(株)クラスキャット
セールス・インフォメーション
E-Mail:sales-info@classcat.com