📍 路标 :本篇位于《从零构建 Transformer》系列 第 6/16 章 · 架构篇 。
系列主线:注意力思想 → 自注意力 → 多头 → 位置编码 → 编解码架构 → 手撕实现 → 预训练模型 → 实战微调 → 知识收官 。
进度:▸ 基石篇(1-5) ▸ 架构篇(6-10·本篇) ▸ 预训练篇(11-13) ▸ 实战篇(14-16)
上一篇(三十四):位置编码(Positional Encoding)------给词打上"顺序"的标签
引言:零件的仓库已经满了,该组装大楼了
从第 1 章到第 5 章,我们像在车间里一样,一件一件地把零件造了出来:
- 第 1-2 章:注意力思想与 Q/K/V 三件套------模型学会"让词和词互相对话";
- 第 3 章:缩放点积注意力------对话时先打分、缩放、归一化、再加权求和;
- 第 4 章:多头注意力------一个头不够,8 个头从不同角度同时看;
- 第 5 章:位置编码------给每个词贴上"我是第几个"的标签,补上"顺序"这个救命信息。
零件齐了吗?齐了。但零件堆在仓库里,还不是一栋楼。注意力、多头、位置编码这些"积木",怎么拼成一个能真正做翻译、做生成的完整模型?这就是架构篇(第 6-10 章)要回答的问题。
这一章,我们不写一行复杂的数学,只做一件事:把整栋 Transformer 大楼的平面图,一笔一笔画给你看。看完这一章,你再回头看论文《Attention Is All You Need》里那张著名的架构图,会觉得它亲切得像老朋友。
🎯 本章目标
- 用一张全景图,看懂 Transformer 的"左右两大块":编码器与解码器各管什么;
- 逐块拆解编码器:为什么它叫"速读冠军",一次就能读完整个句子;
- 逐块拆解解码器:为什么它叫"逐字作者",一个词一个词地憋;
- 分清楚三种注意力:自注意力、掩码自注意力、交叉注意力,Q/K/V 到底从哪来;
- 走一遍完整的数据流:从源句进来,到目标句出去,每一层的张量形状怎么变;
- 讲清"编码器并行、解码器串行"这个最容易被问倒的面试题;
- 背下经典超参数:N=6、d_model=512、h=8、d_ff=2048;
- 亲手搭一个"微型 Transformer 骨架",让架构图的每一块都对应一段能跑的代码。
一、先看全景图:Transformer 就两大块
不卖关子,直接把整栋大楼的平面图拍在桌面上。下面这张图就是 Transformer 的完整架构,请先花 30 秒只看不读,感受一下整体轮廓:

看完轮廓,我们用一句话给整栋楼定性:Transformer = 编码器(左)+ 解码器(右)。
- 左边一列是编码器(Encoder) :输入是源句 (比如英文 "I love cats"),责任是一次读完整句,把整句话"消化"成一串向量记忆,供右边使用;
- 右边一列是解码器(Decoder) :输入是目标句 (比如中文"我 爱 猫"),责任是一个词一个词地生成:先出"我",再出"爱",再出"猫",每出一个词,都要回头"查"一下左边编码器留给它的记忆。
所以最直白的比喻是:
编码器是"速读冠军":刷一眼,整本书的内容全记住;解码器是"逐字作者":对着速读冠军做好的读书笔记,一个字一个字地把书重新写出来。
再看图 1 的细节。左边编码器一列,从上到下是:源句嵌入+位置编码 → 编码器层1 → 编码器层2 → ...共N层 → 第N层输出。每一层内部长这样:① 多头自注意力 → ② 前馈网络 FFN,中间穿插着残差连接和层归一化。
右边解码器一列,从上到下是:目标词嵌入+位置编码 → 解码器层1 → 解码器层2 → ...共N层 → Linear+Softmax → 输出词。每一层内部长这样:① 掩码多头自注意力 → ② 多头交叉注意力 → ③ 前馈网络 FFN。
注意两条紫色的虚线箭头------它们从编码器的"第 N 层输出"指向解码器每一层的"交叉注意力",箭头上写着 K、V 。这一条线是全图最重要的信息流:编码器把"记忆"(K、V)借给解码器,解码器拿着自己的"问题"(Q)去查这份记忆。我们把这条线单独拎出来讲。
二、编码器:一眼读完整个源句的"速读冠军"
先看左半边。把编码器一列单独放大,就是下面这张图:

