土法炼钢兴趣小组的算法知识备份

【Transformer 与注意力机制】27|原论文怎么训出来的:8 张 P100、12 小时、warmup 4000 步

文章导航

分类入口
transformer
标签入口
#transformer#training#learning-rate#warmup#label-smoothing#adam

目录

上一篇我们把 Transformer block 的最后一个零件——前馈网络——讲透了。到这里,整个模型的结构已经完全摆开:embedding、位置编码、多头注意力、残差、LN、FFN、stack 6 层。

但「结构对了」不等于「能训出来」。深度学习里有一个让人头疼的事实:同一个模型架构,配方稍微一动,结果可能差几个 BLEU。学习率怎么调、warmup 多长、batch 怎么算、dropout 放哪儿、label 平滑多少——这些超参数的取值,比模型结构本身更难猜对。

2017 年原论文之所以能让人信服,不只是因为提出了新结构,更是因为他们公开了一套完整的训练配方,并在 WMT2014 这个公认的 benchmark 上跑出了当时的 SOTA。这份配方后来被几乎所有 Transformer 实现奉为「标准答案」,直到现代 LLM 时代才慢慢演化出新版本。

这一篇要做的事情,是把这个配方一项一项地拆开,讲清楚每一个数字、每一个公式背后的含义。读完之后你应该能做到:

先把整套配方一次性贴出来,然后我们一节一节地拆:

模型:    Transformer base / big
数据集:  WMT 2014 En-De(4.5M 句对)/ En-Fr(36M 句对)
硬件:    8 × NVIDIA P100 GPU
训练时长:base 12 小时(100k 步) / big 3.5 天(300k 步)
batch:   每步约 25000 source token + 25000 target token
优化器:  Adam, β₁=0.9, β₂=0.98, ε=1e-9
学习率:  lr = d^(-0.5) · min(step^(-0.5), step · warmup^(-1.5))
warmup:  4000 步
正则化:  dropout 0.1(embedding 之后、每个子层输出)
          label smoothing 0.1
推理:    beam search, beam=4, length penalty α=0.6
最终:    En-De BLEU 27.3 (base) / 28.4 (big)
          En-Fr BLEU 38.1 (base) / 41.8 (big)

每一行都有故事。


一、数据集与基准任务

1.1 WMT 2014:那个时代的 ImageNet

WMT(Workshop on Machine Translation)从 2006 年开始举办,是机器翻译领域的标准评测。每届会发布若干语言对的训练数据、开发集、测试集,参赛者提交翻译结果,组委会统一评分(人工或 BLEU)。

2017 年原论文用的是 WMT 2014 的两组数据:

两个任务的官方测试集是 newstest2014,每对约 3000 个句子。BLEU(Bilingual Evaluation Understudy)分数是在这个测试集上算出来的——具体说,是用 SacreBLEU 或 multi-bleu 这样的工具,把模型翻译和参考译文比较 n-gram 匹配率。

为什么选这两组而不是别的?因为它们在 2017 年是「公认的硬骨头」——之前几年的 SOTA 模型(GNMT、ConvS2S、ByteNet)都在这上面打榜,已经形成了稳定的对比基线。

1.2 数据预处理

原论文用的是字节对编码(Byte Pair Encoding,BPE)做子词切分,词表大小约 37000(En-De)或 32000(En-Fr)。这个细节后面 29 篇会专门讲,这里只要记住:

句子按长度排序后再分 batch(后面讲 batching 时再展开),最大长度截到 100 token 左右。

1.3 为什么这个任务现在看起来「小」

在 GPT-3 那个 570GB 数据、1750 亿参数的时代,回头看 WMT 2014 的几百万句对、6 千万参数的 Transformer,会觉得「就这?」

但要明白:2017 年这是相当不小的工程。8 张 P100 在那时候是顶级配置(A100、H100 还要等 4-5 年才出来),单机 8 卡训 12 小时跑出 SOTA,是一个大组才能做的实验。

而且更关键的是:原论文的训练配方在它的尺度下是经过仔细调过的——后来人复现,照搬这套配方,结果稳定可复现;这是一个工程上的重要保证。


二、硬件与训练时长

2.1 8 张 P100 是什么概念

NVIDIA P100(2016 年发布):

对比 2024 年的 H100:

也就是说,今天一张 H100 大概顶 50-60 张 2016 年的 P100。原论文「8 张 P100」的总算力,按今天看大约相当于 0.15 张 H100。这能训出 SOTA 翻译模型,确实是工程上的奇迹。

2.2 base 12 小时、big 3.5 天

两个模型的训练时长,主要由模型大小、序列长度、batch 大小三个因素决定。换到现代 GPU,整个训练时长会被压缩到几小时甚至更短——但配方本身不会变。

2.3 多卡同步

8 卡之间的同步方式是「数据并行」(Data Parallel):每张卡拿一份 batch 子集,独立前向反向,最后用 all-reduce 把梯度加到一起,再各自更新参数(参数完全相同)。

具体的 all-reduce 实现,原论文没细说,应该用的是 NCCL 或类似的工具。因为只有 8 卡同机,all-reduce 的通信成本不大;如果跨机,就要考虑 ring all-reduce、梯度压缩等。

后来 GPT-3、LLaMA 的训练用了「数据并行 + 流水线并行 + 张量并行」三套并行方案叠加(3D Parallelism),那是为千卡规模准备的;2017 年单机 8 卡的尺度,纯数据并行就够了。


三、优化器:Adam 的微调

3.1 Adam 简介

Adam(Adaptive Moment Estimation,Kingma & Ba, 2014)是当时最流行的自适应优化器。它维护两个移动平均:

更新规则:

