深入拆解 Transformer:从注意力到完整架构
很多 Transformer 教程一上来就把整张架构图拍到你脸上,然后默认你已经知道图里的每根箭头在干什么。
我第一次看 Transformer 也是这样:Q、K、V 的类比似乎听懂了,合上文章,还是说不清一个词到底怎样穿过模型。后来发现,问题出在学习顺序。完整架构应该是最后拼起来的东西,不该成为第一张考卷。
所以这次换一种讲法。全文只反复使用两个例子:
例子一:小猫没追上汽车,因为它跑得太快了。
用它拆开自注意力、Q/K/V、多头和 Encoder。
例子二:我爱猫 → I love cats
用它串起 Encoder、Decoder、训练和逐词生成。
位置编码需要观察一串连续数字,中间会临时换成一个更方便列表计算的短句,其余章节都尽量回到这两条主线。
先不谈多头,也不背公式。我们会先看清一个 token 怎样向其他 token 取信息,再把它装进 Encoder,最后看 Decoder 怎样一个词一个词生成结果。公式和矩阵会等具体例子走通之后再出现。
先别看细节:翻译到底分几步
把 Transformer 当成两个配合工作的团队:
- **Encoder(编码器)**阅读完整的中文句子,为每个 token 写一份结合上下文的说明;
- **Decoder(解码器)**拿着这些说明,根据已经生成的英文,预测下一个英文 token。
大致过程是:
“我 爱 猫”
↓ 分词、查表,变成三组向量
Encoder 反复整理上下文
↓
得到“我”“爱”“猫”各自的上下文表示
↓
Decoder 先看到 <BOS>,预测 I
Decoder 再看到 <BOS> I,预测 love
Decoder 再看到 <BOS> I love,预测 cats
Decoder 最后预测 <EOS>,停止
这里的 <BOS> 可以理解为“开始生成”,<EOS> 表示“句子结束”。
原版 Transformer 的完整结构如下。现在不用硬看懂,只需要认出左边是 Encoder、右边是 Decoder,中间那条线把源句信息送给 Decoder。后面我们会把图里的方块逐个解释。

