4. 训练损失震荡排查:学习率与批次大小的联合诊断

震荡现象与联合诊断思路

农业图像分类模型(例如番茄叶片病害四分类)训练时,若损失曲线出现“锯齿状大幅上下跳动、整体不下降或下降极慢”,通常不是单一超参数问题,而是学习率与批次大小不匹配共同作用的结果。诊断的核心是:把“震荡”拆成可观测的指标,再用对照实验逐项排除。

先明确两个关键关系:

  • 学习率 η 过大:单步更新跨过损失谷底,损失在最优值两侧来回跳,表现为高频大幅震荡。
  • 批次大小 B 过小:梯度估计噪声大,即使 η 合适,损失也会抖动;B 越小,噪声越大,可容忍的 η 上限越低。
  • 二者耦合:线性缩放经验为 η ∝ B(大批次可配大学习率),但超过临界批次后收益递减,需配合 warmup 或调整优化器 β。

因此诊断顺序应为:先固定 B 扫 η,再固定 η 扫 B,最后检查优化器参数(β₁、β₂、weight decay)是否放大了震荡。

学习率 × 批次大小 对照实验矩阵(损失曲线形态判读) 纵轴:批次大小 B 横轴:学习率 η → η=1e-4 η=1e-3 η=5e-3 η=1e-2 B=8 B=32 B=128 缓慢下降 可接受 小抖动 噪声主导 剧烈震荡 η过大 发散/NaN 不可用 平稳收敛 推荐基线 平稳收敛 最佳区间 轻度震荡 需降η 剧烈震荡 η过大 下降偏慢 η偏小 平稳收敛 可接受 平稳收敛 线性缩放 轻度震荡 需warmup 判读规则 绿色:损失单调下降,验证集同步改善 → 稳定配置 黄色:损失有抖动但趋势向下 → 降 η 或增大 B 后复测 红色:损失锯齿/发散 → η 超临界,优先降 η 再调 B 蓝色:收敛过慢 → η 偏小,可适度上调 紫色:NaN/发散 → 立即停止,检查 η 与数据归一化 临界批次:B 增大到某点后 η 再大也不稳,需 warmup

对照实验设计与执行

以番茄叶片病害四分类(健康、早疫病、晚疫病、叶霉病)为例,训练集约 4800 张,输入 224×224,骨干为 ResNet-18,优化器先用 AdamW。基线配置:B=32、η=1e-3、weight decay=1e-4、余弦退火 30 epoch。若基线已出现震荡,按下面矩阵做 12 组短程实验(每组 8 epoch,记录每 epoch 训练损失与验证损失)。

关键实现要点:

  • 每组实验只改一个变量,其余固定,避免混淆。
  • 记录损失标准差与相邻 epoch 损失差绝对值均值,作为“震荡强度”量化指标,比肉眼看曲线更可靠。
  • 同时记录验证集准确率,防止把“训练损失平稳但过拟合”误判为稳定。
  • 若出现 NaN,立即终止该组,标记为不可用配置。
import torch, torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models

def build_loader(batch_size, root="data/tomato"):
    tf = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
    ])
    ds = datasets.ImageFolder(root, transform=tf)
    return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=4)

def run_one(batch_size, lr, epochs=8, wd=1e-4):
    loader = build_loader(batch_size)
    model = models.resnet18(weights=None, num_classes=4).cuda()
    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=wd)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
    crit = nn.CrossEntropyLoss()
    losses = []
    for ep in range(epochs):
        model.train()
        ep_loss = 0.0
        for x, y in loader:
            x, y = x.cuda(), y.cuda()
            opt.zero_grad()
            loss = crit(model(x), y)
            loss.backward()
            opt.step()
            ep_loss += loss.item() * x.size(0)
        ep_loss /= len(loader.dataset)
        losses.append(ep_loss)
        sched.step()
        if not torch.isfinite(torch.tensor(ep_loss)):
            return losses, float("nan")
    return losses, float(torch.tensor(losses).std())

# 示例:固定 B=32,扫学习率
for lr in [1e-4, 1e-3, 5e-3, 1e-2]:
    losses, std = run_one(batch_size=32, lr=lr)
    print(f"B=32 lr={lr:.0e} std={std:.4f} losses={[round(v,3) for v in losses]}")

示例输出(教学示例,非实测):B=32、η=1e-3 时损失标准差约 0.02,曲线单调下降;η=5e-3 时标准差升到 0.09,出现明显锯齿;η=1e-2 时第 3 epoch 出现 NaN。据此可判断:在 B=32 下,η 的稳定上限在 1e-3 到 5e-3 之间。

根因定位与稳定配置确定

把 12 组结果按“震荡强度”和“验证准确率”两个维度排序,通常会出现三类典型模式:

现象最可能根因处置
小 B 下任何 η 都抖,大 B 下同 η 平稳批次过小导致梯度噪声主导增大 B 至 32/64,或启用梯度累积
同 B 下 η 稍大即锯齿,η 减半即平稳学习率超过临界值降 η,或加 warmup 前 3 epoch
大 B 下 η 按线性缩放放大后仍抖超过临界批次,AdamW 二阶矩估计不稳加 warmup、调 β₂=0.99、或改用 SGD+momentum
损失平稳但验证准确率不升不是震荡问题,是过拟合或数据问题转查正则化与数据划分

最终稳定配置的确定原则:在验证准确率不下降的前提下,选震荡强度最低且收敛速度可接受的组合。本例教学示例中,B=32、η=1e-3、warmup 3 epoch、余弦退火 30 epoch 为推荐配置;若显存允许,B=64、η=2e-3 加 warmup 也可作为备选,需复测验证集准确率确认无退化。

复测时保持随机种子一致,并至少跑 2 个种子取平均,避免把随机波动误判为配置差异。
不要仅凭训练损失判断稳定:训练损失平稳但验证损失上升,说明是过拟合而非震荡已解决,应转入正则化与数据增强排查。