m_t = β₁ · m_{t-1} + (1 - β₁) · g_t
v_t = β₂ · v_{t-1} + (1 - β₂) · g_t²
m̂_t = m_t / (1 - β₁^t)        # bias correction
v̂_t = v_t / (1 - β₂^t)
θ_t = θ_{t-1} - lr · m̂_t / (√v̂_t + ε)

直觉:m 给方向(带动量),v 给步长(在梯度方差大的方向走小步、方差小的方向走大步)。Adam 比纯 SGD 在多数任务上更稳更快,缺点是参数多一份(每个参数要存 m 和 v,显存占用 3x)。

3.2 原论文的 β₂ = 0.98(注意不是 0.999)

Adam 默认 β₂ = 0.999,但原论文用了 β₂ = 0.98。这是个小但重要的差异。

β₂ 控制 v(梯度平方的移动平均)的「记忆长度」。β₂ 越大,记忆越长,对噪声越鲁棒,但对学习率变化的响应越慢;β₂ 越小,记忆越短,对最近梯度更敏感。

β₂ = 0.999 对应「记忆 1000 步」,这个对长训练(百万步)很合适,但对 100k 步的训练就太长了——v̂ 的 bias correction 在前几千步还很大,可能导致更新不稳定。

β₂ = 0.98 对应「记忆 50 步」,更适合短训练,让 v 能更快跟上学习率的变化。这是原论文调出来的——后人复现 Transformer 翻译都沿用 0.98。

到了 GPT-3 时代,因为训练步数大幅增加(百万级),β₂ 又调回了 0.95 或 0.999。

3.3 ε = 1e-9(注意不是 1e-8)

Adam 的 ε 是分母里防止除零的小常数,默认 1e-8。原论文用 1e-9——比默认小一个数量级。

这个差异更细,影响主要在「梯度平方接近 0」时的步长。ε 小,分母可以更小,更新步长更大;ε 大,分母被压住,更新步长偏小但更稳。

实际复现时,ε 用 1e-8 还是 1e-9 影响不大,但要保持配方一致以便对照论文结果。

3.4 没有 weight decay

原论文没有 weight decay(权重衰减),这是另一个值得注意的点。

现代大模型(GPT-3、LLaMA)几乎都用 AdamW(Adam + decoupled weight decay),weight decay 设到 0.1 或 0.01。原论文那时还没有 AdamW(Loshchilov & Hutter 2019 才提出),他们也没用普通的 L2 正则。

为什么没用?我猜测两个原因:

  1. 当时 dropout + label smoothing 已经提供了相当充分的正则化;
  2. 翻译任务的训练数据相对模型容量已经足够大,过拟合不是主要矛盾。

到了现代大模型,因为参数量爆炸、数据相对模型容量未必充裕,weight decay 就重新成为标配了。


四、学习率公式:那个看起来很神秘的式子

4.1 公式与图像

原论文的学习率公式是:

lr(step) = d_model^(-0.5) · min(step^(-0.5), step · warmup_steps^(-1.5))

第一次看这个公式,多数人都会愣一下:为什么是这种形式?三个超参数(d_model、step、warmup_steps)混在一起,min 还把两支函数粘在一起。

把它拆开看就清楚了。min 里面是两个函数:

两者交点在 step = warmup_steps 处——你可以代入验证:当 step = warmup_steps 时,A = warmup_steps · warmup_steps^(-1.5) = warmup_steps^(-0.5) = B。

所以这个公式描述的是一条「先线性升、再 1/√step 衰减」的曲线,转折点在 warmup_steps 步:

学习率曲线

4.2 为什么先 warmup

「为什么要 warmup」是个非常有意思的问题,2017 年的时候大家也没完全说清楚,到 2020 年前后才有比较像样的理论解释。

直觉版的解释是这样的:

训练刚开始时,模型参数是随机初始化的,梯度方向「指向哪儿」非常嘈杂。Adam 的 v(梯度平方移动平均)需要积累几百到几千步才能给出一个相对靠谱的「方差估计」。

如果你一开始就用大学习率,会发生什么?

warmup 的作用是「给优化器和模型几千步缓冲,让 v 估计稳定下来、让参数从随机初始化温和地移动」。等到 v 比较准了,再放开学习率。

4.3 warmup_steps = 4000:魔法常数

为什么是 4000 步?这是论文实验出来的。

实测上:

所以 4000 是「足够长以保证稳定,又不至于浪费」的点。

warmup 影响

到了现代大模型,warmup_steps 通常按总步数的 1-2% 选——GPT-3 是 375M token 的 warmup(约 0.5% 的总训练 token),LLaMA-2 用 2000 步。这个比例和原论文(4000/100000 = 4%)大致是同一个数量级。

4.4 d_model^(-0.5) 是怎么来的

公式里 d_model^(-0.5) 这个因子的作用是「让学习率随模型宽度自动适配」。

直觉是:模型越宽,每个矩阵乘的输出方差越大;为了让参数更新幅度不随宽度爆炸,学习率要随宽度的某个幂次降下来。Vaswani 等人选了 -0.5 这个幂次,有点类似 Glorot/He 初始化的 1/√d 那个量。

具体说,对于 d=512:lr_peak = 512^(-0.5) · 4000^(-0.5) = (1/22.6) · (1/63.2) ≈ 7e-4。
对于 d=1024(big):lr_peak = 1024^(-0.5) · 4000^(-0.5) ≈ 4.95e-4。

big 模型的峰值学习率自动比 base 低一些——这正是「宽模型不能用太大 lr」这条经验的数学化。

4.5 后续 1/√step 衰减

warmup 之后按 1/√step 衰减。这个衰减率比 1/step(线性衰减)慢,比常数(不衰减)快——属于「中等强度」的衰减。

