3.5 二阶方法与拟牛顿法
3.3 与 3.4 节的方法只使用梯度:方向的选取由一阶信息决定,步长由线搜索或调度确定。本节引入曲率信息。有了曲率,迭代可以沿二次模型的实际最优点前进,条件数的影响被大幅削弱,收敛从线性升级为二次。
三种使用曲率的方式按代价递增排列。牛顿法使用精确的 Hessian,单步代价最高,收敛最快,适合中小规模问题与需要高精度的场景。拟牛顿法用梯度差分近似曲率(割线条件),保持每步
3.5.1 牛顿法
二阶模型与牛顿方向
在点
右侧是
这一方向称为牛顿方向(Newton direction),迭代
从预条件的角度读同一件事:牛顿方向满足
局部二次收敛
牛顿法的收敛速率由以下结论刻画:
误差的平方级别收缩,称为二次收敛(quadratic convergence)。直观说法是有效数字的位数每步翻倍:误差
二次收敛的前提是初值足够接近最优解。远离最优解时,牛顿方向可能指向糟糕的位置(Hessian 在大步长处与二次模型差距很大),甚至因为 Hessian 不定而不是下降方向。
全局化与修正
两种修正使牛顿法在全局范围内可用。
阻尼牛顿法(damped Newton)在方向后加线搜索:沿
加上
自协调性给出全局收敛的定量分析。
代价
牛顿法每步需要组装 Hessian(
3.5.2 拟牛顿法
割线方程
拟牛顿法用梯度差分构造曲率信息。定义
梯度变化与位移之间的关系由割线方程(secant equation)给出:
DFP 更新(对逆矩阵):
BFGS 更新(对
BFGS 是四个提出者姓氏的首字母缩写,在实践中几乎完全取代了 DFP:两者互为对偶(交换
它可以直接逼近不定的 Hessian(信赖域方法常用它),但需要谨慎处理分母接近零的情形。Broyden 族把上述更新写成凸组合参数化的连续族,DFP 与 BFGS 是其中两个端点。
性质
三条性质决定了 BFGS 的地位。
保持正定:只要
超线性收敛:在适当假设下
每步代价:存储与更新
有限内存:L-BFGS
下列代码在同一逻辑回归问题上对比四种方法:牛顿法(精确 Hessian)、BFGS、L-BFGS 与梯度下降。判断标准是梯度范数降到
import numpy as np
rng = np.random.default_rng(0)
n, d = 2000, 50
X = rng.normal(size=(n, d))
w_true = rng.normal(size=d)
p = 1 / (1 + np.exp(-X @ w_true))
y = (rng.random(n) < p).astype(float)
lam = 1e-3
def f(w):
z = X @ w
return float(np.mean(np.logaddexp(0, z) - y * z) + 0.5 * lam * w @ w)
def g(w):
z = X @ w
s = 1 / (1 + np.exp(-z))
return X.T @ (s - y) / n + lam * w
def H(w):
z = X @ w
s = 1 / (1 + np.exp(-z))
return (X * (s * (1 - s))[:, None]).T @ X / n + lam * np.eye(d)
L = np.linalg.eigvalsh(X.T @ X / (4 * n)).max() + lam
tol = 1e-8
print(f"L = {L:.4f},m = {lam:.4f},kappa = {L / lam:.0f}")
# 牛顿法(阻尼)
w = np.zeros(d); hist_n = []
for k in range(60):
gg = g(w); hist_n.append(np.linalg.norm(gg))
if hist_n[-1] < 1e-11:
break
p_dir = -np.linalg.solve(H(w), gg)
t = 1.0
while f(w + t * p_dir) > f(w) + 1e-4 * t * gg @ p_dir:
t *= 0.5
w = w + t * p_dir
print(f"牛顿法:{k} 步达到 1e-11")
print(" ||g|| 序列:" + " ".join(f"{v:.2e}" for v in hist_n))
# BFGS
w = np.zeros(d); B = np.eye(d)
for k in range(500):
gg = g(w)
if np.linalg.norm(gg) < tol:
break
p_dir = -np.linalg.solve(B, gg)
t = 1.0
while f(w + t * p_dir) > f(w) + 1e-4 * t * gg @ p_dir:
t *= 0.5
w_new = w + t * p_dir
s = w_new - w; yv = g(w_new) - gg
if yv @ s > 1e-12:
B = B - np.outer(B @ s, B @ s) / (s @ B @ s) + np.outer(yv, yv) / (yv @ s)
w = w_new
print(f"BFGS:{k} 步达到 1e-8")
# L-BFGS(m = 5,两循环递归)
w = np.zeros(d); S_list, Y_list = [], []
for k in range(500):
gg = g(w)
if np.linalg.norm(gg) < tol:
break
q = gg.copy(); alphas = []
for s, yv in zip(reversed(S_list), reversed(Y_list)):
a = s @ q / (yv @ s); alphas.append(a); q = q - a * yv
gamma = 1.0 if not S_list else (S_list[-1] @ Y_list[-1]) / (Y_list[-1] @ Y_list[-1])
r_vec = gamma * q
for (s, yv), a in zip(zip(S_list, Y_list), reversed(alphas)):
b = yv @ r_vec / (yv @ s); r_vec = r_vec + s * (a - b)
p_dir = -r_vec
t = 1.0
while f(w + t * p_dir) > f(w) + 1e-4 * t * gg @ p_dir:
t *= 0.5
w_new = w + t * p_dir
s = w_new - w; yv = g(w_new) - gg
if yv @ s > 1e-12:
S_list.append(s); Y_list.append(yv)
if len(S_list) > 5:
S_list.pop(0); Y_list.pop(0)
w = w_new
print(f"L-BFGS(m = 5):{k} 步达到 1e-8")
# 梯度下降
w = np.zeros(d)
for k in range(200000):
gg = g(w)
if np.linalg.norm(gg) < tol:
break
w = w - (1 / L) * gg
print(f"梯度下降(步长 1/L):{k} 步达到 1e-8")输出:
L = 0.3355,m = 0.0010,kappa = 335
牛顿法:9 步达到 1e-11
||g|| 序列:3.83e-01 1.12e-01 4.19e-02 1.36e-02 2.85e-03 1.93e-04 1.01e-06 2.68e-11 1.34e-11 4.40e-17
BFGS:95 步达到 1e-8
L-BFGS(m = 5):20 步达到 1e-8
梯度下降(步长 1/L):584 步达到 1e-8牛顿法的梯度序列展示了两个阶段:前四步按约
3.5.3 高斯-牛顿与 Levenberg-Marquardt
非线性最小二乘的结构
很多问题的目标函数是残差平方和:
其中
丢弃项的合理性有两种情形:残差本身接近零(拟合良好,
阻尼与自适应
Levenberg-Marquardt 方法(LM)在法方程中加入阻尼:
分母是二次模型预测的下降量。
下列代码用同一个指数衰减曲线的拟合任务对比两种方法:高斯-牛顿从好初值出发收敛很快,从坏初值出发失败;LM 从同样的坏初值出发收敛。
import numpy as np
rng = np.random.default_rng(1)
xs = np.linspace(0, 1, 200)
y_obs = 2.0 * np.exp(-3.0 * xs) + 0.05 * rng.normal(size=xs.size)
def resid(t): # 残差 r = a exp(b x) - y
return t[0] * np.exp(t[1] * xs) - y_obs
def jac(t): # 雅可比:对 a 与 b 的偏导
e = np.exp(t[1] * xs)
return np.stack([e, t[0] * xs * e], axis=1)
def loss(t):
r = resid(t)
return 0.5 * float(r @ r)
# 高斯-牛顿:好初值
t = np.array([1.8, -2.5])
for k in range(60):
r = resid(t); J = jac(t)
step = np.linalg.solve(J.T @ J, -J.T @ r)
t = t + step
if np.linalg.norm(step) < 1e-12:
break
print(f"高斯-牛顿(初值 a=1.8, b=-2.5):{k} 步收敛到 a = {t[0]:.4f}, b = {t[1]:.4f},损失 = {loss(t):.4e}")
# 高斯-牛顿:坏初值
t = np.array([0.1, -5.0])
traj = []
for k in range(60):
traj.append(loss(t))
r = resid(t); J = jac(t)
step = np.linalg.solve(J.T @ J, -J.T @ r)
t = t + step
if not np.all(np.isfinite(t)):
break
print(f"高斯-牛顿(初值 a=0.1, b=-5):{k} 步后损失轨迹前 3 项 = {['%.2e' % v for v in traj[:3]]}")
print(f" 最终参数 a = {t[0]:.4f}, b = {t[1]:.4f},损失 = {loss(t):.2e}(陷入退化解)")
# Levenberg-Marquardt:同样的坏初值
t = np.array([0.1, -5.0]); lam = 1e-3
for k in range(200):
r = resid(t); J = jac(t)
step = np.linalg.solve(J.T @ J + lam * np.eye(2), -J.T @ r)
if loss(t + step) < loss(t):
t = t + step
lam = max(lam * 0.3, 1e-12)
if np.linalg.norm(step) < 1e-12:
break
else:
lam *= 5.0
print(f"LM(同样的坏初值):{k} 步收敛到 a = {t[0]:.4f}, b = {t[1]:.4f},损失 = {loss(t):.4e}")
# 真值 a = 2, b = -3;噪声标准差 0.05 时最优损失期望约 0.25输出:
高斯-牛顿(初值 a=1.8, b=-2.5):5 步收敛到 a = 2.0049, b = -3.0299,损失 = 2.1313e-01
高斯-牛顿(初值 a=0.1, b=-5):2 步后损失轨迹前 3 项 = ['6.21e+01', '5.55e+48', '6.71e+01']
最终参数 a = 0.0000, b = 55.4007,损失 = 6.71e+01(陷入退化解)
LM(同样的坏初值):12 步收敛到 a = 2.0049, b = -3.0299,损失 = 2.1313e-01好初值下高斯-牛顿
3.5.4 信赖域方法
子问题
线搜索先定方向再找步长;信赖域方法(trust region)同时确定两者。它维护一个半径
柯西点、狗腿法与 Steihaug-CG
子问题很少精确求解,常用的近似解有三个层次。
柯西点(Cauchy point)沿负梯度方向做截断的一维最小化:
它是子问题的最简单可行近似,代价只有两次矩阵乘法,并且给出模型下降量的保证:
狗腿法(dogleg)在柯西点与高斯-牛顿步之间连一条折线,取折线与信赖域边界的交点。它在两个端点都不精确时给出折衷,适合
Steihaug-CG 用共轭梯度在 Krylov 子空间内近似求解子问题,遇到负曲率方向(
半径更新
每步计算增益比
下列代码在同一个问题里对比柯西点与信赖域子问题的精确解,说明近似解的位置与质量。
import numpy as np
rng = np.random.default_rng(3)
B = rng.normal(size=(5, 5))
B = B @ B.T + 0.5 * np.eye(5) # 正定矩阵
gv = rng.normal(size=5)
Delta = 1.0
m = lambda p: gv @ p + 0.5 * p @ B @ p
# 柯西点:沿 -g 的截断一维最小化
t_c = (gv @ gv) / (gv @ B @ gv)
if t_c * np.linalg.norm(gv) > Delta:
t_c = Delta / np.linalg.norm(gv)
p_C = -t_c * gv
print(f"柯西点:t* = {t_c:.6f},||p_C|| = {np.linalg.norm(p_C):.6f},模型值 m(p_C) = {m(p_C):.6f}")
# 精确解:对 (B + lambda I) p = -g 扫描 lambda
eigvals = np.linalg.eigvalsh(B)
best = None
for lam in np.linspace(0, 20, 200001):
p = -np.linalg.solve(B + lam * np.eye(5), gv)
if np.linalg.norm(p) <= Delta:
best = p
break
print(f"信赖域精确解:||p*|| = {np.linalg.norm(best):.6f},模型值 m(p*) = {m(best):.6f}")
print(f"柯西点完成的模型下降占比 = {m(p_C) / m(best) * 100:.1f}%")
# 柯西点位于信赖域边界上(本例中 t* 被半径截断),模型下降约为精确解的 27%输出:柯西点位于信赖域边界(
3.5.5 共轭梯度法
线性共轭梯度
求解线性系统
其中
收敛速率的界为
迭代次数为
非线性共轭梯度与信赖域中的应用
非线性问题上的共轭梯度用线搜索确定步长,方向由以下递推给出:
CG 的另一个角色是作为信赖域子问题(3.5.4 节)与截断牛顿法的内层求解器:牛顿方程
下列代码在条件数
import numpy as np
rng = np.random.default_rng(2)
n_cg = 100
U, _ = np.linalg.qr(rng.normal(size=(n_cg, n_cg)))
eigs = np.logspace(0, 3, n_cg) # 特征值从 1 到 1000
Q = U @ np.diag(eigs) @ U.T
Q = 0.5 * (Q + Q.T)
b = rng.normal(size=n_cg)
# 共轭梯度
x = np.zeros(n_cg)
r = b - Q @ x
pv = r.copy()
rs = r @ r
res = [np.sqrt(rs) / np.linalg.norm(b)]
for k in range(1, 1000):
Ap = Q @ pv
alpha = rs / (pv @ Ap)
x = x + alpha * pv
r = r - alpha * Ap
rs_new = r @ r
res.append(np.sqrt(rs_new) / np.linalg.norm(b))
if res[-1] < 1e-8:
break
pv = r + (rs_new / rs) * pv
rs = rs_new
print(f"kappa = {eigs.max() / eigs.min():.0f},维度 {n_cg}")
print(f"CG:{k} 步使相对残差降到 1e-8(理论量级 0.5 sqrt(kappa) log(2/tol) = {0.5 * np.sqrt(1000) * np.log(2 / 1e-8):.0f})")
# 梯度下降
x = np.zeros(n_cg)
for k in range(1000000):
r = Q @ x - b
if np.linalg.norm(r) / np.linalg.norm(b) < 1e-8:
break
x = x - (1.0 / eigs.max()) * r
print(f"梯度下降(步长 1/L):{k} 步")输出:
kappa = 1000,维度 100
CG:169 步使相对残差降到 1e-8(理论量级 0.5 sqrt(kappa) log(2/tol) = 302)
梯度下降(步长 1/L):16077 步CG 用
3.5.6 本节小结
曲率的三种用法
本节按使用曲率的方式组织了二阶方法。牛顿法用精确 Hessian,方向是二次模型的最优解,局部二次收敛(误差每步平方收缩),代价是
二阶方法到此完成。它们与一阶方法共享同一个光滑性框架,区别只在曲率信息的来源与代价。下一节换上另一类工具:约束优化与对偶理论,那里需要 3.2 节的分离定理与共轭函数,最优性条件从梯度为零升级为 KKT 系统,而对偶问题把约束处理转化为无约束问题,在内点法与分解算法中反复使用。
练习题
第 1 题 概念推导
参考答案
误差递推:记
其中
用
二次收敛:Hessian 以常数
收敛常数与
远处的失效:推导用了三个局部性质:Hessian 的可逆性与
第 2 题 计算推理
参考答案
最优解:这是三个指数的加权和(对数配分函数的形式,权重为常数的相反数向量):
梯度与 Hessian:与 log-sum-exp 相同结构,梯度为
条件数:
牛顿法步数:函数是指数的和,非二次,但
第 3 题 代码验证
用 3.5.2 与 3.5.3 节的代码框架完成两个实验并解释:其一,把 L-BFGS 的内存
参考答案
import numpy as np
# 实验一:L-BFGS 的 m 与迭代次数
rng = np.random.default_rng(0)
n, d = 2000, 50
X = rng.normal(size=(n, d))
w_true = rng.normal(size=d)
p_true = 1 / (1 + np.exp(-X @ w_true))
y = (rng.random(n) < p_true).astype(float)
lam = 1e-3
def f(w):
z = X @ w
return float(np.mean(np.logaddexp(0, z) - y * z) + 0.5 * lam * w @ w)
def g(w):
z = X @ w
s = 1 / (1 + np.exp(-z))
return X.T @ (s - y) / n + lam * w
def lbgfs(m_mem):
w = np.zeros(d); S_list, Y_list = [], []
for k in range(2000):
gg = g(w)
if np.linalg.norm(gg) < 1e-8:
return k
q = gg.copy(); alphas = []
for s, yv in zip(reversed(S_list), reversed(Y_list)):
a = s @ q / (yv @ s); alphas.append(a); q = q - a * yv
gamma = 1.0 if not S_list else (S_list[-1] @ Y_list[-1]) / (Y_list[-1] @ Y_list[-1])
r_vec = gamma * q
for (s, yv), a in zip(zip(S_list, Y_list), reversed(alphas)):
b = yv @ r_vec / (yv @ s); r_vec = r_vec + s * (a - b)
p_dir = -r_vec
t = 1.0
while f(w + t * p_dir) > f(w) + 1e-4 * t * gg @ p_dir:
t *= 0.5
w_new = w + t * p_dir
s = w_new - w; yv = g(w_new) - gg
if yv @ s > 1e-12:
S_list.append(s); Y_list.append(yv)
if len(S_list) > m_mem:
S_list.pop(0); Y_list.pop(0)
w = w_new
return None
for m_mem in [1, 5, 20]:
print(f"L-BFGS(m = {m_mem}):{lbgfs(m_mem)} 步达到 1e-8")
# 实验二:LM 的初始 lambda
rng = np.random.default_rng(1)
xs = np.linspace(0, 1, 200)
y_obs = 2.0 * np.exp(-3.0 * xs) + 0.05 * rng.normal(size=xs.size)
resid = lambda t: t[0] * np.exp(t[1] * xs) - y_obs
def jac(t):
e = np.exp(t[1] * xs)
return np.stack([e, t[0] * xs * e], axis=1)
loss = lambda t: 0.5 * float(resid(t) @ resid(t))
for lam0 in [1e-6, 1e-3, 1.0]:
t = np.array([0.1, -5.0]); lam = lam0; prev = loss(t)
for k in range(300):
r = resid(t); J = jac(t)
step = np.linalg.solve(J.T @ J + lam * np.eye(2), -J.T @ r)
new_loss = loss(t + step)
if new_loss < prev:
t = t + step; lam = max(lam * 0.3, 1e-12)
if prev - new_loss < 1e-13 * max(1.0, prev):
break
prev = new_loss
else:
lam *= 5.0
print(f"LM(lambda0 = {lam0:.0e}):{k} 步,a = {t[0]:.4f},b = {t[1]:.4f},损失 = {loss(t):.4e}")输出:
L-BFGS(m = 1):42 步达到 1e-8
L-BFGS(m = 5):20 步达到 1e-8
L-BFGS(m = 20):18 步达到 1e-8
LM(lambda0 = 1e-06):13 步,a = 2.0049,b = -3.0299,损失 = 2.1313e-01
LM(lambda0 = 1e-03):7 步,a = 2.0049,b = -3.0299,损失 = 2.1313e-01
LM(lambda0 = 1e+00):5 步,a = 2.0049,b = -3.0299,损失 = 2.1313e-01结论:内存
LM 的初始
第 4 题 综合应用
某团队要为一个三维重建系统实现 Bundle Adjustment:优化变量是若干相机位姿与三维点坐标(合计
参考答案
方法选择:目标函数是残差平方和,结构上适合 Levenberg-Marquardt。理由:残差来自几何投影,非线性中等,初值通常由其他流程(特征匹配、三角化)给出,离最优解不远;LM 的阻尼在残差大的区域提供稳健性,在收敛后期退化为高斯-牛顿,享受二次收敛。牛顿法需要完整的 Hessian 且对不定矩阵敏感,
子问题求解:LM 每步解
稀疏结构的利用:BA 的雅可比按相机块与点块分块,
复杂度量级:每步组装
两个数值风险:其一是规范(gauge)自由度:整体平移与旋转不改变重投影误差,Hessian 奇异(零特征值对应这些方向)。处理方式是固定一个相机(或加入先验),或让阻尼项
常见错误
错误 1 · 在 Hessian 不定时直接使用牛顿方向
原因:牛顿方向由解
解决:使用阻尼牛顿或信赖域:前者对 Hessian 加
错误 2 · 把 L-BFGS 用于随机梯度的场景
原因:L-BFGS 的割线方程建立在梯度是确定性函数的前提上。随机梯度带噪时,
解决:随机场景用随机拟牛顿的修正版本(要求大批量、对
错误 3 · 在高斯-牛顿失败时只调步长,不引入阻尼
原因:高斯-牛顿的失败模式是步长本身太大(
解决:引入阻尼项(LM)并让
错误 4 · 把共轭梯度的 n 步精确终止当作浮点情形的保证
原因:CG 在精确算术下
解决:用残差范数作为终止判据(相对残差或绝对残差),设置迭代上限作为保险。需要更高精度或更稳健行为时使用重启动(每