第一步:文字怎样进入模型
token 不是“一个单词”的正式叫法
模型接收的是数字,不认识“我爱猫”这三个字。Tokenizer 会先把文字切成 token,再把 token 换成整数 id。
为了方便,这篇文章假设它正好被切成:
[“我”, “爱”, “猫”]
真实情况不一定这么整齐。一个英文单词可能被拆成几个子词,一个中文词也可能对应多个 token。token 是分词器处理文本的基本单位,不保证等于字或单词。
接下来,Embedding 层按照 id 查表,把每个 token 换成一个向量。可以暂时把向量想成一张写满数字的小纸条:
“我” → [ 0.2, -0.7, 0.1, ...]
“爱” → [-0.4, 0.3, 0.8, ...]
“猫” → [ 0.6, 0.1, -0.2, ...]
这些数字没有人工填写的含义标签,它们是训练出来的参数。刚初始化时它们近乎随机;训练久了,模型逐渐把有用的语言特征编码进去。
光有 token 向量还不够
自注意力会比较“有哪些 token”,却天然不知道谁在前、谁在后。假如没有位置信息,“猫爱我”和“我爱猫”只是在输入行的排列上不同,模型没有额外线索理解语序。
原论文给每个位置生成一组固定的正弦和余弦数值,再与 token embedding 逐元素相加:
最终输入 = token embedding + position encoding
这里很容易产生一个误会:“高频位置”和“低频位置”分别属于不同 token 吗?其实不是。频率属于位置向量的维度,每个 token 都会同时拿到高频、中频和低频分量。 token 之间的区别,只是它们在不同的 pos 上读取这些波形。
原论文的公式是:
$$ PE_{(pos,2i)}=\sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) $$$$ PE_{(pos,2i+1)}=\cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) $$先别被下标吓到。把正弦波写成 $\sin(\omega pos)$,$\omega$ 就表示变化速度:$\omega$ 越大,波动越快,频率越高。位置编码中:
$$ \omega_i=\frac{1}{10000^{2i/d_{model}}} $$$i$ 越大,分母越大,$\omega_i$ 越小,所以靠后的维度变化得更慢。这就是高频和低频的判断依据。
放进一句话里看
假设一句话被切成 8 个 token:
位置: 0 1 2 3 4 5 6 7
token:小明 把 一本 有趣 的 书 送给 小红
为了让数字还能手算,假设 $d_{model}=8$,暂时只展示位置编码的前两组 sin/cos:
第 0、1 维:[sin(pos), cos(pos)] 变化快
第 2、3 维:[sin(pos/10), cos(pos/10)] 变化慢
各个 token 在自己位置上读到的数值大约是:
| 位置 | token | 高频 sin | 高频 cos | 低频 sin | 低频 cos |
|---|---|---|---|---|---|
| 0 | 小明 | 0.000 | 1.000 | 0.000 | 1.000 |
| 1 | 把 | 0.841 | 0.540 | 0.100 | 0.995 |
| 2 | 一本 | 0.909 | -0.416 | 0.199 | 0.980 |
| 3 | 有趣 | 0.141 | -0.990 | 0.296 | 0.955 |
| 4 | 的 | -0.757 | -0.654 | 0.389 | 0.921 |
| 5 | 书 | -0.959 | 0.284 | 0.479 | 0.878 |
| 6 | 送给 | -0.279 | 0.960 | 0.565 | 0.825 |
| 7 | 小红 | 0.657 | 0.754 | 0.644 | 0.765 |
看相邻的“有趣”和“的”。高频组从 [0.141, -0.990] 变到 [-0.757, -0.654],变化很明显;低频组只从 [0.296, 0.955] 变到 [0.389, 0.921]。两组都随着位置变化,只是速度不同。
为什么不全部使用高频?因为高频波很快就会绕一圈。sin(pos) 和 cos(pos) 大约每 $2\pi\approx6.28$ 个位置重复一个周期,位置 0 和位置 6 的高频读数已经有些接近。低频组的周期约为 $2\pi\times10\approx62.8$ 个位置,这时还能明显看出两者处在不同区段。
反过来,如果全部使用低频,相邻 token 的数值变化又太小。多种频率放在一起,有点像里程表的个位、十位和百位:个位变化快,却会不断归零;百位变化慢,却看不出很短的移动。把它们一起读,位置才更容易区分。
所以,对位置 3 的“有趣”来说,它拿到的不是一个“高频编码”或“低频编码”,而是一整组读数:
位置 3 的编码:
[
sin(3), cos(3), ← 高频
sin(3/10), cos(3/10), ← 较低频
sin(3/100), cos(3/100), ← 更低频
sin(3/1000), cos(3/1000) ← 最低频
]
这组位置向量再与“有趣”的 token embedding 相加。句子中每个 token 使用的是同一组频率,只是 pos 不同,最终读数也不同。
下图把更多位置和更多维度放在一起。每一行对应一个位置,每一列对应位置向量的一个维度;左侧维度变化快,右侧维度变化慢。

