Tracks
在训练深层神经网络时,您多少次见过损失值变成 NaN?
训练了好几个小时后,损失曲线一直很健康,突然之间却飙到无穷大。通常原因是梯度爆炸——在反向传播过程中梯度值变得过大,导致参数更新不稳定、模型崩溃。这个问题在循环网络中最为严重,但在 transformer 和深度前馈网络中也会出现。
梯度裁剪通过在梯度到达优化器之前限制其大小来解决这个问题。它只需在训练循环中加一行代码,就能在不改变模型的前提下,将更新幅度限制在合理范围内。
本文将介绍梯度裁剪背后的直觉、两种主要方法、如何选择阈值,以及如何在 PyTorch 和 TensorFlow 中实现。
但数据科学中的“损失”究竟是什么?阅读我们的 机器学习中的损失函数博文了解详情。
什么是梯度裁剪?
梯度裁剪是一种在训练过程中限制梯度幅度的技术,用于防止不稳定的参数更新。
当梯度过大时,优化器会在参数空间中迈出巨大一步,将权重推到损失爆炸的区域。裁剪通过在伤害发生前对步长进行封顶来帮您避免这种情况。
需要注意的是,梯度裁剪不会影响模型架构。您无需添加层或更改激活函数。它只是在反向传播与优化器更新之间拦截梯度,从而修改训练过程。
这使得它尝试成本低、移除也容易。正如您稍后所见,只需一行代码。
梯度裁剪如何工作
机制很简单。裁剪操作位于反向传播与优化器步之间,每次迭代都遵循相同的四个步骤。
- 计算梯度: 执行前向传播,计算损失,并进行反向传播。此处不作改变,梯度像往常一样在网络中流动。
- 检查梯度幅度: 测量梯度有多大。根据方法不同,这可能意味着查看单个值或计算所有参数的整体范数。
- 超过阈值则缩小梯度: 如果幅度超出您设定的上限,就将梯度按比例缩小;否则保持不变。
- 更新模型参数: 将裁剪后的梯度传给优化器并应用权重更新。
大多数时候,您的梯度都低于阈值,训练过程与未使用梯度裁剪时相同。当突发尖峰出现时,裁剪会在优化器反应之前将其拦下。
就是这样。
常见的梯度裁剪方法
梯度裁剪有两种做法,差别在于您测量什么、缩放什么。
按数值裁剪(clip by value)
按数值裁剪会对每个梯度元素单独封顶。
您选定一个区间,例如 [-1.0, 1.0],任何超出该区间的梯度值会被截到最近的边界。2.5 会变为 1.0,-2.5 会变为 -1.0。区间内的值保持不变。

按数值裁剪示例
它的优点在于足够简单。除最小/最大操作外几乎没有额外计算,运行也很快。
但这种方法也有缺点。对单个值进行裁剪会改变梯度向量的方向。如果某个分量被裁剪而其他分量未裁剪,更新后的向量就不再指向反向传播建议的方向。优化器最终会朝着略微错误的方向迈步。
这也是实践中较少使用按数值裁剪的原因。
按范数裁剪(clip by norm)
按范数裁剪会在整体幅度超出阈值时缩放整个梯度向量。
它不看单个值,而是将所有梯度一起计算范数(通常为 L2 范数),并与最大值比较。若范数低于阈值,则不作处理;若高于阈值,则将每个梯度乘以同一个缩放因子,把范数拉回到上限。

按范数裁剪示例
其优势在于保持方向不变。由于每个分量按相同系数缩小,梯度向量仍指向原始方向。您只是缩短了步长,而不是改变方向。
这正是按范数裁剪成为标准的原因。PyTorch 的 clip_grad_norm_ 和 TensorFlow 的 clipnorm 都实现了该方法,且大多数现代训练流程默认使用它。
梯度爆炸 vs 梯度消失
梯度爆炸和梯度消失都是深度学习中常见的问题,但梯度裁剪只解决其中之一。
梯度爆炸
当反向传播过程中梯度值过度增大时,就会发生梯度爆炸。
这通常出现在深层网络或循环架构中,梯度要在许多层或时间步上相乘。如果这些乘积朝着不利方向累积,梯度幅度就会暴涨。此时优化器会进行一次巨大更新,权重跳到极端值,损失常常变为 NaN 或 Inf。
您会看到损失突然尖峰,或模型莫名其妙地发散。
梯度消失
梯度消失则相反。梯度在向后传播过程中逐渐趋近于零。
当梯度太小时,权重更新变得微乎其微。早期层停止学习、更深层学习缓慢,训练几乎停滞。损失曲线变平,即使经过许多轮也不再改善。
在 LSTM 和 GRU 出现之前,这曾是 RNN 在长序列上表现不佳的主要原因。
梯度裁剪的作用范围
梯度裁剪针对的是梯度爆炸,而非梯度消失。
裁剪会缩小过大的梯度,但当梯度过小时无能为力。应对梯度消失,需要更好的权重初始化、残差连接、批量归一化,或那些能保持梯度流动的架构。
按范数裁剪详解
当人们搜索梯度裁剪时,绝大多数其实是在找按范数裁剪的方法。
流程分三步:首先计算所有梯度合并后的范数;其次与您选择的阈值比较;最后若范数过大则对梯度重缩放。
范数通常为 L2 范数,即将每个梯度值平方、求和、再开方。若跨所有模型参数的梯度为 g_1, g_2, ..., g_n,则 L2 范数为:

