第 14 章 · 扩展与正则化

读懂一份有代表性的 LLM 训练配置(学习率、预热、裁剪、权重衰减),并解释每个旋钮在防止什么出错。

在深层 Transformer 上,交叉熵 + AdamW 还不够——没有特定的工程技巧,损失会发散、梯度会爆炸、模型会过拟合。

7 分钟
术语表 · 6 个术语
AdamW
LLM 预训练的主流优化器。Adam(逐参数自适应步长)加上解耦的权重衰减(即那个 W)。
learning rate warmup
在训练最初约 1-10% 的步数里,从 0 线性爬升到 lr_max。让优化器的滑动平均先稳定下来,再迈大步。
cosine decay
预热之后,学习率沿半个余弦波从 lr_max 降到 lr_min,覆盖剩余的全部步数。LLM 预训练的标准做法。
gradient clipping
若 ||g|| > c(通常为 1.0),就把 g 缩放为 c/||g||。当坏 batch 产生巨大梯度时给步长封顶。
weight decay
与 ||θ||^2 成正比的惩罚项,加进损失(或者像 AdamW 那样,直接从权重里减去)。把权重拉向零,起正则化作用。
dropout
训练时随机把一部分激活置零,防止模型过度依赖任何单一特征。旧模型里常见;现代 LLM 预训练往往不用——数据集的多样性完成了正则化。

扩展与正则化:让训练循环真正收敛

第 13 章把训练化简成一行交叉熵。这个描述正确,但作为配方惊人地不够用。一个用朴素 SGD 在数千亿 token 上训练的 24 层 Transformer 会:在前 100 步内发散;因为单个坏 batch 让损失飙到 NaN(Not-a-Number——算术爆炸到浮点数装不下);落进一个泛化很差(训练文本上表现好、没见过的文本上表现差)的局部极小值。解法是一小撮工程技巧——没有一个属于模型架构,但每一次现代训练都全部用上。

先问一句:这里说的“扩展”到底是什么?标题里这个词指的是参数量。一条直线 y = a·x + b 有两个参数。这个模型有将近十亿个。最大的研究模型则有几千亿个。下面这个阶梯把它们全都放到同一条对数轴上,好让这道鸿沟看得清楚——也好对“这个模型究竟处在什么位置”保持诚实。

参数阶梯(对数刻度)
≈ 这个模型的 205 倍
10^010^210^410^610^810^1010^12一条直线2这个模型852,985,920GPT-2 XL(2019)~1.5BGPT-3(2020)175B
参数更少参数更多

从 GPT-2 到 GPT-3 这一跳几乎是纯粹的扩展:同一套配方,参数约 100 倍,输出就从“语法通顺但说胡话”变成了出人意料的连贯。

这里只能读作原始参数量的比较。GPT-3 是 dense Transformer——每一层都是 softmax 注意力。而这个模型是混合架构:只有 6 层全注意力,外加 18 层 GatedDeltaNet(线性循环)层。所以条形更长并不意味着“同一种注意力更多”——它是一种不同形状的网络,而不是放大版的副本。

这根横档为什么放在本章:堆参数不会自动导致过拟合。在足够多文本上训练的更大模型,泛化得更好而非更差——这正是下面权重衰减和 dropout 这些旋钮成为一个有讨论价值的问题(而不是显而易见的稳赢)的原因。你是在“模型本就巨大、数据本就海量”的前提下,去调正则化的。

在讲技巧之前,先说整个扩展故事赖以成立的一个前提:Transformer 之所以值得扩展,是因为它的训练计算几乎全是对所有位置同时进行的矩阵乘法。RNN 必须算完第 i 个 token 才能碰第 i+1 个;Transformer 一次并行前向就处理整个序列——因果掩码正是让这次并行保持诚实的那道闸门——而巨大的批量矩阵乘法恰好是 GPU 为之而生的工作负载。(我们这个模型的 GatedDeltaNet 层在解码时确实是逐 token 循环运行的;并行优势说的是训练和预填充。)这才是这个架构真正买到的东西:不是某种单一的聪明行为,而是便宜到可以一路扩展、直到聪明行为自己涌现的计算。

优化器:AdamW,而不是 SGD

