自然语言处理——03 RNN及其变体

1 认识RNN

1.1 概念

  • 循环神经网络 RNN (Recurrent Neural Network,简称RNN)------处理序列数据的神经网络;

    • 一般以序列数据作为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,一般也是以序列形式进行输出;

    • 应用场景:比如自然语言(句子是"词的序列")、时间序列(股票价格是"时间点的序列")、音频(声音是"波形的序列");

    • 独特能力:普通神经网络"看一个样本是一个样本"(比如识别单张图片),但 RNN 能记住前面的信息,用历史信息辅助当前判断

  • RNN网络结构:左图是普通神经网络结构,右图是RNN神经网络结构

    • 输入层:接收"序列中的一个元素"(比如句子里的一个词,股票的一个时间点价格);

    • 隐藏层 :RNN 的核心!负责存历史信息 :上一个时间步(比如上一个词)的隐藏层输出,会作为当前时间步的输入,和新数据一起计算;

    • 输出层:输出当前时间步的结果(比如预测下一个词,预测下一时间点价格);

  • 为什么叫"循环"?------因为隐藏层自己连接自己

    • 普通神经网络:隐藏层输出直接给输出层;

    • RNN 隐藏层:上一个时间步的输出会在下一个时间步作为输入(上面右图的隐藏层有个环);

  • 以时间步对RNN进行展开后的单层网络结构

    • RNN 实际运行时,是按时间一步步算 的(时间步 t1→t2→t3... )。为了方便理解,我们把"循环的隐藏层"拆成"多个隐藏层,按时间串联"

    • 展开后的RNN网络结构特点

      1. 输入有两个 :数据端输入(当前时间步的新词、新价格)、隐藏层输入(上一时间步隐藏层记住的信息);
        • 如果是第一个时间步,对于隐藏层的输入,输入的是初始化的隐藏层状态 h0h_0h0;
      2. 输出有两个:数据端输出(当前时间步的预测结果)、隐藏层输出(传给下一时间步,继续当"记忆")。

1.2 作用

  • 作用:

    • RNN 结构能够连续性地输入序列数据,进行特征提取。比如:人类的语言、语音特别适合用RNN进行处理;
    • RNN 广泛应用于NLP领域的各项任务,如文本分类、情感分析、意图识别、机器翻译等;
  • 例:用户意图识别

    • 第一步:用户输入"What time is it ?",先进行分词。RNN是一个时间步一个时间步地接收输入数据,所以每次只接收一个单词处理;

    • 第二步:首先将单词"What"输送给RNN,它将产生一个输出O1;

    • 第三步:继续将单词"time"输送给RNN,RNN不仅利用"time"来产生输出O2,还使用上一个时间步的隐藏层输出的O1作为输入信息;

      • 所以下图中,第一个隐藏层向右的箭头输出的数据,可以理解为是向上的箭头输出的数据的副本;
    • 第四步:重复这样的步骤,直到处理完所有的单词;

    • 第五步:最后,将最终的隐藏层输出O5进行处理来解析用户意图;

