跳转至

2 传统RNN模型

学习目标

  • 了解传统RNN的内部结构及计算公式
  • 掌握Pytorch中传统RNN工具的使用
  • 了解传统RNN的优势与缺点

适用场景:

  • 短序列任务:对于较短的序列,传统RNN仍然是一个可行的选择,例如:简单的文本分类、情感分析等。
  • 计算资源有限的场景:在计算资源有限的情况下,传统RNN可以作为一种替代方案。
  • 作为学习RNN的基础:学习传统RNN是理解更复杂的RNN变体(如LSTM和GRU)的基础。

不适用场景:

  • 长序列任务:对于长序列数据,如长文本、长语音等,传统RNN的表现往往不佳,需要使用LSTM或GRU等更高级的模型。
  • 需要长期依赖的任务:对于需要记住长期信息的任务,传统RNN难以胜任。
  • 对训练稳定性要求较高的任务:由于梯度问题,传统RNN的训练可能不太稳定,需要仔细调整超参数。

2.1 传统RNN的内部结构图

  • 结构解释图:

    1737642357744

    1737642365890

  • 内部结构分析:

    我们把目光集中在中间的方块部分,它的输入有两部分:分别是\(h_{t-1}​\)以及\(x_t​\),代表上一时间步的隐藏层输出以及此时间步的输入。它们进入RNN结构体后,会”融合”到一起,这种融合我们根据结构解释可知是将二者进行拼接,形成新的张量[\(x_t​\), \(h_{t-1}​\)]。之后这个新的张量将通过一个全连接层(线性层),该层使用tanh作为激活函数,最终得到该时间步的输出\(h_t​\),它将作为下一个时间步的输入和\(x_{t+1}​\)一起进入结构体。以此类推…

  • 内部结构过程演示:

  • 根据结构分析得出内部计算公式:

    1737642916924

  • 激活函数tanh的作用:用于帮助调节流经网络的值,tanh函数将值压缩在-1和1之间。

2.2 Pytorch构建RNN模型

2.2.1 RNN函数

Pytorch中RNN函数为:

Python
out=torch.nn.RNN(input_size,hidden_size,num_layers,batch_first)

每个参数的含义如下:

  • input_size:输入数据的维数,也就是词嵌入的维度
  • hidden_size:隐藏层的维数
  • num_layers:隐藏层的层数
  • batch_first:当batch_first设置为True时,输入的参数x顺序变为:(batch_size, seq_len, input_size)

2.2.2 输入的表示

输入的表示形式,输入如下图所示:

1749048366404

输入主要有向量\(x\)、初始的\(h_0\),其中x:(seq_len, batch_size, input_size)h0:(num_layers, batch, hidden_size),每个参数的含义如下:

  • seq_len:输入序列的长度, 也就是句子的长度
  • batch_size:批次大小, 句子数
  • input_size:输入特征维度, 就是torch.nn.RNN(input_size,hidden_size,num_layers)中的input_size,二者要保持一致
  • num_layers:隐藏层层数, 与torch.nn.RNN中一致
  • hidden_size:隐藏层维度数, 与torch.nn.RNN中一致

2.2.3 输出的表示

RNN的输出可以是\(y\)向量和最后一个时刻隐藏层的输出\(h_T\)

  • 输出是\(y​\)向量,如下图所示:

    1749048467605

\(y\)向量的结构为out:(seq_len, batch_size, hidden_size),每个参数的意义与上述一致。

  • 输出是最后一个时刻隐含层的输出\(h_T​\),如下图所示:

    1749048546117

那么ht:(num_layers, batch_size, hidden_size),与h0结构完全一样。

2.2.4 RNN模型构建

1.句子长度为1的基础RNN模型代码:

Python
import torch
import torch.nn as nn

