一句话总结:一段文字先变成 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 = 768
  • n_layers = 12
  • n_heads = 12,所以每头 d_head = 64
  • d_ff = 3072
  • vocab_size = 50,257
  • 最大训练位置表长度为 1,024
小沈阳例子从文字、GPT-2 Token IDs、embedding、12 层 Pre-Norm Block 到词表 logits 的完整前向传播

15.1.2 GPT-1 与 GPT-2 的归一化位置

两代模型都使用因果 self-attention,但 LayerNorm 位置不同:

GPT-1GPT-2
子层结构LN(x + Sublayer(x))x + Sublayer(LN(x))
常用名称Post-NormPre-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 Pre-Norm Block 内部的 LayerNorm、masked multi-head attention、MLP、两条残差路径和张量形状

一个 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;当前位置仍可看见自己。

教学用 Attention 分数经过 causal mask 和逐行 Softmax 后形成权重

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 final hidden state 通过共享 Token embedding 的转置得到词表 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 参数量

GPT-2 Small 的 Token embedding、位置 embedding、Attention、MLP、LayerNorm 与共享输出头参数账本

按公开配置直接把 weight、bias、embedding 与 LayerNorm 参数相加:

组件近似参数量占比
Token embedding38.60M31.0%
Position embedding0.79M0.6%
Attention(12 层,含 bias)28.35M22.8%
MLP(12 层,含 bias)56.67M45.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、交叉熵、反向传播梯度到优化器更新参数的四个独立阶段

四个阶段不要混在一起:

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操作关键输出
1GPT-2 BPEToken IDs [B, T]
2Token embedding lookup[B, T, 768]
3+ learned position embeddingresidual stream [B, T, 768]
4Pre-LN masked MHAcontextual update [B, T, 768]
5第一条 residual add[B, T, 768]
6Pre-LN MLP + 第二条 residual add[B, T, 768]
7重复 12 层 + final LNhidden states [B, T, 768]
8共享输出矩阵logits [B, T, 50,257]

15.9.2 核心认知

Transformer 不是在 Block 内永远维持同一个形状。准确说法是:residual stream 的入口和出口宽度固定,Block 内部会出现 head、T×T attention 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 铺路。

引用本文 / Cite
Zhang, Wayland (2026). 第 15 章:Transformer 完整前向传播 - 从输入到输出. In Transformer 架构:从直觉到实现. https://waylandz.com/llm-transformer-book/%E7%AC%AC15%E7%AB%A0-Transformer%E5%AE%8C%E6%95%B4%E5%89%8D%E5%90%91%E4%BC%A0%E6%92%AD-%E4%BB%8E%E8%BE%93%E5%85%A5%E5%88%B0%E8%BE%93%E5%87%BA/
@incollection{zhang2026transformer_15_-Transformer_-,
  author = {Zhang, Wayland},
  title = {第 15 章:Transformer 完整前向传播 - 从输入到输出},
  booktitle = {Transformer 架构:从直觉到实现},
  year = {2026},
  url = {https://waylandz.com/llm-transformer-book/%E7%AC%AC15%E7%AB%A0-Transformer%E5%AE%8C%E6%95%B4%E5%89%8D%E5%90%91%E4%BC%A0%E6%92%AD-%E4%BB%8E%E8%BE%93%E5%85%A5%E5%88%B0%E8%BE%93%E5%87%BA/}
}