1.3 分类

  • 按照输入和输出的角度进行分类

    • N vs N -- RNN:

      • 输入N个序列,输出N个序列。输入和输出序列是等长的;
      • 写诗、写对联等固定场景表达;
    • N vs 1 -- RNN:

      • 输入N个序列,输出1个的值。输入是一个序列,而要求输出是一个单独的值而不是序列;
      • 还要使用s igmoid或者softmax进行处理;
      • 情感分类、文本识别;
    • 1 vs N -- RNN:

      • 输入1个序列,输出N个的值。输入是一个值,使该输入作用于每次的输出上;
      • 看图说话;
    • N vs M -- RNN:也称作seq2seq(序列到序列)模型

      • 输入N个序列,输出M个序列;
      • 结构特点:编码器、解码器、语义向量C,编码器和解码器的内部结构都是某类RNN;
      • 输入数据首先通过编码器,最终输出一个语义变量c,再将隐含变量作用在解码器进行解码的每一步上,得到输出结果;

      h0h_0h0是什么?

      • h0h_0h0是 RNN 开始处理输入序列时,隐藏层的初始状态;
      • 一般情况下,h0h_0h0会被初始化为全零向量 ,但在一些复杂的场景或预训练模型中,也可能被初始化为经过训练得到的特定值, 从而为后续的隐藏状态计算提供一个更有意义的起始点;

      h0′h_0'h0′是什么?

      • 解码器开始生成输出序列时,需要一个初始的隐藏状态,这个状态就是h0′h_0'h0′;

      • 最简单的 seq2seq,h0′h_0'h0′直接用编码器最后一个时间步的隐藏状态h4h_4h4 初始化;

      • 复杂点的模型(比如加了 Attention),h0′h_0'h0′可能结合编码器的多个隐藏状态,甚至随机初始化,但核心逻辑不变:给解码器一个 "初始记忆",让它知道 "要生成的内容和输入序列啥关系"

      编码器(Encoder)------把输入序列压缩成语义向量

      • 输入:序列数据(比如中文句子的词序列x1,x2,x3,x4x_1, x_2, x_3, x_4x1,x2,x3,x4);
      • 过程:用 RNN(h1→h2→h3→h4h_1→h_2→h_3→h_4h1→h2→h3→h4)逐步处理每个词,把整个序列的信息压缩成一个"语义向量CCC"(可以理解成"把整段话的意思浓缩成一个向量");
      • 关键:语义向量CCC要包含输入序列的全部关键信息,后续解码器全靠它"还原"成输出序列;

      解码器(Decoder)------把语义向量,还原成输出序列

      • 输入:语义向量CCC+ 解码器自己的"隐藏状态"(h1′,h2′,h3′h'_1, h'_2, h'_3h1′,h2′,h3′);
      • 过程:解码器也是 RNN,每一步用CCC和"上一步的隐藏状态",生成一个输出(比如一个英文词y1,y2,y3y_1, y_2, y_3y1,y2,y3),并更新自己的隐藏状态,直到输出完整序列;
      • 关键:解码器像个"翻译器",拿着CCC(浓缩的语义),一步步"吐出"目标序列(比如英文句子);

      "语义向量CCC"------承上启下的桥梁

      • 作用:把输入序列(可能很长)的信息,压缩成固定长度的向量,让解码器能基于它生成任意长度的输出序列;
      • 缺点与改进:早期 seq2seq 用"一个CCC包打天下",但长序列时容易"信息丢失"(比如翻译长句子,前面的词信息会被后面的覆盖)。后来的 Attention(注意力机制) 就是为解决这问题,让解码器"动态关注CCC里的不同部分";
      • 机器翻译、阅读理解、文本摘要......因其输入输出不受限制,如今也是最广泛使用的RNN模型;
  • 按照RNN的内部构造进行分类

    • 传统RNN
    • LSTM
    • Bi-LSTM
    • GRU
    • Bi-GRU

2 传统RNN

2.1 RNN内部结构分析

  • RNN内部结构图:

    • 核心设计:同一隐藏层,时间步循环

    • 模块 A:是 RNN 的隐藏层核心计算单元,包含"全连接 + 激活函数"(比如 tanh);

    • 循环过程:

      • 上一时间步的隐藏层输出ht−1h_{t-1}ht−1,会作为当前时间步的输入 ,和新数据xtx_txt"融合"在一起后计算,得到新的张量[xt,ht−1][x_t,h_{t-1}][xt,ht−1];
      • 这个[xt,ht−1][x_t,h_{t-1}][xt,ht−1]将通过一个全连接层,该层使用tanh作为激活函数,最终得到该时间步的输出,即hth_tht;
      • 然后hth_tht和xt+1x_{t+1}xt+1将一起作为下一个时间步的输入;
    • 为什么要"循环"?

      • 普通神经网络:输入→隐藏层→输出,"一锤子买卖",记不住历史;
      • RNN:隐藏层自己"循环",把历史信息存到hhh里,让模型能"记住前面的内容"(比如句子前几个词的意思);
    • 三层结构:输入层 → 隐藏层 → 输出层

      • 输入层 :接收当前时间步的数据xtx_txt(比如句子里的一个词,股票的一个时间点价格);

      • 隐藏层 :用xtx_txt和ht−1h_{t-1}ht−1计算新的hth_tht,负责"存历史信息 + 处理新信息";

      • 输出层 :用hth_tht生成当前时间步的输出(比如预测下一个词,预测下一时间点价格);

  • 隐藏层中是怎么计算的?

    • 学术界公式(简化版)
      ht=tanh⁡(We[Xt,ht−1]+bt) h_t = \tanh(W_e [X_t, h_{t-1}] + b_t) ht=tanh(We[Xt,ht−1]+bt)

      • Xt,ht−1\]\[X_t, h_{t-1}\]\[Xt,ht−1\]:把"当前输入XtX_tXt"和"历史隐藏状态ht−1h_{t-1}ht−1"**拼接**成一个长向量;

      • btb_tbt:偏置项,调整计算结果;

      • tanh⁡\tanhtanh:激活函数,给结果加"非线性"(否则多层 RNN 会退化成单层线性变换);

    • 工业界实现(PyTorch 版)
      ht=tanh⁡(Win∗Xt+bin+Whn∗ht−1+bhn) h_t = \tanh(W_{in} * X_t + b_{in} + W_{hn} * h_{t-1} + b_{hn}) ht=tanh(Win∗Xt+bin+Whn∗ht−1+bhn)

      • 工业界为了灵活控制"输入权重"和"历史权重",把WeW_eWe拆成 WinW_{in}Win(输入的权重)WhnW_{hn}Whn(历史隐藏状态的权重) ,偏置项也拆成binb_{in}bin和bhnb_{hn}bhn;
      • 本质和简化版一样,只是更细粒度地控制"新信息"和"历史信息"的影响力;
    • 不管公式咋变,核心都是:
      新 ht=激活函数(当前输入的加权+历史隐藏状态的加权+偏置) \text{新 } h_t = \text{激活函数}(\text{当前输入的加权} + \text{历史隐藏状态的加权} + \text{偏置}) 新 ht=激活函数(当前输入的加权+历史隐藏状态的加权+偏置)

  • 激活函数 tanh 的作用

    • tanh⁡\tanhtanh函数的输出范围是 (-1, 1)

    • 为什么要压缩?

      • 如果不压缩,隐藏状态hth_tht的数值会"越来越大"(因为每一步都加权求和),导致 梯度爆炸(训练时参数更新幅度过大,模型直接崩溃);
      • 用tanh⁡\tanhtanh把数值锁在 (-1, 1),能一定程度上稳定训练
  • 对比其他激活函数(比如 ReLU)

    • tanh⁡\tanhtanh是"老一代"激活函数,现在 RNN 变体(比如 LSTM)常用 ReLUGELU,但核心逻辑不变:给隐藏状态加"非线性",同时控制数值范围;

    • 小缺点:tanh⁡\tanhtanh在"-1 附近"梯度很小,容易导致梯度消失(训练时,历史信息传不到后面时间步)。这也是 LSTM/GRU 诞生的原因之一(用门控机制解决梯度消失)。

2.2 API

2.2.1 API简介

  • RNN 的API可以提取文本序列的特征;
  • torch.nn工具包之中,通过torch.nn.RNN可调用;
  • 案例需求:
    • 输入:3 个批次(即 3 条文本),每个批次 1 个单词(序列长度 1),每个单词用 5 个特征表示(input_size=5);
    • 输出:每个单词被编码成 6 个特征。

2.2.2 参数

python 复制代码
# 1. 初始化 RNN 模型
#    - input_size=5:输入序列中每个元素的特征维度是 5(比如每个词用 5 个数值表示)
#    - hidden_size=6:隐藏层神经元的个数是 6(每个时间步输出 6 维特征)
#    - num_layers=1:隐藏层的层数是 1(基础单向 RNN)
rnn = nn.RNN(5, 6, 1)

