Chapter 03

整体架构:把零件拼成一张网络

第 02 章给了注意力这个核心算子——本章把它和位置编码、残差、归一化、FFN 拼成一块完整的 block,再堆成整张网络,并讲清经典 encoder-decoder 与现代 decoder-only 的区别。

本章交付:位置编码必要性 + 残差梯度分析 + post/pre-norm 选择依据 + FFN 键值记忆观 + 完整 block 前向 + 堆叠结构 + enc-dec vs decoder-only | 带走的能力:能画出一个完整的 Transformer 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) 的线性函数,理论上让模型学习"相对距离"变得更容易。

洞察 · 2026 年的状态

正弦绝对位置编码已被现代 LLM 全面替换。第 05 章的 RoPE(旋转位置编码)通过在 Q/K 上直接旋转、让点积天然依赖相对距离,目前是 Llama、Qwen、Mistral、DeepSeek 等几乎所有主流开放模型的标配。本章讲原版是为了理解机制根源,05 章再讲演化。

无位置编码 dog bites man 「dog bites man」 man bites dog 「man bites dog」 注意力分数相同 两句 token 相同,仅词序不同 → 模型无法区分语义 加位置编码后 dog pos=0 bites pos=1 man pos=2 man pos=0 bites pos=1 dog pos=2 注意力分数不同 dog 在 pos=0 / pos=2 产生 不同 embedding → 可区分
图 3.2置换等变性示意。左侧:无位置编码时,「dog bites man」和「man bites dog」的每个 token embedding 相同(仅顺序不同),注意力分数完全相同,模型无法区分两句语义。右侧:加入位置编码后,同一个词在不同位置得到不同的最终 embedding,注意力分数随词序改变。注意:位置信息不是可选的优化,而是让注意力能区分序列的最低条件。

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数学
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(原版 Vaswani 2017)结构
post-norm:  y = LayerNorm( x + Sublayer(x) )
            └ 归一化作用在残差相加结果上
            └ 归一化"覆盖"了恒等捷径 x
            └ 残差捷径被归一化改变了量级

post-norm 的问题:归一化发生在残差相加之后,等于把捷径 x 的量级也归一化掉了——恒等捷径被破坏,初始化时无法形成近乎恒等的 block。超过 12 层后训练极不稳定,需要精心调节 warmup 和学习率;深层(如 N=24 以上)经常需要 Post-LN 专门的初始化方案。

pre-norm 把归一化移到子层内部、残差捷径不经过归一化:

pre-norm(GPT-2 起、Llama/PaLM 等现代 LLM 标配)结构
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 成了标配。

表 3.3 · post-norm vs 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 位置独立地做非线性变换。原版公式:

Position-wise FFN数学
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)正是基于此。

洞察 · FFN 是参数存储、attention 是检索

这个视角下,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 版本)前向流程如下:

pre-norm Transformer block 前向伪代码
输入: 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 的输入输出形状完全相同,因此可以任意堆叠。

输入 x 残差高速路 1 LayerNorm pre-norm Multi-Head Attention 通信 — 跨位置信息汇聚 + 残差相加① 残差高速路 2 LayerNorm pre-norm FFN 计算 — 逐位置独立 + 残差相加② 输出 x(形状不变,供下一层)
图 3.1pre-norm Transformer block。左侧朱红虚线是第一条残差高速路(绕过 LayerNorm + Attention),右侧是第二条(绕过 LayerNorm + FFN)。注意:两处 LayerNorm 都在子层内部,残差捷径上的 x 原封不动直接相加——这是 pre-norm 与 post-norm 的本质区别,也是任意深度都能稳定训练的原因。

3.6堆叠 N 层,与 embedding / unembedding

将相同结构的 block 堆叠 N 次,头尾各加一个 embedding/unembedding 矩阵,就是完整的 Transformer;unembedding 在多数现代 LLM 中与 input embedding 矩阵共享权重(权重绑定)。

单个 block 的输入输出形状完全相同(seq_len, d_model),因此可以直接串联。完整网络的前向流程:

完整 Transformer 前向(decoder-only)伪代码
输入: 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 的关系

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 不是「encoder-decoder 的一半」

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 层堆叠。
Encoder-Decoder(2017) ENCODER 双向 Self-Attn FFN × N 层 Encoder 输出 K / V DECODER 因果 Self-Attn Cross-Attention Q ← decoder FFN × N 层(每层 3 个子层) 输出 logits cross-attention: decoder 读取 encoder 表示 Decoder-Only(GPT/Llama) 因果 Self-Attention Cross-Attention ✕ 不存在 FFN × N 层(每层 2 个子层) 输出 logits 每块仅 2 个子层,无 cross-attn 输入即源、生成即目标 next-token prediction 一个目标
图 3.3经典 encoder-decoder(左)与 decoder-only(右)并排对照。朱红高亮的 Cross-Attention 是区分两者的核心:encoder-decoder 的每个 decoder block 含三个子层,cross-attention 把 encoder 输出的 K/V 引入;decoder-only 删掉了整个 encoder 和 cross-attention,每块只有因果自注意力 + FFN 两个子层。注意:decoder-only 不是「砍掉 encoder 的 enc-dec」——每个 block 的结构本就不同,参数量和子层数也不同。

§本章 self-check

先合上教程,把能想到的答案写在纸上或编辑器里。写完再点开答案对照——直接点开等于把这一节当再读一遍。

  1. 为什么注意力需要位置编码?"置换等变"的精确含义是什么?正弦编码是加到 embedding 上还是拼接?
  2. 写出残差连接的梯度公式 ∂L/∂x = ?,解释其中常数项 1 的作用。post-norm 和 pre-norm 各在哪个位置放 LayerNorm?
  3. FFN 占整个 block 大约多少比例的参数(给出推导过程中的参数量对比)?Geva 2021 的键值记忆视角如何解读 W₁ 和 W₂?
  4. 经典 encoder-decoder 的 decoder block 有几个子层?decoder-only 的每个 block 有几个子层?cross-attention 的 Q/K/V 分别来自哪里?
答案(先做完再展开)
  1. 注意力是置换等变的:打乱输入,输出同样打乱;任意两 token 之间的注意力分数对词序无感知,模型看到的是一袋 embedding 而非序列。精确含义:对输入施加排列变换 π,输出以同样的 π 响应。正弦编码是加法(x = embedding + PE),不是拼接——拼接会让 d_model 翻倍,代价为零的加法已足够。
  2. ∂L/∂x = ∂L/∂y · (∂F(x)/∂x + 1)。常数项 1 保证即使子层梯度 ∂F/∂x 趋近于零,顶层梯度仍原样传到 x,消除梯度消失。post-norm:LayerNorm(x + Sublayer(x)),归一化在残差相加后;pre-norm:x + Sublayer(LayerNorm(x)),归一化在子层内、残差捷径不经过归一化。
  3. attention 投影 4 个 (d, d) 矩阵 = 4d²;FFN W₁(d, 4d) + W₂(4d, d) = 8d²;FFN 占约 8/(4+8) = 2/3。键值记忆:W₁ 的每一行是 key(对应一种输入模式的检测器),W₂ 的对应列是 value(激活后叠加到输出的贡献向量),低层存句法、高层存事实。
  4. decoder block(enc-dec)有 3 个子层:带掩码自注意力 + cross-attention + FFN。decoder-only 每 block 有 2 个子层:因果自注意力 + FFN。cross-attention:Q 来自 decoder 当前隐状态,K/V 来自 encoder 最后一层的输出——decoder 通过 Q 向 encoder 的所有 token 查询,把源序列信息引入生成过程。