机器不学习
首发于 机器不学习
关注专栏 写文章
从Encoder到Decoder实现Seq2Seq模型

从Encoder到Decoder实现Seq2Seq模型

300 人 赞同了该文章

更新:感谢@Gang He指出的代码错误。get_batches函数中第15行与第19行,代码已经重新修改,GitHub已更新。

前言

好久没有更新专栏,今天我们来看一个简单的Seq2Seq实现,我们将使用TensorFlow来实现一个基础版本的Seq2Seq,主要帮助理解Seq2Seq中的基础架构。

最基础的Seq2Seq模型包含了三个部分,即Encoder、Decoder以及连接两者的中间状态向量,Encoder通过学习输入,将其编码成一个固定大小的状态向量S,继而将S传给Decoder,Decoder再通过对状态向量S的学习来进行输出。


图中每一个box代表了一个RNN单元,通常是LSTM或者GRU。其实基础的Seq2Seq是有很多弊端的,首先Encoder将输入编码为固定大小状态向量的过程实际上是一个信息“信息有损压缩”的过程,如果信息量越大,那么这个转化向量的过程对信息的损失就越大,同时,随着sequence length的增加,意味着时间维度上的序列很长,RNN模型也会出现梯度弥散。最后,基础的模型连接Encoder和Decoder模块的组件仅仅是一个固定大小的状态向量,这使得Decoder无法直接去关注到输入信息的更多细节。由于基础Seq2Seq的种种缺陷,随后引入了Attention的概念以及Bi-directional encoder layer等,由于本篇文章主要是构建一个基础的Seq2Seq模型,对其他改进tricks先不做介绍。

总结起来说,基础的Seq2Seq主要包括Encoder,Decoder,以及连接两者的固定大小的State Vector。


实战代码

下面我们就将利用TensorFlow来构建一个基础的Seq2Seq模型,通过向我们的模型输入一个单词(字母序列),例如hello,模型将按照字母顺序排序输出,即输出ehllo。

版本信息:Python 3 / TensorFlow 1.1

1. 数据集

数据集包括source与target:

- source_data: 每一行是一个单词

- target_data: 每一行是经过字母排序后的“单词”,它的每一行与source_data中每一行一一对应

例如,source_data的第一行是hello,第二行是what,那么target_data中对应的第一行是ehllo,第二行是ahtw。

2. 数据预览

我们先把source和target数据加载进来,可以看一下前10行,target的每一行是对source源数据中的单词进行了排序。下面我们就将基于这些数据来训练一个Seq2Seq模型,来帮助大家理解基础架构。

3. 数据预处理

在神经网络中,对于文本的数据预处理无非是将文本转化为模型可理解的数字,这里都比较熟悉,不作过多解释。但在这里我们需要加入以下四种字符,<PAD>主要用来进行字符补全,<EOS>和<GO>都是用在Decoder端的序列中,告诉解码器句子的起始与结束,<UNK>则用来替代一些未出现过的词或者低频词。

  • < PAD>: 补全字符。
  • < EOS>: 解码器端的句子结束标识符。
  • < UNK>: 低频词或者一些未遇到过的词等。
  • < GO>: 解码器端的句子起始标识符。


通过上面步骤,我们可以得到转换为数字后的源数据与目标数据。

4. 模型构建

Encoder

模型构建主要包括Encoder层与Decoder层。在Encoder层,我们首先需要对定义输入的tensor,同时要对字母进行Embedding,再输入到RNN层。

在这里,我们使用TensorFlow中的tf.contrib.layers.embed_sequence来对输入进行embedding。

我们来看一个栗子,假如我们有一个batch=2,sequence_length=5的样本,features = [[1,2,3,4,5],[6,7,8,9,10]],使用

tf.contrib.layers.embed_sequence(features,vocab_size=n_words, embed_dim=10)

那么我们会得到一个2 x 5 x 10的输出,其中features中的每个数字都被embed成了一个10维向量。

官方关于tf.contrib.layers.embed_sequence()的解释如下:
Maps a sequence of symbols to a sequence of embeddings.
Typical use case would be reusing embeddings between an encoder and decoder.

Decoder

