RNN
现在我们的数据样本是这样的序列的形式:
序列中的每个元素都不是独立的,而是和其他元素存在着一定联系,例如一个句字就是这种形式,句子中的每个词和其他词是存在上下文关系的。现在我们要对这个句子建模,捕捉它整体的含义,那我们的模型就必须要考虑这种数据结构中元素和元素间的关联。为了处理这样的序列数据,RNN应运而生,它的一般形式如下:
输入有两个,x和隐藏状态h;输出有两个,y和下一个时间步的隐藏状态h。RNN并不是一次性把整个序列输入,而是每次按时间步t把序列中元素依次输入,这样的做法可以让模型处理每个样本序列长度不一的情况。RNN的关键就是这个隐藏状态h,每个时间步结合上一步的h和当前的x更新h,当前x的信息和前文的信息被整合进h,然后传入下一时间步,实现了对前文的记忆。
通常来说,RNN可以用下面这样的形式实现,将来自上个时间步的h和x做拼接然后用全连接层映射。注意h到y的映射不是必须的,有的实现中直接用h作为y,这一点很好理解,你确实需要的话,对h用一层全连接映射到y就是了,这种额外的操作是容易实现的。
尽管RNN被设计用来记忆信息,但它的记忆力实在是不怎么样,观察RNN的输出,它经过激活函数映射到(0,1),在多步运算后数值衰减很快,也就是学习的信息在几步运算后就趋于零了。那我们不用激活函数呢?且不提激活函数引入非线性性的事,激活函数可以把输出映射到一定的区间范围内,假设撇掉x,计算h时没有激活函数,h在多步运算后像下面这样,若w的特征值小于1,h100几乎全为0,w特征值大于1,h100数值爆炸。
从反向传播的角度来看RNN也是不行,同一层神经元在求导时共享权重矩阵,多个时间步的计算后,梯度从后向前一通链式法则连乘过来,求导时雅可比矩阵特征值小于1,梯度消失,特征值大于1,梯度爆炸。梯度爆炸还可以裁剪梯度,梯度消失就很头疼了。
综上所述,RNN无法很好地处理长程依赖。
LSTM
为了解决RNN在长序列训练过程中的梯度消失和梯度爆炸问题,LSTM诞生了。LSTM仍然是一种RNN,不过在计算的时候增加了一些额外的操作。RNN只有一个传递状态h,LSTM有两个传递状态h和c,RNN中的h对应LSTM中的c。LSTM中c改变慢,通常是上一个状态传递过来加上一些数值;h在不同节点下改变明显。
来看LSTM具体的计算,同样先将上一个时间步的h和当前输入x拼接,但用全连接层计算出四个状态:
后面三个介于0和1之间的状态作为一种门控状态。它们的作用类似于权重系数,控制信息通过或屏蔽的程度。这些门控状态全是x和h拼接到一起计算得到的,不是单独看x或者h,因为你是根据当前的输入和前面的历史信息一起来决定哪些重要哪些不重要的。然后,LSTM的计算分为几个阶段:
-
忘记阶段:用zf控制上一个传递状态c哪些需要留下哪些需要忘记。
-
选择记忆阶段:用zi控制选择,对当前输入x选择记忆,哪些重要哪些不重要。在此之前还有一件事,先把x处理成z,为啥要这么做?因为你得把x弄成和zi一样的形状然后再选择。
此时,传递状态和输入都被处理了,你可以来更新传递状态了。
-
输出阶段:用zo控制,哪些被当前状态输出,以及用tanh缩放。
用黑话翻译一下上面的过程(⊙是元素级乘法):
同样的,h到y的映射不是必须的,有的实现中直接用h作为y,Pytorch中就是如此。Pytorch中LSTM的具体计算实现如下:
这里x和h分别和对应的w相乘在相加,实际上和上面写的x和h拼接后再乘w的形式是一样的,但注意Pytorch每个状态的计算用了两个偏置项。所以如下形式LSTM的参数量为:
lstm = nn.LSTM(input_size=32, hidden_size=16, num_layers=2, batch_first=True, bidirectional=False)
Pytorch中LSTM输入是(input, (h_0, c_0))
的形式,(h_0, c_0)
默认设置为零向量。输出是(output, (h_n, c_n))
的形式,当有多层LSTM时,这里output
是最后一层LSTM每个时间步的h的集合[h_1,h_2,...,h_n]
,(h_n, c_n)
是每层LSTM最后一个时间步的h和c。通过下面这段简单的代码可以理解pytorch中LSTM的输入输出和参数量:
import torch
import torch.nn as nn
x = torch.randn((3, 5, 32)) # [n_seq, seq_length, n_feature]
lstm = nn.LSTM(input_size=32, hidden_size=16, num_layers=2, batch_first=True, bidirectional=False)
(o, (h, c)) = lstm(x, )
print(sum(p.numel() for p in lstm.parameters()))
print(o.size()) # [n_seq, seq_length, hidden_size*n_direction]
print(h.size()) # [n_layer*n_direction, n_seq, hidden_size]
print(c.size()) # [n_layer*n_direction, n_seq, hidden_size]
5376
torch.Size([3, 5, 16])
torch.Size([2, 3, 16])
torch.Size([2, 3, 16])
双向RNN
我们可以分别构建两个RNN分别从左向右,和从右向左,各自输出自己的状态向量,然后拼接起来:
双向RNN比单向RNN表现好的原因:
-
减轻对前面记忆的遗忘:从左向右的RNN输出的h可能遗忘掉左端的信息,从右向左的RNN输出的h可能遗忘掉右端的信息,把两者结合到一起就能不足对方遗忘的信息。
-
补足后文的信息:对序列中某个元素的理解可能不仅仅依靠前面的信息,也需要借助后文,所以单方向的移动可能是不够的。
在Pytorch中实现双向的LSTM只需要设置参数bidirectional=True
即可:
import torch
import torch.nn as nn
x = torch.randn((3, 5, 32)) # [n_seq, seq_length, n_feature]
lstm = nn.LSTM(input_size=32, hidden_size=16, num_layers=2, batch_first=True, bidirectional=True)
(o, (h, c)) = lstm(x, )
print(sum(p.numel() for p in lstm.parameters()))
print(o.size()) # [n_seq, seq_length, hidden_size*n_direction]
print(h.size()) # [n_layer*n_direction, n_seq, hidden_size]
print(c.size()) # [n_layer*n_direction, n_seq, hidden_size]
12800
torch.Size([3, 5, 32])
torch.Size([4, 3, 16])
torch.Size([4, 3, 16])
网友评论