深度学习入门(十)RNN、LSTM、GRU

RNN模型分类

从两个角度对RNN模型进行分类. 第一个角度是输入和输出的结构, 第二个角度是RNN的内部构造

N vs N - RNN:

它是RNN最基础的结构形式, 最大的特点就是: 输入和输出序列是等长的. 由于这个限制的存在, 使其适用范围比较小, 可用于生成等长度的合辙诗句.

N vs 1 - RNN:

有时候我们要处理的问题输入是一个序列,而要求输出是一个单独的值而不是序列,应该怎样建模呢?我们只要在最后一个隐层输出h上进行线性变换就可以了,大部分情况下,为了更好的明确结果, 还要使用sigmoid或者softmax进行处理. 这种结构经常被应用在文本分类问题上.

1 vs N - RNN:

如果输入不是序列而输出为序列的情况怎么处理呢?我们最常采用的一种方式就是使该输入作用于每次的输出之上. 这种结构可用于将图片生成文字任务等.

N vs M - RNN:

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

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

关于RNN的内部构造进行分类的内容我们将在后面使用单独的小节详细讲解.

传统RNN

内部结构图

公式

复制代码
    h_t = \tanh(x_t W_{ih}^T + b_{ih} + h_{t-1}W_{hh}^T + b_{hh})

其中,:math:h_t 是时间 t 的隐藏状态,:math:x_t 是时间 t 的输入,:math:h_{(t-1)} 是上一时刻 t-1 的隐藏状态,或者时间 0 的初始隐藏状态。如果 :attr:nonlinearity'relu',则使用 :math:\text{ReLU} 函数代替 :math:\tanh

nn.RNN使用示例1

python 复制代码
import torch
import torch.nn as nn


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

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

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

    # [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)


dm_rnn_for_base()
python 复制代码
output---> torch.Size([1, 3, 6]) tensor([[[ 0.4320,  0.3604, -0.3584,  0.3799,  0.2293,  0.2679],
         [-0.2547, -0.5575, -0.6784, -0.4315,  0.5883, -0.1581],
         [ 0.0080,  0.4830,  0.1754, -0.8194,  0.9422, -0.7051]]],
       grad_fn=<StackBackward0>)
hn---> torch.Size([1, 3, 6]) tensor([[[ 0.4320,  0.3604, -0.3584,  0.3799,  0.2293,  0.2679],
         [-0.2547, -0.5575, -0.6784, -0.4315,  0.5883, -0.1581],
         [ 0.0080,  0.4830,  0.1754, -0.8194,  0.9422, -0.7051]]],
       grad_fn=<StackBackward0>)
rnn模型---> RNN(5, 6)

nn.RNN使用示例2

输入数据长度发生变化

python 复制代码
import torch


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

    """
    第一个参数:sequence_length(输入序列的长度)
    第二个参数:batch_size(批次的样本数量)
    第三个参数:input_size(输入张量的维度)
    """
    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)
    print('hn--->', hn.shape)
    print('rnn模型--->', rnn)


dm_rnn_for_sequence_len()
python 复制代码
output---> torch.Size([20, 3, 6])
hn---> torch.Size([1, 3, 6])
rnn模型---> RNN(5, 6)

nn.RNN使用示例3

python 复制代码
import torch


def dm_run_for_hidden_num():
    """
    第一个参数:input_size(输入张量x的维度)
    第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
    第三个参数:num_layer(隐藏层的数量)
    """
    rnn = torch.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=11)


dm_run_for_hidden_num()
python 复制代码
output--> torch.Size([1, 3, 6]) tensor([[[ 0.2826, -0.1749, -0.7063,  0.4330, -0.7987,  0.5027],
         [-0.6392,  0.2742, -0.1241, -0.3682,  0.1564, -0.1928],
         [-0.5529, -0.0269,  0.9240, -0.6768,  0.4402, -0.1243]]],
       grad_fn=<StackBackward0>)