在Decoder端,我们主要要完成以下几件事情:

  • 对target数据进行处理
  • 构造Decoder
    • Embedding
    • 构造Decoder层
    • 构造输出层,输出层会告诉我们每个时间序列的RNN输出结果
    • Training Decoder
    • Predicting Decoder

下面我们会对这每个部分进行一一介绍。


1. target数据处理

我们的target数据有两个作用:

  • 在训练过程中,我们需要将我们的target序列作为输入传给Decoder端RNN的每个阶段,而不是使用前一阶段预测输出,这样会使得模型更加准确。(这就是为什么我们会构建Training和Predicting两个Decoder的原因,下面还会有对这部分的解释)。
  • 需要用target数据来计算模型的loss。


我们首先需要对target端的数据进行一步预处理。在我们将target中的序列作为输入给Decoder端的RNN时,序列中的最后一个字母(或单词)其实是没有用的。我们来用下图解释:


我们此时只看右边的Decoder端,可以看到我们的target序列是[<go>, W, X, Y, Z, <eos>],其中<go>,W,X,Y,Z是每个时间序列上输入给RNN的内容,我们发现,<eos>并没有作为输入传递给RNN。因此我们需要将target中的最后一个字符去掉,同时还需要在前面添加<go>标识,告诉模型这代表一个句子的开始。


如上图,所示,红色和橙色为我们最终的保留区域,灰色是序列中的最后一个字符,我们把它删掉即可。

我们使用tf.strided_slice()来进行这一步处理。

其中tf.fill(dims, value)参数会生成一个dims形状并用value填充的tensor。举个栗子:tf.fill([2,2], 7) => [[7,7], [7,7]]。tf.concat()会按照某个维度将两个tensor拼接起来。

2. 构造Decoder

  • 对target数据进行embedding。
  • 构造Decoder端的RNN单元。
  • 构造输出层,从而得到每个时间序列上的预测结果。
  • 构造training decoder。
  • 构造predicting decoder。

注意,我们这里将decoder分为了training和predicting,这两个encoder实际上是共享参数的,也就是通过training decoder学得的参数,predicting会拿来进行预测。那么为什么我们要分两个呢,这里主要考虑模型的robust。

在training阶段,为了能够让模型更加准确,我们并不会把t-1的预测输出作为t阶段的输入,而是直接使用target data中序列的元素输入到Encoder中。而在predict阶段,我们没有target data,有的只是t-1阶段的输出和隐层状态。

上面的图中代表的是training过程。在training过程中,我们并不会把每个阶段的预测输出作为下一阶段的输入,下一阶段的输入我们会直接使用target data,这样能够保证模型更加准确。

这个图代表我们的predict阶段,在这个阶段,我们没有target data,这个时候前一阶段的预测结果就会作为下一阶段的输入。

当然,predicting虽然与training是分开的,但他们是会共享参数的,training训练好的参数会供predicting使用。

decoder层的代码如下:

构建好了Encoder层与Decoder以后,我们需要将它们连接起来build我们的Seq2Seq模型。

定义超参数

# 超参数
# Number of Epochs
epochs = 60
# Batch Size
batch_size = 128
# RNN Size
rnn_size = 50
# Number of Layers
num_layers = 2
# Embedding Size
encoding_embedding_size = 15
decoding_embedding_size = 15
# Learning Rate
learning_rate = 0.001

定义loss function、optimizer以及gradient clipping

目前为止我们已经完成了整个模型的构建,但还没有构造batch函数,batch函数用来每次获取一个batch的训练样本对模型进行训练。

在这里,我们还需要定义另一个函数对batch中的序列进行补全操作。这是啥意思呢?我们来看个例子,假如我们定义了batch=2,里面的序列分别是

[['h', 'e', 'l', 'l', 'o'],
 ['w', 'h', 'a', 't']]

那么这两个序列的长度一个是5,一个是4,变长的序列对于RNN来说是没办法训练的,所以我们这个时候要对短序列进行补全,补全以后,两个序列会变成下面的样子:

[['h', 'e', 'l', 'l', 'o'],
 ['w', 'h', 'a', 't', '<PAD>']]

这样就保证了我们每个batch中的序列长度是固定的。

