十分鐘搞定Keras序列到序列學習(附程式碼實現)

黃小天發表於2017-10-03
如何在 Keras 中實現 RNN 序列到序列學習?本文中,作者將嘗試對這一問題做出簡短解答;本文預設你已有一些迴圈網路和 Keras 的使用經驗。

GitHub:https://github.com/fchollet/keras/blob/master/examples/lstm_seq2seq.py

什麼是序列到序列學習?

序列到序列學習(Seq2Seq)是指訓練模型從而把一個域的序列(比如英語語句)轉化為另一個域的序列(比如法語中的對應語句)。

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

Seq2Seq 可用於機器翻譯或者省去問題回答——通常來講,它可以隨時生成文字。完成這一任務有很多方式,比如 RNN 或一維卷積。本文只介紹 RNN。

次要案例:當輸入序列和輸出序列長度相同

當輸入序列和輸出序列長度相同時,你可以通過 Keras LSTM 或者 GRU 層(或者其中的堆疊)簡單地實現模型。這一例項指令碼中的案例展示瞭如何教會 RNN 學習新增被編碼為字串的數字:

十分鐘搞定Keras序列到序列學習(附程式碼實現)

一般案例:標準的 Seq2Seq

一般情況下,輸入序列和輸出序列有不同的長度(比如機器翻譯)。這就需要一個更高階的設定,尤其在沒有進一步語境的「序列到序列模型」時。下面是其工作原理:

  • 一個 RNN 層(或其中的堆疊)作為「編碼器」:它處理輸入序列並反饋其內部狀態。注意我們拋棄了編碼器 RNN 的輸出,只恢復其狀態。該狀態在下一步中充當解碼器的「語境」。
  • 另一個 RNN 層作為「解碼器」:在給定目標序列先前字母的情況下,它被訓練以預測目標序列的下一個字元。具體講,它被訓練把目標序列轉化為相同序列,但接下來被一個時間步抵消,這一訓練過程在語境中被稱為「teacher forcing」。更重要的是,編碼器把其狀態向量用作初始狀態,如此編碼器獲得了其將要生成的資訊。實際上,在給定 targets[...t] 的情況下,解碼器學習生成 targets[t+1...],前提是在輸入序列上。

十分鐘搞定Keras序列到序列學習(附程式碼實現)

在推理模式中,即當要解碼未知的輸入序列,我們完成了一個稍微不同的處理:

  1. 把輸入序列編碼進狀態向量
  2. 從大小為 1 的目標序列開始
  3. 饋送狀態向量和 1 個字元的目標序列到解碼器從而為下一字元生成預測
  4. 通過這些預測取樣下一個字元(我們使用 argmax)
  5. 把取樣的字元附加到目標序列
  6. 不斷重複直至我們生成序列最後的字元或者達到字元的極限

十分鐘搞定Keras序列到序列學習(附程式碼實現)

相同的處理也可被用於訓練沒有「teacher forcing」的 Seq2Seq 網路,即把解碼器的預測再注入到解碼器之中。

一個 Keras 例項

讓我們用實際的程式碼演示一下這些想法。

對於例項實現,我們將使用一對英語語句及其法語翻譯的資料集,你可以從 http://www.manythings.org/anki/下載它,檔案的名稱是 fra-eng.zip。我們將會實現一個字元級別的序列到序列模型,逐個字元地處理這些輸入並生成輸出。另一個選擇是單詞級別的模型,它對機器學習更常用。在本文最後,你會發現通過嵌入層把我們的模型轉化為單詞級別模型的一些註釋。

這是例項的全部指令碼:https://github.com/fchollet/keras/blob/master/examples/lstm_seq2seq.py。

下面是這一過程的總結:

1. 把語句轉化為 3 個 Numpy 陣列 encoder_input_data、decoder_input_data、decoder_target_data:

  • encoder_input_data 是一個形態的 3D 陣列(num_pairs, max_english_sentence_length, num_english_characters),包含一個英語語句的獨熱向量化。
  • decoder_input_data 是一個形態的 3D 陣列(num_pairs, max_french_sentence_length, num_french_characters),包含一個法語語句的獨熱向量化。
  • decoder_target_data 與 decoder_input_data 相同,但是被一個時間步抵消。decoder_target_data[:, t, :] 與 decoder_input_data[:, t + 1, :] 相同。

2. 在給定 encoder_input_data 和 decoder_input_data 的情況下,訓練一個基本的基於 LSTM 的 Seq2Seq 模型以預測 decoder_target_data。我們的模型使用 teacher forcing。

3. 解碼一些語句以檢查模型正在工作。

