PyTorch 中 GRU 和 LSTM 循环网络层

在 NLP 任务中主要处理带有序列关系的文本数据,这就需要了解循环(递归)神经网络。下图是一个简单的循环神经网络:

网络中包含一个神经元,但是它具有不同的时间步,能够提取出句子的顺序信息,将其展开如下图所示:

  1. h 表示 hidden state 隐藏状态,其含义为句子前面内容的语义;
  2. x 表示不同的时间步输入的数据;
  3. y 表示当前时间步的输出.

注意:
1. 对于 RNN 网络神经元每次输入有两个数据:x、h,输出的内容也有两个数据:y、h.
2. 不同的时间步共享权重

上面简单的 RNN 网络的计算公式如下:

1. Wih 是输入 x 的权重,Whh 是输入的 h 的权重;
2. bih 是输入 x 的偏置,bhh 是输入的 h 的权重。
3. 神经元输入有两个值: x、h,对这两个输入都是得经过线性变换,求和,最后经过激活函数输出.经过激活函数的输出作为新的时间步对应的隐藏状态 h 的值.

假设输入:我是谁,则:

  1. x=我、初始隐藏状态 h0(一般值为0的张量)经过上述公式计算,得到 h1;
  2. x=是、隐藏状态 h1 经过上述公式计算,得到 h2;
  3. x=谁、隐藏状态 h2 经过上述公式计算,得到 h3;
  4. 注意:三次输入共享参数.

这个过程即循环神经网络的前向传播,反向传播也可以通过展开的神经网络沿着时间的方向进行反向计算、积累梯度,这种方法也叫做: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 ]]]
  1. outputs 输出的 shape 是 (5, 1, 4) 表示每个词送入到 RNN 得到的 hidden_state, 每个 hidden_state 的 shape 是 (1, 4)
  2. 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]]])
未经允许不得转载:一亩三分地 » PyTorch 中 GRU 和 LSTM 循环网络层