现在可以把位置编码理解为:它告诉模型“你是‘猫’,并且你位于第 3 个位置”,但不是额外塞进去一个数字 3,而是加上一组不同速度的波形读数。后来的模型常改用可学习位置向量或 RoPE,要解决的仍是顺序和距离信息。
走到这里,每个 token 已经有了两类信息:Embedding 大致说明“我是谁”,位置编码说明“我在哪里”。但这些 token 仍然各拿着自己的小纸条,彼此还没有交换内容。“它”究竟指小猫还是汽车,单靠身份和位置仍然判断不了。下一步要解决的,就是怎样让一个 token 读取其他 token 的信息。
第二步:为什么一个词必须看其他词
单独看一个词,它的意思经常不完整。
苹果发布了新手机。
这里的“苹果”指公司。换成“苹果熟了,从树上掉下来”,“苹果”又变成了水果。词还是同一个词,周围内容变了,我们对它的理解也跟着变。再看一个指代关系的例子:
小猫没追上汽车,因为它跑得太快了。
“它”更可能指汽车。换成“因为它太累了”,“它”又更可能指小猫。
这就是上下文的作用。Embedding 刚查表时,同一个 token 拿到的还是相近的初始向量;位置编码虽然补充了顺序,也不会自动把“小猫”“汽车”和“它”的含义联系起来。模型还需要一个实际的信息交换机制,让“它”能够读取句中其他 token,再更新自己的表示。
这个机制就是注意力。名字听上去有些抽象,落到当前例子里,它做的事情其实很朴素。
让“它”去句子里找信息
继续用这句话:
小猫 没 追上 汽车,因为 它 跑得 太快了。
为了便于观察,先粗略切成:
[小猫, 没, 追上, 汽车, 因为, 它, 跑得, 太快, 了]
现在轮到“它”更新自己的表示。只看“它”这个 token,我们不知道它指谁;模型需要让它和句中所有位置逐个比较。可以把这次查询翻译成人话:
“它”想找:前面哪个对象最可能是“跑得太快”的主体?
模型不会真的生成这句中文问题。实际发生的是:“它”的向量经过投影变成 Query,然后与每个 token 的 Key 做点积。假设某个注意力头产生了下面这组教学用权重:
| 提供信息的 token | 小猫 | 没 | 追上 | 汽车 | 因为 | 它 | 跑得 | 太快 | 了 |
|---|---|---|---|---|---|---|---|---|---|
| “它”分配的权重 | 0.08 | 0.02 | 0.08 | 0.42 | 0.04 | 0.10 | 0.11 | 0.12 | 0.03 |
权重总和是 1。其中“汽车”得到 0.42,表示这个头在更新“它”时,会从“汽车”所在位置取回较多信息;“跑得”和“太快”也获得一些权重,为“它”补充当前谓语和状态。最后,“它”的新表示是所有 Value 按这些权重混合后的结果:
“它”的新表示
= 0.08 × Value(小猫)
+ 0.02 × Value(没)
+ 0.08 × Value(追上)
+ 0.42 × Value(汽车)
+ ……
所以注意力并不是从句子里硬选一个词,也不会把“汽车”原样复制到“它”上。它把多个位置的信息按比例混合,只是在这个示例中“汽车”占得最多。换成“因为它太累了”,训练良好的某些头可能会给“小猫”更高权重;但真实模型的注意力往往没有这么整齐,这里只是为了看清计算过程。
Q、K、V 在这句话里分别做什么
现在再看 Query、Key、Value,就不用只靠搜索引擎的类比了:
- “它”的 Query:表达当前这个位置想找什么,例如“哪个对象适合作为当前指代对象”;
- 每个 token 的 Key:表达自己有哪些特征可供匹配,例如“汽车”是前文出现的名词和可运动对象;
- 每个 token 的 Value:保存匹配后真正要传回的内容,例如“汽车”在当前上下文中的语义表示。
这几句话仍然有点像定义。下面干脆打开 Q、K、V,看一眼里面的数字可能怎样参与计算。
把 Q、K、V 打开看一眼
真实模型中的向量可能有几十甚至上百维,而且单个维度通常没有“第 3 维就是名词”这种稳定、可直接命名的含义。为了能手算,我们虚构一个只有 3 个匹配维度的注意力头。假设这是 Encoder 较后面的一层,前面的层已经给“它”融入了一些“跑得太快”的上下文。
先把 Q/Key 所在的匹配空间临时解释成:
第 1 维:是否像一个可被指代的前文实体
第 2 维:是否能够运动
第 3 维:是否适合与“跑得太快”搭配
于是,“它”的 Query 可能是:
Query(它) = [0.9, 0.8, 1.0]
↑ ↑ ↑
找实体 能运动 适合“跑得太快”
句中几个候选位置的 Key 假设为:
Key(小猫) = [1.0, 0.7, 0.5]
Key(汽车) = [1.0, 1.0, 0.95]
Key(太快) = [0.0, 0.2, 0.3]
现在用“它”的 Query 分别与这些 Key 做点积。先忽略 $\sqrt{d_k}$,只看匹配关系:
“它”对“小猫”的分数
= 0.9×1.0 + 0.8×0.7 + 1.0×0.5
= 1.96
“它”对“汽车”的分数
= 0.9×1.0 + 0.8×1.0 + 1.0×0.95
= 2.65
“它”对“太快”的分数
= 0.9×0.0 + 0.8×0.2 + 1.0×0.3
= 0.46
在这组虚构参数里,“汽车”的 Key 与“它”的 Query 最匹配。这里只列出了 3 个分数;实际还会同时计算句中另外 6 个位置,再把全部 9 个分数一起送进 softmax,得到前面表格中的完整权重,例如“小猫”是 0.08、“汽车”是 0.42、“太快”是 0.12。
到这里,Q 和 K 的工作已经结束了。它们负责产生权重,却不会直接成为注意力输出。接下来才轮到 Value。
Value 可以携带另一组完全不同的特征。为了演示,假设它有 4 维:
Value(小猫) = [1.0, 0.0, 0.8, 0.6]
Value(汽车) = [0.0, 1.0, 1.0, 0.9]
Value(太快) = [0.0, 0.0, 0.1, 1.0]
这里可以把前两维暂时想成“动物相关信息”和“交通工具相关信息”,后两维则是其他语义和上下文特征。注意力用完整的 9 个权重混合 Value。只展开其中三项是:
输出(它)
= 0.08 × Value(小猫)
+ 0.42 × Value(汽车)
+ 0.12 × Value(太快)
+ 其余 6 个位置的加权 Value
这个输出同时带有多个位置的信息,其中“汽车”的贡献最大。所有加权 Value 相加后仍是一个 4 维向量,它会成为“它”进入后续子层时的新表示,再由 FFN 和下一层继续加工。
现在就能看出三者的区别:
Query(它) 写着“我想找什么”
Key(小猫/汽车/太快) 用来回答“我是否匹配”
Value(小猫/汽车/太快) 真正装着要传回的内容
上面的维度名称和数字全是为了教学手工设计的,不能拿来当真实模型的内部解释。实际的 Q、K、V 是 $X$ 乘以训练得到的矩阵后产生的稠密向量;一个语言概念通常分散在很多维度里,一个维度也可能同时参与多种特征。这个例子只想说明计算职责:Q/K 产生权重,权重再作用到 V。
之所以使用三组投影,是因为用什么条件匹配和匹配后传回什么内容不一定是同一组特征。
如果强行令 $Q=K=V=X$,注意力仍然可以计算:
$$ softmax\left(\frac{XX^T}{\sqrt d}\right)X $$但同一组向量要同时承担“我想找什么”“我能否被匹配”和“我携带什么内容”三项工作。分别学习 $W_Q,W_K,W_V$,模型就能把这些职责拆开。Query 和 Key 分开还有一个好处:匹配关系可以有方向。“它”查找“汽车”和“汽车”查找“它”未必是同一件事;若 Q、K 完全相同,softmax 前的 $XX^T$ 是对称矩阵,而使用不同投影后通常不再对称。
因此,Q、K、V 并不是数学上少一组就无法运行。它们更像一种经过验证的职责划分:Q/K 决定从哪里取,V 决定取回什么。后面的多头注意力还会同时准备多套 Q/K/V,让不同头学习不同的信息交换方式。
写回数学表达,就是同一份输入经过三种可学习的线性投影:
$$Q=XW_Q,\qquad K=XW_K,\qquad V=XW_V$$$X$ 是当前所有 token 的表示,$W_Q$、$W_K$、$W_V$ 是训练出来的参数。同一份输入经过三种投影,得到三种用途不同的表示。
一个 token 怎样完成一次注意力
下图把计算顺序画了出来。先从输入得到 Q/K/V,再用 Q 和 K 计算匹配分数,最后拿权重去混合 V。

