ホーム » Sequence to Sequence

Sequence to Sequence」カテゴリーアーカイブ

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 を参照してください。)

 
以上






TensorFlow : Tutorials : Sequence-to-Sequence モデル

TensorFlow : Tutorials : Sequence-to-Sequence モデル (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 09/15/2017; 10/10/2016, 09/04/2016
作成日時 : 02/24/2016

* 本ページは、TensorFlow 本家サイトの Tutorials – Sequence-to-Sequence Models を翻訳した上で
適宜、補足説明したものです:

* 本ページは、TensorFlow の本家サイトの Tutorials – Sequence-to-Sequence Models を翻訳した上で
適宜、補足説明したものです:
    https://www.tensorflow.org/versions/master/tutorials/seq2seq/index.html#sequence-to-sequence-models
* サンプルコードの動作確認はしておりますが、適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 
本文

既に RNN チュートリアル で議論したように、リカレント・ニューラルネットワークは言語のモデル化を学習できます。(もしまだ読んでいないのであれば、これを進める前に一読してください。) これは興味深い質問を引き起こします: ある入力に対して生成された単語を条件として、意味のあるレスポンスを生成できるでしょうか? 例えば、ニューラルネットワークを英語からフランス語に翻訳するために訓練することができるでしょうか? 答えは yes であることが判明しています。

このチュートリアルはそのようなシステムをどのように構築して訓練するかを端から端まで貴方に示します。貴方が pip パッケージを通してインストール済みで、tensorflow git レポジトリをクローンし、そして git ツリーのルートにいることを仮定しています。

そして翻訳プログラムを実行することで開始できます:

cd tensorflow/models/rnn/translate
python translate.py --data_dir [your_data_directory]

それは WMT’15 Web サイト から 英語フランス語翻訳データをダウンロードして訓練のための準備をして、そして訓練します。それは約 20 GB のディスク容量を取り、そしてダウンロードと準備(詳細は後で)にしばらくの時間がかかりますので、貴方はこのチュートリアルを読みながら始めて実行したままにできます。

このチュートリアルは models/rnn からの次のファイルを参照します。

ファイル

What’s in it? (内容)

seq2seq.py

sequence-to-sequence モデルを構築するライブラリ。

translate/seq2seq_model.py

ニューラル翻訳 sequence-to-sequence モデル。

translate/data_utils.py

翻訳データを準備するためのヘルパー関数。

translate/translate.py

翻訳モデルを訓練して実行するバイナリ。
 

Sequence-to-Sequence 基本

基本的な sequence-to-sequence モデルは、Cho et al., 2014 (pdf)、で紹介されているように、2つのリカレント・ニューラルネットワーク (RNN) から成ります: 入力を扱う エンコーダ と出力を生成する デコーダ です。この基本的なアーキテクチャは下に描かれます。

上の図の各ボックスは RNN のセルを表しています、より一般的には GRU セルまたは LSTM セルです (それらの説明については RNN チュートリアル を参照)。エンコーダとデコーダは重みを共有することができますがあるいは、より一般的に、異なるパラメータセットを使用することもできます。多層セルは sequence-to-sequence モデルでも成功的に使用されています。例えば翻訳のために Sutskever et al., 2014 (pdf)。

上に描かれた基本モデルでは、デコーダに渡される唯一のものとして、全ての入力は固定サイズの状態ベクタにエンコードされなければなりません、デコーダにより直接的な入力へのアクセスを可能にするため、attention メカニズムが Bahdanau et al., 2014 (pdf) で紹介されました。私たちは attention メカニズムの詳細には踏み込みませんが(論文参照)、それはデコーダに全てのデコーディング・ステップで入力をピークすることを許可するとだけ言っておけば十分でしょう。LSTM セルと attention メカニズムによる多層 sequence-to-sequence ネットワークはこのように見えます。

 

TensorFlow seq2seq ライブラリ

上で見たように、多くの異なる sequence-to-sequence があります。これらのモデルの各々は異なる RNN セルを使用することができますが、これら全てはエンコーダ入力とデコーダ入力を受容します。これは TensorFlow seq2seq ライブラリ (models/rnn/seq2seq.py) のインターフェイスへの興味を起こさせます。基本的な RNN encoder-decoder sequence-to-sequence モデルは次のように動作します。

outputs, states = basic_rnn_seq2seq(encoder_inputs, decoder_inputs, cell)

上の呼び出しで、encoder_inputs はエンコーダへの入力を表すテンソルのリストです、すなわち、上の最初の図の文字 A, B, C に相当します。同様に、decoder_inputs はデコーダへの入力を表すテンソルで、最初の図の GO, W, X, Y, Z に相当します。

cell 引数は クラスのインスタンスで、どのセルがモデル内部で使用されるかを決定します。GRUCell または LSTMCell のような既存のセルを使用できますし、あるいは貴方自身のものを書くこともできます。更に、rnn_cell は多層セルを構築し、セル入力または出力にドロップアウトを追加し、あるいは他の変換を行なうためのラッパーを提供します。例としては RNN チュートリアル を見てください。

basic_rnn_seq2seq への呼び出しは2つの引数を返します: outputsstates です。両方とも decoder_inputs と同じ長さのテンソルのリストです。もちろん、outputs は各時間ステップのデコーダの出力に相当します。上の最初の図ではそれは W, X, Y, Z, EOS でした。返された states は全ての時間ステップにおけるデコーダの内部状態を表します。

sequence-to-sequence モデルの多くのアプリケーションでは、時刻 t におけるデコーダの出力はフィードバックされ、時刻 t+1 におけるデコーダの入力になります。テスト時、シークエンスをデコードする時、このようにシークエンスが構築されます。訓練時には、他方、デコーダが前にミスをしていた場合でも一般に全ての時間ステップで正しい入力をデコーダに提供します。seq2seq.py の関数は feed_previous 引数を使って両方のモードをサポートします。例えば、埋め込み RNN モデルの次の用法を解析しましょう。

outputs, states = embedding_rnn_seq2seq(
    encoder_inputs, decoder_inputs, cell,
    num_encoder_symbols, num_decoder_symbols,
    output_projection=None, feed_previous=False)

embedding_rnn_seq2seq では、全ての入力は(both encoder_inputs と decoder_inputs 両方について)離散した値を表す整数テンソルです。これらは密な表現に埋め込まれます(埋め込みについての詳細は ベクタ表現チュートリアル を参照)、しかしこれらの埋め込みを構築するには、次のように現れる離散シンボルの最大値を指定する必要があります: エンコーダ側の num_encoder_symbols とデコーダ側の num_decoder_symbols です。

上記の呼び出しでは、feed_previous を False に設定しました。これはデコーダが decoder_inputs テンソルを提供されたものとして使用することを意味しています。もし feed_previous を True に設定した場合には、デコーダは decoder_inputs の最初の要素だけを使います。このリストからの他の全てのテンソルは無視され、代わりにエンコードの前の出力が使用されます。これは、私たちの翻訳モデルで翻訳をデコードするために使われますが、モデルを自身の誤りに対して堅固にするために、訓練時にもまた使うことができます。Bengio et al., 2015 (pdf) と同様です。

上で使われているもう一つの重要な引数は output_projection です。指定されない場合は、埋め込みモデルの出力は、各々の生成されたシンボルのロジットを表す、バッチ・サイズ x num_decoder_symbols の shape のテンソルになります。大規模出力語彙でモデルを訓練する時、すなわち num_decoder_symbols が大きい時、これらの大規模なテンソルをストアするのは実践的ではありません。代わりに、より小さな出力テンソルを返すのが良いです。これらは output_projection を使って後で大規模出力テンソルに射影されます。これは Jean et. al., 2014 (pdf) に記述されているように、私たちの seq2seq モデルを sampled softmax 損失とともに使うことを可能にします。

basic_rnn_seq2seq と embedding_rnn_seq2seq に加えて、seq2seq.py には2、3の更なる sequence-to-sequence モデルがありますので、見てください。それらは全て同様のインターフェイスを持ちますので、詳細は説明しません。下の翻訳モデルのためには embedding_attention_seq2seq を使います。

 

ニューラル翻訳モデル

sequence-to-sequence モデルの核が models/rnn/seq2seq.py の関数で構築される一方で、言及するに値する2、3のトリックがまだあります。それは models/rnn/translate/seq2seq_model.py における翻訳モデルで使われます。

Sampled softmax と出力射影 (projection)

上で既に述べたような理由で、大規模出力語彙を処理するために sampled softmax を使いたいです。それからデコードするために、出力射影を追跡する必要があります。sampled softmax 損失と出力射影の両者は seq2seq_model.py の次のコードにより構築されます。

  if num_samples > 0 and num_samples < self.target_vocab_size:
    w = tf.get_variable("proj_w", [size, self.target_vocab_size])
    w_t = tf.transpose(w)
    b = tf.get_variable("proj_b", [self.target_vocab_size])
    output_projection = (w, b)

    def sampled_loss(inputs, labels):
      labels = tf.reshape(labels, [-1, 1])
      return tf.nn.sampled_softmax_loss(w_t, b, inputs, labels, num_samples,
                                        self.target_vocab_size)

最初に、サンプルの数 (デフォルト 512) がターゲット語彙サイズよりも小さい場合にのみ sampled softmax を構築することに注意してください。512 より小さい語彙のためには標準 softmax 損失だけを使うのはより良い考えでしょう。

そして、見て取れるように、出力射影を構築します。それはペアで、重み行列とバイアスベクタから成ります。使用された場合、rnn セルは shape batch-size x size の shape のベクタを返します、batch-size x target_vocab_size ではありません。ロジットを取り出すためには、seq2seq_model.py の 124-126 行目で行なわれているように、重み行列で乗算してバイアスを加算する必要があります。

if output_projection is not None:
  self.outputs[b] = [tf.matmul(output, output_projection[0]) +
                     output_projection[1] for ...]

Bucketing とパディング

sampled softmax に加えて、私たちの翻訳モデルはまた bucketing を使います、これは異なる長さの文を効率的に処理するための方法です。まず問題をクリアにしましょう。英語からフランス語に翻訳する時、入力として異なる長さ L1 の英語の文があるでしょう、そして出力として異なる長さの L2 のフランス語の文があります。英語の文は encoder_inputs として渡され、フランス語の文は(GO シンボルで prefix される)decoder_inputs として現れますから、原理上は英語とフランス語の文の長さの全てのペア (L1, L2+1) のために seq2seq モデルを作成すべきです。これは多くの非常に類似したサブグラフから成る巨大なグラフという結果になるでしょう。一方で、特殊な PAD シンボルで全ての文をパディングすることもできます。そうすればパディングされた長さのため、一つの seq2seq モデルだけが必要になります。しかしより短い文では私たちのモデルは非効率的となり、必要のない、多くの PAD シンボルをエンコードしてデコードすることになります。長さの全てのペアのためのグラフを構築することと単一の長さにパディングすることの間の妥協として、幾つかのバケツを使いそして個々の文をその長さを超えてバケツの長さにパディングします。

buckets = [(5, 10), (10, 15), (20, 25), (40, 50)]

このことは、もし入力が 3 トークンの英語文であり、そして対応する出力が 6 トークンのフランス語文である場合、これらは最初のバケツに入れられエンコーダ入力のために長さ 5 に、そしてデコーダ入力のためには長さ 10 にパディングされることを意味しています。もし8 トークンの英語文があり対応するフランス語文が 18 トークンである場合には、(10, 15) バケツにはフィットしませんので、(20, 25) バケツが使用されます。すなわち、英語文は 20 にパディングされ、フランス語文は 25 にパディングされます。

デコーダ入力を構築する時、特殊な GO シンボルを入力データに追加することを思い出してください。

これは seq2seq_model.py の get_batch() 関数で行なわれます、これはまた入力英文の反転もします。入力の反転はニューラル翻訳モデルに対して結果を改善することが Sutskever et al., 2014 (pdf) で示されています。すべてを理解するために、"I go." という文があり、入力として ["I", "go", "."] としてトークン分割されそして出力は "Je vais." という文で、["Je", "vais", "."] とトークン分割されていることを想像してください。それは (5, 10) バケツに入れられます、[PAD PAD "." "go" "I"] を表すエンコーダ入力と デコーダ入力 [GO "Je" "vais" "." EOS PAD PAD PAD PAD PAD] と共に。

 

Let's Run It

上述のモデルを訓練するためには、大規模な英仏コーパスが必要です。私たちは訓練のために WMT'15 Website からの10^9-英仏 コーパスを使用して、開発セットとして同じサイトから 2013 ニューステストを使用します。次のコマンドが実行された時、両方のデータセットが data_dir にダウンロードされて訓練が始まり、train_dir にチェックポイントが保存されます。

python translate.py
  --data_dir [your_data_directory] --train_dir [checkpoints_directory]
  --en_vocab_size=40000 --fr_vocab_size=40000

訓練コーパスを準備するためには約 18 GB のディスク空間を必要とし数時間かかります。それはアンパックされ、語彙ファイルは data_dir で作成され、そしてコーパスはトークン分割されて整数 id に変換されます。語彙サイズを決定するパラメータに注意してください。上の例では、40 K のもっとも一般的なもの以外の全ての単語は未知の単語を表す UNK トークンに変換されます。もし語彙サイズを変更するのであれば、バイナリはコーパスをトークン-id に再度マップし直します。

データが準備された後、訓練が始まります。translate(.py) のデフォルト・パラメータは非常に大きな値に設定されています。長時間かけて訓練されたラージ・モデルは良い結果を与えますが、時間がかかり過ぎたり貴方の GPU のメモリを消費し過ぎたりします。次の例のようにより小さなモデルを訓練するように要求することもできます。

python translate.py
  --data_dir [your_data_directory] --train_dir [checkpoints_directory]
  --size=256 --num_layers=2 --steps_per_checkpoint=50

上のコマンドは 2 層(デフォルトは 3 層)、各層は 256 ユニット(デフォルトは 1024)のモデルを訓練して、50 ステップ毎にチェックポイントを保存します(デフォルトは 200)。貴方はこれらのパラメータと遊ぶことでどの程度の大きさのモデルが貴方の GPU メモリに適合することができるか見つけられます。訓練の間、steps_per_checkpoint ステップ毎にバイナリは新しいステップから統計情報をプリントアウトします。デフォルト・パラメータ(サイズ 1024 の 3 層)では、最初のメッセージはこのようなものです。

global step 200 learning rate 0.5000 step-time 1.39 perplexity 1720.62
  eval: bucket 0 perplexity 184.97
  eval: bucket 1 perplexity 248.81
  eval: bucket 2 perplexity 341.64
  eval: bucket 3 perplexity 469.04
global step 400 learning rate 0.5000 step-time 1.38 perplexity 379.89
  eval: bucket 0 perplexity 151.32
  eval: bucket 1 perplexity 190.36
  eval: bucket 2 perplexity 227.46
  eval: bucket 3 perplexity 238.66

各ステップがちょうど 1.4 秒以下かかること、各バケツのための訓練セット上の perplexity と開発セット上の perplexity が見て取れます。約 30K ステップ後、短い文(バケツ 0 と 1)の perplexity が一桁に入ることを見ます。訓練コーパスは ~ 22M 文を含みますので、一つの epoch(訓練データを一巡すること) はバッチサイズ 64 で約 340K ステップかかります。この時点でモデルは --decode オプションで英語文をフランス語に翻訳するために使用することができます。

python translate.py --decode
  --data_dir [your_data_directory] --train_dir [checkpoints_directory]

Reading model parameters from /tmp/translate.ckpt-340000
>  Who is the president of the United States?
 Qui est le président des États-Unis ?
 

What Next?

上の例は貴方自身の英仏翻訳機をどのようにビルドするかを示しています、端から端まで。それを実行してモデルがどのように遂行するかを貴方自身で見てください。それは reasonable な品質を持つ一方で、デフォルト・パラメータはベストな翻訳モデルを提示してはいません。貴方が改良できる2、3のことがここにあります。

まず最初に、私たちは非常にプリミティブな tokenizer (トークン解析器) - data_utils の basic_tokenizer 関数を使用しています。より良い tokenizer は WMT'15 Web サイト で見つかります。その tokenizer、そしてより大規模な語彙を使用することは、翻訳を改良します。

また、翻訳モデルのデフォルト・パラメータはチューニングされていません。学習率、減衰を変更してみる、あるいはモデルの重みを違う方法で初期化してみることができます。また seq2seq_model.py のデフォルト GradientDescentOptimizer をより進んだもの、AdagradOptimizer のようなものに変更することもできます。これらのことにトライしてそれらがどのように結果を改善するか見てみましょう。

最後に、上で提示されたモデルは翻訳だけではなく、任意の sequence-to-sequence タスクに使用できます。例えば解析木を生成するためにシークエンスをツリーに変換することを望む場合、Vinyals & Kaiser et al., 2014 (pdf) で示されてるように、上と同じモデルが最高水準の結果を与えることができます。従って貴方自身の翻訳器をビルドできるだけでなく、解析器、チャットボット、あるいは思いつく任意のプログラムをビルドすることもできます。 Experiment!

 

以上

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