【LSTM系列·终篇】PyTorch nn.LSTM 终极指南:从API原理到双向多层实战,彻底告别维度错误!

文章目录

  • [25、LSTM - API](#25、LSTM - API)
    • [一、API 设计哲学:为什么这样设计?](#一、API 设计哲学:为什么这样设计?)
    • [二、完整函数签名(PyTorch 2.5+)](#二、完整函数签名(PyTorch 2.5+))
    • 三、参数详解(逐个深度解析)
      • [1. `input_size: int`](#1. input_size: int)
      • [2. `hidden_size: int`](#2. hidden_size: int)
      • [3. `num_layers: int = 1`](#3. num_layers: int = 1)
      • [4. `bias: bool = True`](#4. bias: bool = True)
      • [5. `batch_first: bool = False` ⭐️ **极易出错!**](#5. batch_first: bool = False ⭐️ 极易出错!)
      • [6. `dropout: float = 0.0`](#6. dropout: float = 0.0)
      • [7. `bidirectional: bool = False`](#7. bidirectional: bool = False)
      • [8. `proj_size: int = 0` (Projected LSTM)](#8. proj_size: int = 0 (Projected LSTM))
      • [9. `device=None, dtype=None`](#9. device=None, dtype=None)
    • 四、输入与输出:维度全解析
      • [1. **输入 `input`**](#1. 输入 input)
      • [2. **初始状态 `hx`(可选)**](#2. 初始状态 hx(可选))
      • [3. 输出详解](#3. 输出详解)
    • 五、模型对象的内部结构与常用属性
      • [1. **参数命名规则(关键!)**](#1. 参数命名规则(关键!))
      • [2. **查看所有参数**](#2. 查看所有参数)
      • [3. **获取总参数量**](#3. 获取总参数量)
    • 六、常用操作与高级技巧
      • [1. **基本前向传播**](#1. 基本前向传播)
      • [2. **自定义初始状态**](#2. 自定义初始状态)
      • [3. **处理变长序列(推荐!)**](#3. 处理变长序列(推荐!))
      • [4. **权重初始化(重要!)**](#4. 权重初始化(重要!))
      • [5. **冻结某一层**](#5. 冻结某一层)
      • [6. **提取中间层输出(用于特征提取)**](#6. 提取中间层输出(用于特征提取))
      • [7. **梯度裁剪(防爆炸)**](#7. 梯度裁剪(防爆炸))
      • [8. **使用 device 和 dtype 参数(PyTorch 2.0+)**](#8. 使用 device 和 dtype 参数(PyTorch 2.0+))
    • [七、`nn.LSTM` vs `nn.LSTMCell`](#七、nn.LSTM vs nn.LSTMCell)
    • 八、常见错误与最佳实践
      • [❌ 错误 1:忽略 `batch_first` 导致维度错乱](#❌ 错误 1:忽略 batch_first 导致维度错乱)
      • [❌ 错误 2:双向时误用 `h_n` 形状](#❌ 错误 2:双向时误用 h_n 形状)
      • [❌ 错误 3:变长序列未 packed,导致 padding 干扰](#❌ 错误 3:变长序列未 packed,导致 padding 干扰)
      • [❌ 错误 4:忘记初始化 forget gate bias](#❌ 错误 4:忘记初始化 forget gate bias)
    • 九、参数量计算公式(供参考)
    • 十、总结:一张表掌握全部
  • [26、在PyTorch里输入到底有几个------1类 or 2类](#26、在PyTorch里输入到底有几个——1类 or 2类)
  • 27、Bi-LSTM
  • [28、多层 Bi-LSTM](#28、多层 Bi-LSTM)
  • 29、代码:

由于字数限制,本篇是【LSTM系列】第五篇,也是最后一篇

第一篇链接:【LSTM系列·第一篇】彻底搞懂:细胞状态、隐藏状态、候选状态、遗忘门------新手最晕的4个概念,一篇厘清

第二篇链接:【LSTM系列·第二篇】彻底搞懂输入门、输出门与LSTM全流程:维度分析+PyTorch工程实现

第三篇链接:【LSTM系列·第三篇】单样本 vs Batch:LSTM全流程计算对比,彻底搞懂为何 h_t 与 c_t 维度必须相同

第四篇链接:【LSTM系列·第四篇】彻底搞懂:单样本与 batch 的矩阵等价性、参数共享原理、多层 LSTM 的 PyTorch 维度自动适配

25、LSTM - API

一、API 设计哲学:为什么这样设计?

在深入细节前,先理解 PyTorch 对 RNN 的抽象逻辑:

  • nn.LSTM 是一个"序列到序列"或"序列到状态"的函数 ,它封装了:
    • 多层堆叠
    • 双向处理
    • 层间 dropout
    • 投影输出(Projected LSTM)
    • 自动零初始化
  • 目标:用户只需指定高层语义(如"我要一个 2 层双向 LSTM"),框架自动处理底层维度衔接、参数分配、计算图构建。
  • 核心原则参数共享 + 时间展开 + 层堆叠 = 统一接口

这使得 nn.LSTM 成为高内聚、低耦合的工业级组件。


二、完整函数签名(PyTorch 2.5+)

python 复制代码
torch.nn.LSTM(
    input_size: int,          # 必填|输入特征维度(如词向量维度)
    hidden_size: int,         # 必填|LSTM cell 的隐藏状态和细胞状态的内部维度(H)
    num_layers: int = 1,      # 可选|堆叠的 LSTM 层数,默认 1
    bias: bool = True,        # 可选|是否在 Linear 层加偏置,默认 True
    batch_first: bool = False,# 可选|若为 True,输入/输出形状为 (B, L, D);否则为 (L, B, D)
    dropout: float = 0.0,     # 可选|层间 dropout 概率(仅当 num_layers > 1 时生效),默认 0(无 dropout)
    bidirectional: bool = False, # 可选|是否使用双向 LSTM,默认单向
    proj_size: int = 0,       # 可选|投影维度(若 >0,则隐藏状态输出维度 = proj_size,但细胞状态仍为 hidden_size);默认 0 表示不投影
    device=None,              # 可选|设备(如 'cpu' 或 'cuda')
    dtype=None                # 可选|数据类型(如 torch.float32)
)

✅ 所有参数均为 关键字参数(keyword-only),不可位置传参(从 PyTorch 1.8 起)。


三、参数详解(逐个深度解析)

1. input_size: int

  • 含义 :输入特征维度 D D D,即每个时间步输入向量 x t ∈ R D x_t \in \mathbb{R}^D xt∈RD 的长度。
  • 典型值
    • 词嵌入维度(如 300)
    • 语音 MFCC 特征数(如 40)
    • 传感器通道数(如 6)
  • 注意 :仅影响第 0 层weight_ih 形状;高层输入来自上一层输出(维度 = hidden_size2*hidden_size)。

2. hidden_size: int

  • 含义 :LSTM 细胞状态 c t c_t ct隐藏状态 h t h_t ht(若无投影) 的维度 H H H。
  • 关键点
    • 所有层的 hidden_size 必须相同(除非手动拼接多个 LSTM);
    • 细胞状态始终是 hidden_size ,不受 proj_size 影响;
    • 门控信号(i/f/g/o)均为 H H H 维(因由 W ∈ R 4 H × ⋅ W\in\mathbb{R}^{4H\times\cdot} W∈R4H×⋅ 输出)。
  • 经验选择:128, 256, 512(越大容量越高,但易过拟合)。

📌 1. 关于 4H 的含义

在 LSTM 中,4H 表示的是:4 倍的隐藏层维度

  • H = hidden_size = 隐藏状态 h_t 和细胞状态 c_t 的维度
  • 为什么要乘以 4?因为 LSTM 有 4 个门控信号
    • 输入门 i_t ∈ ℝ^H
    • 遗忘门 f_t ∈ ℝ^H
    • 候选记忆门 g_t ∈ ℝ^H
    • 输出门 o_t ∈ ℝ^H

所以,如果我们将这 4 个门的输出垂直拼接 在一起,就会得到一个 4H 维的向量:

复制代码
z_t = [i_t; f_t; g_t; o_t] ∈ ℝ^(4H)

📌 2. 关于 · 符号的含义

在数学公式中,· 通常表示占位符省略号,表示该位置的维度取决于上下文。

例如:

  • W ∈ ℝ^(4H × ·) 意味着:W 的行数是 4H,列数由具体输入决定
  • W_ih 中:W ∈ ℝ^(4H × input_size) → 列数 = input_size
  • W_hh 中:W ∈ ℝ^(4H × hidden_size) → 列数 = hidden_size

涉及到 LSTM 实现中的一个重要设计细节。让我来详细解释。


理论公式中,LSTM 的门控计算确实是这样的:

z t = W [ h t − 1 x t ] + b z_t = W \begin{bmatrix} h_{t-1} \\ x_t \end{bmatrix} + b zt=W[ht−1xt]+b

其中:

  • h t − 1 x t \] ∈ R ( H + D ) \\begin{bmatrix} h_{t-1} \\\\ x_t \\end{bmatrix} \\in \\mathbb{R}\^{(H + D)} \[ht−1xt\]∈R(H+D)

但是!PyTorch 的实现方式不同。


🔍 PyTorch 的实际实现方式

PyTorch 将权重矩阵拆分成了两个独立的矩阵

  1. weight_ih(input-to-hidden)
  • 只处理输入 x t x_t xt
  • 形状:(4H, input_size)
  • 计算: W i h ⋅ x t W_{ih} \cdot x_t Wih⋅xt
  1. weight_hh(hidden-to-hidden)
  • 只处理隐藏状态 h t − 1 h_{t-1} ht−1
  • 形状:(4H, hidden_size)
  • 计算: W h h ⋅ h t − 1 W_{hh} \cdot h_{t-1} Whh⋅ht−1
  1. 最终计算
python 复制代码
# PyTorch 内部实际计算(简化版)
gates = (x_t @ weight_ih.T) + (h_{t-1} @ weight_hh.T) + bias
i, f, g, o = gates.chunk(4, dim=-1)

📊 两种实现方式的对比

实现方式 权重矩阵 参数数量 计算方式
理论公式 W ∈ R 4 H × ( H + D ) W \in \mathbb{R}^{4H \times (H + D)} W∈R4H×(H+D) 4 H ( H + D ) 4H(H + D) 4H(H+D) W ⋅ [ h ; x ] W \cdot [h; x] W⋅[h;x]
PyTorch W i h ∈ R 4 H × D W_{ih} \in \mathbb{R}^{4H \times D} Wih∈R4H×D W h h ∈ R 4 H × H W_{hh} \in \mathbb{R}^{4H \times H} Whh∈R4H×H 4 H ( D + H ) 4H(D + H) 4H(D+H) W i h x + W h h h W_{ih}x + W_{hh}h Wihx+Whhh

💡 参数数量完全相同! 只是存储和计算方式不同。


🧠 为什么 PyTorch 要这样设计?

  1. 内存效率
  • 如果拼接 [h; x],需要额外的内存分配
  • 分开计算避免了这种开销
  1. 计算效率
  • 矩阵乘法可以并行执行
  • 更好地利用 BLAS 库优化
  1. 灵活性
  • 可以单独初始化 weight_ihweight_hh
  • 便于实现某些变体(如只更新部分权重)
  1. 与 RNNCell 一致
  • nn.LSTMCell 也采用同样的设计
  • 保持 API 一致性

✅ 总结

  1. 理论上权重矩阵应该是 (4H, H+D)
  2. PyTorch 的实现 :将其拆分为两个矩阵 (4H, D)(4H, H)
  3. 数学等价性:两种方式在数学上是完全等价的
  4. 工程优势:PyTorch 的方式在内存和计算上更高效

所以当我之前说 "列数 = input_size" 时,我指的是 weight_ih 这个特定矩阵的列数,而不是整个理论上的权重矩阵。


📌 3. 权重矩阵的完整形状

在 LSTM 中,我们有两类主要的权重矩阵:

(1) weight_ih (input-to-hidden weights)

  • 用于将输入 x_t 映射到门控信号
  • 形状:weight_ih ∈ ℝ^(4H × input_size)
  • 例如:input_size=100, hidden_size=256weight_ih ∈ ℝ^(1024 × 100)

(2) weight_hh (hidden-to-hidden weights)

  • 用于将前一时刻隐藏状态 h_{t-1} 映射到门控信号
  • 形状:weight_hh ∈ ℝ^(4H × hidden_size)
  • 例如:hidden_size=256weight_hh ∈ ℝ^(1024 × 256)

📌 4. 为什么是 4H 而不是 H

因为 LSTM 会同时计算 4 个门,而不是一个门。在 PyTorch 中,为了效率,它会一次性计算所有门,而不是分别计算:

python 复制代码
# PyTorch 内部实际操作(简化版)
combined_gates = input @ weight_ih.T + hidden @ weight_hh.T + bias
i, f, g, o = combined_gates.chunk(4, dim=-1)  # 拆分成 4 个 H 维向量

📌 5. 修正后的参数详解

hidden_size: int

  • 含义 :LSTM 细胞状态 c_t隐藏状态 h_t(若无投影) 的维度 H
  • 关键点
    • 所有层的 hidden_size 必须相同(除非手动拼接多个 LSTM);
    • 细胞状态始终是 hidden_size ,不受 proj_size 影响;
    • 门控信号(i/f/g/o)均为 H 维(因由 W ∈ ℝ^(4H × input_dim) 输出,然后拆分成 4 个 H 维向量)。
  • 经验选择:128, 256, 512(越大容量越高,但易过拟合)。

📌 6. 实际例子

python 复制代码
lstm = nn.LSTM(input_size=100, hidden_size=256)

# 查看权重形状
print(lstm.weight_ih_l0.shape)  # torch.Size([1024, 100]) ← 4H = 4×256 = 1024
print(lstm.weight_hh_l0.shape)  # torch.Size([1024, 256]) ← 4H = 4×256 = 1024
  • 1024 = 4 × 256,其中 256 = hidden_size
  • 1024 维输出会被拆分成 4 个 256 维向量(i/f/g/o 门)

📌 7. 总结符号含义

符号 含义
H hidden_size(隐藏状态维度)
4H 4 × hidden_size(因为有 4 个门)
· 占位符,表示该维度由上下文决定
ℝ^(4H × input_size) 行数为 4H,列数为 input_size 的矩阵

现在你明白了:4H 是因为 LSTM 同时计算 4 个门控信号,每个都是 H 维,所以总输出是 4H


3. num_layers: int = 1

  • 含义:垂直堆叠的 LSTM 层数。
  • 工作机制
    • 第 0 层输入:原始序列 X X X
    • 第 l l l 层输入( l ≥ 1 l\geq1 l≥1):第 l − 1 l-1 l−1 层的输出序列 H ( l − 1 ) H^{(l-1)} H(l−1)
  • 注意
    • num_layers=1dropout 参数被忽略;
    • 每层有独立参数集,不共享权重。

4. bias: bool = True

  • 含义:是否在门控计算中添加偏置项。

  • 公式体现

    z t = W [ h t − 1 x t ] + b z_t = W \begin{bmatrix} h_{t-1} \\ x_t \end{bmatrix} + b zt=W[ht−1xt]+b

  • 几乎所有场景都应设为 True 。设为 False 会降低模型表达能力。

  • 参数数量影响 :每层增加 8 H 8H 8H 个参数(4 个门 × 2 偏置:bias_ih + bias_hh)。


5. batch_first: bool = False ⭐️ 极易出错!

  • 含义:输入/输出张量的第一个维度是否为 batch。
  • 形状对比
设置 输入 input 输出 output
batch_first=False (默认) (seq_len, batch, input_size) (seq_len, batch, H_out × D_dir)
batch_first=True (batch, seq_len, input_size) (batch, seq_len, H_out × D_dir)
  • batch_first 只影响输入 x 和输出 output 的维度顺序,
    h_nc_n 的形状完全没有影响。
  • 建议
    • NLP 任务强烈推荐 batch_first=True ,与 DataLoaderTokenizer 输出对齐;
    • 若使用 pack_padded_sequence,也需保持一致。

6. dropout: float = 0.0

  • 含义仅在 num_layers > 1 时生效 ,在层与层之间应用 dropout。
  • 作用位置
    • 第 0 层输出 → 经 dropout → 作为第 1 层输入
    • 第 1 层输出 → 经 dropout → 作为第 2 层输入
    • ...
  • 注意
    • 不在时间步之间加 dropout(那是你自己的事);
    • 不在最后一层输出后加 dropout
    • 双向时,前向和后向分支分别加 dropout
  • 典型值:0.2 ~ 0.5

7. bidirectional: bool = False

后面有详情

  • 含义:是否启用双向处理。
  • 内部实现
    • 创建 两个独立的 LSTM 子模块forwardbackward
    • 两者参数不共享
  • 输出融合 :默认拼接 [h_forward; h_backward]
  • 方向数num_directions = 2 if bidirectional else 1

8. proj_size: int = 0 (Projected LSTM)

  • 含义 :隐藏状态的投影维度(Sak et al., 2014)。
  • 工作机制
    • 细胞状态 c t ∈ R h i d d e n _ s i z e c_t\in\mathbb{R}^{hidden\_size} ct∈Rhidden_size
    • 隐藏状态 h t = W proj ⋅ tanh ⁡ ( c t ) ∈ R p r o j _ s i z e h_t=W_{\text{proj}}\cdot\tanh(c_t)\in\mathbb{R}^{proj\_size} ht=Wproj⋅tanh(ct)∈Rproj_size
  • 何时使用
    • 语音识别中降低后续层计算量(如 hidden_size=1024, proj_size=512
  • 注意
    • proj_size==0,则 h t = o t ⊙ tanh ⁡ ( c t ) h_t=o_t\odot\tanh(c_t) ht=ot⊙tanh(ct),维度 = hidden_size
    • proj_size>0,则额外学习投影矩阵 W proj ∈ R p r o j _ s i z e × h i d d e n _ s i z e W_{\text{proj}}\in\mathbb{R}^{proj\_size\times hidden\_size} Wproj∈Rproj_size×hidden_size
  • PyTorch 支持:✅ 从 1.8+ 开始

9. device=None, dtype=None

  • 含义:创建时直接指定设备和数据类型。

  • 优势 :避免后续 .to(device) 调用,减少内存拷贝。

  • 使用示例

    python 复制代码
    lstm = nn.LSTM(100, 256, device='cuda', dtype=torch.float16)

四、输入与输出:维度全解析

✅ 调用方式

python 复制代码
output, (h_n, c_n) = lstm(input, hx=None)

1. 输入 input

  • 类型TensorPackedSequence
  • 形状(Tensor 模式)
    • batch_first=False(L, N, D)
    • batch_first=True(N, L, D)
  • 其中
    • L = sequence length
    • N = batch size
    • D = input_size

📌 支持任意 L(包括 L=1,即单步预测)。


2. 初始状态 hx(可选)

  • 格式(h_0, c_0),两个 Tensor 的元组
  • 若未提供:自动初始化为全零张量
  • 形状要求

h_0(初始隐藏状态):

h 0 ∈ R ( D num_layers × N × H out ) h_0 \in \mathbb{R}^{(D_{\text{num\layers}} \times N \times H{\text{out}})} h0∈R(Dnum_layers×N×Hout)

  • 注意,其中的 D num_layers = n u m _ l a y e r s × n u m _ d i r e c t i o n s D_{\text{num\_layers}}=num\_layers \times num\_directions Dnum_layers=num_layers×num_directions
  • H_out = proj_size if proj_size > 0 else hidden_size

c_0(初始细胞状态):

c 0 ∈ R ( D num_layers × N × h i d d e n _ s i z e ) c_0 \in \mathbb{R}^{(D_{\text{num\_layers}} \times N \times hidden\_size)} c0∈R(Dnum_layers×N×hidden_size)

  • 注意c_0 不受 proj_size 影响 ,始终是 hidden_size 维!

🌰 示例:

python 复制代码
# 配置: 2层, 双向, hidden_size=256, proj_size=128, batch=32
num_directions = 2
D_num_layers = 2 * 2 = 4
H_out = 128
hidden_size = 256

h_0 = torch.zeros(4, 32, 128)   # (4, B, 128)
c_0 = torch.zeros(4, 32, 256)   # (4, B, 256) ← 注意这里是 256!
output, (h_n, c_n) = lstm(x, (h_0, c_0))

3. 输出详解

📘 PyTorch LSTM 输出详解

核心宗旨
output[t] 表示原始输入序列中第 t 个词 w_t 的上下文表示。所有状态下标 t 永远绑定「原始输入位置」,与处理顺序、方向、层数无关。


(1) output

🔹 含义

  • output 是「最后一层」在「每个原始输入位置 t」上,前向和后向隐藏状态的拼接结果。
  • 只包含最后一层的信息,中间层输出不暴露。
  • 用途
    • Token-level 任务 (如 NER、POS 标注):直接使用 output
    • Sentence-level 任务 (如分类):不应使用 output[-1](原因见后)

🔹 形状

  • batch_first=False(L, N, H_out × num_directions)
  • batch_first=True(N, L, H_out × num_directions)

其中:

  • num_directions = 2 if bidirectional else 1
  • H_out = proj_size if proj_size > 0 else hidden_size

🔹 单向 vs 双向对比

配置 output[t] 构成 说明
单向 (bidirectional=False) output[:, t, :] = F_t 仅前向状态,F_t = 最后一层在 w_t 处的隐藏状态
双向 (bidirectional=True) output[:, t, :] = [F_t, B_t] 前向 + 后向在 w_t 处的状态拼接

关键:无论单向还是双向,t 始终指「原始输入位置」


✅ 场景二:双向 LSTM(bidirectional=True)← 重点!

设原始输入序列为:

w 0 , w 1 , w 2 , ... , w L − 1 \] \[w_0,\\ w_1,\\ w_2,\\ \\dots,\\ w_{L-1}\] \[w0, w1, w2, ..., wL−1

对于任意时间步 t ∈ [ 0 , L − 1 ] t \in [0, L-1] t∈[0,L−1]:

python 复制代码
output[:, t, :H_out]      = 最后一层 Forward 在 w_t 处的隐藏状态 → 记为 F_t
output[:, t, H_out:]      = 最后一层 Backward 在 w_t 处的隐藏状态 → 记为 B_t

即:
output [ t ] = [ F t    ∥    B t ] \texttt{output}[t] = [F_t \;\|\; B_t] output[t]=[Ft∥Bt]


🔁 前向 LSTM(Forward Direction)如何得到 F t F_t Ft?

  • 处理顺序 :从左到右
    w 0 → w 1 → ⋯ → w t → ⋯ → w L − 1 w_0 \rightarrow w_1 \rightarrow \cdots \rightarrow w_t \rightarrow \cdots \rightarrow w_{L-1} w0→w1→⋯→wt→⋯→wL−1

  • 状态生成

    • 读入 w 0 w_0 w0 → 输出 F 0 F_0 F0
    • 读入 w 1 w_1 w1 → 输出 F 1 F_1 F1
    • ...
    • 读入 w t w_t wt → 输出 F t F_t Ft
    • ...
    • 读入 w L − 1 w_{L-1} wL−1 → 输出 F L − 1 F_{L-1} FL−1

F t F_t Ft 编码了从 w 0 w_0 w0 到 w t w_t wt 的历史信息(左上下文)

F L − 1 F_{L-1} FL−1 是前向最终状态


🔁 后向 LSTM(Backward Direction)如何得到 B t B_t Bt?

  • 处理顺序 :从右到左
    w L − 1 → w L − 2 → ⋯ → w t → ⋯ → w 0 w_{L-1} \rightarrow w_{L-2} \rightarrow \cdots \rightarrow w_t \rightarrow \cdots \rightarrow w_0 wL−1→wL−2→⋯→wt→⋯→w0

  • 状态生成(关键!)

    • 读入 w L − 1 w_{L-1} wL−1(原始位置 t = L − 1 t = L-1 t=L−1)→ 输出 B L − 1 B_{L-1} BL−1
    • 读入 w L − 2 w_{L-2} wL−2(原始位置 t = L − 2 t = L-2 t=L−2)→ 输出 B L − 2 B_{L-2} BL−2
    • ...
    • 读入 w t w_t wt(原始位置 t t t)→ 输出 B t B_t Bt
    • ...
    • 读入 w 0 w_0 w0(原始位置 t = 0 t = 0 t=0)→ 输出 B 0 B_0 B0

B t B_t Bt 编码了从 w t w_t wt 到 w L − 1 w_{L-1} wL−1 的未来信息(右上下文)

B 0 B_0 B0 是后向最终状态(因为它最后才处理 w 0 w_0 w0)

B L − 1 B_{L-1} BL−1 是后向初始状态(因为它最先处理 w L − 1 w_{L-1} wL−1)
💥 命名铁律
状态下标 t 永远跟随「词在原句中的位置」,不跟随「处理步骤编号」!


📊 示例:序列长度=3( w 0 , w 1 , w 2 w_0, w_1, w_2 w0,w1,w2)

原始位置 t t t F t F_t Ft(前向) B t B_t Bt(后向) output[t] 说明
0 w 0 w_0 w0 看到 w 0 w_0 w0 看到 w 0 , w 1 , w 2 w_0, w_1, w_2 w0,w1,w2 [ F 0 , B 0 ] [F_0, B_0] [F0,B0] B 0 B_0 B0 = backward 最终状态
1 w 1 w_1 w1 看到 w 0 , w 1 w_0, w_1 w0,w1 看到 w 1 , w 2 w_1, w_2 w1,w2 [ F 1 , B 1 ] [F_1, B_1] [F1,B1] ---
2 w 2 w_2 w2 看到 w 0 , w 1 , w 2 w_0, w_1, w_2 w0,w1,w2 看到 w 2 w_2 w2 [ F 2 , B 2 ] [F_2, B_2] [F2,B2] F 2 F_2 F2 = forward 最终状态; B 2 B_2 B2 = backward 初始状态

每个 output[t] 都是关于 w_t 的完整双向上下文表示


⚠️ 常见误区纠正

错误认知 正确认知
"backward 先处理 w 2 w_2 w2,所以它的状态叫 B 0 B_0 B0" " w 2 w_2 w2 在原句中是位置 2 → 状态叫 B 2 B_2 B2"
" B 0 B_0 B0 是 backward 的初始状态" " B 0 B_0 B0 是 backward 的最终状态 (最后处理 w 0 w_0 w0)"
"output[2] = [F_2, B_0]" "output[2] = [F_2, B_2]"
"前向和后向按处理步骤对齐" "前向和后向按原始输入位置对齐"

🔗 与 h_n 的关系(仅最后一层相关)

h_n 形状:(num_layers × num_directions, N, H_out)

  • h_n[-2] = F_{L-1}(最后一层 forward 最终状态)
  • h_n[-1] = B_0(最后一层 backward 最终状态)

因此:

python 复制代码
# ✅ 正确对应
output[:, -1, :H_out] == h_n[-2]   # True → F_{L-1}
output[:,  0, H_out:] == h_n[-1]   # True → B_0

但:

python 复制代码
# ❌ 错误期待
torch.allclose(output[:, -1], torch.cat([h_n[-2], h_n[-1]], dim=-1))  # False!
# 因为 output[-1] = [F_{L-1}, B_{L-1}], 而不是 [F_{L-1}, B_0]

🎯 实际使用建议

任务类型 推荐做法
Token-level(NER, POS) 直接使用 output
Sentence-level(分类) 使用 torch.cat([h_n[-2], h_n[-1]], dim=-1) 或等价地: torch.cat([output[:, 0, H_out:], output[:, -1, :H_out]], dim=-1)

🚫 切勿用 output[:, -1] 作为句子表示!


🧪 验证代码

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

torch.manual_seed(0)
x = torch.randn(1, 3, 5)  # (B=1, L=3, D=5)
lstm = nn.LSTM(5, 4, bidirectional=True, batch_first=True)
output, (h_n, _) = lstm(x)

H = 4
print("output[0] =", output[0, 0])   # [F₀, B₀]
print("output[2] =", output[0, 2])   # [F₂, B₂]

print("h_n[-2] (F₂) =", h_n[-2, 0])
print("h_n[-1] (B₀) =", h_n[-1, 0])

assert torch.allclose(output[0, -1, :H], h_n[-2, 0])  # True
assert torch.allclose(output[0,  0, H:], h_n[-1, 0])  # True

✅ 总结

前向看过去,后向看未来;
状态下标 t,永远跟词走;
output[t] = [Fₜ, Bₜ],
专为 wₜ 服务!


(2) h_n

注意batch_first 仅影响输入 x 和输出 output 的维度排列,h_nc_n 的形状完全无影响

  • 含义:包含所有层、所有方向在完整处理序列后产生的最终隐藏状态。

    • forward 方向:处理完序列最后一个 token 后的状态;
    • backward 方向:处理完序列第一个 token 后的状态(从右向左运行)。
  • 形状(num_layers × num_directions, N, H_out)

    其中:

    • N 为 batch size
    • num_directions = 1bidirectional= False);num_directions = 2bidirectional= True
    • H_out = proj_size(若 proj_size > 0),否则 H_out = hidden_size
  • 存储顺序(分情况说明)

    ✅ 情况 1:单向 LSTMbidirectional=False

    • 每层只有 forward 方向

    • h_n 长度 = num_layers

    • 存储顺序:

      text 复制代码
      h_n[0] → layer 0 的 forward 状态  
      h_n[1] → layer 1 的 forward 状态  
      ...  
      h_n[L−1] → layer L−1 的 forward 状态

    ✅ 情况 2:双向 LSTMbidirectional=True

    • 每层包含 forward + backward 两个方向

    • h_n 长度 = 2 × num_layers

    • 存储顺序(按层优先,每层内先 forward 后 backward):

      text 复制代码
      h_n[0]     → layer 0 的 forward 状态  
      h_n[1]     → layer 0 的 backward 状态  
      h_n[2]     → layer 1 的 forward 状态  
      h_n[3]     → layer 1 的 backward 状态  
      ...  
      h_n[2k]    → layer k 的 forward 状态  
      h_n[2k+1]  → layer k 的 backward 状态

    💡 通用规则

    • 如果是单向h_n[i] 就是第 i 层的 forward 状态;
    • 如果是双向,偶数索引为 forward,奇数索引为 backward,层数 = 索引 // 2。
  • 典型用途

    常用于 sentence-level 任务(如文本分类)。

    获取最后一层完整表示的方法:

    python 复制代码
    if bidirectional:
        # 需要自己手动拼接
        h_final = torch.cat([h_n[-2], h_n[-1]], dim=-1)   # (N, 2 × H_out)
    else:
        h_final = h_n[-1]                                 # (N, H_out)

(3) c_n

细胞状态 c_n 的组织方式与 h_n 完全一致------唯一的区别是:

  • h_n 存的是隐藏状态 (维度 = H_out
  • c_n 存的是细胞状态 (维度 = hidden_size不受 proj_size 影响

✅ 核心结论(先说重点)

c_n 的形状、存储顺序、索引含义 与 h_n 完全相同,
唯一不同:c_n 的最后一维始终是 hidden_size(不是 H_out


📦 c_n 的通用属性

  • 含义 :所有层、所有方向在处理完整序列后的最终细胞状态
  • 形状(num_layers × num_directions, N, hidden_size)
    • 注意:最后一维是 hidden_size,不是 H_out
    • 即使你设置了 proj_size=4c_n 仍是 hidden_size 维(比如 8)
  • 不受 batch_first 影响 (和 h_n 一样)

🔍 分情况详解

✅ 情况 1:单向 LSTMbidirectional=False

  • c_n 长度 = num_layers

  • 存储顺序:

    text 复制代码
    c_n[0] → layer 0 的 forward 细胞状态  
    c_n[1] → layer 1 的 forward 细胞状态  
    ...  
    c_n[L−1] → layer L−1 的 forward 细胞状态

✅ 情况 2:双向 LSTMbidirectional=True

  • c_n 长度 = 2 × num_layers

  • 存储顺序(按层优先,每层内先 forward 后 backward):

    text 复制代码
    c_n[0]     → layer 0 的 forward 细胞状态  
    c_n[1]     → layer 0 的 backward 细胞状态  
    c_n[2]     → layer 1 的 forward 细胞状态  
    c_n[3]     → layer 1 的 backward 细胞状态  
    ...  
    c_n[2k]    → layer k 的 forward 细胞状态  
    c_n[2k+1]  → layer k 的 backward 细胞状态

💡 关键提醒

虽然 h_n[1](backward 隐藏状态)的维度可能是 proj_size

c_n[1](backward 细胞状态)的维度永远是 hidden_size


📌 使用建议

场景 h_n 还是 c_n
初始化解码器(如 seq2seq) 通常只传 h_n(有时也传 c_n
继续推理/生成 需同时传 (h_n, c_n) 作为初始状态
提取句向量 只用 h_nc_n 一般不用于表示学习)
调试 LSTM 内部 可观察 c_n 看记忆单元变化

⚠️ 注意:如果你要把 (h_n, c_n) 传给另一个 LSTM,必须确保两个 LSTM 的 hidden_sizeproj_size 配置一致,否则维度对不上!


✅ 总结表

属性 h_n c_n
形状 (L×D, N, H_out) (L×D, N, hidden_size)
存储顺序 完全相同 完全相同
proj_size 影响? ✅ 是(决定 H_out ❌ 否(始终 hidden_size
典型用途 表示学习、分类 初始化下一阶段 LSTM

其中 D = 1(单向)或 2(双向)

现在你可以放心地说:c_n 就是 h_n 的"孪生兄弟",结构一样,但维度更"原始" 😊

概念 标准 LSTM (proj_size=0) 投影 LSTM (proj_size>0)
细胞状态 c_t hidden_size hidden_size 维(不变
隐藏状态 h_t hidden_size proj_size 维(被投影
H_out 定义 hidden_size proj_size
h_n 最后一维 H_out = hidden_size H_out = proj_size
c_n 最后一维 hidden_size hidden_size

五、模型对象的内部结构与常用属性

创建模型后,你可以访问其内部组件:

python 复制代码
lstm = nn.LSTM(100, 256, num_layers=2, bidirectional=True, device='cuda')

1. 参数命名规则(关键!)

参数名 含义 形状(单向) 形状(双向)
weight_ih_l0 第 0 层 input→hidden (4*H, D) (4*H, D)
weight_hh_l0 第 0 层 hidden→hidden (4*H, H) (4*H, H)
bias_ih_l0 第 0 层 input 偏置 (4*H,) (4*H,)
bias_hh_l0 第 0 层 hidden 偏置 (4*H,) (4*H,)
weight_ih_l0_reverse 第 0 层反向 input→hidden --- (4*H, D)
weight_hh_l0_reverse 第 0 层反向 hidden→hidden --- (4*H, H)
... ... ... ...
weight_hr_l0 投影权重(若 proj_size>0 (P, H) (P, H)

规律

  • _l{k} 表示第 k 层(0-based)
  • _reverse 表示反向 LSTM
  • _hr 表示 hidden-to-projection(仅当 proj_size>0

2. 查看所有参数

python 复制代码
for name, param in lstm.named_parameters():
    print(name, param.shape)

3. 获取总参数量

python 复制代码
total = sum(p.numel() for p in lstm.parameters())
trainable = sum(p.numel() for p in lstm.parameters() if p.requires_grad)

六、常用操作与高级技巧

1. 基本前向传播

python 复制代码
x = torch.randn(10, 32, 100, device='cuda')  # (L=10, B=32, D=100)
output, (h_n, c_n) = lstm(x)

2. 自定义初始状态

python 复制代码
h0 = torch.randn(2, 32, 256, device='cuda')  # 1层双向 → 2 directions
c0 = torch.randn(2, 32, 256, device='cuda')
output, _ = lstm(x, (h0, c0))

3. 处理变长序列(推荐!)

python 复制代码
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

# 假设 lengths = [10, 8, 5, ...] (降序 or enforce_sorted=False)
packed = pack_padded_sequence(x, lengths, batch_first=False, enforce_sorted=False)
packed_out, (h_n, c_n) = lstm(packed)
output, out_lengths = pad_packed_sequence(packed_out, batch_first=False)

✅ 优势:避免 padding 干扰,提升速度与精度。

4. 权重初始化(重要!)

LSTM 默认使用正交初始化,但有时需自定义:

python 复制代码
# 自定义初始化
def init_lstm(lstm):
    for name, param in lstm.named_parameters():
        if 'weight' in name:
            nn.init.orthogonal_(param)
        elif 'bias' in name:
            # forget gate bias 初始化为 1(防止早期遗忘)
            n = param.size(0)
            start, end = n//4, n//2
            param.data[start:end].fill_(1.0)

init_lstm(lstm)

5. 冻结某一层

python 复制代码
# 冻结第 0 层
for name, param in lstm.named_parameters():
    if 'l0' in name:             # 匹配 weight_ih_l0, weight_hh_l0, etc.  (etc 表示 "等等)
        param.requires_grad = False

6. 提取中间层输出(用于特征提取)

python 复制代码
# 手动逐层 forward
x1, _ = lstm.layer0(x)      # 需自行拆分 nn.LSTM
x2, _ = lstm.layer1(x1)
# 但 nn.LSTM 不暴露中间层 → 需用 nn.LSTMCell 或自定义 Module

7. 梯度裁剪(防爆炸)

python 复制代码
torch.nn.utils.clip_grad_norm_(lstm.parameters(), max_norm=1.0)

8. 使用 device 和 dtype 参数(PyTorch 2.0+)

python 复制代码
# 直接在创建时指定设备和数据类型
lstm = nn.LSTM(100, 256, device='cuda', dtype=torch.float16)
x = torch.randn(10, 32, 100, device='cuda', dtype=torch.float16)
output, (h_n, c_n) = lstm(x)

七、nn.LSTM vs nn.LSTMCell

特性 nn.LSTM nn.LSTMCell
输入 整个序列 (L, N, D) 单个时间步 (N, D)
输出 所有时间步输出 + 最终状态 单步输出 (N, H)
多层/双向 内置支持 需手动堆叠
性能 高度优化(CUDNN) 较慢(Python 循环)
使用场景 标准训练/推理 自定义循环逻辑(如强化学习、teacher forcing 控制)

建议 :除非需要精细控制时间步,否则永远用 nn.LSTM


八、常见错误与最佳实践

❌ 错误 1:忽略 batch_first 导致维度错乱

python 复制代码
# 错误:数据是 (B, L, D),但 LSTM 默认 (L, B, D)
lstm = nn.LSTM(100, 256)  # batch_first=False
x = torch.randn(32, 10, 100)
output, _ = lstm(x)  # RuntimeError!

修复lstm = nn.LSTM(100, 256, batch_first=True)


❌ 错误 2:双向时误用 h_n 形状

python 复制代码
# 以为 h_n 是 (B, 2*H)
h_final = h_n.squeeze(0)  # 错!h_n 是 (2, B, H)

正确

python 复制代码
# 获取最后一层双向拼接
h_forward = h_n[-2]  # 倒数第二:最后一层 forward
h_backward = h_n[-1] # 最后:最后一层 backward
h_final = torch.cat([h_forward, h_backward], dim=-1)  # (B, 2H)

❌ 错误 3:变长序列未 packed,导致 padding 干扰

最佳实践 :只要序列长度不一,就用 pack_padded_sequence


❌ 错误 4:忘记初始化 forget gate bias

技巧:将 forget gate 偏置初始化为 1,有助于保留长期记忆。


九、参数量计算公式(供参考)

对于 nn.LSTM(input_size=D, hidden_size=H, num_layers=L, bidirectional=B, proj_size=P)

  • P == 0(标准 LSTM):

    Params = L × ( 1 + B ) × [ ( D in + H ) × 4 H + 8 H ] \text{Params} = L \times (1 + B) \times \left[ (D_{\text{in}} + H) \times 4H + 8H \right] Params=L×(1+B)×[(Din+H)×4H+8H]

    其中 D in = D D_{\text{in}} = D Din=D(第 0 层), D in = H × ( 1 + B ) D_{\text{in}} = H \times (1+B) Din=H×(1+B)(高层)

  • P > 0(Projected LSTM):

    Params = L × ( 1 + B ) × [ ( D in + H ) × 4 H + 8 H + H × P ] \text{Params} = L \times (1 + B) \times \left[ (D_{\text{in}} + H) \times 4H + 8H + H \times P \right] Params=L×(1+B)×[(Din+H)×4H+8H+H×P]

📌 实际可用 summary 工具验证。


十、总结:一张表掌握全部

类别 关键点
核心参数 input_size, hidden_size, num_layers, batch_first, bidirectional, proj_size
新增参数 device, dtype:创建时直接指定设备和数据类型
输入形状 (L, N, D)(N, L, D)
输出 output (L, N, H_out × num_directions),含所有时间步
输出 h_n (D_layers, N, H_out),仅最终状态
输出 c_n (D_layers, N, hidden_size),不受投影影响
参数命名 weight_ih_l0, weight_ih_l0_reverse, weight_hr_l0(投影)
变长序列 必须用 pack_padded_sequence
初始化 建议初始化 forget gate bias 为 1
调试技巧 打印 named_parameters(),检查形状
设备管理 使用 devicedtype 参数直接创建

26、在PyTorch里输入到底有几个------1类 or 2类

我们现在严格聚焦于 PyTorch 中 torch.nn.LSTM 的前向调用(forward pass),明确回答:

在 PyTorch 中,调用一个 nn.LSTM 层时,需要传入几个输入张量?


一、结论(先说答案)

在 PyTorch 中,调用 nn.LSTM 时:

  • 必须传入 1 个输入张量(输入序列)
  • 可以额外传入 1 个可选的状态元组(包含两个张量:初始 hidden state 和初始 cell state)

因此:

  • 最少输入:1 个张量
  • 完整输入:1 个序列张量 + 1 个状态元组(含 2 个张量)→ 共 3 个张量参与调用

但注意:从函数参数角度看,LSTMforward 方法接受两个位置参数

python 复制代码
output, (h_n, c_n) = lstm(input_seq, initial_states)

其中 initial_states 是一个包含两个张量的元组

所以更准确地说:

PyTorch 的 nn.LSTM 前向调用接受 2 个参数:

  1. input:输入序列(1 个张量)
  2. hx(可选):初始状态(1 个元组,内含 2 个张量)

二、详细拆解每个输入

  1. 必需输入:input(输入序列)
  • 类型torch.Tensor

  • 形状 (假设 batch_first=True):

    python 复制代码
    (batch_size, seq_len, input_size)
  • 含义 :整个输入序列,包含所有时间步的 x 1 , x 2 , . . . , x T \mathbf{x}_1, \mathbf{x}_2, ..., \mathbf{x}_T x1,x2,...,xT

  • 是否可省略?❌ 不可省略,必须提供


  1. 可选输入:hx(初始状态)
  • 类型tuple of two tensors: (h_0, c_0)
  • 形状 (单层 LSTM):
    • h_0: 详情在《LSTM - API》下的《四、输入与输出:维度全解析》下的 "2.初始状态 hx(可选)"
    • c_0: 详情在《LSTM - API》下的《四、输入与输出:维度全解析》下的 "2.初始状态 hx(可选)"
  • 含义
    • h_0 对应理论中的 h 0 \mathbf{h}_0 h0
    • c_0 对应理论中的 c 0 \mathbf{c}_0 c0
  • 是否可省略?✅ 可以省略。若不提供,PyTorch 自动初始化为全零张量

🔔 注意:虽然 hx 是一个"参数",但它本身是一个包含两个张量的元组 。所以实际传入的是 两个状态张量打包在一起


三、标准代码示例

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

lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=1, batch_first=True)

# 1. 输入序列(必需)
x = torch.randn(32, 15, 10)  # (B, T, D_in)

# 2. 初始状态(可选)
h0 = torch.zeros(1, 32, 20)  # (num_layers, B, D_hid)
c0 = torch.zeros(1, 32, 20)  # (num_layers, B, D_hid)

# 方式一:提供初始状态(共传入 1 个张量 + 1 个元组)
output, (hn, cn) = lstm(x, (h0, c0))

# 方式二:不提供初始状态(只传 1 个张量)
output, (hn, cn) = lstm(x)  # h0, c0 自动设为 0

四、关键澄清:到底算"几个输入"?

视角 输入数量 说明
函数参数个数 2 个 inputhx(后者是可选元组)
实际张量个数(用户提供) 1 个(仅 input)或 3 个(input + h0 + c0 因为 hx = (h0, c0) 包含两个张量
内部计算所需 每个时间步仍只需 x t x_t xt + ( h t − 1 , c t − 1 ) (h_{t-1}, c_{t-1}) (ht−1,ct−1) 与理论一致

最准确的说法是
PyTorch 的 nn.LSTM 接受两个参数:一个输入序列张量,和一个可选的初始状态元组(包含 hidden 和 cell 两个张量)。


五、常见误区纠正

❌ 误区1:"LSTM 在 PyTorch 中有三个输入参数"

→ 错。forward 方法只有两个参数:inputhxhx 是一个元组,不是两个独立参数。

❌ 误区2:"必须传 h0 和 c0,所以总是三个张量"

→ 错。h0c0 是可选的;不传时自动初始化为零。

✅ 正确认知:

  • 接口设计上 :LSTM 的状态输入被封装为一个元组 (h0, c0),体现其"单一逻辑状态"的本质
  • 这与理论中"一个状态输入(含 h 和 c)"完全对应

六、与经典 RNN 的对比(PyTorch 层面)

模型 必需输入 可选状态输入 状态结构
nn.RNN input(1 张量) h0(1 张量) 单向量 h 0 \mathbf{h}_0 h0
nn.LSTM input(1 张量) (h0, c0)(1 元组 = 2 张量) 双向量 ( h 0 , c 0 ) (\mathbf{h}_0, \mathbf{c}_0) (h0,c0)

这再次印证:LSTM 的状态更复杂,但输入接口的逻辑结构(序列 + 状态)与 RNN 一致


七、总结(PyTorch LSTM 输入)

问题 答案
nn.LSTM 前向调用有几个参数? 2 个input(必需),hx(可选)
hx 是什么? 一个元组 (h0, c0),包含两个形状相同的张量
用户最少要提供几个张量? 1 个 (只有 input
用户最多要提供几个张量? 3 个input, h0, c0
这与 LSTM 理论一致吗? ✅ 完全一致:input 对应 x 1 : T \mathbf{x}_{1:T} x1:T,(h0, c0) 对应初始状态 ( h 0 , c 0 ) (\mathbf{h}_0, \mathbf{c}_0) (h0,c0)

27、Bi-LSTM

第一部分:理论基础

  1. 标准 LSTM 回顾

LSTM(Long Short-Term Memory)是一种特殊的循环神经网络(RNN),用于解决长序列中的梯度消失问题。其核心是通过门控机制控制信息流动:

对于时间步 t t t,给定输入 x t x_t xt 和前一时刻隐藏状态 h t − 1 h_{t-1} ht−1,LSTM 计算:

i t = σ ( W i [ h t − 1 , x t ] + b i ) (输入门) f t = σ ( W f [ h t − 1 , x t ] + b f ) (遗忘门) g t = tanh ⁡ ( W g [ h t − 1 , x t ] + b g ) (候选记忆) o t = σ ( W o [ h t − 1 , x t ] + b o ) (输出门) c t = f t ⊙ c t − 1 + i t ⊙ g t (细胞状态更新) h t = o t ⊙ tanh ⁡ ( c t ) (隐藏状态输出) \begin{aligned} i_t &= \sigma(W_i [h_{t-1}, x_t] + b_i) \quad &\text{(输入门)} \\\\ f_t &= \sigma(W_f [h_{t-1}, x_t] + b_f) \quad &\text{(遗忘门)} \\\\ g_t &= \tanh(W_g [h_{t-1}, x_t] + b_g) \quad &\text{(候选记忆)} \\\\ o_t &= \sigma(W_o [h_{t-1}, x_t] + b_o) \quad &\text{(输出门)} \\\\ c_t &= f_t \odot c_{t-1} + i_t \odot g_t \quad &\text{(细胞状态更新)} \\\\ h_t &= o_t \odot \tanh(c_t) \quad &\text{(隐藏状态输出)} \end{aligned} itftgtotctht=σ(Wi[ht−1,xt]+bi)=σ(Wf[ht−1,xt]+bf)=tanh(Wg[ht−1,xt]+bg)=σ(Wo[ht−1,xt]+bo)=ft⊙ct−1+it⊙gt=ot⊙tanh(ct)(输入门)(遗忘门)(候选记忆)(输出门)(细胞状态更新)(隐藏状态输出)

  • c t ∈ R H c_t \in \mathbb{R}^H ct∈RH:细胞状态(长期记忆)
  • h t ∈ R H h_t \in \mathbb{R}^H ht∈RH:隐藏状态(短期输出)
  • H = hidden_size H = \text{hidden\_size} H=hidden_size:维度

✅ 在标准 LSTM 中, h t h_t ht 和 c t c_t ct 维度相同,均为 H H H。


  1. 双向 LSTM 的动机

标准 LSTM 只能利用过去的信息 ( x 1 , . . . , x t − 1 x_1, ..., x_{t-1} x1,...,xt−1)来理解当前 token x t x_t xt。但在许多任务中(如命名实体识别、句法分析),未来上下文同样重要

例如:

  • "Apple released a new iPhone." → 要判断 "Apple" 是公司,需看后面的 "iPhone"
  • "I ate an apple." → 要判断 "apple" 是水果,需看前面的 "ate an"

解决方案:同时从两个方向建模序列!


  1. 双向 LSTM 的理论结构

双向 LSTM 包含 两个独立的 LSTM

  • 前向 LSTM(Forward)

    • 输入: x 1 , x 2 , . . . , x T x_1, x_2, ..., x_T x1,x2,...,xT
    • 输出: h → 1 , h → 2 , . . . , h → T \overrightarrow{h}_1, \overrightarrow{h}_2, ..., \overrightarrow{h}_T h 1,h 2,...,h T
    • h → t \overrightarrow{h}_t h t 编码了 x 1 x_1 x1 到 x t x_t xt 的信息
  • 后向 LSTM(Backward)

    • 输入: x T , x T − 1 , . . . , x 1 x_T, x_{T-1}, ..., x_1 xT,xT−1,...,x1
    • 输出: h ← T , h ← T − 1 , . . . , h ← 1 \overleftarrow{h}T, \overleftarrow{h}{T-1}, ..., \overleftarrow{h}_1 h T,h T−1,...,h 1
    • h ← t \overleftarrow{h}_t h t 编码了 x T x_T xT 到 x t x_t xt 的信息

🔁 注意:后向 LSTM 的输出 h ← t \overleftarrow{h}_t h t 对应的是原始位置 t t t 的反向上下文。

最终表示(每个时间步)

将两个方向的隐藏状态拼接,得到完整的上下文表示:

h t bi = [ h → t ; h ← t ] ∈ R 2 H h_t^{\text{bi}} = \left[ \overrightarrow{h}_t; \overleftarrow{h}_t \right] \in \mathbb{R}^{2H} htbi=[h t;h t]∈R2H

  • 这个 h t bi h_t^{\text{bi}} htbi 同时包含过去和未来信息
  • 维度为 2 H 2H 2H(假设无投影)

  1. 投影 LSTM(可选扩展)

在某些大型模型中(如语音识别),为了减少参数量,引入隐藏状态投影

  • 细胞状态 c t ∈ R H c_t \in \mathbb{R}^H ct∈RH(保持高维,保留记忆容量)
  • 隐藏状态 h t = W proj ⋅ tanh ⁡ ( c t ) ∈ R P h_t = W_{\text{proj}} \cdot \tanh(c_t) \in \mathbb{R}^P ht=Wproj⋅tanh(ct)∈RP,其中 P < H P < H P<H

此时:

  • 前向隐藏状态: h → t ∈ R P \overrightarrow{h}_t \in \mathbb{R}^P h t∈RP
  • 后向隐藏状态: h ← t ∈ R P \overleftarrow{h}_t \in \mathbb{R}^P h t∈RP
  • 拼接后维度: 2 P 2P 2P

细胞状态始终是 H H H 维,不受投影影响


第二部分:PyTorch 实现与代码细节

  1. 创建双向 LSTM
python 复制代码
import torch.nn as nn

lstm = nn.LSTM(
    input_size=D,          # 输入维度
    hidden_size=H,         # 细胞状态维度(核心)
    num_layers=L,          # 堆叠层数
    bidirectional=True,    # ← 关键:启用双向
    batch_first=False,     # 输入/输出格式
    proj_size=P            # 投影维度(0 表示不投影)
)
  • bidirectional=True,则自动创建两个 LSTM(前向 + 后向)
  • 权重矩阵数量翻倍(weight_ih_l0, weight_ih_l0_reverse, ...)

  1. 输入张量格式

设批次大小为 B B B,序列长度为 T T T,输入特征维度为 D D D:

batch_first 输入 x 形状 说明
False(默认) (T, B, D) 时间步在第 0 维
True (B, T, D) 批次在第 0 维

  1. 输出详解

调用:

python 复制代码
output, (h_n, c_n) = lstm(x)

(1) output:每个时间步的拼接表示

  • 含义 :最后一层中,每个时间步 t t t 的双向隐藏状态拼接结果
  • 理论对应 : h t bi = [ h → t ; h ← t ] h_t^{\text{bi}} = [\overrightarrow{h}_t; \overleftarrow{h}_t] htbi=[h t;h t] (这里面的 t 很有讲究,在"LSTM - API" 里的 "输出详解" 里已经提及)
  • 形状
    • batch_first=False(T, B, 2 × H_out)
    • batch_first=True(B, T, 2 × H_out)
  • 特点 :✅ 已经拼接好,可直接用于下游任务

其中 H out = { P if P > 0 H otherwise H_{\text{out}} = \begin{cases} P & \text{if } P > 0 \\ H & \text{otherwise} \end{cases} Hout={PHif P>0otherwise

(2) h_n

注意batch_first 仅影响输入 x 和输出 output 的维度排列,h_nc_n 的形状完全无影响

  • 含义:包含所有层、所有方向在完整处理序列后产生的最终隐藏状态。

    • forward 方向:处理完序列最后一个 token 后的状态;
    • backward 方向:处理完序列第一个 token 后的状态(从右向左运行)。
  • 形状(num_layers × num_directions, N, H_out)

    其中:

    • N 为 batch size
    • num_directions = 1bidirectional= False);num_directions = 2bidirectional= True
    • H_out = proj_size(若 proj_size > 0),否则 H_out = hidden_size
  • 存储顺序

    双向 LSTMbidirectional=True

    • 每层包含 forward + backward 两个方向

    • h_n 长度 = 2 × num_layers

    • 存储顺序(按层优先,每层内先 forward 后 backward):

      text 复制代码
      h_n[0]     → layer 0 的 forward 状态  
      h_n[1]     → layer 0 的 backward 状态  
      h_n[2]     → layer 1 的 forward 状态  
      h_n[3]     → layer 1 的 backward 状态  
      ...  
      h_n[2k]    → layer k 的 forward 状态  
      h_n[2k+1]  → layer k 的 backward 状态

    💡 通用规则

    • 如果是单向h_n[i] 就是第 i 层的 forward 状态;
    • 如果是双向,偶数索引为 forward,奇数索引为 backward,层数 = 索引 // 2。
  • 典型用途

    关键 :❌ 未拼接 !需手动拼接才能得到完整句子表示

    常用于 sentence-level 任务(如文本分类)。

    获取最后一层完整表示的方法:

    python 复制代码
    # 1
    if bidirectional:
        # 需要自己手动拼接
        h_final = torch.cat([h_n[-2], h_n[-1]], dim=-1)   # (N, 2 × H_out)
    else:
        h_final = h_n[-1]                                 # (N, H_out)
        
    
    # 2
    # h_n: (L*2, B, H_out)
    h_fwd = h_n[-2]   # 最后一层 forward
    h_bwd = h_n[-1]   # 最后一层 backward
    h_final = torch.cat([h_fwd, h_bwd], dim=-1)  # (B, 2 × H_out)

(3) c_n

细胞状态 c_n 的组织方式与 h_n 完全一致------唯一的区别是:

  • h_n 存的是隐藏状态 (维度 = H_out
  • c_n 存的是细胞状态 (维度 = hidden_size不受 proj_size 影响

✅ 核心结论(先说重点)

c_n 的形状、存储顺序、索引含义 与 h_n 完全相同,
唯一不同:c_n 的最后一维始终是 hidden_size(不是 H_out

📦 c_n 的通用属性

  • 含义 :所有层、所有方向在处理完整序列后的最终细胞状态
  • 形状(num_layers × num_directions, N, hidden_size)
    • 注意:最后一维是 hidden_size,不是 H_out
    • 即使你设置了 proj_size=4c_n 仍是 hidden_size 维(比如 8)
  • 不受 batch_first 影响 (和 h_n 一样)

双向 LSTMbidirectional=True

  • c_n 长度 = 2 × num_layers

  • 存储顺序(按层优先,每层内先 forward 后 backward):

    text 复制代码
    c_n[0]     → layer 0 的 forward 细胞状态  
    c_n[1]     → layer 0 的 backward 细胞状态  
    c_n[2]     → layer 1 的 forward 细胞状态  
    c_n[3]     → layer 1 的 backward 细胞状态  
    ...  
    c_n[2k]    → layer k 的 forward 细胞状态  
    c_n[2k+1]  → layer k 的 backward 细胞状态

💡 关键提醒

虽然 h_n[1](backward 隐藏状态)的维度可能是 proj_size

c_n[1](backward 细胞状态)的维度永远是 hidden_size


📌 使用建议

场景 h_n 还是 c_n
初始化解码器(如 seq2seq) 通常只传 h_n(有时也传 c_n
继续推理/生成 需同时传 (h_n, c_n) 作为初始状态
提取句向量 只用 h_nc_n 一般不用于表示学习)
调试 LSTM 内部 可观察 c_n 看记忆单元变化

⚠️ 注意:如果你要把 (h_n, c_n) 传给另一个 LSTM,必须确保两个 LSTM 的 hidden_sizeproj_size 配置一致,否则维度对不上!


✅ 总结表

属性 h_n c_n
形状 (L×D, N, H_out) (L×D, N, hidden_size)
存储顺序 完全相同 完全相同
proj_size 影响? ✅ 是(决定 H_out ❌ 否(始终 hidden_size
典型用途 表示学习、分类 初始化下一阶段 LSTM

其中 D = 1(单向)或 2(双向)


  1. 维度变化总结表
组件 理论符号 PyTorch 变量 维度(单层双向) 说明
输入 x t x_t xt x (T, B, D) 原始输入
前向隐藏 h → t \overrightarrow{h}_t h t output[t, :, :H_out] (B, H_out) 已包含在 output
后向隐藏 h ← t \overleftarrow{h}_t h t output[t, :, H_out:] (B, H_out) 已包含在 output
拼接输出 [ h → t ; h ← t ] [\overrightarrow{h}_t; \overleftarrow{h}_t] [h t;h t] output[t] (B, 2×H_out) ✅ 直接可用
最终前向状态 h → T \overrightarrow{h}_T h T h_n[0] (B, H_out) 需手动拼接
最终后向状态 h ← 1 \overleftarrow{h}_1 h 1 h_n[1] (B, H_out) 注意:对应位置 1
细胞状态 c T c_T cT c_n[0] (B, H) 始终为 hidden_size

💡 注意:后向 LSTM 的最终状态 h_n[1] 对应的是原始序列位置 1(因为它是从右往左处理的)。


  1. 完整代码验证
python 复制代码
import torch
import torch.nn as nn

# 参数
T, B, D = 4, 2, 5
H = 3
P = 0  # 不投影

# 创建双向 LSTM
lstm = nn.LSTM(D, H, bidirectional=True, batch_first=False)

# 输入
x = torch.randn(T, B, D)

# 前向传播
output, (h_n, c_n) = lstm(x)

print("=== 理论 vs 代码 ===")
print(f"output shape: {output.shape}")      # (4, 2, 6) ← 2×H = 6
print(f"h_n shape:    {h_n.shape}")        # (2, 2, 3) ← 1层×2方向
print(f"c_n shape:    {c_n.shape}")        # (2, 2, 3) ← 始终 H=3

# 验证:output 最后一个时间步 = 拼接 h_n
last_output = output[-1]                   # (2, 6)
manual_cat = torch.cat([h_n[0], h_n[1]], dim=-1)  # (2, 6)
print(f"Match? {torch.allclose(last_output, manual_cat)}")  # True

第三部分:使用建议

✅ Token-level 任务(如 NER、POS 标注)

  • 直接使用 output
  • 每个时间步已有完整上下文表示

✅ Sentence-level 任务(如分类、编码器)

  • 推荐使用 output[-1](已拼接)
  • 或手动拼接 h_n[-2]h_n[-1]

⚠️ 注意事项

  1. 不要直接 squeeze h_n → 会丢失方向信息
  2. 变长序列必须 pack → 否则 padding 干扰后向 LSTM
  3. 投影 LSTM 中 c_nh_n 维度 → 这是正常设计

总结

层面 关键点
理论 双向 LSTM = 前向 LSTM + 后向 LSTM,拼接隐藏状态得到完整上下文表示
代码 output 自动拼接(可直接用),h_n 分别存储(需手动拼接)
维度 output: 2×H_outh_n: H_outc_n: H(始终)

现在你既理解了双向 LSTM 的数学原理 ,也掌握了 PyTorch 中的实现细节,可以自信地在项目中正确使用它了!

28、多层 Bi-LSTM

多层双向 LSTM(Multi-layer Bi-LSTM)的层间维度传递机制是理解其整体结构的核心,从理论和代码两个层面彻底讲清楚。


✅ 核心结论(先说答案)

从第 1 层开始,每一层的输入维度 = 上一层的输出维度 = 2 × H_out

因为上一层的 output 是双向拼接的结果,所以所有后续层的输入都是 2 × H_out


🧠 第一部分:理论分析

  1. 第 0 层(底层)
  • 输入 :原始输入 x t ∈ R D x_t \in \mathbb{R}^D xt∈RD
  • 前向 LSTM :处理 x t x_t xt,输出 h → t ( 0 ) ∈ R H out \overrightarrow{h}t^{(0)} \in \mathbb{R}^{H{\text{out}}} h t(0)∈RHout
  • 后向 LSTM :处理 x t x_t xt,输出 h ← t ( 0 ) ∈ R H out \overleftarrow{h}t^{(0)} \in \mathbb{R}^{H{\text{out}}} h t(0)∈RHout
  • 拼接输出 : h t ( 0 ) = [ h → t ( 0 ) ; h ← t ( 0 ) ] ∈ R 2 H out h_t^{(0)} = [\overrightarrow{h}_t^{(0)}; \overleftarrow{h}t^{(0)}] \in \mathbb{R}^{2H{\text{out}}} ht(0)=[h t(0);h t(0)]∈R2Hout
  1. 第 1 层(中间层)
  • 输入 :第 0 层的拼接输出 h t ( 0 ) ∈ R 2 H out h_t^{(0)} \in \mathbb{R}^{2H_{\text{out}}} ht(0)∈R2Hout
  • 前向 LSTM :处理 h t ( 0 ) h_t^{(0)} ht(0),输出 h → t ( 1 ) ∈ R H out \overrightarrow{h}t^{(1)} \in \mathbb{R}^{H{\text{out}}} h t(1)∈RHout
  • 后向 LSTM :处理 h t ( 0 ) h_t^{(0)} ht(0),输出 h ← t ( 1 ) ∈ R H out \overleftarrow{h}t^{(1)} \in \mathbb{R}^{H{\text{out}}} h t(1)∈RHout
  • 拼接输出 : h t ( 1 ) = [ h → t ( 1 ) ; h ← t ( 1 ) ] ∈ R 2 H out h_t^{(1)} = [\overrightarrow{h}_t^{(1)}; \overleftarrow{h}t^{(1)}] \in \mathbb{R}^{2H{\text{out}}} ht(1)=[h t(1);h t(1)]∈R2Hout
  1. 第 k 层(k ≥ 1)
  • 输入维度2 × H_out
  • 输出维度2 × H_out

🔑 关键洞察 :除了第 0 层接收原始输入(维度 D),所有上层都接收 2 × H_out 维的输入


⚙️ 第二部分:PyTorch 实现细节

  1. 权重矩阵的维度变化

让我们看看各层权重矩阵的形状:

python 复制代码
import torch.nn as nn

# 配置
D = 10      # input_size
H = 8       # hidden_size  
P = 0       # proj_size = 0 → H_out = H = 8
L = 3       # num_layers = 3

lstm = nn.LSTM(D, H, num_layers=L, bidirectional=True)

# 查看各层权重
for name, param in lstm.named_parameters():
    if 'weight_ih' in name:  # input-to-hidden weights
        print(f"{name}: {param.shape}")

输出:

python 复制代码
weight_ih_l0: torch.Size([32, 10])     # 4×H × D = 32 × 10
weight_ih_l0_reverse: torch.Size([32, 10])

weight_ih_l1: torch.Size([32, 16])     # 4×H × (2×H) = 32 × 16  
weight_ih_l1_reverse: torch.Size([32, 16])

weight_ih_l2: torch.Size([32, 16])     # 4×H × (2×H) = 32 × 16
weight_ih_l2_reverse: torch.Size([32, 16])
  1. 维度分析
权重矩阵 形状 说明
Layer 0 weight_ih_l0 (4H, D) 输入是原始特征,维度 D
Layer 1+ weight_ih_l1, weight_ih_l2, ... (4H, 2H) 输入是上层的双向输出,维度 2H

💡 注意:weight_hh(hidden-to-hidden)始终是 (4H, H),因为每个方向内部的隐藏状态仍是 H 维。


📊 第三部分:完整数据流示例

假设配置:input_size=10, hidden_size=8, num_layers=3, bidirectional=True

数据流维度变化:

输入维度 前向输出 后向输出 拼接输出(传给下一层)
Input 10 - - -
Layer 0 10 8 8 16
Layer 1 16 8 8 16
Layer 2 16 8 8 16
Final Output - - - 16

对应 PyTorch 输出:

python 复制代码
x = torch.randn(5, 3, 10)  # (T=5, B=3, D=10)
output, (h_n, c_n) = lstm(x)

print(f"output shape: {output.shape}")  # (5, 3, 16) ← 最终输出维度 = 2×H = 16
print(f"h_n shape: {h_n.shape}")       # (6, 3, 8) ← L×2 = 3×2 = 6, 每个方向 H=8

🔍 第四部分:为什么这样设计?

  1. 信息完整性
  • 第 0 层的双向输出包含了完整的上下文信息(2H 维)
  • 上层需要基于这个完整信息进行更高级的抽象
  1. 参数效率
  • 如果上层只接收单向信息,会丢失一半的上下文
  • 双向拼接确保每层都能看到完整的序列表示
  1. 与标准 RNN 一致
  • 多层 RNN 中,每层的输入都是上层的输出
  • Bi-LSTM 遵循同样的原则,只是输出维度翻倍

🧪 第五部分:投影 LSTM 的特殊情况

如果启用了投影(proj_size=P < H):

输入维度 前向输出 后向输出 拼接输出
Layer 0 D P P 2P
Layer 1+ 2P P P 2P
python 复制代码
lstm_proj = nn.LSTM(10, 8, proj_size=4, num_layers=2, bidirectional=True)
x = torch.randn(5, 3, 10)
output, _ = lstm_proj(x)
print(output.shape)  # (5, 3, 8) ← 2×P = 2×4 = 8

✅ 投影不影响层间传递逻辑,只是把 H 换成了 P


📌 第六部分:实际使用注意事项

  1. 不要手动修改中间层输入

PyTorch 自动处理层间连接,你只需要关注:

  • 第 0 层输入:原始数据(维度 D)
  • 最终输出:2 × H_out
  1. 参数量计算
  • Layer 0 参数量:2 × [4H × (D + H)](双向)

​ 【注意:(D+H)是理论上的列数,即拼接形式,实现是拆开形式,详情在《LSTM-API》下的 "参数详解" 里的 "hidden_size" 】

  • Layer 1+ 参数量:2 × [4H × (2H + H)] = 2 × [4H × 3H](双向)
  1. 调试技巧

如果想验证某层的输入维度,可以逐层提取:

python 复制代码
# 手动逐层运行(用于调试)
lstm_layer0 = nn.LSTM(10, 8, bidirectional=True)
lstm_layer1 = nn.LSTM(16, 8, bidirectional=True)  # 输入维度=16

out0, _ = lstm_layer0(x)
out1, _ = lstm_layer1(out0)

✅ 总结

问题 答案
第 0 层输入维度 input_size(原始特征维度)
第 1+ 层输入维度 2 × H_out(上层的双向拼接输出)
所有层输出维度 2 × H_out(最终 output 的最后一维)
权重矩阵变化 Layer 0: (4H, D),Layer 1+: (4H, 2H_out)

核心思想 :多层 Bi-LSTM 中,只有最底层看到原始输入,所有上层都基于完整的双向上下文进行进一步抽象。这种设计确保了深层网络能够利用最丰富的序列信息。

29、代码:

  1. 单层、单向
python 复制代码
import torch
import torch.nn as nn

x = torch.randn(2, 3, 10)    # (batch_size=2, seq_len=5, input_size=10)

lstm = nn.LSTM(input_size=10,
               hidden_size=8,    # 隐藏状态=8
               num_layers=1,
               bidirectional=False,
               batch_first=True
               )

# output, (h_n, c_n) = lstm(x, (h_0, c_0))
output, (h_n, c_n) = lstm(x)

# output: (N, L, H_out × num_directions)
print(output.shape)     # [2, 3, 8]
print(output)
# tensor([[[ 0.0744, -0.1063,  0.0979,  0.0402, -0.1841,  0.0397,  0.0915,
#           -0.1419],
#          [ 0.0626,  0.0357,  0.2427, -0.1108, -0.0271, -0.0748, -0.0850,
#           -0.0025],
#          [-0.0426, -0.0188,  0.2564,  0.0080, -0.1285,  0.0029,  0.0029,
#           -0.0975]],
# 
#         [[ 0.1587, -0.1029, -0.0059, -0.0356,  0.0604, -0.1189,  0.0676,
#           -0.1352],
#          [ 0.2082, -0.2960, -0.0799,  0.1532,  0.0179, -0.1355, -0.0254,
#            0.0186],
#          [ 0.0678, -0.1982, -0.0302, -0.0109, -0.0524, -0.0661,  0.1695,
#           -0.0839]]], grad_fn=<TransposeBackward0>)
print('-' * 20)

# h_n: (num_layers × num_directions, N, H_out)
print(h_n.shape)        # [1, 2, 8]
print(h_n)
# tensor([[[-0.0426, -0.0188,  0.2564,  0.0080, -0.1285,  0.0029,  0.0029,
#           -0.0975],
#          [ 0.0678, -0.1982, -0.0302, -0.0109, -0.0524, -0.0661,  0.1695,
#           -0.0839]]], grad_fn=<StackBackward0>)
print('-' * 20)

# 注意, 有两个样本: 对于 output, output[0, -1] 才是第0个样本的最后的输出
#                对于 h_n, h_n[0, 0] 才是第0个样本的最后隐藏状态
# 对于 output, output[1, -1] 才是第1个样本的最后的输出
# 对于 h_n, h_n[0, 1] 才是第1个样本的最后隐藏状态
# output里第0个样本的最后输出 与 h_n里第0个样本的第0个值 是相同的
print(torch.allclose(output[0, -1], h_n[0, 0]))     # True
print(torch.allclose(output[1, -1], h_n[0, 1]))     # True

# c_n: (num_layers × num_directions, N, H_out)
print(c_n.shape)    # [1, 2, 8]
print(c_n)
# tensor([[[-0.0716, -0.0292,  0.4764,  0.0119, -0.1983,  0.0094,  0.0051,
#           -0.2437],
#          [ 0.1345, -0.3100, -0.0613, -0.0170, -0.1743, -0.2287,  0.3117,
#           -0.1621]]], grad_fn=<StackBackward0>)
  1. 单层、双向(注意 output 拼接的向前和后向,在 "LSTM - API" 里 "输出详解" 里详细提过)
python 复制代码
import torch
import torch.nn as nn

torch.manual_seed(66)

x = torch.randn(2, 3, 10)    # (batch_size=2, seq_len=5, input_size=10)

lstm = nn.LSTM(input_size=10,
               hidden_size=8,    # 隐藏状态=8
               num_layers=1,
               bidirectional=True,
               batch_first=True
               )

# output, (h_n, c_n) = lstm(x, (h_0, c_0))
output, (h_n, c_n) = lstm(x)

# output: (N, L, H_out × num_directions)
print(output.shape)     # [2, 3, 16]
print(output)
# tensor([[[-0.0399, -0.0228,  0.2337,  0.0284, -0.0947, -0.2026,  0.1378,
#            0.0769,  0.0634,  0.0184,  0.2126, -0.0110,  0.2545,  0.0434,
#           -0.2116, -0.1422],
#          [ 0.0416, -0.0197,  0.0341,  0.1351, -0.0869, -0.1309, -0.1873,
#           -0.0274, -0.0874, -0.0536,  0.0097, -0.0543,  0.1749, -0.1169,
#           -0.2147, -0.1130],
#          [ 0.2526, -0.1042,  0.3451,  0.0258, -0.2472,  0.2673,  0.0786,
#            0.0118,  0.0360, -0.2344,  0.0707,  0.0845,  0.0491,  0.0521,
#            0.0185, -0.0972]],
#
#         [[-0.1091,  0.0746,  0.1262, -0.1478,  0.1477,  0.0418,  0.1266,
#            0.1000, -0.3266,  0.2003,  0.1831, -0.1865,  0.3465,  0.1068,
#           -0.1330, -0.1628],
#          [-0.1793, -0.1134,  0.3052, -0.0680, -0.0959,  0.0908,  0.2914,
#            0.1235, -0.1224,  0.0249,  0.4632, -0.0732,  0.2676,  0.0935,
#           -0.1039, -0.1263],
#          [ 0.1279, -0.2226, -0.0365,  0.0541, -0.0917,  0.1208, -0.1006,
#            0.0891, -0.1520,  0.2312,  0.1175,  0.0063,  0.1033, -0.0947,
#           -0.0377, -0.0989]]], grad_fn=<TransposeBackward0>)
print('-' * 20)

# h_n: (num_layers × num_directions, N, H_out)
print(h_n.shape)        # [2, 2, 8]
print(h_n)
# tensor([[[ 0.2526, -0.1042,  0.3451,  0.0258, -0.2472,  0.2673,  0.0786, 0.0118],
#          [ 0.1279, -0.2226, -0.0365,  0.0541, -0.0917,  0.1208, -0.1006, 0.0891]],
#
#         [[ 0.0634,  0.0184,  0.2126, -0.0110,  0.2545,  0.0434, -0.2116, -0.1422],
#          [-0.3266,  0.2003,  0.1831, -0.1865,  0.3465,  0.1068, -0.1330, -0.1628]]]
#         , grad_fn=<StackBackward0>)
print('-' * 20)

# output 是自动拼接好了的, 但注意拼接的内容, 是根据原始输入位置拼接的,而不是处理顺序
# h_n 不会自动拼接

print(output[0, 0, : 8])   # 第0个样本,原始输入位置为0的后向LSTM
# [-0.0399, -0.0228,  0.2337,  0.0284, -0.0947, -0.2026,  0.1378,  0.0769]

print(output[0, 0, 8: ])     # 第0个样本,原始输入位置为0的后向LSTM
# [ 0.0634,  0.0184,  0.2126, -0.0110,  0.2545,  0.0434, -0.2116, -0.1422]

print(h_n[0])       # 第0层的所有样本的前向LSTM
# tensor([[ 0.2526, -0.1042,  0.3451,  0.0258, -0.2472,  0.2673,  0.0786,  0.0118],
#         [ 0.1279, -0.2226, -0.0365,  0.0541, -0.0917,  0.1208, -0.1006,  0.0891]],
#        grad_fn=<SelectBackward0>)

print({h_n[1]})     # 第0层的所有样本的后向LSTM
# {tensor([[ 0.0634,  0.0184,  0.2126, -0.0110,  0.2545,  0.0434, -0.2116, -0.1422],
#         [-0.3266,  0.2003,  0.1831, -0.1865,  0.3465,  0.1068, -0.1330, -0.1628]],
#        grad_fn=<SelectBackward0>)}

print(h_n[0, 0])    # 第0层, 第0个样本的最终前向隐藏状态
# [ 0.2526, -0.1042,  0.3451,  0.0258, -0.2472,  0.2673,  0.0786,  0.0118]

print(h_n[0, 1])    # 第0层, 第1个样本的最终前向隐藏状态
# [ 0.1279, -0.2226, -0.0365,  0.0541, -0.0917,  0.1208, -0.1006,  0.0891]

print(h_n[1, 0])    # 第0层, 第0个样本的最终后向隐藏状态
# [ 0.0634,  0.0184,  0.2126, -0.0110,  0.2545,  0.0434, -0.2116, -0.1422]

print(h_n[1, 1])    # 第0层, 第1样本的最终后向隐藏状态
# [-0.3266,  0.2003,  0.1831, -0.1865,  0.3465,  0.1068, -0.1330, -0.1628]

print(torch.allclose(output[0, -1, :8], h_n[0, 0]))     # True
print(torch.allclose(output[1, -1, :8], h_n[0, 1]))     # True
print(torch.allclose(output[0, 0, 8: ], h_n[1, 0]))     # True
print(torch.allclose(output[1, 0, 8: ], h_n[1, 1]))     # True

# 这样拼接得到的是: 第0层, 最终的前向LSTM 和 最终的后向LSTM
cat_hn = torch.cat([h_n[0, 0], h_n[1, 0]], dim=-1)
print(torch.allclose(output[0, -1], cat_hn))    # False

# c_n: (num_layers × num_directions, N, H_out)
print(c_n.shape)    # [2, 2, 8]
print(c_n)
# tensor([[[ 0.3087, -0.1275,  0.5394,  0.1689, -0.5057,  0.4794,  0.1285,
#            0.0756],
#          [ 0.1728, -0.3190, -0.0685,  0.2140, -0.2644,  0.2630, -0.1196,
#            0.5861]],
# 
#         [[ 0.0911,  0.0285,  0.3964, -0.0349,  0.4289,  0.1753, -0.6480,
#           -0.5754],
#          [-0.8439,  0.3440,  0.4013, -0.4000,  0.5617,  0.4017, -0.3236,
#           -0.4820]]], grad_fn=<StackBackward0>)
相关推荐
deep_drink12 小时前
【论文精读(三)】PointMLP:大道至简,无需卷积与注意力的纯MLP点云网络 (ICLR 2022)
人工智能·pytorch·python·深度学习·3d·point cloud
lanbo_ai16 小时前
基于yolov10的火焰、火灾检测系统,支持图像、视频和摄像实时检测【pytorch框架、python源码】
pytorch·python·yolo
Dev7z18 小时前
基于LSTM的共享单车需求预测研究
人工智能·rnn·lstm
盼小辉丶1 天前
PyTorch实战(29)——使用TorchServe部署PyTorch模型
人工智能·pytorch·深度学习·模型部署
IRevers2 天前
【YOLO】YOLO-Master 腾讯轻量级YOLO架构超越YOLO-13(含检测和分割推理)
图像处理·人工智能·pytorch·python·yolo·transformer·边缘计算
小锋java12342 天前
【技术专题】PyTorch2 深度学习 - 张量(Tensor)的定义与操作
pytorch·深度学习
归一码字2 天前
DDPG手写讲解
人工智能·pytorch
七夜zippoe3 天前
图神经网络实战:从社交网络到推荐系统的工业级应用
网络·人工智能·pytorch·python·神经网络·cora
本是少年3 天前
构建 HuggingFace 图像-文本数据集指南
pytorch·transformer