# 2. 构造输入数据
#    - 形状是 (seq_len, batch_size, input_size) = (1, 3, 5)
#    - seq_len=1:每个批次的序列长度是 1(比如每个批次只有 1 个单词)
#    - batch_size=3:有 3 个批次(即 3 条平行的输入数据)
#    - input_size=5:每个元素的特征维度是 5(和 RNN 的 input_size 对应)
input = torch.randn(1, 3, 5)

# 3. 构造初始隐藏状态 h0
#    - 形状是 (num_layers, batch_size, hidden_size) = (1, 3, 6)
#    - num_layers=1:隐藏层的层数是 1(和 RNN 的 num_layers 对应)
#    - batch_size=3:批次大小是 3(和输入数据的批次对应)
#    - hidden_size=6:隐藏层神经元个数是 6(和 RNN 的 hidden_size 对应)
h0 = torch.randn(1, 3, 6)

# 4. 执行 RNN 前向计算
#    - 将输入数据 input 和初始隐藏状态 h0 传入 RNN
#    - 输出包含两部分:
#      1) output:每个时间步的隐藏层输出,形状 (seq_len, batch_size, hidden_size) = (1, 3, 6)
#      2) hn:最终的隐藏层状态,形状 (num_layers, batch_size, hidden_size) = (1, 3, 6)
output, hn = rnn(input, h0)

# 5. 打印输出结果
#    - 打印 output 的形状和具体值,查看每个时间步的隐藏层输出
print('output-->', output.shape, '\n', output)
#    - 打印 hn 的形状和具体值,查看最终的隐藏层状态(和 output[-1] 结果一致,因为是单层单向 RNN)
print('hn-->', hn.shape, '\n', hn)
复制代码
output--> torch.Size([1, 3, 6]) 
 tensor([[[ 0.6236,  0.5790, -0.2830,  0.4407,  0.7677,  0.1880],
         [ 0.5468,  0.4484, -0.5309,  0.7859,  0.8436,  0.6732],
         [ 0.1489,  0.3383,  0.0618,  0.0298,  0.3531,  0.0852]]],
       grad_fn=<StackBackward0>)
hn--> torch.Size([1, 3, 6]) 
 tensor([[[ 0.6236,  0.5790, -0.2830,  0.4407,  0.7677,  0.1880],
         [ 0.5468,  0.4484, -0.5309,  0.7859,  0.8436,  0.6732],
         [ 0.1489,  0.3383,  0.0618,  0.0298,  0.3531,  0.0852]]],
       grad_fn=<StackBackward0>)

2.2.3 调整输入尺寸

python 复制代码
# 1. 初始化 RNN 模型
#    - input_size=55:输入序列中每个元素的特征维度是 55(比如每个词用 55 个数值表示)
rnn = nn.RNN(55, 6, 1)

# 2. 构造输入数据
#    - input_size=55:每个元素的特征维度是 55(和 RNN 的 input_size 对应)
input = torch.randn(1, 3, 55)

# 3. 构造初始隐藏状态 h0
h0 = torch.randn(1, 3, 6)

# 4. 执行 RNN 前向计算
output, hn = rnn(input, h0)

# 5. 打印输出结果的形状
print('output-->', output.shape)
print('hn-->', hn.shape)

2.2.4 调整输出尺寸

python 复制代码
# 1. 初始化 RNN 模型
#    - hidden_size=66:隐藏层神经元的个数是 66(每个时间步输出 66 维特征)
rnn = nn.RNN(5, 66, 1)

# 2. 构造输入数据
input = torch.randn(1, 3, 5)

# 3. 构造初始隐藏状态 h0
#    - hidden_size=66:隐藏层神经元个数,和 RNN 模型初始化时的 hidden_size 对应
h0 = torch.randn(1, 3, 66)

# 4. 执行 RNN 前向计算
output, hn = rnn(input, h0)

# 5. 打印输出结果的形状信息
print('output-->', output.shape)
print('hn-->', hn.shape)

2.2.5 调整输入数据长度

python 复制代码
# 1. 初始化 RNN 模型
rnn = nn.RNN(5, 6, 1)

# 2. 构造输入数据
#    - seq_len=11:每个批次的序列长度是 11(比如每个批次有 11 个单词)
input = torch.randn(11, 3, 5)

# 3. 构造初始隐藏状态 h0
h0 = torch.randn(1, 3, 6)

# 4. 执行 RNN 前向计算
output, hn = rnn(input, h0)

# 5. 打印输出结果的形状
print('output-->', output.shape)
print('hn-->', hn.shape)

2.2.6 调整数据批次

python 复制代码
# 1. 初始化 RNN 模型
rnn = nn.RNN(5, 66, 1)

# 2. 构造输入数据
#    - batch_size=33:表示有 33 个批次,也就是同时处理 33 组平行的输入数据
input = torch.randn(1, 33, 5)

# 3. 构造初始隐藏状态 h0
#    - batch_size=33:批次大小,与输入数据的批次数量一致
h0 = torch.randn(1, 33, 66)

# 4. 执行 RNN 前向计算
output, hn = rnn(input, h0)

# 5. 打印输出结果的形状信息
print('output-->', output.shape)
print('hn-->', hn.shape)

2.2.7 调整模型隐藏层个数

python 复制代码
# 1. 初始化多层 RNN 模型
#    - num_layers=2:隐藏层的**层数是 2**(构建多层 RNN,这里是 2 层单向 RNN 堆叠)
rnn = nn.RNN(5, 6, 2)

# 2. 构造输入数据
input = torch.randn(1, 3, 5)

# 3. 构造初始隐藏状态 h0
#    - num_layers=2:隐藏层的层数是 2(和 RNN 模型的 num_layers 对应)
h0 = torch.randn(2, 3, 6)

# 4. 执行 RNN 前向计算
output, hn = rnn(input, h0)

