前言
最近看完了 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 timeimport numpy as npimport 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×M的数组,相当于我们的每个 batch 中有N×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 × n_steps),下面我们首先构建输入层。
同样的,输出层的(因为输入一个字符,同样会输出一个字符)。除了输入输出外,我们还定义了 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×L(L 为 LSTM cell 隐层的结点数量),我们上面也分析过输入一个 N x M 的 batch,我们从 LSTM 层得到的输出为N×M×L,要将这个输出与 softmax 全连接层建立连接,就需要对 LSTM 的输出进行重塑,变成( N * M ) × L 的一个 2D 的 tensor。softmax 层的结点数应该是 vocab 的大小(我们要计算概率分布)。因此整个 LSTM 层到 softmax 层的大小为L×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,i,e,u,b],我们将随机从这五个中挑选一个作为新的字符,让过程加入随机因素会减少一些噪音的生成。
代码封装了两个函数来做文本生成,具体请参看文章尾部的 git 链接中的源码。
训练步数:200
当训练步数为 200 的时候,LSTM 生成的文本大概长下面这个样子:
看起来像是字符的随机组合,但是可以看到有一些单词例如 hat,her 等已经出现,并且生成了成对的引号。
训练步数:1000
当训练步数到达 1000 的时候,已经开始有简单的句子出现,并且单词看起来似乎不是那么乱了。
训练步数:2000
当训练步数达到 2000 的时候,单词和句子看起来已经有所规范。
训练步数:3960
当训练结束时(本文仅训练了 3960 步),生成的文本已经有小部分可以读的比较通顺了,而且很少有单词拼写的错误。
五. 总结
整个文章通过构建 LSTM 模型完成了对《安娜卡列宁娜》文本的学习并且基于学习成果生成了新的文本。
通过观察上面的生成文本,我们可以看出随着训练步数的增加,模型的训练误差在持续减少。本文仅设置了 20 次迭代,尝试更大次数的迭代可能会取得更好的效果。
个人觉得 LSTM 对于文本的学习能力还是很强,后面可能将针对中文文本构造一些学习模型,应该会更有意思!
我对 RNN 也是在不断地探索与学习中,文中不免会有一些错误和谬误,恳请各位指正,非常感谢!
----------------------------------------------------------------------------------------------------------
整个项目地址已经上传到个人的 GitHub上。
如果觉得有用,请叫上全村儿的小伙伴儿帮我 star 一下,不胜感激!