文章目录
- [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_size: int`](#1.
- 四、输入与输出:维度全解析
-
- [1. **输入 `input`**](#1. 输入
input) - [2. **初始状态 `hx`(可选)**](#2. 初始状态
hx(可选)) - [3. 输出详解](#3. 输出详解)
- [1. **输入 `input`**](#1. 输入
- 五、模型对象的内部结构与常用属性
-
- [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.LSTMvsnn.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)
- [❌ 错误 1:忽略 `batch_first` 导致维度错乱](#❌ 错误 1:忽略
- 九、参数量计算公式(供参考)
- 十、总结:一张表掌握全部
- [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_size或2*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 将权重矩阵拆分成了两个独立的矩阵:
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
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
- 最终计算
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 要这样设计?
- 内存效率
- 如果拼接
[h; x],需要额外的内存分配- 分开计算避免了这种开销
- 计算效率
- 矩阵乘法可以并行执行
- 更好地利用 BLAS 库优化
- 灵活性
- 可以单独初始化
weight_ih和weight_hh- 便于实现某些变体(如只更新部分权重)
- 与 RNNCell 一致
nn.LSTMCell也采用同样的设计- 保持 API 一致性
✅ 总结
- 理论上权重矩阵应该是
(4H, H+D)- PyTorch 的实现 :将其拆分为两个矩阵
(4H, D)和(4H, H)- 数学等价性:两种方式在数学上是完全等价的
- 工程优势: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=256→weight_ih ∈ ℝ^(1024 × 100)(2)
weight_hh(hidden-to-hidden weights)
- 用于将前一时刻隐藏状态
h_{t-1}映射到门控信号- 形状:
weight_hh ∈ ℝ^(4H × hidden_size)- 例如:
hidden_size=256→weight_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. 实际例子
pythonlstm = 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. 总结符号含义
符号 含义 Hhidden_size(隐藏状态维度)4H4 × 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=1,dropout参数被忽略; - 每层有独立参数集,不共享权重。
- 若
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_n和c_n的形状完全没有影响。- 建议 :
- NLP 任务强烈推荐
batch_first=True,与DataLoader和Tokenizer输出对齐; - 若使用
pack_padded_sequence,也需保持一致。
- NLP 任务强烈推荐
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 子模块 :
forward和backward - 两者参数不共享
- 创建 两个独立的 LSTM 子模块 :
- 输出融合 :默认拼接
[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)调用,减少内存拷贝。 -
使用示例:
pythonlstm = nn.LSTM(100, 256, device='cuda', dtype=torch.float16)
四、输入与输出:维度全解析
✅ 调用方式
python
output, (h_n, c_n) = lstm(input, hx=None)
1. 输入 input
- 类型 :
Tensor或PackedSequence - 形状(Tensor 模式) :
batch_first=False→(L, N, D)batch_first=True→(N, L, D)
- 其中 :
L= sequence lengthN= batch sizeD=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](原因见后)
- ✅ Token-level 任务 (如 NER、POS 标注):直接使用
🔹 形状
batch_first=False→(L, N, H_out × num_directions)batch_first=True→(N, L, H_out × num_directions)
其中:
num_directions = 2 if bidirectional else 1H_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_n和c_n的形状完全无影响。
-
含义:包含所有层、所有方向在完整处理序列后产生的最终隐藏状态。
- forward 方向:处理完序列最后一个 token 后的状态;
- backward 方向:处理完序列第一个 token 后的状态(从右向左运行)。
-
形状 :
(num_layers × num_directions, N, H_out)其中:
N为 batch sizenum_directions = 1(bidirectional= False);num_directions = 2(bidirectional= True)H_out = proj_size(若proj_size > 0),否则H_out = hidden_size
-
存储顺序(分情况说明):
✅ 情况 1:单向 LSTM (
bidirectional=False)-
每层只有 forward 方向
-
h_n长度 =num_layers -
存储顺序:
texth_n[0] → layer 0 的 forward 状态 h_n[1] → layer 1 的 forward 状态 ... h_n[L−1] → layer L−1 的 forward 状态
✅ 情况 2:双向 LSTM (
bidirectional=True)-
每层包含 forward + backward 两个方向
-
h_n长度 =2 × num_layers -
存储顺序(按层优先,每层内先 forward 后 backward):
texth_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 任务(如文本分类)。
获取最后一层完整表示的方法:
pythonif 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=4,c_n仍是hidden_size维(比如 8)
- 注意:最后一维是
- 不受
batch_first影响 (和h_n一样)
🔍 分情况详解
✅ 情况 1:单向 LSTM (bidirectional=False)
-
c_n长度 =num_layers -
存储顺序:
textc_n[0] → layer 0 的 forward 细胞状态 c_n[1] → layer 1 的 forward 细胞状态 ... c_n[L−1] → layer L−1 的 forward 细胞状态
✅ 情况 2:双向 LSTM (bidirectional=True)
-
c_n长度 =2 × num_layers -
存储顺序(按层优先,每层内先 forward 后 backward):
textc_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_n(c_n 一般不用于表示学习) |
| 调试 LSTM 内部 | 可观察 c_n 看记忆单元变化 |
⚠️ 注意:如果你要把
(h_n, c_n)传给另一个 LSTM,必须确保两个 LSTM 的hidden_size和proj_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(),检查形状 |
| 设备管理 | 使用 device 和 dtype 参数直接创建 |
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 个张量参与调用
但注意:从函数参数角度看,LSTM 的 forward 方法接受两个位置参数:
python
output, (h_n, c_n) = lstm(input_seq, initial_states)
其中 initial_states 是一个包含两个张量的元组。
所以更准确地说:
✅ PyTorch 的
nn.LSTM前向调用接受 2 个参数:
input:输入序列(1 个张量)hx(可选):初始状态(1 个元组,内含 2 个张量)
二、详细拆解每个输入
- 必需输入:
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
-
是否可省略?❌ 不可省略,必须提供
- 可选输入:
hx(初始状态)
- 类型 :
tupleof 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 h0c_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 个 | input 和 hx(后者是可选元组) |
| 实际张量个数(用户提供) | 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 方法只有两个参数:input 和 hx。hx 是一个元组,不是两个独立参数。
❌ 误区2:"必须传 h0 和 c0,所以总是三个张量"
→ 错。h0 和 c0 是可选的;不传时自动初始化为零。
✅ 正确认知:
- 接口设计上 :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
第一部分:理论基础
- 标准 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。
- 双向 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"
解决方案:同时从两个方向建模序列!
- 双向 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(假设无投影)
- 投影 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 实现与代码细节
- 创建双向 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, ...)
- 输入张量格式
设批次大小为 B B B,序列长度为 T T T,输入特征维度为 D D D:
batch_first |
输入 x 形状 |
说明 |
|---|---|---|
False(默认) |
(T, B, D) |
时间步在第 0 维 |
True |
(B, T, D) |
批次在第 0 维 |
- 输出详解
调用:
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_n和c_n的形状完全无影响。
-
含义:包含所有层、所有方向在完整处理序列后产生的最终隐藏状态。
- forward 方向:处理完序列最后一个 token 后的状态;
- backward 方向:处理完序列第一个 token 后的状态(从右向左运行)。
-
形状 :
(num_layers × num_directions, N, H_out)其中:
N为 batch sizenum_directions = 1(bidirectional= False);num_directions = 2(bidirectional= True)H_out = proj_size(若proj_size > 0),否则H_out = hidden_size
-
存储顺序:
双向 LSTM (
bidirectional=True)-
每层包含 forward + backward 两个方向
-
h_n长度 =2 × num_layers -
存储顺序(按层优先,每层内先 forward 后 backward):
texth_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=4,c_n仍是hidden_size维(比如 8)
- 注意:最后一维是
- 不受
batch_first影响 (和h_n一样)
双向 LSTM (bidirectional=True)
-
c_n长度 =2 × num_layers -
存储顺序(按层优先,每层内先 forward 后 backward):
textc_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_n(c_n 一般不用于表示学习) |
| 调试 LSTM 内部 | 可观察 c_n 看记忆单元变化 |
⚠️ 注意:如果你要把
(h_n, c_n)传给另一个 LSTM,必须确保两个 LSTM 的hidden_size和proj_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(双向)
- 维度变化总结表
| 组件 | 理论符号 | 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(因为它是从右往左处理的)。
- 完整代码验证
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]
⚠️ 注意事项
- 不要直接 squeeze
h_n→ 会丢失方向信息 - 变长序列必须 pack → 否则 padding 干扰后向 LSTM
- 投影 LSTM 中
c_n≠h_n维度 → 这是正常设计
总结
| 层面 | 关键点 |
|---|---|
| 理论 | 双向 LSTM = 前向 LSTM + 后向 LSTM,拼接隐藏状态得到完整上下文表示 |
| 代码 | output 自动拼接(可直接用),h_n 分别存储(需手动拼接) |
| 维度 | output: 2×H_out,h_n: H_out,c_n: H(始终) |
现在你既理解了双向 LSTM 的数学原理 ,也掌握了 PyTorch 中的实现细节,可以自信地在项目中正确使用它了!
28、多层 Bi-LSTM
多层双向 LSTM(Multi-layer Bi-LSTM)的层间维度传递机制是理解其整体结构的核心,从理论和代码两个层面彻底讲清楚。
✅ 核心结论(先说答案)
从第 1 层开始,每一层的输入维度 = 上一层的输出维度 =
2 × H_out因为上一层的
output是双向拼接的结果,所以所有后续层的输入都是2 × H_out维
🧠 第一部分:理论分析
- 第 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 层(中间层)
- 输入 :第 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
- 第 k 层(k ≥ 1)
- 输入维度 :
2 × H_out - 输出维度 :
2 × H_out
🔑 关键洞察 :除了第 0 层接收原始输入(维度 D),所有上层都接收
2 × H_out维的输入
⚙️ 第二部分:PyTorch 实现细节
- 权重矩阵的维度变化
让我们看看各层权重矩阵的形状:
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])
- 维度分析
| 层 | 权重矩阵 | 形状 | 说明 |
|---|---|---|---|
| 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
🔍 第四部分:为什么这样设计?
- 信息完整性
- 第 0 层的双向输出包含了完整的上下文信息(
2H维) - 上层需要基于这个完整信息进行更高级的抽象
- 参数效率
- 如果上层只接收单向信息,会丢失一半的上下文
- 双向拼接确保每层都能看到完整的序列表示
- 与标准 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
📌 第六部分:实际使用注意事项
- 不要手动修改中间层输入
PyTorch 自动处理层间连接,你只需要关注:
- 第 0 层输入:原始数据(维度 D)
- 最终输出:
2 × H_out维
- 参数量计算
- Layer 0 参数量:
2 × [4H × (D + H)](双向)
【注意:(D+H)是理论上的列数,即拼接形式,实现是拆开形式,详情在《LSTM-API》下的 "参数详解" 里的 "hidden_size" 】
- Layer 1+ 参数量:
2 × [4H × (2H + H)] = 2 × [4H × 3H](双向)
- 调试技巧
如果想验证某层的输入维度,可以逐层提取:
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、代码:
- 单层、单向
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>)
- 单层、双向(注意 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>)