# 5. 打印输出结果
print('output-->', output.shape, '\n', output)
print('hn-->', hn.shape, '\n', hn)

# 注释说明
# 注1:当 `num_layers=1` 时,output 和 hn[-1](最后一层的最终状态)结果一致
# 注2:当 `num_layers>1` 时,output 是最后一层每个时间步的输出,hn 包含所有层的最终状态
#      因此,output 和 hn[-1](最后一层的最终状态)结果一致
  • 输出结果解读:

    • 1 层隐藏层 :输入 → 隐藏层 1 → 输出(output = 隐藏层 1 输出);

    • 2 层隐藏层 :输入 → 隐藏层 1 → 隐藏层 2 → 输出(output = 隐藏层 2 输出)

    • n 层隐藏层 :输入 → 隐藏层 1 → 隐藏层 2 → ... → 隐藏层 n → 输出(output = 隐藏层 n 输出)

  • 总结:当隐藏层个数配置成 n 时,output 的结果和最后一个隐藏层输出一致

2.2.8 调整数据批次数在前

python 复制代码
# 1. 初始化 RNN 模型,设置 batch_first=True
#    - batch_first=True:
#      作用:让输入和输出的张量形状以 batch_size 开头(即形状为 [batch_size, seq_len, input_size])
#      注意:仅影响 input 和 output 的形状,不影响 h0、hn 的形状(它们仍以 num_layers 开头)
my_rnn = nn.RNN(5, 6, 1, batch_first=True)

# 2. 构造输入数据
#    - 形状是 (batch_size, seq_len, input_size) = (3, 1, 5)
#    - 因为 batch_first=True,所以第一维是 batch_size(3 个批次)
#    - seq_len=1(每个批次的序列长度是 1),input_size=5(每个元素的特征维度)
input = torch.randn(3, 1, 5)

# 3. 构造初始隐藏状态 h0
#    - 形状是 (num_layers, batch_size, hidden_size) = (1, 3, 6)
#    - batch_first=True 不影响 h0 的形状,仍以 num_layers 开头
h0 = torch.randn(1, 3, 6)

# 4. 执行 RNN 前向计算
#    - 输入数据 input 形状 (3, 1, 5)(符合 batch_first=True 的要求)
#    - 初始隐藏状态 h0 形状 (1, 3, 6)(不受 batch_first 影响)
#    - 输出包含两部分:
#      1) output:形状 (batch_size, seq_len, hidden_size) = (3, 1, 6)(因为 batch_first=True)
#      2) hn:形状 (num_layers, batch_size, hidden_size) = (1, 3, 6)(不受 batch_first 影响)
output, hn = my_rnn(input, h0)

# 5. 打印输出结果的形状
print('output-->', output.shape)
print('hn-->', hn.shape)

# 代码核心逻辑:
# batch_first=True 的作用是让 input 和 output 的形状以 batch_size 开头(更符合直观习惯)
# 但 h0 和 hn 的形状仍保持 (num_layers, batch_size, hidden_size),不受影响

2.2.9 输入数据的两种方式

python 复制代码
# 1. 初始化 RNN 模型
rnn = nn.RNN(5, 6, 1, batch_first=True)

# 2. 构造随机输入数据
#    - 形状:(batch_size, seq_len, input_size) = (1, 10, 5)
#    - 含义:1 个批次,10 个字符(序列长度 10),每个字符用 5 维特征表示
input = torch.randn(1, 10, 5)

# 3. 初始化隐藏状态
#    - 形状:(num_layers, batch_size, hidden_size) = (1, 1, 6)
#    - 用全 0 初始化隐藏状态(也可用随机值)
hidden = torch.zeros(1, 1, 6)

# -------------------- 方式1:逐个时间步输入数据(字符) --------------------
# 逻辑:手动遍历每个时间步,逐个字符输入 RNN,手动传递隐藏状态
for i in range(input.shape[1]):  # input.shape[1] = 10(序列长度,共 10 个时间步)
    # 取出当前时间步的输入(单个字符)
    # input[:, i, :] 取出第 i 个时间步的特征,形状 (1, 5)
    # 但 RNN 要求输入形状 (batch_size, seq_len, input_size),所以用 unsqueeze 补全维度
    tmp = input[:, i, :].unsqueeze(1)  # 补成 (1, 1, 5)

    # 输入给 RNN:当前时间步输入 + 上一时间步的隐藏状态
    # rnn 输出:当前时间步的 output(形状 (1, 1, 6)) + 更新后的 hidden(形状 (1, 1, 6))
    output, hidden = rnn(tmp, hidden)

    # 打印每个时间步的输出(验证逐个传递的过程)
    print(f'{i + 1} -->', output.shape, output)

# -------------------- 方式2:一次性输入全部数据 --------------------
# 逻辑:直接把整个序列(10 个字符)输入给 RNN,模型内部自动遍历时间步,传递隐藏状态
# 重新初始化隐藏状态(避免受方式1影响)
hidden = torch.zeros(1, 1, 6)

# 一次性输入整个序列(形状 (1, 10, 5))
output, hidden = rnn(input, hidden)

# 打印一次性输入数据的输出(验证模型内部自动遍历的结果)
print('一次性输入数据 output\n', output.shape, output)
复制代码
1 --> torch.Size([1, 1, 6]) tensor([[[ 0.4178, -0.2417,  0.3189,  0.6201, -0.3831,  0.6262]]],
       grad_fn=<TransposeBackward1>)
2 --> torch.Size([1, 1, 6]) tensor([[[-0.6708,  0.1563, -0.7931, -0.5868, -0.4512, -0.0880]]],
       grad_fn=<TransposeBackward1>)
3 --> torch.Size([1, 1, 6]) tensor([[[ 0.2514,  0.5975, -0.7491,  0.3919, -0.4360, -0.0557]]],
       grad_fn=<TransposeBackward1>)
