基于循环神经网络搭建语言模型

本文首发于个人博客https://blog.chyelang.ml/language_model/,欢迎关注

利用 PyTorch,本项目实现了一个基于word embedding和GRU的语言模型(Language Model,以下简称LM)。其包括一个encoder层、一个GRU层和一个decoder层,embedding维度与GRU的hidden state维度均为1500,采用了自己搭建的带有Layer Normalization(LN)的GRU模块,运用了dropout、学习初始状态、锁定encoder与decoder参数、梯度裁剪等技巧来提升模型性能,最终在测试集上模型的perplexity (PP)值为89.52,所得模型大小约为108M。本文将从模型搭建、模型训练、测试方法等方面对本项目的工作进行详细说明。

本文代码仓库见于:https://github.com/chyelang/hw3_language_model 。项目所有代码以及训练好的模型存放于 hw3_language_model_handed 文件夹中。参考借鉴 Pytorch 的构建LM的官方教程1及项目启动代码,本项目代码的组织结构如下表所示:

代码文件功能
prepare_data.py根据任务类型(训练或是测试),获得相应的语料库
modules.py构建带LN的GRU模块及对其进行测试
build_model.py根据选定的参数构建语言模型
main.py主程序。包含了训练与测试代码,可从命令行接受参数输入。

模型搭建

思路分析

模型计算图的构建由 build_model.LMModel类完成。根据课上所学及一些博客所分享的NLP最佳实践2,本项目模型设计的主要思路如下:

  1. 在各种神经网络中,RNN最适合对序列进行建模,但是为缓解梯度消失、梯度爆炸的问题,使网络可以得到训练,需要选用LSTM或者GRU。一般而言两者在精度上区别不大,LSTM更容易过拟合,GRU则收敛更快。GRU正变得越来越流行。
  2. 在纵向堆叠RNN模块可以得到深度RNN网络,其建模能力更强,但也更难训练,目前深度一般最多不超过五层。对RNN单元做Layer Normalization(LN)可以加快RNN网络的训练,同时提高其泛化能力。
  3. 在对单词做特征表示时,word embedding方法能度量不同单词的相似程度,在NLP问题中比词袋法效果更好。
  4. dropout、学习初始状态、锁定encoder与decoder参数、梯度裁剪等技巧可以使模型更容易被训练,同时提高模型的精度。
GRU的实现

在modules.py中,首先实现了LayerNormGRUCell类。在torch.nn.GRUCell中,重置门r、更新门z、输出门n和隐状态h的基本更新公式如下所示,本项目在其基础上引入了新的可训练参数对重置门r输出和更新门z输出的正则化。为提高效率,公式中的所有矩阵运算都直接调用torch.mm完成。

为构建多层的GRU网络,本项目进一步将LayerNormGRUCell封装在了LayerNormGRUCellModule中,以便借助torch.nn.ModuleList容器管理多个GRU Cell。最后在LayerNormGRU类中,程序利用一个for循环构建起多层的GRU网络,并对非最后一层输出进行正则化。总结来说,用户可通过LayerNormGRU(input_size, hidden_size, nlayers, dropout=0.0, bias=True, layer_norm=False)类来建立带LN、dropout的多层GRU。

modules.py中还包含了LayerNormGRUCellTest()和LayerNormGRUTest()两个测试函数,通过分别与torch.nn.GRUCell 和torch.nn.GRU输出结果的对比,验证了LayerNormGRUCell和LayerNormGRU输出的正确性。值得注意的是,由于PyTorch中并没有实现带LN的GRU模块,因此验证代码并没有验证自己搭建的模块带LN功能时的正确性。但从实际试验中LN功能带来的效果提升来看,代码中的实现是可靠的。

模型训练

数据预处理