直觉是:

后来有些工作(GPT-3、LLaMA)改用「cosine decay」——按 cos 函数从峰值降到一个最小值(通常是峰值的 10%)。cosine 在工程上更平滑,但和 1/√step 在效果上差异不大。


五、Label Smoothing 0.1

五.1 一个 one-hot 的问题

标准的语言建模损失函数是交叉熵:模型给每个词输出一个概率分布 p,目标是让 p 与「真实分布 q」尽可能接近。

但「真实分布 q」长什么样?通常是 one-hot——目标词的概率是 1,其它所有词的概率是 0。

这个 one-hot 目标有两个问题:

  1. 过自信(overconfident):模型被推着把目标词的概率推到 1。要做到这点,目标词的 logit 必须远远大于其它词的 logit——softmax 的指数性意味着「无穷大的 logit 差距才能推出 1.0」。模型倾向于输出极端 sharp 的分布,这种「过度自信」反而损害泛化;
  2. 零概率惩罚不可控:如果模型给某个非目标词分配了一点点概率(比如 0.001),交叉熵会照样惩罚它(因为目标分布是 0),这种惩罚对于罕见词的处理有时过于严格。

5.2 Label smoothing 是怎么做的

label smoothing(Szegedy et al., 2016, Rethinking the Inception Architecture)的做法是把 one-hot 目标稍微「软化」:

q_i = (1 - ε) · 1[i = target] + ε / V

其中 ε 是平滑系数(原论文取 0.1),V 是词表大小。

直观上:目标词的概率从 1 变成 0.9,剩下 0.1 平均分给所有其它词。

这样一来:

5.3 为什么 PPL 变差但 BLEU 变好

这是 label smoothing 的「反直觉」之处。

PPL(perplexity)= exp(交叉熵),它衡量的是「模型对真实数据的概率估计」。如果你让模型「不那么自信」,它给目标词的概率自然降低,PPL 自然变差。

但 BLEU 衡量的是翻译质量——具体说,beam search 解码出的句子和参考译文的 n-gram 匹配率。这里关键的是 beam search 的多样性:当模型分布太 sharp 时,beam search 早期就被某条路径锁定,错过更好的路径;分布柔和一点,beam search 能保留更多备选,最终找到更优解。

label smoothing 的好处主要体现在 beam search 的过程中——它让模型给「次优词」保留合理概率,beam search 能在这些次优词上展开更深的搜索。

原论文的 ε = 0.1 是经验值。后来很多工作沿用,但也有些尝试不同值(0.05、0.15)的,差异不大。

5.4 现代大模型还用吗

GPT 系列、LLaMA 系列基本不用 label smoothing。原因之一是:自回归生成(一步一个 token)在 beam search 不再常用(greedy 或 sampling 是主流);原因之二是:超大词表(50k+)下,label smoothing 把 ε 平摊到几万个词上,每个词得到的「软概率」太小,效果不明显。

但翻译任务、专门用 beam search 解码的场景,label smoothing 仍然是标配。


六、Dropout 0.1:放在哪些位置

dropout(Srivastava et al., 2014)是经典的正则化方法:训练时按一定概率随机把激活值置零,强制网络不依赖特定神经元。

原论文的 dropout 配置是 P_drop = 0.1,加在以下位置:

  1. Embedding + 位置编码之后:把 token embedding 和 PE 加起来后过一次 dropout;
  2. 每个子层的输出:attention 子层和 FFN 子层的输出,在加到残差之前过一次 dropout。

具体到一个 block 的写法:

x' = x + dropout(Attention(x))
y  = x' + dropout(FFN(x'))

注意 dropout 是在「子层输出」上做的,不是在 attention 的内部 softmax 上。后来很多实现还会额外加:

这些是从 fairseq、Tensor2Tensor 等开源实现里来的微调,原论文没明确说。

6.1 为什么 0.1 而不是 0.5

经典视觉模型(VGG、ResNet 上的 dropout)常用 0.5。Transformer 用 0.1,差很多。

直觉是:Transformer 的层数和参数量已经很大,加上 LN、残差、label smoothing、weight tying 等多重正则,再加重的 dropout 反而让训练不稳定、收敛变慢。0.1 是「轻量正则化」,给训练加点扰动就够。

6.2 现代大模型里 dropout 几乎被放弃

GPT-3 训练时 dropout=0(取消了),LLaMA 系列也是。原因:

但如果你做小数据量的 fine-tune,dropout 又会重新有用。它是「数据-模型容量比」的函数:数据相对模型小时有用,反过来没用。


七、Batching by Tokens:长度归一化的妙招

7.1 一个常被忽视的设计

原论文里有一句话:「Each training batch contained a set of sentence pairs containing approximately 25000 source tokens and 25000 target tokens.」

这句话的意思是:每个 batch 的大小不是「固定句对数」,而是「固定 token 数」

这是一个非常重要的工程细节,但很多教程一带而过。

7.2 为什么不用固定 batch_size

机器翻译的句子长度差异巨大——短的 5 个词,长的 100 个词。如果你按固定 batch_size = 64 做:

更糟糕的是,显存占用主要由 batch 内最长句子决定——padding 让 batch 实际大小是 64 × max_len,短句白白占着位置。

按 token 数 batch,行为完全不同:

显存占用稳定,GPU 利用率最大化。

7.3 实现:buckets + dynamic batch

典型实现是把训练数据按长度分桶(bucket),每个桶内长度相近:

bucket 0: 句子长度 1-10
bucket 1: 句子长度 11-20
...
bucket 9: 句子长度 91-100

