从 RNN 到 Transformer--序列建模架构的演进与变革(二)

目录

三、Transformer:抛弃循环,只留注意力

这一章的目标只有一个:让你在不看任何公式的情况下,先弄明白 Transformer 这台机器是怎么运转的。公式排在后面,用来验证直觉,而不是制造障碍。

3.1 先建立一个心智模型:它到底在干什么?

忘掉神经网络,先想象一家翻译公司接到一份英译中的任务。

老办法(RNN):一个人从头读到尾

小王拿到英文稿 → 逐字读,边读边在脑子里记笔记
                → 读完后,脑子里只剩一份"笔记"
                → 凭这份笔记,开始逐字写中文

问题显而易见:

  • 他必须一个字一个字读,不能跳读,不能几个人分工(串行);
  • 文章一长,前面的内容就从笔记里挤掉了(遗忘);
  • 写中文时,他手上只有那份”笔记”,原文已经不在手边了(信息瓶颈)。

新办法(Transformer):一个团队协同作业

【理解组】全员同时读英文稿,边读边互相讨论
          → 每个人手里的笔记都变成了"结合全文理解后的笔记"

【写作组】写中文时,每写一个词,就回头翻阅理解组的全套笔记,
          挑出当前最相关的部分重点参考 → 写出下一个词

这个新流程有三个关键改进,正好对应 Transformer 的灵魂:

改进对应机制
全员同时读、互相讨论Self-Attention(自注意力):所有位置并行,彼此直接通信
每个人手里的笔记都包含全文上下文上下文相关的表示
写的时候回头翻阅原文笔记Cross-Attention(交叉注意力)

一句话心智模型:Transformer 就是”反复开讨论会”。 每开一次会,每个词都把所有相关词的信息收集过来,更新自己的理解;会开得越多(层数越深),理解越充分。最后带着这份理解去生成答案。

这个”开会”的动作,就是 Attention。 理解了这一点,剩下的都是工程细节。

3.2 宏观架构:两层楼的小工厂

现在把上面那家公司画成建筑图。Transformer 是 Encoder-Decoder 结构,可以看成一栋两层的小楼:

graph TB
    subgraph B1["一楼:理解组 Encoder"]
        direction TB
        IN["英文原文<br/>The cat sat on the mat"] --> EMB1["第一步 编号加座位牌<br/>Embedding 加 Positional Encoding"]
        EMB1 --> BLK1["第二步 讨论室 重复 6 层<br/>每层 开会 后 各自整理笔记"]
        BLK1 --> MEM["第三步 成品笔记柜<br/>每个词一份笔记 全套保留"]
    end
    subgraph B2["二楼:写作组 Decoder"]
        direction TB
        PREV["已写出的中文<br/>猫 坐 在"] --> EMB2["第一步 编号加座位牌"]
        EMB2 --> BLK2["第二步 写作间 重复 6 层<br/>每层 内部开会 再 查阅一楼笔记柜"]
        BLK2 --> OUT["第三步 猜下一个词<br/>Linear 加 Softmax"]
    end
    MEM -.->|"随时查阅 Cross-Attention"| BLK2
    OUT --> NEXT["下一个词 垫子"]
    NEXT -.->|"补进已写内容 继续生成"| PREV

请记住这张图里的三个反直觉之处,它们是初学者的常见卡点:

🤔 常见误解✅ 实际情况
Encoder 把句子压成一个向量不压! 每个词都保留一份独立的笔记向量,全部传给 Decoder
Decoder 只接收 Encoder 最后一层Decoder 每一层都能去查一次笔记柜
输出是一次性生成的一次只生成一个词,生成完再补回输入,循环往复(自回归)

3.3 每层楼里,一场”讨论会”的标准流程

无论一楼二楼,每个”房间”(层)里干的事情都是同一套三件套。用一楼的房间举例:

进门:每个人带着自己的笔记 x
  │
  ├─ 第 1 步:开会(Multi-Head Self-Attention)
  │    每个人发言:"我需要什么信息?" → 全场匹配 → 收集到别人的信息
  │    得到:会议收获 a
  │
  ├─ 第 2 步:合并(Add & Norm)
  │    新笔记 = 旧笔记 + 会议收获,再归一化一下
  │    即:x + a,然后 LayerNorm
  │    ⭐ 这就是"残差连接":永远不丢掉自己原来的想法
  │
  ├─ 第 3 步:独自消化(Feed Forward Network)
  │    每个人回到座位,独自把刚收集来的信息加工、提炼一遍
  │    得到:消化结果 f
  │
  └─ 第 4 步:再合并(Add & Norm)
       最终笔记 = 上一步结果 + 消化结果
       即:(x + a) + f,然后 LayerNorm
出门:带着更新后的笔记去下一个房间

写成公式就是:

x_out = LayerNorm( x + Attention(x) )          # 开会 + 合并
x_out = LayerNorm( x_out + FFN(x_out) )        # 消化 + 合并

为什么每个子层都要包一层”残差 + 归一化”? 这是 CNN 时代 ResNet 留下的宝贵经验:

  • 残差(+ x):给信息留一条”直通车道”。就算这一层的会议开得一塌糊涂,原始信息也不会丢,梯度也能原路返回。这是 Transformer 能堆到上百层的根本原因。
  • LayerNorm(归一化):把数值拉回稳定范围,防止层层放大后失控。

打个比方:残差连接就像开会时保留原始笔记的复印件,新收获单独记在活页上。无论会议结论多离谱,你手里的原始材料都还在。

3.4 跟着一个词走完全程(具体例子)

光看结构图还是抽象。我们来跟着一个词走一遍,把所有零件串起来。

任务:翻译 The cat sat on the mat → 猫 坐 在 垫子 上 此时:已经写出了「猫 坐 在」,要预测第 4 个词。

第 0 步:进场前的准备

处理做什么为什么要做
分词The / cat / sat / on / the / mat模型只认词表里的编号
编号(Embedding)每个词 → 一个 512 维向量把符号变成可计算的数字
发座位牌(位置编码)向量 + 位置信息⚠️ Attention 本身不看顺序,必须手动告诉它”你是第几个”

为什么 Attention 会丢顺序? 因为它的核心运算是”两两互相看”,这个动作对顺序不敏感——把 {A, B, C} 换成 {C, A, B},两两之间的关系还是那些,输出集合不变(置换不变性)。所以必须显式加上位置信息。

位置编码怎么做?用一个不需要学习的固定公式,给每个位置的 512 个维度分别填入不同频率的 sin/cos 值:

PE(pos, 2i)   = sin( pos / 10000^(2i/512) )
PE(pos, 2i+1) = cos( pos / 10000^(2i/512) )

可以把它想成一块手表:有的指针转得快(区分相邻的词),有的指针转得慢(区分段落首尾)。把快针慢针的组合拼起来,就能唯一标定”第几个位置”。

现代大模型主流改用 RoPE(旋转位置编码),思路更优雅:不额外加一个向量,而是直接把 Q、K 向量”旋转”一个与位置相关的角度。两个向量各自旋转后再做点积,结果只取决于它们的相对距离——天生就是在表达”相隔多远”。这也是它能更好地外推到长文本的原因。

第 1 步:一楼 —— 理解组开会(跑 6 层)

对 The / cat / sat / on / the / mat 这 6 个词,同时地、并行地做注意力。

开会过程中会发生什么?举几个真实会出现的现象:

词它重点看了谁为什么
sat(坐)重点看 cat、mat要搞清楚”谁坐、坐在哪”
the(第二个)重点看 mat冠词要跟它修饰的名词绑在一起
on重点看 sat、mat介词要连接动作和地点

经过 6 层会议后,每个词的笔记都不再是”这个词本身的意思”,而是**“这个词在整句话语境下的意思”**。比如 sat 的笔记里已经融合了”主语是猫、地点是垫子”。

✅ 6 份笔记全部保留,摆进笔记柜。这就是”没有信息瓶颈”的关键。

第 2 步:二楼 —— 写作组动笔(也跑 6 层)

输入是已写出的「猫 坐 在」。这一层有两个额外机制:

(a)戴眼罩写作:因果掩码(Causal Mask)

写作组给自己立了条规矩:写第 4 个词时,不许看第 5、6 个词(因为还没写出来呢)。

