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很适合文本、语音等时序数据。

image-20231129152146509

RNN跟人的阅读习惯很类似,人每次看一个词, 逐渐在大脑中积累信息。RNN每看一个词, 用状态向量h来积累阅读过的信息。我们把输入的一个词用word embedding变成一个向量x, 每次把一个词向量输入到RNN中, 然后RNN会更新状态h, 把新的内容更新到状态h中。h0包含了第一个词the的信息, h1包含了前两个词the cat的信息, 以此类推, 最后一个状态ht包含了整句话的信息。

可把ht看成是从输入的这句话抽取得到的特征向量, 更新状态h时需要用到参数矩阵A。注意整个RNN只有一个参数A, 不管这条链路有多长, 参数A只有一个, A随机初始化, 然后利用训练数据来学习A。

image-20231129152706261


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。

image-20231129153119779

双曲正切函数的曲线图如下所示。

image-20231129154031226

思考一下, 为什么需要双曲正切函数tanh, 能否将其去掉。

假设输入的词向量x全部都是0(这里考虑极端情况), 这等同于把输入的词向量x给去掉, 把矩阵A右边那一半也去掉, 这样第100个状态向量h100就等于矩阵A乘以h99, 一直等于矩阵A的100次方乘以h0。加入矩阵A最大的特征值略小于1, 比如最大的特征值等于0.9。

image-20231130100739550

那么会发生什么, 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的参数。

image-20231130165042224

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的维度不一样。

image-20231130165417313

之前介绍过, 状态向量h积累输入的信息, 比如h0包含第一个单词i的信息, h1包含前两个词的信息, 最后一个ht积累了整句话的信息。

可以让keras输出所有的状态向量, 也可以让keras只输出最后一个向量ht, ht积累了整句话的信息。所以使用ht这一个向量就够了。

只使用ht, 而把ht前面的状态h全都丢掉, ht相当于从文本中提取的特征向量,把ht输入到分类器, 分类器就会输出一个0~1之间的数值, 0代表负面评价, 1代表正面评价。

image-20231130171244821

然后设置这些超参数, 设置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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# !/usr/bin/env python
# -*-coding:utf-8 -*-
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import SimpleRNN, Embedding, Dense

# unique words in the dictionary
vocabulary = 10000
# shape(x) = 32
embedding_dim = 32
# sequence length 每条评论500个单词, 去长补短
word_num = 500
# shape(h) = 32
state_dim = 32

model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))
# return_sequences=False表示只输出最后的ht
model.add(SimpleRNN(units=state_dim, return_sequences=False))
model.add(Dense(1, activation="sigmoid"))
model.summary()

打印的模型概要如下:

image-20231130173214390

SimpleRNN层的参数量计算公式=h*(h + x) = 32×(32 + 32) + 32 = 2080, 最后面加的32表示偏置。

搭建模型后开始编译模型, 然后用训练数据拟合模型, 编译模型时指定算法为RMSprop, 损失函数是crossentropy, 评价标准是acc, 然后用训练数据来拟合模型。

1
2
3
4
5
6
7
8
9
10
from tensorflow.keras import optimizers
# Early stoping alleviates overfitting
epochs = 3
model.compile(optimizer=optimizers.RMSprop(lr=0.001),
loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train, epochs=epochs,
batch_size=32, validation_data=(x_valid, y_valid))

# 用数据评价模型的表现
scores = model.evaluate(x_test, y_test)

上面搭建模型时只使用了最后一个状态ht, 把ht之前的状态丢掉了。

想要h0到ht所有的状态也可以, 如果让keras返回所有的状态, RNN的输出就是个矩阵。矩阵的每一行是一个状态向量。如果使用所有的状态, 需要加上一个flatten层, 把所有状态变成一个向量, 然后这些向量作为分类器的输入来判断电影是正面的还是负面的, 只需要把前面的网络结构稍作改动即可。

image-20231201094445824

如果返回所有的状态, 对应的代码做如下的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from tensorflow.keras.layers import SimpleRNN, Embedding, Dense, Flatten
from tensorflow.keras.models import Sequential

# unique words in the dictionary
vocabulary = 10000
# shape(x) = 32
embedding_dim = 32
# sequence length 每条评论500个单词, 去长补短
word_num = 500
# shape(h) = 32
state_dim = 32

# 下面是return_sequences=True的情况
model = Sequential()
model.add(Embedding(vocabulary, embedding_dim, input_length=word_num))
# return_state=False表示只输出最后的ht
model.add(SimpleRNN(units=state_dim, return_sequences=True))
model.add(Flatten())
model.add(Dense(1, activation="sigmoid"))
model.summary()

打印的模型参数如下所示:

image-20231201101052199

上面是改动模型后打印的概要, 当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。

image-20231201102334418

Simple RNN的缺点是不擅长long-term dependence。

image-20231201102839929

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知道下一个单词应该是某种语言, 预测输出可能是任意一种语言。

image-20231201104615371

5、总结

RNN是一种神经网络, 但是它的结构不同于全连接网络和卷积网络, RNN适合文本、语音、时序序列等数据。RNN按照顺序读取每一个词向量, 并且在状态向量h中积累看过的信息。ht只积累了之前所有x的信息。有一种错误的看法是ht只包含了xt的信息,这是不对的。可以认为ht就是从整个输入序列中抽取的特征向量,所以只需要ht向量就可以判断电影评论是正面的还是负面的。

image-20231201111508008

RNN也有一个缺点, RNN的记忆比较短, 它会遗忘很久之前的输入x, 如果这个时间序列很长, 比如好几十步, 最终的ht已经忘记了早先的输入。

image-20231201111840310

simple RNN有一个参数矩阵A, 它还有可能有一个intercept向量, 这里忽略了这个参数b, 这个参数矩阵的维度是h的维度乘以h加上输入x的维度。

参数矩阵A一开始是随机初始化的, 然后从训练数据中学习这个参数矩阵。注意simple RNN只有这一个参数矩阵, 不管这个序列有多长, 参数矩阵只有一个, 所有模块里面的参数都是一样的。

image-20231201112048270