4 --> torch.Size([1, 1, 6]) tensor([[[-0.5499,  0.7572, -0.8983,  0.0600,  0.2337, -0.3928]]],
       grad_fn=<TransposeBackward1>)
5 --> torch.Size([1, 1, 6]) tensor([[[-0.5120,  0.6703,  0.1447,  0.5362, -0.5573,  0.1616]]],
       grad_fn=<TransposeBackward1>)
6 --> torch.Size([1, 1, 6]) tensor([[[-0.5205,  0.7027, -0.4184,  0.3433, -0.4837, -0.4589]]],
       grad_fn=<TransposeBackward1>)
7 --> torch.Size([1, 1, 6]) tensor([[[-0.8803,  0.8959, -0.7245, -0.2207,  0.3849, -0.4234]]],
       grad_fn=<TransposeBackward1>)
8 --> torch.Size([1, 1, 6]) tensor([[[-0.6703,  0.8615, -0.5916, -0.0171, -0.4583, -0.3254]]],
       grad_fn=<TransposeBackward1>)
9 --> torch.Size([1, 1, 6]) tensor([[[-0.8539,  0.7584,  0.4446,  0.7087, -0.3283, -0.0868]]],
       grad_fn=<TransposeBackward1>)
10 --> torch.Size([1, 1, 6]) tensor([[[-0.7833,  0.4795, -0.5264, -0.5058, -0.0571, -0.1314]]],
       grad_fn=<TransposeBackward1>)
一次性输入数据 output
 torch.Size([1, 10, 6]) tensor([[[ 0.4178, -0.2417,  0.3189,  0.6201, -0.3831,  0.6262],
         [-0.6708,  0.1563, -0.7931, -0.5868, -0.4512, -0.0880],
         [ 0.2514,  0.5975, -0.7491,  0.3919, -0.4360, -0.0557],
         [-0.5499,  0.7572, -0.8983,  0.0600,  0.2337, -0.3928],
         [-0.5120,  0.6703,  0.1447,  0.5362, -0.5573,  0.1616],
         [-0.5205,  0.7027, -0.4184,  0.3433, -0.4837, -0.4589],
         [-0.8803,  0.8959, -0.7245, -0.2207,  0.3849, -0.4234],
         [-0.6703,  0.8615, -0.5916, -0.0171, -0.4583, -0.3254],
         [-0.8539,  0.7584,  0.4446,  0.7087, -0.3283, -0.0868],
         [-0.7833,  0.4795, -0.5264, -0.5058, -0.0571, -0.1314]]],
       grad_fn=<TransposeBackward1>)

2.3 优缺点

  • 优点:
    • 内部结构简单,对计算资源要求低;
      • RNN变体:LSTM和GRU模型的参数总量少了很多;
    • 在短序列任务上性能和效果都表现优异;
  • 缺点:长序列文本特征提取效果差。过长的序列易导致梯度的计算异常,发生梯度消失或爆炸。

3 LSTM

3.1 概念

  • LSTM(Long Short-Term Memory)也称长短时记忆结构

    • 它是传统RNN的变体
    • 与经典RNN相比,能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象
  • 结构说明:

    • 由输入层、隐藏层、输出层组成;
    • 每个时间步有三个输入:数据端输入,上一个时间步细胞状态Ct−1C_{t-1}Ct−1,上一个时间步的ht−1h_{t-1}ht−1
    • 每个时间步有三个输出:数据端输出,本时间步细胞状态CtC_{t}Ct,本时间步的hth_{t}ht
    • LSTM结构更加复杂,内部有3个门和1个细胞状态:遗忘门、输入门、细胞状态、输出门
  • RNN 最大的痛点:序列过长时,隐藏层的历史信息会"被冲刷掉"(梯度消失),导致模型记不住早期内容(比如长句子的开头);

  • LSTM 用细胞状态CtC_tCt+ 三门控,让信息能"选择性记住/遗忘",解决梯度消失问题。