hn--> torch.Size([2, 3, 6]) tensor([[[ 0.2711,  0.1775, -0.4353,  0.6829, -0.6478, -0.8098],
         [-0.6810,  0.2980,  0.2135,  0.6189, -0.3950,  0.4263],
         [-0.7016, -0.7970,  0.1254, -0.7231, -0.8568,  0.6428]],

        [[ 0.2826, -0.1749, -0.7063,  0.4330, -0.7987,  0.5027],
         [-0.6392,  0.2742, -0.1241, -0.3682,  0.1564, -0.1928],
         [-0.5529, -0.0269,  0.9240, -0.6768,  0.4402, -0.1243]]],
       grad_fn=<StackBackward0>)
rnn模型---> RNN(5, 6, num_layers=2)

LSTM模型

内部结构图

LSTM(Long Short-Term Memory)也称长短时记忆结构, 它是传统RNN的变体, 与经典RNN相比能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象. 同时LSTM的结构更复杂, 它的核心结构可以分为四个部分去解析:

遗忘门(Forget Gate)------ "该忘掉什么?"

  • 作用:决定上一时刻的细胞状态(长期记忆)里,哪些信息要丢掉

  • 通俗比喻:就像你在记日记,昨天写了"我今天要减肥",但今天吃了火锅,你就想把"减肥"这条记忆忘掉。

  • 数学上:用 sigmoid 函数输出 0~1 的值,0 = 完全忘记,1 = 完全保留。

  • 0~1 的遗忘权重 → 乘到旧细胞状态 C_{t-1} 上。

输入门(Input Gate)------ "该记住什么新东西?"

  • 作用:决定当前输入的新信息,有多少要加到长期记忆里
  • 分两步:
    • 第一步:用 sigmoid 决定"要不要记"(0~1)
    • 第二步:用 tanh 算出"如果要记,具体记什么内容"(-1~1 的候选值)
  • 通俗比喻:你今天吃了火锅,决定"火锅真香"这条信息要记下来,但不是全部情绪都要记,只记"辣度 8 分,很过瘾"这条精华。
  • 公式:

细胞状态(Cell State Update)------ "把新旧记忆融合"

  • 作用:把"旧记忆(经过遗忘门过滤后的)" + "新记忆(经过输入门过滤后的)"加起来,形成新的长期记忆 C_t。
  • 通俗比喻:昨天的日记(旧记忆)删掉"减肥"后,今天加上"火锅真香",合成新日记。
  • 公式:

输出门(Output Gate)------ "该对外输出什么?"

  • 作用:决定当前时刻要对外输出(给下一个 cell 或最终预测)哪些信息
  • 分两步:
    • sigmoid 决定"要输出什么"(0~1)
    • 把细胞状态 C_t 通过 tanh 变成 -1~1,再乘上输出权重,得到最终隐藏状态 h_t。
  • 通俗比喻:你今天吃了火锅,决定对外说"今天很爽",但不说"我吃了三碗米饭"。
  • 公式:

LSTM优缺点

  • LSTM优势:

    LSTM的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸, 虽然并不能杜绝这种现象, 但在更长的序列问题上表现优于传统RNN.

  • LSTM缺点:

    由于内部结构相对较复杂, 因此训练效率在同等算力下较传统RNN低很多.

Bi-LSTM介绍

Bi-LSTM即双向LSTM, 它没有改变LSTM本身任何的内部结构, 只是将LSTM应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出。

我们看到图中对"我爱中国"这句话或者叫这个输入序列, 进行了从左到右和从右到左两次LSTM处理, 将得到的结果张量进行了拼接作为最终输出. 这种结构能够捕捉语言语法中一些特定的前置或后置特征, 增强语义关联,但是模型参数和计算复杂度也随之增加了一倍, 一般需要对语料和计算资源进行评估后决定是否使用该结构。

python 复制代码
import torch.nn as nn
import torch


def dm_lstm():
    """
    第一个参数:input_size(输入张量x的维度)
    第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
    第三个参数:num_layer(隐藏层的数量)
    """
    rnn = nn.LSTM(5, 6, 2)

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

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

    c0 = torch.randn(2, 3, 6)

    output, (hn, cn) = rnn(input, (h0, c0))

    print('output-->', output.shape, output)
    print('hn-->', hn.shape, hn)
    print('cn-->', cn.shape, cn)
    print('rnn模型--->', rnn)


