Seq2Seq まとめ

以前作った Seq2Seq を利用した chatbot はゆるやかに改良中なのだが、進捗はあまり良くない。学習の待ち時間は長く暇だし、コード自体も拡張性が低い。そういうわけで最新の Tensorflow のバージョンで書き直そうと思って作業を始めた。しかし深掘りしていくと Seq2Seq の詳細を分かっていなかったことが発覚したのでここにまとめる。間違いを見つけたらコメントか @higepon まで。

Seq2Seq のすべてを解説するのではなく、Tensoflow/nmt/README.mdチュートリアルをベースにする。読んだだけでは、理解できなかった部分を補っていく形で進める。

必要とされる前提知識

  • DNN の基礎。構造、training、 loss とかそういう話。back prop は別に理解できなくても可。
  • RNN の基礎。RNN が時系列の扱いに向いているとか。RNN の構造を unfold した遷移図が理解できていればよい(ここを読むと良い)
  • Word Embedding。単語をベクトルとして表現みたいな話。

Seq2Seq のおおよその構造と罠

以下の図は Tensoflow/nmtREADME.md から持ってきた。翻訳で使われている Seq2Seq の図。

I am a student を Encoder に通すと、その文を言語に依存しないベクトル表現にしてくれる(図の真ん中)。それを Decoder に渡すと Je suis étudiant という特定の言語に翻訳してくれると説明されることが多い。Training 時には Encoder と Decoder のパラメータを学習していくことになる。Seq2Seq ライブラリを使う立場のプログラマから見ると、翻訳のペアの学習データさえ用意すれば Seq2Seq はブラックボックスとして使える。


はずなのだが、そんなにうまくいかない。


なぜかというと以下のような詳細質問に答えられず。実装やチューニングが難しいのだ。

  • RNN の亜種たちのうちどれを使うのだろう? LSTM? GRU。違いはなんだろうか。
  • hyper parameters は具体的に何を意味するのだろう。RNN の hidden state。Embedding のサイズ。Vocabulary 数?。
  • Attention が良いらしいが何が違うの?
  • Beam Search はパフォーマンスが良いらしい。それはどこのレイヤの話?
  • 具体的な loss は何と何を比較してるの?
  • 強化学習を導入するときに Rewards はどのように計算して、どのように与えるのだろうか?
  • bucketing って何?

Seq2Seq の詳細


Seq2Seq の詳細を上の図から理解していこう。ここでは筆者の「理解できたふり」を防ぐために各 input / output などの shape をごまかさずに書いていく。またデータは batch size は省いて 1 つのデータのみを扱う。また を扱うので、どういう順番で処理されていくのか番号を振っていく。


さて Training 済の Seq2Seq モデルがあるとして “I am a student” を入力して翻訳結果を得るフローを見ていこう。

Embedding layer への入力

図の (1) の矢印の部分。単語 “I” を学習済みのモデルの Embedding lookup でベクトル表現に変換する。つまり “I” => [0.1, -0.0045, 0.79, …, 0.45] のように変換する。変換後のベクトルを emb_input とする。shape は [emb_size] である。embedding の空間上では意味が近いと距離も近い。例えば “I” と “We” は近いけど “I” と “Apple” は遠いみたいなイメージ。

hidden layer 1 への入力

図の (2) の部分。青い四角形は RNN の state だと思うと分かりやすい。つまり emb_input を入力されると layer 1 の enc_state[1] が S11 に更新される。enc_state[1] の shape はRNN初期化時に指定したパラメータ。tensorflow なら num_units を使って [num_units] 。この enc_state[1] は “I” という単語が入力されたよと覚えておくイメージ。

hidden layer 2 への入力

ほぼ同じ。図の (3) の部分。enc_state[1] (=S11) の値は入力して enc_state[2] が S21 に更新される。これも同じく “I” という単語が入力されたよと覚えておくものだけど、きっともっと抽象度の高い何かを覚えている気がする。(要確認)。

ここまで起きたこと

Encoder に “I” を入力したら layer 1, 2 の enc_state が変わった。Encoderが “I” を受け取ったことを覚えたイメージ。ここまでが入力の1ステップ。

"am" を入力