实现极其简单粗暴——把”未来位置”的分数直接填成 -∞,softmax 之后权重就变成 0:

正在生成的位置 ↓ / 能否看到 →猫坐在垫子
猫(第 1 个词)✅ 能看❌ 屏蔽❌ 屏蔽❌ 屏蔽
坐(第 2 个词)✅ 能看✅ 能看❌ 屏蔽❌ 屏蔽
在(第 3 个词)✅ 能看✅ 能看✅ 能看❌ 屏蔽
垫子(第 4 个词)✅ 能看✅ 能看✅ 能看✅ 能看

读法:每一行是一个”正在写的位置”,每一列是”它想看的位置”。左下角能看、右上角全屏蔽,形成一个下三角。

这个下三角矩阵就是因果掩码。GPT 之所以叫”自回归”模型,全靠它。没有它,模型训练时会直接抄答案。

(b)查阅笔记柜:Cross-Attention

这是 Decoder 比 Encoder 多出来的那一层,也是翻译这件事能成立的关键:

  • Q(我要什么) 来自二楼:「我现在要写介词后面的宾语了,需要什么信息?」
  • K / V(有什么) 来自一楼的笔记柜:全部 6 个词的笔记

于是模型会自动把注意力集中到 mat 上——它自己学会了”垫子”这个词该对齐到 mat。这就是机器翻译里”词对齐”的全过程,没有任何人工规则。

第 3 步:出口 —— 猜下一个词

最后一层输出的向量,投影到整个词表(比如 5 万个词),softmax 后得到概率分布:

垫子   0.71   ← 选中
地毯   0.12
沙发   0.06
...   ...

选出「垫子」,把它补进已写内容 → 「猫 坐 在 垫子」→ 回到第 2 步继续生成 → 直到输出结束符。

这里有个重要的工程事实:每生成一个词,都要把前面所有词重新算一遍(所以有 KV Cache 这个优化——把算过的 K、V 缓存起来,避免重复计算)。这也是为什么 LLM 的”首字延迟”和”后续吐字速度”感受完全不同。

3.5 数据流:形状追踪(写给要实现它的人)

从工程实现角度,Transformer 有一个极其友好的性质:从进到出,张量形状始终不变。

输入 token ids                : (B, L)                        B=batch, L=序列长度
  ↓ Token Embedding           : (B, L, 512)
  ↓ + 位置编码                : (B, L, 512)
  ↓ ┌──── 以下重复 6 层 ────┐
  │  Q/K/V 投影 + 切头        : (B, L, 512) → (B, 8, L, 64)
  │  注意力分数 QKᵀ/√d_k      : (B, 8, L, L)                  ← O(L²) 就出自这里
  │  加权求和 × V             : (B, 8, L, 64)
  │  拼回 + 输出投影 W_O      : (B, L, 512)
  │  残差 + LayerNorm         : (B, L, 512)
  │  FFN 512→2048→512         : (B, L, 512)                   ← 参数量占全模型约 2/3
  │  残差 + LayerNorm         : (B, L, 512)
  ↓ └────────────────────┘
Encoder 输出                  : (B, L, 512)

“保形”为什么重要? 因为输入输出的形状一样,模块就可以像乐高一样任意堆叠、任意插拔——想堆 6 层就 6 层,想堆 96 层就 96 层,不用改任何代码。这是 Transformer 可扩展性的工程根基。

顺带说一个常被忽略的事实:FFN 的参数量约占整个模型的 2/3(每层注意力约 1.05M,FFN 约 2.1M)。所以业内有种说法——Attention 负责”路由”,FFN 才是”知识库”。后来的 MoE 正是在 FFN 上做稀疏化。

3.6 对照表:Transformer 逐个击破 RNN 的痛点

RNN 的痛点Transformer 怎么解决效果
必须一个字一个字读全场同时开会训练速度提升一个数量级
读到后面忘了前面任意两位置直接对话信息通路 O(1)
全句压成一个向量每个词的笔记全部保留长句翻译质量跃升
堆到 4 层就堆不动残差连接提供梯度直通车道可堆 上百层
不知道模型在看哪注意力权重可直接可视化可解释性大幅提升

