AI/ML 详解:RNN 与序列

Day 19 · 2026-06-05 · 难度 ★★★☆☆
面向:有编程经验的非 AI 方向工程师

Transformer 不是凭空出现的。在它之前,处理「序列」(一句话、一段语音、一串时间戳)的主力是 RNN(Recurrent Neural Network,循环神经网络)。今天我们走一遍 2014–2015 这条进化线:LSTM → GRU → Seq2Seq → Attention 起源。理解这条线,你会发现 Transformer 的 attention 不是魔法,而是对 RNN 一个具体瓶颈的精确回应——而那个瓶颈,用你熟悉的「有损压缩」一句话就能讲清。

长短期记忆网络LSTM · Long Short-Term Memory

门控长依赖
一句话类比

普通 RNN 像一个只有一个变量的滚动聚合(running aggregate):每读一个词就把它揉进同一个状态里,旧信息被反复覆盖。LSTM 在旁边加了一条独立的「传送带」(cell state)——类似数据库的 WAL(write-ahead log,预写日志):信息默认原样往前传,只有在「门」明确批准时才读、写、擦除。这条传送带让久远的信息能近乎无衰减地走很远。

它解决什么问题 + 工作机制

先说 RNN 的病根:梯度消失(vanishing gradient)。RNN 每往前一步,信息都要乘一个权重矩阵;训练时误差反向传播要把这些矩阵连乘几十上百次。连乘小于 1 的数 → 指数级趋零,远处的梯度消失(大于 1 则爆炸)。结果:RNN 学不到「The clouds are in the ___」这种隔了很多词的长依赖——就像信号经过太多跳(hop)后衰减殆尽。

LSTM(Hochreiter & Schmidhuber, 1997)的解法是把「记忆」和「计算」拆开。cell state 这条传送带上的更新是加法而非反复相乘——加法路径上梯度不会指数衰减,这是 LSTM 能记长的数学核心。三个门控制传送带:

  • 遗忘门 f:决定传送带上哪些旧信息该擦掉(比如新主语出现,旧主语的性别信息作废);
  • 输入门 i:决定当前哪些新信息写进传送带;
  • 输出门 o:决定从传送带读出哪部分,作为这一步对外的输出。

每个门都是 σ(W·[h, x] + b)——sigmoid 把结果压到 0~1,可以直观理解成「开关的开合程度」:0 = 完全关闭,1 = 完全放行。门不是人写死的规则,而是学出来的

cell state 传送带(信息默认直行,门按需读写)

C_{t-1} ──×遗忘门──+输入门──→ C_t (加法更新,梯度不衰减)
        ↓ 输出门
        h_t 本步输出

对比 RNN:只有一条 h,每步被权重矩阵反复相乘 → 远处梯度消失
代码示例
import torch, torch.nn as nn

# PyTorch 内置 LSTM,门控逻辑都封装好了
lstm = nn.LSTM(input_size=16, hidden_size=32, batch_first=True)

x = torch.randn(4, 10, 16)   # (batch=4, 序列长10, 每步特征16)
out, (h_n, c_n) = lstm(x)     # c_n 就是那条「传送带」cell state

print(out.shape)    # (4, 10, 32) 每个时间步都有输出
print(h_n.shape)    # (1, 4, 32) 最后一步的隐状态——常拿来当整段的「摘要」
print(c_n.shape)    # (1, 4, 32) 最后一步的 cell state
常见误区 + 实践场景
误区:「LSTM 能记无限长」。错。加法路径缓解了梯度消失,但没消灭它——实践中 LSTM 有效记忆通常几十到一两百步,远超不了。真要长上下文,得靠 attention(往下看)。这也是为什么 2017 后 Transformer 取代了它。
📌 超级个体场景:理解「门控」这个抽象本身比记公式值钱。当你设计一个有状态的 Agent 工作流时,「什么信息该遗忘、什么该写入长期记忆、什么该输出」正是遗忘门/输入门/输出门的工程版——LSTM 给了你一套思考记忆管理的母语。
Takeaway + 思考题
💡 LSTM 的核心创新不是「更复杂」,而是给信息开了一条加法高速路,绕开了连乘导致的梯度消失。
🤔 你熟悉的系统里,哪些地方也用「默认直传 + 显式门控」来对抗信息衰减?(提示:缓存穿透、消息队列的 ack)