数据预处理(即语料库的生成)由prepare_data.Corpus类完成。该类提供两个模式:train和test。在train模式下,程序首先读入train.txt,建立并持久化保存一个word to id的字典,然后将trian.txt和valid.txt中的单词都替换成该单词对应的id,得到一个表示该文档的特征向量。随后依据给定的batch size,将该向量整理成矩阵形式(矩阵列数等于batch size)。经过处理后,不同列之间将失去依赖关系,损失极小部分的训练数据。test模式下的数据处理与train模式类似,只不过此时程序将直接读入已保存好的word to id词典,为test文档建立数据矩阵。

在训练或者测试过程中,main.get_batch()函数将从语料库中获取相应位置、相应长度的序列,以供输入到网络中。本项目中将序列的长度定为35,即认为当前时刻的单词是什么最多与之前的35个单词有关系。合理设置该长度将有利于模型的训练与精度的提升。

训练方法与参数调优

本作用通过main.train()来完成单个epoch的训练,通过 main.train_main()来完成多个epoch的循环,并在其中动态地调整学习率与batch size,还能根据验证集pp的情况提前结束训练。训练采用adam优化器,开始训练时,为给梯度下降提供更多的随机性,采用较小的batch size,并逐步增大batch size到一定的值,之后保持该batch size一直训练下去。实践证明这种动态调整batch size的方法在本任务中能带来一定的性能提升。另外,当验证集pp陷入停滞时,验证集将降低为原来的一半;当连续停滞次数超过一定的阈值时,模型训练将终止。

下面一系列表格记录了本项目中参数调优的过程。其中所涉及到的参数解释如下:

  • model表示所使用的RNN网络类型
  • ninp表示word embedding的向量维度
  • nhid表示RNN网络输出的维度
  • tie参数为选择是否锁定encoder与decoder参数,当其为true时, ninp与nhid必须设置为相同的值
  • layer_norm参数为是否使用LN

表1:LSTM与GRU性能比较

实验编号modelninp/nhidnlayerstiedbatch sizetrain speed(ms/batch)val pp
1LSTM1500/15002true20254.9995.94
2GRU1500/15002true20207.8798.98

由上表可知,LSTM的精度比GRU更好一些,但差别不大;由于参数较少,GRU比LSTM的训练速度更快一些。处于GRU正变得越来越流行的考虑,接下来将基于GRU进行参数调优,并始终将tied设置为true。

表2:batch size对模型的影响

实验编号ninp/nhidnlayersbatch sizeval pp
31500/15002固定为10094.74
41500/15002固定为25694.64
51500/15002从20开始,增长到20093.18
61500/15002从10开始,增长到38499.30

通过比较实验2、3、4,可知本任务的最佳batch size在200左右,比较实验4、5、6,可知采取动态调整batch size的方式有助于提升模型精度,但过大的batch size则会损害模型精度。接下来的表中若无特别说明,都将将采用实验5所确定的batch size调整策略,

表3:Embedding 维度、隐状态维度、初始状态学习对模型的影响

实验编号ninp/nhidnlayers初始状态学习val pp
71500/15001false91.43
8750/7501false92.38
91000/10002false91.98
101000/10003false96.16
111000/10002true91.46
121500/15001true90.18

通过比较实验5、7及比较实验9、10,可发现单层网络对本任务效果最好;通过比较实验9、11及比较实验7、12,可发现对网络的初始状态也进行学习能够提升一些性能。接下来的表中若无特别说明,都将ninp与nhid都设为1500,nlayers设为1,并将初始状态学习设置为true。

表4:使用自己搭建的GRU、层正则化(LN)对模型的影响

实验编号nlayersGRU来源layer_normval pp
131自己搭建false92.12
141自己搭建true89.52
152自己搭建true91.04

通过比较实验12、13,可发现采用自己搭建的GRU比采用PyTorch原生的GRU在精度上要差一些;通过比较实验13、14,可发现LN确实能提升一点模型的精度;通过比较实验14、15,可发现在使用LN的情况下,两层GRU的效果仍然不如单层GRU,这可能是由于两层GRU仍然没有得到充分的训练、未能找到全局最优参数所造成的。

