训练时,某一步的梯度可能突然变得很大。梯度裁剪通过限制梯度的大小,降低异常梯度对更新过程的冲击,但它不能保证训练一定稳定,也不能代替对数据、学习率和数值问题的检查。
全局范数裁剪怎样计算
假设三个参数对应的梯度为 g=[3,4,0]。它的 L2 范数为:
‖g‖₂ = sqrt(3² + 4² + 0²) = 5
设阈值 c=1,理想化的计算形式是:
g' = g × min(1, c/‖g‖₂)
缩放比例为 1/5=0.2,因此裁剪后为 [0.6,0.8,0],范数为
1。梯度小于阈值时无需放大。实现通常还会加入很小的数值稳定项。
全局范数是把所传入参数的梯度看作一个整体计算,不能误解为“每个参数各自最多为
1”。PyTorch 的 clip_grad_norm_
会原地修改梯度,并返回裁剪前的总范数。PyTorch
文档
对参数更新意味着什么
在普通 SGD 且没有其他更新项的简化情况下:
θ_new = θ_old − learning_rate × g'
若某参数原值为 1.5、学习率为 0.01,使用裁剪后的梯度 0.6,则新值为
1.494;不裁剪、使用梯度 3 时,新值为 1.47。
这只说明这一步的差异,不能因为更新较小就断言“绝对安全”。Adam、动量与权重衰减等机制还会改变更新,因此不能把梯度范数直接等同于所有优化器的参数更新范数。
范数裁剪与逐元素裁剪
| 方式 | 处理对象 | 对方向的影响 |
|---|---|---|
| 全局范数裁剪 | 按同一比例缩放整体梯度 | 对有限非零梯度保持方向 |
| 逐元素数值裁剪 | 将每个分量限制到区间 | 一般会改变整体方向 |
例如把 [3,4] 每一项限制到 [-1,1],得到
[1,1];按 L2 范数裁剪到 1,则得到
[0.6,0.8]。两者不是同一种操作。
裁剪应放在哪一步
下面是一个可独立运行的 PyTorch 单步示例:
import torch
model = torch.nn.Linear(2, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
x = torch.tensor([[1.0, 2.0]])
y = torch.tensor([[1.0]])
optimizer.zero_grad()
loss = torch.nn.functional.mse_loss(model(x), y)
loss.backward()
norm_before = torch.nn.utils.clip_grad_norm_(
model.parameters(), max_norm=1.0, error_if_nonfinite=True
)
optimizer.step()
print("裁剪前范数:", float(norm_before))
顺序是反向传播生成梯度、裁剪梯度、优化器更新。手写训练循环时需要显式调用,不能认为它天然会自动发生。
采用梯度累积时,通常在本次更新所需梯度累积完后再裁剪。使用带梯度缩放的混合精度训练时,需要先解除缩放再裁剪,避免用放大后的梯度与原尺度阈值比较。
阈值应该怎样理解
max_norm=1.0
是常见起点,不是跨模型通用最优值。可以观察裁剪前范数和触发频率:如果几乎每步都被强烈缩放,应进一步检查学习率、损失尺度和异常样本。
已经出现 NaN
的梯度不会因为裁剪就自动恢复正常。裁剪是控制更新过程的一项措施,不是错误数值的修复器。