门控循环单元GRU · Gated Recurrent Unit

门控轻量
一句话类比

GRU 是 LSTM 的精简版。如果说 LSTM 是「三副本强一致」的存储,GRU 就是「两副本」——少了一条独立的 cell state,把「记忆」和「输出」合并成同一个隐状态,门也从 3 个砍到 2 个。换来的是更少参数、更快训练,多数任务上效果和 LSTM 打平。典型的工程权衡:用一点理论上的表达力,换实打实的速度。

它解决什么问题 + 工作机制

LSTM 三门 + 独立 cell state,参数多、计算重。GRU(Cho et al., 2014)问:能不能更省?它的设计:

  • 更新门 z(update gate):一个旋钮同时控制「保留多少旧状态」和「写入多少新状态」——把 LSTM 的遗忘门和输入门合二为一(保留 = 1 − 写入);
  • 重置门 r(reset gate):决定算新候选状态时,要不要忽略掉旧状态(r→0 等于「忘掉过去,只看当前输入」);
  • 没有独立 cell state:只有一个隐状态 h 既当记忆又当输出。

关键直觉在更新门的插值(interpolation)结构:h_t = (1−z)·h_{t-1} + z·h̃_t。读作「新状态 = (1−z) 份旧的 + z 份新的」。当 z→0,h 几乎原样直传——这和 LSTM 传送带是同一个对抗梯度消失的招式:留一条接近恒等(identity)的直传通道。

代码示例
import torch, torch.nn as nn

gru  = nn.GRU(16, 32, batch_first=True)
lstm = nn.LSTM(16, 32, batch_first=True)

# 直接对比参数量:GRU 用 3 组门权重,LSTM 用 4 组
n_gru  = sum(p.numel() for p in gru.parameters())
n_lstm = sum(p.numel() for p in lstm.parameters())
print(n_gru, n_lstm)   # 4800 vs 6400 —— GRU 约少 1/4 参数

out, h_n = gru(torch.randn(4, 10, 16))  # 注意:GRU 只返回 h,没有 c
常见误区 + 实践场景
误区:「GRU 比 LSTM 新,所以更好」。错。两者没有普适赢家——数据少/要快时 GRU 常占优,超长依赖或大数据上 LSTM 偶尔更稳。该跑实验对比,而不是认招牌。这也是经典 ML 的常态:架构选择是数据驱动的。
📌 决策辅助场景:GRU vs LSTM 是「简化是否值得」的小型样本。你做技术选型时同样在问:砍掉一个组件,省下的成本能否覆盖损失的能力?GRU 的答案——「多数场景值得」——本身就是个有用的先验。
Takeaway + 思考题
💡 GRU = 把 LSTM 的「保留 vs 写入」用一个更新门做插值,少一条传送带、少一个门,速度换微小表达力。
🤔 「(1−z)·旧 + z·新」这个插值公式,和你见过的指数移动平均(EMA)/ 加权滑动窗口有什么深层联系?

序列到序列Seq2Seq · Encoder-Decoder

架构编码解码
一句话类比

Seq2Seq 把「输入序列 → 输出序列」拆成两段:编码器(encoder)把整句话读完,压成一个固定长度的向量解码器(decoder)拿这个向量逐词生成输出。像一个 RPC 调用:客户端(encoder)把整个请求序列化成一个定长 payload,发给服务端(decoder)反序列化出结果。问题马上就来了——把任意长的句子塞进一个固定大小的向量,必然有损

它解决什么问题 + 工作机制

痛点:翻译、摘要、对话这类任务,输入和输出长度都不固定且不对齐(中文 5 个词可能译成英文 8 个词)。普通分类网络做不到「变长进、变长出」。Seq2Seq(Sutskever, Vinyals & Le, 2014)的解法优雅:

  • 编码器(一个 LSTM)顺序读完输入,最后一步的隐状态 = 整句的「摘要向量」(context vector,也叫 thought vector);
  • 解码器(另一个 LSTM)以这个向量为初始状态,自回归地一个词一个词往外吐,直到生成结束符。