感谢@Gang He提出的错误。此处代码已修正。修改部分为get_batches中的两个for循环,for target in targets_batch和for source in sources_batch(之前的代码是for target in pad_targets_batch和for source in pad_sources_batch),因为我们用sequence_mask计算了每个句子的权重,该权重作为参数传入loss函数,主要用来忽略句子中pad部分的loss。如果是对pad以后的句子进行loop,那么输出权重都是1,不符合我们的要求。在这里做出修正。GitHub上代码也已修改。

至此,我们完成了整个模型的构建与数据的处理。接下来我们对模型进行训练,我定义了batch_size=128,epochs=60。训练loss如下:

模型预测

我们通过实际的例子来进行验证。

输入“hello”:


输入“machine”:

输入“common”:


总结

至此,我们实现了一个基本的序列到序列模型,Encoder通过对输入序列的学习,将学习到的信息转化为一个状态向量传递给Decoder,Decoder再基于这个输入得到输出。除此之外,我们还知道要对batch中的单词进行补全保证一个batch内的样本具有相同的序列长度。

我们可以看到最终模型的训练loss相对已经比较低了,并且从例子看,其对短序列的输出还是比较准确的,但一旦我们的输入序列过长,比如15甚至20个字母的单词,其Decoder端的输出就非常的差。

完整代码已上传至GitHub

转载请联系作者获得授权。
编辑于 2018-03-19
「真诚赞赏,手留余香」
赞赏

4 人已赞赏

赞赏用户 赞赏用户 赞赏用户 赞赏用户
深度学习(Deep Learning)
文本处理
神经网络
赞同 300​ 149 条评论
分享
收藏

文章被以下专栏收录

  • 机器不学习

    机器不学习

    不定期分享机器学习与深度学习实战代码。
    关注专栏

推荐阅读

  • 两行代码玩转Google BERT句向量词向量

    两行代码玩转Google BERT句向量词向量

    Sequence to Sequence学习简述

    Sequence to Sequence学习简述

    RNN Part1-RNN介绍

    RNN Part1-RNN介绍

    当循环神经网络碰上强化学习-建立通用的空间模型

    许铁 以色列理工大学博士 你可能了解强化学习, 你也可能了解RNN, 这两个在机器学习的世界里都不算简单的东西在一起能碰出什么火花, 我给大家随便聊几句。 在讲RNN前我们先来侃侃强化学习…

149 条评论