3.2 内部结构

  • LSTM 通过遗忘门(丢历史)→ 输入门(存新信息)→ 细胞状态更新(历史+新信息)→ 输出门(选输出)的流程,让模型能选择性记住长期信息,解决了 RNN 梯度消失的问题,特别适合处理长序列任务(比如文本翻译、时间序列预测);

  • 遗忘门(Forget Gate)------决定历史细胞状态要丢多少

    • 公式:
      ft=σ(Wf[ht−1,xt]+bf) f_t = \sigma(W_f [h_{t-1}, x_t] + b_f) ft=σ(Wf[ht−1,xt]+bf)

      • ht−1,xt\]\[h_{t-1}, x_t\]\[ht−1,xt\]:把上一时间步的隐藏状态ht−1h_{t-1}ht−1和当前输入xtx_txt拼接;

      • σ\sigmaσ:sigmoid 激活函数(输出 0~1 之间的数);
    • ftf_tft是一个遗忘比例

      • ftf_tft接近 1 → 保留大部分历史细胞状态Ct−1C_{t-1}Ct−1;
      • ftf_tft接近 0 → 遗忘大部分历史细胞状态Ct−1C_{t-1}Ct−1;
    • 历史细胞状态经过遗忘门过滤后,剩下的部分:
      ft∗Ct−1 f_t * C_{t-1} ft∗Ct−1

  • 输入门(Input Gate)------决定新信息要存多少

    • 公式:
      it=σ(Wi[ht−1,xt]+bi)(输入门控制要不要存新信息)Ct~=tanh⁡(Wc[ht−1,xt]+bc)(生成新的候选细胞状态) i_t = \sigma(W_i [h_{t-1}, x_t] + b_i) (输入门控制要不要存新信息) \\ \tilde{C_t} = \tanh(W_c [h_{t-1}, x_t] + b_c) (生成新的候选细胞状态) it=σ(Wi[ht−1,xt]+bi)(输入门控制要不要存新信息)Ct~=tanh(Wc[ht−1,xt]+bc)(生成新的候选细胞状态)

      • iti_tit是新信息的保留比例(0~1 之间);

      • Ct~\tilde{C_t}Ct~是"当前输入xtx_txt生成的新信息"(tanh 把值压缩到 -1~1);

      • it∗Ct~i_t * \tilde{C_t}it∗Ct~表示新信息经过输入门过滤后,要加入细胞状态的部分;

  • 细胞状态更新------历史 + 新信息,得到新细胞状态

    • 公式:
      Ct=ft∗Ct−1+it∗Ct~ C_t = f_t * C_{t-1} + i_t * \tilde{C_t} Ct=ft∗Ct−1+it∗Ct~

    • 作用:把遗忘门过滤后的历史细胞状态和输入门过滤后的新候选状态相加,得到新的细胞状态CtC_tCt

    • 这一步是 LSTM 的核心------细胞状态CtC_tCt实现了"长期记忆"的更新,让重要信息能跨时间步传递,不被梯度消失冲刷;

  1. 输出门(Output Gate)------决定细胞状态要输出多少给隐藏层

    • 公式:
      ot=σ(Wo[ht−1,xt]+bo)(输出门控制细胞状态有多少要输出)ht=ot∗tanh⁡(Ct)(细胞状态经过tanh压缩后,按输出门比例输出) o_t = \sigma(W_o [h_{t-1}, x_t] + b_o) (输出门控制细胞状态有多少要输出)\\ h_t = o_t * \tanh(C_t) (细胞状态经过 tanh 压缩后,按输出门比例输出) ot=σ(Wo[ht−1,xt]+bo)(输出门控制细胞状态有多少要输出)ht=ot∗tanh(Ct)(细胞状态经过tanh压缩后,按输出门比例输出)

      • oto_tot是"细胞状态的输出比例"(0~1 之间);
      • tanh⁡(Ct)\tanh(C_t)tanh(Ct)把细胞状态CtC_tCt压缩到 -1~1;
      • hth_tht是当前时间步的隐藏层输出,同时作为下一时间步的ht+1h_{t+1}ht+1;
    • 输出门决定"细胞状态CtC_tCt有多少要传递给隐藏层hth_tht",影响当前输出和下一时间步的输入;

  • LSTM 核心优势:细胞状态CtC_tCt实现长期记忆

    • 普通 RNN:隐藏状态hth_tht直接传递,历史信息易丢失;

    • LSTM:细胞状态CtC_tCt用"三门控"选择性保留/更新,让重要信息能跨多个时间步传递(比如长句子的开头信息,能被结尾的 LSTM 记住)。

3.3 双向BI-LSTM模型

  • 双向 LSTM 要解决的问题

    • 普通 LSTM(或 RNN)处理序列时,只能"从左到右"看历史信息(比如处理"我爱中国",看到"国"时,只能基于"我→爱→中"的历史);
    • 但很多任务需要未来信息辅助当前判断(比如填空"我__中国",需要看后面的"中国"才能确定填"爱");
    • 双向 LSTM 的思路:同时从左到右和从右到左跑两个 LSTM,把结果拼接,让模型同时看到历史和未来信息
  • 结构拆解

    • 两个方向的 LSTM

      • 正向 LSTM(LSTM_L):从左到右处理序列(我→爱→中→国 ),每个时间步的隐藏状态记为hL0,hL1,hL2h_{L0}, h_{L1}, h_{L2}hL0,hL1,hL2;

      • 反向 LSTM(LSTM_R):从右到左处理序列(国→中→爱→我 ),每个时间步的隐藏状态记为hR0,hR1,hR2h_{R0}, h_{R1}, h_{R2}hR0,hR1,hR2;

    • 隐藏状态拼接 。对于每个时间步ttt(比如处理"爱"时):

      • 正向 LSTM 输出hL1h_{L1}hL1(基于"我→爱"的历史)

      • 反向 LSTM 输出hR1h_{R1}hR1(基于"爱→中→国"的未来)

      • 拼接hL1h_{L1}hL1和hR1h_{R1}hR1,得到同时包含历史和未来信息的隐藏状态,作为当前时间步的最终输出

    • 对应图中的流程

      • 输入序列:"我"、"爱"、"中国"(图里拆成"我"、"爱"、"中国"三个 token)

      • 正向 LSTM(上半部分):从左到右跑,输出hL0,hL1,hL2h_{L0}, h_{L1}, h_{L2}hL0,hL1,hL2

      • 反向 LSTM(下半部分):从右到左跑,输出hR0,hR1,hR2h_{R0}, h_{R1}, h_{R2}hR0,hR1,hR2

      • 每个时间步的输出:拼接正向和反向的隐藏状态(比如h0=[hL0,hR0]h_0 = [h_{L0}, h_{R0}]h0=[hL0,hR0])

  • 特点

    • 不改变 LSTM 内部结构:只是同时跑两个 LSTM(正向+反向),每个 LSTM 的内部逻辑(三门控)不变;
    • 两次处理,拼接结果:对同一序列,正向和反向各跑一次,把两个方向的隐藏状态拼接,作为最终输出;
    • 参数和计算量翻倍:因为有两个独立的 LSTM(正向和反向),所以参数数量(权重、偏置)和计算量都是普通 LSTM 的 2 倍
  • 应用:

    • 适合需要上下文全信息的任务

      • 自然语言处理:命名实体识别(比如识别"中国"是国家,需要前后文 )、情感分析(比如"不开心"需要结合前后判断);

      • 时间序列:股票预测(注意:此处股票的未来不可知,实际中更多是用双向看全局);

    • 不适合在线实时任务(比如实时翻译,未来信息不可得)。

3.4 API

python 复制代码
import torch
import torch.nn as nn
python 复制代码
# 1. 初始化 LSTM 模型
#    - input_size=5:表示输入序列中每个元素的特征维度是 5,即每个时间步输入数据的特征数量为 5
#    - hidden_size=6:隐藏层中 LSTM 单元的维度(也可理解为隐藏层神经元个数),决定了隐藏状态的维度
#    - num_layers=2:LSTM 隐藏层的层数,这里构建的是 2 层的 LSTM 结构,多层 LSTM 可提取更复杂特征
lstm = nn.LSTM(5, 6, 2)

