反向传播算出了所有参数的梯度。理论上,直接用这些梯度更新参数就行了。但实际训练中有一个频繁出现的问题:梯度偶尔会变得非常大。
为什么梯度会突然变大
几种常见原因:
异常的训练数据。 训练数据是从互联网上爬取的,质量参差不齐。偶尔会遇到一些极端的样本——比如一段全是重复字符的文本,或者编码错误导致的乱码。这些样本会让 loss 突然增大,梯度随之飙升。
数值不稳定。 即使归一化做得很好,28 层的链式法则连乘偶尔也会产生异常大的梯度——尤其是在训练早期,参数还没有稳定下来的时候。
学习率和数据的交互。 某些特定的数据 batch 和当前的参数状态组合在一起,恰好产生了一个非常陡的 loss 曲面,梯度自然就大了。
大梯度的危害
正常梯度的大小在一个可预期的范围内——比如大多数参数的梯度在 ±0.01 之间。更新量 = 学习率 × 梯度,在 ±0.00001 左右,参数只做微调。
但如果梯度突然变成 1000,更新量变成 0.01——这对一个精心训练了很久的参数来说是一次巨幅跳变。参数可能从一个好的位置跳到一个很差的位置,loss 瞬间飙升,之前的训练成果部分或全部被破坏。
更糟的情况是连锁反应:一次大梯度更新导致参数异常 → 下一步的前向传播产生更异常的输出 → loss 更大 → 梯度更大 → 参数跳得更远……训练直接崩溃,loss 变成 NaN(非数字),无法恢复。
梯度裁剪的做法
梯度裁剪(gradient clipping)的思路很简单:如果梯度太大,就把它等比例缩小到一个安全的范围内。
最常用的做法叫 grad_norm clipping:
- 计算所有参数梯度的全局范数(所有梯度平方的和,再开方——类似于向量的长度)。
- 如果全局范数超过一个阈值(比如 1.0),就把所有梯度等比例缩小,使全局范数恰好等于阈值。
# 伪代码
grad_norm = sqrt(sum(g**2 for g in all_gradients))
if grad_norm > max_norm:
scale = max_norm / grad_norm
for g in all_gradients:
g *= scale # 等比例缩小
注意是等比例缩小——所有参数的梯度乘以同一个系数。这保持了梯度的方向不变(更新方向仍然是正确的),只是缩短了步长。
# PyTorch 提供了现成的函数
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
Sophia 的梯度裁剪
Sophia 的训练使用了 grad_norm = 1.0 的裁剪阈值。
这意味着:每一步更新之前,PyTorch 计算所有十亿个参数梯度的全局范数。如果范数 ≤ 1.0,梯度原样使用;如果范数 > 1.0(意味着梯度偏大),所有梯度被等比例缩小到范数恰好为 1.0。
1.0 是一个经验值——大多数语言模型的训练使用 1.0 或附近的值。太小会限制模型每一步能走的距离(训练变慢),太大则起不到保护作用。
在 Sophia 的训练过程中,梯度裁剪在大部分时候不触发(梯度范数小于 1.0),只在遇到异常 batch 时才生效——像一个安全阀,平时不起作用,在压力过大时自动减压。
这个单元学了什么
四个章节覆盖了训练中梯度的全部核心概念:
- 前向传播(第 24 章节):从输入到 loss 的完整计算路径。
- 反向传播(第 25 章节):用链式法则从 loss 反向计算每个参数的梯度。
- 自动微分(第 26 章节):PyTorch 自动记录计算图并算梯度,无需手写。
- 梯度裁剪(本章节):限制梯度大小,防止异常梯度破坏训练。
有了梯度之后,下一步是用梯度来更新参数。但「参数 -= 学习率 × 梯度」这种最简单的更新方式在实践中并不好用——它太容易在陡峭的方向上跳过头,在平坦的方向上又走得太慢。下一个单元介绍优化器——更聪明的参数更新方法。