10.2 RNN、LSTM 与 BPTT:状态既能携带上下文,也能携带泄漏
预言厅的日志不再是独立行:一次任务的第十秒要结合前九秒判断。模型工坊需要让相同的更新规则跨时间复用,并保留一个有限维状态。
RNN 的优势和风险来自同一个地方——hidden state。它可以汇总过去,也可能把上一个用户、未来方向或 padding 残留带进不该出现的位置。
本课目标
- 写出 vanilla RNN recurrence 与 tensor shape;
- 从 unrolled graph 理解 BPTT 与梯度消失/爆炸;
- 推导 LSTM cell/hidden state 和 gates;
- 正确处理 padding、length、bidirection 与 streaming state;
- 区分 sequence classification、token prediction 与 generation。
1. RNN 在时间上共享参数
对输入 $x_t$ 与上一步状态 $h_{t-1}$:
$$ h_t=\phi(W_{xh}x_t+W_{hh}h_{t-1}+b_h), $$
$$ o_t=W_{ho}h_t+b_o. $$
同一 $W_{xh},W_{hh}$ 用于所有 $t$,所以可处理可变长度并把参数量与序列长度解耦。它假设更新规则随时间共享;存在强季节/阶段差异时,time features 或其他架构仍需显式表达。
h_t 是压缩状态,不保证保留所有历史。Hidden size、training objective 与梯度路径决定保留什么。
2. Shape Contract
batch_first=True 时常见:
input: [B, T, input_size]
output: [B, T, directions * hidden_size]
h_n: [layers * directions, B, hidden_size]batch_first 不改变 hidden state 的 layout。多层/bidirectional 下直接拿 h_n[-1] 可能只取最后一层某个方向;要按 [layers, directions, B, H] reshape 后明确选择/拼接。
Embedding 输入通常是 long token IDs [B,T],输出 [B,T,E]。Padding ID 必须与 vocabulary/embedding 配置一致。
3. BPTT 是 Unrolled Graph 上的 Backprop
把 recurrence 展开 $T$ 步后,loss 对早期状态包含 Jacobian product:
$$ \frac{\partial L}{\partial h_t} =\sum_{k\ge t} \frac{\partial L_k}{\partial h_k} \prod_{j=t+1}^{k} \frac{\partial h_j}{\partial h_{j-1}}. $$
反复乘 $W_{hh}$、activation derivatives 和 gates:典型 singular values 小于 1 时贡献衰减,大于 1 时爆炸。Gradient clipping 限制爆炸 update,不能恢复已经消失的长期信号。
Sequence length 越长,activation memory 和反向成本越高。Truncated BPTT 每隔若干步 detach state,只在有限窗口反向;它降低成本,也明确截断了跨窗口 credit assignment。
4. LSTM 给 Cell State 一条受控加法路径
一种常见 LSTM 记法:
$$ i_t=\sigma(W_i[x_t,h_{t-1}]+b_i), $$
$$ f_t=\sigma(W_f[x_t,h_{t-1}]+b_f), $$
$$ o_t=\sigma(W_o[x_t,h_{t-1}]+b_o), $$
$$ g_t=\tanh(W_g[x_t,h_{t-1}]+b_g), $$
$$ c_t=f_t\odot c_{t-1}+i_t\odot g_t, $$
$$ h_t=o_t\odot\tanh(c_t). $$
Forget/input/output gates 分别控制旧 cell、候选写入和暴露输出。Cell 的加法更新可让某些梯度更长久流动,但 gates 仍会饱和,LSTM 不保证无限记忆或没有梯度问题。
GRU 合并部分状态/gates,参数更少。选择应按 validation、latency 和数据量比较,而不是固定“LSTM 比 RNN 高级”。
5. Padding 不是空白信息
变长序列通常 pad 到 batch 最大长度。若直接运行 RNN:
- padding steps 会继续更新 hidden state;
output[:, -1]可能对应 padding,不是最后有效 token;- token loss 若不 mask,会把 padding 当训练目标;
- Batch statistics/metric 分母也可能被污染。
可用 lengths gather 最后有效输出,或 pack_padded_sequence 跳过 padding。PyTorch 中 lengths tensor 传给 pack 时通常需在 CPU。
import torch
from torch import nn
from torch.nn.utils.rnn import pack_padded_sequence
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, classes, pad_id=0):
super().__init__()
self.embedding = nn.Embedding(
vocab_size,
embedding_dim,
padding_idx=pad_id,
)
self.lstm = nn.LSTM(
embedding_dim,
hidden_dim,
batch_first=True,
)
self.head = nn.Linear(hidden_dim, classes)
def forward(self, token_id, length):
embedded = self.embedding(token_id)
packed = pack_padded_sequence(
embedded,
length.cpu(),
batch_first=True,
enforce_sorted=False,
)
_, (h_n, _) = self.lstm(packed)
last_layer_hidden = h_n[-1]
return self.head(last_layer_hidden)若 bidirectional/multilayer,要重新处理 h_n directions,不能直接沿用最后一行。
6. Bidirectional RNN 会看未来
Bidirectional encoder 同时从左到右、从右到左读取完整序列,适合离线 tagging/classification。它不适合必须因果在线预测或 autoregressive generation,因为 backward direction 使用未来 token。
训练 feature extraction 里无意打开 bidirectional,是一种 temporal leakage。先问部署时完整序列是否已经到齐。
7. Many-to-one、Many-to-many 与 Causal LM
- sequence classification:整段序列输出一个 label;
- token labeling:每个有效位置输出 label;
- forecasting:根据过去输出未来数值;
- autoregressive LM:预测下一个 token distribution;
- seq2seq:encoder state/outputs 条件化 decoder。
Loss placement 不同。只在最后一步监督,早期信息路径更长;每步监督提供更密集 gradient,但必须与任务标签对应。
Teacher forcing 在训练 decoder 时喂真实上一个 token,推理则喂模型自己的输出,产生 exposure mismatch。Scheduled sampling 等方法也改变 objective,没有自动解法。
8. Stateful Streaming 的边界
流式 RNN 可跨 chunk 保留 state,减少重复计算。但必须定义 reset:
- 新用户/新 session;
- 设备重启/长空档;
- batch 中流顺序改变;
- model version 更新;
- backfill/out-of-order event。
训练 truncated chunks 时把 state detach(),避免 graph 无限增长;不同独立 sequences 之间要清零/正确索引 state。State cache 还是有状态服务数据,需要 TTL、一致性、隐私和故障恢复。
9. RNN 的顺序成本与替代方案
每个 $h_t$ 依赖 $h_{t-1}$,时间维难以完全并行;batch/layer matrix ops 仍可并行。长序列训练吞吐可能不如 attention/temporal convolution。
替代选择:
- 1D/dilated causal convolution:并行、固定 receptive field;
- Transformer:全局 content-based interaction,attention memory 可能二次;
- state-space models:不同的长序列计算/状态设计;
- feature aggregation + tabular model:小数据任务可能更稳。
RNN 在低延迟 streaming、有限 state 和较短序列上仍有价值。
10. 训练与评估
记录:
- length 分布与 length-bucket 指标;
- padding fraction/pack 吞吐;
- hidden/cell norm、gradient norm 与 clip rate;
- state reset correctness;
- causal cutoff 与 label maturity;
- teacher-forced loss 与 free-running generation/forecast error。
Random 行切分可能把同一 session 邻近窗口放入 train/test。按 entity/time/horizon 切分,避免高度重叠窗口泄漏。
常见误区
- RNN 会记住整个历史:hidden state 是有限压缩,训练未必保留目标信息。
- LSTM 解决梯度消失:它改善路径,不提供无限记忆保证。
output[:, -1]是最后有效状态:有 padding 时常不是。- Bidirectional 只是更强:在线因果任务会看未来。
- 保留 state 总能提高连续性:错误跨实体复用是严重泄漏。
练习
- 展开 4 步 RNN 计算图并标出共享 parameters。
- 检查
batch_first=True下 output 与h_nshape。 - 比较 padding 后
output[:,-1]、length gather 与 packed LSTM。 - 实现 truncated BPTT,观察 detach 前后的 graph/memory。
- 为流式多用户服务写 state key、reset 与 TTL 规则。
小结
RNN 用共享递推把历史压入 hidden state,BPTT 沿展开时间图求梯度。LSTM 的 gate/cell 改善长期路径,但 padding、bidirection、truncation 和 state 生命周期决定模型是否真正符合部署因果边界。
下一课去掉单一递推瓶颈:attention 让每个 query 直接从一组 keys/values 读取信息,同时带来 mask、二次矩阵和解释边界。