在 NLP 任务中主要处理带有序列关系的文本数据,这就需要了解循环(递归)神经网络。下图是一个简单的循环神经网络:
网络中包含一个神经元,但是它具有不同的时间步,能够提取出句子的顺序信息,将其展开如下图所示:
- h 表示 hidden state 隐藏状态,其含义为句子前面内容的语义;
- x 表示不同的时间步输入的数据;
- y 表示当前时间步的输出.
注意:
- 对于 RNN 网络神经元每次输入有两个数据:x、h,输出的内容也有两个数据:y、h.
- 不同的时间步共享权重
上面简单的 RNN 网络的计算公式如下:
- W~ih~ 是输入 x 的权重,W~hh~ 是输入的 h 的权重;
- b~ih~ 是输入 x 的偏置,b~hh~ 是输入的 h 的权重。
- 神经元输入有两个值: x、h,对这两个输入都是得经过线性变换,求和,最后经过激活函数输出.经过激活函数的输出作为新的时间步对应的隐藏状态 h 的值.
假设输入:我是谁,则:
- x=我、初始隐藏状态 h0(一般值为0的张量)经过上述公式计算,得到 h1;
- x=是、隐藏状态 h1 经过上述公式计算,得到 h2;
- x=谁、隐藏状态 h2 经过上述公式计算,得到 h3;
- 注意:三次输入共享参数.
这个过程即循环神经网络的前向传播,反向传播也可以通过展开的神经网络沿着时间的方向进行反向计算、积累梯度,这种方法也叫做:BPTT(Back Propagation Though Time,基于时间步的反向传播)。
最后的输出的 h3 可以理解为包含了整个句子的语义信息,包括:字词的信息、序列信息等。GRU 和 LSTM 是以 RNN 为基础来构建的更好、更有效的序列模型。
import torch
import torch.nn as nn
def test():
# 初始化 rnn 网络
# input_size 表示输入的数据的每个词的维度是2
# hidden_size 表示有4个神经元,它会影响到输出数据的维度
# num_layers 表示有1个层
rnn = nn.RNN(input_size=2, hidden_size=4, num_layers=1)
# 初始化隐藏状态 [num_layers, batch_size, hidden_size]
hidden_state = torch.zeros(1, 1, 4)
# 数据输入到网络 [sentence_length, batch_size, input_size]
# 下面数据表示: 1个句子, 每个句子5个词长度,每个词使用4个维度表示
# 注意每个词的维度要和网络的 input_size 匹配
inputs = torch.randint(0, 1, size=[5, 1, 2]).float()
outputs, hidden_state = rnn(inputs, hidden_state)
# 打印输出结果
print('outputs shape:', outputs.shape, 'hidden_state shape:', hidden_state.shape)
print('hidden_state:\n', hidden_state.data.numpy())
print('outputs:\n', outputs.data.numpy())
if __name__ == '__main__':
test()
程序输出结果:
outputs shape: torch.Size([5, 1, 4]) hidden_state shape: torch.Size([1, 1, 4])
hidden_state:
[[[-0.39084607 -0.11768528 0.13888825 0.1998262 ]]]
outputs:
[[[-0.44237813 -0.16170034 0.12351193 0.33893934]]
[[-0.4135559 -0.09886695 0.14220692 0.14758277]]
[[-0.3595522 -0.12520736 0.14719774 0.20742273]]
[[-0.38422772 -0.12500143 0.12881942 0.2235737 ]]
[[-0.39084607 -0.11768528 0.13888825 0.1998262 ]]]
-
outputs 输出的 shape 是 (5, 1, 4) 表示每个词送入到 RNN 得到的 hidden_state, 每个 hidden_state 的 shape 是 (1, 4)
-
hidden_state 的值和 outputs 的最后一行数据相同,这也说明了 hidden_state 就是预测最后词的隐藏状态输出
-
长短期记忆网络(LSTM) {#title-0} ===========================
根据 tanh 激活函数 http://mengbaoliang.cn/?p=22588 的函数图像、导数图像可见,如果激活值的绝对值接近于 1 的话,那么其梯度值就接近于 0,造成梯度消失,这样在反向传播的过程中,导致网络更新不动。
从另外一个角度也可以理解为随着句子长度增加,RNN 无法保留更多的语句的信息,也就是说 RNN 很难对更长的句子信息进行有效的提取。
LSTM(Long Short-term Memory Network)不同于简单的 RNN 网络,它的隐藏状态是由两个状态共同组成的,即:细胞状态 C 和隐藏状态 H,如下图所示:
上图是一个 LSTM 神经元的输入、输出、计算过程。输入有三个,分别是:x、h、c,输出同样有三个值:y、h、c,具体计算公式如下:
LSTM 看起来确实比 RNN 复杂多了,其思想主要是缓解在反向传播过程中梯度消失的问题,从而使得网络能够输入更长的文本。
import torch
import torch.nn as nn
def test():
# 初始化 LSTM 循环神经网络
# input_size 表示输入数据的维度
# hidden_size 表示神经元的个数,会影响到输入数据的维度
# num_layers 表示层数
lstm = nn.LSTM(input_size=2, hidden_size=4, num_layers=1)
# 初始化输入数据 [sentence_length, batch_size, input_size]
inputs = torch.randint(0, 10, size=[5, 1, 2]).float()
# 初始化细胞状态 [num_layers, batch_size, hidden_size]
c = torch.zeros(1, 1, 4)
# 初始化隐藏状态 [num_layers, batch_size, hidden_size]
h = torch.zeros(1, 1, 4)
outputs, (h, c) = lstm(inputs, (h, c))
print('outputs shape:', outputs.shape, 'h shape:', h.shape, 'c shape:', c.shape)
print(outputs.data)
print(h.data)
print(c.data)
if __name__ == '__main__':
test()
程序输出结果:
outputs shape: torch.Size([5, 1, 4]) h shape: torch.Size([1, 1, 4]) c shape: torch.Size([1, 1, 4])
tensor([[[ 0.0204, 0.4493, -0.5026, -0.2404]],
[[-0.2598, 0.2232, -0.2666, -0.0971]],
[[-0.3835, 0.2697, -0.4898, -0.1185]],
[[-0.1795, 0.7796, -0.4930, -0.4654]],
[[-0.0772, 0.6699, -0.4304, -0.4343]]])
tensor([[[-0.0772, 0.6699, -0.4304, -0.4343]]])
tensor([[[-0.0862, 3.5959, -0.6075, -1.6546]]])
我们从结果也可以看到,LSTM 的隐藏状态与 outputs 的最后一个元素相同。
由于循环神经网络在垂直方向增加神经元的个数,num_layers 是从水平方向增加网络的层数,这也可以增加网络的参数数量,使得网络能够适应更复杂的序列结构。
- 门控循环单元(GRU) {#title-1} =========================
从 LSTM 神经元的内部结构来看,其计算量比 RNN 大不少,而 GRU 则是对 LSTM 做了一定程度上的简化。其神经元内部结构图如下:
上图中的圆圈表示按元素逐个做乘积,其计算公式如下: 据相关研究表示,对于不同的自然语言处理问题,LSTM 和 GRU 的表现差异不大。
import torch
import torch.nn as nn
def test():
# 初始化门控循环单元
gru = nn.GRU(input_size=2, hidden_size=4, num_layers=1)
# 初始化输入数据
inputs = torch.randint(0, 1, size=[5, 1, 2]).float()
# 初始化隐藏状态
hidden_state = torch.zeros(1, 1, 4)
outputs, hidden_state = gru(inputs, hidden_state)
# 打印计算结果
print('outputs shape:', outputs.shape, 'hidden_state shape:', hidden_state.shape)
print(outputs.data)
print(hidden_state.data)
if __name__ == '__main__':
test()
程序输出结果:
outputs shape: torch.Size([5, 1, 4]) hidden_state shape: torch.Size([1, 1, 4])
tensor([[[-0.0112, 0.1688, 0.2208, -0.2205]],
[[-0.0132, 0.2716, 0.2834, -0.3462]],
[[-0.0043, 0.3369, 0.2973, -0.4253]],
[[ 0.0098, 0.3796, 0.2964, -0.4776]],
[[ 0.0244, 0.4082, 0.2922, -0.5132]]])
tensor([[[ 0.0244, 0.4082, 0.2922, -0.5132]]])