切换为时间排序
  • StudierA
    StudierA 1 年前
    支持下
  • 天雨粟
    天雨粟 (作者) 回复 StudierA 1 年前
    谢谢!嘿嘿
  • 秦以
    秦以 1 年前
    不错不错,支持!
  • 天雨粟
    天雨粟 (作者) 回复 秦以 1 年前
    谢谢啦!
  • 九九四
    九九四 1 年前
    资次资次
  • 天雨粟
    天雨粟 (作者) 回复 九九四 1 年前
    谢谢!
  • 陈福华
    陈福华 1 年前
    如果是不同长度的两个句子做attention,怎么避免pad部分的计算而又可以使用到矩阵运算呢?
  • 天雨粟
    天雨粟 (作者) 回复 陈福华 1 年前
    目前我对attention 还在学习研究中,还没有太透彻理解哈,不好意思
  • slmady
    slmady 1 年前

    get_batches的返回值应该是序列的原始长度,你返回的是补全之后的长度,是你写错了吗,还是我理解有问题?

  • 天雨粟
    天雨粟 (作者) 回复 slmady 1 年前
    感谢提出问题哈,是这样的,get_batches最后返回的就是补全以后的长度。我来解释一下哈,对于一个batch 来说,里面的每一个序列长短可能都不一样,对于一个batch 中的数据,我们要求具有相同长度,因为对于rnn 来说是没办法处理变长的,因此pad 是为了保证同一个batch 中的序列具有相同长度,这样给dynamic_rnn 以后才可以处理。但不同batch 长度是可以不同的。
  • 如子
    如子 1 年前

    经过测试,把 with tf.variable_scope('decode'): 和 with tf.variable_scope('decode', reuse=True): 去掉,代码依然可以运行,结果无误。看来动态decoder不像静态decoder要求参数明确共享

  • 天雨粟
    天雨粟 (作者) 回复 如子 1 年前
    去掉scope代码肯定不会报错,但此时是不会共享参数的。你可以看下算法最后的预测结果,应该是不如共享参数的结果。去掉scope我还没仔细研究,感谢你的回复!我回去再测试下
  • 如子
    如子 回复 天雨粟 (作者) 1 年前

    感谢回复,我重新测试了多次,共享确实比不共享好,我想问一下,这里除了decoder_rnn_cell还有其他参数需要共享吗

  • 查看全部 10 条回复
  • sigma
    sigma 1 年前

    py文件的代码直接运行,报了这个错,能麻烦帮我分析下嘛?

    TensorFlow Version: 1.2.0-rc1

    Traceback (most recent call last):

    File "/data/PycharmProjects/MoneyRecognize/zhihu-master/basic_seq2seq2/Seq2seq_char.py", line 325, in <module>

    num_layers)

    File "/data/PycharmProjects/MoneyRecognize/zhihu-master/basic_seq2seq2/Seq2seq_char.py", line 280, in seq2seq_model

    decoder_input)

    File "/data/PycharmProjects/MoneyRecognize/zhihu-master/basic_seq2seq2/Seq2seq_char.py", line 227, in decoding_layer

    maximum_iterations=max_target_sequence_length)

    ValueError: too many values to unpack (expected 2)

  • 天雨粟
    天雨粟 (作者) 回复 sigma 1 年前

    tensorflow版本不对,版本要求1.1,你用的是r1.2版本,这里面的tf.contrib.seq2seq.dynamic_decode()会返回三个值,参考tensorflow.org/api_docs。我用的是1.1版本,仅返回output和final_state两个值。如果你用r1.2的话,你调用函数时,这个函数还会多返回一个sequence_length,需要打包三个返回值,而不是两个。你可以改成training_decoder_output, _, _ = tf.contrib.seq2seq.dynamic_decode(参数)

  • 木筱
    木筱 回复 sigma 1 年前

    你的tensorflow版本是1.2的,卸载安装1,1的就好了,这两个版本的方法接受的参数不一样

  • 查看全部 9 条回复
  • 木筱
    木筱 1 年前

    题主您好,我发下这份coder,如果数据换成中文的,预测出来的就是空了,是为什么呢...

  • 天雨粟
    天雨粟 (作者) 回复 木筱 1 年前

    可以把你的代码上传到git,给我个链接,我下班帮你看下

  • 木筱
    木筱 回复 天雨粟 (作者) 1 年前

    谢谢题主,我没有改动你的代码,紧紧只是把训练数据的英文单词换成了中文句子,实现输入一个句子,输出还是这个句子。。昨天调了一下,发现大概是数据比较少,然后出来的就是空【但其实还是不懂,按理来说,即便模型很差,应该也会输出<EOS>, 但是不知道为什么就是什么也没输出。】, 训练迭代次数增加之后就不会出现这个问题了==。 然后还有两个问题想请教一下题主:

    1。 training_logits = tf.identity(training_decoder_output.rnn_output, 'logits')
    predicting_logits = tf.identity(predicting_decoder_output.sample_id, name='predictions')


    这两行代码中,predicting_decoder_output是dynamic_decoder()返回的结果,那么sample_id和rnn_output是什么?

    我看了一下dynamic_decoder的op,没找到答案==.


    2. 还有就是感觉这样写有些乱,比如这样预测的时候,输入是一个句子,然后在feed数据的时候,还是得输入batch_size的大小,训练跟预测放在一起感觉也是怪怪的。

  • 展开其他 1 条回复
  • 讳莫如深
    讳莫如深 1 年前

    可否告知全连接层的作用?

  • 天雨粟
    天雨粟 (作者) 回复 讳莫如深 1 年前

    对Decoder层的输出做映射(projection),Dense会把输出变成字典大小,这样才能计算预测出来哪个单词的概率最大呀

  • 讳莫如深
    讳莫如深 回复 天雨粟 (作者) 1 年前

    谢谢!

  • Gang He
    Gang He 1 年前

    请问,mask这里怎么理解?

  • 天雨粟
    天雨粟 (作者) 回复 Gang He 1 年前
    用来计算loss 时候忽略padding 部分。举个例子,比如我们的batch 是5,意味着我们每次训练5个句子,这五个句子的实际长度肯定是不一样的,比如句子长度分别是5,3,8,10,3,那么我们在传入训练时,会把整个batch 的句子都pad 到10,同样的,对于target 句子也是这样,把一个batch 中数据处理为同一个长度,那么在计算loss 的时候,我们实际上是不想计算每个句子padding 部分的loss,只计算真实长度的部分。所以用mask 来乘以句子,对应padding 部分会变成0,也就不会算入loss 。建议看下tf 文档的mask 说明
  • Gang He
    Gang He 回复 天雨粟 (作者) 1 年前

    谢谢您的解释,明白了很多;但我在运行的时候,run打印出了mask,发现值都是1,这个是正确的吗?我的理解是句子长度为4,那么mask应该有4个1,3个0. 您的代码这个地方是不是该loop targets_batch【未经过padding的句子长度为2,比如[1,3],这样,padding后的句子是[1,3,pad, pad, pad, pad, pad]】

    for target in pad_targets_batch:
    pad_targets_lengths.append(len(target))

  • 展开其他 3 条回复
  • 刘木木
    刘木木 1 年前

    你好,请问你的github链接是失效了嘛?显示page not found

  • 天雨粟
    天雨粟 (作者) 回复 刘木木 1 年前

    不好意思,之前重新push了一次,NELSONZHAO/zhihu这是我的整个知乎里代码的完整repo,你可以看一下哈

  • YF GGG
    YF GGG 1 年前

    你好,请问一下你给的例子输入hello输出ehllo,模型的超参数就是github上面的吗?我跑了一下感觉结果不太好,出不来ehllo的结果

  • 天雨粟
    天雨粟 (作者) 回复 YF GGG 1 年前
    你说得对,我重新跑了一遍,效果确实不如之前,所以我在思考这里我们是不是应该考虑pad的loss ,超参数我应该重新调整过,当时试了挺多次。我这几天也在查相关资料,我跑了几次都是之前的好,也就是把pad 也看成预测的内容一起计算loss 会让模型结果更优。
  • YF GGG
    YF GGG 回复 天雨粟 (作者) 1 年前

    我发现这个问题是因为在test的时候有一个地方feed错了,第485行source_sequence_length的值应该是len(input_word)而不是len(text),len(text)是加了pad之后的长度。把这个地方修改一下结果就正确了。

  • 展开其他 1 条回复
  • YF GGG
    YF GGG 1 年前

    如果用你修改get_batch之前的代码运行一遍,结果很好。用最新push的代码运行一遍,结果反而不好了

  • 感谢分享?想问一下为什么encoder和decoder中embedding的定义方式不同?
  • 天雨粟
    天雨粟 (作者) 回复 走向最后的海洋 1 年前

    两个功能是一样的啦!只是尝试了下不同的接口,返回结果是一样的哈

  • 走向最后的海洋
    走向最后的海洋 回复 天雨粟 (作者) 1 年前

    哦哦,encoder中的embedding也是可训练的对吧?

  • 展开其他 2 条回复
  • yujun luo
    yujun luo 1 年前

    您好!如果训练数据的source和target长度不相等可以用这个模型吗?

  • 天雨粟
    天雨粟 (作者) 回复 yujun luo 1 年前
    当然可以的呀,模型本身就是可以处理变长数据的~
  • yujun luo
    yujun luo 回复 天雨粟 (作者) 1 年前

    非常感谢,已经跑起来了

  • 王二牛
    王二牛 1 年前
    github链接404
  • 天雨粟
    天雨粟 (作者) 回复 王二牛 1 年前

    NELSONZHAO/zhihu

    抱歉之前忘记更新地址,上面是地址

  • 寇莉
    寇莉 1 年前

    谢谢分享,支持

  • Johnson
    Johnson 1 年前

    感谢作者

  • Johnson
    Johnson 1 年前

    请问在decoding_layer方法的 #4 Training decoder 中,使用tf.contrib.seq2seq.TrainingHelper()遇到了一个错误。能不能指点一下呀?发生错误的原因是ValueError: Expected sequence_length to be a vector, but received shape: <unknown>。万分感谢?