这是「理解」和「生成」第一次被干净地分成两个模块——今天所有 encoder-decoder 架构(包括原版 Transformer)都继承自这个骨架。原论文还有个反直觉的工程技巧:把输入句子倒着喂能提分,因为它让源句开头和译文开头在时间上更近,缓解了长距离传递的衰减。

Seq2Seq:整句被压成一个定长向量(瓶颈在这)

我 爱 猫 Encoder LSTM [一个定长向量] Decoder LSTM I love cats

↑ 不管句子多长,全部信息都要挤进中间这一个红框 → 句子越长,信息丢得越多
代码示例
import torch, torch.nn as nn

class Seq2Seq(nn.Module):
    def __init__(self, vocab, dim=64):
        super().__init__()
        self.emb = nn.Embedding(vocab, dim)
        self.encoder = nn.LSTM(dim, dim, batch_first=True)
        self.decoder = nn.LSTM(dim, dim, batch_first=True)
        self.out = nn.Linear(dim, vocab)

    def forward(self, src, tgt):
        _, state = self.encoder(self.emb(src))   # 编码:只取最终状态当「摘要」
        dec, _ = self.decoder(self.emb(tgt), state)  # 用摘要初始化解码器
        return self.out(dec)                      # 每步预测下一个词
常见误区 + 实践场景
误区:「向量维度调大就不丢信息了」。治标不治本。瓶颈不在维度大小,而在「用一个固定向量代表任意长输入」这个结构假设——50 个词的句子和 5 个词的句子被迫挤进同样大小的空间。真正的解药是结构性的:让 decoder 别只看摘要,而能回头看原文每一处——这就是 attention。
📌 跨学科思考:这个「定长摘要瓶颈」和人类记忆惊人地像——你读完一本书,脑里也只留一个模糊「摘要」,细节要回头翻书。attention 之于 Seq2Seq,正如「带着书随时查」之于「只靠脑补」
Takeaway + 思考题
💡 Seq2Seq 确立了 encoder-decoder 骨架,但把整句压成一个定长向量,是个结构性的有损瓶颈
🤔 如果让你不改 RNN、只改信息流,怎么让 decoder「记住」输入的细节而非只拿到摘要?(这正是下一张卡片的答案)

注意力机制的起源Attention · Bahdanau 2014

对齐里程碑
一句话类比

Attention 把 Seq2Seq 的「只传一份摘要」改成「保留全部原始记录 + 一个按需检索的索引」。decoder 每生成一个词,就对 encoder 的所有隐状态做一次加权检索——像数据库的 JOIN,但用相关性打分代替精确匹配,也像 RAG 每步从原文动态召回最相关的片段。它彻底干掉了定长瓶颈,也是后来 Transformer「Attention is All You Need」的直系祖先

它解决什么问题 + 工作机制

痛点就是上一张卡的瓶颈:decoder 只拿到一个定长摘要,长句细节全丢。Bahdanau, Cho & Bengio(2014)的解法是软对齐(soft alignment)——decoder 生成第 i 个词时,不再只用那一个向量,而是临时算一个专属于这一步的上下文向量:

ci = Σj αij · hj
αij = softmax( score(si-1, hj) )

逐符号拆解(这是今天唯一需要嚼的公式):

  • hj:encoder 第 j 个词的隐状态——保留下来的「原始记录」,不再丢弃;
  • score(·):打分函数,衡量「decoder 当前状态 si-1」和「源词 hj」有多相关——即该不该关注这个源词
  • αij:把所有分数过一遍 softmax,归一成和为 1 的权重,可读成「这一步该把多少注意力分给源词 j」;
  • ci:用这组权重对所有 hj 加权求和——为当前这一步量身定制的上下文

