[转]从Encoder到Decoder实现Seq2Seq模型

前言python

最基础的seq2seq模型包含了三个部分,即encoder、decoder以及链接二者的中间状态向量,encoder经过学习输入,将其编码成一个固定大小的状态向量s,继而将s传给decoder,decoder再经过对状态向量s的学习来进行输出。git

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

总结起来讲,基础的Seq2Seq主要包括Encoder,Decoder,以及链接二者的固定大小的State Vector。api

1. 数据集网络

数据集包括source与target:架构

- source_data: 每一行是一个单词ide

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

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

2. 数据预览oop

咱们先把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

实战代码

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

相关文章
相关标签/搜索