完整 Transformer:编码器 + 解码器 本节摘要:注意力是主角,其余一切——残差连接、层归一化、前馈网络、交叉注意力——都是让你能把注意力块堆深的脚手架。单层注意力只是个特征提取器,一次矩阵乘法装不下语言所需的表达容量,你需要深度,而深度没有正确的「管道」就会崩。2017 年的 Vaswani 论文用六个设计决策把一层注意力打包成一个可堆叠的块:嵌入与位置信号、自注意力、前馈网络(FFN)、残差连接、层归一化、交叉注意力(仅解码器)。此后每一个 Transformer——编码器型(BERT)、解码器型(GPT)、编码器-解码器型(T5)——都继承同一副骨架。到 2026 年,这些块被进一步打磨(RMSNorm、SwiGLU、pre-norm、RoPE),但骨架分毫未改。
本节摘要:注意力是主角,其余一切——残差连接、层归一化、前馈网络、交叉注意力——都是让你能把注意力块堆深的脚手架。单层注意力只是个特征提取器,一次矩阵乘法装不下语言所需的表达容量,你需要深度,而深度没有正确的「管道」就会崩。2017 年的 Vaswani 论文用六个设计决策把一层注意力打包成一个可堆叠的块:嵌入与位置信号、自注意力、前馈网络(FFN)、残差连接、层归一化、交叉注意力(仅解码器)。此后每一个 Transformer——编码器型(BERT)、解码器型(GPT)、编码器-解码器型(T5)——都继承同一副骨架。到 2026 年,这些块被进一步打磨(RMSNorm、SwiGLU、pre-norm、RoPE),但骨架分毫未改。本节就是这副骨架:带你把六块积木拼成一个 2 层编码器 + 2 层解码器,看一个向量如何流过它,再用现代组件把 2017 版块升级成 2026 版。读完本节,你能说清为什么深度需要残差、为什么 pre-norm 干掉了 post-norm、以及怎么按任务选编码器/解码器/编码器-解码器。
阅读完本节,你应当能够:
单个注意力层是一个特征提取器,不是一个模型。每层一次矩阵乘不足以承载语言的表达容量。你需要深度,而深度没有正确的管道就会崩。
Vaswani 2017 用六个设计决策把一层注意力变成可堆叠的块。本节就是这副骨架,06 节把它特化成编码器,07 节特化成解码器,08 节特化成编码器-解码器。
W_2 · activation(W_1 · x),默认扩展比 4 倍。x + sublayer(x),没有它,约 6 层之后梯度消失。LayerNorm 或 RMSNorm(现代),稳定残差流。看一个向量流过一个块:注意力跨位置混合,残差把它带向前,FFN 再变换它,归一化保持流稳定。
编码器块(双向,无掩码): x → LN → MHA(自注意力) → + → LN → FFN → + → out ^ ^ └─── 残差 ─────┘ 解码器块(自回归 + 看编码器): x → LN → MHA(掩码自注意力) → + → LN → MHA(交叉到编码器) → + → LN → FFN → + → out
解码器每块三个子层。中间那个——交叉注意力——是信息从编码器流向解码器的唯一通道。在纯解码器架构(GPT)里,交叉注意力被省略,只剩掩码自注意力 + FFN。
编码器是双向的,无掩码,所有位置互相可见;解码器是自回归的,未来位置必须被遮住(因果掩码,见第 02 节)。
原论文用 post-norm:LN(x + sublayer(x)),归一化在残差相加之后。2019 年左右它失宠——不精心 warmup 就难训深。Pre-norm(x + sublayer(LN(x)),归一化在子层之前)是 2026 年的默认:Llama、Qwen、GPT-3+、Mistral 全用 pre-norm。
差别看似微小,后果巨大:post-norm 把残差流也归一化了,深堆叠时主干信号会被反复重塑,激活容易爆炸或塌缩;pre-norm 让残差主干保持原样,梯度可以一路无阻碍地传到底层。
Vaswani 2017 出货时是 LayerNorm + ReLU,生产块已经把两者都换了:
| 组件 | 2017 | 2026 |
|---|---|---|
| 归一化 | LayerNorm | RMSNorm |
| FFN 激活 | ReLU | SwiGLU |
| FFN 扩展比 | 4× | 2.6×(SwiGLU 用三矩阵,总参数匹配) |
| 位置 | 绝对正弦 | RoPE |
| 注意力 | 完整 MHA | GQA(或 MLA) |
| 偏置项 | 有 | 无 |
RMSNorm 去掉了 LayerNorm 的去中心化步骤(少一次减法),省算力且经验上至少同等稳定。SwiGLU(Swish(W1·x) ⊙ (W3·x) 再过 W2)在 Llama、PaLM、Qwen 论文里一致地比 ReLU/GELU FFN 低约 0.5 点困惑度。
💡 SwiGLU 的本质:门控线性单元(GLU)。
Swish(W1·x)当门,决定W3·x的哪些维度被放过——让 FFN 学到「条件性」的非线性,而非对每个输入一刀切。
对一块 d_model = d、FFN 扩展比 r:
4·d²(Q、K、V、O 四个投影)3·d·(r·d) ≈ 3r·d²取 d=4096, r=2.6, 层数=32(约等于 Llama 3 8B),总参数 32·(4·4096² + 3·2.6·4096²) ≈ 32·(16 + 32)M ≈ 1.5B/层 × 32 ≈ 7B(再加嵌入和输出头),与公布计数吻合。
完整接线见原课程 phases/07-transformers-deep-dive/05-full-transformer/code/main.py。关键函数:
def layer_norm(x, eps=1e-5): mean = sum(x) / len(x) var = sum((xi - mean) ** 2 for xi in x) / len(x) return [(xi - mean) / (var + eps) ** 0.5 for xi in x] def rms_norm(x, eps=1e-6): rms = (sum(xi ** 2 for xi in x) / len(x) + eps) ** 0.5 return [xi / rms for xi in x] # 不去中心,省一次减法 def ffn_swiglu(x, W1, W2, W3): gate = [silu(sum(W1[i][j] * x[j] for j in range(len(x)))) for i in range(len(W1[0]))] value = [sum(W3[i][j] * x[j] for j in range(len(x))) for i in range(len(W3[0]))] hidden = [gate[i] * value[i] for i in range(len(gate))] # GLU 门控 return [sum(W2[i][j] * hidden[j] for j in range(len(hidden))) for i in range(len(W2[0]))]
设计要点:
silu(x) = x · sigmoid(x)(Swish)。门控gate ⊙ value让 FFN 学到「这层该激活哪些维度」,比 ReLU 的硬阈值灵活。
把上面六块拼起来,每个子层包一个残差:
def encoder_block(x, p): h = layer_norm(x) h = multi_head_attention(h, h, h) # 自注意力,Q=K=V x = [x[i] + h[i] for i in range(len(x))] # 残差 h = layer_norm(x) h = ffn_swiglu(h, p.W1, p.W2, p.W3) return [x[i] + h[i] for i in range(len(x))]# 残差 def decode(target_tokens, encoder_out, params): x = embed(target_tokens, params.emb) x = [x[i] + pe[i] for i in range(len(x))] # 加位置 for block in params.decoder_blocks: x = decoder_block(x, encoder_out, block) # 内含掩码自注意力 + 交叉注意力 + FFN return x
喂 6 个源 token、5 个目标 token,验证输出形状是 (5, vocab)。本节不训练——它讲的是架构,不是损失。
把 layer_norm 换成 rms_norm、ReLU-FFN 换成 ffn_swiglu,确认形状不变。这就是 2026 年的现代化:一次函数替换,骨架不动。
PyTorch 提供 nn.TransformerEncoderLayer、nn.TransformerDecoderLayer 作参考。但 2026 年大多数生产代码自己写块,因为:
nn.MultiheadAttention。HuggingFace transformers 有干净的参考块值得读一遍:modeling_llama.py 是 2026 年标准的解码器块,约 500 行,值得通读一次。
| 需求 | 选 | 例子 |
|---|---|---|
| 分类、嵌入、文本问答 | 编码器型 | BERT、DeBERTa、ModernBERT |
| 文本生成、聊天、代码、推理 | 解码器型 | GPT、Llama、Claude、Qwen |
| 结构化输入→结构化输出(翻译、摘要) | 编码器-解码器型 | T5、BART、Whisper |
解码器型赢了语言,因为它 scale 得最干净,且同时处理理解和生成。编码器-解码器型在输入有明确「源序列」身份时仍是最佳——翻译、语音识别、结构化任务。
原课程产出 outputs/skill-transformer-block-reviewer.md:一个代码审查 Skill,把一个新的 Transformer 块实现对照 2026 默认配置打分,标出缺失项(pre-norm、RoPE、RMSNorm、GQA、FFN 扩展比)。喂给它任何一份块代码,它产出一份「现代化差距清单」。
encoder_block 在 d_model=512, n_heads=8, ffn_expansion=4, swiglu=True 下的参数量,用 sum(p.numel() for p in block.parameters()) 验证。x 反转输出)上实现一个 4 层编码器-解码器,训 100 步报告损失。再换上 RMSNorm + SwiGLU + RoPE,损失降了吗?x + sublayer(x) 让梯度绕过非线性一路传到底层,没有它约 6 层后梯度消失。4d² + FFN 3r·d²,归一化可忽略——Llama 3 8B 的计数就是这么算出来的。下一节,我们将把这副骨架特化成编码器,实现 BERT 的掩码语言建模——把 15% 的 token 盖住,让模型猜回来,从而学到双向上下文表示。