按范数裁剪公式
得到范数后,与阈值 c 比较。若 ||g|| <= c,梯度保持不变;若 ||g|| > c,则每个梯度都乘以缩放因子 c / ||g||,使新的范数恰好等于 c。
重要之处在于,每个分量都按相同因子缩小。梯度值之间的相对比例保持不变,意味着向量仍指向原始方向。您只是缩短了优化器迈出的步长,而不是改变步向。
这种保持方向的特性使得范数裁剪成为默认选择。按数值裁剪可能会扭转梯度向量方向;按范数裁剪只改变长度。
PyTorch 的 clip_grad_norm_ 和 TensorFlow 的 clipnorm 都是这样做的。人们说“我在用梯度裁剪”时,几乎总是指按范数裁剪。
如何选择梯度裁剪阈值
阈值是一个超参数,意味着没有放之四海而皆准的值能适用于每个模型。
若设得过高,裁剪几乎不会触发。梯度几乎总在上限之下,这张安全网就抓不到任何异常。训练与未使用裁剪时无异,梯度爆炸时您仍会看到损失尖峰。
若设得过低,裁剪过于激进。每个批次的梯度都会被缩小,导致权重更新小于应有幅度。学习变慢,模型收敛时间更长,有时会长很多。
常见的起点是 1.0,对许多架构都比较有效。0.5 到 5.0 的范围覆盖了大多数实际用例。
更好的做法是在训练期间监控梯度范数。记录每一步未裁剪的范数并观察其分布。若大多数范数在 0.3 左右,偶尔飙到 50,就把阈值设在典型范围之上但远低于尖峰的位置——2.0 或 3.0 在这里都很合理。
像对待其他超参数一样对待它。从 1.0 起步,观察训练表现,再据此调整。
循环神经网络中的梯度裁剪
RNN 是梯度裁剪首先成为标准做法的地方。
原因在于 RNN 的时间反向传播方式。时间反向传播会在许多时间步上重复相乘相同的权重矩阵,这些重复乘法可能累积成巨大的数值。序列越长问题越严重。
LSTM 和 GRU 通过门控机制缓解了问题,但并未彻底消除。在长序列或较大学习率训练时,这两种架构仍然从裁剪中受益。
对于 RNN 训练,通常默认使用按范数裁剪,阈值在 1.0 到 5.0 之间。如果您在使用 PyTorch 的 nn.LSTM 或 nn.GRU 时损失在训练中发生爆炸,添加 clip_grad_norm_ 通常是首选尝试。
现代深度学习中的梯度裁剪
当 transformer 取代 RNN 时,梯度裁剪并未消失。
像 GPT 和 BERT 这样的大型语言模型在预训练和微调时都会使用裁剪。视觉 transformer、扩散模型以及具有数百层的大多数深度架构也是如此。现代训练中占主导地位的 Adam 和 AdamW 通常会与范数裁剪配合使用,阈值大多在 1.0 附近。
原因与 RNN 相同。深层网络在多层间相乘梯度;大批量大小与高学习率的组合会偶尔产生梯度尖峰。裁剪在不影响正常训练步骤的情况下处理这些尖峰。
大多数参考实现默认包含裁剪。Hugging Face 的 Trainer、PyTorch Lightning 和 DeepSpeed 都将裁剪作为标准配置选项。如果您训练的模型不止是一个小玩具模型,裁剪几乎肯定是流程的一部分。
它是一行的增量,几乎没有成本,却能防止耗时数小时的训练崩溃。这就是它被保留下来的原因。
PyTorch 中的梯度裁剪
PyTorch 通过一个工具函数处理梯度裁剪:torch.nn.utils.clip_grad_norm_。
裁剪调用应位于 loss.backward() 与 optimizer.step() 之间。反向传播要先填充梯度,然后(必要时)裁剪缩小,最后优化器应用更新。放在其他位置都不起作用。
下面是一段完整、可运行的训练脚本:在合成回归数据上训练一个小型 MLP,并启用梯度裁剪:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(42)
# Synthetic regression data
n_samples = 1000
n_features = 20
inputs = torch.randn(n_samples, n_features)
targets = (inputs.sum(dim=1, keepdim=True) * 2.0 + torch.randn(n_samples, 1) * 0.1)
dataset = TensorDataset(inputs, targets)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# Small feedforward network
model = nn.Sequential(
nn.Linear(n_features, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, 1),
)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.MSELoss()
# Training loop with gradient clipping
n_epochs = 5
max_grad_norm = 1.0
for epoch in range(n_epochs):
epoch_loss = 0.0
for batch_inputs, batch_targets in dataloader:
optimizer.zero_grad()
predictions = model(batch_inputs)
loss = loss_fn(predictions, batch_targets)
loss.backward()
# Gradient clipping
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_grad_norm)
optimizer.step()
epoch_loss += loss.item()
print(f"Epoch {epoch + 1}: loss = {epoch_loss / len(dataloader):.4f}")

