3.3 无约束优化的最优性条件与梯度法
3.2 节建立了凸性的判定工具,本节开始使用它们。无约束问题是最简单的一类:可行域是整个
本节回答四个问题。怎么判断一个点是极小点(最优性条件);沿着什么方向走(下降方向与线搜索);走多快、多少次能到(收敛速率与条件数);梯度本身怎么算(有限差分与自动微分)。四个问题是所有一阶方法的共同框架:3.4 节的各种加速与随机变体改变的是方向或步长的取法,分析工具与本节相同;3.5 节的二阶方法把方向换成牛顿方向,条件数的影响被进一步削弱;3.7 节的近端方法处理目标函数不可微的情形,最优性条件换成次微分形式。
3.3.1 最优性条件
一阶与二阶必要条件
考虑无约束问题
第一条是一阶必要条件:若梯度不为零,沿
两个条件都是必要的,单独都充分。二阶条件加强为严格正定时得到充分条件。
定理(二阶充分条件):若
证明用泰勒展开:在
凸情形
因此
这一结论是凸优化的核心:不需要二阶信息,不需要检查曲率,找到一个驻点就完成了全部工作。算法设计因此可以聚焦于如何高效地把梯度推向零,而不必担心陷入错误的驻点。
非凸情形没有这一保证。驻点可能是极小点、极大点或鞍点,二阶条件只能区分局部形状,不能给出全局结论。鞍点的识别与逃逸是 3.8 节的主题,那里的判据用到 Hessian 的负特征值(负曲率方向)。
非光滑情形
目标函数不可微时,梯度不存在,条件改用次微分表述。
是
3.3.2 线搜索
下降方向
迭代算法的通用格式是
确定步长有两条路。精确线搜索求解一维问题
Armijo 条件与回溯
最基本的非精确条件是 Armijo 条件(也称充分下降条件):
右端是一条斜率被压小的直线:函数值至少要下降到这条参考线以下。常数常取
回溯线搜索(backtracking)从初始步长
只要
Wolfe 条件
只用 Armijo 条件可能导致步长过小:在某些方向上,很小的步长也满足充分下降,而这样的步长会拖慢后续进展。Wolfe 条件在 Armijo 之外补充一条曲率条件:
含义是:新点处的方向导数相比原点处已经回升(不再那么负),说明步长已经推进到曲率开始起作用的位置,可以停止。常取
Goldstein 条件是一组更早的替代方案,用函数值的上下界同时控制步长;由于存在满足 Goldstein 条件的步长区间可能不与任何极小点相邻,现代实现更常用 Wolfe 条件。
精确线搜索的实现依赖一维插值:用两个试探点的函数值与方向导数拟合二次或三次多项式,取多项式极小点作为下一个试探点。对二次函数,三次插值可以一步定位精确步长;对一般函数,几次插值即可达到足够的精度。
3.3.3 下降引理与收敛速率
下降引理
收敛性分析从一条基本不等式开始。
这条下降引理(descent lemma)给出函数的二次上界。取
每步的函数值下降量与梯度范数的平方成正比。沿极小点方向前进的量在此式中没有出现,这正是分析收敛速率时需要补上的部分:把梯度下降与最优性间隙
三档速率
按函数的凸性强度,梯度法(固定步长
强凸且光滑(
每步把最优性间隙乘以
其中
凸且光滑:只有凸性时不能写出上述因子,收敛速率为
即
非凸且光滑:没有全局最优的保证,可保证的目标是梯度范数趋于零:
这是
三档速率放在一起,可以看到条件数在其中的地位:强凸情形的最优速率由
步长与条件数的数值验证
二次函数上的梯度下降可以精确计算,是检验上述速率的理想对象。对
import numpy as np
# 强凸二次函数:对角矩阵,条件数 25
Q = np.diag([1.0, 25.0])
L, m = 25.0, 1.0
kappa = L / m
def run(eta, k=400):
x = np.array([1.0, 1.0])
hist = []
for _ in range(k):
hist.append(float(x @ x)) # 记录到原点的平方距离
x = x - eta * (Q @ x)
return np.array(hist)
h1, h2 = run(1 / L), run(2 / (L + m))
print(f"kappa = {kappa:.0f}")
print(f"步长 1/L:平方距离收缩因子 = {h1[21] / h1[20]:.6f}(理论 (1 - m/L)^2 = {(1 - m / L) ** 2:.6f})")
print(f"最优步长 2/(L+m):收缩因子 = {h2[21] / h2[20]:.6f}(理论 ((kappa-1)/(kappa+1))^2 = {((kappa - 1) / (kappa + 1)) ** 2:.6f})")
for name, h in [("1/L", h1), ("2/(L+m)", h2)]:
print(f" 步长 {name}:平方距离降到 1e-6 的步数 = {int(np.argmax(h <= 1e-6))}")
# 条件数 1e4 的问题:慢方向主导迭代次数
Q2 = np.diag([1e-4, 1.0])
x = np.array([1.0, 1.0])
hist = []
for _ in range(100000):
hist.append(float(x @ x))
x = x - 1.0 * (Q2 @ x)
hist = np.array(hist)
print(f"\nkappa = 1e4:平方距离收缩因子 = {hist[100] / hist[99]:.8f},"
f"降到 1e-6 的步数 = {int(np.argmax(hist <= 1e-6))}")
# 变量缩放 x1 = 100 z1 后,问题变为 0.5 (z1^2 + z2^2),条件数为 1
z = np.array([1.0, 1.0])
for k in range(1, 101):
z = z - 1.0 * z # 缩放后一步把两个方向同时归零
if z @ z <= 1e-6:
break
print(f"缩放后:平方距离降到 1e-6 的步数 = {k},z = {z}")
# 步长 1/L 的收缩因子为 0.9216,最优步长把两个方向同步按 0.8521 收缩
# 条件数 1e4 时需要约 7 万步,变量缩放把条件数降到 1 后一步收敛输出给出四个数字。步长
3.3.4 梯度的计算
有限差分
目标函数的解析梯度并非总能写出,退一步可以用差分近似。前向差分与中心差分为
误差由两部分组成:截断误差(前向
有限差分适合作为开发期的检查手段(梯度检查),不适合作为训练时的梯度来源:代价随维数线性增长,精度也受限。
自动微分
自动微分(automatic differentiation)在计算过程中精确地传播导数,不引入截断误差。两种模式。
前向模式沿计算顺序同时计算函数值与方向导数。实现方式是为每个数与一个导数分量组成对偶数(dual number)
反向模式先做一次前向传播记录计算图,再从输出反向传播伴随量(adjoint),一次反向传播给出全部
两种模式的选择由输入输出维数决定:求
下列代码用对偶数实现前向模式自动微分,与解析梯度对比,并给出中心差分的误差随步长的变化。
import numpy as np
class Dual:
"""前向模式自动微分的对偶数:值 + 导数"""
def __init__(self, v, dv=0.0):
self.v, self.dv = float(v), float(dv)
def __add__(self, o):
o = o if isinstance(o, Dual) else Dual(o)
return Dual(self.v + o.v, self.dv + o.dv)
def __mul__(self, o):
o = o if isinstance(o, Dual) else Dual(o)
return Dual(self.v * o.v, self.dv * o.v + self.v * o.dv)
def __neg__(self):
return Dual(-self.v, -self.dv)
def __sub__(self, o):
return self + (-o)
def exp(self):
e = np.exp(self.v)
return Dual(e, e * self.dv)
def log(self):
return Dual(np.log(self.v), self.dv / self.v)
def f_dual(t):
"""f(t) = log(1 + exp(2t)) + t^3 / 3"""
return (Dual(1.0) + (Dual(2.0) * t).exp()).log() + t * t * t / 3
def f_val(t): return np.log(1 + np.exp(2 * t)) + t ** 3 / 3
def f_der(t): return 2 / (1 + np.exp(-2 * t)) + t ** 2
t0 = 0.7
val = f_dual(Dual(t0, 1.0))
print(f"对偶数:f = {val.v:.8f},f' = {val.dv:.8f}")
print(f"解析值:f = {f_val(t0):.8f},f' = {f_der(t0):.8f}")
print("\n中心差分误差:")
for h in [1e-2, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8]:
err = abs((f_val(t0 + h) - f_val(t0 - h)) / (2 * h) - f_der(t0))
print(f" h = {h:.0e}: 绝对误差 = {err:.3e}")
# 对偶数给出的导数是精确的(与解析值逐位相同)
# 中心差分误差在 h = 1e-5 附近最小(约 1e-11),两侧分别由截断误差与舍入误差主导输出验证两点。对偶数与解析梯度的数值逐位一致,说明前向模式自动微分在机器精度内精确;中心差分的误差在
3.3.5 本节小结
条件、步长、速率与梯度
本节给出无约束优化的一阶方法框架。最优性条件方面,一阶必要条件
本节的分析都以每步都能看到真实梯度为前提,实际训练中梯度往往只由一小批样本估计,且问题规模巨大。下一节处理这些情形:动量与加速改善条件数依赖,随机梯度把每次迭代的代价降到与样本量无关,方差缩减与自适应步长处理噪声与尺度问题,本地 SGD 与联邦学习处理分布式场景。
练习题
第 1 题 概念推导
参考答案
负特征值情形:设
当
零特征值情形:
结论:二阶条件在 Hessian 满秩时是判别工具(正定给出极小,有负特征值排除极小),在出现零特征值时失效。数值算法处理这一退化时通常加入微小扰动或利用高阶信息(三次正则化是 3.8 节的应对手段之一)。
第 2 题 计算推理
参考答案
极小点:梯度为
常数与收敛因子:Hessian 为
迭代次数:
注意初值的间隙约
缩放后的变化:令
与牛顿法的关系:牛顿法用
第 3 题 代码验证
用数值实验检验本节的三个结论。取一维非对称双井函数
在区间
参考答案
import numpy as np
def f(x): return 0.25 * x ** 4 - 0.5 * x ** 2 + 0.35 * x
def g(x): return x ** 3 - x + 0.35
L, eta = 11.0, 1 / 11
rp = np.sort(np.roots([1, 0, -1, 0.35]).real)
print("临界点:", np.round(rp, 6))
print("对应函数值:", [round(float(f(r)), 6) for r in rp])
f_star = float(f(rp[0])) # 全局最小值
for x0 in [-1.6, 1.6]:
x, best, viol = x0, np.inf, 0
for k in range(1, 4001):
grad = g(x)
best = min(best, grad * grad)
if f(x - grad / L) > f(x) - grad ** 2 / (2 * L) + 1e-12: # 下降引理
viol += 1
x = x - eta * grad
print(f"x0 = {x0:+.1f}:收敛到 x = {x:.6f},f = {f(x):.6f},下降引理违背 {viol} 次")
print(f" min grad^2 = {best:.3e},界 2L(f(x0)-f*)/k 在 k = 100 处 = {2 * L * (f(x0) - f_star) / 100:.4f}")输出:
临界点: [-1.142907 0.428896 0.714011]
对应函数值: [-0.626572, 0.066597, 0.059975]
x0 = -1.6:收敛到 x = -1.142907,f = -0.626572,下降引理违背 0 次
min grad^2 = 6.040e-31,界 2L(f(x0)-f*)/k 在 k = 100 处 = 0.0935
x0 = +1.6:收敛到 x = 0.714011,f = 0.059975,下降引理违背 0 次
min grad^2 = 3.729e-31,界 2L(f(x0)-f*)/k 在 k = 100 处 = 0.3399结论:函数有三个临界点,中间一个是局部最大点(
其一,下降引理从两个初值出发的
其二,界在
其三,两个初值收敛到不同的局部极小点(
第 4 题 综合应用
某训练系统实现了一个通用梯度下降求解器,用户可以传入目标函数与梯度。上线后发现两类问题:其一,用户传入的梯度函数存在实现错误(数值上不对),求解器却报告收敛,停在一个错误的位置;其二,多个用户报告不同量纲的特征导致收敛极慢。请设计一个梯度检查流程,说明检查的步长、容差与判断标准;针对第二类问题给出具体的排查方法与三种可行的修复方案;并说明修复方案中哪一种最适合用户无法修改目标函数的场景。
参考答案
梯度检查流程:对随机选取的若干点与若干方向,用中心差分估计方向导数,与传入的梯度点乘结果比较:
步长取
第二类问题的排查:计算或估计 Hessian 的最大与最小特征值(对小规模问题直接计算,对大规模问题用幂迭代估计谱的极端值),得到条件数
三种修复方案:其一,特征标准化(减均值除标准差);其二,用自适应方法(Adam 类)自动调整每个坐标的有效步长;其三,用二阶信息或拟牛顿法估计曲率做预条件。
无法修改目标函数的场景:最适合的方案是第二种。标准化要求改写目标函数与梯度(因为数据被改变),拟牛顿法需要求解器内部实现并假设函数两阶可微。自适应方法(第 3.4 节)只使用梯度的历史信息,在求解器内部即可实现,用户无感知。它的原理正是本节的例子所显示的:用梯度平方的滑动平均近似每个方向的曲率,等价于学习一个对角缩放矩阵,把条件数大的问题在迭代中逐步校正。
常见错误
错误 1 · 把驻点当作极小点
原因:一阶必要条件只排除非驻点,不能排除鞍点与极大点。非凸问题中相当一部分驻点是鞍点(高维问题中这一比例更高),算法停在鞍点后报告收敛会给出错误的最优值。
解决:报告最小的同时报告二阶信息或梯度范数的变化:计算最优点处 Hessian 的最小特征值(小规模问题直接计算,大规模问题用 Hessian-向量积与幂迭代估计),若存在负特征值则说明结果不可信。非凸训练中采用带扰动的初始化或多起点策略,3.8 节给出系统的逃逸方法。
错误 2 · 线搜索只保留 Armijo 条件并用过小的初始步长
原因:只有充分下降条件时,线搜索可能返回极小的步长,迭代看起来稳定但进展极慢(方向正确而步子太小)。若初始步长又设得很小,回溯根本不会触发,问题被误解为算法效率低。
解决:初始步长取
错误 3 · 用有限差分作为大规模训练中的梯度来源
原因:中心差分对每个参数需要两次函数求值,参数规模
解决:实现解析梯度或用自动微分(反向模式)。开发期用有限差分做梯度检查(3.3.4 节),训练期用自动微分。若必须使用无梯度方法(目标函数不可微或实现为黑箱),改用 3.9 节的专门方法,而不是在梯度框架里硬塞差分。
错误 4 · 把强凸的线性收敛速率套用到所有凸问题
原因:
解决:先检查强凸性:计算 Hessian 的最小特征值(或它的下界估计),为零则改用