综上所述,经过参数调优,最终模型使用自己搭建的GRU模块,ninp与nhid均为1500,nlayers为1,embedding向量的dropout设为0.5,使用动态调整batch size、锁定encoder与decoder参数、使用层正则化。最终所得模型在验证集上的pp为89.52。

测试命令

由项目所提供的train.txt生成的word to id词典已保存为./data/pta/word_id.pkl。当测试用txt文档所用的单词没有超出词典范围时,可进入hw3_language_model 文件夹,通过以下命令计算测试集的pp:

python main.py --mode test --cuda --gpu_id 0 --test_file /path/to/test/file

总结

经过一系列的模型结构优化、参数调试,所提出的模型在验证集上的perplexity由最初的98.98 左右提高到了最终的89.52 左右,现将相关经验总结如下:

  1. LSTM与GRU的精度大致相同,但GRU的收敛速度和训练速度更快一些。一般而言,LSTM模型更复杂,更适合大数据集;而GRU则更适合小数据集,因为它没那么复杂,从而有助于减少过拟合。
  2. word embedding方法能对单词之间的相似性进行度量,所以用它对单词进行表示的效果更好。
  3. RNN网络中要预测下一个词时,都需要显式或者隐式地依赖与模型的初始状态。如果将该初始状态设置为可学习的参数而不是随机生成的变量,则可以略微提高模型的精度。
  4. 由于NLP任务中的单词表往往很大,因此单词表示具有很大的稀疏性,给encoder的训练带来了一定的困难。如果将encoder的参数与decoder参数联系起来,那么这些参数将有机会利用更多的样本进行训练,从而得到更好的特征表示。
  5. Layer Normalization是仿照CNN网络中的Batch Normalization而提出来的,它能减少一个样本中模型某一层输出的covariate shift,在一定程度上使得模型的训练速度加快,泛化能力增强。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于循环神经网络(RNN)的智能聊天机器人系统可以使用Python编程语言实现。RNN是一种适合处理序列数据的机器学习模型,对于自然语言处理任务特别有用。下面将简要介绍实现智能聊天机器人系统的主要步骤: 1. 数据处理:首先,需要准备用于训练聊天机器人的数据集。可以使用开源的对话语料库或者自己收集一些对话数据。然后,进行数据清洗和预处理,比如去除特殊字符、标记化文本等。 2. 构建词汇表:创建一个词汇表将训练数据中的单词映射到唯一的整数索引。可以使用Python中的库,如NLTK或者spaCy来帮助处理文本和构建词汇表。 3. 序列填充与分批:由于RNN模型需要固定长度输入,在训练之前需要对句子进行填充或截断,使其长度保持一致。然后,将数据集划分为小批次来进行训练。 4. RNN模型构建:使用Python中的深度学习库,如TensorFlow、Keras或PyTorch构建RNN模型。常用的RNN类型有LSTM(长短期记忆)和GRU(门控循环单元)。模型的输入是一个独热编码的词向量,通过层叠RNN单元以及全连接层进行训练和预测。 5. 模型训练:将准备好的数据输入到RNN模型中进行训练。使用适当的损失函数(如交叉熵)和优化算法(如Adam),通过反向传播算法不断调整模型的参数。可以定义合适的停止准则或者使用验证集来评估模型的性能,并保存训练好的模型。 6. 智能回答生成:训练好的RNN模型可以用于生成智能回答。通过传入用户的输入,将其转换为词向量后输入到模型中得到预测结果。根据模型输出的概率分布,选择最高概率的单词作为回答的一部分,再将生成的单词添加到输入序列中,重复该过程直到生成完整的回答。 7. 用户交互界面:为了提供友好的用户体验,可以使用Python的GUI库,如Tkinter或PyQt,构建一个简单的聊天界面。用户可以通过界面与机器人进行对话,输入问题并查看机器人的回答。 综上所述,使用Python实现基于循环神经网络的智能聊天机器人系统需要进行数据处理、构建词汇表、RNN模型构建、模型训练、智能回答生成以及用户交互界面搭建等步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值