朴素的随机梯度下降(θ := θ - η·g——把每个权重 θ 沿其梯度 g 的反方向微调,按学习率 η 缩放)在 Transformer 上效果不佳。不同参数看到的梯度量级天差地别——单一的全局步长,要么对响亮的参数太大,要么对安静的参数太小。Adam 为每个参数维护 g 的滑动平均,再按 √g² 归一化迈步,于是每个参数的步长由它自己的梯度历史重新标定,而不是共用一个速率。“滑动平均”(论文称之为矩,moments)一点也不玄: avg ← 0.9·avg + 0.1·g——保留 90% 的昨日估计,混入 10% 的新梯度。AdamW 加上了解耦权重衰减:不是通过损失去惩罚 ||θ||²,而是优化器在每一步直接从 θ 里减去它的一小部分。为什么要把权重往零拉?大权重让单个特征压过一切;保持权重小,迫使模型把证据分散到许多特征上。这是每个现代 LLM 预训练的标准配方。

学习率调度:预热 + 余弦

这是 LLM 训练里最普及的技巧。学习率不是常数——它走一条山丘形的曲线:

  • 线性预热:训练最初约 1-10% 的步数里,从 0 升到 lr_max
  • 余弦衰减:剩余步数里,从 lr_max 降到 lr_min(≈ 1e-5)。

预热存在,是因为 AdamW 的滑动平均需要几百步的梯度历史才有意义;第 1 步就迈满步长,基本等于朝随机方向开枪。余弦衰减存在,是因为训练后期受益于越来越小的步长——模型离极小值更近,大步会把它弹出去。

学习率调度——线性预热 + 余弦衰减

标准配方:最初约 10% 的步数里从 0 线性预热到 lr_max,随后余弦衰减到约 1e-5。预热让早期梯度不至于在优化器的滑动平均尚未积累时爆掉;衰减让训练后期安顿在一个低损失的平台上。

02,5005,0007,50010,000训练步03e-4学习率预热余弦衰减

把预热拉到 0,看曲线直接从 lr_max 起步——这就是没有预热时发生的事。把峰值学习率调得更高(例如 1e-3),你就能看出为什么没有梯度裁剪时训练初期会发散:在一个全新的模型上,任何一记大步都会把权重带到无法恢复的地方。

有预热 vs 无预热——损失曲线
激进峰值学习率 1e-3(压力测试) · 10,000 步

足够大的学习率作用在近乎随机的初始权重上,可能产生一次巨大的首步,让损失不降反升。预热在训练最初约 1–10% 的步数里把学习率从 0 拉到 lr_max,让这些早期步保持温和;随后由余弦衰减接手。下方的无预热曲线是刻意激进的设定——峰值学习率 1e-3,远高于上方调度所用的 3e-4——为的是让爆炸清晰可见;更温和的设定可能只是抖一抖又恢复,而不会彻底发散。许多「损失在第 1 步就爆炸了」的故事背后,就是这个形状。

02,5005,0007,50010,000训练步09损失已发散(NaN)
有预热——损失从 6.5 平滑衰减到接近 2.0 的平台。AdamW 的滑动平均尚未积累时,早期步长极小,所以什么都不会爆炸。

仅为示意——两条曲线都是脚本化的公式(指数衰减 vs 被钳制的爆炸),不是真实训练输出。损失轴为了易读而被压缩:对这么大的词表,真实随机初始化交叉熵约为 ln(248,320) ≈ 12.4,而不是 6.5。

梯度裁剪:一行代码的护栏

单个坏 batch——比如一段全是空白的文本,或一个分词器的边角案例——能产生巨大的梯度。没有保护时,AdamW 会尽职尽责地朝那个方向迈出巨大一步,损失从 2.5 跳到 8.0,模型要花几百步才能恢复(如果还能恢复的话)。

按范数裁剪(clip-by-norm)是通用答案。计算模型全部梯度的全局 L2 范数,若超过裁剪阈值 c(几乎总是 1.0),就把每个分量按 c / ||g|| 缩放。方向不变、幅度封顶,训练平稳继续。下面的小部件让你把两个旋钮都拧拧看。

梯度裁剪——范数超过阈值时整体缩放

反向传播算出逐参数梯度后,取它们的全局 L2 范数 ||g||——每个分量平方、求和、再开方。若超过裁剪阈值 c,就把每个分量按 c / ||g|| 缩放。方向保持不变;只有幅度被封顶。一个小算例:g = [3, 4] ||g|| = √(9 + 16) = 5;裁剪到 c = 1.0 → 乘以 1/5 = 0.2 [0.6, 0.8]——方向相同,长度只剩五分之一。