def dm_rnn_for_base():
        # 第一个参数:input_size(输入张量x的维度)
        # 第二个参数:hidden_size(隐藏层的维度,隐藏层的神经元个数)
        # 第三个参数:num_layer(隐藏层的数量)
        rnn = nn.RNN(input_size=5, hidden_size=6, num_layers=1) # A

        # 第一个参数:sequence_length(输入序列的长度),每个句子1个词
        # 第二个参数:batch_size(批次的样本数量),3个句子
        # 第三个参数:input_size(输入张量的维度),每个词用5维张量表示
        input = torch.randn(1, 3, 5) # B


        # 第一个参数:num_layer * num_directions(层数*网络方向)
        # 第二个参数:batch_size(批次的样本数)
        # 第三个参数:hidden_size(隐藏层的维度,隐藏层神经元的个数)
        h0 = torch.randn(1, 3, 6) # C

        # [1,3,5],[1,3,6] ---> [1,3,6],[1,3,6]
        output, hn = rnn(input, h0)

        print('output--->',output.shape, output)
        print('hn--->',hn.shape, hn)
        print('rnn模型--->', rnn)
        # 结论: 若句子只有1个词,output输出结果等于hn

输出结果:

Python
output---> torch.Size([1, 3, 6]) tensor([[[ 0.8947, -0.6040,    0.9878, -0.1070, -0.7071, -0.1434],
                 [ 0.0955, -0.8216,    0.9475, -0.7593, -0.8068, -0.5549],
                 [-0.1524,    0.7519, -0.1985,    0.0937,    0.2009, -0.0244]]],
             grad_fn=<StackBackward0>)

hn---> torch.Size([1, 3, 6]) tensor([[[ 0.8947, -0.6040,    0.9878, -0.1070, -0.7071, -0.1434],
                 [ 0.0955, -0.8216,    0.9475, -0.7593, -0.8068, -0.5549],
                 [-0.1524,    0.7519, -0.1985,    0.0937,    0.2009, -0.0244]]],
             grad_fn=<StackBackward0>)

rnn模型---> RNN(5, 6)

2.句子长度大于1的RNN模型代码示例:

Python
# 输入数据长度发生变化
def dm_rnn_for_sequencelen():
        # 第一个参数:input_size(输入张量x的维度)
        # 第二个参数:hidden_size(隐藏层的维度,隐藏层的神经元个数)
        # 第三个参数:num_layer(隐藏层的数量)
        rnn = nn.RNN(5, 6, 1) # A

        # 第一个参数:sequence_length(输入序列的长度),每个句子20个词
        # 第二个参数:batch_size(批次的样本数量),3个句子
        # 第三个参数:input_size(输入张量的维度),每个词用5维张量表示
        input = torch.randn(20, 3, 5) # B

        # 第一个参数:num_layer * num_directions(层数*网络方向)
        # 第二个参数:batch_size(批次的样本数)
        # 第三个参数:hidden_size(隐藏层的维度,隐藏层神经元的个数)
        h0 = torch.randn(1, 3, 6) # C

        # [20,3,5],[1,3,6] --->[20,3,6],[1,3,6]
        output, hn = rnn(input, h0)    

        print('output--->', output.shape, output)
        print('hn--->', hn.shape, hn)
        print('rnn模型--->', rnn)
        # 结果: 若句子由多个词组成, output的最后一组词向量(每个句子的最后一个词组成)等于h0

输出结果:

Python
output---> torch.Size([20, 3, 6]) tensor([[[-0.1175,    0.9412,    0.4137,    0.8700, -0.9546, -0.8624],
                 [-0.3786,    0.7321,    0.3956, -0.0332, -0.1671,    0.5342],
                 [-0.9199, -0.0294,    0.1968, -0.2076,    0.8274, -0.9380]],
             ...

                [[ 0.6937,    0.9086, -0.5759,    0.6497, -0.1622,    0.3149],
                 [ 0.3938, -0.1056, -0.4888, -0.0488,    0.1909,    0.1397],
                 [ 0.3134, -0.0552, -0.2793, -0.6143,    0.7715, -0.3820]]],
             grad_fn=<StackBackward0>)
hn---> torch.Size([1, 3, 6]) tensor([[[ 0.6937,    0.9086, -0.5759,    0.6497, -0.1622,    0.3149],
                 [ 0.3938, -0.1056, -0.4888, -0.0488,    0.1909,    0.1397],
                 [ 0.3134, -0.0552, -0.2793, -0.6143,    0.7715, -0.3820]]],
             grad_fn=<StackBackward0>)