还是以“它”为例:
- 取出“它”的 Query;
- 用这个 Query 与句中所有 token 的 Key 做点积;
- 分数越大,说明当前投影下越匹配;
- 用 softmax 把一排分数变成总和为 1 的权重;
- 按这些权重混合所有 token 的 Value。
最后得到的是一份混合后的新向量,并非某个原词的复制品。假如“汽车”的权重最高,新向量就会带入较多“汽车”的信息。
用公式写就是:
$$ Attention(Q,K,V)=softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$第一次读时,可以先把它翻译成人话:
QKᵀ 给所有候选位置打分
除以 √dₖ 别让分数大得太离谱
softmax 把分数变成权重
再乘 V 按权重取回并混合内容
一张注意力矩阵应该怎样读
这句话有 9 个 token,每个 token 都向 9 个位置查询一次,因此会产生 $9\times9$ 个权重。

读这类图时只记两个方向:
- 行:谁正在更新自己,也就是 Query;
- 列:它从谁那里取信息,也就是 Key/Value 所在的位置。
图中“它”那一行在“汽车”那一列颜色最深,对应前面手算的 0.42。横向看完整一行,就能看到“它”怎样在句中各个位置之间分配权重。
但别把注意力热力图当成模型的“思维过程截图”。真实模型的头经常很杂,有的偏向相邻 token,有的偏向标点,有的彼此重复。权重只告诉我们信息怎样路由,最终结果还取决于 Value 里装了什么,以及后续层怎样继续加工。
第三步:把整句话一起放进矩阵
前面一直盯着“它”这一个 token,实际计算时,“小猫”“追上”“汽车”等位置也会同时更新。继续使用这 9 个 token:
[小猫, 没, 追上, 汽车, 因为, 它, 跑得, 太快, 了]
假设每个 token 当前有 8 个数字,把 9 张小纸条叠起来就是输入矩阵:
X:[9, 8]
↑ ↑
9个token,每个8维
为了演示,设这个注意力头把 Q、K 投影到 3 维,把 V 投影到 4 维:
Q:[9, 3]
K:[9, 3]
V:[9, 4]
QKᵀ 会一次算出所有 token 两两之间的分数:
Q Kᵀ 分数矩阵
[9,3] × [3,9] = [9,9]
这个 [9,9] 矩阵可以想成一张表:
- 第 1 行:小猫准备从 9 个位置分别取多少信息;
- 第 4 行:汽车准备从 9 个位置分别取多少信息;
- 第 6 行:它准备从 9 个位置分别取多少信息。
我们前面手算的 [0.08, 0.02, ……, 0.03],就是“它”这一行经过 softmax 后的结果。其他 8 个 token 也各有自己的一行,并且全部可以并行算出。
最后,整个权重矩阵一起乘 V:
注意力权重 V 输出
[9,9] × [9,4] = [9,4]
输出仍有 9 行:每个 token 都得到一个 4 维新表示。行数没有减少,改变的是每一行都混入了上下文。
真实实现还会多一个 batch 维,常见输入形状是:
[batch_size, sequence_length, d_model]
框架中的 reshape、transpose 看起来很多,主要是在排列 batch、token、注意力头和特征维。把这句 [9,8] → Q/K/V → [9,9] → [9,4] 的路线看懂,代码就不再像凭空变形。
为什么必须除以 $\sqrt{d_k}$
点积会把 $d_k$ 个乘积加起来。假设 Q 和 K 的每个分量均值为 0、方差为 1,而且近似独立,那么点积的方差约为 $d_k$,标准差约为 $\sqrt{d_k}$。
$d_k$ 越大,送入 softmax 的分数越容易散得很开。softmax 随后会变得过于尖锐:某一项接近 1,其余项接近 0,梯度也会变小。除以 $\sqrt{d_k}$,相当于把分数重新压回比较稳定的尺度。

