循环神经网络(RNN)

循环神经网络(Recurrent Neural Network, RNN)是一类用于处理序列数据的神经网络。

什么是序列数据?
序列数据是指按照一定顺序排列的数据集合,其中的每个元素被称为序列的一个项。序列数据可以是有限的,也可以是无限的。简言之:

  • 每个元素都有一个位置,其位置对于理解和处理数据很重要。
  • 序列数据中的元素之间可能存在依赖关系。

比如:我爱你、你爱我

1. 算法原理

RNN 的基本思想是通过循环连接的方式使网络能够记住先前输入的信息,从而在处理当前输入时考虑到之前的信息。RNN 通过隐藏状态(hidden state)来存储之前时间步的信息,每次对输入的表示都会结合之前的信息来表示。

具体的计算公式如下:

  • \(h_t\)​:在时间步 t 的隐藏状态向量
  • \(h_{t-1}\)​:在时间步 t-1 的隐藏状态向量
  • \(x_t\):在时间步 t 的输入向量
  • \(W_{ih}\):输入 x 对应的权重矩阵
  • \(b_{ih}\)​:输入 x 对应的偏置向量
  • \(W_{hh}\):隐藏状态对应的权重矩阵
  • \(b_{hh}\)​:隐藏状态对应的偏置向量
  • \(tanh\)​:隐藏层激活函数

2. 算法使用

在 PyTorch 中,nn.RNNCellnn.RNN 都用于构建循环神经网络(RNN),但它们的用途和适用场景有所不同。

2.1 nn.RNN

import torch
import torch.nn as nn


def test():
    # 固定网络模型参数
    torch.manual_seed(42)

    # 1. 重点: 对象参数
    # input_size 输入数据维度
    # hidden_size 隐藏状态维度(神经元个数,输出数据维度)
    rnn = nn.RNN(input_size=2, hidden_size=4, batch_first=False)

    # 2. 重点: 输入
    if rnn.batch_first:
        # 构造输入(batch_size, seq_len, dim)
        inputs = torch.rand(1, 3, 2)
    else:
        # 构造输入(seq_len, batch_size, dim)
        inputs = torch.rand(3, 1, 2)

    # 3. 重点: 输出
    # output 每个元素的隐藏状态
    # hn 最后一个元素的隐藏状态
    output, hn = rnn(inputs)
    # output shape: torch.Size([3, 1, 4]) hn shape: torch.Size([1, 1, 4])
    print('output shape:', output.shape, 'hn shape:', hn.shape)

    # 4. 重点:参数量
    # weight_ih_l0 torch.Size([4, 2])
    # weight_hh_l0 torch.Size([4, 4])
    # bias_ih_l0 torch.Size([4])
    # bias_hh_l0 torch.Size([4])
    for name, parameters in rnn.named_parameters():
        print(name, parameters.size())


if __name__ == '__main__':
    test()

2.2 num_players

num_layers 参数在 PyTorch 的 nn.RNN 模块中定义了堆叠在一起的 RNN 层的数量。每一层的隐藏状态将作为下一层的输入,从而使模型能够学习更复杂的表示和捕捉更高层次的特征。

单层 RNN 可能只能捕捉到输入序列的低级别特征。通过堆叠多层 RNN,可以逐层提取更加抽象和高级的特征,从而提高模型的表达能力和性能。

import torch
import torch.nn as nn


def test():
    torch.manual_seed(42)
    # num_layers 设置层数
    rnn = nn.RNN(input_size=2, hidden_size=4, num_layers=2)
    inputs = torch.rand(3, 1, 2)
    output, hn = rnn(inputs)
    print('output shape:', output.shape)
    print('hn shape:', hn.shape)
    print('-' * 30)
    for name, parameters in rnn.named_parameters():
        print(name, parameters.size())

if __name__ == '__main__':
    test()

程序输出结果:

output shape: torch.Size([3, 1, 4])
hn shape: torch.Size([2, 1, 4])
------------------------------
weight_ih_l0 torch.Size([4, 2])
weight_hh_l0 torch.Size([4, 4])
bias_ih_l0 torch.Size([4])
bias_hh_l0 torch.Size([4])
weight_ih_l1 torch.Size([4, 4])
weight_hh_l1 torch.Size([4, 4])
bias_ih_l1 torch.Size([4])
bias_hh_l1 torch.Size([4])

