外观
RNN 模型变体
LSTM 模型
LSTM(Long Short-Term Memory)也称长短时记忆结构,它是传统 RNN 的变体,与经典 RNN 相比能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象。同时 LSTM 的结构更复杂,它的核心结构可以分为四个部分去解析:
- 遗忘门
- 输入门
- 细胞状态
- 输出门
LSTM 的结构如下图所示:


遗忘门
首先来看遗忘门,遗忘门由下面的部分组成:

σ 为激活函数。
与传统 RNN 的内部结构计算非常相似,首先将当前时间步输入 x(t) 与上一个时间步隐含状态 h(t-1) 拼接,得到[x(t), h(t-1)],然后通过一个全连接层做变换,最后通过 sigmoid 函数进行激活得到 f(t)。我们可以将 f(t) 看作是门值,好比一扇门开合的大小程度,门值都将作用在通过该扇门的张量。遗忘门门值将作用的上一层的细胞状态上,代表遗忘过去的多少信息,又因为遗忘门门值是由 x(t)、h(t-1) 计算得来的。因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态 h(t-1) 来决定遗忘多少上一层的细胞状态所携带的过往信息。

激活函数 sigmiod 用于帮助调节流经网络的值,sigmoid 函数将值压缩在 0 和 1 之间。
输入门
输入门的结构:

我们看到输入门的计算公式有两个,第一个就是产生输入门门值的公式,它和遗忘门公式几乎相同,区别只是在于它们之后要作用的目标上。这个公式意味着输入信息有多少需要进行过滤。输入门的第二个公式是与传统 RNN 的内部结构计算相同。对于 LSTM 来讲,它得到的是当前的细胞状态,而不是像经典 RNN 一样得到的是隐含状态。

细胞状态
下图是细胞状态的更新图:

细胞更新的结构与计算公式非常容易理解,这里没有全连接层,只是将刚刚得到的遗忘门门值与上一个时间步得到的 C(t-1) 相乘,再加上输入门门值与当前时间步得到的未更新 C(t) 相乘的结果。最终得到更新后的 C(t) 作为下一个时间步输入的一部分。整个细胞状态更新过程就是对遗忘门和输入门的应用。
细胞状态更新图演示:

输出门
输出门结构与公式:

输出门部分的公式也是两个,第一个即是计算输出门的门值,它和遗忘门,输入门计算方式相同。第二个即是使用这个门值产生隐含状态 h(t),它将作用在更新后的细胞状态 C(t) 上,并做 tanh 激活,最终得到 h(t) 作为下一时间步输入的一部分。整个输出门的过程,就是为了产生隐含状态 h(t)。
输出门内部结构过程演示:

BiLSTM
Bi-LSTM 即双向LSTM,它没有改变 LSTM 本身任何的内部结构,只是将 LSTM 应用两次且方向不同,再将两次得到的 LSTM 结果进行拼接作为最终输出。

