Keras : Ex-Tutorials : Seq2Seq 学習へのイントロ

Keras : Ex-Tutorials : Seq2Seq 学習へのイントロ (翻訳/解説)

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

* 本ページは、Keras 開発チーム推奨の外部チュートリアル・リソースの一つ : “A ten-minute introduction to sequence-to-sequence learning in Keras” を題材にしてまとめ直したものです:

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

 

sequence-to-sequence 学習

Sequence-to-Sequence 学習 (Seq2Seq) はある一つのドメインのシークエンス (e.g. 英語文) を他のドメインのシークエンス (e.g. フランス語に翻訳された同じ文) に変換するモデルやその訓練を指します :

"the cat sat on the mat" -> [Seq2Seq model] -> "le chat etait assis sur le tapis"

これは機械翻訳や自由形式の質問応答 (自然言語の質問が与えられたときに自然言語を生成します) のために使用できますが、より一般的には、テキストを生成する必要があるときはいつでも適用できます。このタスクを処理するためには RNN の使用や 1D ConvNet の使用など複数の方法がありますが、ここでは RNN をフィーチャーします。

 

自明なケース: 入力と出力シークエンスが同じ長さを持つ場合

入力シークエンスと出力シークエンスが同じ長さを持つとき、単純に Keras LSTM や GRU 層 (あるいはそのスタック) でそのようなモデルを実装できます。これは このサンプル・スクリプト の場合で、文字列としてエンコードされた、数字の加算を学習することをどのように RNN に教えるかを示します :

このアプローチの一つの注意点は、input[…t] が与えられたとき target[…t] を生成することが可能であることを仮定していることです。それは幾つかのケース (e.g. 数字の文字列の加算) では動作しますが、殆どのユースケースでは動作しません。一般的なケースでは、ターゲット・シークエンスを生成し始めるために入力シークエンス全体についての情報が必要です。

 

一般的なケース: 標準的な sequence-to-sequence

一般的なケース (e.g. 機械翻訳) では、入力シークエンスと出力シークエンスは異なる長さを持ち、ターゲットを予測し始めるために入力シークエンス全体が必要です。これが (それ以上のコンテキストを伴わずに) “sequence to sequence モデル” と言及するとき一般に参照されるものです。

これは次のように動作します :

  • 「エンコーダ」として機能する RNN 層 (or それのスタック): それは入力シークエンスを処理してそれ自身の内部状態を返します。エンコーダ RNN の出力は廃棄して、状態をリカバーするだけであることに注意してください。この状態は次のステップのデコーダの 「コンテキスト」または「条件 (= conditioning)」として役立ちます。
  • 「デコーダ」として機能するもう一つの RNN 層 (or それのスタック): ターゲット・シークエンスの前の文字が与えられたとき、ターゲット・シークエンスの次の文字を予測するために訓練されます。特に、それはターゲット・シークエンスを同じシークエンスに変えるように (未来の 1 タイムステップのオフセットで) 訓練され、これは (このコンテキストでは) “teacher forcing” と呼ばれる訓練プロセスになります。重要なこととして、エンコーダは初期状態としてエンコーダからの状態ベクトルを使用します、これがデコーダが生成しなければならないものについての情報をどのように得るかです。デコーダは targets[…t] が与えられたとき targets[t+1…] を生成することを効果的に学習します。

推論モードでは、i.e. 未知の入力シークエンスをデコードすることを望むとき、少し異なるプロセスを通ります :

  1. 入力シークエンスを状態ベクトルにエンコードします。
  2. サイズ 1 のターゲット・シークエンス (単なる start-of-sequence 文字) で開始します。
  3. 次の文字のための予測を生成するために状態ベクトルと 1-文字ターゲット・シークエンスをデコーダに供給します。
  4. これらの予測を使用して次の文字をサンプリングします (単純に argmax を使用します)。
  5. サンプリングされた文字をターゲット・シークエンスに追加します。
  6. end-of-sequence 文字を生成するか文字制限に到達するまで繰り返します。

デコーダの予測をデコーダに再注入することで、同じプロセスが “teacher forcing” なしの Seq2Seq ネットワークを訓練するためにも使用できます。

 

Keras サンプル