3.7 三条演化路线:搭积木的不同搭法

原版 Transformer 是”理解组 + 写作组”齐全的完整版。但后来大家发现,拆开来单独用反而更好:

路线代表注意力怎么遮预训练任务擅长
只用理解组
Encoder-only
BERT、RoBERTa不遮,双向全看完形填空(遮住词猜词)理解类:分类、NER、检索、句向量
只用写作组
Decoder-only
GPT、LLaMA、Qwen因果掩码,只看左边续写(预测下一个词)生成类:对话、写作、代码
两组都用
Encoder-Decoder
T5、BART组内遮 + 跨组查去噪重建条件生成:翻译、摘要

为什么 Decoder-only 成了今天大模型的绝对主流? 三个原因:

  1. 预训练任务和真实任务同构——“续写”就是”生成”,训练和推理没有割裂;
  2. KV Cache 让推理最高效——单向注意力可以最大限度地复用缓存;
  3. Scaling 曲线最平滑——规模越大效果越好,规律最稳定。

顺带解释一个常见困惑:BERT 为什么不能用来聊天? 因为它是双向的,训练时”看到了答案”。让它逐词生成,等于让一个习惯了看着全文做填空的人去闭着眼睛续写——能力不对口。

3.8 诚实地说:Transformer 自己的新麻烦

它不是终点,而且引入了自己的代价:

新问题大白话主流对策
O(n²) 复杂度文本长度翻倍,计算量变 4 倍FlashAttention(不改数学,改显存读写)、稀疏/滑窗注意力
KV Cache 吃显存生成时要把所有历史的 K、V 存着,长对话非常吃显存GQA(多个头共用一份 K/V)、MLA(压缩后再存)
超出训练长度就变笨训练时最长 4K,给你 100K 就懵了RoPE 外推、YaRN 等长上下文技术
FFN 太耗参数模型一大,参数量爆炸MoE:拆成多个”专家”,每次只激活少数几个

有意思的是,最后一条催生了一次螺旋式回归:Mamba 这类状态空间模型重新捡起了 RNN 的”线性复杂度”优点,但保留了并行训练能力。架构演进从来不是简单的谁替代谁,而是取舍的重新平衡。


四、Attention 机制详解:从”加权平均”说起

Attention 被讲得玄学,是因为大家都从 Q、K、V 开始讲。本章反过来:先讲你早就会的加权平均,再看 Q/K/V 是怎么自然而然被逼出来的。

4.1 第一步:Attention 的本质就是”加权平均”

假设你要理解句子里的”它”指的是谁:

「猫 追 老鼠 , 因为 它 饿 了」

人一眼就知道”它”是猫,不是老鼠。这个判断过程拆开其实是三步:

  1. 扫一遍:把候选对象(猫、追、老鼠…)都过一遍;
  2. 打相关性分:猫和”饿”最相关 → 高分;“追”是动词 → 低分;
  3. 按分混合:把高分对象的信息多掺一些进来。

这三步用数学写出来,就是:

结果 = 分数₁ × 信息₁  +  分数₂ × 信息₂  +  ... (分数加起来 = 1)

这就是 Attention 的全部。 剩下的 Q、K、V 只是让这个”打分”和”信息”变得可学习而已。

4.2 第二步:Q / K / V 是怎么被”逼”出来的?

上面的三步有个问题:分数怎么打?信息又是什么? 如果靠人工规则,就退回符号主义老路了。于是设计者做了个巧妙的拆分——把每个词的向量拆成三种角色:

角色通俗理解类比干什么用
Q(Query,查询)“我在找什么”手里的购物清单用来跟别人比对,算分数
K(Key,键)“我能提供什么”商店门口的招牌被 Q 比对,决定匹配度
V(Value,值)“我实际给的东西”货架上的实际商品被加权平均,构成输出

关键设计:K 和 V 是分离的。 招牌(K)只需要简洁、便于比对;商品(V)才是真正要传递的内容。这个分离让模型可以”用一种标准去检索,用另一种内容去输出”,表达能力强了很多。

