Transformer: 详解
Transformer: Attention Is All You Need
论文: Attention Is All You Need (NeurIPS 2017)
作者: Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser, Illia Polosukhin (Google Brain)
代码: tensor2tensor 官方实现
一、背景
把《Attention Is All You Need》放回 2017 年的语境,理解它当时要解决什么问题、采用了哪些关键设计,有助于更准确地把握后续架构细节。
1.1 2017 年的世界:Transformer 诞生时的 NLP 图景
2017 年 6 月这篇论文出现时,NLP 学术圈与工业界的图景是这样的:
- 机器翻译是 NLP 的旗舰任务。Google Translate 在 2016 年 9 月把生产环境从短语统计 MT(PBMT)切到神经 MT(GNMT),翻译质量显著提升,推动行业转向 NMT,几乎所有 NLP 实验室都在做翻译。
- LSTM 是事实标准。所有 seq2seq、对话模型、语言模型都是 LSTM(GRU 被视为轻量替代)。Schmidhuber 1997 年提出的 LSTM,20 年后才成为序列建模的主流。
- attention 是 LSTM 的辅助机制。Bahdanau (2015)、Luong (2015) 的 attention 被定位成 RNN 的附加模块,RNN 仍是主体,attention 仅帮助 decoder 关注 encoder 的相关信息。
- 训练慢是公认瓶颈。GNMT 训练要好几天到一周,因为 LSTM 的时间步必须串行。Facebook 的 ConvS2S(2017.5)开始用 CNN 做 seq2seq 来并行化,引起讨论。
- 没有预训练范式。word2vec / GloVe 词嵌入是有的,但”先预训练大 LM 再 fine-tune 下游”的范式还没诞生——BERT 还要再等 17 个月。
- “大模型”指 <1B 参数的东西。GPT 还没出现,GPT-2 要再等一年半。
在这个语境里,标题 Attention Is All You Need 略带挑衅意味,意在表明:去掉 RNN 同样能完成翻译,而且更快更好。
1.2 真正的动机:为「训练效率」而生,而非「容量」
论文表面是 Google Brain 的研究,背后还有一条隐藏脉络:Google Translate 团队的工程压力。
GNMT 上线后,翻译团队要在生产环境维护一个每月迭代的庞大模型。真实痛点不是”翻译不够好”,而是:
- 训练一次新模型要 1 周以上(LSTM 串行);
- 实验迭代极慢,调一个超参要等几天;
- LSTM 的长依赖在长句子上仍不行。
⭐ 关键认知:Transformer 在诞生时是为”训练效率”而生的架构,不是为”容量”而生的架构。它的并行性是优化目标,attention 的全局连接是手段。今天我们说”Transformer 因 attention 能建模长依赖所以强大”——这种叙事其实是后人的总结。当年作者真正想要的,是把 N 个时间步的串行计算变成可并行的矩阵乘法。
这也是论文反复强调 “3.5 days on 8 P100 GPUs” 的原因:在当年的语境下,这一数字强调的是训练速度上的优势。
同时,2017 年 5 月 Facebook 的 ConvS2S 用 CNN 并行化做 seq2seq,WMT’14 EN-FR 达到 40.46 BLEU、超过 GNMT,引起广泛关注。Google 团队由此感受到竞争压力,论文实验里 ConvS2S 是核心 baseline。要回答的问题就是:能不能用 attention 同样实现并行化,而且效果更好? 答案是 Transformer big 在 EN-DE 28.4(ConvS2S 25.16)、EN-FR 41.8(ConvS2S 40.46),用更小模型、更短训练时间超过两者。
1.3 技术脉络:从 RNN+attention 到纯 self-attention
很多教程把 Bahdanau 2015 当作 self-attention 的祖先,这是不准确的。Bahdanau attention 是 decoder-encoder 之间的 cross-attention,不是 self-attention,且 RNN 仍是主体。
self-attention 真正的前身有几条线:
| 工作 | 年份 | 贡献 |
|---|---|---|
| Cheng, Dong, Lapata | 2016 | 让 LSTM 的某 step attend 到自己历史所有隐状态(”序列内部 attention”的早期想法) |
| Parikh et al.(Google) | 2016 | A Decomposable Attention Model,把 attention 用作两文本相似度建模的核心而非辅助(Niki Parmar 团队) |
| Lin et al. | 2017.3 | A Structured Self-Attentive Sentence Embedding,专门用 self-attention 做句子表示 |
Transformer 的创新不是”发明”了 self-attention,而是把它推到极端:整个网络都不要 RNN/CNN,纯靠 self-attention 堆叠。在 2017 年,”能不能完全没有 RNN”这个问题本身就大胆——多数同行认为这一思路过于激进。
1.4 八位作者与后续动向
论文署名八人,arXiv 顺序:Ashish Vaswani、Noam Shazeer、Niki Parmar、Jakob Uszkoreit、Llion Jones、Aidan N. Gomez(多伦多大学实习生)、Łukasz Kaiser、Illia Polosukhin。结尾一行 “Equal contribution”——八人贡献相等、顺序随机,反映这不是传统的主导模式,而是一群高水平工程师/研究员的平等合作。
几人后续都成了 AI 领域的重要人物,且几乎没人留在 Google 原岗位——这是 2018–2022 年间被媒体(Wired 等)称为 “Transformer Mafia” 现象的一部分:
- Noam Shazeer:后做 Sparse MoE Transformer,创办 Character.AI(2021),2024 年又重返 Google,被广泛认为是核心作者之一。
- Łukasz Kaiser:做 Tensor2Tensor(论文实验框架),后去 OpenAI 参与 GPT 系列。
- Aidan Gomez:当时本科实习生,后创办 Cohere。
- Illia Polosukhin:后做 NEAR Protocol。
- Jakob Uszkoreit:后创办 Inceptive 做生物医药 AI。
1.5 论文真正说了什么——以及刻意没说什么
论文的真正贡献可归纳为五点:
- 完全去掉 RNN 和 CNN,仅用 attention + FFN 构造序列模型;
- Multi-head self-attention:把 attention 拆成多个并行 head;
- Positional encoding:用 sin/cos 给位置无关的 attention 注入位置信息;
- Encoder-decoder + cross-attention:保留 seq2seq 框架但内部全用 attention;
- 训练速度大幅提升:8 卡 P100、3.5 天达到 SOTA。
实验设定非常 minimal:
| 项 | 设定 | |
|---|---|---|
| 数据 | WMT’14 EN-DE (4.5M 句对)、EN-FR (36M 句对) | |
| 硬件 | 1 台 8×P100 | |
| 训练 | base 12 小时(100k step)、big 3.5 天(300k step) | |
| 指标 | BLEU | |
| 结果 | EN-DE base 27.3 / big 28.4(前 SOTA 26.03);EN-FR base 38.1 / big 41.8 |
⚠️ 注意论文里”没有”什么:没有预训练、没有 in-context learning、没有 chain-of-thought、没有 instruct-tuning。这些后来”让 Transformer 真正改变世界”的东西都不在原论文里。原论文做的事非常聚焦:在机器翻译这一个任务上,证明纯 attention 架构可行且训练更快。模型也小——base 65M、big 213M,按今天的标准看规模很小。
1.6 接收度与历史转折:从「翻译论文」到「大模型圣经」
论文在 NeurIPS 2017 只被接收为 poster(不是 oral),如今看来略显意外。当年的反应大致是:
- 机器翻译圈:迅速接受,SOTA + 训练时间优势太明显,2018 年内几乎所有 NMT 系统都迁到 Transformer。
- NLP 整体:观望,多数人觉得这是”翻译领域的论文”。
- CV 圈:几乎未关注,当时少有人认为 attention 适用于 CV。
真正的爆发来自几个转折点:
- 2018 BERT:证明 Transformer encoder 可预训练成通用语言表示,下游任务全面 SOTA,把整个 NLP 拉到 Transformer 旗帜下;
- 2018 GPT-1 → 2019 GPT-2 → 2020 GPT-3:decoder-only 路线被验证,GPT-3 在 175B 涌现出 in-context learning;
- 2022.11 ChatGPT:decoder-only + RLHF 引发广泛关注,奠定对话式 AI 的产品形态,decoder-only 从此成为 LLM 事实标准。
有意思的是:原论文展示的是 encoder-decoder,而今天主流大模型(GPT / LLaMA / Claude / Gemini / Qwen)几乎全是 decoder-only。从 encoder-decoder 简化到 decoder-only 只是”去掉一半”——如果 2017 年作者已知 decoder-only 这么强,可能根本不会讲 encoder-decoder。但他们当时面对的是翻译这个本质 seq2seq 任务,encoder-decoder 是默认选择。
到 2024 年,这篇论文 Google Scholar 引用超 13 万,是过去十年被引最多的 ML 论文之一。
1.7 广泛影响:从翻译到通用骨干
七年里,Transformer 的核心结构几乎没变,却从翻译模型发展为几乎所有现代 AI 的共同基础:
| 领域 | 代表工作 | 用法 |
|---|---|---|
| NLP 大模型 | BERT / GPT / T5 / LLaMA / Claude | encoder-only / decoder-only / enc-dec 三大流派 |
| 视觉 | ViT | 把图块当 token,attention 建模全局 |
| 检测 | DETR | 用 attention 取代 NMS 后处理 |
| 自动驾驶 | BEVFormer / DETR3D / 时空 attention | 时序 + 多视角 BEV 特征建模、占据网络中的 attention |
| 3D 高斯泼溅 | Transformer 预测高斯参数 / 动态场景建模 | 把 3DGS 与序列/生成模型结合 |
核心结构七年不变,但实现细节被社区不断更新:位置编码改 RoPE / ALiBi、归一化改 RMSNorm、激活改 SwiGLU、attention 加 KV cache / FlashAttention 优化。这正是 Bitter Lesson 的又一例证——“少改动 + 多 scale + 好工程”击败了几乎所有精巧但局限于局部改进的变体(ConvS2S、Universal Transformer、Reformer、Performer、Linformer 等都没能成为主流)。
1.8 常见误解澄清
- ❌「核心创新是 attention」——不准确。attention 2014 年就有,self-attention 也有前作。真正创新是”全部用 attention 不用 RNN” + multi-head + positional encoding 的组合。
- ❌「一发表就引爆 NLP」——错。当时只是 NMT 圈接受,全行业转向是 BERT 之后的事。
- ❌「原论文是 decoder-only」——错。原论文是 encoder-decoder 用于翻译。
- ❌「论文里有 GPT/BERT 雏形」——只能说有 encoder/decoder 模块,pretraining-finetuning 范式当时根本不存在。
- ❌「Transformer 当年很大」——错。base 65M、big 213M 按今天的标准看规模都很小。
- ❌「八位作者后来都留在 Google」——错。几乎所有人都离开了。
二、整体架构
这一章只做一件事:把 Transformer 的整体骨架讲清楚–主架构图长什么样、由哪些部件组成、每个部件负责什么、数据怎么从输入流到输出。具体每个部件的内部细节,留到第三~六章展开。
2.1 主架构图:论文 Figure 1
Transformer 的全貌就是论文 Figure 1 这张图(下图是清晰重绘版)。熟悉这张图后,后续内容都可视为在其基础上的局部展开:
骨架四句话:
- 左半边 Encoder(堆 $N=6$ 层):处理源序列(如待翻译的句子);
- 右半边 Decoder(堆 $N=6$ 层):自回归地生成目标序列;
- Cross-Attention 是两边唯一连接:encoder 的输出作为 decoder 的 K、V;
- Decoder 顶端 -> Linear -> Softmax -> 词表概率。
💡 这张图是 2017 年为机器翻译设计的完整 Encoder-Decoder 架构。今天的 LLM(GPT/LLaMA 等)只用了右半边的 decoder-only;BERT 只用了左半边的 encoder-only。理解了完整图,就顺带理解了 BERT 与 GPT 的关系。
2.2 两大模块与职责
| 模块 | 职责 | 输入 | 输出 |
|---|---|---|---|
| Encoder | 把源序列编码成一组”上下文记忆” | 源 token + 位置编码 | 记忆 mem(每层输出形状不变) |
| Decoder | 自回归生成目标序列,并经 cross-attention 读取 encoder 的记忆 | 目标 token(shifted right)+ 位置编码 + encoder 的 mem | 每个位置的隐表示 -> 词表概率 |
| 输入端 | token id -> 向量 + 位置信息 | token id | $d_{model}$ 维序列表示 |
| 输出端 | 隐表示 -> 词表分布 | decoder 最后一层输出 | 每位置一个词表概率分布 |
Encoder 与 Decoder 之间只有一个信息通道:cross-attention。encoder 算完一次,结果被 decoder 的每一层反复”查询”–这继承了 2014 年 Bahdanau RNN attention 的思想。
2.3 组件清单:每个组件负责什么
Transformer 由以下组件构成(标注 src/transformer.py 中已实现的对应代码):
| 组件 | 职责 | 实现 | 详见 |
|---|---|---|---|
| Token Embedding | token id -> $d_{model}$ 向量(×√d_model 缩放) | TransformerEmbedding |
第三章 |
| Positional Encoding | 注入位置信息(sin/cos),因模型本身无顺序感 | PositionalEncoding |
第三章 |
| Multi-Head Self-Attention | 序列内部建模依赖:encoder 双向、decoder 因果(mask) | SelfAttention(单头版) |
第四章 |
| Cross-Attention | decoder 查询 encoder,是两边唯一信息通道(Q来自decoder, K/V来自encoder) | 待补 | 第五章 |
| Feed-Forward (FFN) | 非线性变换 + 记忆容量,两层线性夹 ReLU,宽度 4×d | 待补 | 第四章 |
| Add & Norm | 残差连接 + LayerNorm,缓解梯度消失、稳定训练 | 待补 | 第四章 |
| Linear + Softmax | 投影到词表维度 + 归一化为概率 | 待补 | 第六章 |
几个关键认知(后面章节会展开):
- 全程无递归、无卷积 -> 必须靠 Positional Encoding 显式注入位置信息;
- FFN 占了约 2/3 参数量(每层 attention ~1.05M,FFN ~2.1M),这就是为什么 MoE 把 FFN 稀疏化能省大量参数;
- 原论文用 Post-LN(Add 后再 Norm),今天大模型几乎都改 Pre-LN(Norm 在前),稳定性更好。
2.4 单层内部:Encoder 6 步 / Decoder 9 步
Encoder 一层做 6 件事(一个 sublayer 群 + 一个 FFN 群):
1 | 1. Multi-Head Self-Attention -> Attn(x) |
Decoder 一层多一个 cross-attention,共 9 步(三个 sublayer 群):
1 | 第一组 Masked Self-Attention: 1. Masked MHA (QKV 都来自 decoder 输入) 2. Add 3. LayerNorm |
三个关键差异:
- 第一组 self-attention 必须带 causal mask(decoder 生成时不能提前看到未来 token);
- 第二组 cross-attention 不需要 causal mask(源序列每个位置都可被完整看到);
- 第二组的 K、V 来自 encoder 最后一层输出,是 encoder/decoder 唯一信息通道。
2.5 数据流:一个 token 的旅程
以翻译 "Hello world" -> "Bonjour le monde" 为例,架构层面的数据流:
1 | 源: "Hello world" --tokenize--> [15496, 995] |
训练 vs 推理,同一套权重、两种执行方式:
| 训练(teacher forcing) | 推理(autoregressive) | |
|---|---|---|
| decoder 输入 | 整个目标序列一次喂入 | 从 BOS 开始逐 token 生成 |
| 并行性 | 一次 forward 算出所有位置 loss(高度并行) | 每生成一个 token 重新 forward(串行,用 KV cache 优化) |
| mask 作用 | causal mask 保证位置 t 只看 0..t-1 | 保证只看已生成部分 |
| 复杂度 | $O(1)$(不算 batch) | $O(m)$ 步,m 为输出长度 |
这正是 Transformer 训练快、推理慢的根源:训练时整句并行,推理时必须自回归串行。所有”推理优化”(KV cache、speculative decoding 等)都在攻这个串行瓶颈。
2.6 三种架构变体
原论文是完整 Encoder-Decoder,2018 年后工业界把它拆成三种用法,对应这张图用哪一半:
| 维度 | Encoder-only | Decoder-only | Encoder-Decoder |
|---|---|---|---|
| 用图哪部分 | 左半边 | 右半边(去 cross-attn) | 完整 |
| Self-attention | 双向 | causal | enc双向 / dec因果 |
| 训练目标 | MLM(掩码预测) | LM(下一 token) | seq2seq |
| 主要用途 | 理解(分类、NER、QA) | 生成(对话、LM) | 翻译、摘要 |
| 代表 | BERT、RoBERTa | GPT、LLaMA、Qwen | 原版、T5、BART |
| 时代 | 2018–2020 | 2020+ 主流 | 2017–2020 |
今天 LLM 几乎全是 decoder-only:参数效率高、数据简单(纯文本自回归)、任务可统一成”prompt -> text”、推理优化生态成熟。Encoder-only 主要留在向量检索/分类;Encoder-Decoder 主要在 T5 派研究里。
三、输入表示 (Input)
这一章围绕
src/transformer.py与src/transformer.ipynb中的实现,说明文本如何转换为模型可处理的张量,并解答几个常见疑问:编码器输入如何处理、变长序列如何对齐、mask 如何获得、长度限制、中英文输入关系、位置编码。
3.1 输入处理总览:完整链路
文本不能直接输入网络,要经过以下处理(对应代码中的执行顺序):
1 | 原始文本 (中英句对 clean_cmn.txt) |
编码器的输入是怎么处理的? 即上述 ①->⑤ 五步:前三步把文本变成对齐的 token id 矩阵 + mask,后两步把 id 矩阵变成带位置信息的连续向量。下面逐节展开。
3.2 分词:BPE 与 SentencePiece(中英联合词表)
为什么用 BPE
直接按”词”分词表会爆炸、按”字符”分序列太长且丢语义。BPE(Byte Pair Encoding)在两者间取平衡:
| 优势 | 说明 |
|---|---|
| 解决 OOV | 只要词表含基础字符,就能用子词拼出任何生僻词,不再有 <unk> |
| 语义对齐 | 子词常携带语义(词根/前缀),如 unhappily = un + happily |
| 效率最优 | 序列长度适中,词表大小可控 |
BPE 原理(
doc/Byte-Pair-Encoding.md有完整演示):先 把词拆成字符 + 加</w>边界标记,再 反复统计最高频相邻对、合并成新 token,直到达到预设vocab_size。所以高频词会变成单个 token,低频词被拆成子词。
代码实现:Joint BPE 训练中英共享词表
1 | spm.SentencePieceTrainer.train( |
几个关键点:
- Joint BPE(联合分词):中英文合并训练一个共享词表,不分开训练。这样一个
nn.Embedding表就能同时表示中英文。 - 特殊 token 占住前 4 个 id:
pad=0 / unk=1 / bos=2 / eos=3。pad_id=0是为后面 padding 直接用 0 填充做准备。 - 编码:
sp.encode_as_ids("Hello world")->[15496, 995]这样的 id 列表。
英译汉中,英文和中文是同时输入的吗? 不是。词表是共享的(合并训练),但输入分两路:英文句子进 Encoder,中文句子(shifted right,首位加 BOS)进 Decoder。共享词表只是让两边能用同一张 embedding 表,而非把中英拼成一条序列输入。
3.3 变长处理:Padding 与长度限制
句子长度不一,无法组成矩阵,必须对齐到 batch 内最长。
1 | tokenized_data = [sp.encode_as_ids(s) for s in input_data] # 每句变长 id 列表 |
句子长度不一怎么处理? padding 到 batch 内最长(不足的尾部补
pad_id=0),超长的截断到limit。原文输入长度限制多少? 代码中
limit = 512。原论文做 WMT 翻译时句子较短(且按子词计),实际设置视任务而定;如今 LLM 上下文窗口从 2k 到 128k+ 不等,但”截断到一个上限”的思路不变。
3.4 Padding Mask:让模型忽略填充位
padding 补的 0 不是真 token,不能让 attention 去关注这些位置,否则会污染结果。所以需要一个 mask 标出哪些是有效 token。
1 | def GeneratePadMask(input_ids, pad_token_id=0): |
input_ids != pad_token_id得到[batch, seq_len]的布尔矩阵;.unsqueeze(1).unsqueeze(2)升到[batch, 1, 1, seq_len],方便在多头注意力里广播到[batch, heads, query, key]的 key 维度。
在 SelfAttention 中的使用:
1 | if mask is not None: |
把分数设成
-1e9再 softmax,对应位置权重就接近 0–等于让 padding 位对任何 query 都不产生影响。变长句子的 mask 怎么获得? 即
input_ids != pad_token_id,再 unsqueeze 到 4D 广播。Decoder 还多一个 causal mask(下三角,防止提前看到未来),两者按位相加/相乘合并(第四章细讲)。
3.5 词嵌入 Token Embedding
token id 是离散整数,要变成连续向量才能进网络:
1 | class TransformerEmbedding(nn.Module): |
nn.Embedding(vocab_size, d_model):一张[vocab, d_model]的可学习查表,lut(x)把[batch, seq]的 id 变成[batch, seq, d_model]。- 为什么要
×√d_model:后面要加位置编码(PE 的值在 [-1,1] 量级),如果不放大 embedding,加完 PE 后 embedding 信号会被 PE 淹没。乘√d_model(d_model=512 时 ≈22.6)把 embedding 量级抬上去,两者才匹配。
3.6 位置编码 Positional Encoding ⭐
为什么必须有位置编码:self-attention 的排列等变性
一个常被忽视的硬事实:self-attention 是排列等变(permutation-equivariant)的–它对 token 的顺序无感知。把输入序列任意打乱,输出只是跟着重排,每个位置的内容完全不变。用一行公式写死:设 $P$ 是任意置换矩阵,$f(X)=\text{softmax}(QK^\top/\sqrt{d_k})V$ 是一次 self-attention,则
$$
f(PX) = P,f(X)
$$
证明很短:投影逐行做,故 $Q’=PQ,\ K’=PK,\ V’=PV$;分数矩阵 $S’=Q’K’^\top = PS P^\top$;softmax 逐行作用,$\text{softmax}(PSP^\top)=P,\text{softmax}(S),P^\top$;再乘 $V’$ 并用 $P^\top P=I$,即得 $P,f(X)$。
工程后果很直接:在没有位置信息的 self-attention 看来,”猫吃鱼”和”鱼吃猫”是不可区分的–模型不是学不会,而是从根本上接收不到两者有差别的信息。实测中,把 PE 替换成 torch.zeros_like,loss 在前几百步几乎不下降。
RNN/CNN 自带位置感:RNN 的位置信息隐含在”第 t 步只能依赖第 t-1 步”的递推里;CNN 的位置感来自感受野的局部性(哪些 token 一起做了卷积)。Transformer 放弃了这两种结构性偏置,换取长程依赖的全局建模能力,代价就是位置信息归零–除非显式注入。
正弦位置编码公式
$$
PE_{(pos,,2i)} = \sin!\left(\frac{pos}{10000^{2i/d_{model}}}\right),\quad
PE_{(pos,,2i+1)} = \cos!\left(\frac{pos}{10000^{2i/d_{model}}}\right)
$$
pos:token 在序列中的位置(0,1,2,…);i:维度索引(偶数维用 sin,奇数维用 cos);- 每对维度 $(2i, 2i+1)$ 共用一个频率 $\omega_i = 10000^{-2i/d_{model}}$。
代码逐行解析
1 | class PositionalEncoding(nn.Module): |
几个要点:
register_buffer:PE 是固定公式算出来的,不是可学习参数,存成 buffer(随模型一起保存、随.to(device)一起迁移,但不被优化器更新)。div_term用exp(-log(...))而非直接1/10000^...:纯为数值稳定(避免大数幂运算溢出),数学上等价。x + pe[:, :x.size(1)]:PE 预算到max_len(代码中设为 5000),实际用时按当前序列长度切片相加。- dropout:加完 PE 后做 dropout,正则化的是”语义+位置”这个组合。
把它读成”一组不同频率的波”
$d_{model}=512$ 的 PE 摊开,是 256 对 (sin, cos) 通道,频率从最高($i=0,\ \omega=1$,周期约 6.28)平滑下降到最低($i=255,\ \omega=1/10000$,周期约 62832)。高频维度区分相邻位置的差异,低频维度记录”在序列的哪个大段落”,类似傅里叶级数把一个位置坐标同时投影到多个尺度的正弦波上,组合起来成为该位置的唯一指纹。
10000 是怎么来的:
- 数学层:$10000^{2i/d_{model}}$ 的最大值是 10000,最低频周期约 $2\pi\times10000\approx62832$,意味着能区分约六万个位置而不”绕回”相撞,对原论文几千的序列长度非常宽裕。
- 工程层:作者在 §3.5 末尾说明,选 sinusoidal 是假设它”可能让模型外推到训练时未见过的更长序列”。10000 本身没有神奇理论依据,是”足够大、覆盖足够长尺度、无需调参”的经验选择。
d_model=4 的最小手算例子($\omega_0=1,\ \omega_1=0.01$):
| pos | PE 向量 |
|---|---|
| 0 | [0, 1, 0, 1] |
| 1 | [0.841, 0.540, 0.010, 1.000] |
| 2 | [0.909, -0.416, 0.020, 1.000] |
| 3 | [0.141, -0.990, 0.030, 1.000] |
前两维(高频)剧烈变化,后两维(低频)几乎不动;pos=0 是特殊的起点向量 $[0,1,0,1]$。
范数恒定:每个 pos 的 PE 范数都等于 $\sqrt{d_{model}/2}$(每对 (sin,cos) 平方和恒为 1)。这意味着位置信息全部在”方向”上而非”长度”上,对 LayerNorm 友好–LayerNorm 归一掉范数、保留方向,位置信息得以保留。若用”位置越远 PE 越大”的方案,LayerNorm 后位置信息会被显著抹掉。
关键性质:相对位置可线性表达 ⭐
这是原论文选 sin/cos 的真正理由。原话:对任意固定偏移 $k$,$PE_{pos+k}$ 可表示为 $PE_{pos}$ 的线性函数:
$$
PE(pos+k) = M_k, PE(pos)
$$
其中 $M_k$ 不依赖 pos。证明来自一组三角恒等式:
$$
\begin{pmatrix} \sin(\alpha+\beta) \ \cos(\alpha+\beta) \end{pmatrix}
= \begin{pmatrix} \cos\beta & \sin\beta \ -\sin\beta & \cos\beta \end{pmatrix}
\begin{pmatrix} \sin\alpha \ \cos\alpha \end{pmatrix}
$$
即”角度从 $\alpha$ 加到 $\alpha+\beta$”对 $(\sin\alpha,\cos\alpha)$ 是乘一个旋转矩阵 $R(\beta)$。回到 PE:第 $i$ 个频率块上,从 pos 到 pos+k 多出的角度是 $k\omega_i$,故
$$
\begin{pmatrix} \sin((pos{+}k)\omega_i) \ \cos((pos{+}k)\omega_i) \end{pmatrix}
= R(k\omega_i)\begin{pmatrix} \sin(pos\cdot\omega_i) \ \cos(pos\cdot\omega_i) \end{pmatrix}
$$
把所有 $d_{model}/2$ 个频率块的旋转矩阵堆成块对角矩阵,就是完整的 $M_k$。
意义:模型若想识别”离我 k 步的 token”,只需学一个 $M_k$(等价于让 $W_Q, W_K$ 学到合适形状),而不必在数据里见过每对 $(pos, pos+k)$。这种”相对位置一次学习、全 pos 复用”是 sinusoidal 相比可学习位置的关键优势。
⚠️ 注意:线性可表达只是”免费的下限”,不保证模型一定学到这条性质。后续可解释性研究发现原版 sinusoidal 在远端位置上 attention 仍有漂移–这正是 RoPE 的动机:把旋转直接”焊”进 Q/K 投影,把下限推成结构上的保证。
为什么是相加,不是拼接
原论文从头到尾是相加(常被误读为拼接):
$$
x = \text{embed}(token) + PE(pos)
$$
相加看似会污染语义,但 embedding 空间是高维的($d_{model}=512$),模型能在不同子空间分别承载语义与位置。更技术地看,相加让 PE 与 embedding 共享同一组 $W_Q, W_K$,attention 打分 $(W_Q(e+p))(W_K(e+p))^\top$ 展开后产生 $e!\cdot!e$、$e!\cdot!p$、$p!\cdot!e$、$p!\cdot!p$ 四项,模型可同时利用语义-语义、语义-位置、位置-位置三种交互。
拼接(concat)不被采用的原因:①参数效率低(要么挤占语义维度,要么扩大 $d_{model}$ 增加计算量);②强制语义与位置不混合,限制表达;③实验上无稳定优势。
这也呼应 3.5 的
×√d_model:相加前先把 token embedding 放大到与 PE 可比的量级,避免固定的 PE 在训练初期过早主导输入–本质是尺度校准。
其他位置编码方式对比
按”位置信息从哪进入 attention”分为三类:
| 类别 | 方式 | 形式 | 外推 | 代表 |
|---|---|---|---|---|
| 绝对位置(加在输入) | 正弦 | 固定 sin/cos | 中等 | 原版 Transformer |
| 可学习 PE | (max_len, d_model) 查表 | 几乎为零 | BERT、GPT-2、ViT | |
| 相对位置(改打分公式) | 相对位置 bias | 在 Q·Kᵀ 加 (i-j) 偏置 | 强 | T5、Shaw 2018 |
| 旋转/衰减 | RoPE | 旋转矩阵把位置编进 Q/K | 强 | LLaMA、Mistral、Qwen |
| ALiBi | attention score 减 m·|i-j| | 强 | - |
可学习 PE 的外推问题:它是一张有限查表,训练长度外的位置没有梯度。训练 512、推理 2048 时,前 512 位能查表,后面的位置要么不存在、要么未被充分训练。这正是大模型时代主流转向函数型(sinusoidal/RoPE/ALiBi)的原因–函数型在任何 pos 都有定义且服从同一规律。
选型没有”正确答案”,只有”约束下最合适”:固定长度任务(如 NER、分类)用可学习 PE 完全够用;需要相对距离敏感的翻译任务,sinusoidal 或 T5 bias 不错;长上下文对话/文档理解,RoPE/ALiBi 是默认。
常见误解
- ❌「位置编码是可选模块,去掉也能训」–对纯 self-attention 不成立,去掉后”猫吃鱼”与”鱼吃猫”不可分(除非是集合建模任务,或 decoder 的 causal mask 提供了部分顺序偏置)。
- ❌「sinusoidal 是拼接到 embedding 上的」–原论文是相加,且要求 PE 维度 = $d_{model}$。
- ❌「10000 是某种神秘最优值」–是”足够大、覆盖足够长尺度、无需调参”的经验选择,RoPE 沿用 10000,长上下文场景会调到 1e6。
- ❌「可学习位置一定更强,因为能学」–能学的代价是参数量、长度上限、外推能力差;长上下文场景函数型更占优。
- ❌「sinusoidal 的相对位置性质保证模型一定用得上」–只保证”可线性表达”,不保证”一定学到”;这正是 RoPE 把旋转焊进 Q/K 的动机。
今天主流 LLM 基本用 RoPE;原论文的 sin/cos 是起点,理解了它再看 RoPE,就是”把位置从加在输入上,改成融入 Q/K 中”,并从”免费下限”升级为”结构保证”。
3.7 组合与中英分流:Encoder / Decoder 各吃什么
最终输入 = Embedding + PE,代码中的完整调用链:
1 | # ①②③ 文本 -> token id -> padding -> mask |
英译汉任务中,两路输入分别是什么:
| 输入内容 | 处理 | |
|---|---|---|
| Encoder 输入 | 英文句子(源语言) | tokenize -> pad -> mask -> emb -> +PE |
| Decoder 输入 | 中文句子 shifted right(首位加 BOS) | 同上 5 步,再加 causal mask |
| Decoder 目标 | 中文句子(要预测的,即把 BOS 去掉、末位接 EOS) | 算 loss 用 |
训练时 encoder 和 decoder 分别处理各自的输入(英文一路、中文错位一路),通过 cross-attention 连接;并非把中英拼成一条序列输入。共享的只是 BPE 词表和 embedding 表。
四、编码器 Encoder
Encoder 由”自注意力 + FFN + 残差归一化”组成,重复 N 层。这一章以你
src/transformer.py里的SelfAttention为核心讲清单层内部,再回答一个关键问题:既然一层就能看全局,为什么还要堆 6 层? 你代码尚未实现的多头、FFN、Add&Norm、Encoder Block 给出标准写法(可直接拼进工程)。
4.1 自注意力 Self-Attention ⭐⭐
自注意力的本质:序列中每个位置都去”查询”所有位置,按相似度加权求和,得到融合了全局上下文的新表示。核心是 Q(Query)、K(Key)、V(Value)三个量。
你的代码(逐行)
1 | class SelfAttention(nn.Module): |
对应公式(Scaled Dot-Product Attention):
$$
\text{Attention}(Q,K,V) = \text{softmax}!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V
$$
三个关键”为什么”
① Q/K/V 为什么是三个不同投影?
同一个输入 $x$ 经三组不同参数($W_Q, W_K, W_V$)投影,让模型学到”用什么去查(Q)、用什么被查(K)、查出信息取什么(V)”三种角色。若三者相同,attention 退化为对 $x$ 自身加权平均,表达力大幅下降。
② 为什么除以 $\sqrt{d_k}$?
$d_k$ 较大时 $Q\cdot K^\top$ 数值会很大,把 softmax 推入饱和区(最大值趋近 1、其余趋近 0),梯度几乎为 0,训练停滞。除以 $\sqrt{d_k}$ 把方差控制在 1 附近,保持梯度健康。你的代码用 embed_size ** (1/2),单头时 $d_k=d_{model}$,与论文一致。
③ 维度怎么变?
- 特征维度不变:Q/K/V 与输出都是
[batch, seq, d_model]; - 产生 seq×seq 矩阵:
Q @ K^T把[seq, d]变成[seq, seq]的注意力分数矩阵,这是核心中间量,不是特征升维; - 真正的”升维”在后面的 FFN($d_{model}\to 4d_{model}\to d_{model}$),见 4.3。
Encoder 的 self-attention 是双向的
Encoder 的 self-attention 没有 causal mask(那是 Decoder 的事),位置 $i$ 的 query 可以看见所有位置的 key,包括左边和右边。从第一层起每个 token 就是双向的–这正是 Encoder 适合”理解”任务的结构原因。
上下文化表示:以
"The animal didn't cross the street because it was tired."为例,it进入第一层前只是个通用代词向量;经过 self-attention,它的 query 与全句 key 匹配后,输出向量已融合了”指向 animal”的语义信息。attention 决定”从哪里拿信息”,把每个 token 改写成”在这句话里的含义”。
4.2 多头注意力 Multi-Head Attention ⭐
你的 SelfAttention 是单头版。多头把它扩展:把 $d_{model}$ 切成 $h$ 份(论文 $h=8$,每头 $d_k=d_{model}/h=64$),每头独立做一次 attention,最后 concat 再投影。
为什么要多头:单头只用一组 Q/K/V,关注一种”视角”;多头让不同头关注不同子空间/不同模式(局部、长程、语法关系等),表达力更强。
各头如何并行:靠 reshape 把头维度并进 batch 维,一次矩阵乘法同时算完所有头,无需 for 循环:
1 | # x: [batch, seq, d_model] |
计算量与单头相当:单头投影是 $d_{model}\times d_{model}$,多头是 $h\times(d_{model}\times d_k)=d_{model}\times d_{model}$,参数量与 FLOPs 基本相同,只是换成了多视角。
4.3 位置前馈网络 Position-wise FFN
你的代码尚未实现 FFN。它是每个 attention 子层后的两层全连接,对每个位置独立施加(token 间交互已由 attention 完成):
$$
\text{FFN}(x) = \max(0,\ xW_1+b_1),W_2+b_2
$$
1 | class FeedForward(nn.Module): |
FFN 决定”拿回来之后怎么改写”:attention 让 token 带上上下文,但只是线性混合的结果;FFN 把每个位置的隐藏状态投到更高维空间、经 ReLU 引入非线性、再压回来,做逐位置的重写。升维 4 倍是模型”记忆容量”的主要来源–FFN 占每层约 2/3 参数量($\approx2.1M$ vs attention 的 $\approx1.05M$),这也是 MoE 把 FFN 稀疏化能省大量参数的原因。
4.4 残差连接与层归一化 (Add & Norm)
每个子层都包一层残差 + 归一化:
$$
\text{output} = \text{LayerNorm}(x + \text{Sublayer}(x))\quad\text{(Post-LN,原论文)}
$$
为什么需要 Add & Norm:残差(Add)让网络学残差 $F(x)=H(x)-x$ 而非直接学变换,梯度可经 $+x$ 直通浅层,缓解梯度消失,使堆叠多层成为可能;归一化(Norm)稳定每层输入分布,加速收敛、降低对初始化的敏感。两者让”重写”不是硬覆盖,而是在旧表示上做小修正。
为什么用 LayerNorm 而不是 BatchNorm:
| BatchNorm | LayerNorm | |
|---|---|---|
| 归一化方向 | 沿 batch 维(每个特征跨样本统计) | 沿 特征维(每个样本自身统计) |
| 对 batch 大小 | ✅ 强敏感(小 batch 统计不稳) | ❌ 不敏感 |
| 变长序列 | ❌ 难处理(样本长度不同) | ✅ 每位置独立归一 |
| 训练/推理差异 | 需维护 running mean/var | 无 |
NLP 序列任务 batch 小、序列变长,LayerNorm 更合适;BatchNorm 更适合 CV(固定尺寸图像、大 batch)。
Pre-LN vs Post-LN:原论文用 Post-LN(LayerNorm(x+Sublayer(x))),深层训练不稳定、对 warmup 敏感;现代大模型多用 Pre-LN(x+Sublayer(LayerNorm(x)))。直觉差别:Post-LN 每走一层都要先过一次归一化”闸门”;Pre-LN 主路径基本直通,归一化只作用在旁路变换的输入上,残差更像一条真正的直通高速路。
4.5 6 层堆叠到底在做什么 ⭐
这是理解 Encoder 的核心问题:既然第一层每个 token 已经能看全句,为什么还要堆 6 层甚至 24 层?
答案不是”增加感受野”–Transformer 从第一层起感受野就是全局的。堆叠增加的是表示的抽象层级与迭代修正能力:
- 第一层看到的是”原始词之间的关系”:词性、近距离依存、常见搭配、指代/主谓/修饰等浅层模式;
- 更深层重写的是”已经上下文化过的表示”:第二层拿到的不再是裸 embedding,而是第一层改写后的表示,于是能看到”哪些位置已被标成主语/宾语/实体/否定词””这是问句/对比/条件句”等更抽象的关系;
- 类比:像反复读一句复杂句子–第一遍只知字面意思,第二遍抓主干,第三遍才意识到让步、转折、因果。多层堆叠就是这种”反复读、反复重写”。
经验观察(非严格定律但相当稳定):底层头偏局部语法与位置模式,中层偏句法依赖与实体关系,高层偏任务相关语义。
6 层只是 2017 年的折中(WMT 翻译、P100 算力下的合理点),不是神奇数字。后来 BERT-base 用 12 层、BERT-large 24 层、ViT/DeBERTa 动辄 24 层起步。真正不变的是”重复同一种 block,靠深度反复 refinement“这条设计。
Encoder 的输出是什么
设最后一层输出 $H^{(L)}=[h_1,h_2,\ldots,h_n]$,每个 $h_i\in\mathbb{R}^{d_{model}}$。这些向量不是概率、不是标签,而是把”这个 token 在整句里的意义”压缩进来的连续表示–一组上下文化的 token 表示。
- 形状全程不变:输入输出都是
[batch, seq, d_model],Encoder 不是在改变张量外形,而是在反复重写同一组 token 表示(类似迭代 refinement,区别于 RNN 的时间深、CNN 的感受野扩大)。 - 适合”读完再判断”的任务:分类(用
[CLS]或池化)、Token 分类(NER/POS)、抽取式问答、检索/匹配、视觉/多模态编码。 - 不擅长直接生成:没有 causal mask、训练目标不是预测下一 token、输出是表示而非生成状态。Encoder 像个”读者”而非”说话者”。
三大架构家族的分化正源于此(详见 2.6 对照表):Encoder-only(BERT)做理解、Decoder-only(GPT)做生成、Encoder-Decoder(T5)做翻译/摘要。BERT 之所以只要 Encoder,正是抓住了它”双向表示学习”最强的一点。
4.6 完整 Encoder Block
把上述零件组装成一个 Encoder Block(你的代码可在此基础上扩展):
1 | class EncoderBlock(nn.Module): |
- 单个 Block 内部 6 步(回扣 2.4):MHA -> Add -> LayerNorm -> FFN -> Add -> LayerNorm;
- 堆叠 N=6 层:上层输出作为下层输入,逐层提取更高层表示;
- 最后一层输出送入 Decoder 的 cross-attention 作为 K、V(见第五章)。
4.7 常见误解
- ❌「一层已看全局,所以多层是浪费」–全局可见 ≠ 全局理解。第一层看原始 embedding 的全局关系,更深层看多轮改写后的高阶表示;深度带来的是表示变换与迭代推理,不是更大感受野。
- ❌「Encoder 只是 Decoder 的配角」–只在翻译图里像配角;到 BERT、ViT、CLIP 里它是主角,是整条表示学习路线的核心骨架。
- ❌「Encoder 的输出就是一句话的向量」–不对。默认输出是一整串 token 表示,句向量需额外构造(
[CLS]、mean pooling、attention pooling)。 - ❌「Encoder 不建模顺序」–没有位置编码时成立;加上 PE 后它就能利用顺序,只是通过 attention + 位置信号建模,而非递归。
- ❌「Encoder-only 一定比 Decoder-only 更懂语义」–在理解任务上有结构优势,但现代 decoder-only LLM 经大规模自回归训练也能学到很强语义。真正区别是训练目标与推理方式。
你当前的实现进度:
SelfAttention(单头)✅ -> 多头/FFN/Add&Norm/EncoderBlock 待补。把 4.2~4.6 的标准实现拼进transformer.py,即可得到一个完整的 Encoder。
五、解码器 Decoder
Decoder 像个”写作者”:在每个时刻回答”看到前文与外部条件后,下一个 token 该是什么”。这一章用
Homework1_Question.ipynb里的DecoderLayer/Decoder/ mask 代码为主线,讲清三块子层、训练与推理的差异,以及为什么 Decoder 天生适合生成。
5.1 概览:比 Encoder 多一块 cross-attention
Encoder 一层有两块(self-attention + FFN),Decoder 一层多一个中间的 cross-attention,共三块子层,每块都包 Add & Norm:
原论文 Post-LN 形式:
$$
\begin{aligned}
h_1 &= \text{LayerNorm}(x + \text{MaskedMHA}(x)) \
h_2 &= \text{LayerNorm}(h_1 + \text{CrossAttn}(h_1, M)) \
y &= \text{LayerNorm}(h_2 + \text{FFN}(h_2))
\end{aligned}
$$
其中 $M$ 是 Encoder 最后一层输出的 memory。
子层顺序及其原因(不是随便排的):
- 先 masked self-attention:整理目标序列内部的历史,构造”我已经说到哪儿了”的状态;
- 再 cross-attention:拿着这个状态去 encoder memory 里查外部信息,决定对齐输入里的什么内容;
- 最后 FFN:对融合了历史与外部信息的表示做非线性重写。
若把 cross-attention 放到 masked self-attention 之前,”要查什么外部信息”就缺少明确的历史状态作目标,语义上不合理。
5.2 解码器的输入与两种执行方式 ⭐
Decoder 的输入是目标序列 shifted right(首位加 BOS)。训练与推理执行方式截然不同:
1 | decoder 输入 : [BOS, y1, y2, ..., y_{T-1}] ← shifted right |
训练(teacher forcing,并行):已知整条目标序列,一次性喂入 decoder,靠 causal mask 保证位置 $t$ 计算时看不见未来,所有位置在同一次前向里并行算出。例如目标 [BOS, 我, 喜欢, 机器, 学习],位置 0/1/2/3 分别预测 我/喜欢/机器/学习,一次 forward 完成。
推理(autoregressive,串行):模型不知道未来,只能逐步生成:
- 输入
[BOS]-> 预测我; - 输入
[BOS, 我]-> 预测喜欢; - 输入
[BOS, 我, 喜欢]-> 预测机器;……
训练并行、推理串行,不是矛盾,而是同一套自回归约束在”已知/未知未来”两种信息条件下的自然结果。推理串行正是 LLM 推理慢的根源,KV cache 等优化都在攻它。
⚠️ exposure bias:训练时 decoder 总看到真实前缀,推理时看到的是自己生成、可能带误差的前缀,两者分布不完全一致。scheduled sampling、RLHF 的 rollout 等都在缓解此问题。
5.3 掩码自注意力 Masked Self-Attention
Decoder 的核心约束:不能偷看未来。若不加 mask,训练时位置 $t$ 预测第 $t$ 个 token 时能直接看到答案 $y_t$ 及之后的 $y_{t+1},…$,loss 很好看但模型学到的是作弊。
解决:加一个下三角 causal mask:
$$
S = \frac{QK^\top}{\sqrt{d_k}} + \text{Mask},\quad
\text{Mask}_{ij} = \begin{cases} 0, & j \leq i \ -\infty, & j > i \end{cases}
$$
softmax 后所有未来位置权重为 0。
你的代码:mask 怎么生成(pad mask 与 causal mask 合并)
回扣 3.4 的 pad mask,Decoder 还需 causal mask,两者按位与合并。Homework1_Question.ipynb 里 Transformer 类的实现:
1 | def get_pad_mask(self, x, pad_idx): |
tgt_mask = pad_mask & subsequent_mask:同时屏蔽 padding 位与未来位置。这就是第三章预告的”pad mask 与 causal mask 按位合并”的真实写法。
与 Encoder 的差别只在 mask:公式相同,但 Encoder 无 causal mask(双向,适合理解),Decoder 有 causal mask(只看过去,适合生成)。Causal mask 不是小补丁,而是 Decoder 路线的结构分水岭–没有它就没有 GPT 式自回归语言模型。
5.4 交叉注意力 Cross-Attention ⭐
Cross-attention 是 Decoder 读取 Encoder 的唯一通道。与 self-attention 的区别不在公式,而在 Q/K/V 的来源:
- Query:来自 Decoder 当前层的隐藏状态;
- Key、Value:来自 Encoder 最后一层输出(memory)。
$$
\text{CrossAttn}(Q_{\text{dec}}, K_{\text{enc}}, V_{\text{enc}}) = \text{softmax}!\left(\frac{Q_{\text{dec}} K_{\text{enc}}^\top}{\sqrt{d_k}}\right) V_{\text{enc}}
$$
你的代码
DecoderLayer 里第二块子层,Q 来自 decoder、K/V 来自 encoder memory:
1 | # memory 是 encoder 最后一层输出 |
这一步是”条件生成”真正发生的地方。翻译时 decoder 决定下一个 token,masked self-attention 提供”前面已写了什么”,cross-attention 提供”源句里还有什么待对齐”,两者结合才能既符合目标语言历史又忠实于输入。
没有 encoder 时,这一块整块删掉–这就是 GPT 把”带 cross-attention 的 decoder”裁成”decoder-only”的过程。纯语言建模只需左侧历史,cross-attention 可去掉,Decoder layer 退化成 masked self-attention + FFN。
5.5 FFN + Add & Norm
第三块 FFN 与 Encoder 的 FFN 公式相同,但吃到的输入语义不同:Encoder 的 FFN 看到的是”融合了全句上下文的表示”,Decoder 的 FFN 看到的是”融合了历史前缀 + encoder memory 的表示”,作用是把当前生成位置的综合状态压成适合下一个 token 分类的隐藏向量。
Decoder 一层有 3 个 Add & Norm(masked-attn 后、cross-attn 后、FFN 后),结构同 Encoder,只是多一组。
5.6 完整 DecoderLayer 代码与堆叠
Homework1_Question.ipynb 的完整 DecoderLayer:
1 | class DecoderLayer(nn.Module): |
Decoder 在此基础上做 embedding + 位置编码,堆叠 N 层:
1 | class Decoder(nn.Module): |
- 单层内部 9 步(回扣 2.4):MaskedMHA -> Add -> LN -> CrossAttn -> Add -> LN -> FFN -> Add -> LN;
- 堆叠 N=6 层,每层都读取同一个 encoder memory;
- 输出送入 Generator(Linear + softmax/log_softmax)得到词表分布(见第六章)。
5.7 为什么 Decoder 天生适合生成
- 训练目标与推理动作一致:每个位置学的是 $p(y_t \mid y_{<t}, x)$,与生成时”给历史前缀预测下一 token”完全对齐;结构、目标、推理三者一致。
- 历史状态累积在 residual stream:随 token 逐个生成,每层残差主路累积”到目前为止已说了什么”,效果类似 RNN 的 hidden state,但通过 attention 读取整段历史而非压缩向量。
- 对外部条件友好:翻译/摘要/问答/多模态生成只需加回 cross-attention,就能在保留自回归生成能力的同时读取外部输入。
这也是为什么 2017 年至今 Decoder 路线从未消失,只是从”完整 encoder-decoder 的右半边”演化成”GPT 式 decoder-only 主干”。互联网文本天然适合 next-token 目标、统一接口极简(一切任务转成”给定前缀续写”)、少了 cross-attention 后结构与部署更干净–这是 decoder-only 成为主流的工程合力。
5.8 常见误解
- ❌「Decoder 就是 Encoder 多了个 mask」–不止。它还多了”自回归目标”这整个训练-推理闭环;没有闭环,mask 只是矩阵操作,Decoder 才成为生成模型。
- ❌「cross-attention 是可有可无的附加模块」–对 decoder-only 可去;对翻译/摘要/条件生成,它是读取输入条件的唯一主通道,删掉就失去对齐输入的能力。
- ❌「训练时 Decoder 不能并行,所以慢」–错。训练有 teacher forcing + causal mask,所有位置一次性并行算出;真正串行的是推理。
- ❌「decoder-only 一定比 encoder-decoder 更先进」–它只是更适合大规模统一预训练与通用生成;翻译、摘要、语音识别等输入输出天然两段的任务里,encoder-decoder 仍很有竞争力。
- ❌「Decoder 只能做文本生成」–代码、音频 token、图像 token、动作序列都可自回归生成;关键是”序列化的下一步预测”,不是”文本”。
六、输出 (Output)
Decoder 最后一层输出的是每个位置的隐表示,要转成”下一个 token 在词表上的概率分布”才能用于计算 loss 或生成。这一章用
Homework1_Question.ipynb的Generator代码讲清输出头、训练目标与解码策略。
6.1 输出头:Linear + Softmax
Generator 把 decoder 输出从 $d_{model}$ 维投影到词表大小,再过 softmax 得到概率分布:
1 | class Generator(nn.Module): |
用
log_softmax而非softmax是为数值稳定,且直接配合NLLLoss(负对数似然)使用;若用CrossEntropyLoss则内部已含 softmax,不需再 log_softmax。Weight tying(原论文 §3.4):encoder 输入 embedding、decoder 输入 embedding、decoder 输出投影 $W_{out}$ 三者共享同一张
[vocab, d_model]矩阵。省参数(词表 37k、d=512 时省下 ~38M)且有正则化效果。现代大模型因词表巨大(50k+)多不再共享。
6.2 训练目标:交叉熵 + Label Smoothing
训练用 teacher forcing:decoder 输入是目标序列去掉最后一个 token(target[:, :-1]),监督标签是去掉 BOS(target[:, 1:]):
1 | # train_fn 核心 |
Label Smoothing(ε=0.1):把 one-hot 目标软化–目标词概率从 1 变 0.9,剩 0.1 平均分给所有其它词:
$$
q_i = (1-\varepsilon)\cdot\mathbf{1}[i=\text{target}] + \varepsilon/V
$$
反直觉但有效:label smoothing 让训练 PPL 变差(最优分布被人为软化),但 BLEU 变好–因为分布更柔和时 beam search 能保留更多候选路径,找到更优解。现代 LLM(自回归生成、不用 beam search)基本不再用,但翻译任务仍是标配。
6.3 推理解码策略
推理时没有真实标签,decoder 必须用自己生成的 token 作下一步输入。三种主流策略:
| 策略 | 做法 | 特点 |
|---|---|---|
| Greedy | 每步选概率最高的 token | 快,但易被早期局部最优锁死 |
| Beam Search | 保留 k 条候选路径(原论文 beam=4) | 翻译/摘要等”有标准答案”任务主流 |
| Sampling | 按概率采样(top-k / top-p) | 现代 LLM 对话生成主流,多样性好 |
Beam search 的长度偏置:每生成一个 token 乘一个 $P<1$,句子越长总概率越低,beam search 倾向输出短句。原论文用 length penalty 抵消:
$$
\text{score}(y) = \frac{\log P(y)}{lp(|y|)},\quad lp(|y|)=\left(\frac{5+|y|}{6}\right)^\alpha,\quad \alpha=0.6
$$
现代 LLM 对话生成几乎不用 beam search(输出”过于安全”、多样性差、长生成算力开销大),改用 temperature sampling + top-k/top-p;但翻译、摘要这类有标准答案的任务 beam search 仍占主流。
七、训练与优化细节
“结构对了”不等于”能训出来”–同一架构配方稍动可能差几个 BLEU。这一章用
NoamOptim/train_fn代码 + 原论文配方,讲清优化器、学习率调度、正则化、batching 等关键细节。
原论文完整配方一览:
1 | 模型: Transformer base / big |
7.1 优化器:Adam 的微调
Adam 维护一阶矩 $m$(动量)和二阶矩 $v$(梯度平方移动平均),自适应调节各参数步长。原论文对默认值做了两处关键调整:
- β₂ = 0.98(不是默认 0.999):β₂ 控制 $v$ 的记忆长度。0.999 对应”记忆 1000 步”,对百万步训练合适但对 100k 步太长;0.98 对应”记忆 50 步”,让 $v$ 更快跟上学习率变化,适合短训练。
- ε = 1e-9(不是默认 1e-8):影响梯度平方接近 0 时的步长,复现时保持一致即可。
- 无 weight decay:当时靠 dropout + label smoothing 已提供充分正则;现代大模型改用 AdamW + weight decay 0.1。
7.2 学习率调度:warmup + 1/√step ⭐
原论文的学习率公式(你的 NoamOptim 即此实现):
$$
lr = d_{model}^{-0.5} \cdot \min!\left(step^{-0.5},\ step \cdot warmup_steps^{-1.5}\right)
$$
1 | class NoamOptim(object): |
min 里两支函数:A = step·warmup⁻¹·⁵(线性增长)、B = step⁻⁰·⁵(平方根衰减),交点在 step = warmup_steps。所以曲线先线性升、再 1/√step 衰减:
- step < 4000:lr 沿 A 线性升到峰值 $lr_{peak}=d^{-0.5}\cdot warmup^{-0.5}\approx7\times10^{-4}$(d=512);
- step > 4000:lr 沿 B 按 1/√step 慢慢降,100k 步时降到约 1/5 峰值。
为什么 warmup:训练初期参数随机初始化、梯度方向嘈杂,Adam 的二阶矩 $v$ 需积累几千步才能给出靠谱的方差估计。若一开始就用大 lr,参数会被嘈杂梯度推到离解空间很远的”坏 basin”,后续难以恢复。warmup 给优化器几千步缓冲,让 $v$ 稳定后再放开 lr。
warmup=4000 不是魔法常数:是原论文那套模型规模/batch/硬件下的经验折中。太短训练不稳,太长前期 lr 偏低、收敛慢。现代 LLM 仍有 warmup(GPT-3 报 375M tokens、LLaMA-2 报 2000 steps),只是按各自尺度重选。
d_model⁻⁰·⁵:让学习率随模型宽度自动适配–模型越宽、矩阵乘输出方差越大,lr 要随宽度降下来(类似 Glorot/He 初始化的 1/√d)。big 模型(d=1024)峰值自动比 base(d=512)低约 30%。
7.3 正则化:Dropout + Label Smoothing
Dropout(P=0.1) 加在两处:
- Embedding + 位置编码相加之后;
- 每个子层(attention、FFN)的输出,在加到残差之前。
1 | x' = x + dropout(Attention(x)) # 子层输出 dropout |
为什么 0.1 而非经典 CV 的 0.5:Transformer 已有 LN、残差、label smoothing、weight tying 多重正则,再加重 dropout 反而让训练不稳定。0.1 是”轻量正则”。现代 LLM(数据量千亿 token)过拟合非主要矛盾,dropout 降到 0。
Label Smoothing(ε=0.1):见 6.2,PPL 变差但 BLEU 变好。
7.4 Batching by Tokens
原论文每个 batch 含约 25000 source token + 25000 target token,即按 token 数而非句对数组 batch。
为什么不用固定 batch_size:翻译句子长度差异大(5 词到 100 词)。固定 64 句时,短句 batch(512 token)GPU 利用率极低,长句 batch(5120 token)可能爆显存;且显存由 batch 内最长句决定,短句 padding 白占位。按 token 数 batch 则短句 batch 句数多、长句 batch 句数少,显存占用稳定、利用率最大化。
实现:按长度分桶(bucket),每桶内长度相近,凑够 25000 token 提交一个 batch。
7.5 训练循环
train_fn 的核心流程(teacher forcing + 梯度裁剪):
1 | def train_fn(model, iterator, optimizer, criterion, clip=1.0): |
- 梯度裁剪(clip=1.0):防止偶发大梯度把训练弄崩,原论文未明确但 fairseq 默认开启,现代 LLM 普遍保留。
- 8 卡数据并行:每卡一份 batch 子集,前向反向后 all-reduce 梯度,再各自更新(参数相同)。单机 8 卡纯数据并行即可,千卡规模才需 3D 并行。
7.6 现代大模型 vs 2017 配方
| 配方项 | Transformer base (2017) | GPT-3 175B (2020) | LLaMA-2 70B (2023) |
|---|---|---|---|
| 优化器 | Adam | Adam | AdamW |
| β₂ | 0.98 | 0.95 | 0.95 |
| weight decay | 0 | 0.1 | 0.1 |
| LR schedule | inverse √step | cosine | cosine |
| LR peak | ~7e-4 (d=512) | 6e-5 | 1.5e-4 |
| dropout | 0.1 | 0 | 0 |
| label smoothing | 0.1 | 0 | 0 |
| batch (tokens) | 25k+25k | 3.2M | 4M |
| 训练 token | ~3B | 300B | 2T |
| 精度 | FP32 | FP16+loss scale | BF16 |
| 梯度 clip | 无 | 1.0 | 1.0 |
核心观察:scale 变了 1000 倍,但训练配方的”形状”基本没变–仍是 Adam 系、有 warmup、按 token batch、学习率峰值后衰减。变的细节:dropout/label smoothing 砍到 0、加了 weight decay、1/√step 换 cosine、FP32 换 BF16。梯度下降优化的几何性质并不随模型规模变化太多。
7.7 常见误解
- ❌「warmup 可有可无,去掉只是慢一点」–错。warmup 缩过短或去掉,训练稳定性通常明显变差(Adam 二阶统计起步未站稳)。
- ❌「Transformer 用 Adam 默认参数就行」–原论文用 β₂=0.98、ε=1e-9,非框架默认值;复现论文结果需先对齐这些超参。
- ❌「label smoothing 让 PPL 变差所以是错误设计」–PPL 变差是预期内的,但 BLEU 变好,而翻译关心的是 BLEU。PPL 与 BLEU 不总同向。
- ❌「beam size 越大越好」–beam=4 是原论文选择,继续加大不自动带来更好翻译,常只是把算力花在相近候选上。
- ❌「现代 LLM 训练完全用了不一样配方」–部分对。变了很多细节,但”骨架”(Adam 系 + warmup + token batching + 峰值后衰减)是 2017 年定下的。
八、优缺点与对比
8.1 vs RNN/LSTM/CNN
| 维度 | RNN/LSTM | CNN (seq) | Transformer |
|---|---|---|---|
| 并行性 | ❌ 时间步串行 | ✅ 可并行 | ✅ 全并行 |
| 长距离依赖 | 弱(梯度衰减) | 中(需堆叠扩感受野) | 强(一步全局) |
| 复杂度 | O(n) | O(k·n)(k 为核宽) | O(n²) |
| 位置信息 | 隐含在递推顺序 | 隐含在感受野局部性 | 需显式位置编码 |
RNN/CNN 的位置感是”派生”的(递推顺序、感受野局部性),Transformer 放弃了这两种结构性偏置以换取全局建模,代价是位置信息归零(需 PE 注入)与 O(n²) 复杂度。
8.2 核心局限:O(n²) 复杂度 ⭐
Attention 的复杂度来自 $QK^\top$ 产生的 $n\times n$ 矩阵:
$$
S=\frac{QK^\top}{\sqrt{d}}\in\mathbb{R}^{n\times n}\to A=\text{softmax}(S)\to O=AV
$$
- 计算复杂度 $O(n^2 d)$:$QK^\top$ 与 $AV$ 各约 $2n^2 d$ FLOPs;
- 显存复杂度 $O(n^2)$:$S$ 和 $A$ 都是 $n\times n$,这才是真正卡死长上下文的瓶颈。
| 序列长度 $n$ | $n^2$ 矩阵显存 (fp16, 单 head) | 单 H100 (80GB) 能放几层 |
|---|---|---|
| 2k | 8 MB | 上万层 |
| 32k | 2 GB | ~30 层 |
| 128k | 32 GB | ~2 层(无法训练) |
| 1M | 2 TB | 不可能 |
$n=128k$ 时单层单 head 的 attention 矩阵就 32GB,一张 80GB 的 H100 都装不下–没有 FlashAttention 的话 128k 上下文在硬件上根本不可能。
Attention vs FFN 谁是计算大头:FFN 计算量 $\approx 16nd^2$($O(nd^2)$),attention $\approx 4n^2d$($O(n^2d)$),比值 $\approx n/(4d)$。
- $n<4d$ 时 FFN 主导(短上下文,attention 仅占小头,FFN 优化如量化/MoE 更值钱);
- $n>4d$ 时 attention 主导(LLaMA-7B 的 $d=4096$,$n=16k$ 是临界点;$n=128k$ 时 attention 是 FFN 的 8 倍)。
KV cache(推理隐藏成本):推理时每生成一个 token,需存下所有历史 K、V。KV cache 大小 $=2\cdot n\cdot d\cdot L$。LLaMA-2-7B 每个 token 约 512KB;$n=128k$ 时 KV cache 达 64GB,甚至超过模型权重本身。这是 GQA、MLA、PagedAttention 等推理优化的攻击点。
8.3 降复杂度的五类方案
| 类别 | 思路 | 复杂度 | 代表 | 代价 |
|---|---|---|---|---|
| 滑窗 (local) | 只看前后各 $w$ 位 | $O(nw)$ | Longformer、Mistral SWA | 丢全局信息 |
| 稀疏 (sparse) | 按规则稀疏化 attention | $O(n\sqrt{n})$ | Sparse Transformer、BigBird | GPU 不友好、效果有差距 |
| 线性 (linear) | 核分解 $\phi(Q)\phi(K)^\top V$ | $O(n)$ | Linformer、Performer | 大规模上效果显著弱于 full |
| SSM/RNN | 不走 attention,状态空间建模 | $O(n)$ | Mamba、RWKV、RetNet | 长程检索仍弱、未到 70B+ |
| FlashAttention | IO-aware 分块,不物化 $n\times n$ | 显存 $O(n)$、计算仍 $O(n^2)$ | FlashAttention 1/2/3 | 精确非近似、已成事实标准 |
⚠️ 最常见的误解:FlashAttention 把 attention 降到 $O(n)$–错。它把显存从 $O(n^2)$ 降到 $O(n)$,计算仍是 $O(n^2)$(每对 Q-K 仍要做内积)。加速来自减少 HBM↔SRAM 数据搬运(现代 GPU 瓶颈是带宽不是 FLOPs),而非减少计算量。
为什么主流仍用 full attention:①线性方案放大到 7B+ 时质量差距被放大;②FlashAttention 让显存不再是瓶颈,计算贵可用 GPU 数量解决;③长上下文真正瓶颈是 KV cache 而非 attention 计算;④稀疏方案 GPU 不友好;⑤混合架构(如 Jamba=Mamba+attention)兜底。所以 GPT-4/Claude/LLaMA/Qwen 仍是 full attention + FlashAttention + GQA/MLA。
8.4 优点 / 局限
优点 ✅
- 全并行:无递归,训练时所有位置一次算出,吞吐远高于 RNN;
- 全局建模:任意两位置一跳直达,长距离依赖强;
- 通用性强:同一架构覆盖 NLP/CV/语音/多模态,扩展到实例分割、关键点等;
- 可扩展性好:结构骨架 7 年不变,靠 scale + 局部改进持续受益。
局限 ❌
- O(n²) 复杂度:长序列显存/计算爆炸,需 FlashAttention、KV cache 等工程优化;
- 位置编码是补充:本身无顺序感,依赖 PE 注入,外推能力有限(sinusoidal 不够,需 RoPE/ALiBi);
- 推理串行:自回归生成必须逐 token,长生成 latency 高;
- 数据/算力 hungry:大规模预训练需千亿 token + 千卡,小数据场景未必优于 RNN。
8.5 常见误解
- ❌「FlashAttention 把 attention 降到 $O(n)$」–是 $O(n)$ 显存,不是 $O(n)$ 计算。
- ❌「线性 attention 已替代 full attention」–主流大模型仍是 full。
- ❌「Mamba 比 Transformer 强」–中小模型上接近,长程检索仍弱。
- ❌「attention 是模型计算量大头」–只在长序列成立,短序列时 FFN 才是大头。
- ❌「降到 $O(n)$ 就能无限长上下文」–长上下文还有位置编码外推、训练数据、评测等多重瓶颈,复杂度只是表面问题。
九、变体与影响
9.1 三大架构流派
- Encoder-only:BERT(理解任务)
- Decoder-only:GPT 系列 / 现代 LLM(生成任务)
- Encoder-Decoder:T5 / BART
9.2 跨模态
- 视觉:ViT(把图块当 token)
- 检测:DETR(目标检测去 NMS)
- 语音/多模态
9.3 领域应用
- 自动驾驶:BEVFormer / DETR3D / 时空注意力
- 3D 高斯泼溅:Transformer 预测高斯参数、动态场景
9.4 后续改进方向
- 位置编码:相对位置 / RoPE / ALiBi
- 效率:Flash Attention、稀疏注意力、线性注意力
- 长上下文、MoE 等
十、总结
- 核心贡献:用纯注意力取代 RNN/CNN,实现并行 + 全局建模
- 设计精髓:Q/K/V 自注意力、多头、位置编码、残差+LayerNorm、交叉注意力
- 历史地位:现代大模型与多模态/自动驾驶/3D 场景的通用骨干
相关链接
- 📋 论文原文: arxiv.org/abs/1706.03762
- 📋 The Annotated Transformer (Harvard NLP): nlp.seas.harvard.edu/annotated-transformer
- 📋 Jay Alammar 图解 Transformer: jalammar.github.io/illustrated-transformer
- 📋 [[10.clippings/感知算法/骨干网络/Transformer/【Transformer 与注意力机制】19|《Attention Is All You Need》论文背景]] - 第一章背景的主要来源(含 2017 年时代图景、作者脉络、历史转折)
- 📋 [[10.clippings/感知算法/骨干网络/Transformer/【Transformer 与注意力机制】21|位置编码:为什么需要它,为什么用正弦]] - 第三章 3.6 位置编码的主要来源(排列等变证明、相对位置线性可表达、相加vs拼接等)
- 📋 [[10.clippings/感知算法/骨干网络/Transformer/【Transformer 与注意力机制】22|Encoder 详解:6 层堆叠到底在做什么]] - 第四章编码器的主要来源(6 层堆叠意义、encoder 输出性质、Post/Pre-LN)
- 📋 [[10.clippings/感知算法/骨干网络/Transformer/【Transformer 与注意力机制】23|Decoder 详解:为什么它天生适合生成]] - 第五章解码器的主要来源(三子层、causal mask、cross-attention、训练/推理差异)
- 📋 [[10.clippings/感知算法/骨干网络/Transformer/【Transformer 与注意力机制】27|原论文怎么训出来的:8 张 P100、12 小时、warmup 4000 步]] - 第七章训练优化的主要来源(Adam 微调、warmup 公式、label smoothing、batching by tokens、现代配方对照)
- 📋 [[10.clippings/感知算法/骨干网络/Transformer/【Transformer 与注意力机制】18|注意力的复杂度问题]] - 第八章优缺点对比的主要来源(O(n²) 复杂度来源、显存瓶颈、五类降复杂度方案、FlashAttention 误解)
- 🔗 待补:相关笔记(ViT / DETR / BERT / GPT 等建立后回链)
- 📋 Transformer 系列原文(quant67): quant67.com/post/transformer/index.html

