1. 收敛性基础:什么是优化不收敛?
大家好,我是你们这门课的主讲。咱们直接开门见山。
优化不收敛,说白了就是——模型训练了半天,损失函数就是不听话。要么上下乱跳,要么直接飞上天,要么干脆卡死不动。我刚开始做深度学习那会儿,经常盯着 loss 曲线发呆,心想这玩意儿到底在干嘛?
这一章,咱们就把「不收敛」的几种典型长相认清楚。你只有知道它长什么样,后面才能对症下药。
1.1 损失函数震荡:模型在「左右横跳」
震荡是最常见的不收敛表现。损失曲线像心电图一样,上下起伏,就是不往下走。
为什么会这样?我个人习惯把原因归为三类:
- 学习率太大:步子迈得太大,跨过了最优点,在谷底两边来回弹。
- batch size 太小:每次看到的样本太少,梯度方向不稳定,像喝醉了酒走路。
- 数据噪声过大:标签有错误,或者特征分布太乱,模型不知道该往哪走。
我记得有一次做图像分类项目,loss 曲线一直在 0.8 到 1.2 之间震荡,死活降不下去。后来发现是学习率设成了 0.1,改成 0.01 之后,曲线立马就老实了。
1.2 损失函数发散:模型「起飞了」
发散比震荡严重得多。loss 曲线不是上下跳,而是直接往天上冲——从 1.0 到 10.0,再到 100.0,甚至变成 NaN。
发散的原因通常比较暴力:
- 梯度爆炸:深层网络中,梯度连乘导致数值爆炸。常见于 RNN 或很深的 CNN。
- 学习率严重过大:比震荡场景更极端,一步跨到「悬崖」外面去了。
- 权重初始化不当:初始值太大或太小,导致激活值饱和,梯度消失或爆炸。
你想想看,梯度爆炸的时候,参数更新量可能比参数本身还大。模型直接「原地升天」。我做过一个 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,参数几乎不更新。
- 陷入局部极小值:模型掉进了一个「浅坑」,周围梯度很小,跳不出来。
- 学习率过小:步子太小,在平坦区域走不动。
- 特征表达能力不足:模型容量不够,或者特征提取层设计不合理。
1.4 三种不收敛的「长相」对比
为了让你更直观地理解,我画了一张对比图。三种曲线放在一起,你一眼就能分辨。
1.5 如何快速判断不收敛类型?
在实际项目中,我一般用三步法快速定位:
- 看趋势:loss 是上升、下降还是水平?上升就是发散,水平就是停滞,波动就是震荡。
- 看数值:loss 是否远高于理论下界?比如分类任务,随机猜测的 loss 是 log(C),如果训练 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("诊断完成。")
1.7 本章小结
咱们这一章把不收敛的三种「长相」认清楚了:
- 震荡:loss 上下波动,降不下去。常见原因是学习率偏大或 batch 偏小。
- 发散:loss 持续上升或变 NaN。常见原因是梯度爆炸或初始化不当。
- 停滞:loss 几乎不变,但数值偏高。常见原因是梯度消失或陷入局部极小。