Transformer 回顾
第 1 课:Transformer 回顾
用你熟悉的 BERT 作为锚点,一步步理解 Self-Attention 做了什么,以及为什么 GPT 必须走 Decoder-only 路线。
② 说清为什么生成任务必须用 Causal Mask,以及它怎么把 BERT 和 GPT 分成两条路
③ 向别人解释「RNN 到底哪里不行」并用具体数字举例,而不是背概念
先看一个任务:语言建模(language modeling)——给定前文预测下一个 token(分词后的最小单位,可以是字、词或 subword)。用「我把昨天在图书馆借的那本书还了」走一遍:句首的"把"和句尾的"还"构成同一个"把…还了"结构,两者相隔 12 个字("把"在第 2 位、"还"在第 14 位)——信息必须跨过这 12 个位置传过来。① 相隔任意远的两个位置必须能直接交换信息,即建模长距离依赖;② 不能按时间步串行计算,必须可并行。这两条就是我们要满足的任务约束。
先用 RNN(循环神经网络)撞一下墙。它的工作方式:从左到右,一个词接一个词地读。"我" → 更新隐藏状态 → "把" → 更新隐藏状态 → ...(隐藏状态 = RNN 用来记住"前面所有词"的那个向量。)RNN 的隐藏状态每步都被整体重写(像只用一张便签纸记笔记),读到句尾开头的信息早被擦没了;而且它必须按时间步一个个算。这两点正好同时踩中上面两条约束。在 2017 年之前,处理句子的主力就是 RNN 及其门控版本 LSTM。
打个比方:RNN 读句子像一个人只用一张便签纸做笔记——每读一个词就把便签擦掉重写,便签上只剩当前这一行;读到句尾,开头记的细节早被擦没了。后面所有"遗忘"和"消失"的毛病,都源于这一张便签。
RNN 的两个致命问题(一前一后):
① 前向:hidden state 被反复覆盖,远处信息留不住。每读一个新词,整个隐藏向量就被重写一次——像一张便签纸擦了又写,读到句尾时开头的细节早被挤没了。这踩中了第一条约束(长距离依赖)。
② 反向:梯度消失,开头学不到。训练时要反向传播(把误差从输出端往回传、据此调参数)。梯度沿时间步往回传,每传一步就乘一个小于 1 的数,传到开头几乎为零——模型根本看不到句首在说什么。这进一步加剧了远距离遗忘:即便信息勉强传到句尾,梯度也回不到句首去调参数。
③ 串行慢。第 100 个词必须等前 99 个词全算完。GPU 有几千个核心,RNN 的时间步却只能一个一个来——并行度用不上。这踩中了第二条约束(必须可并行)。
| 步数 | 梯度残余 | 含义 |
|---|---|---|
| 1 | 0.9 | 还行 |
| 10 | 0.9¹⁰ = 0.35 | 不到一半了 |
| 50 | 0.9⁵⁰ = 0.005 | 几乎为零,模型学不到任何东西 |
现在我们知道必须同时满足:任意两位置直连 + 计算能并行。
下一个问题是:这两条要求,具体该用一个什么样的算式实现?
上一张卡片看到 RNN 的 hidden state 每读一个词就被整体覆盖一次,开头的信息留不住。要解决它,最直接的做法是让每两个位置之间都算一个"像不像"的分数——n 个 token 就是 n×n 张分数表。
Self-Attention 的核心:让每个词都去"查"一遍所有其他词,看它们和自己有多相关。
用图书馆打个比方:Q 是你手里的索书号,K 是每本书书脊上的分类标签,V 是书页里的正文——拿 Q 去和每一张 K 比对,谁最像就把谁的 V 借走。句子里的每个词同时扮演这三种角色。
Q(Query) = "我在找什么?" K(Key) = "我有什么可以提供?" V(Value) = "如果选了我,我给你什么信息?"
为什么不直接用词向量本身两两算点积、非要投影出 Q、K?因为词向量的相似度是固定的——不管任务是翻译还是问答,「苹果」和「甜」的分数都一样,模型没法按任务调;投影成 Q、K 之后,同一对词的分数可以被训练成任意大小(本例 Q_我·K_吃 = 0.16 就是这样算出来的)。
| 词 | d₁ | d₂ | d₃ |
|---|---|---|---|
| "我" | 0.5 | 0.3 | 0.8 |
| "吃" | 0.2 | 0.9 | 0.4 |
| 词 | Q | K | V |
|---|---|---|---|
| "我" | [0.6, 0.2] | [0.4, 0.7] | [0.8, 0.3] |
| "吃" | [0.3, 0.9] | [0.1, 0.5] | [0.6, 0.2] |
0.75 0.48
关键洞察:所有词的计算是同时进行的——这就是为什么 Self-Attention 能并行:串行步数从 RNN 的 O(n) 降到 O(1)。总计算量并不一定更小——原论文 Table 1 给出每层复杂度,Self-Attention 是 O(n²·d),RNN 是 O(n·d²):n 小于 d 时前者更省,n 远大于 d 时反而更贵(例如 d=512:n=100 时前者约为后者的 0.2,n=1000 时约为 1.95 倍)。
Q(Query) = 每个词在问"谁和我最相关?"
K(Key) = 每个词在答"我是什么,来找我"
V(Value) = "如果真的选了我,我给你这些信息"
这种"查询-索引-取值"的三步机制,让 Self-Attention 的每条信息路径长度恒为 1——不管两个词距离多远,梯度都能一步直达。这就是它在"长距离依赖"上明显优于 RNN 的核心原因。
以原论文 base 的每头维度 d_k=64(d_model=512、8 个头)为例,两个独立随机向量的点积,标准差是 √d_k=√64=8(均值为 0);每头维度越大,点积的方差越大。不缩放的话,Softmax 的输入量级太大 → 输出极度尖锐(≈one-hot)→ 梯度趋于零,模型学不动。
| 两个分数 | Softmax 权重 | 较大那项的梯度 p(1−p) |
|---|---|---|
| [1, 0](缩放后,量级≈1) | [0.73, 0.27] | 0.197 |
| [8, 0](不缩放,量级≈8) | [0.9997, 0.0003] | 0.000335,缩小约 587 倍 |
前提是 q、k 各分量独立、均值 0、方差 1,点积方差才恰好是 d_k。除以 √d_k 把量级拉回 1 附近。这就是一根"保险丝"——d_k 小时无害(本卡走查的小例子里 d_k=2),统一保留。
上一张卡片算出了一张 n×n 的分数矩阵(attention 分数)。但生成有个前提:模型从左往右吐词,生成"我"的时候"爱"还不存在。所以矩阵里"列号大于行号"的格子(上三角)必须作废——填成 -∞,Softmax 之后自然是 0。
这就像闭卷考试:练习时若允许翻书(看到未来),上考场却只能凭记忆,成绩必然崩,所以练习就得按考场规则来。
训练一个"理解"任务时,双向注意力没问题——"苹果很甜"中,"甜"确认了"苹果"是水果。
但生成时完全不同。模型从左往右逐 token 生成,生成"我"的时候,"爱"还不存在。如果训练时允许模型"偷看"未来 token,因为整句话是并行喂进去的,每个位置都能直接读到自己要预测的答案,学到的只是照抄;上线做生成时,右边没有答案可抄,这个本事就用不上了。
| "我" | "爱" | "你" | |
|---|---|---|---|
| "我" | 0.5 | 0.3 | 0.1 |
| "爱" | 0.2 | 0.6 | 0.4 |
| "你" | 0.1 | 0.3 | 0.7 |
| "我" | "爱" | "你" | |
|---|---|---|---|
| "我" | 0.5 | -∞ | -∞ |
| "爱" | 0.2 | 0.6 | -∞ |
| "你" | 0.1 | 0.3 | 0.7 |
| "我" | "爱" | "你" | |
|---|---|---|---|
| "我" | 1.0 | 0 | 0 |
| "爱" | 0.40 | 0.60 | 0 |
| "你" | 0.25 | 0.30 | 0.45 |
同一个架构,只因"要不要挡未来",就分成两条路:不挡未来的叫 BERT(双向 Encoder),必须挡的叫 GPT(单向 Decoder)。下一张卡片会把这两条路的训练方式、分工逐项对比。
填 0 → Softmax 后 = e⁰/(...) ≠ 0,被 mask 的词仍有残留权重。
填 -∞ → Softmax 后 = e^(-∞)/(...) = 0/(...) = 0,真正"完全忽略"。
上一张卡片看到:Causal Mask 在分数矩阵的上三角填 -∞,Softmax 之后变成 0——每个词只能看到自己和左边的词。这个"挡不挡未来"的选择,让同一个 Transformer 自然分成了两条路线:因为训练时能看到什么,决定了模型学会做什么,所以训练目标不同,擅长的任务就不同。
就像两种考生的分工:一个负责阅读理解(前后文一起看,判断"苹果很甜"是不是在说水果),一个负责续写作文(只能看已经写出来的部分)。前者对应 BERT,后者对应 GPT;真正的区别在下面这张表里的训练方式。
| Encoder-only(BERT) | Decoder-only(GPT) | |
|---|---|---|
| 注意力(attention) | 双向(看全部) | 单向(←只看左边) |
| 训练目标 | MLM:随机遮15%的词,猜被遮住的是什么 | 自回归:预测下一个词 |
| 擅长 | 理解:分类、实体识别、语义匹配 | 生成:续写、翻译、对话、写代码 |
| 代表模型规模 | BERT-base 110M、BERT-large 340M(2018) | GPT-3 175B(2020);两者差距主要来自年代与训练预算,不是架构本身 |
为什么 BERT 不适合直接生成?
BERT 训练时习惯看到整句再判断。让它生成,就只能从左往右逐 token 地来——而 BERT 从来没被训练过"在只知道前半句的情况下猜下一个词"。
GPT 从预训练第一天起就在做这件事:只看左边,猜右边。这不是能力高低,而是训练范式决定的分工。
| 步骤 | 输入 | 预测 | 可用信息 |
|---|---|---|---|
| 1 | 苹果 | 很 | 仅"苹果"(左边 1 个词) |
| 2 | 苹果 很 | 甜 | 仅"苹果 很"(左边 2 个词) |
| 被监督的位置数 | 怎么算 | |
|---|---|---|
| BERT(MLM) | 约 6 个 | 遮 15%:40 × 0.15 = 6 |
| GPT(自回归) | 39 个 | 除第 1 个词外,每个位置都在预测它的下一个词:40 − 1 = 39 |
因为 Embedding 本质是理解任务——要把整段商品描述压缩成一个向量。双向注意力能同时利用上下文所有信息,而且 BERT-base 只有 110M 参数,比动辄 1B 以上的 Decoder-only 模型小得多,在需要大批量编码的召回场景里算得起。
而对话产品需要逐 token 生成回答,用的是 Decoder-only。没有谁更好,只是服务于不同目的。
上一张卡片看到 BERT 与 GPT 的差别在"能看到哪些位置";但前几张卡片算的一直是单个头的注意力:一个词对其他词只有一组 attention 权重,所有关系被迫压进同一张矩阵。(BERT 与 GPT 的每一层其实都是多个头并行,本卡就来拆开看为什么。)"苹果"和"甜"(属性)、"苹果"和"买"(动作)、"苹果"和"斤"(数量)是三种异质关系,一组权重表达不了。
就像多个人同时读同一句话:一个人盯语法、一个人盯语义、一个人盯数量,最后把各自的笔记合并成一份更完整的理解。
"苹果很甜,我昨天买了三斤"——"苹果"既和"甜"有关(属性),又和"买"有关(动作),还和"斤"有关(数量)。一个头很难同时捕捉三种异质关系。
那能不能干脆不分组,只把单头加宽(每头维度由 64 提到 128)?不能——加宽只是把同一张打分表放大,Softmax 之后仍然只有一行权重;要同时表达「属性 / 动作 / 数量」三套彼此独立的分布,就必须真的开出三个头,让它们各持一张打分表。
| 苹果→ | 苹果(自) | 甜(属性) | 买(动作) | 斤(数量) |
|---|---|---|---|---|
| 权重 | 0.18 | 0.22 | 0.27 | 0.33 |
| 头 | 功能 | 苹果(自) | 甜(属性) | 买(动作) | 斤(数量) |
|---|---|---|---|---|---|
| #1 | 属性 | 0.10 | 0.55 | 0.15 | 0.20 |
| #3 | 动作 | 0.08 | 0.10 | 0.62 | 0.20 |
| #6 | 数量 | 0.05 | 0.08 | 0.15 | 0.72 |
每个 head 都要存一份 K/V,头数越多,生成时要缓存的 K/V 越大(下一张卡片会看到这笔账)。GQA(Grouped Query Attention,分组查询注意力)的做法是:保留全部 Q 头,但让每组 Q 头共享同一份 K/V。若 Q 头有 h 个、K/V 分成 g 组,缓存就降为原来的 g/h。GQA 论文(Ainslie et al., 2023)报告:已训练好的 MHA 模型,只花 5% 的原训练算力就能改成 GQA。改完后质量接近 MHA,速度接近 MQA(Multi-Query Attention,所有 Q 头只共用 1 组 K/V)。
旁证是头确实有冗余:Voita 等人 2019 年在英俄翻译模型的 encoder 上剪头,48 个剪掉 38 个,BLEU(机器翻译标准评测指标,分数越高越好)只掉 0.15——但那是整个头被删掉,GQA 是保留所有 Q 头、只共享 K/V,二者机制不同,这里只能说"头冗余使共享成为可能"。Llama 2 70B 起、Llama 3 全系列及 Mistral、Qwen 采用 GQA;Llama 2 的 7B/13B 仍是 MHA(Multi-Head Attention,标准多头注意力)。
上一张卡片看到每一层有多个 head、每个 head 一组 K/V。进入生成阶段后,每一步都要用到前面所有 token 的 K/V——如果每一步都从头算一遍,同样的 K/V 就被反复重算。
| 步数 | 当前输入 | 预测 | 概率 | 本步 K/V 投影量(不含Cache) |
|---|---|---|---|---|
| 1 | [BOS] | 我 | 0.32 | 1×d |
| 2 | [BOS,我] | 爱 | 0.41 | 2×d |
| 3 | [BOS,我,爱] | 吃 | — | 3×d |
| 4 | [BOS,我,爱,吃] | 苹果 | — | 4×d |
| 5 | [BOS,…,苹果] | EOS | — | 5×d |
关键洞察:把已经算过的 Key 和 Value 存下来、下一步直接复用,这就是 KV Cache(Key-Value Cache,键值缓存)。每一步只需要为新 token 算一组新的 K/V,再和缓存里的旧 K/V 拼起来做注意力。打个比方,就像读书时在书页上做过的批注不用每翻一页都重做一遍;回到术语,缓存下来的就是前面所有 token 的 K 和 V。
它之所以成立,靠的是 Causal Mask:旧 token 看不到未来,所以新 token 的出现不会改变旧 token 的 K/V,缓存下来的值永远有效。如果像 BERT 那样双向可见,新增一个词会改变所有旧词的表示,缓存就失效了。
BERT 做 Embedding = encode 一次,拿向量就用。GPT = 逐 token 生成。
但 GPT 也能做 Embedding——最朴素的做法是取最后一个 token 的 hidden state 作为整个序列的表示;而真正把 Decoder-only 改造成 Embedding 模型的 LLM2Vec、GTE-Qwen、NV-Embed 都各自做了额外改造(LLM2Vec 加双向注意力改造 + 池化,GTE-Qwen 走指令微调,NV-Embed 用 latent 池化——都不是单纯"取最后一个 token")。(注意区分:E5 原始版基于 BERT 式双向编码器,BGE 基于 BERT 编码器 + CLS 池化,都不是 Decoder-only 取 last-token 的做法。)Decoder-only 正在"侵蚀"原本属于 BERT 的领地。