核心直觉:翻译每个词时,模型自己学会该回头看原文的哪几个词。译「cats」时权重集中在「猫」,译「love」时集中在「爱」。这套 score→softmax→加权求和的三步,正是今天 Transformer self-attention 的原型——区别只在 Transformer 把它从 encoder-decoder 之间,推广到了序列对自己。

Attention:decoder 每步回看全部源词,按相关性加权

源词隐状态:h(我) h(爱) h(猫)
       ↘ 0.1  ↘ 0.1  ↘ 0.8
解码「cats」 加权求和 c(高度关注「猫」)

权重 α 是学出来的、随每一步动态变化 → 没有固定瓶颈
代码示例
import torch, torch.nn.functional as F

# 加性注意力(Bahdanau)的核心三步,去掉工程封装看本质
def attention(s_prev, enc_h):       # s_prev:(B,d) decoder上一步; enc_h:(B,T,d) 全部源词
    score = (enc_h * s_prev.unsqueeze(1)).sum(-1)  # ① 打分 (B,T)
    alpha = F.softmax(score, dim=-1)              # ② 归一成权重,和为1
    ctx   = (alpha.unsqueeze(-1) * enc_h).sum(1)   # ③ 加权求和 (B,d)
    return ctx, alpha             # alpha 还能可视化「模型在看哪个词」

ctx, a = attention(torch.randn(2,8), torch.randn(2,5,8))
print(ctx.shape, a.shape)   # (2,8) (2,5) —— 每步一个定制上下文 + 一行注意力分布
常见误区 + 实践场景
误区:「attention 是 Transformer 发明的」。不对。2014 年的 Bahdanau attention 早 3 年就有了,当时它还嫁接在 RNN 上当配件。Transformer(2017)的真正激进之处是把 RNN 整个扔掉、只留 attention——「Attention is All You Need」这个标题就是冲着「不再需要循环」喊的。理解这段历史,你才懂 attention 不是又一个 trick,而是解开了序列建模 20 年的长依赖死结
📌 超级个体场景:attention 权重 α 是可解释的——能直接画出「模型翻译这个词时在看原文哪里」。这种「让模型告诉你它在关注什么」的思路,正是你做 AI 协作时该索取的:不只要答案,还要它的注意力落点,以判断推理是否可信。
Takeaway + 思考题
💡 Attention = score → softmax → 加权求和,让 decoder 动态回看全部源信息,一举打掉定长瓶颈,并直接孕育了 Transformer。
🤔 既然 attention 这么强,为什么还要等 3 年(2014→2017)才有人敢把 RNN 完全删掉?阻力可能在哪?(提示:并行化、归纳偏置、算力)

深入资源Further Reading

深入思考Deep Questions