我们看到图中对“我爱中国”这句话或者叫这个输入序列,进行了从左到右和从右到左两次 LSTM 处理,将得到的结果张量进行了拼接作为最终输出。这种结构能够捕捉语言语法中一些特定的前置或后置特征,增强语义关联,但是模型参数和计算复杂度也随之增加了一倍,一般需要对语料和计算资源进行评估后决定是否使用该结构。
LSTM API
位置:在 torch.nn 工具包之中,通过 torch.nn.LSTM 可调用。
nn.LSTM 类初始化主要参数解释:
input_size:输入张量 x 中特征维度的大小。hidden_size:隐层张量 h 中特征维度的大小。num_layers:隐含层的数量。bidirectional:是否选择使用双向 LSTM,如果为 True,则使用;默认不使用。
nn.LSTM 类实例化对象主要参数解释:
input:输入张量 x。h0:初始化的隐层张量 h。- ``c0`:初始化的细胞状态张量 c。
nn.LSTM 使用示例:
# 定义LSTM的参数含义: (input_size, hidden_size, num_layers)
# 定义输入张量的参数含义: (sequence_length, batch_size, input_size)
# 定义隐藏层初始张量和细胞初始状态张量的参数含义:
# (num_layers * num_directions, batch_size, hidden_size)
>>> import torch.nn as nn
>>> import torch
>>> rnn = nn.LSTM(5, 6, 2)
>>> input = torch.randn(1, 3, 5)
>>> h0 = torch.randn(2, 3, 6)
>>> c0 = torch.randn(2, 3, 6)
>>> output, (hn, cn) = rnn(input, (h0, c0))
>>> output
tensor([[[ 0.0447, -0.0335, 0.1454, 0.0438, 0.0865, 0.0416],
[ 0.0105, 0.1923, 0.5507, -0.1742, 0.1569, -0.0548],
[-0.1186, 0.1835, -0.0022, -0.1388, -0.0877, -0.4007]]],
grad_fn=<StackBackward>)
>>> hn
tensor([[[ 0.4647, -0.2364, 0.0645, -0.3996, -0.0500, -0.0152],
[ 0.3852, 0.0704, 0.2103, -0.2524, 0.0243, 0.0477],
[ 0.2571, 0.0608, 0.2322, 0.1815, -0.0513, -0.0291]],
[[ 0.0447, -0.0335, 0.1454, 0.0438, 0.0865, 0.0416],
[ 0.0105, 0.1923, 0.5507, -0.1742, 0.1569, -0.0548],
[-0.1186, 0.1835, -0.0022, -0.1388, -0.0877, -0.4007]]],
grad_fn=<StackBackward>)
>>> cn
tensor([[[ 0.8083, -0.5500, 0.1009, -0.5806, -0.0668, -0.1161],
[ 0.7438, 0.0957, 0.5509, -0.7725, 0.0824, 0.0626],
[ 0.3131, 0.0920, 0.8359, 0.9187, -0.4826, -0.0717]],
[[ 0.1240, -0.0526, 0.3035, 0.1099, 0.5915, 0.0828],
[ 0.0203, 0.8367, 0.9832, -0.4454, 0.3917, -0.1983],
[-0.2976, 0.7764, -0.0074, -0.1965, -0.1343, -0.6683]]],
grad_fn=<StackBackward>)LSTM 优势:LSTM 的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸,虽然并不能杜绝这种现象,但在更长的序列问题上表现优于传统 RNN。
LSTM 缺点:由于内部结构相对较复杂,因此训练效率在同等算力下较传统 RNN 低很多。
GRU 模型
GRU(Gated Recurrent Unit)也称门控循环单元结构,它也是传统 RNN 的变体,同 LSTM 一样能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象。同时它的结构和计算要比 LSTM 更简单,它的核心结构可以分为两个部分去解析:
- 更新门
- 重置门
GRU 模型的结构如下:


GRU 的更新门和重置门结构:

和之前分析过的 LSTM 中的门控一样,首先计算更新门和重置门的门值,分别是 z(t) 和 r(t),计算方法就是使用 X(t) 与 h(t-1) 拼接进行线性变换,再经过 sigmoid 激活。之后重置门门值作用在了 h(t-1) 上,代表控制上一时间步传来的信息有多少可以被利用。接着就是使用这个重置后的 h(t-1) 进行基本的 RNN 计算,即与 x(t) 拼接进行线性变化,经过 tanh 激活,得到新的 h(t)。最后更新门的门值会作用在新的 h(t),而 1 - 门值会作用在 h(t-1) 上,随后将两者的结果相加,得到最终的隐含状态输出 h(t),这个过程意味着更新门有能力保留之前的结果,当门值趋于 1 时,输出就是新的 h(t),而当门值趋于 0 时,输出就是上一时间步的 h(t-1)。
Bi-GRU 介绍
Bi-GRU 与 Bi-LSTM 的逻辑相同,都是不改变其内部结构,而是将模型应用两次且方向不同,再将两次得到的 LSTM 结果进行拼接作为最终输出。具体参见上小节中的 Bi-LSTM。
GRU API
位置:在 torch.nn 工具包之中,通过 torch.nn.GRU 可调用。
nn.GRU 类初始化主要参数解释:
input_size:输入张量 x 中特征维度的大小hidden_size:隐层张量 h 中特征维度的大小num_layers:隐含层的数量bidirectional:是否选择使用双向 LSTM,如果为True,则使用;默认不使用
nn.GRU 类实例化对象主要参数解释:
input:输入张量 xh0:初始化的隐层张量 h
nn.GRU 使用示例:
>>> import torch
>>> import torch.nn as nn
>>> rnn = nn.GRU(5, 6, 2)
>>> input = torch.randn(1, 3, 5)
>>> h0 = torch.randn(2, 3, 6)
>>> output, hn = rnn(input, h0)
>>> output
tensor([[[-0.2097, -2.2225, 0.6204, -0.1745, -0.1749, -0.0460],
[-0.3820, 0.0465, -0.4798, 0.6837, -0.7894, 0.5173],
[-0.0184, -0.2758, 1.2482, 0.5514, -0.9165, -0.6667]]],
grad_fn=<StackBackward>)
>>> hn
tensor([[[ 0.6578, -0.4226, -0.2129, -0.3785, 0.5070, 0.4338],
[-0.5072, 0.5948, 0.8083, 0.4618, 0.1629, -0.1591],
[ 0.2430, -0.4981, 0.3846, -0.4252, 0.7191, 0.5420]],
[[-0.2097, -2.2225, 0.6204, -0.1745, -0.1749, -0.0460],
[-0.3820, 0.0465, -0.4798, 0.6837, -0.7894, 0.5173],
[-0.0184, -0.2758, 1.2482, 0.5514, -0.9165, -0.6667]]],
grad_fn=<StackBackward>)GRU 的优势:GRU 和 LSTM 作用相同,在捕捉长序列语义关联时,能有效抑制梯度消失或爆炸,效果都优于传统 RNN,且计算复杂度相比 LSTM 要小。
GRU 的缺点:GRU 仍然不能完全解决梯度消失问题,同时其作为 RNN 的变体,有着 RNN 结构本身的一大弊端,即不可并行计算,这在数据量和模型体量逐步增大的未来,是 RNN 发展的关键瓶颈。