**编码器的输入是什么?**是源句的每个词。以 "I love cats" 为例,三个词分别查词嵌入表得到向量,再按第 5 章的方法加上位置编码,就得到输入 X ∈ R ( 3 , d m o d e l ) X \in \mathbb{R}^{(3, d_{model})} X∈R(3,dmodel)(3 个词,每词一个 d m o d e l d_{model} dmodel 维向量)。
**编码器每一层做什么?**看图 2,从下往上走一遍:
- ① 多头自注意力:三个词互相"对话"。"I" 会看一眼 "love"、看一眼 "cats",决定自己该吸收多少它们的信息。这是第 4 章的老朋友,Q/K/V 都来自输入 X 自己;
- 残差连接 :把原始输入 X X X 绕道相加到自注意力的输出上,得到 X + Attn ( X ) X + \text{Attn}(X) X+Attn(X)。为什么要绕这一下?因为这样梯度能"抄近路"往回传,深层网络才不会"梯度消失"(第 7 章会专门展开);
- 层归一化 LayerNorm:把向量拉回统一尺度,训练更稳;
- ② 前馈网络 FFN:一个"先扩维再缩回"的小神经网络,让每个词独立地再"想一想"(第 7 章展开);
- 再来一次残差 + 层归一化:输出。
关键点:每一层的输出形状和输入一模一样 ,都是 ( 3 , d m o d e l ) (3, d_{model}) (3,dmodel)。所以编码器可以一层一层往上叠,叠多少层都行(论文叠了 6 层)。叠完后,最后一层输出的每个位置的向量,都"吸收"了整句话的信息------这就是编码器记忆 ,记作 Z Z Z。
💡 怎么理解编码器输出 Z? 原来 "I" 的向量只代表 "I" 这个单词;经过 6 层自注意力后,"I" 的向量里已经包含了"I 是句子的主语、它爱 cats、cats 是复数"这些上下文信息。我们把这一串"读懂了整句的向量"叫内存/记忆,英文叫 memory,它就是解码器要反复查询的"笔记"。
编码器为什么是"速读冠军"?因为它一次就能处理完整句话------三个词同时进入自注意力,1 个时间步就把整句消化完,不需要像 RNN 那样一个词一个词地读。这个"并行"的优越性,第 7 节会专门对比。
三、解码器:一个词一个词"憋"出来的"逐字作者"
再看右半边。解码器比编码器复杂一点------它内部多了一种注意力 (交叉注意力),还多了一张遮未来的掩码。单层解剖图如下:

解码器的输入是什么?是目标句------但注意,它右移了一位 :不是直接输入"我 爱 猫",而是输入"<BOS> 我 爱"(<BOS> 是"开始"标记)。为什么右移?因为解码器要做的任务是"看到前面的词,预测下一个词 ":看到 <BOS> 预测"我",看到 <BOS> 我 预测"爱",看到 <BOS> 我 爱 预测"猫"。所以它的输入天然比输出"慢半拍"。
**解码器每一层做什么?**看图 3,三件套:
- ① 掩码多头自注意力 :和编码器的自注意力几乎一样,Q/K/V 都来自解码器自己,但多了一张"掩码" ------生成第 t t t 个词时,只许看它左边的词(已经生成的),不许偷看右边的词(还没生成的)。图 3 右上角那个红色小三角,就是"上三角遮罩"的示意:未来位置全部被盖住(细节在第 8 章展开,这一章先记住"有这道保险");
- ② 多头交叉注意力 :这是解码器独有的、也是全模型最核心的一环。Q 来自解码器自己 ("我下一步该说什么?"),K、V 来自编码器的记忆 Z("源句里有什么内容可以回答我?")。解码器拿着自己的问题,去查编码器做的笔记------具体查哪个词,由图 5 的热力图可见;
- ③ 前馈网络 FFN:和编码器一样,每个词独立地"再想一想"。
每一件之后都跟着残差连接 + 层归一化,和编码器的套路完全一致。
解码器为什么是"逐字作者"?因为它一次只能生成一个词 :先看到 <BOS> 输出"我",再把"我"并进输入,看到 <BOS> 我 输出"爱"......如此循环,直到输出 <EOS>(结束标记)。这就是第 7 节要讲的"串行"。
💡 左侧编码器 vs 右侧解码器,一句话总结:
- 编码器:自注意力 读懂源句,并行,一次搞定;
- 解码器:掩码自注意力 + 交叉注意力 生成目标句,串行 ,逐词憋。
编码器负责"读书",解码器负责"写书",交叉注意力就是"写书时翻笔记"。
四、三种注意力:Q、K、V 的三个不同来源
讲到这儿,细心的人会发现:全模型里"注意力"出现了三次,但每次的 Q/K/V 来源都不一样。这是新手最容易混的地方,必须用一张对比图彻底讲清楚:

| 注意力 | 出现在哪 | Q(问题)来自 | K、V(答案)来自 | 特殊之处 |
|---|---|---|---|---|
| 自注意力 | 编码器每层 | 源句 X 自己 | 源句 X 自己 | 无 |
| 掩码自注意力 | 解码器每层 | 目标句 Y 自己 | 目标句 Y 自己 | 上三角遮罩,不许看未来 |
| 交叉注意力 | 解码器每层 | 解码器自己 | 编码器记忆 Z | 两个模型之间的"对话" |
看懂这张表,你就是架构通了:
- 自注意力:"词和词互相对话"------源句内部交流;
- 掩码自注意力:"已生成的词之间对话,但不许剧透"------目标句内部交流,且有防偷看保险;
- 交叉注意力 :"解码器问,编码器答"------两个模块之间的跨语言交流,这是 Transformer 能翻译的根本原因。
注意:三种注意力用的是同一个公式 : A = softmax ( Q K ⊤ d k ) V A = \text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V A=softmax(dk QK⊤)V。公式从头到尾没变,变的只是 Q/K/V 从哪来。这就是图 4 底部那句话:"变的是原料,不变的是配方"。
五、一次完整翻译的"流水账":从输入到输出
理论讲完了,现在把整栋楼"通电"------走一遍从源句到目标句的完整流程。翻译 "I love cats" → "我 爱 猫",整个过程如下:

