RNN
1、简介
RNN循环神经网络在nlp领域有些过时了, 训练数据足够多时, RNN的效果不如transformers模型, 但在小规模问题上, RNN还是很有用。机器学习中经常用到的文本、语音等时序数据, 思考一下, 如何对时序数据建模。在上面的基础部分讲到把一段文字整体输入到一个logistics regression模型, 让模型做二分类, 这属于one-to-one模型。 一个输入对应一个输出, 全连接神经网络和卷积网络都是one-to-one模型,但人类并不会把一整段文字全部输入到大脑中。 人类阅读时会从左到右阅读一段文字, 阅读时逐渐在大脑中积累文本的信息, 阅读一段话后脑中积累了整段文字的大意。one-to-one模型要求一个输入对应一个输出, 比如输入一张图片, 输出每一类的概率值。one-to-one模型很适合图片的问题, 但不太适合文本的问题。
对于文本问题, 输入和输出长度并不固定。 一句话可长可短, 所以输入的长度并不固定, 输出的长度也不固定。 比如把英文翻译成汉语, 英语可能有10个单词, 但翻译成的汉语可能有10个也可能有8个, 输出汉语的字数并不固定。由于输入和输出的长度不固定, one-to-one模型就不太适合了。
对于时序数据, 更好的模型是many-to-one或者many-to-many模型, RNN就是这样的模型, 输入和输出都不需要固定, RNN很适合文本、语音等时序数据。
RNN跟人的阅读习惯很类似,人每次看一个词, 逐渐在大脑中积累信息。RNN每看一个词, 用状态向量h来积累阅读过的信息。我们把输入的一个词用word embedding变成一个向量x, 每次把一个词向量输入到RNN中, 然后RNN会更新状态h, 把新的内容更新到状态h中。h0包含了第一个词the的信息, h1包含了前两个词the cat的信息, 以此类推, 最后一个状态ht包含了整句话的信息。
可把ht看成是从输入的这句话抽取得到的特征向量, 更新状态h时需要用到参数矩阵A。注意整个RNN只有一个参数A, 不管这条链路有多长, 参数A只有一个, A随机初始化, 然后利用训练数据来学习A。
2、Simple RNN
simple RNN的结构如下图所示。
首先来看一下Simple RNN怎么把输入的词向量x结合到状态h里面? 上一个状态记住的是h_t-1, 新输入的词向量为xt, 把这两个向量做concatination, 得到一个更高维的向量。矩阵A是RNN的模型参数, 这里计算矩阵A和向量的乘积。 矩阵和向量的乘积是个向量, 然后把激活函数用到该向量的每个元素上。 激活函数是双曲正切函数tanh, 输入是任意实数, 输出在-1到1之间。把激活函数的输出作为新的状态向量ht。由于使用了tanh激活函数, 所以向量ht的每个元素都在-1到+1之间。
RNN神经网络的结构图可以这样理解。新的状态ht是旧的状态ht-1和新的输入xt的函数, 神经网络的模型模型参数是矩阵A, 新的状态ht依赖于旧的状态向量ht-1, 向量xt以及矩阵A。
双曲正切函数的曲线图如下所示。
思考一下, 为什么需要双曲正切函数tanh, 能否将其去掉。
假设输入的词向量x全部都是0(这里考虑极端情况), 这等同于把输入的词向量x给去掉, 把矩阵A右边那一半也去掉, 这样第100个状态向量h100就等于矩阵A乘以h99, 一直等于矩阵A的100次方乘以h0。加入矩阵A最大的特征值略小于1, 比如最大的特征值等于0.9。
那么会发生什么, 0.9的100次方非常接近0, 那么新的状态向量h100几乎也是全零的向量。
假如矩阵A最大的特征值略大于1, 同理矩阵A的100次方会超级大, 那么新的状态向量h100的每个元素也都非常巨大。假如循环的次数更多,状态向量h就会爆炸或消失。由此可见, 如果每个这个激活函数, 数值计算可能会出现问题, 要么计算结果全为0, 要么计算结果都超级大。
what will happen if λmax(A) = 0.9 or 1.2 ?
所以需要使用tanh激活函数更新h, 而且更新之后会做一个normalization, 让h恢复到-1和+1这个合适的区间里。
首先针对ht-1与xt拼接后的向量, 这个向量的维度是h的维度加上x的维度, 所以A必须有h的维度加上x的维度这么多列, A的行数等于向量ht的维度, 所以矩阵A的大小就是矩阵h的维度乘以(h+x的维度), 这个乘积就是simple rnn的参数。
3、模型搭建
之前是通过logistics regression来判断电影评论是正面的还是负面的, 下面通过RNN来完成这个分类任务。
首先最底层是word embedding, 它可以把词映射到向量x, 词向量的维度可自由设置, 可以使用cross validation交叉验证来选择最优的维度, 这里设置x的维度是32,然后搭建simple rnn层, 输入的词向量x, 输出的状态h, h的维度也是也可自由设置, 应用用corss validation交叉验证来选择最优的维度,这里设置h的维度是32, 所以x和h的维度都是32, 但这里只是一个巧合而已, 通常h和x的维度不一样。
之前介绍过, 状态向量h积累输入的信息, 比如h0包含第一个单词i的信息, h1包含前两个词的信息, 最后一个ht积累了整句话的信息。
可以让keras输出所有的状态向量, 也可以让keras只输出最后一个向量ht, ht积累了整句话的信息。所以使用ht这一个向量就够了。
只使用ht, 而把ht前面的状态h全都丢掉, ht相当于从文本中提取的特征向量,把ht输入到分类器, 分类器就会输出一个0~1之间的数值, 0代表负面评价, 1代表正面评价。
然后设置这些超参数, 设置vocabulary是1万, 意思是词典中有1万个词汇, embedding_dim是32, 意思是词向量x的维度是32, word_num是500, 意思是每个电影评论有500个单词, 如果超过了500个, 超过部分就会被截掉。如果不到500个, 就用zero padding补成长度等于500。state_dim是32, 意思就是状态向量h的维度等于32。
下面开始使用keras搭建网络。 首先添加embedding 层, 把它映射成向量, 然后是simpleRnn层。 SimpleRNN层需要指定状态向量h的维度, 其中return_sequences=False, 意思是RNN只输出最后一个状态向量, 而把之前的状态向量全部扔掉。
1 | # !/usr/bin/env python |
打印的模型概要如下:
SimpleRNN层的参数量计算公式=h*(h + x) = 32×(32 + 32) + 32 = 2080, 最后面加的32表示偏置。
搭建模型后开始编译模型, 然后用训练数据拟合模型, 编译模型时指定算法为RMSprop, 损失函数是crossentropy, 评价标准是acc, 然后用训练数据来拟合模型。
1 | from tensorflow.keras import optimizers |
上面搭建模型时只使用了最后一个状态ht, 把ht之前的状态丢掉了。
想要h0到ht所有的状态也可以, 如果让keras返回所有的状态, RNN的输出就是个矩阵。矩阵的每一行是一个状态向量。如果使用所有的状态, 需要加上一个flatten层, 把所有状态变成一个向量, 然后这些向量作为分类器的输入来判断电影是正面的还是负面的, 只需要把前面的网络结构稍作改动即可。
如果返回所有的状态, 对应的代码做如下的修改。
1 | from tensorflow.keras.layers import SimpleRNN, Embedding, Dense, Flatten |
打印的模型参数如下所示:
上面是改动模型后打印的概要, 当return_sequence=False时只输出最后一个状态ht, 所以RNN层的输出是32维的向量。当return_sequences=True时, RNN输出所有的状态向量, 所以RNN的输出是500×32的矩阵。500的意思是每条电影评论中有500个单词, 所以一共有500个状态向量, 每个状态向量都是32维。
4、RNN模型的缺陷
举个例子, 现在有这样一个问题, 给定半句话要求预测下一个单词。比如clouds are in the _正确的输出是sky。现在如果在大量文本上训练RNN, 应该是有能力做出这种预测的。在这个例子中, RNN只需要看最近的几个单词。RNN并不需要更多的上下文, 并不需要看得更远, 这个例子对simple RNN有利, simple RNN很适合做这种short term dependence。
Simple RNN的缺点是不擅长long-term dependence。
RNN中的状态h跟之前的所有输入的x都有函数依赖关系。 理论上, 若改变输入的单词x1, 那么之后所有的状态h都是发生变化,但实际上simpleRnn并没有这种性质,所以很不合理。如果把第100个向量h100关于输入x1求导, 会发现导数几乎等于0。导数等于0说明改变输入x1, h100几乎不会发生任何变化。也就是说状态h100跟100步之前的x1几乎没有关系了, 说明状态h100将很多步之前的输入给忘记了, 这显然不合理。
Simple RNN的遗忘会造成一些问题。举个例子, 这是一段话, 开始是I grew up in China, when I was a child,…, 说了很多之后,来了一句, I speak fluent _。然后Simple RNN并不会做出Chinese这个正确的预测, 因为RNN已经把前文忘记了, simple RNN很擅长short term dependence。RNN看到最近的单词是speak fluent, 所以RNN知道下一个单词应该是某种语言, 预测输出可能是任意一种语言。
5、总结
RNN是一种神经网络, 但是它的结构不同于全连接网络和卷积网络, RNN适合文本、语音、时序序列等数据。RNN按照顺序读取每一个词向量, 并且在状态向量h中积累看过的信息。ht只积累了之前所有x的信息。有一种错误的看法是ht只包含了xt的信息,这是不对的。可以认为ht就是从整个输入序列中抽取的特征向量,所以只需要ht向量就可以判断电影评论是正面的还是负面的。
RNN也有一个缺点, RNN的记忆比较短, 它会遗忘很久之前的输入x, 如果这个时间序列很长, 比如好几十步, 最终的ht已经忘记了早先的输入。
simple RNN有一个参数矩阵A, 它还有可能有一个intercept向量, 这里忽略了这个参数b, 这个参数矩阵的维度是h的维度乘以h加上输入x的维度。
参数矩阵A一开始是随机初始化的, 然后从训练数据中学习这个参数矩阵。注意simple RNN只有这一个参数矩阵, 不管这个序列有多长, 参数矩阵只有一个, 所有模块里面的参数都是一样的。