跳到内容

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 时常见:

text
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。

python
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 总能提高连续性:错误跨实体复用是严重泄漏。

练习

  1. 展开 4 步 RNN 计算图并标出共享 parameters。
  2. 检查 batch_first=True 下 output 与 h_n shape。
  3. 比较 padding 后 output[:,-1]、length gather 与 packed LSTM。
  4. 实现 truncated BPTT,观察 detach 前后的 graph/memory。
  5. 为流式多用户服务写 state key、reset 与 TTL 规则。

小结

RNN 用共享递推把历史压入 hidden state,BPTT 沿展开时间图求梯度。LSTM 的 gate/cell 改善长期路径,但 padding、bidirection、truncation 和 state 生命周期决定模型是否真正符合部署因果边界。

下一课去掉单一递推瓶颈:attention 让每个 query 直接从一组 keys/values 读取信息,同时带来 mask、二次矩阵和解释边界。

Built with VitePress | Software Systems Atlas