每次从一个 bucket 抽样,凑够 25000 token 就提交一个 batch。这样既保证 batch 内长度均匀(padding 浪费小),又能在 token 维度上保持稳定的 batch 大小。

fairseq、Tensor2Tensor 的实现都是这一套。

7.4 现代 LLM 怎么做

GPT-3、LLaMA 训练时同样按 token 数算 batch,但因为是单语自回归(不是翻译),处理简单一些:把所有训练文本拼成一长串、按固定上下文长度(2048、4096)切,每片就是一个样本。这样每个样本天然等长,padding 完全不需要。

但 token-based batching 的核心思想——「用 token 作为基本计算单位、而不是 sample」——一直延续到今天。


八、推理:beam search 与 length penalty

8.1 为什么不能 greedy

训练时模型用「teacher forcing」——每一步看真实历史预测下一个词。但推理时没有真实历史,必须用模型自己生成的词作为下一步输入。

最直接的做法是 greedy:每一步选概率最高的词。但 greedy 有一个致命问题——「早期局部最优锁死后续」。比如:

greedy 看不到这个,它只看每一步当下的最优。

beam search 的想法是「保留多个候选路径」:

beam search 不能保证找到全局最优解(那需要遍历所有路径,exponential),但比 greedy 显著提升翻译质量。

8.3 length penalty α = 0.6

beam search 有一个臭名昭著的 bias:短句子总概率高。因为每生成一个词都要乘一个 P < 1,句子越长概率越低,beam search 倾向输出短句。

length penalty 的作用是抵消这个 bias。原论文用 GNMT 提出的版本:

score(y) = log P(y) / lp(|y|)
lp(|y|) = ((5 + |y|) / 6)^α

α = 0.6 时,长句的 score 被「除以一个比 1 大的数」之前先「除以一个比 1 还略小的数」,平衡掉短句偏好。

实际效果:α = 0 时 beam search 输出明显偏短,α = 1 时偏长,α = 0.6 是中间的甜点。

GPT-3、LLaMA 的对话生成几乎都不用 beam search,而是用 temperature sampling、top-k、top-p(nucleus sampling)。原因:

但翻译、摘要这类「有标准答案」的任务,beam search 仍然占主流。


九、复现性:fairseq、Tensor2Tensor 的微妙差异

原论文公开了大部分细节,但仍然有一些边角细节没明确写。后人的复现实现(Google 自家的 Tensor2Tensor、Facebook 的 fairseq、HuggingFace 的 transformers)在以下点上有差异:

9.1 Pre-LN vs Post-LN

原论文是 Post-LN(每个子层之后做 LN)。但后来发现这个写法在大尺度下训练不稳定——梯度在深层会爆炸或消失。

Pre-LN(先 LN 再过子层)由 Xiong et al. 2020 系统分析后成为主流。GPT-3、LLaMA 都用 Pre-LN。

但在 base 6 层的尺度上,Post-LN 和 Pre-LN 表现相近,原论文用 Post-LN 没问题。

9.2 Embedding 缩放

原论文有一行:「In the embedding layers, we multiply those weights by √d_model.」

也就是 token embedding 出来后要乘以 √d。这是为了让 embedding 的方差和位置编码的方差对齐——位置编码 sinusoidal 的幅度大约是 1,而 embedding 初始化方差 1/d,乘 √d 后方差变成 1。

这是论文里写了的,但很容易漏。fairseq 默认乘了,HuggingFace 的早期 transformers 实现里有过 bug。

9.3 Dropout 的精确位置

前面提到,原论文的 dropout 加在「子层输出」「embedding 之后」。但具体到细节:

这些差异让不同实现的 BLEU 在 ±0.2 范围内浮动。

9.4 初始化

原论文没明确说初始化方法。Xavier 是默认。但具体是 Xavier-uniform 还是 Xavier-normal?fan_in 还是 (fan_in + fan_out) / 2?这些细节不同实现有差异。

后来 GPT-2 系列用了一个特殊缩放:W₂(FFN 输出投影)和 W_O(attention 输出投影)的初始化额外乘 1/√(2 n_layers),目的是让深层残差累积方差不爆。这是 GPT-2 的细节,原 Transformer 没用。

9.5 总结

如果你照着论文复现,BLEU 大概能到 27.0 左右(base)。要稳定到 27.3,需要额外注意上述细节。fairseq 是公认最接近原论文的实现,可以拿它当参考。


十、训练损失曲线长什么样

虽然原论文没公开完整的训练曲线,但根据公开的 fairseq 复现日志,我们大致能描绘出来:

训练曲线

BLEU 在 50k 步左右开始进入收益递减区——再训也只能再涨 0.3-0.5 BLEU。这是 base 模型选 100k 步的原因。big 模型因为容量大,能继续从训练中受益,所以训到 300k 步。

10.1 验证 vs 训练 loss

训练 loss 和验证 loss 大体重合——这个任务过拟合不严重。这也印证了前面 dropout 0.1、label smoothing 0.1 这种「轻正则」是合适的。


十一、现代大模型 vs 2017 年配方

时隔 8 年,原论文的训练配方哪些被沿用、哪些被改了?

11.1 沿用的部分

11.2 改了的部分

11.3 没变的核心

最有意思的观察是:虽然 scale 变了 1000 倍,但训练配方的「形状」基本没变

这背后其实有一个深层原因:梯度下降优化的几何性质并不随模型规模变化太多。warmup 还是要 warmup(梯度估计需要稳定时间),衰减还是要衰减(后期需要小步精修)——这些「软规律」在不同 scale 下都有效。

11.4 一份对照表

把原论文 vs LLaMA-2 vs GPT-3 的配方放到一张表里,差异看得很清楚:

配方项 Transformer base (2017) GPT-3 175B (2020) LLaMA-2 70B (2023)
优化器 Adam Adam AdamW
β₁ 0.9 0.9 0.9
β₂ 0.98 0.95 0.95
ε 1e-9 1e-8 1e-5
weight decay 0 0.1 0.1
warmup steps 4000 375M tokens 2000
LR schedule inverse √step cosine cosine
LR peak ~7e-4 (d=512) 6e-5 (d=12288) 1.5e-4 (d=8192)
dropout 0.1 0 0
label smoothing 0.1 0 0
batch (tokens) 25k src + 25k tgt 3.2M 4M
训练 token ~3B (En-Fr) 300B 2T
训练步数 100k (base) / 300k (big) ~95k ~500k
精度 FP32 FP16 + dynamic loss scale BF16
梯度 clip 0.0 (无) 1.0 1.0

读这张表的几个有趣观察:


十二、训练成本与论文影响

12.1 算成本

把 base 模型的训练 FLOPs 估算一下:

8 张 P100 的 FP16 算力 ≈ 8 × 19 = 152 TFLOPS。如果利用率 50%,单步耗时 ≈ 1.95e13 / 7.6e13 ≈ 0.26 秒;100k 步 ≈ 7.2 小时。考虑数据加载、通信开销,实际 12 小时合理。

12.2 与对比方法

原论文 Table 2 给出了和 ConvS2S、GNMT、ByteNet 的对比。Transformer base 在 BLEU 上同时打过这三家,而且训练成本只有 GNMT 的 ~1/10、ConvS2S 的 ~1/30。

这个「算力 × 性能」的甜蜜点,是 Transformer 当年 take over 整个 NLP 的核心动力——不是它能做新事情,而是它能用更少算力做得更好

成本对比

12.3 影响力

到 2024 年,Attention Is All You Need 的引用数已经超过 12 万次(Google Scholar 统计),是 21 世纪被引最多的 AI 论文之一。

更重要的是:几乎所有现代大语言模型——GPT 系列、Claude、Gemini、LLaMA、Qwen、DeepSeek——都基于这个 2017 年提出的架构,只是 scale 大了 1000 倍、加了一些边角改进(RoPE 位置编码、SwiGLU FFN、RMSNorm 等)。

架构稳定 8 年是非常罕见的。深度学习历史上,每隔几年就有「这次真的不一样」的新架构出现(LSTM → GRU → Transformer),但 Transformer 的根基一直没被撼动。


十三、一个手算的学习率示例

为了让公式从纸上走到具象,把原论文的配方带几个具体的 step 进去算一下。

设 d_model = 512, warmup_steps = 4000。

step = 1: - 函数 A:1 · 4000^(-1.5) = 1 / 252982 ≈ 3.95e-6 - 函数 B:1^(-0.5) = 1.0 - min(A, B) = A = 3.95e-6 - lr = 512^(-0.5) · 3.95e-6 = (1/22.627) · 3.95e-6 ≈ 1.75e-7

非常小的学习率。这是 warmup 第一步的样子——模型几乎不动,等 v 估计稳定。

step = 1000: - 函数 A:1000 · 4000^(-1.5) ≈ 3.95e-3 - 函数 B:1000^(-0.5) ≈ 0.0316 - min = A = 3.95e-3 - lr ≈ (1/22.627) · 3.95e-3 ≈ 1.75e-4

已经不算小,但还在 warmup 中(step < 4000),仍按线性增长。

step = 4000(warmup 完成的拐点): - 函数 A:4000 · 4000^(-1.5) = 4000^(-0.5) ≈ 0.01581 - 函数 B:4000^(-0.5) ≈ 0.01581 - 两者相等 - lr = (1/22.627) · 0.01581 ≈ 6.99e-4

这是峰值学习率 ≈ 7e-4。论文配方就是按这个峰值的。

step = 16000(4 倍 warmup): - 函数 B:16000^(-0.5) ≈ 7.91e-3 - lr ≈ (1/22.627) · 7.91e-3 ≈ 3.5e-4

峰值的一半。

step = 100000(base 训练结束): - 函数 B:100000^(-0.5) ≈ 3.16e-3 - lr ≈ (1/22.627) · 3.16e-3 ≈ 1.4e-4

降到峰值的 1/5 左右。这是训练后期的精修阶段。

把这 5 个 step 连起来看,就是图里那条曲线——先线性快速升到 7e-4,然后慢慢降到 1.4e-4,整个过程跨越 100k 步。

13.1 改了 warmup 会怎样

把 warmup 从 4000 改成 8000,重算:

step = 4000(中点): - A:4000 · 8000^(-1.5) = 4000 · 1/715541 ≈ 5.59e-3 - B:4000^(-0.5) ≈ 0.0158 - min = A = 5.59e-3 - lr ≈ 5.59e-3 / 22.627 ≈ 2.47e-4

比 warmup=4000 的峰值(7e-4)小 2.8 倍。也就是说,warmup 拉长一倍,整个训练前期的 lr 都偏低,模型「学得慢」——这就是图里 warmup=16000 那条曲线偏右下的原因。

新峰值出现在 step = 8000: - lr_peak = 512^(-0.5) · 8000^(-0.5) = (1/22.627) · 0.01118 ≈ 4.94e-4

比 warmup=4000 的 7e-4 低 30%。

直观结论:warmup 长度越长,峰值学习率越低。这是公式 lr_peak = d^(-0.5) · warmup^(-0.5) 直接给出的。

如果你的训练总步数本来就少(比如只有 10k 步),warmup 取 4000 已经占了 40%——前期太慢,后期没几步收敛。这时该把 warmup 缩到 1000 左右。

