959
魔獸
十分鍾教程:用Keras實現seq2seq學習
看到有很多人問這個問題:如何在Keras中實現RNN序列到序列(seq2seq)學習?本文將對此做一個簡單的介紹。
請注意,要讀懂本文,你需要具備循環網絡和Keras方麵的相關經驗。
什麼是seq2seq學習?
序列到序列學習(seq2seq)是一種把序列從一個域(例如英語中的句子)轉換為另一個域中的序列(例如把相同的句子翻譯成法語)的模型訓練方法。
"the cat sat on the mat" -> [Seq2Seq model] -> "le chat etait assit sur le tapis"
這可以用於機器翻譯或免費問答(對於自然語言的問題,產生自然語言的答案)。一般來說,它適用於任何需要生成文本的場景。
目前有多種方法可以用來處理這個任務,可以使用RNN,也可以使用一維卷積網絡。這裏,我們將重點介紹RNN。
一個簡單的例子:當輸入和輸出序列的長度相同時
當輸入序列和輸出序列具有相同長度的時候,你可以使用Keras LSTM或GRU層(或其堆疊)很輕鬆地實現這樣地模型。這個示例腳本就是一個例子,它展示了如何教RNN計算加法,並編碼為字符串:
對於這個方法有一點要注意:我們假定了對於給定的input[...t]
是可以生成target[...t]
的。這在某些情況下有效(例如,數字字符串的加法),但在大多數情況下都無效。在一般情況下,要生成目標序列,必須要有輸入序列的完整信息。
一般情況:標準的序列到序列
一般來說,輸入序列和輸出序列的長度是不同的(例如機器翻譯),並且需要有完整的輸入序列才能開始預測目標。這需要一個更高級的設置,這就是人們在“序列到序列模型”時經常提及的沒有上下文。下麵是它的工作原理:
有一個RNN層(或其堆疊)作為“編碼器”:它負責處理輸入序列並返回其自身的內部狀態。注意,我們將丟棄編碼器RNN的輸出,隻恢複狀態。該狀態將在下一步驟中用作解碼器的“上下文”或“環境”。
另外還有一個RNN層(或其堆疊)作為“解碼器”:在給定目標序列前一個字符的情況下,對其進行訓練以預測目標序列的下一個字符。具體來說,就是訓練該層使其能夠將目標序列轉換成向將來偏移了一個時間步長的同一個序列,這種訓練過程被稱為“teacher forcing(老師強迫)”。有一點很重要,解碼器將來自編碼器的狀態向量作為初始狀態,這樣,解碼器就知道了它應該產生什麼樣的信息。實際上就是解碼器以輸入序列為條件,對於給定的
targets[...t]
學習生成targets[t+1...]
,。
在推理模式下,即當我們要解碼未知輸入序列時,過程稍稍會有些不同:
- 將輸入序列編碼為狀態向量。
- 以大小為1的目標序列開始。
- 將狀態向量和一個字符的目標序列提供給解碼器,以產生下一個字符的預測。
- 使用這些預測對下一個字符進行采樣(我們簡單地使用argmax)。
- 將采樣的字符添加到目標序列上
- 重複上述步驟,直到生成序列結束字符,或者達到字符數限製。
也可以在沒有“teacher forcing”的情況下使用相同的過程來訓練Seq2Seq網絡,例如,通過將解碼器的預測重新注入到解碼器中。
一個Keras的例子
下麵我們用代碼來實現上麵那些想法。
對於這個例程,我們將使用英文句子和對應的法語翻譯數據集,可以從manythings.org/anki下載。下載的文件名為fra-eng.zip
。我們將實現一個字符級別的序列到序列模型,處理逐個字符輸入並逐個字符的生成輸出。我們也可以實現一個單詞級別的模型,這對於機器翻譯而言更常見。在本文的最後,你能找到一些使用Embedding
層把字符級別的模型變成單詞級別模型的信息。
完整例程可以在GitHub上找到。
下麵簡單介紹一下處理過程:
- 將句子轉換為3個Numpy數組,
encoder_input_data
,decode_input_data
,decode_target_data
: -encoder_input_data
是一個三維數組(num_pairs
,max_english_sentence_length
,num_english_characters
),包含英文句子的獨熱向量化。 -decoder_input_data
是一個三維數組(num_pairs
,max_french_sentence_length
,num_french_characters
),包含法語句子的獨熱向量化。 -decoder_target_data
與decoder_input_data
相同但偏移一個時間步長。decoder_target_data[:, t, :]
將與decoder_input_data[:, t + 1, :]
相同 - 訓練一個基於LSTM的基本的Seq2Seq模型來預測
encoder_input_data
和decode_input_data
的decode_target_data
。模型使用了“teacher forcing”。 - 解碼一些句子以檢查模型是否正常工作(即將
encoder_input_data
中的樣本從decoder_target_data
轉換為相應的樣本)。
由於訓練過程和推理過程(譯碼句)是完全不同的,所以我們要使用不同的模型,盡管它們都是利用相同的內部層。
這是我們的訓練模型。它利用了Keras RNN的三個主要功能:
-
return_state contructor
參數,配置一個RNN層返回第一個條目是輸出,下一個條目是內部RNN狀態的列表。用於恢複編碼器的狀態。 -
inital_state
參數,指定RNN的初始狀態。用於將編碼器狀態傳遞到解碼器作為初始狀態。 -
return_sequences
構造函數參數,配置RNN返回其完整的輸出序列。在解碼器中使用。
from keras.models import Model
from keras.layers import Input, LSTM, Dense
# Define an input sequence and process it.
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]
# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None, num_decoder_tokens))
decoder_lstm = LSTM(latent_dim, return_sequences=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)
# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
我們對模型進行了訓練,同時監測到了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上,經過一個小時左右的時間,就可以開始推斷了。 要解碼一個測試語句,要重複這幾個步驟:
- 對輸入的句子進行編碼並獲取初始解碼器的狀態
- 以該初始狀態和“序列開始”令牌為目標,執行解碼器的一個步驟。 輸出是下一個目標字符。
- 添加預測到的目標字符並重複上述步驟。
這是我們的推理設置:
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 = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs = decoder_lstm(decoder_inputs,
initial_state=decoder_states)
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_inputs] + decoder_states,
decoder_outputs)
我們用它來實現上述推理循環:
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 = 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
# Add the sampled character to the sequence
char_vector = np.zeros((1, 1, num_decoder_tokens))
char_vector[0, 0, sampled_token_index] = 1.
target_seq = np.concatenate([target_seq, char_vector], axis=1)
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 !
有關Keras的序列到序列模型的十分鍾介紹已經結束了。 請注意:完整的代碼可在GitHub上找到。
參考資料
使用神經網絡進行序列到序列的學習
使用用於統計機器翻譯的RNN編碼器-解碼器來學習短語的表達
常見問題
如果我想使用GRU層而不是LSTM該怎麼辦?
這實際上更簡單,因為GRU隻有一個狀態,而LSTM有兩個狀態。 以下代碼展示了如何讓訓練模型適應使用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)
如果我想對整數序列的輸入使用單詞級模型該怎麼辦?
如果輸入是整數序列,該怎麼辦呢? 通過嵌入層嵌入這些整數令牌即可。 就是這樣:
# 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”。例如 如果需要對一個很長的序列做在線訓練,那麼緩衝完整的輸入幾乎是不可能的。 在這種情況下,你可能希望通過將解碼器的預測重新注入到解碼器的輸入中來進行訓練,就像我們在推理中做的那樣。
你可以通過構建一個硬編碼輸出重新注入回路的模型來實現這個目的:
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)
如果你還有更多問題,請在Twitter上聯係我。
文章原標題《A ten-minute introduction to sequence-to-sequence learning in Keras》,作者:Francois Chollet,譯者:夏天,審校:主題曲。
文章為簡譯,更為詳細的內容,請查看原文
最後更新:2017-10-05 10:33:21