入门 | 十分钟搞定Keras序列到序列学习(附代码实现)

选自Keras Blog

作者:Francois Chollet

机器之心编译

参与:黄小天、路雪

如何在 Keras 中实现 RNN 序列到序列学习?本文中,作者将尝试对这一问题做出简短解答;本文预设你已有一些循环网络和 Keras 的使用经验。

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

什么是序列到序列学习?

序列到序列学习(Seq2Seq)是指训练模型从而把一个域的序列(比如英语语句)转化为另一个域的序列(比如法语中的对应语句)。

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

Seq2Seq 可用于机器翻译或者省去问题回答——通常来讲,它可以随时生成文本。完成这一任务有很多方式,比如 RNN 或一维卷积。本文只介绍 RNN。

次要案例:当输入序列和输出序列长度相同

当输入序列和输出序列长度相同时,你可以通过 Keras LSTM 或者 GRU 层(或者其中的堆栈)简单地实现模型。这一实例脚本中的案例展示了如何教会 RNN 学习添加被编码为字符串的数字:

一般案例:标准的 Seq2Seq

一般情况下,输入序列和输出序列有不同的长度(比如机器翻译)。这就需要一个更高级的设置,尤其在没有进一步语境的「序列到序列模型」时。下面是其工作原理:

  • 一个 RNN 层(或其中的堆栈)作为「编码器」:它处理输入序列并反馈其内部状态。注意我们抛弃了编码器 RNN 的输出,只恢复其状态。该状态在下一步中充当解码器的「语境」。
  • 另一个 RNN 层作为「解码器」:在给定目标序列先前字母的情况下,它被训练以预测目标序列的下一个字符。具体讲,它被训练把目标序列转化为相同序列,但接下来被一个时间步抵消,这一训练过程在语境中被称为「teacher forcing」。更重要的是,编码器把其状态向量用作初始状态,如此编码器获得了其将要生成的信息。实际上,在给定 targets[...t] 的情况下,解码器学习生成 targets[t+1...],前提是在输入序列上。

在推理模式中,即当要解码未知的输入序列,我们完成了一个稍微不同的处理:

  1. 把输入序列编码进状态向量
  2. 从大小为 1 的目标序列开始
  3. 馈送状态向量和 1 个字符的目标序列到解码器从而为下一字符生成预测
  4. 通过这些预测采样下一个字符(我们使用 argmax)
  5. 把采样的字符附加到目标序列
  6. 不断重复直至我们生成序列最后的字符或者达到字符的极限

相同的处理也可被用于训练没有「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 反馈输出的全部序列。这被用在解码器中。
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))
# We set up our decoder to return full output sequences,
# and to return internal states as well. We don't use the 
# return states in the training model, but we will use them in inference.
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)

# 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)

大约 1 小时后在 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_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)

我们使用它实现上述推断循环(inference loop):

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 !

这就是我们的十分钟入门 Keras 序列到序列模型教程。完整代码详见 GitHub:https://github.com/fchollet/keras/blob/master/examples/lstm_seq2seq.py。

常见问题

1. 我想使用 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)

2. 我想使用整数序列的单词级别模型,应该怎么做?

如果你的输入是整数序列(如按词典索引编码的单词序列),你可以通过 Embedding 层嵌入这些整数标记。方法如下:

# 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)

3. 如果我不想使用「teacher forcing」,应该怎么做?

一些案例中可能不能使用 teacher forcing,因为你无法获取完整的目标序列,比如,在线训练非常长的语句,则缓冲完成输入-目标语言对是不可能的。在这种情况下,你要通过将解码器的预测重新注入解码器输入进行训练,就像我们进行推断时所做的那样。

你可以通过构建硬编码输出再注入循环(output reinjection loop)的模型达到该目标:

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)

原文链接:https://blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html

本文为机器之心编译,转载请联系本公众号获得授权。

原文发布于微信公众号 - 机器之心(almosthuman2014)

原文发表时间:2017-10-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏漫漫深度学习路

pytorch学习笔记(二):gradient

gradient 在BP的时候,pytorch是将Variable的梯度放在Variable对象中的,我们随时都可以使用Variable.grad得到对应Var...

32080
来自专栏人工智能LeadAI

pytorch入门教程 | 第二章:Autograd

autograd自动微分 假如我们有一个向量x=(1,1)当成input,经过一系列运算得到了output变量y,如下图所示: ? 如图所示,向量x经过与4和自...

404120
来自专栏机器学习和数学

[编程经验] Tensorflow中的共享变量机制小结

今天说一下tensorflow的变量共享机制,首先为什么会有变量共享机制? 这个还是要扯一下生成对抗网络GAN,我们知道GAN由两个网络组成,一个是生成器网络G...

83630
来自专栏深度学习自然语言处理

基于attention的seq2seq机器翻译实践详解

理理思路 文本处理,这里我是以eng_fra的文本为例,每行是english[tab]french,以tab键分割。获取文本,清洗。 分别建立字典,一个engl...

57960
来自专栏木东居士的专栏

Bloom Filter 的数学背景

17930
来自专栏Leetcode名企之路

【Leetcode】64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

27710
来自专栏漫漫深度学习路

pytorch 学习笔记(一)

pytorch是一个动态的建图的工具。不像Tensorflow那样,先建图,然后通过feed和run重复执行建好的图。相对来说,pytorch具有更好的灵活性。...

54460
来自专栏CreateAMind

keras doc 8 BatchNormalization

该层在每个batch上将前一层的激活值重新规范化,即使得其输出数据的均值接近0,其标准差接近1

21150
来自专栏小樱的经验随笔

容斥原理

容斥原理 对容斥原理的描述 容斥原理是一种重要的组合数学方法,可以让你求解任意大小的集合,或者计算复合事件的概率。 描述 容斥原理可以描述如下: 要计算几个...

47170
来自专栏余林丰

12.高斯消去法(1)——矩阵编程基础

对于一阶线性方程的求解有多种方式,这里将介绍利用高斯消去法解一阶线性方程组。在介绍高斯消去法前需要对《线性代数》做一下温习,同时在代码中对于矩阵的存储做一个简...

25770

扫码关注云+社区

领取腾讯云代金券