# 2. 构造输入数据
#    - 生成的张量形状为 (seq_len, batch_size, input_size) = (1, 3, 5)
#    - seq_len=1:每个批次对应的序列长度是 1,意味着每个批次里只有一个时间步的数据(例如一句话里只有一个词 )
#    - batch_size=3:表示有 3 个批次,也就是同时处理 3 组平行的输入数据
#    - input_size=5:每个元素的特征维度是 5,要和 LSTM 初始化时设置的 input_size 保持一致
input = torch.randn(1, 3, 5)

# 3. 构造初始隐藏状态和细胞状态
#    - LSTM 内部有两个状态:隐藏状态 h0 和细胞状态 c0,它们的形状都遵循 (num_layers, batch_size, hidden_size)
#    - num_layers=2:LSTM 隐藏层的层数,和模型初始化时的 num_layers 对应
#    - batch_size=3:批次大小,与输入数据的批次数量一致
#    - hidden_size=6:隐藏层神经元个数,和 LSTM 模型初始化时的 hidden_size 对应
h0 = torch.randn(2, 3, 6)  # 初始化隐藏状态
c0 = torch.randn(2, 3, 6)  # 初始化细胞状态,LSTM 特有,用于长期记忆的传递

# 4. 执行 LSTM 前向计算
#    - 将构造好的输入数据 input 以及初始隐藏状态 h0、细胞状态 c0 传入实例化的 LSTM 模型
#    - LSTM 会依据其内部的门控机制(遗忘门、输入门、细胞状态更新、输出门 )进行计算
#    - 输出包含三部分:
#      - output:每个时间步的隐藏层输出结果,形状为 (seq_len, batch_size, hidden_size) = (1, 3, 6)
#      - hn:最终的隐藏层状态,形状为 (num_layers, batch_size, hidden_size) = (2, 3, 6),包含各层最后一个时间步的隐藏状态
#      - cn:最终的细胞状态,形状为 (num_layers, batch_size, hidden_size) = (2, 3, 6),包含各层最后一个时间步的细胞状态
output, (hn, cn) = lstm(input, (h0, c0))

# 5. 打印输出结果
#    - 打印 output 的形状和具体值,查看每个时间步的隐藏层输出情况
print('output-->', '\n', output.shape, '\n', output)
#    - 打印 hn 的形状和具体值,查看最终的隐藏层状态,它是各层最后时间步隐藏状态的集合
print('hn-->', '\n', hn.shape, '\n', hn)
#    - 打印 cn 的形状和具体值,查看最终的细胞状态,它是各层最后时间步细胞状态的集合,体现 LSTM 长期记忆的保留情况
print('cn-->', '\n', cn.shape, '\n', cn)

3.5 优缺点

  • 优点:LSTM的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸,虽然并不能杜绝这种现象,但在更长的序列问题上表现优于传统RNN;
  • 缺点:由于内部结构相对较复杂,因此训练效率在同等算力下较传统RNN低很多。

4 GRU

4.1 概念

  • GRU(Gated Recurrent Unit)也称门控循环单元结构

    • 它也是传统RNN的变体
    • 同LSTM一样,能够有效捕捉长序列之间的语义关联,缓解梯度消失或爆炸现象
    • 同时它的结构比LSTM更简单,但当然比RNN复杂
  • 结构说明:

    • 由输入层、隐藏层、输出层组成
    • 同RNN,每个时间步有2个输入:数据端输入,上一个时间步的ht−1h_{t-1}ht−1
    • 同RNN,每个时间步有2个输出:数据端输出,本时间步的hth_{t}ht
    • GRU结构有2个门:更新门、重置门
  • LSTM 用"三门控 + 细胞状态"解决了 RNN 的梯度消失,但结构复杂(参数多、计算慢);

  • GRU 的思路:简化门控机制(两门控 + 合并细胞状态和隐藏状态),在保证效果的同时,减少参数和计算量。

4.2 内部结构

  • 两门控:重置门(Reset Gate) + 更新门(Update Gate)

    • 重置门(rtr_trt)
      rt=σ(Wr[ht−1,xt]+br) r_t = \sigma(W_r [h_{t-1}, x_t] + b_r) rt=σ(Wr[ht−1,xt]+br)

      • 决定历史隐藏状态要遗忘多少(值接近 0 → 更多遗忘历史;接近 1 → 更多保留历史);
      • 类比:LSTM 的遗忘门,但更简化;
    • 更新门(ztz_tzt)
      zt=σ(Wz[ht−1,xt]+bz) z_t = \sigma(W_z [h_{t-1}, x_t] + b_z) zt=σ(Wz[ht−1,xt]+bz)

      • 决定新隐藏状态要保留多少历史/新信息(值接近 0 → 更多用新信息;接近 1 → 更多用历史);
      • 类比:LSTM 的"输入门 + 输出门"的合并,用一个门控控制"历史 vs 新信息"的比例;
  • 隐藏状态更新候选隐藏状态(ht~\tilde{h_t}ht~)
    ht~=tanh⁡(W[rt∗ht−1,xt]+b) \tilde{h_t} = \tanh(W [r_t * h_{t-1}, x_t] + b) ht~=tanh(W[rt∗ht−1,xt]+b)

    • 重置门过滤后的历史状态 (rt∗ht−1r_t * h_{t-1}rt∗ht−1)和当前输入xtx_txt,生成新信息;
  • 最终隐藏状态(hth_tht)
    ht=(1−zt)∗ht−1+zt∗ht~ h_t = (1 - z_t) * h_{t-1} + z_t * \tilde{h_t} ht=(1−zt)∗ht−1+zt∗ht~

    • 更新门 控制"历史隐藏状态ht−1h_{t-1}ht−1"和"候选隐藏状态ht~\tilde{h_t}ht~"的比例:
      • ztz_tzt接近 1 → 更多保留历史ht−1h_{t-1}ht−1(长期记忆);
      • ztz_tzt接近 0 → 更多用新信息ht~\tilde{h_t}ht~(短期记忆);
  • GRU 记忆机制的通俗解释:

    • 重置门:决定"要不要忘记历史":遇到新信息时,重置门说"历史信息要过滤多少"(比如遇到全新内容,重置门让模型少用历史);

    • 更新门:决定"新信息和历史信息的比例":如果更新门接近 1,模型说"还是历史信息靠谱,多用历史";如果接近 0,模型说"新信息更重要,多用新的";

    • 隐藏状态合并 :GRU 没有单独的"细胞状态",直接用hth_tht同时承载"历史记忆"和"新信息",通过两门控动态调整比例;

