在 NLP 任务中主要处理带有序列关系的文本数据,这就需要了解循环(递归)神经网络。下图是一个简单的循环神经网络:
网络中包含一个神经元,但是它具有不同的时间步,能够提取出句子的顺序信息,将其展开如下图所示:
- h 表示 hidden state 隐藏状态,其含义为句子前面内容的语义;
- x 表示不同的时间步输入的数据;
- y 表示当前时间步的输出.
注意:
1. 对于 RNN 网络神经元每次输入有两个数据:x、h,输出的内容也有两个数据:y、h.
2. 不同的时间步共享权重
上面简单的 RNN 网络的计算公式如下:
1. Wih 是输入 x 的权重,Whh 是输入的 h 的权重;
2. bih 是输入 x 的偏置,bhh 是输入的 h 的权重。
3. 神经元输入有两个值: 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 就是预测最后词的隐藏状态输出
1. 长短期记忆网络(LSTM)
根据 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 是从水平方向增加网络的层数,这也可以增加网络的参数数量,使得网络能够适应更复杂的序列结构。
2. 门控循环单元(GRU)
从 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]]])