1. 收敛基础:什么是收敛?为什么优化会发散?

做算法优化这些年,我见过太多团队花了几周调参,结果损失函数越跑越高。说白了,他们连收敛是什么都没搞清楚,就开始盲目调优。

这一节,咱们先把地基打牢。

1.1 收敛的直观理解

想象你在下山。目标是走到山谷最低点。

  • 收敛:每一步都在往下走,最终到达谷底。
  • 发散:走着走着反而往山上跑了,甚至滚回山顶。

优化算法里的收敛,就是这个意思。我们不断调整参数,让目标函数(比如损失函数)的值越来越小,最终稳定在一个最小值附近。

核心定义:收敛是指迭代过程中,序列 {xₙ} 趋近于某个极限值 x*。用数学话说:对于任意 ε > 0,存在 N,当 n > N 时,|xₙ - x*| < ε。

我在项目中遇到过一位同事,他盯着训练曲线看了三天,问我:“这曲线上下跳,算收敛吗?”我告诉他,收敛不是绝对不动,而是在一个小区间内稳定波动。就像湖面,不可能绝对平静,但波纹不能变成海啸。

1.2 为什么优化会发散?

发散的原因很多。我总结了最常见的几种,你对照看看踩过几个坑。

原因 表现 典型场景
学习率过大 损失值震荡上升 SGD 步长超过山谷宽度
梯度爆炸 参数更新量级失控 深层网络、RNN 长期依赖
鞍点/局部极值 梯度接近零但未到全局最优 高维非凸优化
数据/标签噪声 梯度方向随机摆动 小批量采样不均匀
初始化不当 一开始就偏离最优区域 权重全零或过大

我曾经接手过一个推荐系统项目,离线 AUC 一直上不去。排查了两天,发现是学习率设成了 0.1,而特征尺度差了三个数量级。说白了,步子迈太大,直接扯到蛋了。

避坑指南:我曾经在训练一个 50 层的 ResNet 时,忘记加梯度裁剪。结果 loss 从 2.3 直接跳到 1e8,然后 NaN。那次之后,我养成了两个习惯:一是监控梯度范数,二是设置梯度裁剪阈值。

1.3 收敛的数学定义

数学上,收敛分几种。我挑最常用的两个说。

  • 线性收敛:误差以固定比例衰减。比如每次迭代误差减半。|eₖ₊₁| ≤ c|eₖ|,c ∈ (0,1)。
  • 二次收敛:误差平方级衰减。|eₖ₊₁| ≤ c|eₖ|²。牛顿法就是典型,快到飞起,但代价也大。

你想想看,线性收敛就像走路,每步走固定距离。二次收敛就像坐火箭,离目标越近速度越快。但火箭燃料贵啊——二次收敛每次迭代要算 Hessian 矩阵,大模型根本扛不住。

实用建议:实际工程中,我们很少追求严格的二次收敛。Adam、RMSProp 这类自适应方法,介于线性和二次之间,性价比最高。我个人习惯先用 Adam 跑个大概,再切 SGD 精调。

1.4 收敛判据:怎么才算收敛了?

这个问题,面试里经常有人答错。不是 loss 不变了就叫收敛。

我常用的判据有三个:

  1. 目标函数变化量:连续 N 轮 loss 下降小于阈值(比如 1e-6)。
  2. 梯度范数:梯度向量的模长接近零(比如 < 1e-5)。
  3. 参数变化量:参数更新幅度小于阈值。

注意,这三个要组合使用。我见过 loss 不动但梯度还很大的情况——那是卡在鞍点了,不是真收敛。

1.5 知识结构图

下面这张图,把收敛的核心概念串起来了。建议你保存下来,后面每章都可以回来对照。

收敛知识体系 收 敛 数学定义 线性收敛:|eₖ₊₁| ≤ c|eₖ| 二次收敛:|eₖ₊₁| ≤ c|eₖ|² 收敛判据 目标函数变化量 < ε 梯度范数 < ε 参数变化量 < ε 发散原因 学习率过大 梯度爆炸 鞍点 / 局部极值 数据 / 标签噪声 初始化不当 收敛 ≠ 不动,而是在目标附近稳定波动 发散 ≠ 失败,可能是超参没调对

1.6 一个简单的收敛实验

光说不练假把式。咱们用代码感受一下收敛和发散的区别。

import numpy as np

def gradient_descent(f, df, x0, lr=0.01, epochs=100):
    x = x0
    history = [x]
    for _ in range(epochs):
        grad = df(x)
        x = x - lr * grad
        history.append(x)
    return x, history

# 目标函数:f(x) = x²,最小值在 x=0
def f(x): return x**2
def df(x): return 2*x

# 收敛:学习率 0.1
x_conv, hist_conv = gradient_descent(f, df, 5.0, lr=0.1)
print(f"收敛结果: x={x_conv:.4f}")  # 接近 0

# 发散:学习率 1.2
x_div, hist_div = gradient_descent(f, df, 5.0, lr=1.2)
print(f"发散结果: x={x_div:.4f}")  # 震荡甚至爆炸

跑一下你就明白了。学习率 0.1 时,x 从 5 慢慢走到接近 0。学习率 1.2 时,x 在 5 和 -7 之间来回跳,永远到不了谷底。

小技巧:调试时,我习惯把学习率从 1e-4 开始,每次翻 10 倍试。哪个范围 loss 稳定下降,就选那个量级。这叫「学习率扫描」,比瞎猜靠谱多了。

嗯,收敛的基础就这些。记住一句话:收敛是过程,不是终点。后面我们会深入各种加速技巧,但地基不稳,楼盖得再高也得塌。