一句话总结:一段文字先变成 Token IDs,再变成 Token embedding 与位置 embedding 的和;随后经过多层 Pre-Norm Block,最后投影成词表 logits。Softmax、贪心或采样再决定下一个 Token。把这条路的形状追清楚,就理解了 GPT-2 如何完成一次计算。
15.1 全景图:Decoder-Only 架构
15.1.1 “Decoder-Only”到底省掉了什么?
GPT-1、GPT-2、GPT-3 与 LLaMA 都采用自回归的 Decoder-Only Transformer。它不是把原始 Transformer 的 Decoder 原封不动地剪下来:原始 Decoder 还有一层读取 Encoder 输出的 cross-attention,而纯语言模型没有 Encoder,也就没有这层 cross-attention。留下的是 masked self-attention、逐位置 MLP、残差连接和归一化。
本章用 GPT-2 Small 作一条一致的参考线:
d_model = 768n_layers = 12n_heads = 12,所以每头d_head = 64d_ff = 3072vocab_size = 50,257- 最大训练位置表长度为 1,024
15.1.2 GPT-1 与 GPT-2 的归一化位置
两代模型都使用因果 self-attention,但 LayerNorm 位置不同:
| GPT-1 | GPT-2 | |
|---|---|---|
| 子层结构 | LN(x + Sublayer(x)) | x + Sublayer(LN(x)) |
| 常用名称 | Post-Norm | Pre-Norm |
| 最后的 LayerNorm | 无同样的 GPT-2 式 final LN | 有 |
GPT-2 论文还改变了初始化、词表和 context size,因此不能说两者“只有 LayerNorm 不同”。这一章选择 GPT-2,只是为了让整条数据流有一个具体版本。
15.2 Step 1-3:准备输入
15.2.1 Step 1:Tokenization
仍用原来的句子:
输入:"小沈阳江西演唱会邀请了"
为了与本章的 GPT-2 参数保持一致,这里改用 GPT-2 自己的 byte-level BPE,而不是前面演示过的 cl100k_base。实测结果是:
Token IDs:
[22887, 237, 162, 110, 230, 165, 246, 111, 162, 109, 253,
164, 98, 123, 162, 120, 242, 161, 242, 109, 27670, 21253,
224, 222, 46237, 115, 12859, 228]
序列长度 T = 28
同一个汉字可能由多个 Token 共同表示;一个 Token 甚至可能只含某个 UTF-8 字符的一部分字节。因此这里处理的是 Token 序列,不是“一个汉字必然对应一个 ID”。
15.2.2 Step 2:Token Embedding
每个 ID 都在可学习矩阵 E ∈ R^(50257×768) 中查一行:
token_ids [B, 28]
token_embedding(token_ids) [B, 28, 768]
这些行是上下文无关的可学习参数。训练会让它们承载词法与统计特征,但不要把某一行当成模型对这个词的全部“理解”;上下文信息要在后面的 Block 中形成。
15.2.3 Step 3:加上位置 Embedding
GPT-2 使用可学习的绝对位置表 P ∈ R^(1024×768):
positions [0, 1, ..., 27]
position_embedding(positions) [28, 768]
X = token_embedding + position_embedding
X [B, 28, 768]
位置向量沿 batch 维广播。GPT-2 的 embedding dropout 可在训练时作用于相加后的结果,推理时关闭。
这里的“相加”沿用第 14 章的结论:两种信号共同影响 X,但这个和不是可以唯一拆回两张表的无损压缩。另一些模型会用 RoPE 或 ALiBi,也就不会在这一位置做同样的加法。
15.3 Step 4-6:一个 Transformer Block 内部
一个 GPT-2 Block 可以写成两行:
U = X + Dropout(MHA(LN₁(X)))
Y = U + Dropout(MLP(LN₂(U)))
Dropout 只在训练模式下生效,具体 rate 由配置决定。最容易混淆的一点是:Block 的输入输出接口都保持 [B, T, 768],不代表 Block 内部所有维度都不变。
15.3.1 Step 4:Masked Multi-Head Attention
先对 X 做 LayerNorm,再生成 Q、K、V。用按 head 展开的形状表示:
LN₁(X) [B, 28, 768]
Q, K, V [B, 12, 28, 64]
Q @ Kᵀ [B, 12, 28, 28]
每个 head 的完整分数是:
scores = Q @ Kᵀ / √64 + causal_mask
weights = Softmax(scores, dim=-1)
head_output = weights @ V [B, 12, 28, 64]
Q 与 K 的点积是学到的兼容度分数,不是 cosine similarity,也不是“两个词在语义上相关的概率”。Causal mask 把未来位置加成 -∞,所以 Softmax 后它们的权重为 0;当前位置仍可看见自己。
12 个 head 的输出要先拼回 [B, 28, 768],再经过 W_O 混合,最后才能与 residual stream 逐元素相加:
concat(heads) [B, 28, 768]
attention_delta = concat @ W_O [B, 28, 768]
U = X + attention_delta [B, 28, 768]
15.3.2 Step 5:第一条残差路径
残差连接给变换分支旁边加了一条恒等路径。它通常让深层网络更容易优化,也允许某个子层暂时产生接近 0 的更新;但它不保证梯度永不消失,也不保证永不爆炸。
这里绕过子层的是当前 residual stream X,不是把最初的 Token embedding 原封不动保存到最后一层。
15.3.3 Step 6:逐位置 MLP 与第二条残差路径
GPT-2 的 MLP 对每个位置独立应用同一组参数:
LN₂(U) [B, 28, 768]
Linear 768 → 3072
GELU
Linear 3072 → 768
Y = U + mlp_delta [B, 28, 768]
MLP 内部确实扩宽到 3072,所以“整个 Block 中维度从不变化”是错误的;不变的是 residual stream 的外部宽度。
在 GPT-2 Small 里,MLP 权重约占总参数的 45.5%。一些分析发现 MLP 与事实关联、模式变换有关,但知识并不只住在 MLP:embedding、Attention 和其他层也共同参与。
15.4 Step 7:堆叠 12 个 Block,再做 Final LayerNorm
X₀ [B, 28, 768]
→ Block 1 → Block 2 → ... → Block 12
X₁₂ [B, 28, 768]
→ Final LayerNorm
H [B, 28, 768]
每层参数各自独立;只是它们共享同一种结构和输入输出形状。不同层可能呈现局部、句法或更抽象的倾向,但这不是“前几层固定管语法、后几层固定管推理”的硬编码课程表。
15.5 Step 8:从 Hidden State 到词表 Logits
GPT-2 把输出矩阵与 Token embedding 共享权重(weight tying)。设 embedding 表为 E [50257, 768]:
H [B, 28, 768]
logits = H @ Eᵀ [B, 28, 50,257]
第 i 个 logit 是当前 hidden state 与 E[i] 的点积。它是一个未归一化兼容度分数,会受方向和模长共同影响;不要直接叫作语义相似度或概率。
15.5.1 训练时用所有位置
训练的目标序列向右错一位。实现通常让 CrossEntropy 直接接收 logits:
input : token₀, token₁, ..., token₂₆
target: token₁, token₂, ..., token₂₇
loss = CrossEntropy(logits[:, :-1, :], targets[:, 1:])
数值稳定的 CrossEntropy 会在内部完成 log_softmax,不应该先把 probabilities 传进去。
15.5.2 生成时通常只取最后位置
next_logits = logits[:, -1, :] [B, 50,257]
- Greedy decoding:取
argmax(next_logits) - Sampling:把 logits 经过温度、Softmax、top-k/top-p 等处理后抽样
所以“概率最高的 Token 就是输出”只描述贪心解码;采样可能选中另一个候选。
15.6 完整形状追踪
输入文字 B 条字符串
GPT-2 BPE [B, 28]
Token embedding [B, 28, 768]
+ position embedding [B, 28, 768]
每个 Block 的 residual stream: [B, 28, 768]
Q/K/V(按 head) [B, 12, 28, 64]
Attention scores [B, 12, 28, 28]
MLP 中间层 [B, 28, 3072]
12 个 Block 后 [B, 28, 768]
Final LayerNorm [B, 28, 768]
Vocabulary logits [B, 28, 50,257]
生成时取最后位置 [B, 50,257]
选择或采样一个 Token ID [B]
现在可以把“形状不变”说准确了:在每个 Block 的入口和出口,hidden width 保持 768;Attention score、head split 与 MLP 内部都有自己的形状。
15.7 GPT-2 Small 参数量
按公开配置直接把 weight、bias、embedding 与 LayerNorm 参数相加:
| 组件 | 近似参数量 | 占比 |
|---|---|---|
| Token embedding | 38.60M | 31.0% |
| Position embedding | 0.79M | 0.6% |
| Attention(12 层,含 bias) | 28.35M | 22.8% |
| MLP(12 层,含 bias) | 56.67M | 45.5% |
| LayerNorm(25 个) | 0.04M | 不到 0.1% |
| LM head | 与 Token embedding 共享,0 个额外参数 | — |
合计约 124.4M。GPT-2 论文与早期介绍曾把 Small 写成 117M;OpenAI 官方仓库后来明确注记,早期参数统计有误。这也是为什么同一个 checkpoint 会同时看到“117M”这个历史名字和约 124M 的直接计数。
15.8 训练时的反向传播
四个阶段不要混在一起:
logits = model(input_ids) # 1. forward:参数还没变
loss = cross_entropy(logits, y) # 2. 计算标量 loss
loss.backward() # 3. 计算并累积梯度,参数仍没变
optimizer.step() # 4. 根据梯度更新参数
optimizer.zero_grad() # 清理梯度,准备下一步
如果输出 head 与 Token embedding 共享,二者实际上是同一个参数,梯度会汇总到同一张表。反向传播还会经过 final LN、12 个 Block 和位置 embedding;优化器再统一应用更新。
15.9 本章总结
15.9.1 八个阶段
| Step | 操作 | 关键输出 |
|---|---|---|
| 1 | GPT-2 BPE | Token IDs [B, T] |
| 2 | Token embedding lookup | [B, T, 768] |
| 3 | + learned position embedding | residual stream [B, T, 768] |
| 4 | Pre-LN masked MHA | contextual update [B, T, 768] |
| 5 | 第一条 residual add | [B, T, 768] |
| 6 | Pre-LN MLP + 第二条 residual add | [B, T, 768] |
| 7 | 重复 12 层 + final LN | hidden states [B, T, 768] |
| 8 | 共享输出矩阵 | logits [B, T, 50,257] |
15.9.2 核心认知
Transformer 不是在 Block 内永远维持同一个形状。准确说法是:residual stream 的入口和出口宽度固定,Block 内部会出现 head、
T×Tattention matrix 和更宽的 MLP。最终 hidden state 再投影为词表 logits;训练用全部位置算 next-token loss,生成通常只用最后一个位置选下一个 Token。
本章交付物
学完这一章,你应该能够:
- 从文字一路追踪到词表 logits
- 区分 residual stream 形状与 Block 内部形状
- 写出 Pre-Norm Block 的两条残差公式
- 解释 causal mask、logit、Softmax 与 decoding 的不同职责
- 解释 backward 与 optimizer update 为什么不是同一步
- 按配置估算 GPT-2 Small 的参数量
代码实现
完整代码会在 Part 5 展开:
- 第 18 章:
model.py— 模型定义 - 第 19 章:
train.py— 训练循环 - 第 20 章:
inference.py— 推理逻辑
下一章预告
前向函数本身可以相同,但训练与生成调用它的方式不同:训练一次并行预测许多位置,生成则把新 Token 接回输入并继续。下一章就把这两种模式拆开,也为后面的 KV Cache 铺路。