跟着图 5 的箭头,把流水账从头到尾捋一遍:
第 1 步:编码器读源句(只跑 1 次)。
"I love cats" 三个词分别变成向量(嵌入 + 位置编码),一起进入编码器。经过 N 层自注意力 + 前馈,输出三个"读懂了整句"的向量 z 1 , z 2 , z 3 z_1, z_2, z_3 z1,z2,z3,合起来叫记忆 Z Z Z。注意:编码器全程只运行这一次,之后它就在旁边"待命",随时供解码器查询。
第 2 步:解码器生成第一个词。
解码器输入只有 <BOS>。它先做掩码自注意力(只有一个词,没什么可看的),然后做交叉注意力:拿自己的 Q 去查 Z Z Z 里的 z 1 , z 2 , z 3 z_1, z_2, z_3 z1,z2,z3,加权求和后过 FFN,最后经过 Linear + Softmax 输出一个"词表上的概率分布"------挑概率最大的那个词,就是"我"。
第 3 步:解码器生成第二个词。
把"我"接到输入后面,现在是 <BOS> 我。再跑一遍同样的流程,掩码自注意力让"我"看一眼"<BOS>"(但不能看未来的词),交叉注意力再查一次编码器记忆,输出"爱"。
第 4 步、第 5 步:继续憋。
<BOS> 我 爱 → 输出"猫";<BOS> 我 爱 猫 → 输出 <EOS>,生成结束。
流水账的四个要点:
- 编码器跑 1 次,解码器跑 N 词次------编码器是"一次读懂",解码器是"逐词生成";
- 每一步解码器的输入都比上一步多一个词------这是"自回归"(Autoregressive)的本质:模型吃自己上一步的输出;
- 交叉注意力贯穿解码器的每一步------无论生成到第几个词,都要回头查编码器的记忆,这就是图 1 那些紫色 K、V 箭头的含义;
- 每一步都输出一个概率分布------Softmax 之后挑 argmax 就是"贪心生成",后面第 13 章讲 GPT 时会详细展开采样技巧。
六、实操验证(一):亲手搭一个"微型 Transformer 骨架"
理论说得再明白,不如亲手跑一遍。这一节我们搭一个微型 Transformer 骨架 :维度压到 d m o d e l = 4 d_{model}=4 dmodel=4,源句 3 个词、目标句 3 个词,但架构图和真实模型完全一致------图上有哪一块,代码里就有哪一段。代码越简单越好,全程只用 NumPy,复用前几章的零件。
python
import numpy as np
def softmax_rows(M):
e = np.exp(M - M.max(axis=1, keepdims=True))
return e / e.sum(axis=1, keepdims=True)
def scaled_dot_product_attention(Q, K, V, mask=None):
d_k = K.shape[-1]
scores = (Q @ K.T) / np.sqrt(d_k) # 打分 + 缩放(第3章)
if mask is not None:
scores = scores + mask # 加掩码:未来位置变成 -1e9
weights = softmax_rows(scores) # 行归一化成权重
return weights @ V, weights # 加权和 + 权重(供查看)
def positional_encoding(max_len, d_model): # 第5章
pe = np.zeros((max_len, d_model))
pos = np.arange(max_len)[:, None]
i = np.arange(d_model // 2)
inv = 1.0 / (10000.0 ** (2 * i / d_model))
pe[:, 0::2] = np.sin(pos * inv)
pe[:, 1::2] = np.cos(pos * inv)
return pe
def layer_norm(X, eps=1e-5): # 归一化(第7章细讲)
mu = X.mean(axis=-1, keepdims=True)
var = X.var(axis=-1, keepdims=True)
return (X - mu) / np.sqrt(var + eps)
def ffn(X, W1, b1, W2, b2): # 前馈网络(第7章细讲)
return np.maximum(0, X @ W1 + b1) @ W2 + b2
np.random.seed(42)
d_model = 4
vocab_tgt = 6 # <BOS> <EOS> 我 爱 猫 <PAD>
W_emb_src = np.random.randn(3, d_model) * 0.1 # 源句 3 个词的表
W_emb_tgt = np.random.randn(vocab_tgt, d_model) * 0.1
Wq = np.random.randn(d_model, d_model) * 0.1
Wk = np.random.randn(d_model, d_model) * 0.1
Wv = np.random.randn(d_model, d_model) * 0.1
W1 = np.random.randn(d_model, d_model * 2) * 0.1; b1 = np.zeros(d_model * 2)
W2 = np.random.randn(d_model * 2, d_model) * 0.1; b2 = np.zeros(d_model)
Wo = np.random.randn(d_model, vocab_tgt) * 0.1 # 输出投影
# ---------- 编码器:一次读完整句 ----------
X = W_emb_src + positional_encoding(3, d_model)[:3] # 嵌入 + 位置编码
Q = X @ Wq; K = X @ Wk; V = X @ Wv
Z, _ = scaled_dot_product_attention(Q, K, V) # ① 自注意力
Z = layer_norm(X + Z) # 残差 + 归一化
Z = layer_norm(Z + ffn(Z, W1, b1, W2, b2)) # FFN + 残差 + 归一化
# 到这里 Z 就是"编码器记忆"
# ---------- 解码器:teacher forcing 一次喂完整目标句 ----------
Y = W_emb_tgt[[0, 1, 2]] + positional_encoding(3, d_model)[:3] # <BOS> 我 爱
mask = np.triu(np.full((3, 3), -1e9), k=1) # 上三角掩码
Q = Y @ Wq; K = Y @ Wk; V = Y @ Wv
M, _ = scaled_dot_product_attention(Q, K, V, mask) # ① 掩码自注意力
M = layer_norm(Y + M)
Q = M @ Wq; K = Z @ Wk; V = Z @ Wv # ② 交叉注意力
C, attn = scaled_dot_product_attention(Q, K, V) # Q 自解码器,K/V 自编码器
C = layer_norm(M + C)
C = layer_norm(C + ffn(C, W1, b1, W2, b2)) # ③ FFN
logits = C @ Wo # 输出层 Linear
跑起来,看看每一层的 Shape------你会发现每一条都神奇地保持 (3, 4):
源句嵌入 + 位置编码 X : (3, 4)
编码器·自注意力 : (3, 4)
编码器·残差 + 归一化 : (3, 4)
编码器·前馈 + 残差 + 归一化(= 编码器记忆 Z): (3, 4)
目标句嵌入 + 位置编码 Y : (3, 4)
掩码矩阵(上三角 = -1e9):
[[ 0.e+00 -1.e+09 -1.e+09]
[ 0.e+00 0.e+00 -1.e+09]
[ 0.e+00 0.e+00 0.e+00]]
解码器·掩码自注意力 : (3, 4)
解码器·残差 + 归一化 : (3, 4)
解码器·交叉注意力 : (3, 4) (K、V 来自编码器 Z)
解码器·残差 + 前馈 + 归一化: (3, 4)
输出层 Linear → logits : (3, 6) (词表大小)
对着架构图一段一段看,每一行都对应图上的一块:
X是 (3, 4):3 个源词,每个 4 维------对应图 1 左下角"源句嵌入 + 位置编码";- 编码器自注意力输出还是 (3, 4):自注意力不改变形状------对应图 2 的"① 多头自注意力";
- 残差 + 归一化、FFN、残差 + 归一化,全都不变形状:所以编码器想叠几层就叠几层------对应图 2 整张图;
Z是 (3, 4):这就是编码器记忆,供交叉注意力调用------对应图 1 的紫色 K、V 箭头;- 掩码矩阵:对角线及以下全是 0(看得见),右上角全是 -1e9(遮成负无穷,softmax 后权重为 0)------对应图 3 右上角的小三角;
- 交叉注意力输出 (3, 4):Q 是解码器的、K/V 是编码器的,形状都不变------对应图 4 第三列;
- 最后
logits变成 (3, 6) :3 个目标词位置,每个预测 6 个词表词的概率------对应图 1 的Linear + Softmax输出层。
这就是"一张图看懂全貌"的实操版:架构图上每一个方块,都能在你自己的代码里找到对应的一行。把这段骨架代码跑通,你就已经亲手"搭"过一遍 Transformer 了(第 9 章再把它升级成完整版)。
七、实操验证(二):掩码自注意力 vs 交叉注意力,K/V 从哪来
上面骨架里有一个细节值得单独验证:为什么掩码自注意力和交叉注意力不一样? 我们用两个小实验把它们的区别"钉死"。
实验 A:掩码真的把未来遮死了吗?
构造 4 个词的序列 <BOS> 我 爱 猫,看位置 1("我")在加掩码前后,对后面两个词("爱"、"猫")的注意力权重:
python
S = 4
mask4 = np.triu(np.full((S, S), -1e9), k=1) # 上三角掩码
np.random.seed(7)
Qs = np.random.randn(S, 4) * 0.5
Ks = np.random.randn(S, 4) * 0.5
_, W_no = scaled_dot_product_attention(Qs, Ks, np.ones((S, 4))) # 不加掩码
_, W_ma = scaled_dot_product_attention(Qs, Ks, np.ones((S, 4)), mask4) # 加掩码
真实结果:
不加掩码------位置 1("我")对后两词的权重: [0.391 0.168] (>0,偷看到了未来)
加了掩码------位置 1("我")对后两词的权重: [0. 0.] (=0,未来被遮死)
铁证如山:不加掩码时,"我"会偷看"爱"和"猫"(权重 0.391、0.168);加了掩码后,这两个权重被强制压成 0 。这就是解码器"不剧透"的物理实现-------1e9 加进打分矩阵,softmax 之后就是 0。
实验 B:交叉注意力的 K/V 到底来自谁?
在骨架代码里,交叉注意力那两行是:
python
Q = M @ Wq # Q 来自解码器自己(M 是解码器状态)
K = Z @ Wk # K 来自编码器记忆
V = Z @ Wv # V 来自编码器记忆
打印三个来源的形状:
解码器 Q 的形状: (3, 4) (来自解码器自己)
编码器 K 的形状: (3, 4) (来自编码器记忆)
编码器 V 的形状: (3, 4) (来自编码器记忆)
→ 两个来源不同:解码器"问问题",编码器"提供答案"
对比结论:
- 掩码自注意力 :Q、K、V 都来自解码器自己,是"已生成的词之间对话";
- 交叉注意力 :Q 来自解码器,K、V 来自编码器,是"解码器问、编码器答"。
同一个注意力公式,换一下 K/V 的来源,就从"自言自语"变成了"跨语言对话"------这就是图 4 那张对比表的意义。
八、交叉注意力到底"看"了源句的哪个词?热力图实锤
实操验证(二)只证明了"K/V 来自编码器",但还差最后一步:解码器生成"我"的时候,真的会重点看源句里的 "I" 吗? 我们把交叉注意力的权重矩阵画成热力图,眼见为实。
为了让规律肉眼可见,我们给源句和目标词各造一组"语义对齐"的简易向量(比如 "I" 和"我"偏同一维、"love" 和"爱"偏同一维),然后跑一遍标准的交叉注意力公式:
python
E_src = np.array([[1.0, 0.05, 0.15], # I ------ 偏"我"的维度
[0.05, 1.0, 0.05], # love ------ 偏"爱"的维度
[0.15, 0.05, 1.0]]) # cats ------ 偏"猫"的维度
E_tgt = np.array([[1.0, 0.08, 0.10], # 我
[0.08, 1.0, 0.08], # 爱
[0.10, 0.08, 1.0]]) # 猫
S = E_tgt @ E_src.T
W = softmax_rows(S) # 每行归一化成注意力权重
真实算出的权重矩阵:
行=目标词(Q),列=源词(K/V):
我: 对 I=0.53 love=0.22 cats=0.25
爱: 对 I=0.23 love=0.54 cats=0.23
猫: 对 I=0.25 love=0.22 cats=0.53
画成热力图:

看图 6,规律不能再明显了:主对角线(我→I、爱→love、猫→cats)一片深红,权重都超过 0.5;斜对角一片浅黄。用一句话说:
解码器生成第 t t t 个目标词时,交叉注意力会自动把注意力集中在源句里"语义最相关"的那几个词上------这就是"查笔记"的微观画面。训练完成后,真实模型学到的交叉注意力权重,形态正是这个样子(只是每一行不再这么"干净",会同时关注多个相关的源词)。
这一步把整个架构的"最后一公里"也打通了:图 1 的紫色 K、V 箭头,图 5 的"回头看"虚线,到这里全部落地成了一张具体的权重矩阵。
九、为什么编码器能并行、解码器必须串行?
这是面试中最爱考、也最容易答混的一个问题。答案其实就藏在两边的输入里:编码器的输入是"完整的一整句",解码器的输入是"当前已生成的部分"。我们看图说话:

**编码器为什么并行?**源句 "I love cats" 在输入的那一刻就是完整的------三个词全都摆在那儿。多头自注意力让它们互相看,一次前向传播就同时算出三个词的新表示。词与词之间没有"谁先谁后"的依赖,天然可以并行。这也是 Transformer 比 RNN 快一个数量级的根本原因:RNN 只能一个词一个词地读(串行),Transformer 一口气读完(并行)。
**解码器为什么串行?**目标句是"边生成边变长"的:生成"爱"之前,世界上还不存在"爱"这个词的位置------它得先靠"<BOS> 我"这两个词算出来。也就是说,第 t t t 个词的输出依赖前 t − 1 t-1 t−1 个词的输出,这种依赖关系决定了解码器推理时只能一个词一个词地来。
但注意一个容易混的点:**训练时解码器反而是"并行"的!**请把图 7 右半边和骨架代码对照一下------骨架里我们直接喂入了完整的 <BOS> 我 爱(三个位置同时算),这叫 Teacher Forcing(老师强迫):训练时正确答案已知,我们一次性把整句喂进去,让三个位置同时预测,再用掩码确保"每个位置只依赖它左边的词"。所以:
- 训练时 :喂完整目标句 + 掩码 → 三个位置并行算(快);
- 推理时 :没有正确答案 → 只能逐词生成,串行(慢,但这是唯一的路)。
💡 一句话记住:编码器输入是"完整的书",所以并行;解码器推理时输入是"自己刚写的前半本书",所以串行;训练时输入是"老师给的完整答案",所以也能并行(加掩码防剧透即可)。
十、经典超参数:N=6、d_model=512、h=8 从哪来?
看论文《Attention Is All You Need》时,Table 3 里有一串数字:N=6、d_model=512、h=8、d_ff=2048。这些数字是"标准配置"(Base 版),几乎所有入门教程都会用到。它们分别管什么?看图 8:

每个超参数在架构图里的位置:
- N = 6:图 1 里"...共 N 层"的 N。编码器 6 层 + 解码器 6 层,共 12 个"大块"(每一块含 2~3 个注意力/前馈子层);
- d_model = 512:所有词向量、所有层输出的维度。图 1 里每个方块进出的向量都是 512 维;
- h = 8:多头注意力把头数。第 4 章讲过,512 维切成 8 份,每份 64 维,8 个头并行看;
- d_k = d_v = 64:每个头的维度,等于 d_model ÷ h = 512 ÷ 8;
- d_ff = 2048:前馈网络"先扩维"到 2048(4 倍 d_model),再缩回 512。图 2 的"② 前馈网络"里就藏着这个 4 倍;
- Dropout = 0.1:每个子层的输出随机丢弃 10%,防过拟合。
**这些数字能算出参数量吗?**能。以编码器单层为例:自注意力有 Q/K/V 三个投影矩阵,每个 512×512,加上输出投影一个 512×512,共 4 × 512 2 4 \times 512^2 4×5122;前馈有两个矩阵 512×2048 和 2048×512,共 2 × 512 × 2048 2 \times 512 \times 2048 2×512×2048。代入验证脚本算出的真实数字:
编码器单层参数量 = 4×d_model² + 2×d_model×d_ff = 3145728 (约 315 万)
解码器单层参数量 = 6×d_model² + 2×d_model×d_ff = 3670016 (约 367 万,多出两个交叉注意力的投影)
全部 6 层(编+解)参数量 ≈ 4089 万
看到没有------解码器单层比编码器单层多约 52 万参数,就差在"交叉注意力"那组额外的 Q/K/V 投影上,这正好和我们前面讲的"解码器多一种注意力"对上了。约 4089 万的骨干参数量(还没算嵌入层和输出层),这就是论文里"约 6500 万总参数"的骨干来源。
🎤 记忆口诀 :N=6、d=512、h=8、ff=2048------四个数字背下来,你就能随口描述一个标准 Transformer,也就能看懂任何一篇论文的开始部分了。
十一、常见坑与自查
- 以为编码器和解码器结构一样 :编码器是"自注意力 + 前馈"两件套;解码器是"掩码自注意力 + 交叉注意力 + 前馈"三件套,多一个交叉注意力,多一张掩码;
- 以为交叉注意力是"另一种公式" :公式和自注意力完全一样 ( A = softmax ( Q K ⊤ / d k ) V A=\text{softmax}(QK^\top/\sqrt{d_k})V A=softmax(QK⊤/dk )V),变的是 Q/K/V 的来源------Q 来自解码器,K/V 来自编码器;
- 把掩码自注意力和交叉注意力的顺序记反 :解码器每层先 做掩码自注意力(处理自己),再做交叉注意力(查源句),顺序不能换;
- 以为编码器也是逐词生成的 :编码器一次读完整句 (并行);只有解码器推理时逐词生成(串行);而解码器训练时因为 teacher forcing 也可以并行;
- 以为解码器的输入就是目标句本身 :是右移一位 的目标句------前面补
<BOS>,真正的任务是"用前 t t t 个词预测第 t + 1 t+1 t+1 个词"; - 以为"掩码"是模型学出来的 :掩码是手工构造的固定矩阵 (
np.triu生成的上三角),不是参数,不参与训练; - 以为交叉注意力只看一个源词 :真实模型的每一行权重通常分散在多个相关的源词上(图 6 只是为了直观才显得"干净"),它做的是"加权求和",不是"只挑一个";
- 把 N 和 h 混为一谈 :N 是层数 (纵向堆叠),h 是头数(横向切分),一个在"楼层"维度,一个在"维度"维度;
- 以为参数量只和 d_model 有关 :FFN 的 d_ff 贡献更大( 2 × 512 × 2048 ≈ 210 2 \times 512 \times 2048 \approx 210 2×512×2048≈210 万,比自注意力的 4 × 512 2 ≈ 105 4 \times 512^2 \approx 105 4×5122≈105 万还多一倍)。
小结
这一章我们把五章的零件组装成了整栋大楼,核心收获八条:
- 整体架构 :Transformer = 编码器(读懂源句)+ 解码器(生成目标句),右边每层都要通过交叉注意力"查"左边的记忆;
- 编码器 :
自注意力 + 前馈 + 残差 + 归一化× N 层,一次读完整句(并行),输出"编码器记忆" Z; - 解码器 :
掩码自注意力 + 交叉注意力 + 前馈 + 残差 + 归一化× N 层,逐词生成(串行),输入是右移一位的目标句; - 三种注意力 :自注意力(Q=K=V=自己)、掩码自注意力(同左 + 上三角遮罩)、交叉注意力(Q 自解码器,K/V 自编码器)------配方不变,原料不同;
- 数据流:编码器跑 1 次 → 解码器逐词跑 N 次,每一步都"回头看"编码器记忆,最后 Linear+Softmax 出概率分布;
- 并行 vs 串行:编码器并行(输入完整),解码器推理串行(依赖前序输出),训练时 teacher forcing 可并行;
- 经典超参数:N=6、d_model=512、h=8、d_k=d_v=64、d_ff=2048、Dropout=0.1,骨干参数量约 4089 万;
- 实操验证:微型骨架让架构图每一块都对应一行代码,掩码把未来权重压成 0,交叉注意力热力图证明"生成哪个词就看哪个源词"。
架构篇的"平面图"已经拿到手了。但你可能已经注意到,这一章里反复出现"残差连接""层归一化""前馈网络"这几个词,而我们始终是"先记住它在哪、有什么作用",没有展开讲它到底怎么算 。下一章,我们就钻进编码器内部,把这栋楼的第一块承重墙------残差连接、LayerNorm 与前馈网络------一块砖一块砖地拆开看。
下一篇(三十六):Encoder 内部------残差连接、LayerNorm 与前馈网络