Chapter 03
整体架构:把零件拼成一张网络
第 02 章给了注意力这个核心算子——本章把它和位置编码、残差、归一化、FFN 拼成一块完整的 block,再堆成整张网络,并讲清经典 encoder-decoder 与现代 decoder-only 的区别。
读完本章,你脑子里会多出这几条
- 注意力是置换等变的——打乱输入、输出同样打乱,模型看到的是一袋 embedding 不是序列;位置信息必须显式注入。
- 残差连接是给梯度的加法高速路:
∂L/∂x = ∂L/∂F(x) + 1,顶层梯度至少原样到底层,解决了深层训练的梯度消失。 - FFN 占 Transformer 约 2/3 参数,是"知识"的主要存储处——W₁ 的行是 keys,W₂ 的列是 values,低层存句法、高层存事实。
- 一块 block = 注意力(通信)+ FFN(计算)+ 残差 + 归一化,堆 N 层就是整张网络;encoder-decoder 多一层 cross-attention,decoder-only 没有 cross-attention。
3.1位置编码:注意力是置换等变的
点积注意力对输入的排列顺序没有感知,"dog bites man" 与 "man bites dog" 在注入位置前产生完全相同的注意力分数——位置必须显式注入。
第 02 章的 scaled dot-product attention 计算的是 softmax(QKᵀ/√d_k)·V。把输入 token 顺序打乱,Q、K、V 矩阵的行跟着一起打乱,输出矩阵的行也同样打乱——任意一对 token 之间的注意力分数完全不变。这个性质叫置换等变(permutation-equivariant):对输入施加某种排列变换,输出也以同样的排列变换响应。
置换等变的直接后果:注意力层把输入序列当成一个集合(set),而不是序列。两个词序完全相反的句子,在加位置编码之前,每个 token 的 query 向量和 key 向量都一模一样,因而所有位置两两之间的相似度也一样。语义就此丢失——"dog bites man" 和 "man bites dog" 对模型来说无法区分。
位置编码加到 embedding 向量上,而不是拼接在后面。拼接会把 d_model 扩到 2d_model,之后所有投影矩阵的规模翻倍,参数量和 FLOPs 都翻倍。加法代价为零:每个维度上,embedding 值和位置信号同时贡献,学习过程会自动把两种信息分配到不同维度或以叠加方式编码。
原版 Transformer 用的是正弦位置编码(sinusoidal positional encoding),公式如下:
PE(pos, 2i) = sin( pos / 10000^(2i / d_model) )
PE(pos, 2i+1) = cos( pos / 10000^(2i / d_model) )
pos: 词在序列中的位置 (0, 1, 2, ...)
i : 编码维度下标 (0 ≤ i < d_model/2)
每个 pos 得到一个 d_model 维向量;
偶数维用 sin、奇数维用 cos;
频率从低(i=0, 周期≈62832)到高(i=d_model/2, 周期≈2π)。
最终: x_embedded = token_embedding + PE ← 相加,不拼接
正弦编码的两个设计意图:第一,不同位置的编码互相不同,模型能区分 pos=0 和 pos=7;第二,位置之间的相对距离可以用线性变换表达——PE(pos+k) 可以写成 PE(pos) 的线性函数,理论上让模型学习"相对距离"变得更容易。
正弦绝对位置编码已被现代 LLM 全面替换。第 05 章的 RoPE(旋转位置编码)通过在 Q/K 上直接旋转、让点积天然依赖相对距离,目前是 Llama、Qwen、Mistral、DeepSeek 等几乎所有主流开放模型的标配。本章讲原版是为了理解机制根源,05 章再讲演化。
3.2残差连接:给梯度一条高速路
残差连接把前向路径变成 y = x + F(x),梯度反传时得到 ∂L/∂x = ∂L/∂y · (∂F(x)/∂x + 1),常数项 1 保证顶层梯度至少原样传到底层。
没有残差连接时,梯度要流过每一层的 ∂F/∂x 项连乘。超过几十层之后,这些小于 1 的导数连乘趋近于零(梯度消失);或大于 1 的连乘指数爆炸。ResNet 在 2015 年用残差连接解决了 CV 领域的深层训练问题,Transformer 从第一版起就内置了同样的机制。
前向: y = x + F(x) ← x 是残差捷径,F(x) 是子层输出
反传: ∂L/∂x = ∂L/∂y · ∂y/∂x
= ∂L/∂y · (∂F(x)/∂x + 1)
└────────────┘
即使 ∂F/∂x → 0
这一项仍 ≥ 1,顶层梯度原样穿透
结论:梯度 = 子层梯度 + 直传梯度
直传梯度永不消失 → 任意深度仍可有效训练
残差连接还有第二个作用:初始化时,如果子层权重初始化为零附近,则 F(x) ≈ 0,整个 block 退化为恒等映射 y ≈ x。这让网络在训练之初接近恒等变换,每一层只需学习在现有表示上的增量——这被称为 identity shortcut(恒等捷径)初始化优势,也是后文 pre-norm 设计的依据之一。
3.3归一化:LayerNorm,以及 post-norm vs pre-norm
归一化稳定每层的激活分布;原版 Transformer 用 post-norm(归一化在残差相加后),现代 LLM 全面切换到 pre-norm(归一化在子层内),原因不是质量而是深层稳定性。
LayerNorm(层归一化)对单个样本的每一个 token 位置独立计算均值和方差,然后缩放还原:
LayerNorm(x) = γ · (x - μ) / √(σ² + ε) + β
x: 单个 token 的 d_model 维向量
μ = mean(x), σ² = var(x) ← 在 d_model 维度上统计
γ, β: 可学习缩放/偏移,形状 (d_model,)
ε: 数值稳定小量 (通常 1e-5)
与 BatchNorm 区别:BN 在 batch 维统计,LN 在特征维统计
→ LN 对 batch size 无要求,NLP 可变长序列天然适用
post-norm(原版)vs pre-norm(现代)
原版 Transformer 的 post-norm 结构把归一化放在残差相加之后:
post-norm: y = LayerNorm( x + Sublayer(x) )
└ 归一化作用在残差相加结果上
└ 归一化"覆盖"了恒等捷径 x
└ 残差捷径被归一化改变了量级
post-norm 的问题:归一化发生在残差相加之后,等于把捷径 x 的量级也归一化掉了——恒等捷径被破坏,初始化时无法形成近乎恒等的 block。超过 12 层后训练极不稳定,需要精心调节 warmup 和学习率;深层(如 N=24 以上)经常需要 Post-LN 专门的初始化方案。
pre-norm 把归一化移到子层内部、残差捷径不经过归一化:
pre-norm: y = x + Sublayer( LayerNorm(x) )
└ 归一化只作用在子层输入上
└ 残差捷径 x 原封不动地加回来
└ 初始化时 Sublayer 输出趋零 → y ≈ x(恒等)
└ 任意深度稳定训练,无需特殊 warmup
浅层模型(N ≤ 12)上,pre-norm 的最终收敛质量略逊于 post-norm。切换到 pre-norm 的驱动力不是质量提升,而是让深层(N=32/64/96)的训练从根本上变得可行——post-norm 在这个规模下需要繁琐的初始化和精细的调参,pre-norm 则开箱即用地稳定。现代 LLM 的层数动辄 32-96 层,所以 pre-norm 成了标配。
| 维度 | post-norm(原版) | pre-norm(现代) |
|---|---|---|
| 公式 | LayerNorm(x + Sublayer(x)) | x + Sublayer(LayerNorm(x)) |
| 捷径状态 | 被归一化,恒等捷径破坏 | 原封不动,恒等捷径完整 |
| 浅层质量(N≤12) | 略优 | 略逊 |
| 深层稳定性(N≥24) | 不稳定,需 warmup / 特殊初始化 | 任意深度稳定,无需特殊调参 |
| 代表模型 | 原版 Transformer(2017) | GPT-2/3, Llama, PaLM, Mistral, Qwen, DeepSeek |
3.4FFN:逐位置计算,知识住在哪
FFN 是 Transformer 里另一半算力:每个 token 位置独立经过两层线性变换+激活,占整个网络约 2/3 参数——它是模型存储事实知识的主要场所。
第 01 章介绍了通信(attention)+ 计算(FFN)的 Karpathy 框架。FFN 是其中的"计算"部分:注意力完成跨位置的信息汇聚后,FFN 对每个 token 位置独立地做非线性变换。原版公式:
FFN(x) = max(0, x·W₁ + b₁)·W₂ + b₂
W₁: (d_model, d_ff) W₂: (d_ff, d_model)
d_ff ≈ 4 × d_model ← 原版 512 → 2048
每个 token 位置独立计算——无跨位置通信
(注意力才是唯一跨位置算子)
参数量对比(d_model = d):
attention 投影:4 个 (d, d) 矩阵 = 4d²
FFN:W₁(d, 4d) + W₂(4d, d) = 8d²
→ FFN 占整个 block ~2/3 参数
键值记忆视角(Geva 2021)
Geva 等人(2021,arxiv 2012.14913)给出了 FFN 的另一种解读,与第 02 章 attention 的软字典查找形成对照:把 FFN(x) = W₂ · ReLU(W₁ x) 拆开看,W₁ 的每一行是一个 key——当输入 x 与该 key 内积为正时,对应的神经元激活;W₂ 的对应列是这个神经元的 value——它直接加权叠加进输出空间,偏向某些 token 的概率分布。
实验支持:低层神经元在输入匹配浅层句法模式(冠词、词性)时激活,高层神经元对应具体事实(如"法国首都是巴黎"这条知识的召回恰好发生在高层 FFN 的某几个神经元激活时)。这不仅是比喻——直接修改 W₂ 中对应列,就能改变模型对某条事实的"记忆",知识编辑方法(knowledge editing)正是基于此。
这个视角下,attention 在序列上做"检索"(哪些位置的表示值得关注),FFN 做"查表"(当前 token 应该调出哪条知识)。两者交替运行:先让 token 交换信息(第 02 章的 self-attention),再让每个 token 基于汇聚后的表示独立查取知识(FFN)。
模型在第 20 层的 FFN 中"召回"了某条事实,但如果只看最终输出没有出错,这条事实调用过程是否会被 outcome 层的评测发现?这涉及哪个 Karpathy 框架中的原则?
展开思路(先停 10 秒再点)
不会被 outcome 层发现——outcome 只看最终输出对不对,看不见某一层 FFN 激活了哪些神经元。要观察 FFN 的内部行为需要在组件层做激活分析(probing),这与 01 章中通信(attention)+计算(FFN)框架的划分一致:FFN 是逐位置的计算单元,其内部激活不会在 token 间流动,因此在正常推理输出中不可见。
3.5一块完整的 Transformer block
一个 block = pre-norm → attention(通信)→ 残差 → pre-norm → FFN(计算)→ 残差,共两条残差高速路,把第 02 章的注意力和本章的 3.2–3.4 节串在一起。
把前四节组合起来,现代 decoder-only 的单个 block(pre-norm 版本)前向流程如下:
输入: x (seq_len, d_model)
# 第一条残差:attention 子层
h = LayerNorm(x) # pre-norm,不改变残差捷径
h = MultiHeadAttention(h, h, h) # self-attention (Q=K=V=h)
# decoder: 加因果掩码
x = x + h # 残差相加 ← 第一条高速路
# 第二条残差:FFN 子层
h = LayerNorm(x) # pre-norm
h = FFN(h) # 两层线性 + 激活,逐位置
x = x + h # 残差相加 ← 第二条高速路
输出: x (seq_len, d_model) # 形状不变,供下一层继续
两处 pre-norm + 两条残差是现代 LLM 的标准骨架。整个 block 的输入输出形状完全相同,因此可以任意堆叠。
3.6堆叠 N 层,与 embedding / unembedding
将相同结构的 block 堆叠 N 次,头尾各加一个 embedding/unembedding 矩阵,就是完整的 Transformer;unembedding 在多数现代 LLM 中与 input embedding 矩阵共享权重(权重绑定)。
单个 block 的输入输出形状完全相同(seq_len, d_model),因此可以直接串联。完整网络的前向流程:
输入: token ids (batch, seq_len)
1. Embedding
x = Embedding(token_ids) # (batch, seq_len, d_model)
x = x + PositionalEncoding(seq) # 加位置编码
2. N 个相同的 block(各含 attention + FFN + 残差 + LN)
for i in range(N):
x = TransformerBlock_i(x) # 形状不变
3. 最终 LayerNorm(pre-norm 架构的末端 norm)
x = LayerNorm(x)
4. Unembedding → logits
logits = x @ Embedding.weight.T # 权重绑定:转置复用 embedding
# logits: (batch, seq_len, vocab_size)
5. 下一 token 概率
probs = softmax(logits[:, -1, :]) # 取最后位置,归一化成概率分布
权重绑定(Weight Tying)
多数现代 LLM(GPT 系列、Llama、Mistral 等)让 unembedding 矩阵 = input embedding 矩阵的转置。输入端:token id → d_model 维向量;输出端:d_model 维隐状态 → vocab_size 维 logits(点积相似度)。两端用的是同一个矩阵,正向和反向读取。
权重绑定有两个好处:第一,节省约 vocab_size × d_model 的参数量(对 vocab=32000、d=4096 的模型约 500M 参数,接近模型总量的 10–20%);第二,语义一致性——embedding 空间和输出 logit 空间共享,模型预测"下一 token 是 X"等价于"隐状态与 X 的 embedding 最相似",两者形成自洽的语义空间。
logits(未归一化的得分向量)经过 softmax 变成概率分布,之后再用 temperature(温度)或 top-k/top-p 采样策略选出下一个 token。logits 的绝对值不重要,相对大小决定概率分配;softmax 的数值稳定写法是先减 max 再 exp(等价变换,防止 fp16 溢出)。第 04 章会展开采样策略。
3.7经典 encoder-decoder → decoder-only
原版 Transformer(2017)是 encoder-decoder 结构,decoder 含额外的 cross-attention 把 encoder 上下文引入;现代 GPT/Llama 等 decoder-only 删掉了整个 encoder 和 cross-attention,每块只是因果自注意力 + FFN。
经典 encoder-decoder(Vaswani 2017)
原版论文为机器翻译设计,encoder 读入源语言序列、decoder 生成目标语言序列。结构差异集中在 decoder 的多一层:
- Encoder block:多头自注意力(双向,所有位置互相可见)+ FFN + 残差 + LayerNorm,共 N=6 层。
- Decoder block:比 encoder 多一个 cross-attention 子层。每个 decoder block 含三个子层:① 带掩码的自注意力(因果,只看已生成的 token);② cross-attention(Q 来自 decoder 当前隐状态,K/V 来自 encoder 最后一层的输出);③ FFN。
cross-attention 是 encoder-decoder 的连接点:decoder 的每个 token 通过 Q 向 encoder 的所有 token 发出查询,把源序列的信息"拉入"目标序列的生成过程。第 02 章的 self vs cross attention 解释了 cross-attention 和 self-attention 的数学区别(K/V 来自不同序列)。
decoder-only(GPT、Llama 等现代 LLM)
decoder-only 架构删掉了整个 encoder 和所有 cross-attention:每个 block 只有因果自注意力(causal self-attention)+ FFN + 残差 + LayerNorm。输入和输出都是同一个序列,模型预测下一个 token,训练目标是 next-token prediction。
decoder-only 不等于"只用了原版 Transformer 的 decoder 侧"。原版 decoder 里有 cross-attention,decoder-only 没有——两者每个 block 的子层数不同(原版 3 个,decoder-only 2 个)。把 decoder-only 当成"砍掉了 encoder 的 enc-dec"会误解参数量和结构。
为什么 decoder-only 成为主流
几个互相强化的原因:
- 统一目标:next-token prediction 可以用一个极简目标在海量无标注文本上预训练。encoder-decoder 需要有对应的输入-输出对(翻译语料、摘要语料),数据规模受限。
- 生成任务自然:所有生成任务(问答、代码、摘要)都可以转化为"给定前缀,预测后续",decoder-only 天然适配。
- scaling 表现优越:GPT-3 证明单纯的 decoder-only 在足够规模下具备惊人的 few-shot 能力,推动了整个领域的预训练范式迁移。
- 工程简洁:没有 cross-attention 的参数管理和 encoder/decoder 分离带来的推理复杂性,整个前向传播是单一的 N 层堆叠。
§本章 self-check
先合上教程,把能想到的答案写在纸上或编辑器里。写完再点开答案对照——直接点开等于把这一节当再读一遍。
- 为什么注意力需要位置编码?"置换等变"的精确含义是什么?正弦编码是加到 embedding 上还是拼接?
- 写出残差连接的梯度公式
∂L/∂x = ?,解释其中常数项 1 的作用。post-norm 和 pre-norm 各在哪个位置放 LayerNorm? - FFN 占整个 block 大约多少比例的参数(给出推导过程中的参数量对比)?Geva 2021 的键值记忆视角如何解读 W₁ 和 W₂?
- 经典 encoder-decoder 的 decoder block 有几个子层?decoder-only 的每个 block 有几个子层?cross-attention 的 Q/K/V 分别来自哪里?
答案(先做完再展开)
- 注意力是置换等变的:打乱输入,输出同样打乱;任意两 token 之间的注意力分数对词序无感知,模型看到的是一袋 embedding 而非序列。精确含义:对输入施加排列变换 π,输出以同样的 π 响应。正弦编码是加法(
x = embedding + PE),不是拼接——拼接会让 d_model 翻倍,代价为零的加法已足够。 ∂L/∂x = ∂L/∂y · (∂F(x)/∂x + 1)。常数项 1 保证即使子层梯度 ∂F/∂x 趋近于零,顶层梯度仍原样传到 x,消除梯度消失。post-norm:LayerNorm(x + Sublayer(x)),归一化在残差相加后;pre-norm:x + Sublayer(LayerNorm(x)),归一化在子层内、残差捷径不经过归一化。- attention 投影 4 个 (d, d) 矩阵 = 4d²;FFN W₁(d, 4d) + W₂(4d, d) = 8d²;FFN 占约 8/(4+8) = 2/3。键值记忆:W₁ 的每一行是 key(对应一种输入模式的检测器),W₂ 的对应列是 value(激活后叠加到输出的贡献向量),低层存句法、高层存事实。
- decoder block(enc-dec)有 3 个子层:带掩码自注意力 + cross-attention + FFN。decoder-only 每 block 有 2 个子层:因果自注意力 + FFN。cross-attention:Q 来自 decoder 当前隐状态,K/V 来自 encoder 最后一层的输出——decoder 通过 Q 向 encoder 的所有 token 查询,把源序列信息引入生成过程。