PyTorch 输出
clip_grad_norm_ 函数接收两个主要参数:
-
parameters:希望裁剪其梯度的模型参数。传入model.parameters()可覆盖整个模型。 -
max_norm:梯度范数的阈值。1.0是常见的起点。
还有一个可选的 norm_type 参数,默认 2.0 为 L2 范数。通常无需更改。
clip_grad_norm_ 末尾的下划线表示原地操作。该函数直接修改每个参数 .grad 属性中的梯度,因此无需关注返回值。不过它会返回裁剪前的总梯度范数,便于记录日志。
若要按数值而非按范数裁剪,PyTorch 提供 torch.nn.utils.clip_grad_value_:
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)
但如前所述,您很少(如果不是从不的话)会用到它。
这就是全部配置。在训练循环中增加两行。
TensorFlow 中的梯度裁剪
TensorFlow 在优化器层面处理裁剪,而不是单独的函数调用。
创建优化器时,您将 clipnorm 或 clipvalue 作为参数传入。优化器会在每一步内部应用裁剪,您无需修改训练循环。
下面是使用 Keras API 在合成回归数据上的完整示例:
import numpy as np
import tensorflow as tf
tf.random.set_seed(42)
np.random.seed(42)
# Synthetic regression data
n_samples = 1000
n_features = 20
x_train = np.random.randn(n_samples, n_features).astype(np.float32)
y_train = (x_train.sum(axis=1, keepdims=True) * 2.0
+ np.random.randn(n_samples, 1).astype(np.float32) * 0.1)
# Small feedforward network
model = tf.keras.Sequential([
tf.keras.layers.Dense(64, activation="relu", input_shape=(n_features,)),
tf.keras.layers.Dense(64, activation="relu"),
tf.keras.layers.Dense(1),
])
# Optimizer with gradient clipping by norm
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3, clipnorm=1.0)
model.compile(optimizer=optimizer, loss="mse")
model.fit(x_train, y_train, epochs=5, batch_size=32)