これらのアイデアを実際のコードで説明します。サンプル実装のために、英語文とそれらのフランス語翻訳のペアのデータセットを使用します、これは manythings.org/anki からダウンロード可能です。ダウンロードするファイルの名前は fra-eng.zip です。一文字ずつ入力を処理して一文字ずつ出力を生成する、文字レベル sequence-to-sequence モデルを実装します。もう一つの選択肢は単語レベル・モデルで、これは機械学習においてはより一般的な傾向にあります。この記事の最後で埋め込み層を使用してモデルを単語レベル・モデルに変更することについても言及します。

サンプルの完全なスクリプトは GitHub で見つかります

プロセスを要約すると次のようなものになります :

  1. センテンスを 3 つの Numpy 配列 encoder_input_data, decoder_input_data, decoder_target_data に変えます :
    • encoder_input_data は shape (num_pairs, max_english_sentence_length, num_english_characters) の 3D 配列で、英語文の one-hot ベクトル化を含みます。
    • decoder_input_data は shape (num_pairs, max_french_sentence_length, num_french_characters) の 3D 配列で、フランス語文の one-hot ベクトルを含みます。
    • decoder_target_data は 1 タイムステップのオフセット以外は decoder_input_data と同じです。decoder_target_data[:, t, :] は decoder_input_data[:, t + 1, :] と同じです。
  2. encoder_input_data と decoder_input_data が与えれたとき decoder_target_data を予測するための基本的な LSTM-ベースの Seq2Seq モデルを訓練します。私達のモデルは teacher forcing を使用します。
  3. モデルが動作しているか確認するために幾つかのセンテンスをデコードします (i.e. encoder_input_data からのサンプルを decoder_target_data からの対応するサンプルに変えます)。

訓練プロセスと推論プロセス (センテンスのデコーディング) は非常に異なるので、両者に対して異なるモデルを使用しますが、それらは同じ内部層を活用します。

以下が私達の訓練モデルで、Keras RNN の 3 つの主要な特徴を活用しています :

  • return_state コンストラクタ引数、最初のエントリが出力で次のエントリが内部 RNN 状態であるようなリストを返すために RNN 層を設定します。これはエンコーダの状態をリカバーするために使用されます。
  • inital_state 呼び出し引数、RNN の初期状態を指定します。これはエンコーダ状態をデコーダに初期状態として渡すために使用されます。
  • return_sequences コンストラクタ引数、出力の完全シークエンスを返すように RNN を設定します((デフォルト挙動である) 最後の出力を単に返す代わりにです)。これはデコーダで使用されます。
from keras.models import Model
from keras.layers import Input, LSTM, Dense

# 入力シークエンスを定義してそれを処理します。
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]

# デコーダをセットアップします、初期状態として `encoder_states` を使用します。
decoder_inputs = Input(shape=(None, num_decoder_tokens))
# デコーダを、完全な出力シークエンスを返し、内部状態もまた返すように設定します。
# 訓練モデルでは返された状態を使用しませんが、推論では使用します。
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs,
                                     initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

# `encoder_input_data` & `decoder_input_data` を `decoder_target_data` に変えるモデルを定義します。
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

モデルは 2 行で訓練できます。サンプルの 20 % の取り置いたセット上で損失を監視します。

# Run training
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=batch_size,
          epochs=epochs,
          validation_split=0.2)

MacBook CPU 上 1 時間かそこらの後で、推論のための準備ができます。テスト・センテンスをデコードするためには、以下を反復します :

  1. 入力センテンスをエンコードして初期デコーダ状態を取得します。
  2. この初期状態とターゲットとしての “start of sequence” トークンでデコーダの 1 ステップを実行します。出力は次のターゲット文字です。
  3. 予測されたターゲット文字を追加して繰り返します。

ここに推論のセットアップがあります :

encoder_model = Model(encoder_inputs, encoder_states)

decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
    decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
    [decoder_inputs] + decoder_states_inputs,
    [decoder_outputs] + decoder_states)

それを上で説明した推論ループを実装するために使用します :

def decode_sequence(input_seq):
    # Encode the input as state vectors.
    states_value = encoder_model.predict(input_seq)

    # Generate empty target sequence of length 1.
    target_seq = np.zeros((1, 1, num_decoder_tokens))
    # Populate the first character of target sequence with the start character.
    target_seq[0, 0, target_token_index['\t']] = 1.

    # Sampling loop for a batch of sequences
    # (to simplify, here we assume a batch of size 1).
    stop_condition = False
    decoded_sentence = ''
    while not stop_condition:
        output_tokens, h, c = decoder_model.predict(
            [target_seq] + states_value)

        # Sample a token
        sampled_token_index = np.argmax(output_tokens[0, -1, :])
        sampled_char = reverse_target_char_index[sampled_token_index]
        decoded_sentence += sampled_char

        # Exit condition: either hit max length
        # or find stop character.
        if (sampled_char == '\n' or
           len(decoded_sentence) > max_decoder_seq_length):
            stop_condition = True

        # Update the target sequence (of length 1).
        target_seq = np.zeros((1, 1, num_decoder_tokens))
        target_seq[0, 0, sampled_token_index] = 1.

        # Update states
        states_value = [h, c]

    return decoded_sentence