13.2 改了 d_model 会怎样

把 d 从 512 改成 1024(big 模型):

step = 4000 处的峰值: - lr_peak = 1024^(-0.5) · 4000^(-0.5) = (1/32) · (1/63.25) ≈ 4.95e-4

比 d=512 的 7e-4 低约 30%。这印证了 「模型越宽、学习率越低」 的经验,是公式自动给出的。

实际工程里你会看到很多大模型的峰值 lr 在 1-3e-4 量级——它们的 d 通常 4096-12288,按 d^(-0.5) 缩,比 d=512 的 7e-4 低好几倍。


十四、训练循环的伪代码

把整个训练 loop 用伪代码写一遍,所有上面提到的部件放到位:

import math, torch
from torch.optim import Adam

def lr_schedule(step, d_model, warmup_steps):
    return d_model ** -0.5 * min(step ** -0.5,
                                  step * warmup_steps ** -1.5)

# 模型与优化器
model = Transformer(d_model=512, n_layers=6, n_heads=8, d_ff=2048,
                    vocab_size=37000)
optim = Adam(model.parameters(), lr=1.0,
             betas=(0.9, 0.98), eps=1e-9)
# 注意:lr=1.0 是占位,真实 lr 由 schedule 给

# Label smoothing 损失
loss_fn = LabelSmoothingLoss(eps=0.1, vocab_size=37000, pad_id=0)

step = 0
for batch in dataloader:                  # batch 已经 token-balanced
    step += 1
    # 1. 设定本步学习率
    lr = lr_schedule(step, d_model=512, warmup_steps=4000)
    for g in optim.param_groups:
        g["lr"] = lr

    # 2. 前向
    logits = model(batch.src, batch.tgt[:, :-1])  # teacher forcing
    loss = loss_fn(logits, batch.tgt[:, 1:])

    # 3. 反向 + 更新
    optim.zero_grad()
    loss.backward()
    optim.step()

    # 4. 日志
    if step % 1000 == 0:
        print(f"step {step}  lr {lr:.2e}  loss {loss.item():.3f}")

    if step >= 100_000:
        break

这段代码概念上完整,但工程上还差几样东西:

加上这些工程细节,单文件训练脚本大概 300-500 行,可读性还可以——这正是 fairseq 早期版本的水平。


十五、原论文之外:averaging 与「test 提升术」

原论文有一个细节,很多教程忽略:最后报的 BLEU 是用 checkpoint averaging 算出来的

具体说:训练结束时不是用最后那个 checkpoint,而是把最后 5 个(base)或 20 个(big)checkpoint 的参数逐元素平均,得到一个「averaged model」,再用这个 averaged model 做推理。

15.1 为什么 averaging 有效

参数平均的本质是「对训练后期的几个解做几何中点」。深度学习的损失曲面是高度非凸、但局部相对平坦的——后期的几个 checkpoint 在损失曲面上「走来走去」,每一个都是某种局部最优,但它们的中点往往比任何单点都好

直觉上:

实测中,averaging 能给 base 提升 0.3-0.5 BLEU,给 big 提升 0.5-0.8 BLEU。

15.2 现代版本:EMA

现代大模型训练用的是 EMA(Exponential Moving Average):在训练过程中维护一份「影子参数」 θ_ema,每步按 θ_ema = α θ_ema + (1-α) θ 更新(α 通常 0.999 或 0.9999)。

EMA 比 checkpoint averaging 更连续——不需要显式存几个 checkpoint,每步自动累积。Stable Diffusion、扩散模型、强化学习的 actor-critic 都广泛使用 EMA。

但本质上 EMA 和 averaging 是同类技巧——都属于「让最终模型用一个相对平滑的参数版本,而不是某一刻的随机快照」。


十六、原论文里几个容易忽视的细节

在收尾之前,我想再补几个原论文文本里写了、但读快了容易错过的工程细节。这些细节单看每个不大,但加在一起决定了你能不能复现到 BLEU 27.3 这个具体值。

16.1 Embedding weight tying

原论文 §3.4 提到一句:「we share the same weight matrix between the two embedding layers and the pre-softmax linear transformation」。

意思是:

这三个矩阵共享同一份参数——共用一个 [vocab_size, d_model] 的矩阵。

为什么这么做?

第一,省参数。词表 37000、d=512,单矩阵 19M 参数;如果三个独立,就是 57M。共享省下 38M,相当于一个完整的 base 模型大小。

第二,正则化效果。三个矩阵被强制保持一致,相当于「输入 → 表示」和「表示 → 输出」用同一个映射的转置——这种对称性约束让训练更稳。

第三,在小词表上几乎不损失性能;在大词表(GPT-3 的 50k)上仍然有效。

后来 Press & Wolf(2017)写了一篇专门的文章 Using the Output Embedding to Improve Language Models 系统验证了 weight tying 的好处。

实现上:

# 共享一个 nn.Embedding
shared_embed = nn.Embedding(vocab_size, d_model)
# 输出投影把 embedding 矩阵转置
output_logits = hidden @ shared_embed.weight.T

16.2 没有 bias 的 attention 投影

很多复现里 W_Q、W_K、W_V、W_O 都不加 bias。LLaMA 系列也是这样。原论文文本里没明说,但代码里 W_Q/W_K/W_V/W_O 通常 bias=False——线性投影后立刻接 softmax 或残差,bias 的作用被吸收。

16.3 Dropout 在 attention 内部的位置

前面说过 dropout 加在「子层输出」。但实际 fairseq 实现还在 attention 的 softmax 之后加了一个 dropout:

attn_weights = softmax(QK^T / sqrt(d_k))
attn_weights = dropout(attn_weights)   # 这一步原论文没明说
output = attn_weights @ V