原始梯度(逐参数)
g0
0.40
g1
-0.20
g2
0.10
g3
-0.35
g4
0.25
g5
0.15
g6
-0.30
g7
0.20
||g|| = 0.74
按范数裁剪之后
g0
0.40
g1
-0.20
g2
0.10
g3
-0.35
g4
0.25
g5
0.15
g6
-0.30
g7
0.20
||g_clipped|| = 0.74

把梯度缩放往上拉——超过 ~0.07 后你会看到 ||g|| 超出阈值、裁剪面板变成琥珀色。没有这道护栏,单个坏 batch(序列中段的损失尖峰)就能把 24 层堆叠的权重推进训练无法恢复的区域。c = 1.0 是 LLM 预训练的默认值。

正则化:dropout 的缓慢退场

原始 Transformer 论文激进地使用 dropout——每个注意力层、每个 MLP、每条残差连接。现代 LLM 预训练配置通常把 dropout 设为 0。两个原因:

  • 预训练数据极其充裕。在数万亿 token 的语料上,模型对每个 token 大约只看一次。不存在可供 dropout 预防的“背下训练集”失效模式。
  • 权重衰减覆盖了大部分相同的地盘。AdamW 的衰减项把权重拉向零,防止任何单一特征独大。

微调是另一回事——小而精的数据集可能过拟合(模型背下那些具体样例,而不是学到可迁移的模式),dropout 在微调配方里常以非零值(通常 0.05-0.1)重新出现。

读一份有代表性的配置

上面这些技巧的组合,把一次训练从“立刻发散”变成“终于收敛”。一份有代表性的预训练配置——泛指,不是任何特定模型公布的设置——大致长这样:

optimizer:   AdamW(beta1=0.9, beta2=0.95, eps=1e-8, weight_decay=0.1)
lr:          3e-4 peak, 2000 warmup steps, cosine decay to 1e-5
grad_clip:   1.0  (clip by global norm)
dropout:     0.0  (pretraining)
batch_size:  4M tokens (gradient accumulation across many devices)
seq_len:     8192
total_steps: 500,000

其中两个数字值得乘出来。“4M token 的 batch”是每个优化器步 512 sequences × 8,192 tokens = 4,194,304 ≈ 4M 个 token。而整次训练是 4M × 500,000 steps ≈ 2 trillion tokens——这就是训练一章反复念叨的那个“数万亿”。

这块配置里不那么显然的旋钮,用大白话说:

  • beta1 = 0.9beta2 = 0.95——AdamW 两个滑动平均的遗忘速度。beta1 平滑梯度 g;beta2 平滑梯度的平方 。值越高 = 记忆越长。
  • eps = 1e-8——加在分母里的微小常数,保证当某个参数的 平均接近零时,步长不会除以零。
  • weight_decay = 0.1——上文那股拉向零的力的强度;0.1 是典型的预训练取值。
  • 梯度累积(gradient accumulation)——先把几个小 batch 的梯度加起来,再做一次优化器步,让少量 GPU 也能模拟出一个它们根本装不进内存的 4M token 巨型 batch。

这里的每一行,都是针对过去十年 LLM 训练中用血泪换来的某个具体失效模式的护栏。架构是模型本身;而这份配方,才让架构得以训成。

想学更多

本章是整门课中刻意最轻的一章——不内化这里的每个细节也能训练 LLM,但不认识这些旋钮就读不懂研究论文。这套代码库里可训练的那一侧,见 @mlx-node/trl(GRPO 与 SFT)和 crates/mlx-tuimlx-train TUI 程序)——它们在 Apple Silicon 上实现的是同一份配方。

工程要点
  • 深层 Transformer 不是用固定学习率训练的——预热接余弦是标准调度,也是训练初期损失曲线保持正常的原因。
  • 梯度裁剪是一行代码的护栏,把“模型在第 3,247 个 batch 上发散了”变成损失曲线上的一个小鼓包。
  • dropout 基本退出了现代 LLM 预训练;权重衰减 + 数据规模 + 早停纪律取代了它。
动手练习

在学习率小部件里,把预热设为 0、峰值学习率设为约 1e-3,然后看曲线的开头。对一个全新初始化的模型,用这个学习率直接开训会发生什么?余弦的那一半为什么存在?

随堂测验
1. 为什么 LLM 训练要从学习率预热开始?
2. “把梯度范数裁剪到 1.0”做的是什么?
3. 为什么现代 LLM 预训练配置不再像 2017 年的模型那样使用 dropout?