2.3 bidirectional

在很多自然语言处理(NLP)和序列处理任务中,当前时间步的输出不仅依赖于之前的时间步,还可能依赖于之后的时间步。

通过结合前向和后向的隐藏状态,双向 RNN 能够提供更丰富的上下文信息。这通常可以提高模型在各种任务上的表现,例如文本分类、命名实体识别和语言建模等。更多的信息通常意味着更准确的预测和更好的泛化性能。

在实现上,双向 RNN 包含两个独立的 RNN,一个处理正向序列(从前到后),一个处理反向序列(从后到前)。每个方向都有自己的一组权重参数。

import torch
import torch.nn as nn


def test():

    torch.manual_seed(42)
    rnn = nn.RNN(input_size=2, hidden_size=3, bidirectional=True)
    rnn_inputs = torch.rand(3, 1, 2)

    output, hn = rnn(rnn_inputs)

    # weight_ih_l0 torch.Size([3, 2])
    # weight_hh_l0 torch.Size([3, 3])
    # bias_ih_l0 torch.Size([3])
    # bias_hh_l0 torch.Size([3])
    # weight_ih_l0_reverse torch.Size([3, 2])
    # weight_hh_l0_reverse torch.Size([3, 3])
    # bias_ih_l0_reverse torch.Size([3])
    # bias_hh_l0_reverse torch.Size([3])
    for name, parameters in rnn.named_parameters():
        print(name, parameters.size())
    print('-' * 67)

    # output shape: torch.Size([3, 1, 6]) hn shape: torch.Size([2, 1, 3])
    print('output shape:', output.shape, 'hn shape:', hn.shape)
    print('-' * 67)

    print(output)
    print('-' * 67)

    print(hn)


if __name__ == '__main__':
    test()

程序输出结果:

weight_ih_l0 torch.Size([3, 2])
weight_hh_l0 torch.Size([3, 3])
bias_ih_l0 torch.Size([3])
bias_hh_l0 torch.Size([3])
weight_ih_l0_reverse torch.Size([3, 2])
weight_hh_l0_reverse torch.Size([3, 3])
bias_ih_l0_reverse torch.Size([3])
bias_hh_l0_reverse torch.Size([3])
-------------------------------------------------------------------
output shape: torch.Size([3, 1, 6]) hn shape: torch.Size([2, 1, 3])
-------------------------------------------------------------------
tensor([[[ 0.3936,  0.7608, -0.1589, -0.0829,  0.1864, -0.5768]],

        [[ 0.1960,  0.6819, -0.0645,  0.1314,  0.1977, -0.5505]],

        [[ 0.3993,  0.7820, -0.0925, -0.2763,  0.5750, -0.3795]]],
       grad_fn=<CatBackward0>)
-------------------------------------------------------------------
tensor([[[ 0.3993,  0.7820, -0.0925]],

        [[-0.0829,  0.1864, -0.5768]]], grad_fn=<StackBackward0>)

2.4 dropout

dropout 参数表示的是一种正则化技术,用于防止神经网络的过拟合。过拟合是指模型在训练数据上表现很好,但在测试数据上表现不佳,原因是模型过于复杂,学到了训练数据中的噪声。

Dropout 的基本思想是在训练过程中,随机将一部分神经元的输出设为零,从而减少神经元之间的相互依赖。这在训练过程中强制模型学习更加鲁棒的特征。

nn.RNN 中,如果设置了非零的 dropout 参数值,那么在每一层 RNN(循环神经网络)的输出之后(除了最后一层),都会添加一个 Dropout 层。

该 Dropout 层的概率等于 dropout 参数的值。例如,如果 dropout=0.5,那么每一层 RNN 输出的神经元有 50% 的概率被设置为零。

默认情况下,dropout 参数是 0,意味着不使用 Dropout。

import torch
import torch.nn as nn