右图用两类 softmax 等价的 sigmoid 切片展示趋势;多分类时梯度是一个雅可比矩阵,不会恰好只有一条 $p(1-p)$ 曲线,但“分数差距太大容易饱和”这个结论不变。
第四步:为什么要同时算多个头
还是这句话:
小猫没追上汽车,因为它跑得太快了。
刚才我们只演示了一个头,让“它”较多读取“汽车”。但这句话里显然不只有指代关系:
“没”需要和“追上”组合,表示动作没有发生;
“追上”需要联系“小猫”和“汽车”,分清谁追谁;
“太快”需要联系“跑得”;
“因为”还连接着前后两部分因果关系。
如果只有一套 $W_Q,W_K,W_V$,所有关系都要挤在同一套匹配方式里。多头注意力干脆准备多套独立投影。用教学化的说法,某一层里可能出现:
头 1:让“它”更多读取“汽车” —— 指代线索
头 2:让“追上”读取“小猫”和“汽车” —— 动作参与者
头 3:让“太快”读取“跑得” —— 相邻搭配
头 4:让各位置收集更分散的整句信息 —— 全局信息
这些职责不是程序员预先指定的,真实模型也未必分得这么整齐。例子只是说明:不同参数的头可以同时建立不同的信息通道。
多头注意力的做法,是准备多套独立投影:
第 1 头:用第 1 套 WQ/WK/WV 计算
第 2 头:用第 2 套 WQ/WK/WV 计算
……
每个头在自己的子空间里完成一次注意力,所有头的结果拼起来,再经过输出投影 $W_O$ 混合。
$$ head_i=Attention(XW_i^Q,XW_i^K,XW_i^V) $$$$ MultiHead(X)=Concat(head_1,\ldots,head_h)W_O $$
工程代码常常一次性投影,再把结果 reshape 成 [batch, heads, seq_len, head_dim],所以教程喜欢说“把向量切成多个头”。更准确的理解是:模型在一次大矩阵运算中,高效实现了多套独立投影。 它不是把原始 embedding 随便切几段,各段原封不动地计算。
图中的四种模式只是为了说明多头能不同,并不表示设计者提前规定“第 1 头必须看相邻词”。头的行为由训练形成,可能清晰,也可能杂乱和冗余。
第五步:把注意力装进 Encoder
现在把镜头拉远一点,看看这句话经过一个 Encoder block 会发生什么。
多头自注意力结束后,9 个 token 都拿到了各自收集的信息。例如“它”的表示混入了“汽车”“跑得”“太快”,“追上”的表示可能混入“小猫”和“汽车”。但“收集回来”不等于已经理解完毕,这些混合向量还需要继续加工。
因此,一个 Encoder block 主要有两个子层:
输入
↓
多头自注意力:token 之间交换信息
↓
FFN:每个 token 单独加工自己的信息
↓
输出给下一层
FFN 为什么不能省
以“它”为例,注意力输出可能同时带着“汽车占比较高”“和跑得太快有关”等信息。FFN 会在“它”自己的向量内部做非线性变换,把这些收集到的特征重新组合。它不会在这一步再次读取“小猫”或“汽车”;token 之间的交换已经由注意力完成。
注意力输出本质上仍是 Value 的加权组合。模型需要 FFN 这样的非线性变换,才能把收集到的信息重新组织成更有用的特征。
原版 FFN 是两层全连接:
$$ FFN(x)=\max(0,xW_1+b_1)W_2+b_2 $$它会先升维,例如从 512 维升到 2048 维,经过 ReLU,再降回 512 维。FFN 对每个 token 分别计算,但同一层的所有位置共享参数。
还是那句比较好记的话:
Attention:出去收集信息
FFN:回到自己的工位加工信息
很多现代模型会把 ReLU 换成 GELU、SwiGLU 等。细节变了,分工没有变。
残差和 LayerNorm 是在保住什么
每个子层旁边还有一条绕行路线:输入不经过子层,直接加回输出。这是残差连接。
新的表示 = 原来的表示 + 子层学到的改动
这样一层即使暂时没学好,也不至于把原信息完全破坏;反向传播时,梯度也有更直接的通道。
LayerNorm 则在单个 token 的特征维上做归一化,帮助控制数值尺度。它不依赖 batch 中还有哪些句子,因而更适合长度不一的序列。
原论文使用 Post-Norm:
$$x'=LN(x+Sublayer(x))$$许多更深的模型使用 Pre-Norm:
$$x'=x+Sublayer(LN(x))$$初学阶段不必争论谁绝对更好。只要知道不同模型的 LayerNorm 位置可能不同,架构图不完全一样并不代表其中一张画错了。
堆很多层以后发生了什么
第一层可能让“它”读取附近名词,FFN 再加工这份信息;下一层拿到的已经不是原始词向量,而是带上下文的表示,于是还能建立更复杂的关系。
单层全局注意力已经允许任意两个位置直接接触。继续堆层,是为了反复执行“交换信息—加工信息”,逐层形成复杂表示;这和卷积逐层扩大感受野不太一样。
Encoder 最终会为每个输入 token 输出一个上下文化向量,而非只交出一个句子向量。对“我爱猫”来说,可以想成三份已经互相参考过的说明书。Decoder 随后会来查询这些说明书。
第六步:Decoder 为什么比 Encoder 多一块
Encoder 已经读完“我爱猫”,接下来轮到 Decoder 生成英文。假设它目前只收到:
<BOS> I
下一步应该预测 love。为了完成这件事,Decoder 要处理两个问题:先读懂已经生成的英文前缀,再回头查询中文原文。因此,一个原版 Decoder block 有三部分:
带因果掩码的自注意力
↓
交叉注意力
↓
FFN
FFN 与 Encoder 中一样。区别集中在两种注意力上。
因果自注意力:不准提前看答案
翻译时,Decoder 预测 love 只能利用已经生成的 I,不能偷看后面的 cats。否则训练时它会学会抄答案,真正生成时却无答案可抄。
解决办法是在 softmax 前给未来位置的分数加上负无穷。softmax 后,这些位置的权重就变成 0。