一个精妙的类比:可微的字典查询。 查字典时,你用拼音(Q)去匹配索引(K),拿到释义(V)。这个过程的”匹配”是离散的、不可求导的。 Attention 做的事,是把这套流程变成连续可微的——不是”精确命中某一条”,而是”按相似度混合所有条目”。于是”检索”这个动作第一次能被塞进神经网络里端到端训练。

Q、K、V 从哪来?

答案是:从输入向量各自做一次线性变换得到。三个不同的矩阵 W_Q、W_K、W_V 都是可训练参数:

同一个词 x  ──× W_Q──→  q (我要找什么)
            ──× W_K──→  k (我的招牌)
            ──× W_V──→  v (我的货)

为什么要投影而不直接用 x?因为同一个词在”被查询”和”去查询别人”时的角色应该不同。经过训练,模型会自己学会该怎么拆。

4.3 第三步:手算一遍(4 个词的小例子)

这是全篇最重要的一节。跟着算一遍,Attention 就再也不会忘。

句子:猫 / 追 / 老鼠 / 它,我们计算第 4 个词「它」的输出。 简化:真实维度是 64,这里用 2 维,方便手算。

Step 1|生成 Q、K、V(假设网络已经学好了,给出数值)

词K(招牌)V(货)
① 猫[1.0, 0.0][2.0, 1.0]
② 追[0.0, 1.0][0.0, 3.0]
③ 老鼠[0.7, 0.7][1.0, 2.0]
④ 它[1.0, 0.0][0.0, 0.0]

「它」的查询向量:q₄ = [1.0, 0.0](可以理解为”我在找一个动物”)

Step 2|打分:用 q 去点乘每一个 k

分数 = q · k        (点积越大 = 越相似)

猫   : [1,0]·[1,0]   = 1×1 + 0×0 = 1.0   ← 高度匹配
追   : [1,0]·[0,1]   = 1×0 + 0×1 = 0.0   ← 完全不相关(动词)
老鼠 : [1,0]·[0.7,0.7] = 1×0.7 + 0×0.7 = 0.7  ← 部分匹配(也是动物)
它   : [1,0]·[1,0]   = 1×1 + 0×0 = 1.0   ← 自己看自己

注意这里「老鼠」得分 0.7 低于「猫」的 1.0——这正是我们希望的结果:“它”应该更倾向于指猫。这个偏好不是人工设定的,而是模型从海量语料里学出来的。

Step 3|缩放:除以 √d_k = √2 ≈ 1.41(为什么除,见 4.4)

1.0/1.41 = 0.71      0.0/1.41 = 0.00      0.7/1.41 = 0.50      1.0/1.41 = 0.71

Step 4|softmax 归一化:把分数变成加起来等于 1 的权重

e^0.71 = 2.03      e^0.00 = 1.00      e^0.50 = 1.65      e^0.71 = 2.03
                        总和 = 6.71

猫   : 2.03 / 6.71 = 0.30  (30%)
追   : 1.00 / 6.71 = 0.15  (15%)
老鼠 : 1.65 / 6.71 = 0.25  (25%)
它   : 2.03 / 6.71 = 0.30  (30%)

Step 5|加权求和:用权重去混合 V

输出 = 0.30 × [2,1]  +  0.15 × [0,3]  +  0.25 × [1,2]  +  0.30 × [0,0]
     = [0.60, 0.30] + [0.00, 0.45]   + [0.25, 0.50]  + [0.00, 0.00]
     = [0.85, 1.25]

结果解读:「它」这个词的向量,从原来的 [0,0] 变成了 [0.85, 1.25]。

这个新向量里,混入了 30% 的”猫” + 25% 的”老鼠” + 15% 的”追”。也就是说——“它”这个位置的表示,现在包含了它所指向的实体的信息。 后续层就能基于这个表示做出正确判断。

✅ 这就是 Attention 的全部魔法:把一个词的表示,替换成”按相关性混合了全场信息”的新表示。

第三步的矩阵形式

上面只是算了「它」一个词。实际上所有词同时做,就是一次矩阵乘法:

              ┌           ┐
Attention  =  │ Q  Kᵀ     │
              │ ─────  V  │     ← 一行公式,就是上面五步
              │  √d_k     │
              └           ┘

