《安娜卡列尼娜》文本生成——利用TensorFlow构建LSTM模型

《安娜卡列尼娜》文本生成——利用TensorFlow构建LSTM模型

前言

最近看完了LSTM的一些外文资料,主要参考了Colah的blog以及Andrej Karpathy blog的一些关于RNN和LSTM的材料,准备动手去实现一个LSTM模型。代码的基础框架来自于Udacity上深度学习纳米学位的课程(付费课程)的一个demo,我刚开始看代码的时候真的是一头雾水,很多东西没有理解,后来反复查阅资料,并我重新对代码进行了学习和修改,对步骤进行了进一步的剖析,下面将一步步用TensorFlow来构建LSTM模型进行文本学习并试图去生成新的文本。本篇文章比较适合新手去操作,LSTM层采用的是BasicLSTMCell。

关于RNN与LSTM模型本文不做介绍,详情去查阅资料过着去看上面的blog链接,讲的很清楚啦。这篇文章主要是偏向实战,来自己动手构建LSTM模型。

数据集来自于外文版《安娜卡列妮娜》书籍的文本文档(本文后面会提供整个project的git链接)。

工具介绍

  • 语言:Python 3
  • 包:TensorFlow及其它数据处理包(见代码中)
  • 编辑器:jupyter notebook
  • 线上GPU:floyd

----------------------------------------------------------------------------------------------------------

正文部分

正文部分主要包括以下四个部分:

- 数据预处理:加载数据、转换数据、分割数据mini-batch

- 模型构建:输入层,LSTM层,输出层,训练误差,loss,optimizer

- 模型训练:设置模型参数对模型进行训练

- 生成新文本:训练新的文本


主题:整个文本将基于《安娜卡列妮娜》这本书的英文文本作为LSTM模型的训练数据,输入为单个字符,通过学习整个英文文档的字符(包括字母和标点符号等)来进行文本生成。在开始建模之前,我们首先要明确我们的输入和输出。即输入是字符,输出是预测出的新字符。

一. 数据预处理

在开始模型之前,我们首先要导入需要的包:

import time
import numpy as np
import tensorflow as tf

这一部分主要包括了数据的转换与mini-batch的分割步骤。

首先我们来进行数据的加载与编码转换。由于我们是基于字符(字母和标点符号等单个字符串,以下统称为字符)进行模型构建,也就是说我们的输入和输出都是字符。举个栗子,假如我们有一个单词“hello”,我们想要基于这个单词构建LSTM,那么希望的到的结果是,输入“h”,预测下一个字母为“e”;输入“e”时,预测下一个字母为“l”,等等。

因此我们的输入便是一个个字母,下面我们将文章进行转换。

上面的代码主要完成了下面三个任务:

- 得到了文章中所有的字符集合 vocab

- 得到一个字符-数字的映射 vocab_to_int

- 得到一个数字-字符的映射 int_to_vocab

- 对原文进行转码后的列表 encoded

完成了前面的数据预处理操作,接下来就是要划分我们的数据集,在这里我们使用mini-batch来进行模型训练,那么我们要如何划分数据集呢?在进行mini-batch划分之前,我们先来了解几个概念。

假如我们目前手里有一个序列1-12,我们接下来以这个序列为例来说明划分mini-batch中的几个概念。首先我们回顾一下,在DNN和CNN中,我们都会将数据分batch输入给神经网络,加入我们有100个样本,如果设置我们的batch_size=10,那么意味着每次我们都会向神经网络输入10个样本进行训练调整参数。同样的,在LSTM中,batch_size意味着每次向网络输入多少个样本,在上图中,当我们设置batch_size=2时,我们会将整个序列划分为6个batch,每个batch中有两个数字。

然而由于RNN中存在着“记忆”,也就是循环。事实上一个循环神经网络能够被看做是多个相同神经网络的叠加,在这个系统中,每一个网络都会传递信息给下一个。上面的图中,我们可以看到整个RNN网络由三个相同的神经网络单元叠加起来的序列。那么在这里就有了第二个概念sequence_length(也叫steps),中文叫序列长度。上图中序列长度是3,可以看到将三个字符作为了一个序列。

有了上面两个概念,我们来规范一下后面的定义。我们定义一个batch中的序列个数为N(即batch_size),定义单个序列长度为M(也就是我们的num_steps)。那么实际上我们每个batch是一个N \times M的数组,相当于我们的每个batch中有N\times M个字符。在上图中,当我们设置N=2, M=3时,我们可以得到每个batch的大小为2 x 3 = 6个字符,整个序列可以被分割成12 / 6 = 2个batch。

基于上面的分析,我们下面来进行mini-batch的分割:

上面的代码定义了一个generator,调用函数会返回一个generator对象,我们可以获取一个batch。

经过上面的步骤,我们已经完成了对数据集的预处理。下一步我们开始构建模型。


二. 模型构建

模型构建部分主要包括了输入层,LSTM层,输出层,loss,optimizer等部分的构建,我们将一块一块来进行实现。

1.输入层

在数据预处理阶段,我们定义了mini-batch的分割函数,输入层的size取决于我们设置batch的size(n\_seqs\times n\_steps),下面我们首先构建输入层。

同样的,输出层的shape=N\times M(因为输入一个字符,同样会输出一个字符)。除了输入输出外,我们还定义了keep_prob参数用来在后面控制dropout的保留结点数。关于dropout正则化请参考链接

2.LSTM层

