返回 登录
0

十分钟教程:用Keras实现seq2seq学习

原文:A ten-minute introduction to sequence-to-sequence learning in
Keras

作者:Francois Chollet 译者:雁惊寒

摘要:序列到序列学习(seq2seq)是一种把序列从一个域(例如英语中的句子)转换为另一个域中的序列(例如把相同的句子翻译成法语)的模型训练方法。目前有多种方法可以用来处理这个任务,可以使用RNN,也可以使用一维卷积网络。这里,我们将重点介绍RNN。

我看到有很多人问这个问题:如何在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计算加法,并编码为字符串:

1.png

对于这个方法有一点要注意:我们假定了对于给定的input[...t]是可以生成target[...t]的。这在某些情况下有效(例如,数字字符串的加法),但在大多数情况下都无效。在一般情况下,要生成目标序列,必须要有输入序列的完整信息。

一般情况:标准的序列到序列

一般来说,输入序列和输出序列的长度是不同的(例如机器翻译),并且需要有完整的输入序列才能开始预测目标。这需要一个更高级的设置,这就是人们在“序列到序列模型”时经常提及的没有上下文。下面是它的工作原理:

  • 有一个RNN层(或其堆叠)作为“编码器”:它负责处理输入序列并返回其自身的内部状态。注意,我们将丢弃编码器RNN的输出,只恢复状态。该状态将在下一步骤中用作解码器的“上下文”或“环境”。

  • 另外还有一个RNN层(或其堆叠)作为“解码器”:在给定目标序列前一个字符的情况下,对其进行训练以预测目标序列的下一个字符。具体来说,就是训练该层使其能够将目标序列转换成向将来偏移了一个时间步长的同一个序列,这种训练过程被称为“teacher forcing(老师强迫)”。有一点很重要,解码器将来自编码器的状态向量作为初始状态,这样,解码器就知道了它应该产生什么样的信息。实际上就是解码器以输入序列为条件,对于给定的targets[...t]学习生成targets[t+1...],。

2.png

在推理模式下,即当我们要解码未知输入序列时,过程稍稍会有些不同:

  1. 将输入序列编码为状态向量。
  2. 以大小为1的目标序列开始。
  3. 将状态向量和一个字符的目标序列提供给解码器,以产生下一个字符的预测。
  4. 使用这些预测对下一个字符进行采样(我们简单地使用argmax)。
  5. 将采样的字符添加到目标序列上
  6. 重复上述步骤,直到生成序列结束字符,或者达到字符数限制。

3.png

也可以在没有“teacher forcing”的情况下使用相同的过程来训练Seq2Seq网络,例如,通过将解码器的预测重新注入到解码器中。

一个Keras的例子

下面我们用代码来实现上面那些想法。

对于这个例程,我们将使用英文句子和对应的法语翻译数据集,可以从manythings.org/anki下载。下载的文件名为fra-eng.zip。我们将实现一个字符级别的序列到序列模型,处理逐个字符输入并逐个字符的生成输出。我们也可以实现一个单词级别的模型,这对于机器翻译而言更常见。在本文的最后,你能找到一些使用Embedding层把字符级别的模型变成单词级别模型的信息。

完整例程可以在GitHub上找到

下面简单介绍一下处理过程:

  1. 将句子转换为3个Numpy数组,encoder_input_datadecode_input_datadecode_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_datadecoder_input_data相同但偏移一个时间步长。 decoder_target_data[:, t, :]将与decoder_input_data[:, t + 1, :]相同
  2. 训练一个基于LSTM的基本的Seq2Seq模型来预测encoder_input_datadecode_input_datadecode_target_data。模型使用了“teacher forcing”。
  3. 解码一些句子以检查模型是否正常工作(即将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上,经过一个小时左右的时间,就可以开始推断了。 要解码一个测试语句,要重复这几个步骤:

  1. 对输入的句子进行编码并获取初始解码器的状态
  2. 以该初始状态和“序列开始”令牌为目标,执行解码器的一个步骤。 输出是下一个目标字符。
  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 = [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上联系我


图片描述
2017年10月14日,SDCC 2017之大数据技术实战线上峰会即将召开,邀请圈内顶尖的布道师、技术专家和技术引领者,共话大数据平台构建、优化提升大数据平台的各项性能、Spark部署实践、企业流平台实践、以及实现应用大数据支持业务创新发展等核心话题,七位大牛与你相聚狂欢,详情查看所有嘉宾和议题,以及注册参会,分享还可优惠30元。

评论