Scores = Q·Kᵀ                (L, L)   ← 所有词对所有词的分数矩阵
Weights = softmax(Scores)    (L, L)   ← 每行和为 1,行 = "我在看谁"
Output = Weights · V         (L, d)   ← 每个词的新表示

那个 (L, L) 的分数矩阵,就是可以被可视化的注意力热力图——横轴是”被看的人”,纵轴是”看的人”。

4.4 一个关键细节:为什么要除以 √d_k?

这是面试和工程里的高频问题,也是理解”为什么 Transformer 能训得动”的钥匙。

推导(很简单):假设向量各分量独立、均值 0、方差 1,则点积

q·k = q₁k₁ + q₂k₂ + ... + q_dk_d

是 d 个独立项的和,所以它的方差 = d_k,即标准差 = √d_k。

真实情况:d_k = 64 → 标准差 = 8。也就是说分数典型落在 ±8 这个范围,甚至更大。

不除会怎样? softmax 对大数极其敏感(指数级放大):

分数 [8, -3]  →  e^8 = 2981,  e^-3 = 0.05  →  softmax ≈ [1.000, 0.000]

分布被推成了 one-hot。而 one-hot 的 softmax 梯度几乎为 0——梯度消失,模型训练初期根本学不动。

除以 √d_k 之后,方差被拉回 1,softmax 待在”温和”的区域,梯度健康。

一句话记忆:分数会因维度升高而”膨胀”,不缩放就会把 softmax 压成 one-hot,导致梯度消失。除以 √d_k 就是给分数做标准化。

4.5 Self-Attention 的真面目:每个词都做一次上面的事

回看 4.3 的例子,我们只算了「它」一个词。而 Self-Attention 是对每个词都做一遍:

猫   → 看看全场 → 更新自己(可能重点看了"追":我是动作发起者)
追   → 看看全场 → 更新自己(可能重点看了"猫"和"老鼠")
老鼠 → 看看全场 → 更新自己(可能重点看了"追":我是动作承受者)
它   → 看看全场 → 更新自己(重点看了"猫")

而且这四件事是完全并行的一次矩阵乘法,不是循环四次。这就是 Transformer 快的根本原因。

“Self(自)“的含义:Q、K、V 全部来自同一个序列自己——自己查自己。与之相对的 Cross-Attention,K、V 来自别的序列。

4.6 Multi-Head Attention:为什么要”多头”?

单头 Attention 有个隐患:一次注意力只能表达一种相关关系。但语言里的关系是多维的:

  • 「它」要看「猫」——这是指代关系
  • 「追」要看「猫」「老鼠」——这是语法依存关系
  • 「的」要看前一个词——这是修饰关系

挤在一个空间里,这些关系会互相干扰。于是有了多头:把 512 维切成 8 份,每份 64 维,在 8 个独立的子空间里各做一次注意力,最后拼起来。

graph LR
    X((输入 x)) --> H1["头 1<br/>子空间 1"]
    X --> H2["头 2<br/>子空间 2"]
    X --> H8["头 8<br/>子空间 8"]
    H1 --> C["拼接<br/>Concat"]
    H2 --> C
    H8 --> C
    C --> WO["W_O 线性混合"] --> OUT((输出))

三个必须知道的要点:

要点说明
不增加计算量每头维度是 d_model / h,8 个 64 维加起来 = 512 维,FLOPs 与单头基本一致。多头是免费的午餐。
像多组卷积核和 CNN 用多个卷积核提取不同特征完全同构——多视角看同一份输入
头会自发分工研究者可视化后发现,不同头确实学到了不同模式:有的专盯下一个词,有的专盯句首,有的专做指代消解

一个直觉:单头 Attention 像一个人开会,多头像 8 个人同时开 8 个专题会。 会后把 8 份纪要拼接汇总,信息自然比一份纪要丰富。

4.7 Transformer 里的三种注意力(一句话区分)

类型谁在提问(Q)查谁的字典(K/V)可否看未来用在哪
Encoder Self-Attention源句的每个词源句自己✅ 双向全看理解源句
Decoder Masked Self-Attention已生成内容的每个词已生成内容自己❌ 戴眼罩建模已写部分
Encoder-Decoder Cross-Attention解码中的每个位置Encoder 的输出—把源句信息注入生成