次のサイクルでは “am” を Encoder に入力する。ポイントは Encoder は以前に “I” を受け取ったことを覚えていることだ。これを実現しているのが緑の (5') と (6') 部分。順番に見ていこう。

  • (4) で “am” をベクトル表現に変換
  • (5) で “am” のベクトル表現を渡す
  • (5') で enc_state[1] = S11 を入力として RNN に渡す
  • (5) と (5') を利用して enc_state[l] = S12 に更新。S12 は以前 “I” を受け取って次に “am” を受け取ったと記憶しているイメージ。
  • (6) と (6’)も同様に state[2] を S22 に更新する。
“a” と “student” も同様

“am" と全く同じ方法で “a” と “student” を入力する。入力後 Encoder の状態は enc_state[1] = S14, enc_state[2] = S24 となっているはずである。この enc_state[1] と enc_state[2] が以前出てきた、言語に依存しない “I am a student” のベクトル表現である。

decoder プロセスの開始

まず Decoderに <s> (decode を開始せよという意味)の入力 (A) をすると (B) が出力される。(B) の shape は [emb_size] である。この emb_input と Encoder の enc_state[1] つまり (B’) も RNN に入力されて、decoder の dec_state[1] が S_dec11 に更新される。ここで左側の Encoder の RNN と右側の Decoder は独立していることを思い出そう。なので S_dec11 のように layer 1 の t=1 での state のような表記をしている。同様に (C) と (C’) つまりS_dec11と S24 から dec_state[2] が S_dec12 に更新される。

projection layer

(C) と (C’) の入力により dec_state[2] が S_dec12 となった。これを (D) として projection layer に入力すると、ようやく一文字目の翻訳結果 “Je” (E) がでてくる。projection layer は RNN の state (=S_dec12) を project して vocabulary 空間の logits に変換する。より具体的には [vocab_size] のベクトルに変換する。そのベクトルに argmax すれば “Je” が出てくる。

次の翻訳結果を得る

(E) で得た "Je “ を今度は、また Decoder に入力 (F) する。そして全く同様に Decoder の state を更新しつつ projection layer から “suis" がでてくる。これをまた Decoder に入力する。というのを繰り返して projection layer から </s> (= decode 終わり)の合図が出たらおしまいである。

Training

さてここまでは Training 済みのモデルを利用しての翻訳の流れだったが Training はどうするのだろう。トレーニング時は “I am a student” <=> "Je suis étudiant" というペアが学習データとしてある。"I am a student" を順々に encoder に入力後、 decoderに <s> を入力して projection layer で出力されたものを “Je” と loss を計算。
このあと2つやり方があって

  • Je を入力して出てきたものを “suis” と loss 計算
  • もしくは
  • projection layer で出力されたものを入力して出てきたものを “suis” と loss 計算

の2つがあるみたい。

nmt 解説

長かったけどこれを踏まえて、tensoflow/nmt の基礎コードを解読していく。

placeholders
  • encoder_inputs [max_encoder_time, batch_size]: source input words.
    • これは encoder_input の batch 。 encoder_input は [max_encoder_time] でボキャブラリ辞書の index 列として表現されている
  • decoder_inputs [max_decoder_time, batch_size]: target input words.
    • Decoder に<s> からはじまる入力を入れるための placeholder
  • decoder_outputs [max_decoder_time, batch_size]: target output words, these are decoder_inputs shifted to the left by one time step with an end-of-sentence tag appended on the right.
    • 英語でも書いてあるが decoder_input を左にシフトして </s> が含まれるもの
Embedding
# 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_encoder は encoder の embedding_layer をパラメータを学習するもの。encoder_emp_inp は図の embedding layer から hidden layer1 に渡される embedding vector の列。 RNN には1つずつ渡されるがここではまとまって扱われている。

Encoder
# Build RNN cell
# encoder 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 する
# encoder_state は最終 state
# encoder_outputs は各入力に対する state の列
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
    encoder_cell, encoder_emb_inp,
    sequence_length=source_sequence_length, time_major=True)
Decoder
# Build RNN cell
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Helper
# この Helper を入れ替えることで Beam search とかにできる
# decoder_lengths は target の長さ
# TrainingHelper は上記の「Je を入力して出てきたものを “suis” と loss 計算」をするみたい
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
Projection Layer
## ただの bias の fully connected layer
projection_layer = layers_core.Dense(
    tgt_vocab_size, use_bias=False)
Loss

ここがちょっとわかりづらいのだけど logits がいまの model から出てきた翻訳結果で、decoder_outputs が正解。

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

おまけ

Beam Search

Beam Search は prediction 時に projection layer から出てきた logits の argmax を取ってつなぐのではなくて確率の積が最大化するものを選ぶ仕組み。

Attention

Attention は RNN の改良。いま対象の time=t を処理しているときにどこに attention を持つべきかを指示できる。翻訳の例だと I と Je のように関連のある単語に decoder 側の RNN が attention を向けられるようになるイメージ。(要確認)。

bucketing

実際の学習データは長さにばらつきがあるため、長さのグループ(bucket) に分けて、グループごとに学習する仕組み。

強化学習

Projection layer で生成されたものに対して rewards を計算して logits と掛け合わせたものを最大化するようにすれば良いと思う。