def test():
    torch.manual_seed(42)
    rnn = nn.RNN(input_size=2, hidden_size=3, dropout=0.2, num_layers=2)
    rnn_inputs = torch.rand(3, 1, 2)
    output, hn = rnn(rnn_inputs)
    print(hn)


if __name__ == '__main__':
    test()

2.5 RNNCell

nn.RNNCell 代表单个时间步的 RNN 单元,它用于需要对每个时间步手动进行处理的场景。适合高级用户或需要对 RNN 的时间步迭代进行细粒度控制的场景。使用 nn.RNNCell 可以更灵活地操作隐藏状态,适合构建自定义的 RNN 结构或在循环体中嵌入其他操作。

import torch
import torch.nn as nn


def test01():
    # 固定网络模型参数
    torch.manual_seed(42)
    # input_size 输入数据维度
    # hidden_size 隐藏状态维度(神经元个数,输出数据维度)
    rnn = nn.RNN(input_size=4, hidden_size=6)
    # 构造输入(seq_len, batch_size, dim)
    rnn_inputs = torch.rand(3, 2, 4)
    output, hn = rnn(rnn_inputs)

    # 输出结果
    print(output.squeeze())
    print(hn)


def test02():
    # 固定网络模型参数
    torch.manual_seed(42)

    # input_size 输入数据维度
    # hidden_size 神经元数量(输出维度)
    rnn = nn.RNNCell(input_size=4, hidden_size=6)
    # 构造输入(seq_len, batch_size, dim)
    rnn_inputs = torch.rand(3, 2, 4)
    # 初始化隐藏状态(每个序列分别初始化隐藏状态)
    rnn_hidden = torch.zeros(2, 6)
    # 保存隐藏状态
    hidden_states = []

    # 训练计算时间隐藏状态
    for idx in range(rnn_inputs.shape[0]):
        rnn_hidden = rnn(rnn_inputs[idx], rnn_hidden)
        hidden_states.append(rnn_hidden)

    output = torch.stack(hidden_states)
    hn = rnn_hidden

    # 输出结果
    print(output)
    print(hn)

if __name__ == '__main__':
    test01()
    print('-' * 30)
    test02()

程序输出结果:

tensor([[[ 0.6295, -0.5197,  0.2157, -0.0886,  0.6734, -0.1085],
         [ 0.7314, -0.3738,  0.0418, -0.2117,  0.5020,  0.1213]],

        [[ 0.4572, -0.2470,  0.1893, -0.0327,  0.3821,  0.2097],
         [ 0.5103, -0.3171,  0.3264,  0.1896,  0.6496, -0.0635]],

        [[ 0.5079, -0.2033, -0.0015, -0.0311,  0.5142,  0.0714],
         [ 0.6826, -0.1805, -0.0191,  0.0802,  0.5302,  0.1724]]],
       grad_fn=<SqueezeBackward0>)
tensor([[[ 0.5079, -0.2033, -0.0015, -0.0311,  0.5142,  0.0714],
         [ 0.6826, -0.1805, -0.0191,  0.0802,  0.5302,  0.1724]]],
       grad_fn=<StackBackward0>)
------------------------------
tensor([[[ 0.6295, -0.5197,  0.2157, -0.0886,  0.6734, -0.1085],
         [ 0.7314, -0.3738,  0.0418, -0.2117,  0.5020,  0.1213]],

        [[ 0.4572, -0.2470,  0.1893, -0.0327,  0.3821,  0.2097],
         [ 0.5103, -0.3171,  0.3264,  0.1896,  0.6496, -0.0635]],

        [[ 0.5079, -0.2033, -0.0015, -0.0311,  0.5142,  0.0714],
         [ 0.6826, -0.1805, -0.0191,  0.0802,  0.5302,  0.1724]]],
       grad_fn=<StackBackward0>)
tensor([[ 0.5079, -0.2033, -0.0015, -0.0311,  0.5142,  0.0714],
        [ 0.6826, -0.1805, -0.0191,  0.0802,  0.5302,  0.1724]],
       grad_fn=<TanhBackward0>)

未经允许不得转载:一亩三分地 » 循环神经网络(RNN)
评论 (0)

5 + 4 =