Cross-Attention 是这个架构里最精妙的设计,值得多说一句:

它把”理解”和”生成”彻底解耦成两个独立阶段,中间只靠一个注意力层连接。这个模式后来成了**“条件注入”的通用接口**——图像生成模型 Stable Diffusion 用它注入文本提示,多模态模型用它注入图像特征。凡是”在 A 的条件下生成 B”,都可以用 Cross-Attention 实现。

4.8 复杂度对比:论文那张表的现代解读

层类型信息通路长度每层计算量串行操作数
Self-AttentionO(1)O(n² · d)O(1)
RNNO(n)O(n · d²)O(n)
CNNO(log n)O(k · n · d²)O(1)

三个结论,第三个最反直觉:

  1. 并行性:Self-Attention 串行操作数 O(1),RNN 是 O(n) → 这就是训练速度差一个数量级的根源。
  2. 通路长度:Self-Attention O(1),任意两位置一步直达 → 这就是长程建模能力的根源。
  3. 其实更省算力:当 n < d(现代 LLM 的典型情况:上下文几千,模型维度几千到上万)时,n²·d < n·d²,Self-Attention 的计算量反而比 RNN 小。

也就是说:Transformer 不只是”并行更快”,在”中等长度序列 + 大模型”这个主战场上,它的绝对计算量也更优。只有当 n 极大时,O(n²) 才真正成为负担。

4.9 最小可读实现(对着 4.3 的例子看,逐行都能对上)

import math
import torch
import torch.nn.functional as F

def attention(Q, K, V, mask=None):
    """
    Q: (B, h, L_q, d_k)   查询
    K: (B, h, L_k, d_k)   键
    V: (B, h, L_k, d_v)   值
    """
    d_k = Q.size(-1)

    # ① 打分:每个 q 与所有 k 点乘 → (B, h, L_q, L_k)
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

    # ② 掩码:把不该看的位置填 -∞(因果掩码 / padding)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))

    # ③ 归一化:每行变成和为 1 的权重
    weights = F.softmax(scores, dim=-1)

    # ④ 加权求和:混合所有 V
    return torch.matmul(weights, V), weights   # 输出 + 权重(可用于可视化)


class MultiHeadAttention(torch.nn.Module):
    def __init__(self, d_model=512, h=8):
        super().__init__()
        self.d_k = d_model // h        # 每头 64 维
        self.h = h
        self.W_Q = torch.nn.Linear(d_model, d_model)   # 生成 Q
        self.W_K = torch.nn.Linear(d_model, d_model)   # 生成 K
        self.W_V = torch.nn.Linear(d_model, d_model)   # 生成 V
        self.W_O = torch.nn.Linear(d_model, d_model)   # 拼接后混合

    def forward(self, query, key, value, mask=None):
        B = query.size(0)
        # 投影 → 切分成 h 个头: (B, L, 512) → (B, h, L, 64)
        Q = self.W_Q(query).view(B, -1, self.h, self.d_k).transpose(1, 2)
        K = self.W_K(key).view(B, -1, self.h, self.d_k).transpose(1, 2)
        V = self.W_V(value).view(B, -1, self.h, self.d_k).transpose(1, 2)

        out, w = attention(Q, K, V, mask)

        # 拼回头: (B, h, L, 64) → (B, L, 512)
        out = out.transpose(1, 2).contiguous().view(B, -1, self.h * self.d_k)
        return self.W_O(out), w

代码与前面章节的对应关系:

代码行对应章节
matmul(Q, Kᵀ)4.3 Step 2 打分
/ sqrt(d_k)4.4 缩放,防 softmax 饱和
masked_fill(-inf)3.4 因果掩码(戴眼罩写作)
softmax4.3 Step 4 归一化
matmul(weights, V)4.3 Step 5 加权求和
view / transpose4.6 多头切分与拼接

五、总结:架构演进的底层逻辑

5.1 全景对比表