这个细节很微妙——它让某些 token 在某些 head 上的「看到」被随机屏蔽,强迫不同 head 不依赖单一来源。后来很多实现沿用,HuggingFace 默认开启。

16.4 训练用 FP32,推理可以 FP16

2017 年 P100 已经支持 FP16,但混合精度训练还不成熟(Apex 是 2018 年的事)。原论文用的是纯 FP32。

但他们提到:「We used 8 NVIDIA P100 GPUs.」P100 的 FP16 算力是 FP32 的 2 倍——意味着训练时其实没用上 FP16 的加速。

到了 2020 年之后,BF16 训练成为大模型主流(更少精度问题),FP16 + loss scaling 也很流行。这块工程演化非常快。


十七、评测方法学

最后讲一下 BLEU 是怎么算的,以及报数字时常见的「玄学」。

17.1 BLEU 公式简介

BLEU(Papineni et al., 2002)的核心是 n-gram 匹配率:

BLEU 的范围 0-100(有时写成 0-1),翻译任务上 30+ 已经算很好。

17.2 不同 BLEU 不能直接比

这是 MT 圈一个长期的痛点:同一个翻译,用不同工具算 BLEU 能差几个点

差异来源:

Post(2018)的 A Call for Clarity in Reporting BLEU Scores 推动了 SacreBLEU 工具的标准化,现在 MT 论文必须标注 SacreBLEU 的具体配置签名。

原论文 2017 年还没有这套标准,他们用的是 multi-bleu.perl(大小写敏感版),所以你直接跑 SacreBLEU 拿到的数字会和论文报的差零点几——不是模型不对,是评测方式不同。

17.3 测试集污染

现代大模型时代多了一个问题:训练数据可能不小心污染了测试集。如果你的 web 数据里包含了 newstest2014 的内容(参考译文也在里面),模型「记忆」了答案,BLEU 虚高。

这在 GPT-3、GPT-4 的评测里反复被讨论。LLaMA 论文里专门做了「污染检测」分析——把测试集每个句子去训练数据里搜,看出现率多高。

2017 年这还不是问题(4.5M 句对的 WMT 数据可控),但今天这是评测的一个隐藏雷区。


十八、训练时的监控指标

光有训练 loss 是不够的。一个稳健的训练循环还要监控以下指标,每个都能在出问题时给你信号。

训练 loss:最直接的指标。健康的曲线是单调下降(带噪声)。如果突然飙升,多半是数值不稳(NaN、爆炸梯度)。如果停滞不下,可能是学习率不对、warmup 没完、数据有问题。

验证 loss:每 N 步在 dev set 上算一次。健康的训练里 val loss 跟训练 loss 平行下降,最后差距很小。如果 val loss 开始反弹(训练 loss 还在降),说明过拟合了——加大正则、减小模型、扩大数据。

梯度范数(gradient norm):所有参数梯度的 L2 范数。健康的训练里这个值在 0.5-5 之间稳定波动。如果突然飙到 100+,多半是某个 batch 数据有 outlier、或者 attention 出现极端 sharp 分布。fairseq 默认 clip 到 1.0 防止这种 outlier 把训练弄崩。

学习率:监控它确认 schedule 正常工作——很多 bug 来自 schedule 写错(比如 warmup 步数和 step 计数不同步、resume 训练时 step 没对上)。

参数范数:所有参数的 L2 范数。健康的训练里这个值缓慢增长。如果迅速爆炸,要警惕。

激活的最大/最小值:在每层的输出上 sample 一下 max/min。健康范围 ±10 量级。如果某一层出现 ±100、±1000,bf16 表示就要溢出了。

attention 熵:把每个 head 的 attention 权重的熵(-Σ p log p)算出来。健康范围 1-3。如果某些 head 熵接近 0,那个 head 已经塌缩成 hard attention(看一个或几个固定 token),可能是「死了」。

这些监控不是必须的——很多人只看 train/val loss 就训完了。但当训练崩了或结果不对时,多看几个指标能更快定位问题。这是工程经验。


十九、关键概念回顾(散文式)

回头看这一篇,我们做了一件相对单调但很扎实的事:把 2017 年原论文的训练配方逐项打开,讲清楚每个数字背后的意图。

最关键的几条结论是:warmup 不是可选项——它给优化器 v 估计的稳定时间,去掉训练直接崩;学习率公式 lr = d^(-0.5) · min(step^(-0.5), step · warmup^(-1.5)) 不是凭空写出来的,它编码了「峰值随模型宽度反比、warmup 后按 1/√step 衰减」的工程经验;β₂ = 0.98 不是默认的 0.999,因为训练只有 100k 步、记忆窗口要短一点;label smoothing 0.1 是为 beam search 服务的——它让模型分布柔和,beam search 才能找到更优解;dropout 0.1 在 2017 还是必须,到了大模型时代就退场了。

整个配方放到 8 张 P100 + 12 小时这个尺度上,跑出 BLEU 27.3(base)/ 28.4(big),同时把 GNMT 当时的算力成本砍到 1/10。这是「架构 + 配方」共同作用的结果——单换架构而不调配方,达不到这个 BLEU;反过来,单换配方不换架构,老 RNN 也吃不下这种 batch。

最后值得记住的一点是:现代大模型的训练配方,相对 2017 年「形状」基本没变——还是 Adam 系、还是 warmup、还是 token-based batching、还是 1/√step 形态的衰减(cosine 也是这一类)。改动的只是细节:β₂、weight decay、dropout、label smoothing。这说明梯度下降的「几何性质」是相对 scale 不变的——这套东西大概还会再用 10 年。