dm_lstm()
python 复制代码
output--> torch.Size([1, 3, 6]) tensor([[[-0.3224,  0.1985, -0.0751,  0.0005,  0.2105, -0.1516],
         [ 0.1574,  0.0314,  0.2190,  0.0948,  0.2185,  0.0148],
         [-0.0928, -0.0878, -0.0488,  0.2341,  0.3469, -0.1718]]],
       grad_fn=<MkldnnRnnLayerBackward0>)
hn--> torch.Size([2, 3, 6]) tensor([[[-3.4701e-02,  2.0348e-01, -1.0518e-02, -2.4653e-02, -2.9480e-02,
           4.3411e-02],
         [ 1.4300e-02, -2.7515e-01, -2.1168e-01,  5.5081e-01,  2.5772e-01,
          -9.0823e-02],
         [-3.2285e-01,  1.3861e-01,  1.4773e-01,  2.6684e-01,  2.5339e-01,
          -9.3171e-02]],

        [[-3.2242e-01,  1.9848e-01, -7.5119e-02,  4.9545e-04,  2.1053e-01,
          -1.5159e-01],
         [ 1.5745e-01,  3.1420e-02,  2.1896e-01,  9.4843e-02,  2.1849e-01,
           1.4844e-02],
         [-9.2831e-02, -8.7809e-02, -4.8831e-02,  2.3410e-01,  3.4690e-01,
          -1.7183e-01]]], grad_fn=<StackBackward0>)
cn--> torch.Size([2, 3, 6]) tensor([[[-0.1281,  0.8242, -0.0518, -0.0968, -0.0456,  0.1523],
         [ 0.0270, -0.6408, -0.8990,  1.0246,  0.5178, -0.2018],
         [-0.8174,  0.2502,  0.2077,  0.8208,  0.7850, -0.5001]],

        [[-0.6332,  0.4563, -0.1930,  0.0016,  0.3750, -0.2496],
         [ 0.2917,  0.0544,  0.3335,  0.2279,  0.5234,  0.0288],
         [-0.2121, -0.1717, -0.0879,  1.1136,  1.2385, -0.2947]]],
       grad_fn=<StackBackward0>)
rnn模型---> LSTM(5, 6, num_layers=2)

GRU模型

GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象。同时它的结构和计算要比LSTM更简单, 它的核心结构可以分为两个部分去解析:

内部结构图

GRU的更新门和重置门结构图:

更新门(Update Gate)------ "这次要记住多少旧的、加多少新的?"

  • 作用 :决定当前时刻的隐藏状态 h_t,要保留多少上一时刻的旧记忆(h_{t-1}),同时要加入多少当前输入的新信息
  • 一句话理解 :它控制"旧记忆"和"新记忆"的混合比例。
    • 更新门值接近 1 → 几乎全保留旧记忆(适合长距离依赖)
    • 更新门值接近 0 → 几乎全用新信息(适合短期变化)
  • 通俗比喻:你在记日记,昨天写了"我要减肥",今天吃了火锅。 更新门值高 → 你就继续写"还是要减肥"(旧记忆占主导)。 更新门值低 → 你直接改成"火锅真香,减肥先放放"(新信息占主导)。
  • 公式: z_t = σ(W_z · [h_{t-1}, x_t] + b_z) → z_t 是 0~1 的值,决定旧记忆和新记忆的加权比例。

重置门(Reset Gate)------ "旧记忆里哪些部分要先忘掉再算新内容?"

  • 作用 :决定在计算当前候选新记忆时,要忽略(重置)多少上一时刻的隐藏状态 h_{t-1}
  • 一句话理解 :它控制"旧记忆对新记忆计算的影响程度"。
    • 重置门值接近 1 → 保留旧记忆的影响(新内容和旧内容强相关)。
    • 重置门值接近 0 → 几乎忽略旧记忆(当前输入几乎从零开始计算)。
  • 通俗比喻:你今天想写"火锅真香",但昨天的"减肥"记忆会干扰你。 重置门值高 → 你会把"减肥"和"火锅"一起考虑(写成"虽然要减肥,但火锅真香")。 重置门值低 → 你直接忽略昨天的减肥,直接写"火锅超爽"(旧记忆不影响新内容)。
  • 公式: r_t = σ(W_r · [h_{t-1}, x_t] + b_r) → r_t 是 0~1 的值,乘到 h_{t-1} 上,决定保留多少旧记忆去计算候选新记忆。

