NLP基础之传统RNN模型

发布于:2025-03-03 ⋅ 阅读:(17) ⋅ 点赞:(0)

RNN及其变体

RNN简述

RNN(Recurrent Neural Network),中文称作循环神经网络,是一种专门用于处理序列数据的神经网络架构。一般以序列数据为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,一般也是以序列形式进行输出。它的特点是能够捕捉序列数据中的时间依赖关系,广泛应用于自然语言处理(NLP)、时间序列预测、语音识别等领域。

  • 序列数据 (Sequential Data):指的是数据项之间存在顺序关系的数据,例如:
    • 文本:句子中的单词顺序
    • 语音:音频信号的时间序列
    • 股票价格:随时间变化的股价
    • 视频:连续的帧序列
  • 循环连接 (Recurrent Connection):RNN的核心在于其循环连接,它允许信息在网络内部循环流动。这使得网络可以保留过去的信息,并将其用于处理当前的输入。
  • 隐藏状态 (Hidden State):RNN在每个时间步都会维护一个隐藏状态,这个隐藏状态包含了过去的信息。它可以被认为是网络的"记忆"。
  • 时间步 (Time Step):指的是序列数据中的每个元素,例如文本中的一个单词、语音中的一个音频帧。

基本工作原理

  1. **输入:**RNN接收一个序列数据作为输入,每次输入序列中的一个元素 (时间步)。

  2. **隐藏状态更新:**对于每个时间步,RNN根据当前的输入和上一个时间步的隐藏状态计算出新的隐藏状态。这个计算通常使用激活函数(如tanh或ReLU)。

    • 隐藏状态更新公式

    h t = σ ( W h h t − 1 + W x x t + b ) h_t=σ(W_hh_{t−1}+W_xx_t+b) ht=σ(Whht1+Wxxt+b)

    Wh,Wx |:权重矩阵

    ht |:t时刻的隐藏状态

  3. **输出:**RNN根据当前的隐藏状态计算出当前的输出。

  4. **循环:**上述过程会在序列的每个时间步重复进行,直到序列结束。

在这里插入图片描述

📓 理解RNN的关键:按时间步对RNN进行展开

在这里插入图片描述

RNN模型分类

  • 概览
类型 输入结构 输出结构 应用场景
N vs N 等长序列输入 等长序列输出 词性标注、逐点预测
N vs 1 序列输入 单个时间步输出 文本分类、情感分析
1 vs N 单个时间步输入 序列输出 文本/音乐生成、图像描述生成
N vs M 不等长序列输入 不等长序列输出 机器翻译、文本摘要

1.按输出和输出的结构分类

  • N vs N:输入和输出序列等长. 适用范围较小,可以用于生成等长度的合辙诗句

    在这里插入图片描述

  • 1 vs N:输入是一个时间步[ 输入(一个词/值/向量) ], 输出是一个序列数据, 常用于文本/音乐生成/图片描述生成

    在这里插入图片描述

  • N vs 1:输入是一个序列数据,一个时间步[ 输入(一个词/值/向量) ], 常用于情感/文本分类任务

在这里插入图片描述

  • N V M模式: 输入输出都是序列数据, 但是不等长,常用于机器翻译/语音识别/文本摘要

    • 这是一种不限输入输出长度的RNN结构,它由编码器和解码器两部分组成,两者的内部结构都是某类RNN,它也被称为seq2seq架构。输入数据首先通过编码器, 最终输出一个隐含变量c, 之后最常用的做法是使用这个隐含变量c作用在解码器进行解码的每一步上,以保证输入信息被有效利用。

      seq2seq架构最早被提出应用于机器翻译,因为其输入输出不受限制,如今也是应用最广的RNN模型结构。在机器翻译、阅读理解、文本摘要等众多领域都进行了非常多的应用实践。

在这里插入图片描述

2.按RNN内部构造分类

  • 概览

    类型
    传统RNN
    LSTM
    Bi-LSTM
    GRU
    Bi-GRU
1.传统RNN模型
概况

[内部结构示意图]

在这里插入图片描述

[解读] :在 t 时刻时(对应图片的中间),存在两部分输入:ht-1 和 Xt ,代表上一时间步的隐藏层输出以及此时间步的输入

在进入RNN结构体后:

​ 1.它们会"融合"到一起,形成新的张量[xt, ht−1]。

​ 2.这个新的张量[xt, ht−1] 将通过一个全连接层(线性层),该层使用tanh作为激活函数,最终得到该时间步的输出ht,它将作为下一个时间步的输入和xt+1一起进入结构体。

即进行了以下运算:

h t = t a n h ( W t [ X t , h t − 1 ] + b t ) h_t = tanh(W_t[X_t,h_{t-1}] + b_t) ht=tanh(Wt[Xt,ht1]+bt)

对这一过程在pytorch有另一种解读:

pytorch对这两部分输入:ht-1 和 Xt 分配了各自的权重和偏置项,并进行了以下运算(两次线性运算 + tanh激活)
h t = tanh ⁡ ( x t W i h T + b i h + h t − 1 W h h T + b h h ) h_t = \tanh(x_t W_{ih}^T + b_{ih} + h_{t-1}W_{hh}^T + b_{hh}) ht=tanh(xtWihT+bih+ht1WhhT+bhh)

  1. 重复前两步直到循环结束
  2. 将所有时间步的隐藏层输出{hi , i = 1,2,…}合并为output,将output的最后的张量(output[-1],即 hn)作为全连接层的输入,进行预测
  3. 计算损失,反向传播,参数更新
pytorch构建RNN模型

[API] : torch.nn.RNN

# torch.nn.RNN 的 __init__参数说明
__init__(input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True, batch_first=False, dropout=0.0, bidirectional=False, device=None, dtype=None)
  • input_size:输入特征的数量,即每个时间步输入的维度。[词向量维度]

  • hidden_size:隐藏状态的特征数量,即每个时间步隐藏状态的维度。

  • num_layers:循环层的数量。例如,设置 num_layers=2 表示堆叠两个 RNN 层。默认值为 1。

  • nonlinearity:使用的激活函数,可以是 'tanh''relu'。默认值为 'tanh'

  • bias:如果为 False,则该层不使用偏置权重 b_ihb_hh。默认值为 True

  • batch_first:如果为 True,则输入和输出张量的形状为 (batch, seq, feature),否则为 (seq, batch, feature)。默认值为 False

  • dropout:如果非零,则在每个 RNN 层(除了最后一层)的输出后引入一个 Dropout 层,丢弃概率为 dropout。默认值为 0。

  • bidirectional:如果为 True,则使用双向 RNN。默认值为 False

  • device:指定模型所在的设备。

  • dtype:指定模型参数的数据类型。


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

def dm01():
    """
    构建句子长度为 1 的基础 RNN模型
    :return:None
    """
    # 0- 手动设定随机种子方便后续复现
    torch.manual_seed(12)

    # 1- 构建 RNN模型
    # input_size: 词向量维度
    # hidden_size: 隐藏层维度
    # num_layers: 隐藏层层数
    rnn = nn.RNN(input_size=3, hidden_size=4, num_layers=1)

    # 2- 定义输出向量
    # size: (seq_len,batch_size,input_dim)
    # seq_len:句子长度
    # batch_size: 批次的样本数
    # input_dim=input_size: 词向量维度
    input = torch.randn(1, 2, 3)

    # 3- 定义初始隐藏状态张量 h0
    # size:(num_layer * 网络方向数,batch_size,hidden_size)
    # num_layer: 隐藏层层数
    # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
    # batch_size: 样本批次数
    # hidden_size: 隐藏层维度
    h0 = torch.zeros(1, 2, 4)

    ## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
    ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
    ### 示例:output, hn = rnn(input)
    # 4- 使用RNN模型
    output, hn = rnn(input, h0)

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

输出结果

output-> tensor(
		[[[-0.2506, -0.3442, -0.2307,  0.1333],
      [-0.1602, -0.1754, -0.5785, -0.3782]]]
, grad_fn=<StackBackward0>) torch.Size([1, 2, 4])
hn-> tensor(
		[[[-0.2506, -0.3442, -0.2307,  0.1333],
      [-0.1602, -0.1754, -0.5785, -0.3782]]]
, grad_fn=<StackBackward0>) torch.Size([1, 2, 4])

结论: 若句子只有1个词,output输出结果等于hn

  • 句子长度大于 1 的RNN模型
def dm02():
    """
    构建句子长度大于 1 的基础 RNN模型
    :return:None
    """
    # 0- 手动设定随机种子方便后续复现
    torch.manual_seed(12)

    # 1- 构建 RNN模型
    # input_size: 词向量维度
    # hidden_size: 隐藏层维度
    # num_layers: 隐藏层层数
    rnn = nn.RNN(input_size=4, hidden_size=3, num_layers=1)

    # 2- 定义输出向量
    # size: (seq_len,batch_size,input_dim)
    # seq_len:句子长度
    # batch_size: 批次的样本数
    # input_dim=input_size: 词向量维度
    input = torch.randn(3, 2, 4)

    # 3- 定义初始隐藏状态张量 h0
    # size:(num_layer * 网络方向数,batch_size,hidden_size)
    # num_layer: 隐藏层层数
    # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
    # batch_size: 样本批次数
    # hidden_size: 隐藏层维度
    h0 = torch.zeros(1, 2, 3)

    ## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
    ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
    ### 示例:output, hn = rnn(input)
    # 4- 使用RNN模型
    output, hn = rnn(input, h0)

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

输出结果

output-> tensor(
		[[[ 0.0096, -0.4331,  0.0036],
      [-0.2697, -0.4088, -0.1225]],

     [[-0.4351, -0.8297, -0.5539],
      [-0.2669, -0.6056,  0.5748]],

     [[-0.3306,  0.0999, -0.7674],
      [-0.0076, -0.7076,  0.6473]]] 
,grad_fn=<StackBackward0>) torch.Size([3, 2, 3])
hn-> tensor(
		[[[-0.3306,  0.0999, -0.7674],
      [-0.0076, -0.7076,  0.6473]]]
, grad_fn=<StackBackward0>) torch.Size([1, 2, 3])