4.3 GRU vs LSTM

对比项 GRU LSTM
门控数量 2 个(重置门 + 更新门) 3 个(遗忘门 + 输入门 + 输出门)
状态数量 1 个(隐藏状态hth_tht) 2 个(隐藏状态hth_tht+ 细胞状态CtC_tCt)
核心逻辑 用两门控直接控制"历史 vs 新信息"的比例 用三门控 + 细胞状态管理"长期记忆"
参数与计算量 更少(适合轻量任务、大规模数据) 更多(适合需要精细记忆控制的任务)

4.4 API

python 复制代码
import torch
import torch.nn as nn
python 复制代码
# 1. 初始化 GRU 模型
#    - input_size=5:输入序列中每个元素的特征维度是 5(比如每个词用 5 个数值表示)
#    - hidden_size=6:隐藏层神经元的个数是 6(每个时间步输出 6 维特征)
#    - num_layers=2:隐藏层的层数是 2(2 层 GRU 堆叠,可提取更复杂特征)
mygru = nn.GRU(5, 6, 2)

# 2. 构造输入数据
#    - 形状是 (seq_len, batch_size, input_size) = (1, 3, 5)
#    - seq_len=1:每个批次的序列长度是 1(比如每个批次只有 1 个单词)
#    - batch_size=3:有 3 个批次(即 3 条平行的输入数据)
#    - input_size=5:每个元素的特征维度是 5(和 GRU 的 input_size 对应)
input = torch.randn(1, 3, 5)

# 3. 构造初始隐藏状态 h0
#    - 形状是 (num_layers, batch_size, hidden_size) = (2, 3, 6)
#    - num_layers=2:隐藏层的层数是 2(和 GRU 模型的 num_layers 对应)
#    - batch_size=3:批次大小是 3(和输入数据的批次对应)
#    - hidden_size=6:隐藏层神经元个数是 6(和 GRU 模型的 hidden_size 对应)
h0 = torch.randn(2, 3, 6)

# 4. 执行 GRU 前向计算
#    - 将输入数据 input 和初始隐藏状态 h0 传入 GRU
#    - 输出包含两部分:
#      1) output:最后一层隐藏层每个时间步的输出,形状 (seq_len, batch_size, hidden_size) = (1, 3, 6)
#      2) hn:所有层的最终隐藏状态,形状 (num_layers, batch_size, hidden_size) = (2, 3, 6)
output, hn = mygru(input, h0)

# 5. 打印输出结果
#    - 查看 output 的维度和内容,验证 GRU 的输出逻辑
print('output-->', '\n', output.shape, '\n', output)
#    - 查看 hn 的维度和内容,验证多层 GRU 的隐藏状态传递
print('hn-->', '\n', hn.shape, '\n', hn)

4.5 优缺点

  • 优点:

    • GRU和LSTM作用相同,在捕捉长序列语义关联时,能有效抑制梯度消失或爆炸,效果都优于传统RNN;
    • 且计算复杂度相比LSTM要小;
  • 缺点:

    • GRU仍然不能完全解决梯度消失问题;
    • 同时其作为RNN的变体,有着RNN结构本身的一大弊端,即不可并行计算,这在数据量和模型体量逐步增大的当下,是RNN发展的关键瓶颈;
  • 注意上面提到的"并行计算",指的是单个序列不可以并行计算

    场景 能否并行 核心原因 典型实现
    单个序列的时间步之间 串行(无法并行) 隐藏状态依赖上一时间步结果 天然限制
    同一时间步的不同序列 并行(常用) 不同序列的隐藏状态相互独立 框架默认的批次并行
    模型训练的参数 / 梯度计算 并行(复杂) 不同设备可分担计算 / 更新任务 分布式训练(如 nn.DDP
    时间步展开的数学优化 理论可行(工程极难) 数学变换消解时间依赖 研究领域的尝试性方案
相关推荐
舒一笑25 分钟前
Building effective agents 建立有效的agents
人工智能
知秋丶1 小时前
大模型应用发展与Agent前沿技术趋势(下)
人工智能·python·ai agent
智驱力人工智能1 小时前
安全帽检测算法如何提升工地安全管理效率
人工智能·算法·安全·边缘计算·安全帽检测·口罩检测·安全鞋检测
即兴小索奇1 小时前
AI适老服务暖人心:AI适老机顶盒破数字鸿沟、毫米波雷达护独居安全,银发生活新保障
人工智能·ai·语音识别·商业·ai商业洞察·即兴小索奇
riveting1 小时前
技术突破引领应用革新:RK3506 开发板的嵌入式革命
人工智能·物联网·制造·明远智睿·rk3506
Lntano__y2 小时前
详细分析大语言模型attention的计算复杂度,从数学角度分析
人工智能·语言模型·自然语言处理
法迪2 小时前
【学习】Linux 内核中的 cgroup freezer 子系统
人工智能·opencv·计算机视觉
魔乐社区2 小时前
OpenAI重新开源!gpt-oss-20b适配昇腾并上线魔乐社区
人工智能·gpt·深度学习·开源·大模型
用户5191495848452 小时前
WordPress开放嵌入自动发现功能中的XSS漏洞分析
人工智能·aigc