Bi-GRU介绍

Bi-GRU与Bi-LSTM的逻辑相同, 都是不改变其内部结构, 而是将模型应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出. 具体参见上小节中的Bi-LSTM.

GRU优缺点

  • GRU的优势:

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

    • GRU仍然不能完全解决梯度消失问题, 同时其作用RNN的变体, 有着RNN结构本身的一大弊端, 即不可并行计算, 这在数据量和模型体量逐步增大的未来, 是RNN发展的关键瓶颈.
python 复制代码
import torch


def dm_gru():
    """
    第一个参数:input_size(输入张量x的维度)
    第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
    第三个参数:num_layer(隐藏层的数量)
    bidirectional=False, 是否是Bi-GRU
    """
    rnn = torch.nn.GRU(5, 6, 2, bidirectional=False, )

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

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

    output, hn = rnn(input, h0)

    print('output-->', output.shape, output)
    print('hn-->', hn.shape, hn)
    print('rnn模型--->', rnn)


dm_gru()
python 复制代码
output--> torch.Size([1, 3, 6]) tensor([[[ 0.1542, -0.1329,  0.7914, -0.3325,  0.1283, -0.1653],
         [-0.1383, -0.8248,  0.6732, -0.2581,  0.8363, -0.9070],
         [-0.0325, -0.8599, -0.5409, -0.1649, -0.1222,  0.7552]]],
       grad_fn=<StackBackward0>)
hn--> torch.Size([2, 3, 6]) tensor([[[-0.2576,  0.1694,  0.5491, -0.1092,  0.2666, -0.3225],
         [ 0.8471,  0.9595, -0.8849, -1.1791,  0.4446, -1.1334],
         [-1.3785,  0.3195,  0.2242,  0.1168, -0.7916,  0.0907]],

        [[ 0.1542, -0.1329,  0.7914, -0.3325,  0.1283, -0.1653],
         [-0.1383, -0.8248,  0.6732, -0.2581,  0.8363, -0.9070],
         [-0.0325, -0.8599, -0.5409, -0.1649, -0.1222,  0.7552]]],
       grad_fn=<StackBackward0>)
rnn模型---> GRU(5, 6, num_layers=2)

RNN,LSTM,GRU对比:

RNN(普通循环)是个金鱼脑子,句子一长它就忘了开头(梯度消失)。

LSTM(长短期记忆网络):给 AI 装了一个"日记本(细胞状态 Cell State)。

它多了三个保安(门控机制):

遗忘门:决定把日记本里哪些没用的废话擦掉。

输入门:决定把今天的新知识写进日记本。

输出门:决定今天跟别人聊天时,要用日记本里的哪些内容。

GRU:是 LSTM 的"缩水省钱版"。把三个保安精简成了两个(更新门、重置门),速度更快,效果差不多。

相关推荐
谁在黄金彼岸2 小时前
构建一个多Agent系统(Multi-Agent System, MAS)方法论
人工智能
pandafeeder2 小时前
Agent工具调用范式:ReAct 和Function Calling
人工智能
jinanwuhuaguo2 小时前
OpenClaw字节跳动的三只不同的claw龙虾飞书妙搭 OpenClaw、ArkClaw、扣子 OpenClaw 核心区别深度解析
人工智能·语言模型·自然语言处理·visual studio code·openclaw
咚咚王者2 小时前
人工智能之语言领域 自然语言处理 第十八章 Python NLP生态
人工智能·python·自然语言处理
yeflx2 小时前
三维空间坐标转换早期笔记
人工智能·算法·机器学习
zzh940772 小时前
Gemini 3.1 Pro 2026年国内使用指南:技术解析与镜像站实测
人工智能
初学大模型2 小时前
基于三层架构的自动驾驶系统设计:环境建模、标准驾驶与风险调制
人工智能
●VON2 小时前
半小时从零开发鸿蒙记事本应用:AI辅助开发实战
人工智能·华为·harmonyos
特立独行的猫a2 小时前
ESP32小智AI的WebSocket 调试工具实现,小智AI后台交互过程揭秘(一、开篇介绍 )
人工智能·websocket·网络协议·esp32·小智ai