rnn模型---> RNN(5, 6)

3.隐藏层大于1的RNN模型代码示例:

Python
def dm_run_for_hiddennum():
        # 第一个参数:input_size(输入张量x的维度)
        # 第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
        # 第三个参数:num_layer(隐藏层的数量)
        rnn = nn.RNN(5, 6, 2)    # A 隐藏层个数从1-->2 下面程序需要修改的地方?

        # 第一个参数:sequence_length(输入序列的长度)
        # 第二个参数:batch_size(批次的样本数量)
        # 第三个参数:input_size(输入张量的维度)
        input = torch.randn(1, 3, 5)    # B

        # 第一个参数:num_layer * num_directions(层数*网络方向)
        # 第二个参数:batch_size(批次的样本数)
        # 第三个参数:hidden_size(隐藏层的维度,隐藏层神经元的个数)
        h0 = torch.randn(2, 3, 6)    # C

        output, hn = rnn(input, h0)    
        print('output-->', output.shape, output)
        print('hn-->', hn.shape, hn)
        print('rnn模型--->', rnn)    # nn模型---> RNN(5, 6, num_layers=2)
        # 结论:若只有1个隐藏层,output输出结果等于hn
        # 结论:如果有2个隐藏层,hn有2个,output输出结果等于最后1个隐藏层的hn

输出结果:

Python
output--> torch.Size([1, 3, 6]) tensor([[[ 0.4987, -0.5756,    0.1934,    0.7284,    0.4478, -0.1244],
                 [ 0.6753,    0.5011, -0.7141,    0.4480,    0.7186,    0.5437],
                 [ 0.6260,    0.7600, -0.7384, -0.5080,    0.9054,    0.6011]]],
             grad_fn=<StackBackward0>)
hn--> torch.Size([2, 3, 6]) tensor([[[ 0.4862,    0.6872, -0.0437, -0.7826, -0.7136, -0.5715],
                 [ 0.8942,    0.4524, -0.1695, -0.5536, -0.4367, -0.3353],
                 [ 0.5592,    0.0444, -0.8384, -0.5193,    0.7049, -0.0453]],

                [[ 0.4987, -0.5756,    0.1934,    0.7284,    0.4478, -0.1244],
                 [ 0.6753,    0.5011, -0.7141,    0.4480,    0.7186,    0.5437],
                 [ 0.6260,    0.7600, -0.7384, -0.5080,    0.9054,    0.6011]]],
             grad_fn=<StackBackward0>)
rnn模型---> RNN(5, 6, num_layers=2)

2.3 传统RNN优缺点

2.3.1 传统RNN的优点

由于内部结构简单,对计算资源要求低,相比之后我们要学习的RNN变体(LSTM和GRU模型)参数总量少了很多,在短序列任务上性能和效果都表现优异。

  • 结构简单,易于理解和实现:
    • 传统RNN的结构非常直观,只有一个隐藏层和循环连接,因此容易理解其工作原理。
    • 由于结构简单,其代码实现也相对容易。
  • 适用于处理序列数据:
    • 传统RNN的核心优势在于它能够处理具有顺序关系的数据,例如文本、语音、时间序列等。
    • 通过循环连接,RNN可以将过去的信息传递到当前时刻,从而学习序列数据中的依赖关系。
  • 能够处理可变长度的序列:
    • 传统RNN可以处理长度不固定的序列,而无需提前固定输入数据的长度。
    • 这使得它在处理自然语言、语音等具有可变长度数据的任务中非常灵活。
  • 参数共享:
    • 传统RNN在每个时间步都共享相同的参数(权重矩阵和偏置项),从而减少了模型参数的数量。
    • 参数共享还允许RNN学习序列数据中跨时间步的通用模式。
  • 计算效率较高 (相对于更复杂的RNN变体):
    • 由于结构简单,传统 NN的计算复杂度相对较低,训练速度相对较快。
    • 在一些计算资源有限的场景下,传统RNN是一个可行的选择。

2.3.2 传统RNN的缺点