LSTM层是整个神经网络的关键部分。TensorFlow中,tf.contrib.rnn模块中有BasicLSTMCell和LSTMCell两个包,它们的区别在于:

BasicLSTMCell does not allow cell clipping, a projection layer, and does not use peep-hole connections: it is the basic baseline.(来自TensorFlow官网)

在这里我们仅使用基本模块BasicLSTMCell。

上面的代码中,我并没有使用tf.contrib.rnn模块,是因为我在使用远程floyd的GPU运行代码时候告诉我找不到这个模块,可以用tf.nn.run_cell.BasicLSTMCell替代。构建好LSTM cell后,为了防止过拟合,在它的隐层添加了dropout正则。

后面的MultiRNNCell实现了对基本LSTM cell的顺序堆叠,它接收的是cell对象组成的list。最后initial_state定义了初始cell state。

3.输出层

到目前为止,我们的输入和LSTM层都已经构建完毕。接下来就要构造我们的输出层,输出层采用softmax,它与LSTM进行全连接。对于每一个字符来说,它经过LSTM后的输出大小是1\times L(L为LSTM cell隐层的结点数量),我们上面也分析过输入一个N x M的batch,我们从LSTM层得到的输出为N\times M\times L,要将这个输出与softmax全连接层建立连接,就需要对LSTM的输出进行重塑,变成(N*M)\times L 的一个2D的tensor。softmax层的结点数应该是vocab的大小(我们要计算概率分布)。因此整个LSTM层到softmax层的大小为L \times vocab\_size

将数据重塑后,我们对LSTM层和softmax层进行连接。并计算logits和softmax后的概率分布。

4.训练误差计算

至此我们已经完成了整个网络的构建,接下来要定义train loss和optimizer。我们知道从sotfmax层输出的是概率分布,因此我们要对targets进行one-hot编码。我们采用softmax_cross_entropy_with_logits交叉熵来计算loss。


5.Optimizer

我们知道RNN会遇到梯度爆炸(gradients exploding)梯度弥散(gradients disappearing)的问题。LSTM解决了梯度弥散的问题,但是gradients仍然可能会爆炸,因此我们采用gradient clippling的方式来防止梯度爆炸。即通过设置一个阈值,当gradients超过这个阈值时,就将它重置为阈值大小,这就保证了梯度不会变得很大。

tf.clip_by_global_norm会返回clip以后的gradients以及global_norm。整个学习过程采用AdamOptimizer

6.模型组合

经过上面五个步骤,我们完成了所有的模块设置。下面我们来将这些部分组合起来,构建一个类。

我们使用tf.nn.dynamic_run来运行RNN序列。


三. 模型训练

在模型训练之前,我们首先初始化一些参数,我们的参数主要有:

  • batch_size: 单个batch中序列的个数
  • num_steps: 单个序列中字符数目
  • lstm_size: 隐层结点个数
  • num_layers: LSTM层个数
  • learning_rate: 学习率
  • keep_prob: 训练时dropout层中保留结点比例

这是我自己设置的一些参数,具体一些调参经验可以参考Andrej Karpathy的git上的建议

参数设置完毕后,离运行整个LSTM就差一步啦,下面我们来运行整个模型。

我这里设置的迭代次数为20次,并且在代码运行中我们设置了结点的保存,设置了每运行200次进行一次变量保存,这样的好处是有利于我们后面去直观地观察在整个训练过程中文本生成的结果是如何一步步“进化”的。

四.文本生成

经过漫长的模型训练,我们得到了一系列训练过程中保存下来的参数,可以利用这些参数来进行文本生成啦。当我们输入一个字符时,它会预测下一个,我们再将这个新的字符输入模型,就可以一直不断地生成字符,从而形成文本。

为了减少噪音,每次的预测值我会选择最可能的前5个进行随机选择,比如输入h,预测结果概率最大的前五个为[o,e,i,u,b],我们将随机从这五个中挑选一个作为新的字符,让过程加入随机因素会减少一些噪音的生成。

代码封装了两个函数来做文本生成,具体请参看文章尾部的git链接中的源码。

训练步数:200

当训练步数为200的时候,LSTM生成的文本大概长下面这个样子:

看起来像是字符的随机组合,但是可以看到有一些单词例如hat,her等已经出现,并且生成了成对的引号。

训练步数:1000

当训练步数到达1000的时候,已经开始有简单的句子出现,并且单词看起来似乎不是那么乱了。

训练步数:2000

当训练步数达到2000的时候,单词和句子看起来已经有所规范。

训练步数:3960

当训练结束时(本文仅训练了3960步),生成的文本已经有小部分可以读的比较通顺了,而且很少有单词拼写的错误。

五.总结

整个文章通过构建LSTM模型完成了对《安娜卡列宁娜》文本的学习并且基于学习成果生成了新的文本。

通过观察上面的生成文本,我们可以看出随着训练步数的增加,模型的训练误差在持续减少。本文仅设置了20次迭代,尝试更大次数的迭代可能会取得更好的效果。

个人觉得LSTM对于文本的学习能力还是很强,后面可能将针对中文文本构造一些学习模型,应该会更有意思!

我对RNN也是在不断地探索与学习中,文中不免会有一些错误和谬误,恳请各位指正,非常感谢!

----------------------------------------------------------------------------------------------------------

整个项目地址已经上传到个人的GitHub上。


如果觉得有用,请叫上全村儿的小伙伴儿帮我star一下,不胜感激!

文章转载请注明出处。
编辑于 2017-05-25

文章被以下专栏收录