1. LSTM 的「加法传送带」和 ResNet 的「残差连接」(Day 18)是不是同一个思想?
本质上是同一招:都在网络里留一条接近恒等(identity)的直传通路,让梯度能无衰减地流过很多层/很多步。LSTM 的 cell state 用加法更新 C_t = f·C_{t-1} + i·C̃,当遗忘门 f≈1 时就是「C_t ≈ C_{t-1} + 增量」;ResNet 的 y = x + F(x) 是「输出 = 输入 + 残差」。两者都让梯度反向传播时有一条「+1」的直达路径,绕开连乘导致的指数衰减——区别只是 LSTM 沿时间方向(同一层、不同时间步),ResNet 沿深度方向(同一时刻、不同层)。GRU 的 (1−z)·h + z·h̃ 插值也是一样。所以「留一条恒等捷径对抗梯度消失」是贯穿深度学习的核心母题,LSTM(1997) 比 ResNet(2015) 早了 18 年就用上了——你看,好思想会在不同地方反复重新发明。
2. 既然 attention 2014 年就有了、还那么有效,为什么直到 2017 年才有人敢删掉 RNN?
三个阻力。(1) 并行化的诱因当时还不够痛:RNN 必须按时间步顺序算(第 t 步依赖第 t−1 步),无法并行,但 2014 年序列和模型都还小,这个慢还能忍。Transformer 去掉循环后所有位置可同时算,恰好吃满了 GPU——是算力增长把「能并行」从「nice to have」变成「决定胜负」。(2) 归纳偏置(inductive bias)的信仰:RNN 的「按顺序处理」天然编码了「序列有先后」这个先验,大家直觉上觉得删掉它模型会失去时序感。Transformer 用位置编码(Day 13)补上了这个信息,证明先验可以显式注入而非靠结构强加。(3) 需要配套创新一起到位:self-attention、多头、位置编码、残差+LayerNorm 这套组合拳缺一不可——单有 attention 删不掉 RNN。历史常如此:关键部件 A 早就存在,但要等 B、C、D 凑齐、且外部条件(算力/数据)成熟,才会发生范式跃迁。attention→Transformer 的 3 年间隔,是「零件齐了但还差临门一脚」的典型。
3. RNN 按时间步串行、Transformer 全并行——这个差异和你熟悉的「流式处理 vs 批处理」是一回事吗?
有深刻的呼应,但不完全等同。RNN 像流式处理(streaming):数据一个个来,维护一个滚动状态(hidden state),天然适合无限长 / 在线(online)场景,内存恒定,但无法并行、且远端信息会衰减。Transformer 像批处理(batch):一次性把整个窗口加载进来,所有位置两两交互,能并行、不丢细节,但代价是 O(n²) 的计算/显存,且窗口有硬上限——装不下「无限流」。有趣的是 2024–2025 的 SSM / Mamba(Day 34)某种程度上想两头都要:像 RNN 一样维护恒定大小的状态、可流式、线性复杂度,又通过精巧设计逼近 attention 的长程能力。所以这不是「谁淘汰谁」,而是串行恒定状态 vs 并行全连接两种范式的长期拉锯——和分布式系统里「有状态流处理 vs 无状态批处理」的取舍同构。你的分布式背景在这里能直接迁移直觉:吞吐 vs 延迟、内存 vs 算力、在线 vs 离线。
4. Attention 权重号称「可解释」,能告诉你模型在看哪个词——但这种解释可信吗?
谨慎乐观。attention 权重确实提供了一个诱人的解释:画出 α 矩阵,能看到翻译「cats」时高亮「猫」,直觉上「模型在对齐」。但学界对「attention 是不是真·解释」有长期争论(Jain & Wallace 2019「Attention is not Explanation」vs Wiegreffe & Pinter 2019「Attention is not not Explanation」)。核心质疑:(a) 权重高 ≠ 因果重要——可以构造出不同的 attention 分布但输出几乎不变,说明它未必是模型决策的真实依据;(b) 多头、多层叠加后,单层权重的语义被稀释,不能简单当成「模型的注意力」。务实结论:attention 权重是有用的调试线索和假设来源,但不是模型推理的权威供词。这对你做 AI 协作有直接启发——当一个 AI 工具「告诉你它的理由」时,那个理由是事后叙述还是真实因果?两者经常不是一回事,需要交叉验证而非照单全收。
5. 从 RNN 到 Transformer 这十年,是「越来越复杂」还是「越来越简单」?
表面看是变复杂(参数从百万到万亿),但核心抽象其实在变简单、变统一。RNN 时代:处理序列要精心设计门控、要管理隐状态的时序传递、要和梯度消失搏斗——结构里塞满了「关于序列该怎么处理」的人工先验。Transformer 把这些大幅删减:没有循环、没有门控、没有时序假设,只剩「所有位置互相 attention + 前馈 + 位置编码」这一套高度一致、可堆叠的积木。这其实是深度学习反复出现的规律——「The Bitter Lesson」(Sutton):长期看,削减人工先验、把空间让给数据和算力的简单通用架构,会赢过精心设计的复杂结构。LSTM 的门控是人类智慧的结晶,但 Transformer 证明:给够数据和算力,一个更简单、更可并行、更少假设的架构能学得更好。这对超级个体是一个深刻隐喻——真正的杠杆常来自删繁就简、让通用机制自己涌现,而非堆砌特例。你设计任何系统(工作流、Agent、决策框架)时都值得自问:我加的这层结构,是必要的归纳偏置,还是迟早会被数据冲掉的人工假设?