传统RNN在解决长序列之间的关联时,通过实践证明经典RNN表现很差。原因是在进行反向传播的时候,过长的序列导致梯度的计算异常,发生梯度消失或爆炸。

  • 梯度消失问题:
    • 这是传统RNN最主要的缺点。在处理长序列时,梯度在反向传播过程中会逐渐衰减,导致网络无法学习到长距离依赖关系。
    • 梯度消失使得模型难以捕捉输入序列中较早的信息,从而限制了其在长序列任务上的性能。
  • 梯度爆炸问题:
    • 与梯度消失相反,在某些情况下,梯度在反向传播过程中可能会呈指数级增长,导致训练不稳定甚至发散。
    • 梯度爆炸也限制了传统RNN在某些任务中的应用。
  • 难以捕捉长期依赖:
    • 由于梯度消失问题,传统RNN很难学习长距离的依赖关系,即难以记住输入序列中较早的信息,并将其用于影响后面的输出。
    • 这意味着在处理文本、语音等长序列时,传统RNN很难理解上下文的含义。
  • 训练不稳定:

    • 由于梯度消失和梯度爆炸问题,传统RNN的训练过程可能会不稳定,需要小心调整超参数。
    • 无法并行计算:

    • 由于RNN的循环结构,每个时间步的计算都依赖于前一个时间步的隐藏状态,因此无法进行并行计算,训练速度受到限制。

  • 梯度消失或爆炸介绍

    • 根据反向传播算法和链式法则, 梯度的计算可以简化为以下公式:

      1737644553695

    • 其中sigmoid的导数值域是固定的,在[0,0.25]之间。而一旦公式中的w也小于1,那么通过这样的公式连乘后,最终的梯度就会变得非常非常小,这种现象称作梯度消失。反之,如果我们人为的增大w的值,使其大于1,那么连乘够就可能造成梯度过大,称作梯度爆炸。

    • 梯度消失或爆炸的危害:

      如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败;梯度爆炸所带来的梯度过大,大幅度更新网络参数,在极端情况下,结果会溢出(NaN值)。

2.4 小结

  • 学传统RNN的结构:
    • 它的输入有两部分, 分别是h(t-1)以及x(t), 代表上一时间步的隐层输出, 以及此时间步的输入, 它们进入RNN结构体后, 会”融合”到一起, 这种融合我们根据结构解释可知, 是将二者进行拼接, 形成新的张量[x(t), h(t-1)], 之后这个新的张量将通过一个全连接层(线性层), 该层使用tanh作为激活函数, 最终得到该时间步的输出h(t), 它将作为下一个时间步的输入和x(t+1)一起进入结构体. 以此类推.
  • nn.RNN类初始化主要参数解释:
    • input_size: 输入张量x中特征维度的大小
    • hidden_size: 隐层张量h中特征维度的大小
    • num_layers: 隐含层的数量
    • nonlinearity: 激活函数的选择, 默认是tanh
  • nn.RNN类实例化对象主要参数解释:
    • input: 输入张量x
    • h0: 初始化的隐层张量h
  • 传统RNN的优势:
    • 由于内部结构简单, 对计算资源要求低, 相比之后我们要学习的RNN变体:LSTM和GRU模型参数总量少了很多, 在短序列任务上性能和效果都表现优异.
  • 传统RNN的缺点:
    • 传统RNN在解决长序列之间的关联时, 通过实践,证明经典RNN表现很差, 原因是在进行反向传播的时候, 过长的序列导致梯度的计算异常, 发生梯度消失或爆炸.
  • 什么是梯度消失或爆炸:
    • 根据反向传播算法和链式法则, 得到梯度的计算的简化公式:其中sigmoid的导数值域是固定的, 在[0, 0.25]之间, 而一旦公式中的w也小于1, 那么通过这样的公式连乘后, 最终的梯度就会变得非常非常小, 这种现象称作梯度消失. 反之, 如果我们人为的增大w的值, 使其大于1, 那么连乘够就可能造成梯度过大, 称作梯度爆炸.
  • 梯度消失或爆炸的危害:
    • 如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败; 梯度爆炸所带来的梯度过大,大幅度更新网络参数,在极端情况下,结果会溢出(NaN值).