维度RNNLSTMGRUTransformer
核心机制隐状态循环传递三门控 + 细胞状态双门控 + 线性插值Self-Attention
时间维度并行❌ O(n) 串行❌ O(n) 串行❌ O(n) 串行✅ O(1) 并行
最长信息通路O(n)O(n)(但梯度可无损)O(n)O(1)
每层复杂度O(n·d²)O(n·d²),常数 ×4O(n·d²),常数 ×3O(n²·d) + O(n·d²)
长程依赖极弱较强较强强
可堆叠深度2–4 层2–4 层2–4 层数十至上百层
可解释性差差差注意力权重可可视化
可扩展性差差差极强(Scaling Law 成立)
主要短板梯度消失慢、参数多记忆容量有限O(n²) 复杂度、KV Cache 显存

5.2 三条架构洞察

洞察一:架构演进的本质是”缩短信息通路”

从 RNN 的 O(n) 串行传递,到 Seq2Seq+Attention 的”跳读”,再到 Transformer 的 O(1) 全连接,每一次质变都是信息通路的缩短。

背后的原理很朴素:梯度与信息在长路径上必然衰减。谁能把通路做短,谁就能训练得更深、更大、更好。这条线索同样解释了 ResNet 的成功(残差连接为梯度开了一条捷径)——ResNet 与 Transformer 用的是同一个思想:提供捷径。

洞察二:并行化能力决定了架构能吃到多少算力红利

RNN 不是被”效果”打败的,而是被算力利用率打败的。在 GPU 算力指数增长的 2010 年代,一个无法并行的架构,无论理论上多优雅,都注定被时代抛下。

Transformer 的真正历史功绩是:它让模型首次能够”吃下”整个数据中心的算力。这也是为什么 Scaling Law 能在 Transformer 上成立,而在 RNN 上从未真正成立过。

洞察三:好的架构 = 可组合的简单模块

Transformer 的每个组件都不复杂:矩阵乘、softmax、残差、LayerNorm、ReLU。但它通过统一维度(保形)+ 残差连接 + 模块化封装,实现了极强的可组合性:

  • 编码器与解码器可以拆开单独用(BERT / GPT);
  • 层数、宽度、头数可以自由缩放;
  • 注意力可以替换(稀疏、线性、MoE),位置编码可以替换(RoPE、ALiBi);
  • 跨模态只需加一个 Cross-Attention 分支。

这种”乐高式”的可组合性,才是 Transformer 能统一 NLP、CV、语音、多模态的深层原因。

5.3 结语

回顾这段历程:

符号主义(人写规则)
    ↓  知识获取瓶颈
统计学习(数据驱动 + 人工特征)
    ↓  特征工程瓶颈
深度学习(表征学习 + RNN/CNN)
    ↓  并行化与长程依赖瓶颈
Transformer(注意力 + 并行 + 可堆叠)
    ↓  算力与长上下文瓶颈
稀疏化 / 混合架构(MoE、Mamba、长上下文技术……)

RNN 解决了”如何处理序列”,LSTM 解决了”如何记住久一点”,而 Transformer 解决的是”如何让模型规模化”——最后一个问题的答案,最终把人工智能推入了大模型时代。

架构没有终局,只有取舍。 今天我们在超长序列上重新审视 O(n²) 的代价,就像 2017 年人们重新审视 O(n) 串行一样。理解这套取舍逻辑,比记住任何一个具体架构都更重要。


参考与延伸阅读

主题文献
RNN / BPTTRumelhart et al., Learning representations by back-propagating errors, 1986
LSTMHochreiter & Schmidhuber, Long Short-Term Memory, 1997
GRUCho et al., Learning Phrase Representations using RNN Encoder-Decoder, 2014
Seq2SeqSutskever et al., Sequence to Sequence Learning with Neural Networks, 2014
Attention 雏形Bahdanau et al., Neural Machine Translation by Jointly Learning to Align and Translate, 2014
TransformerVaswani et al., Attention Is All You Need, NeurIPS 2017
BERTDevlin et al., BERT: Pre-training of Deep Bidirectional Transformers, 2018
FlashAttentionDao et al., FlashAttention: Fast and Memory-Efficient Exact Attention, 2022
RoPESu et al., RoFormer: Enhanced Transformer with Rotary Position Embedding, 2021
MambaGu & Dao, Mamba: Linear-Time Sequence Modeling with Selective State Spaces, 2023

评论