结论: 若句子由多个词组成, output的最后一组词向量(每个句子的最后一个词组成)等于h0

  • 模拟 RNN模型循环机制
def dm03():
    """
    模拟 RNN循环机制
    :return:None
    """
    # 0- 手动设定随机种子方便后续复现
    torch.manual_seed(12)

    # 1- 构建 RNN模型
    # input_size: 词向量维度
    # hidden_size: 隐藏层维度
    # num_layers: 隐藏层层数
    rnn = nn.RNN(input_size=4, hidden_size=3, num_layers=1)

    # 2- 定义输出向量
    # size: (seq_len,batch_size,input_dim)
    # seq_len:句子长度
    # batch_size: 批次的样本数
    # input_dim=input_size: 词向量维度
    input = torch.randn(3, 1, 4)
    # print('input->', input)

    # 3- 定义初始隐藏状态张量 h0
    # size:(num_layer * 网络方向数,batch_size,hidden_size)
    # num_layer: 隐藏层层数
    # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
    # batch_size: 样本批次数
    # hidden_size: 隐藏层维度
    h0 = torch.zeros(1, 1, 3)

    ## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
    ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
    ### 示例:output, hn = rnn(input)
    # 4-  一个词一个词的 给模型喂数据
    for i in range(input.shape[0]):
        tmp_x = input[0].unsqueeze(dim=0)
        # print('tmp_x->', tmp_x)
        output, hn = rnn(tmp_x, h0)
        # 5- 查看模型输出
        print(f'output_{i + 1}->', output, output.shape)
        print(f'hn_{i + 1}->', hn, hn.shape)
        print()

输出结果:

output_1-> tensor([[[ 0.1012, -0.1932,  0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])
hn_1-> tensor([[[ 0.1012, -0.1932,  0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])

output_2-> tensor([[[ 0.1012, -0.1932,  0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])
hn_2-> tensor([[[ 0.1012, -0.1932,  0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])

output_3-> tensor([[[ 0.1012, -0.1932,  0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])
hn_3-> tensor([[[ 0.1012, -0.1932,  0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])
  • 隐藏层大于1的RNN模型
def dm04():
    """
    构建隐藏层层数大于1的 RNN模型
    :return:None
    """
    # 0- 手动设定随机种子方便后续复现
    torch.manual_seed(12)

    # 1- 构建 RNN模型
    # input_size: 词向量维度
    # hidden_size: 隐藏层维度
    # num_layers: 隐藏层层数
    rnn = nn.RNN(input_size=3, hidden_size=3, num_layers=2)

    # 2- 定义输出向量
    # size: (seq_len,batch_size,input_dim)
    # seq_len:句子长度
    # batch_size: 批次的样本数
    # input_dim=input_size: 词向量维度
    input = torch.randn(1, 2, 3)

    # 3- 定义初始隐藏状态张量 h0
    # size:(num_layer * 网络方向数,batch_size,hidden_size)
    # num_layer: 隐藏层层数
    # 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
    # batch_size: 样本批次数
    # hidden_size: 隐藏层维度
    h0 = torch.zeros(2, 2, 3)

    ## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
    ## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
    ### 示例:output, hn = rnn(input)
    # 4- 使用RNN模型
    output, hn = rnn(input, h0)

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

输出结果:

output-> tensor(
		[[[-0.3889, -0.1856, -0.5709],
      [-0.3924, -0.2318, -0.4879]]]
, grad_fn=<StackBackward0>) torch.Size([1, 2, 3])
hn-> tensor(
		[[[-0.6897, -0.7110,  0.7299],
      [-0.8599, -0.6114,  0.4448]],

     [[-0.3889, -0.1856, -0.5709],
      [-0.3924, -0.2318, -0.4879]]]
, grad_fn=<StackBackward0>) torch.Size([2, 2, 3])
rnn模型-> RNN(3, 3, num_layers=2)

结论:若只有1个隐藏层,output输出结果等于hn
结论:如果有2个隐藏层,hn有2个,output输出结果等于最后1个隐藏层的hn

关于output的维数,给出如下步骤方便理解
  • 隐藏状态的维度信息

h i h_{i} hi:(num_layers * 网络方向数,batch_size,hidden_size) ,i=1,2…n

rnn层前向传播时,按照以下步骤执行:
1.每一个时间步会产生一个正向隐藏状态: h i f h^{f}_{i} hif,一个反向隐藏状态** h i b h^{b}_{i} hib**(如果是双向网络)

如果是此时rnn是双向网络则会执行2.0:

​ 2.0 根据dim= -1 将这两个张量拼接(concat)起来

​ 即** h i h_i hi = h i f , h i b {h^{f}_{i},h^{b}_{i}} hif,hib =>(num_layers,batch_size,hidden_size * 网络方向数)**

​ 2.output会按照dim=0维度连接 h 0 h_0 h0/ h 1 h_1 h1…/ h n h_n hn 所有向量(concat操作,并不会增加维度)

因此output也是一个三维张量,最后output的维数 => (seq_len,batch_size,hidden_size * 网络方向数)


传统RNN模型的优点与不足
  • 优点

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

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