训练神经网络时,Loss曲线出现锯齿状震荡并不少见。它可能表现为单个epoch内loss忽高忽低,也可能是相邻epoch之间平均loss反复反弹。出现这种情况时,很多人的第一反应是怀疑模型结构或数据有问题,但实际上优化器参数设置往往才是主因。学习率过大、批次大小过小、数据未充分打乱、梯度爆炸等因素,都会让参数更新方向不稳定,最终反映到Loss曲线上。

一、Loss震荡的常见原因与诊断思路
要解决Loss震荡,首先需要判断震荡发生在哪个层级。如果每个iteration的loss都在剧烈变化,但整体趋势仍然向下,这通常是小批次训练带来的正常噪声。因为每次参数更新只基于一小部分样本,梯度方向存在随机性,loss自然会有波动。如果判断标准只盯着单步loss,很容易误判为异常。
但如果epoch平均loss在多个epoch之间不降反升,或者曲线出现周期性尖峰,就需要重点关注学习率。以随机梯度下降为例,参数更新公式为 θ = θ - lr * ∇L(θ),当学习率过大时,参数可能直接跨过最优区域,甚至远离损失函数的谷底。下一次迭代梯度方向反转,参数又被拉回,这种来回拉扯就会形成震荡。
诊断时可以做一个简单实验:将学习率缩小为原来的十分之一,保持其他设置不变,训练几个epoch。如果Loss震荡明显减弱,说明原先的学习率确实偏高。如果震荡依旧,可以再检查数据加载器的shuffle是否开启、是否存在异常标签,或者尝试增大批次大小观察曲线平滑度。
二、学习率调整策略:从固定到自适应
固定学习率很难同时满足训练初期快速收敛和后期精细调参的需求。训练初期,参数距离最优点较远,可以用较大的学习率快速下降;接近收敛时,需要更小的步长避免在最优解附近来回震荡。因此引入学习率衰减机制是稳定训练的有效手段。
PyTorch提供了多种调度器。CosineAnnealingLR会让学习率按照余弦曲线从初始值平滑衰减到最小值,训练过程中的学习率变化较为温和,不容易出现突变。ReduceLROnPlateau则是在验证指标停滞时自动降低学习率,适合在训练过程中动态响应Loss平台期。下面是一个结合余弦退火和验证loss监控调度的示例:
import torch
import torch.nn as nn
from torch.optim.lr_scheduler import CosineAnnealingLR
model = nn.Linear(10, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.05, momentum=0.9)
scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-5)
for epoch in range(50):
# 训练循环省略,假设已经完成反向传播
optimizer.step()
scheduler.step()
print(f"Epoch {epoch+1}, lr={scheduler.get_last_lr()[0]:.6f}")
如果使用自适应优化器如Adam,学习率引起的震荡通常会少一些,因为Adam对每个参数都维护了一阶矩和二阶矩估计,会自适应地缩放更新步长。但Adam默认学习率0.001在部分任务上仍可能偏大。遇到Loss震荡时,可以先尝试将Adam学习率降到1e-4或5e-5,观察曲线是否稳定。自适应优化器并非完全不需要调学习率,只是敏感度低于SGD。
另一个常用的技巧是warmup。在训练前若干个iteration中,让学习率从很小的值线性增加到设定值,可以避免模型在一开始就因梯度较大或参数初始化不理想而产生剧烈震荡。warmup通常与余弦退火配合使用,形成warmup加decay的完整调度曲线。
三、批次大小对训练稳定性的影响及调参建议
批次大小决定了每一步梯度估计的样本数量。batch size较小时,梯度由少量样本计算得到,噪声较大,反映到Loss曲线上就是单步震荡更明显。但小批次也有好处:它引入了随机性,有时能帮助跳出局部极小值,并且在显存受限的场景下更加灵活。
增大batch size可以让梯度估计更接近全量数据的真实梯度,每一步下降方向更稳定,Loss曲线会变得更平滑。但批次也不能无脑加大。一方面,大batch可能让模型收敛到更尖锐的极小值,泛化性能可能下降;另一方面,大batch下学习率需要同步调整,否则容易出现优化轨迹过于保守、收敛变慢或者梯度方向一致导致更新步长过大的问题。
业界有一个经验法则:当batch size扩大k倍时,学习率可以同比例扩大k倍,称为线性缩放规则。例如原batch size为32,学习率为0.01,如果将batch size增大到256,学习率可以尝试调整为0.08。但这一规则主要适用于SGD,对于Adam等自适应优化器不一定完全成立。实际调参时应先从较小的batch size和较小的学习率组合开始,找到稳定区间后再逐步放大。
如果显存有限无法增大batch size,可以通过梯度累积来模拟大batch训练。做法是每计算一个小batch的梯度后不清空,而是累加,达到指定步数后再执行一次参数更新。这样梯度估计的噪声会降低,同时不增加显存占用。示例代码如下:
accumulation_steps = 4
optimizer.zero_grad()
for i, (inputs, labels) in enumerate(train_loader):
outputs = model(inputs)
loss = criterion(outputs, labels)
loss = loss / accumulation_steps
loss.backward()
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
需要特别注意的是,梯度累积只适用于不依赖批次统计信息的层,例如BatchNorm在梯度累积场景下统计的均值和方差来自单个小batch,可能影响稳定性。如果模型大量使用BatchNorm,建议优先增大真实batch size或改用GroupNorm等替代结构。
四、综合实践:稳定训练的一组可复现参数
在图像分类任务中,假设使用ResNet系列模型,输入尺寸为224×224,训练数据量在十万级别。一个较为稳妥的起点是:SGD优化器,初始学习率0.01,momentum 0.9,weight decay 1e-4,batch size 64,并开启shuffle。训练总轮数90,学习率在第30和第60个epoch分别衰减为原来的十分之一。
这套参数的优点是行为可预期,学习率阶段性下降能显著减少中后期Loss震荡。如果训练初期loss曲线仍有明显尖峰,可以检查数据增强是否过于激进。例如随机裁剪、颜色抖动过强会引入更大样本差异,虽然利于泛化,但也可能让单步loss看起来很不稳定。此时可以适当降低增强强度,或者让调整只作用于输入而不过度扭曲标签含义。
对于使用Transformer结构的任务,训练稳定性对学习率更加敏感。通常推荐使用AdamW配合warmup。例如初始学习率1e-4,warmup步数4000,之后使用线性衰减或余弦退火。如果出现训练初始阶段的loss爆炸,可以在warmup基础上加入梯度裁剪,将梯度范数限制在1.0以内。这样即使某些batch出现极端样本,参数更新幅度也不会失控。
最后总结一下解决Loss震荡的排查顺序:第一步观察单步loss和epoch平均loss,区分正常噪声与异常震荡;第二步检查数据管道,确认shuffle和标签质量;第三步降低学习率做短实验,判断敏感度;第四步调整batch size或使用梯度累积;第五步引入学习率调度器、warmup和梯度裁剪。按照这个顺序逐步排查,大多数训练Loss震荡问题都能得到有效缓解。