TensorFlow 输出
这两个参数的作用不同:
-
clipnorm按每个梯度张量的 L2 范数进行裁剪。若范数超过阈值,则按比例缩小该张量。 -
clipvalue对每个梯度元素单独裁剪。超过正阈值的值被夹到阈值,低于负阈值的值被夹到负阈值。
要从按范数裁剪切换到按数值裁剪,只需更换参数即可:
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3, clipvalue=0.5)
这两个参数适用于所有 Keras 优化器:Adam、SGD、RMSprop、AdamW 等。另有 global_clipnorm 参数,可基于合并所有梯度后计算的范数进行裁剪,而不是按张量逐个裁剪。这与 PyTorch 的默认行为更为一致。
如果您使用 tf.GradientTape 编写自定义训练循环,调用 apply_gradients 时优化器仍会处理裁剪:
for epoch in range(5):
for batch_x, batch_y in zip(np.array_split(x_train, 32), np.array_split(y_train, 32)):
with tf.GradientTape() as tape:
predictions = model(batch_x, training=True)
loss = tf.reduce_mean(tf.square(predictions - batch_y))
gradients = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
这就是两大框架之间的差异。PyTorch 在循环内部把裁剪交给您处理;TensorFlow 则将其下放到优化器自身。底层逻辑一致。
梯度裁剪 vs 其他稳定化技术
梯度裁剪并非稳定训练的唯一方法,也不总是合适的工具。
其他技术处理的是相关但不同的问题。有的从源头防止梯度变得过大,有的防止梯度消失,还有的让损失曲面更易优化。下面介绍几种不同技术。
批量归一化(Batch normalization)
批量归一化在训练期间对每个小批次内的激活进行归一化。
它让各层输出保持在稳定范围内,使梯度幅度更可预测。使用批量归一化训练的网络能承受更高的学习率并更快收敛,对权重初始化也更不敏感。
但批量归一化并不能直接阻止梯度爆炸。它降低爆炸发生的频率,却不能在爆炸发生时替您处理。因此许多模型仍会将批量归一化与梯度裁剪配合使用。
残差连接(Residual connections)
残差连接添加跨越一个或多个层的捷径路径,使梯度可以直接从后层流向前层。
这解决了深层网络中的梯度消失问题。没有残差连接时,训练超过 20–30 层的网络会很困难,因为梯度在反向传播时趋近于零;有了残差连接,上百层的网络也能顺利训练。
残差连接针对的是梯度问题的另一端:裁剪处理过大的梯度,残差处理过小的梯度。
谨慎的权重初始化
权重的初始值决定了激活与梯度的起始幅度。糟糕的初始化会从第一步就导致梯度爆炸或消失。
Xavier 和 He 初始化等方法会根据层的规模缩放初始权重。这能在训练开始时保持各层激活的方差稳定,从而在问题发生前预防许多梯度问题。
良好的初始化能降低您需要裁剪的概率,但不能完全消除。尤其在学习率较高或批次分布异常的情况下,训练后期仍可能出现梯度尖峰。
它们如何配合
这些技术并非相互替代,而是互补工具,从不同侧面解决同一类问题。
现代训练设置通常在起始阶段采用谨慎的初始化,在架构中加入残差连接,在网络内部使用批量归一化(或层归一化),并在优化阶段以梯度裁剪作为安全网。每项技术针对一种特定的失效模式,合在一起让深层网络变得可训练。
结语
梯度裁剪是深度学习中最简单的修复手段之一,它能解决那种几小时训练成果在一瞬间化为乌有的问题。
好消息是,您无需更改模型架构或重写训练代码。在 PyTorch 中加一行,或在 TensorFlow 中加一个参数,就能实现梯度裁剪。
它与更大的整体方案配合效果最佳。将其与谨慎的权重初始化、残差连接以及批量/层归一化组合使用,您将拥有一个能从多个角度应对不稳定性的训练流程。
如果您的损失在爆炸,从裁剪开始;如果在消失,就另寻他法。而且只要训练的模型不止是个小模型,就默认把裁剪加进流程,然后把它忘掉吧。
梯度裁剪只是每位机器学习工程师必须掌握的诸多术语之一。若您想了解更多并在 2026 年胜任岗位,请立即报名我们的 机器学习工程师学习路径。
梯度裁剪常见问题
什么是深度学习中的梯度裁剪?
梯度裁剪是在神经网络训练过程中限制梯度大小的一种技术,用于防止不稳定的参数更新。当梯度在反向传播过程中变得过大时,优化器会迈出巨大的步长,将权重推入不良区域并导致损失爆炸。裁剪在优化器步骤之前封顶梯度幅度,即使原始梯度出现尖峰,更新也能保持有界。
我何时应该使用梯度裁剪?
当训练不稳定时使用梯度裁剪,尤其当您看到损失突然尖峰、出现 NaN 值,或训练数小时后发散时。对于 LSTM、GRU 等循环网络,以及使用高学习率训练的任何深层架构,梯度裁剪是标准做法。若损失曲线健康,您可以不使用,但把它作为安全网也无妨。
按数值裁剪与按范数裁剪有什么区别?
按数值裁剪会对每个梯度元素单独封顶,可能改变梯度向量的方向。按范数裁剪在整体幅度超过阈值时缩放整个向量,既保留原始方向又缩短步长。按范数裁剪是现代深度学习中的标准选择,因为它不会扭曲更新方向。
如何选择合适的梯度裁剪阈值?
从 1.0 开始,并根据训练中的观察进行调整。记录各批次未裁剪的梯度范数并查看其分布。将阈值设在典型范数范围之上、但远低于您想捕捉的尖峰之下。阈值过高则几乎不触发裁剪;过低则会不必要地减慢学习。
梯度裁剪也能解决梯度消失吗?
不会。梯度裁剪只解决梯度爆炸(数值过大)。梯度消失恰恰相反——数值趋近于零,学习几乎停止。要解决梯度消失,应采用更好的权重初始化、残差连接、批量归一化,或 LSTM、GRU 等架构。