模块 06 · 约 20 分钟 · 把零件拼起来

Transformer:把零件拼成一个能跑的模型

上一节学了注意力。但 attention 并不是一整个模型 — 它是 Transformer Block 里的一个子层。 这一节我们把 attention、FFN、残差、LayerNorm 这几样拼起来,看一个完整的 Transformer 长什么样。

① 直觉:Transformer = 「乐高积木」

最终的 GPT 模型在视觉上其实非常简洁:

  1. 输入:一串 token IDs
  2. 查 embedding 表 + 加位置编码 → (L,d)(L, d) 张量
  3. 过 N 个结构完全相同的 Transformer Block
  4. 过一个 LayerNorm 收尾
  5. 用一个线性层投影到词表大小 → 输出每个位置上"下一个 token"的 logits

中间的 N 个 Block 是同一个结构反复堆叠 —— 像同一种乐高积木堆 96 层。 GPT-2 small 是 12 层,GPT-3 是 96 层,LLaMA-65B 是 80 层。 模型变"大",绝大多数时候是层数变多维度变大,结构不变。

② 互动:拆开一个 Transformer Block 看看

点击下面流程图里的任何一个阶段,看它具体做什么、为什么需要它:

阶段 3 / 8

Multi-Head Attention

softmax(QKᵀ/√d) V

这是什么上一节讲过的注意力。让每个 token "回头看"前面所有 token,按需聚合信息。

为什么有这是 Transformer 的灵魂:让 token 之间能"对话"。

张量形状:(L, d)

这是一个 Transformer Block 的内部流程。整个 GPT 就是把这种 block 堆叠 N 次 (N = 12 / 24 / 96 / ... 看模型大小)。点击左边任意一个阶段,右边会显示它做什么、为什么需要它。

重点关注 4 个零件:Attention(让 token 之间换信息)、FFN(在单个 token 内部加工信息)、残差连接(让 梯度顺畅)、LayerNorm(让分布稳定)。 理解了这 4 件,Transformer 没有别的秘密。

③ Attention 和 FFN 的"分工"

一个 Block 里有两个子层。它们的分工非常清楚:

子层作用token 间是否互动参数占比
Attention让每个 token 看"该看的"其他 token,聚合上下文约 1/3
FFN在每个 token 上独立做非线性变换(先升维到 4d 再降回 d)约 2/3

一个普通的比喻:Attention 是会议室里同事互相讨论,FFN 是回到工位上独立深加工。 缺了 attention,token 互相看不到对方;缺了 FFN,token 只能"传话"不能"思考"。两者缺一不可。

④ 残差连接:让 96 层也能训得动的秘密

如果你简单地把 96 个 attention 串起来,会发现训练时梯度消失:损失传不回前面的层,前几层就学不到东西。 2015 年 ResNet 提出的残差连接(skip connection)一举解决了这个问题:

xout=xin+sublayer(xin)x_{out} = x_{in} + \mathrm{sublayer}(x_{in})
↓ 对应的 Python 实现(可以直接改、直接运行)

看起来只是加了一个 x +,但效果是颠覆性的

  • 子层只需要学"对输入做什么修改"(residual),不需要学"完整的输出"
  • 反向传播时,梯度可以直接通过 + 流回前一层,不会因为深而衰减
  • 信息有"高速公路"穿过整个网络 — 哪怕某些层学得不好,原始信息还能被后面的层用上

⑤ LayerNorm:让每一层的"输入分布"保持稳定

训练神经网络最头疼的事情之一叫"内部协变量漂移"(internal covariate shift): 前面层的参数稍微变一下,中间层的输入分布就跟着剧烈变化,导致训练不稳定。

LayerNorm 干一件简单的事:把每个 token 的 d 维向量归一化成均值 0、方差 1,再用可学习的 γ/β 把分布恢复成有用的样子。

LN(x)=xμσγ+β\mathrm{LN}(x) = \frac{x - \mu}{\sigma} \cdot \gamma + \beta
↓ 对应的 Python 实现(可以直接改、直接运行)

和 BatchNorm 的关键差别:LayerNorm 沿"特征维"归一化(每个 token 内部), 和序列里别的位置、batch 里别的样本无关。这对变长序列特别合适。

现在的 LLaMA、PaLM 把 LayerNorm 进一步简化成 RMSNorm(只除以 RMS,不减均值),效果一样还更省。

⑥ Pre-norm vs Post-norm:一个看起来很小但影响巨大的选择

原始 Transformer 论文用的是 post-norm(norm 放在子层之后):

# post-norm(原论文)
x = LayerNorm(x + Sublayer(x))

但后来大家发现这样深层不好训。现在 GPT-3 / LLaMA 都改成了 pre-norm(norm 放在子层之前):

# pre-norm(现代版)
x = x + Sublayer(LayerNorm(x))

差异看起来微妙,但 pre-norm 让残差路径是干净的恒等映射,梯度能从顶层一路顺畅地流回底层。 这是后来大模型能叠到 80、96、120 层的关键之一。

⑦ 代码:完整的 Transformer Block

下面这段把所有零件拼起来。运行能看到输入和输出形状相同 — 所以可以无限叠:

python
直接编辑这段代码即可。输入 np. 看自动提示,⌘/Ctrl + Enter运行。

⑧ 完整的 GPT 长什么样

把上面的 block 包在 embedding 和输出投影之间,就是一个完整的 GPT:

python
直接编辑这段代码即可。输入 np. 看自动提示,⌘/Ctrl + Enter运行。

Karpathy 的 nanoGPT 完整实现(含训练)一共也就 300 行 Python 左右。 你看完这门课就能读懂它的每一行。

⑨ 模型容量的三个旋钮

Transformer 架构定了,模型"做大"实际上只有三个旋钮:

  • 层数 N —— Block 堆几层。GPT-3 = 96 层,LLaMA-70B = 80 层。
  • 维度 d —— 每个 token 多少维。GPT-3 d=12288, LLaMA-70B d=8192。
  • 头数 h —— MHA 拆几个头。通常 d 越大 h 越多,d_k = d/h 一般 64–128。

参数量 ≈ 12 × N × d²。GPT-3:12 × 96 × 12288² ≈ 175B。 这条公式可以让你看到任意一个新模型的尺寸,就估算出它有多少参数。

⑩ 小测验

Q1.Transformer Block 里 FFN(前馈网络)部分占模型参数的大头(>60%)。它的作用是什么?
Q2.为什么 Transformer Block 用 pre-norm(norm 放在子层之前)而不是 post-norm(放在子层之后)?

⑪ 延伸阅读