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

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()
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

输入数据长度发生变化

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()
output---> torch.Size([20, 3, 6])
hn---> torch.Size([1, 3, 6])
rnn模型---> RNN(5, 6)

nn.RNN使用示例3

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()
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处理, 将得到的结果张量进行了拼接作为最终输出. 这种结构能够捕捉语言语法中一些特定的前置或后置特征, 增强语义关联,但是模型参数和计算复杂度也随之增加了一倍, 一般需要对语料和计算资源进行评估后决定是否使用该结构。

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()
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发展的关键瓶颈.
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()
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 的“缩水省钱版”。把三个保安精简成了两个(更新门、重置门),速度更快,效果差不多。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