1. 收敛性基础:什么是优化不收敛?

大家好,我是你们这门课的主讲。咱们直接开门见山。

优化不收敛,说白了就是——模型训练了半天,损失函数就是不听话。要么上下乱跳,要么直接飞上天,要么干脆卡死不动。我刚开始做深度学习那会儿,经常盯着 loss 曲线发呆,心想这玩意儿到底在干嘛?

这一章,咱们就把「不收敛」的几种典型长相认清楚。你只有知道它长什么样,后面才能对症下药。

1.1 损失函数震荡:模型在「左右横跳」

震荡是最常见的不收敛表现。损失曲线像心电图一样,上下起伏,就是不往下走。

典型特征: loss 曲线在某个区间反复波动,无法稳定下降。

为什么会这样?我个人习惯把原因归为三类:

  • 学习率太大:步子迈得太大,跨过了最优点,在谷底两边来回弹。
  • batch size 太小:每次看到的样本太少,梯度方向不稳定,像喝醉了酒走路。
  • 数据噪声过大:标签有错误,或者特征分布太乱,模型不知道该往哪走。

我记得有一次做图像分类项目,loss 曲线一直在 0.8 到 1.2 之间震荡,死活降不下去。后来发现是学习率设成了 0.1,改成 0.01 之后,曲线立马就老实了。

避坑指南: 我曾经遇到一个 case,震荡是因为优化器动量设置太大。SGD 的 momentum 超过 0.95 时,容易在陡峭区域「刹不住车」。建议先从 0.9 开始调。

1.2 损失函数发散:模型「起飞了」

发散比震荡严重得多。loss 曲线不是上下跳,而是直接往天上冲——从 1.0 到 10.0,再到 100.0,甚至变成 NaN。

危险信号: 如果 loss 在几个 epoch 内增长了 10 倍以上,或者直接出现 NaN,立即停止训练!继续跑只会浪费算力。

发散的原因通常比较暴力:

  1. 梯度爆炸:深层网络中,梯度连乘导致数值爆炸。常见于 RNN 或很深的 CNN。
  2. 学习率严重过大:比震荡场景更极端,一步跨到「悬崖」外面去了。
  3. 权重初始化不当:初始值太大或太小,导致激活值饱和,梯度消失或爆炸。

你想想看,梯度爆炸的时候,参数更新量可能比参数本身还大。模型直接「原地升天」。我做过一个 NLP 项目,用 LSTM 做序列预测,loss 在第 5 个 epoch 突然从 2.3 跳到 89.7。查了半天,原来是 embedding 层的初始化标准差设成了 1.0,改成 0.01 就正常了。

现象 loss 变化 常见原因 紧急处理
震荡 在区间内上下波动 学习率偏大 / batch 偏小 降低学习率 / 增大 batch
发散 持续增大或变 NaN 梯度爆炸 / 初始化不当 梯度裁剪 / 减小学习率
停滞 几乎不变,loss 偏高 梯度消失 / 陷入局部极小 调整激活函数 / 使用 Adam

1.3 损失函数停滞:模型「躺平了」

停滞是最隐蔽的不收敛。loss 曲线很平滑,几乎是一条水平线,但数值远高于预期。模型看起来在训练,实际上根本没学到东西。

我遇到过最坑的一次:训练了一个 50 层的 ResNet,loss 从 2.3 降到 2.2 之后,跑了 20 个 epoch 纹丝不动。乍一看以为收敛了,但验证集准确率只有 12%。说白了,模型在「假装学习」。

停滞的常见原因:

  • 梯度消失:深层网络的反向传播中,梯度趋近于 0,参数几乎不更新。
  • 陷入局部极小值:模型掉进了一个「浅坑」,周围梯度很小,跳不出来。
  • 学习率过小:步子太小,在平坦区域走不动。
  • 特征表达能力不足:模型容量不够,或者特征提取层设计不合理。
诊断技巧: 查看每一层的梯度范数。如果靠近输入层的梯度接近 0,那就是梯度消失。我习惯在每个 epoch 打印前 3 层和后 3 层的梯度均值,一眼就能看出问题。

1.4 三种不收敛的「长相」对比

为了让你更直观地理解,我画了一张对比图。三种曲线放在一起,你一眼就能分辨。

三种不收敛模式的 loss 曲线对比 震荡 发散 停滞 正常收敛 训练轮次 (Epoch) 损失值 (Loss) 关键判断点 • 震荡:波动幅度 > 0.1 • 发散:loss 持续增长 • 停滞:loss 几乎不变 • 正常:平滑下降并稳定

1.5 如何快速判断不收敛类型?

在实际项目中,我一般用三步法快速定位:

  1. 看趋势:loss 是上升、下降还是水平?上升就是发散,水平就是停滞,波动就是震荡。
  2. 看数值:loss 是否远高于理论下界?比如分类任务,随机猜测的 loss 是 log(C),如果训练 loss 比这个还高,那肯定有问题。
  3. 看梯度:打印每一层的梯度范数。如果所有层梯度都很小,是停滞;如果某些层梯度极大,是发散。
我的经验法则: 前 10 个 epoch 是「黄金诊断期」。如果 10 个 epoch 后 loss 没有明显下降趋势,基本可以断定不收敛。别傻等,赶紧调参。

1.6 一个简单的诊断代码模板

下面是我常用的诊断代码片段。每次训练新模型,我都会加上这段,方便快速定位问题。

import torch
import matplotlib.pyplot as plt

def diagnose_training(model, loss_history, grad_history=None):
    """
    快速诊断训练是否收敛
    """
    # 1. 检查 loss 趋势
    if len(loss_history) > 10:
        recent_loss = loss_history[-10:]
        avg_loss = sum(recent_loss) / len(recent_loss)
        
        # 发散检测:最后 10 个 loss 持续上升
        if all(recent_loss[i] < recent_loss[i+1] for i in range(len(recent_loss)-1)):
            print("[警告] loss 持续上升,可能发散!")
        
        # 停滞检测:最后 10 个 loss 变化小于 1%
        loss_change = abs(recent_loss[-1] - recent_loss[0]) / recent_loss[0]
        if loss_change < 0.01:
            print("[警告] loss 变化小于 1%,可能停滞!")
        
        # 震荡检测:最后 10 个 loss 标准差大于均值的 5%
        import numpy as np
        std_loss = np.std(recent_loss)
        if std_loss / avg_loss > 0.05:
            print("[警告] loss 波动较大,可能震荡!")
    
    # 2. 检查梯度(可选)
    if grad_history is not None and len(grad_history) > 0:
        last_grad = grad_history[-1]
        if any(g > 10.0 for g in last_grad):
            print("[警告] 存在梯度爆炸!")
        if all(g < 1e-6 for g in last_grad):
            print("[警告] 存在梯度消失!")
    
    print(f"当前 loss: {loss_history[-1]:.4f}")
    print("诊断完成。")
使用建议: 把这个函数放在训练循环的每个 epoch 末尾调用。我习惯每 5 个 epoch 打印一次诊断结果,既不会刷屏,又能及时发现问题。

1.7 本章小结

咱们这一章把不收敛的三种「长相」认清楚了:

  • 震荡:loss 上下波动,降不下去。常见原因是学习率偏大或 batch 偏小。
  • 发散:loss 持续上升或变 NaN。常见原因是梯度爆炸或初始化不当。
  • 停滞:loss 几乎不变,但数值偏高。常见原因是梯度消失或陷入局部极小。