nn.RNN/GRU/LSTM 三个类共享同一套张量约定与隐状态管理逻辑。本篇只讲 API 面——shape 约定、返回值语义、隐状态生命周期与变长序列处理;门控原理与三者的架构对比在 常用的神经网络类型总结,不在此重复。

构造参数与 batch_first 陷阱

RNN 族在 torch.nn 里的三个类构造签名几乎一致,input_size(每个时步的特征维)、hidden_size、num_layers(堆叠层数)、batch_first、dropout、bidirectional 六个参数覆盖日常全部用法,学会一个等于学会三个。

最大的坑是 batch_first 默认 False:输入 shape 约定为 (seq_len, batch, input_size),时间维在前。而 Dataset 与 DataLoader collate 出的 batch 是 (batch, seq_len, …) 布局,不设 batch_first=True 直接喂入,维度语义整体错位——张量往往仍能广播跑通,结果全废且无报错。工程惯例是显式开启:

import torch
import torch.nn as nn
 
lstm = nn.LSTM(input_size=256, hidden_size=512, num_layers=2,
               batch_first=True, dropout=0.2)
 
x = torch.randn(32, 50, 256)          # (batch, seq_len, input_size)
output, (h_n, c_n) = lstm(x)          # 不传第二参,隐状态默认全零初始化

dropout 只作用于层与层之间,最后一层之后不加,且仅在 train() 模式生效——语义同 nn.Dropout,eval() 自动关闭;num_layers=1 时写了也无效果。

output 与 h_n、c_n 的分工

形状走通只是第一步,返回值的语义才是误读重灾区。output 是最后一层每个时步的隐状态,h_n 是每一层最后时步的隐状态,c_n 是对应的 cell state——LSTM 特有,nn.RNN/nn.GRU 返回的是 (output, h_n),没有元组。

张量shape(batch_first=True,单向)语义
output(B, L, H)最后一层全部时步
h_n(layers, B, H)每层最后时步
c_n(layers, B, H)每层最后时步的 cell state

双向时 output 在特征维拼接两方向(H*2),h_n/c_n 在层维拼接(layers*2)。典型误用是把 c_n 当输出接分类头——它是内部记忆,对外该用 output 或 h_n;序列分类取 h_n 末层(双向则拼接两方向),序列标注逐时步取 output。

隐状态生命周期:初始化与 detach

h_n/c_n 记录的是”最后时步”的状态,它们同时是跨 batch 续传记忆的入口。不显式传入时 PyTorch 用全零初始化——对相互独立的样本(每条评论、每个句子)这是正确默认;需要手动传入的场景是语料连续时:流式文本训练、语言模型分段读长序列,上一段末状态应作下一段初始状态。

此时必须遵守一条纪律:跨 batch 传 state 要 .detach()。隐状态携带计算图,不截断则反向传播贯穿此前所有 batch,计算图随训练无限增长,显存逐步吃满:

h = None
for x, _ in loader:                    # 各 batch 是同一条流的不同段落
    out, h = lstm(x, h)
    h = (h[0].detach(), h[1].detach()) # 截断 BPTT:只传值,不传图

detach 放在前向之后,梯度仍能流过当前段内部——这正是 truncated BPTT 的意图。

变长序列:PackedSequence

前三节的约定都假设 batch 内等长,而真实 pipeline 里等长是 collate 时 padding 出来的假象。padding 位一并喂入的话,隐状态被大量零向量稀释,h_n 取到的”最后时步”甚至可能落在 padding 上。解法是 PackedSequence:pack 时告知每条样本的真实长度,内部按时步分组计算,padding 位不参与:

from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
 
lengths = [50, 47, 31, 8]             # 各样本真实长度
packed = pack_padded_sequence(x, lengths,
                              batch_first=True, enforce_sorted=False)
output, (h_n, c_n) = lstm(packed)     # h_n 恰为各样本真实末步的状态
output, _ = pad_packed_sequence(output, batch_first=True)

enforce_sorted=False 免去手动按长度降序排 batch,开销可忽略,默认写上。

三个高频事故

忘设 batch_first=True(静默维度错位);跨 batch 传隐状态忘 .detach()(计算图贯穿、显存膨胀);把 c_n 当输出用(应取 output 或 h_n)。

落到日常,batch_first=True + PackedSequence + 按需 detach 三件套覆盖绝大多数 RNN 族工程场景;剩下的变体(双向、多层堆叠)都只是 shape 参数的组合,不再引入新的心智负担。