图中的下三角可见,上三角屏蔽。除此之外还有 padding mask:同一 batch 中句子长度不一时,短句会补齐,padding mask 负责让模型忽略这些补位 token。两种 mask 目的不同,经常组合使用。
交叉注意力:回头查中文原文
Decoder 只看已生成英文还不够,否则它不知道该翻译什么。交叉注意力会让 Decoder 去查询 Encoder 的输出:
Query:来自 Decoder 当前表示
Key:来自 Encoder 输出
Value:来自 Encoder 输出
生成 love 时,Decoder 的 Query 可能与中文“爱”的 Key 更匹配,于是取回更多“爱”的 Value。这个动作会在每个 Decoder 层、每个生成位置反复发生。
三种注意力可以这样区分:
| 类型 | Q 来自哪里 | K、V 来自哪里 | 能看哪些位置 |
|---|---|---|---|
| Encoder 自注意力 | Encoder | 同一份 Encoder 表示 | 整个源句,除 padding |
| Decoder 因果自注意力 | Decoder | 同一份 Decoder 表示 | 当前及左侧位置 |
| 交叉注意力 | Decoder | Encoder 最终输出 | 整个源句,除 padding |
判断“自注意力还是交叉注意力”,看 Q 和 K/V 是否来自同一条数据流就行。
第七步:训练时为什么可以并行
前面为了叙述方便,说 Decoder 先生成 I,再生成 love。这确实是推理过程,但训练时如果也一步一步算会很慢。
已知正确目标是:
I love cats <EOS>
训练时会把输入和答案错开一位:
Decoder 输入:<BOS> I love cats
监督答案: I love cats <EOS>
四个输入位置可以一次送入 GPU。因果 mask 会保证:
<BOS>只能预测时使用自己已有的信息;I所在位置可以看到<BOS>和I,用来预测love;love所在位置看不到右侧的cats。
模型在每个位置输出词表中所有 token 的分数,也就是 logits。softmax 把它们变成概率,再与正确答案计算交叉熵。误差一路反向传播,更新 Embedding、Q/K/V 投影、FFN 等全部参数。
这里有两个容易混在一起的概念:
- Teacher forcing:训练输入使用真实的目标前缀;
- Causal mask:每个位置不能读取未来的目标 token。
一个决定“喂什么”,一个决定“能看什么”。
第八步:推理为什么慢,KV Cache 又缓存了什么
真正生成时没有完整答案,只能循环:
<BOS> → 预测 I
<BOS> I → 预测 love
<BOS> I love → 预测 cats
<BOS> I love cats → 预测 <EOS>
这就是自回归。后一个 token 依赖前一个 token 的结果,所以 token 之间无法像训练那样全部并行。
不过,生成 cats 时,<BOS>、I、love 的 Key 和 Value 前面已经算过,而且不会改变。实际推理会把每一层历史 token 的 K/V 保存下来,新一步只计算新 token 的 Q/K/V,再让新 Query 查询缓存中的所有 Key。这就是 KV Cache。
它省掉了大量重复投影和历史前向计算,却不是免费午餐:
- 缓存会随着层数、序列长度和 batch 增长,占用显存;
- 新 Query 仍要和全部历史 Key 比较;
- 因此上下文越长,单步生成仍然会变贵。
第九步:BERT、GPT、T5 到底改了哪一边
理解原版 Encoder-Decoder 后,几个常见名字就容易定位了:
- BERT:主要保留 Encoder。它能同时看左右文,适合理解、分类和抽取;
- GPT、Llama:主要保留带因果 mask 的 Decoder 自注意力,但通常没有原版翻译架构中的 Encoder 和交叉注意力;
- T5:保留 Encoder-Decoder,适合把任务统一成“文本到文本”;
- ViT:把图像切成 patch,当作一串 token 交给 Encoder 式结构。
所以“Decoder-only”不等于把原版 Decoder 整块原样搬走。没有 Encoder 时,也就没有那层 Encoder-Decoder 交叉注意力。它保留的核心是因果自注意力、FFN、残差和归一化等结构。
第十步:它为什么有效,又付出了什么
Transformer 最关键的改变,是让每个位置可以通过注意力直接读取其他位置,同时让所有位置在训练时并行计算。RNN 必须沿时间一步步传递状态,远距离信息路径长;自注意力把任意位置之间的路径缩短成一次交互。
代价也很具体。长度为 $n$ 的序列会产生 $n\times n$ 的注意力分数矩阵。序列长度翻倍,这一部分的计算和中间存储大约变成四倍。
更细一点,一层注意力有两类成本:
- Q/K/V 和输出线性投影:约 $O(nd_{model}^2)$;
- 注意力打分与加权汇总:约 $O(n^2d_{model})$。
短序列、宽模型中,线性投影也可能很贵;序列足够长时,$n^2$ 才逐渐成为主角。
FlashAttention 没有把精确注意力从二次复杂度变成线性复杂度。它主要改变计算分块和显存访问方式,避免频繁读写完整中间矩阵,所以实际速度更快、显存更省。滑动窗口和稀疏注意力则是从“哪些位置需要互相看”这件事上减少计算。
位置长度也是限制。固定正弦位置编码理论上对任意位置有定义,不代表模型在远超训练长度时仍能正确使用它。RoPE、ALiBi 以及各种长度外推方法,都在继续处理这个问题。
最后再走一次完整流程
回到最初的“我爱猫”,现在可以把每一步说完整了:
- Tokenizer 把文本切成 token,并映射到 id;
- Embedding 查表得到向量,再加入位置信息;
- Encoder 自注意力让三个 token 交换信息;
- FFN 分别加工它们的新表示;
- 多层堆叠后,Encoder 产出三份上下文化表示;
- Decoder 用因果自注意力读取已经生成的英文前缀;
- Decoder 用交叉注意力查询中文的 Encoder 输出;
- FFN 继续加工,Linear 层映射到词表 logits;
- softmax 得到下一个 token 的概率;
- 推理时选择一个 token 放回输入,直到生成
<EOS>。
如果现在再看开头那张架构图,你应该能解释里面每一种箭头在搬什么:自注意力在同一序列内部搬信息,交叉注意力从 Encoder 搬到 Decoder,残差旁路保留原表示,FFN 则不搬 token,只在每个位置内部加工。
我觉得理解 Transformer 真正的分水岭,并不在于能否默写 attention 公式。看到一个新变体时,如果你会追问几个朴素的问题——它让谁看谁?位置信息放在哪里?token 之间在哪里交互?每个 token 又在哪里单独加工?训练和推理的数据流有什么不同?——架构就已经有地方落脚了。
这些问题能答出来,架构就不再是一张需要背诵的电路图了。
参考资料
- Vaswani 等,《Attention Is All You Need》
- Jay Alammar,《The Illustrated Transformer》
- Harvard NLP,《The Annotated Transformer》
- Dive into Deep Learning,《The Transformer Architecture》
- Transformers 快速入门,《第二章:Transformer 模型》
版权声明: 本文首发于 指尖魔法屋-深入拆解 Transformer:从注意力到完整架构(https://blog.thinkmoon.cn/post/1034-deep-dive-transformer-architecture/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。