更多深度文章,请关注云计算频道:https://yq.aliyun.com/cloud
看到有很多人问这个问题:如何在Keras中实现RNN序列到序列(seq2seq)学习?本文将对此做一个简单的介绍。
请注意,要读懂本文,你需要具备循环网络和Keras方面的相关经验。
序列到序列学习(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]
的。这在某些情况下有效(例如,数字字符串的加法),但在大多数情况下都无效。在一般情况下,要生成目标序列,必须要有输入序列的完整信息。
一般来说,输入序列和输出序列的长度是不同的(例如机器翻译),并且需要有完整的输入序列才能开始预测目标。这需要一个更高级的设置,这就是人们在“序列到序列模型”时经常提及的没有上下文。下面是它的工作原理:
targets[...t]
学习生成targets[t+1...]
,。在推理模式下,即当我们要解码未知输入序列时,过程稍稍会有些不同:
也可以在没有“teacher forcing”的情况下使用相同的过程来训练Seq2Seq网络,例如,通过将解码器的预测重新注入到解码器中。
下面我们用代码来实现上面那些想法。
对于这个例程,我们将使用英文句子和对应的法语翻译数据集,可以从manythings.org/anki下载。下载的文件名为fra-eng.zip
。我们将实现一个字符级别的序列到序列模型,处理逐个字符输入并逐个字符的生成输出。我们也可以实现一个单词级别的模型,这对于机器翻译而言更常见。在本文的最后,你能找到一些使用Embedding
层把字符级别的模型变成单词级别模型的信息。
完整例程可以在GitHub上找到。
下面简单介绍一下处理过程:
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, :]
相同
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%的样本作为验证数据监测损失,剩下80%作为训练数据。
# 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层:
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”。例如 如果需要对一个很长的序列做在线训练,那么缓冲完整的输入几乎是不可能的。 在这种情况下,你可能希望通过将解码器的预测重新注入到解码器的输入中来进行训练,就像我们在推理中做的那样。
你可以通过构建一个硬编码输出重新注入回路的模型来实现这个目的:
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,译者:夏天,审校:主题曲。
文章为简译,更为详细的内容,请查看原文