27

梯度裁剪——grad_norm 为什么要限住

阅读约 5 分钟第 27/60 课

反向传播算出了所有参数的梯度。理论上,直接用这些梯度更新参数就行了。但实际训练中有一个频繁出现的问题:梯度偶尔会变得非常大。

为什么梯度会突然变大

几种常见原因:

异常的训练数据。 训练数据是从互联网上爬取的,质量参差不齐。偶尔会遇到一些极端的样本——比如一段全是重复字符的文本,或者编码错误导致的乱码。这些样本会让 loss 突然增大,梯度随之飙升。

数值不稳定。 即使归一化做得很好,28 层的链式法则连乘偶尔也会产生异常大的梯度——尤其是在训练早期,参数还没有稳定下来的时候。

学习率和数据的交互。 某些特定的数据 batch 和当前的参数状态组合在一起,恰好产生了一个非常陡的 loss 曲面,梯度自然就大了。

大梯度的危害

正常梯度的大小在一个可预期的范围内——比如大多数参数的梯度在 ±0.01 之间。更新量 = 学习率 × 梯度,在 ±0.00001 左右,参数只做微调。

但如果梯度突然变成 1000,更新量变成 0.01——这对一个精心训练了很久的参数来说是一次巨幅跳变。参数可能从一个好的位置跳到一个很差的位置,loss 瞬间飙升,之前的训练成果部分或全部被破坏。

更糟的情况是连锁反应:一次大梯度更新导致参数异常 → 下一步的前向传播产生更异常的输出 → loss 更大 → 梯度更大 → 参数跳得更远……训练直接崩溃,loss 变成 NaN(非数字),无法恢复。

梯度裁剪的做法

梯度裁剪(gradient clipping)的思路很简单:如果梯度太大,就把它等比例缩小到一个安全的范围内。

最常用的做法叫 grad_norm clipping:

  1. 计算所有参数梯度的全局范数(所有梯度平方的和,再开方——类似于向量的长度)。
  2. 如果全局范数超过一个阈值(比如 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 时才生效——像一个安全阀,平时不起作用,在压力过大时自动减压。

这个单元学了什么

四个章节覆盖了训练中梯度的全部核心概念:

  1. 前向传播(第 24 章节):从输入到 loss 的完整计算路径。
  2. 反向传播(第 25 章节):用链式法则从 loss 反向计算每个参数的梯度。
  3. 自动微分(第 26 章节):PyTorch 自动记录计算图并算梯度,无需手写。
  4. 梯度裁剪(本章节):限制梯度大小,防止异常梯度破坏训练。

有了梯度之后,下一步是用梯度来更新参数。但「参数 -= 学习率 × 梯度」这种最简单的更新方式在实践中并不好用——它太容易在陡峭的方向上跳过头,在平坦的方向上又走得太慢。下一个单元介绍优化器——更聪明的参数更新方法。