由於訓練過程和推理過程(解碼語句)相當不同,我們使用了不同的模型,雖然兩者具有相同的內在層。這是我們的模型,它利用了 Keras RNN 的 3 個關鍵功能:

  • return_state 建構函式引數配置一個 RNN 層以反饋列表,其中第一個是其輸出,下一個是內部的 RNN 狀態。這被用於恢復編碼器的狀態。
  • inital_state 呼叫引數指定一個 RNN 的初始狀態,這被用於把編碼器狀態作為初始狀態傳遞至解碼器。
  • return_sequences 建構函式引數配置一個 RNN 反饋輸出的全部序列。這被用在解碼器中。
  1. from keras.models import Model

  2. from keras.layers import Input, LSTM, Dense

  3. # Define an input sequence and process it.

  4. encoder_inputs = Input(shape=(None, num_encoder_tokens))

  5. encoder = LSTM(latent_dim, return_state=True)

  6. encoder_outputs, state_h, state_c = encoder(encoder_inputs)

  7. # We discard `encoder_outputs` and only keep the states.

  8. encoder_states = [state_h, state_c]

  9. # Set up the decoder, using `encoder_states` as initial state.

  10. decoder_inputs = Input(shape=(None, num_decoder_tokens))

  11. # We set up our decoder to return full output sequences,

  12. # and to return internal states as well. We don't use the

  13. # return states in the training model, but we will use them in inference.

  14. decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)

  15. decoder_outputs, _, _ = decoder_lstm(decoder_inputs,

  16.                                     initial_state=encoder_states)

  17. decoder_dense = Dense(num_decoder_tokens, activation='softmax')

  18. decoder_outputs = decoder_dense(decoder_outputs)

  19. # Define the model that will turn

  20. # `encoder_input_data` & `decoder_input_data` into `decoder_target_data`

  21. model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

我們用這兩行程式碼訓練模型,同時在 20% 樣本的留存集中監測損失。

  1. # Run training

  2. model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

  3. model.fit([encoder_input_data, decoder_input_data], decoder_target_data,

  4.          batch_size=batch_size,

  5.          epochs=epochs,

  6.          validation_split=0.2)

大約 1 小時後在 MacBook CPU 上,我們已準備好做推斷。為了解碼測試語句,我們將重複:

  • 編碼輸入語句,檢索初始解碼器狀態。
  • 用初始狀態執行一步解碼器,以「序列開始」為目標。輸出即是下一個目標字元。
  • 附加預測到的目標字元並重復。

這是我們的推斷設定:

  1. encoder_model = Model(encoder_inputs, encoder_states)

  2. decoder_state_input_h = Input(shape=(latent_dim,))

  3. decoder_state_input_c = Input(shape=(latent_dim,))

  4. decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

  5. decoder_outputs, state_h, state_c = decoder_lstm(

  6.    decoder_inputs, initial_state=decoder_states_inputs)

  7. decoder_states = [state_h, state_c]

  8. decoder_outputs = decoder_dense(decoder_outputs)

  9. decoder_model = Model(

  10.    [decoder_inputs] + decoder_states_inputs,

  11.    [decoder_outputs] + decoder_states)

我們使用它實現上述推斷迴圈(inference loop):

  1. def decode_sequence(input_seq):

  2.    # Encode the input as state vectors.

  3.    states_value = encoder_model.predict(input_seq)

  4.    # Generate empty target sequence of length 1.

  5.    target_seq = np.zeros((1, 1, num_decoder_tokens))

  6.    # Populate the first character of target sequence with the start character.

  7.    target_seq[0, 0, target_token_index['\t']] = 1.

  8.    # Sampling loop for a batch of sequences

  9.    # (to simplify, here we assume a batch of size 1).

  10.    stop_condition = False

  11.    decoded_sentence = ''

  12.    while not stop_condition:

  13.        output_tokens, h, c = decoder_model.predict(

  14.            [target_seq] + states_value)

  15.        # Sample a token

  16.        sampled_token_index = np.argmax(output_tokens[0, -1, :])

  17.        sampled_char = reverse_target_char_index[sampled_token_index]

  18.        decoded_sentence += sampled_char

  19.        # Exit condition: either hit max length

  20.        # or find stop character.

  21.        if (sampled_char == '\n' or

  22.           len(decoded_sentence) > max_decoder_seq_length):

  23.            stop_condition = True

  24.        # Update the target sequence (of length 1).

  25.        target_seq = np.zeros((1, 1, num_decoder_tokens))

  26.        target_seq[0, 0, sampled_token_index] = 1.

  27.        # Update states

  28.        states_value = [h, c]

  29.    return decoded_sentence

我們得到了一些不錯的結果——這在意料之中,因為我們解碼的樣本來自訓練測試。

  1. Input sentence: Be nice.

  2. Decoded sentence: Soyez gentil !

  3. -

  4. Input sentence: Drop it!

  5. Decoded sentence: Laissez tomber !

  6. -

  7. Input sentence: Get out!

  8. Decoded sentence: Sortez !