幾つかの素晴らしい結果を得ます — 訓練セットから取ったサンプルをデコードしていますので驚くには値しませんが。

Input sentence: Be nice.
Decoded sentence: Soyez gentil !
-
Input sentence: Drop it!
Decoded sentence: Laissez tomber !
-
Input sentence: Get out!
Decoded sentence: Sortez !

 

参考

 

追加の FAQ

LSTM の代わりに GRU 層を使用することを望む場合

それは実際には少しだけ単純です、というのは GRU は一つの状態だけを持つからで、LSTM は 2 つの状態を持ちます。以下に GRU 層を利用するためにどのように訓練モデルを適応させるかを示します :

encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = GRU(latent_dim, return_state=True)
encoder_outputs, state_h = encoder(encoder_inputs)

decoder_inputs = Input(shape=(None, num_decoder_tokens))
decoder_gru = GRU(latent_dim, return_sequences=True)
decoder_outputs = decoder_gru(decoder_inputs, initial_state=state_h)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

 

整数シークエンスを持つ単語レベル・モデルを使用することを望む場合

入力が整数シークエンスである場合はどうでしょう (e.g. 辞書のインデックスでエンコードされた単語のシークエンスで表現) ? 埋め込み層を通してこれらの整数トークンを埋め込むことができます。

次のようにします :

# Define an input sequence and process it.
encoder_inputs = Input(shape=(None,))
x = Embedding(num_encoder_tokens, latent_dim)(encoder_inputs)
x, state_h, state_c = LSTM(latent_dim,
                           return_state=True)(x)
encoder_states = [state_h, state_c]

# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None,))
x = Embedding(num_decoder_tokens, latent_dim)(decoder_inputs)
x = LSTM(latent_dim, return_sequences=True)(x, initial_state=encoder_states)
decoder_outputs = Dense(num_decoder_tokens, activation='softmax')(x)

# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

# Compile & run training
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
# Note that `decoder_target_data` needs to be one-hot encoded,
# rather than sequences of integers like `decoder_input_data`!
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=batch_size,
          epochs=epochs,
          validation_split=0.2)

 

訓練のために teacher forcing を使用することを望まない場合

幾つかの特定のケースでは teacher forcing を使用することができないかもしれません、何故ならば完全なターゲット・シークエンスへのアクセスを持たないからです、e.g. 非常に長いセンテンス上のオンライン訓練を行なっている場合、そこでは完全な入力-ターゲット・ペアをバッファリングすることは不可能でしょう。

出力再注入ループをハードコードしたモデルを構築することによりこれを達成できます :

from keras.layers import Lambda
from keras import backend as K

# The first part is unchanged
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
states = [state_h, state_c]

# Set up the decoder, which will only process one timestep at a time.
decoder_inputs = Input(shape=(1, num_decoder_tokens))
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')

all_outputs = []
inputs = decoder_inputs
for _ in range(max_decoder_seq_length):
    # Run the decoder on one timestep
    outputs, state_h, state_c = decoder_lstm(inputs,
                                             initial_state=states)
    outputs = decoder_dense(outputs)
    # Store the current prediction (we will concatenate all predictions later)
    all_outputs.append(outputs)
    # Reinject the outputs as inputs for the next loop iteration
    # as well as update the states
    inputs = outputs
    states = [state_h, state_c]

# Concatenate all predictions
decoder_outputs = Lambda(lambda x: K.concatenate(x, axis=1))(all_outputs)

# Define and compile model as previously
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

# Prepare decoder input data that just contains the start character
# Note that we could have made it a constant hard-coded in the model
decoder_input_data = np.zeros((num_samples, 1, num_decoder_tokens))
decoder_input_data[:, 0, target_token_index['\t']] = 1.

# Train model as previously
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=batch_size,
          epochs=epochs,
          validation_split=0.2)
 

以上