5.3 梯度裁剪:拉住失控的梯度


5.3 梯度裁剪:拉住失控的梯度

本节摘要:梯度爆炸会让训练几步内跳到 NaN,梯度裁剪则给梯度设一道"刹车"。本节从爆炸的成因讲起,分词剪法与范数剪法,给出阈值设定建议与早停判断,最后用一段能改的理论演示如何靠裁剪救回一个爆炸中的训练。

第 2 章讲过梯度爆炸是三大顽疾之一。这一节给它上专门的刹车——梯度裁剪。它不改变方向,只限制每个梯度的"冲击力",让训练不至于因为一个失控的梯度瞬间崩盘。

一、爆炸从哪来

梯度爆炸多半源于反向后链式连乘,某层梯度乘出极大的数,一步就把参数顶飞,损失直接 NaN。常见触发:层太深、激活在饱和区、权重初始化过大、或天涯学习率。

其典型信号非常醒目:训练几步以内损失变为 NaN(不是 0/1,是"不是数"),伴随权重出现极大值甚至无穷。

二、两种踩刹车的方式

  • 逐维度裁剪(Clip by value):把每个梯度分量限制在 [−c, c] 内,超出就截断。简单粗暴,但会破坏梯度的整体方向比例。
  • 范数裁剪(Clip by norm):先算整个梯度向量的范数,若超过阈值 c,就把整个向量缩放到 c 的长度,超出则按比例缩放。它保留方向、只限制大小,多数框架推荐。
import math def clip_by_norm(grad, max_norm=1.0): norm = math.sqrt(sum(g * g for g in grad)) if norm > max_norm: scale = max_norm / norm return [g * scale for g in grad] return grad

范数裁剪的直觉:不改变"往哪个方向"的信息,只是把这个方向的"速度"限制在阈值内,让你不会因为一次巨振直接翻车。

三、阈值怎么设

  • 常见默认 1.0 附近;太大形同没裁,太小则把正常的梯度也压扁,训练变慢。
  • 设在"恰好能挡住最极端的一批梯度"即可——你可以看一次梯度范数的分布,把阈值放在分布上限,而不是硬塞个珠光假大小。
  • 它对动量/自适应优化器也有效,因为它作用于"每次反向给的梯度"这一环,在更新前拦下。

四、裁剪不是万能布

裁剪是刹车,不是治本。它把损失从 NaN 拉回来,但根本诱因(比如初始化过大、学习率过高)不修,就会反复爆炸。所以正确的使用法是:

  1. 先用裁剪把训练稳住、看到它不崩。
  2. 再回头压缩真正的"炸药":调小学习率、换合理初始化、检查激活是否在饱和区。
  3. 稳定之后逐步放宽裁剪,确认不是"一直在靠刹车勉强开"。

💡 关键直觉:看到 NaN,第一反应不是处理 NaN,而是问"是什么把它顶成 NaN"。裁剪是止损,根因排查(学习率/初始化/激活)才是祛病。

五、一个救火案例

你训一个深层网络,第 3 步损失变 NaN。你打开梯度裁剪,阈值 1.0,训练终于不再崩,但损失卡着不动——原来学习率还是偏大。你把学习率从 1e-2 降到 1e-3,同时保留裁剪作保险,训练平稳收敛到 0.1 以下。

这个案例的价值在于:裁剪让你第一次"看清"真实训练,但真正让模型变好的,是你在它有保护之后再去调的根因。

现象 信号 处理
梯度爆炸 几步内 NaN 裁剪 + 降学习率 + 查初始化
裁剪后仍卡 损失不动 根因可能是学习率或激活
裁剪失效大概率 反复爆炸 回到初始化/架构找问题

六、剪"值"还是剪"范数":什么时候两者差很大

逐值裁剪把每个分量各自钳进 [−c, c],实现最快、直觉也最直白,但它有个副作用:它可能改变梯度的方向比例。 举个极端例子,一个方向梯度是 [1000, 1],逐值钳到 1.0 后变成 [1, 1]——原来 1000 的主力方向被压得和 1 一样大,方向信息被严重扭曲。范数裁剪则只把整个向量缩放到阈值 c,[1000, 1] 会按同一比例缩放成接近 [c×0.999, c×0.001],比例完好、只是整体限了速。

所以准则很简单:只想"限大小"就用范数裁剪(更保方向),确需逐分量控制才用按值裁剪。 框架默认多走范数这条路是有原因的。

七、裁剪和动量、自适应之间的次序

裁剪作用在梯度那一环、在优化器把梯度变成更新之前的入口处。它跟动量、自适应并不冲突,也不该互相替代:动量让方向更顺,自适应让各参数各走各的,裁剪则给整体冲击力设上限。三者工作在不同环节,可以同时开着。唯一要注意的是,别把"被裁剪"误当成"训练已经安全"——裁剪只是止损,真正的根因(学习率、初始化)没修,你只是在靠安全垫勉强开。

八、怎么用"梯度范数分布"挑阈值而不是瞎设

与其拍脑袋设一个 1.0,不如让数据替你说话:抽几个 batch,在裁剪前打印每 batch 的梯度范数,看它们的分布。

# 概念片段:看一批的梯度范数分布来定阈值 for i, (x, y) in enumerate(data): loss = model(x) loss.backward() total_norm = sum(p.grad.norm().item() ** 2 for p in model.parameters()) ** 0.5 print(f"batch {i}: grad norm = {total_norm:.3f}")

如果一个 batch 的范数大多落在 [0.3, 2.0],只有个别随机冲到 50,那把阈值设在 5.0 左右就很合理——恰好挡住极端值,又不压扁正常量级。阈值要"恰好拦下最暴的脉冲",而不是"看上去很大"或"看上去很小"。

九、裁剪之后还要复查的几件事

把裁剪加上、损失回到非 NaN,只能算"止血"。接下来要按顺序复查真正的病根:

  1. 学习率:是否比合理区间大了一个量级?先降到稳妥值再谈其他。
  2. 初始化:权重是否被初始化得过大,导致前向激活爆发、反向梯度连锁放大。
  3. 激活:是否进入了饱和区(如 Sigmoid / Tanh 两端),导数趋零。

只有把这三点里的"炸药"拆掉,裁剪才能从"常开的安全垫"降级为"偶尔兜底的保险",不再是你训练日常里离不开的拐杖。

本节要点回顾

  • 成因:反向连乘把梯度顶得极大,一步顶飞、损失 NaN。
  • 两种剪法:逐值裁剪快但破坏方向;范数裁剪保方向只限大小,更推荐。
  • 阈值:设在梯度分布上限附近,默认 1.0 起步。
  • 是刹车不是治本:先靠裁剪稳住,再回头治根因(学习率、初始化、激活)。
  • 救火流程:裁剪止 NaN → 调学习率 → 收敛后再逐步放宽裁剪。

刹车装好了,一台车还没"调校到位"。下一节去调校车间,用超参数调优找到最顺手的配比。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U