這就是我們的十分鐘入門 Keras 序列到序列模型教程。完整程式碼詳見 GitHub:https://github.com/fchollet/keras/blob/master/examples/lstm_seq2seq.py。

常見問題

1. 我想使用 GRU 層代替 LSTM,應該怎麼做?

這實際上變簡單了,因為 GRU 只有一個狀態,而 LSTM 有兩個狀態。這是使用 GRU 層適應訓練模型的方法:

  1. encoder_inputs = Input(shape=(None, num_encoder_tokens))

  2. encoder = GRU(latent_dim, return_state=True)

  3. encoder_outputs, state_h = encoder(encoder_inputs)

  4. decoder_inputs = Input(shape=(None, num_decoder_tokens))

  5. decoder_gru = GRU(latent_dim, return_sequences=True)

  6. decoder_outputs = decoder_gru(decoder_inputs, initial_state=state_h)

  7. decoder_dense = Dense(num_decoder_tokens, activation='softmax')

  8. decoder_outputs = decoder_dense(decoder_outputs)

  9. model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

2. 我想使用整數序列的單詞級別模型,應該怎麼做?

如果你的輸入是整數序列(如按詞典索引編碼的單詞序列),你可以通過 Embedding 層嵌入這些整數標記。方法如下:

  1. # Define an input sequence and process it.

  2. encoder_inputs = Input(shape=(None,))

  3. x = Embedding(num_encoder_tokens, latent_dim)(encoder_inputs)

  4. x, state_h, state_c = LSTM(latent_dim,

  5.                           return_state=True)(x)

  6. encoder_states = [state_h, state_c]

  7. # Set up the decoder, using `encoder_states` as initial state.

  8. decoder_inputs = Input(shape=(None,))

  9. x = Embedding(num_decoder_tokens, latent_dim)(decoder_inputs)

  10. x = LSTM(latent_dim, return_sequences=True)(x, initial_state=encoder_states)

  11. decoder_outputs = Dense(num_decoder_tokens, activation='softmax')(x)

  12. # Define the model that will turn

  13. # `encoder_input_data` & `decoder_input_data` into `decoder_target_data`

  14. model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

  15. # Compile & run training

  16. model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

  17. # Note that `decoder_target_data` needs to be one-hot encoded,

  18. # rather than sequences of integers like `decoder_input_data`!

  19. model.fit([encoder_input_data, decoder_input_data], decoder_target_data,

  20.          batch_size=batch_size,

  21.          epochs=epochs,

  22.          validation_split=0.2)

3. 如果我不想使用「teacher forcing」,應該怎麼做?

一些案例中可能不能使用 teacher forcing,因為你無法獲取完整的目標序列,比如,線上訓練非常長的語句,則緩衝完成輸入-目標語言對是不可能的。在這種情況下,你要通過將解碼器的預測重新注入解碼器輸入進行訓練,就像我們進行推斷時所做的那樣。

你可以通過構建硬編碼輸出再注入迴圈(output reinjection loop)的模型達到該目標:

  1. from keras.layers import Lambda

  2. from keras import backend as K

  3. # The first part is unchanged

  4. encoder_inputs = Input(shape=(None, num_encoder_tokens))

  5. encoder = LSTM(latent_dim, return_state=True)

  6. encoder_outputs, state_h, state_c = encoder(encoder_inputs)

  7. states = [state_h, state_c]

  8. # Set up the decoder, which will only process one timestep at a time.

  9. decoder_inputs = Input(shape=(1, num_decoder_tokens))

  10. decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)

  11. decoder_dense = Dense(num_decoder_tokens, activation='softmax')

  12. all_outputs = []

  13. inputs = decoder_inputs

  14. for _ in range(max_decoder_seq_length):

  15.    # Run the decoder on one timestep

  16.    outputs, state_h, state_c = decoder_lstm(inputs,

  17.                                             initial_state=states)

  18.    outputs = decoder_dense(outputs)

  19.    # Store the current prediction (we will concatenate all predictions later)

  20.    all_outputs.append(outputs)

  21.    # Reinject the outputs as inputs for the next loop iteration

  22.    # as well as update the states

  23.    inputs = outputs

  24.    states = [state_h, state_c]

  25. # Concatenate all predictions

  26. decoder_outputs = Lambda(lambda x: K.concatenate(x, axis=1))(all_outputs)

  27. # Define and compile model as previously

  28. model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

  29. model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

  30. # Prepare decoder input data that just contains the start character

  31. # Note that we could have made it a constant hard-coded in the model

  32. decoder_input_data = np.zeros((num_samples, 1, num_decoder_tokens))

  33. decoder_input_data[:, 0, target_token_index['\t']] = 1.

  34. # Train model as previously

  35. model.fit([encoder_input_data, decoder_input_data], decoder_target_data,

  36.          batch_size=batch_size,

  37.          epochs=epochs,

  38.          validation_split=0.2)

相關文章