二十、常见误解

误解一:warmup 可有可无,去掉只是慢一点。

错。warmup 完全去掉,Transformer 训练大概率直接崩——loss 不下降甚至发散。这是因为 Adam 的 v 在前几百步还没稳定下来,过大的学习率配合不准的方差估计会把参数推到坏区域。

误解二:Transformer 用 Adam 默认参数就行。

错。原论文用 β₂ = 0.98(不是 0.999)、ε = 1e-9(不是 1e-8)。这些不是细节,是经过 ablation 调出来的。直接用默认参数训出来的 BLEU 会差几个点。

误解三:label smoothing 让 PPL 变差,所以是个错误的设计。

不对。label smoothing 让训练 PPL 变差是预期内的(因为最优分布被人为软化),但 BLEU 变好——这才是机器翻译关心的指标。PPL 和 BLEU 不总是同向。

误解四:beam search size 越大越好。

错。beam=4 是原论文的选择。再大(beam=10、20)BLEU 几乎不动甚至变差,因为 length penalty 不再能完全平衡掉短句偏好。beam=4 是大多数翻译系统的甜点。

误解五:现代 LLM 训练完全用了不一样的配方。

部分对。变了很多细节(dropout=0、weight decay=0.1、cosine 衰减等),但「形状」没变——仍然是 Adam 系、warmup、token-based batching、按学习率峰值的某个分数衰减。Transformer 训练的「骨架」是 2017 年定下来的。


二十一、下一步

下一篇 28|原论文实验结果 会从「训练配方」切到「实验结果」——具体数字、消融实验、注意力可视化。我们会讲:

再往后:


二十二、参考文献

  1. Vaswani, A. et al. “Attention Is All You Need.” NeurIPS 2017. 训练配方的源头。
  2. Kingma, D. P., Ba, J. “Adam: A Method for Stochastic Optimization.” ICLR 2015. Adam 优化器原始论文。
  3. Loshchilov, I., Hutter, F. “Decoupled Weight Decay Regularization (AdamW).” ICLR 2019. 现代大模型用的优化器。
  4. Szegedy, C. et al. “Rethinking the Inception Architecture for Computer Vision.” CVPR 2016. Label smoothing 提出。
  5. Srivastava, N. et al. “Dropout: A Simple Way to Prevent Neural Networks from Overfitting.” JMLR 2014. Dropout 原始论文。
  6. Wu, Y. et al. “Google’s Neural Machine Translation System (GNMT).” arXiv:1609.08144, 2016. length penalty 公式来源。
  7. Gehring, J. et al. “Convolutional Sequence to Sequence Learning (ConvS2S).” ICML 2017. Transformer 之前的 SOTA。
  8. Kalchbrenner, N. et al. “Neural Machine Translation in Linear Time (ByteNet).” arXiv:1610.10099, 2016.
  9. Ott, M. et al. “fairseq: A Fast, Extensible Toolkit for Sequence Modeling.” NAACL 2019 demo. 最接近原论文的复现实现。
  10. Vaswani, A. et al. “Tensor2Tensor for Neural Machine Translation.” AMTA 2018. Google 自家的复现。
  11. Xiong, R. et al. “On Layer Normalization in the Transformer Architecture.” ICML 2020. Pre-LN vs Post-LN 系统分析。
  12. Liu, L. et al. “On the Variance of the Adaptive Learning Rate and Beyond (RAdam).” ICLR 2020. 解释 warmup 为什么重要。
  13. Goyal, P. et al. “Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour.” arXiv:1706.02677, 2017. linear warmup 的理论支持。
  14. Brown, T. et al. “Language Models are Few-Shot Learners (GPT-3).” NeurIPS 2020. 现代 LLM 训练配方对照。
  15. Touvron, H. et al. “LLaMA: Open and Efficient Foundation Language Models.” arXiv:2302.13971, 2023.
  16. Touvron, H. et al. “Llama 2: Open Foundation and Fine-Tuned Chat Models.” arXiv:2307.09288, 2023.
  17. Hoffmann, J. et al. “Training Compute-Optimal Large Language Models (Chinchilla).” NeurIPS 2022. 训练成本和数据规模的 scaling law。
  18. Kaplan, J. et al. “Scaling Laws for Neural Language Models.” arXiv:2001.08361, 2020. OpenAI 的 scaling law。
  19. Sennrich, R. et al. “Neural Machine Translation of Rare Words with Subword Units (BPE).” ACL 2016. 原论文用的子词切分方法。
  20. Press, O., Wolf, L. “Using the Output Embedding to Improve Language Models.” EACL 2017. weight tying 的来源。
  21. Bahdanau, D., Cho, K., Bengio, Y. “Neural Machine Translation by Jointly Learning to Align and Translate.” ICLR 2015. WMT MT 评测的早期 SOTA 之一。
  22. Papineni, K. et al. “BLEU: a Method for Automatic Evaluation of Machine Translation.” ACL 2002. BLEU 的原始定义。
  23. Post, M. “A Call for Clarity in Reporting BLEU Scores (SacreBLEU).” WMT 2018. 现代 BLEU 标准化工具。

← 上一篇:26|前馈网络 | 下一篇:28|原论文实验结果

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-15 · transformer

【Transformer 与注意力机制】06|梯度下降与反向传播

神经网络真正会「学习」靠的是两件事:把误差变成可微分的损失函数,再沿着这个损失对参数的梯度方向一点点往下挪。本文从一维抛物线讲到多变量梯度,从两层网络的手算反向传播讲到为什么 backprop 是 O(参数量),再到 Transformer 为什么几乎一律选 Adam/AdamW,希望把「网络是怎么学的」这件事彻底讲透。


By .