3.7 近端方法与稀疏优化
3.6 节末尾留下的问题是
处理非光滑问题的方法是把目标拆成两部分:一部分光滑(数据拟合项),一部分非光滑但结构简单(正则项或约束)。对后者用一个能被精确求解的局部算子代替梯度,这个算子就是近端算子(proximal operator)。它是投影的推广:投影处理示性函数(硬约束),近端算子处理任何凸罚函数。有了它,非光滑问题被改造成与梯度法同样简单的迭代格式,收敛速度仍然是
本节按工具层次推进。3.7.1 定义近端算子与 Moreau 包络,给出几个可手算的例子;3.7.2 用近端梯度法求解 Lasso,并给出 ISTA 与 FISTA 的对比;3.7.3 引入算子分裂与 ADMM,处理两个非光滑项或两个块的复合问题;3.7.4 讨论投影不可用时的替代方案 Frank-Wolfe;3.7.5 汇总稀疏优化中的结构化正则、非凸正则与工程实践。
3.7.1 近端算子与 Moreau 包络
定义
对凸函数
结构上是两项的折中:二次项要求结果靠近
投影是近端算子的特例,这一关系使 3.6 节的投影梯度与本节的方法成为同一个框架的两个端点。
最优性条件与不动点
近端算子的定义是无约束问题,写出一阶条件(次微分形式,3.2 节):
读法:
其一是不动点刻画:
其二是与 3.6 节 KKT 条件的联系:
可解析计算的例子
四个常用的近端算子都有闭式解,这些公式是后面所有算法的构件。
超过阈值的分量被平移
箱约束:截断。
二次函数:线性收缩。
Moreau 包络与 Moreau 分解
近端算子导出一个光滑函数。定义 Moreau 包络(Moreau envelope)
它是近端问题的最优值。包络有两个有用的性质:
也就是说,即便
Moreau 分解把近端算子与共轭函数连接起来:
其中
数值验证
下面的代码实现四个近端算子,并用数值手段核对软阈值与 Moreau 包络的结论。
import numpy as np
def soft(v, t):
return np.sign(v) * np.maximum(np.abs(v) - t, 0.0)
def prox_l2ball(v, r):
nv = np.linalg.norm(v)
return v if nv <= r else v * (r / nv)
def prox_box(v, lo, hi):
return np.clip(v, lo, hi)
def prox_quad(v, t, mu):
return v / (1 + t * mu)
# 软阈值:与逐分量的网格最小化对照
v = np.array([-2.3, -0.4, 0.7, 3.1])
t = 1.0
grid = np.linspace(-6, 6, 240001)
num = np.array([grid[np.argmin(t * np.abs(grid) + 0.5 * (grid - vi) ** 2)] for vi in v])
print("软阈值解析解:", ", ".join(f"{u:.4f}" for u in soft(v, t)))
print("网格数值解: ", ", ".join(f"{u:.4f}" for u in num))
w = np.array([3.0, 4.0])
print("L2 球投影 (r=2):", ", ".join(f"{u:.4f}" for u in prox_l2ball(w, 2.0)))
print("箱投影 ([-1,1]):", ", ".join(f"{u:.4f}" for u in prox_box(w, -1.0, 1.0)))
print("二次近端 (mu=3):", ", ".join(f"{u:.4f}" for u in prox_quad(w, 1.0, 3.0)))
# Moreau 分解:prox_tf(v) + t * prox_{f*/t}(v/t) = v,f = ||.||_1
p1 = soft(v, t)
p2 = t * prox_box(v / t, -1.0, 1.0)
print("Moreau 分解残差:", np.linalg.norm(p1 + p2 - v))
# Moreau 包络的梯度:解析式与中心差分对照
def moreau(vv, tt):
p = soft(vv, tt)
return float(np.sum(np.abs(p)) + np.sum((p - vv) ** 2) / (2 * tt))
v0 = np.array([0.3, -1.7, 2.4])
h = 1e-6
g_num = np.array([(moreau(v0 + h * e, t) - moreau(v0 - h * e, t)) / (2 * h)
for e in np.eye(3)])
g_ana = (v0 - soft(v0, t)) / t
print("Moreau 包络梯度(数值):", ", ".join(f"{u:.6f}" for u in g_num))
print("Moreau 包络梯度(解析):", ", ".join(f"{u:.6f}" for u in g_ana))输出:
软阈值解析解: -1.3000, -0.0000, 0.0000, 2.1000
网格数值解: -1.3000, 0.0000, 0.0000, 2.1000
L2 球投影 (r=2): 1.2000, 1.6000
箱投影 ([-1,1]): 1.0000, 1.0000
二次近端 (mu=3): 0.7500, 1.0000
Moreau 分解残差: 0.0
Moreau 包络梯度(数值): 0.300000, -1.000000, 1.000000
Moreau 包络梯度(解析): 0.300000, -1.000000, 1.000000四项核对全部通过。软阈值与网格最小化逐位一致(网格步长
3.7.2 近端梯度法与 ISTA/FISTA
复合问题的结构
本节处理的问题形如
构造迭代的方法沿用 3.3 节的下降引理。由
把右端加上
这正是近端梯度法(proximal gradient):先沿光滑部分做一步梯度下降,再用近端算子修正非光滑部分。
为什么近端算子能替代线搜索
下降引理中的二次项已经给出
收敛速度与加速
向 Lasso 那样的复合凸问题上,近端梯度法的收敛速率为
加速版本称为 FISTA(fast iterative shrinkage-thresholding algorithm),在近端梯度步之外增加动量外推:
其中
Lasso 作为标准例子
Lasso 问题是近端梯度法的标准测试场:
光滑部分
正则参数有一个自然的起点:当
Lasso 的最优性条件(3.6 节 KKT 条件在非光滑情形的形式)为
非零分量处的相关系数恰好被钉在
数值实验
设计矩阵取 AR(1) 结构(列之间相关性为
import numpy as np
def soft(v, t):
return np.sign(v) * np.maximum(np.abs(v) - t, 0.0)
rng = np.random.default_rng(0)
n, d, s = 200, 150, 30
Z = rng.normal(size=(n, d))
X = Z.copy()
for j in range(1, d): # AR(1) 相关设计矩阵
X[:, j] = 0.9 * X[:, j - 1] + np.sqrt(1 - 0.81) * Z[:, j]
X /= np.sqrt(n)
w_true = np.zeros(d)
idx = rng.choice(d, s, replace=False)
w_true[idx] = rng.normal(size=s) * 2
y = X @ w_true + 0.1 * rng.normal(size=n)
G = X.T @ X
ev = np.linalg.eigvalsh(G)
L = float(ev.max())
print(f"L = {L:.4f}, 最小特征值 = {ev.min():.6f}, 条件数 = {L / ev.min():.1f}")
lam = 0.05 * float(np.abs(X.T @ y).max())
print(f"lambda = {lam:.6f}(lambda_max = {np.abs(X.T @ y).max():.6f} 的 5%)")
def obj(w):
return 0.5 * float(np.sum((X @ w - y) ** 2)) + lam * float(np.sum(np.abs(w)))
def grad(w):
return X.T @ (X @ w - y)
# 坐标下降参考解
w_cd = np.zeros(d)
cols = np.sum(X ** 2, axis=0)
for sweep in range(5000):
w_old = w_cd.copy()
for j in range(d):
r = y - X @ w_cd + X[:, j] * w_cd[j]
rho = X[:, j] @ r
w_cd[j] = np.sign(rho) * max(abs(rho) - lam, 0.0) / cols[j]
if np.max(np.abs(w_cd - w_old)) < 1e-13:
break
f_ref = obj(w_cd)
nz = np.abs(w_cd) > 1e-10
overlap = len(set(np.nonzero(nz)[0]) & set(idx))
print(f"坐标下降: {sweep} 轮, 目标 = {f_ref:.10f}, 非零个数 = {int(nz.sum())}, "
f"真实支撑重合 = {overlap}/{s}")
# ISTA
def ista_run(maxit, checkpoints):
w = np.zeros(d)
hist = []
for k in range(maxit + 1):
if k in checkpoints:
hist.append((k, obj(w), (obj(w) - f_ref) / f_ref))
if k < maxit:
w = soft(w - grad(w) / L, lam / L)
return hist
# FISTA(自适应重启动)
def fista_run(maxit, checkpoints):
w = np.zeros(d); w_prev = w.copy(); theta = 1.0
hist = []
for k in range(maxit + 1):
if k in checkpoints:
hist.append((k, obj(w), (obj(w) - f_ref) / f_ref))
if k < maxit:
w_new = soft(w - grad(w) / L, lam / L)
if np.sum((w - w_new) * (w_new - w_prev)) > 0:
theta = 1.0
w = w_new
else:
theta_new = 0.5 * (1 + np.sqrt(1 + 4 * theta ** 2))
w = w_new + ((theta - 1) / theta_new) * (w_new - w_prev)
theta = theta_new
w_prev = w_new
return hist
print("ISTA:")
for k, val, gap in ista_run(4000, {100, 500, 1000, 4000}):
print(f" k = {k:>4}: 目标 = {val:.10f}, 相对次优 = {gap:.3e}")
print("FISTA(自适应重启动):")
for k, val, gap in fista_run(400, {100, 200, 400}):
print(f" k = {k:>4}: 目标 = {val:.10f}, 相对次优 = {gap:.3e}")
# 达到相对次优 1e-6 的步数
def steps_to(tol, use_fista, maxit):
w = np.zeros(d); w_prev = w.copy(); theta = 1.0
for k in range(maxit):
if (obj(w) - f_ref) / f_ref < tol:
return k
w_new = soft(w - grad(w) / L, lam / L)
if not use_fista:
w = w_new
else:
if np.sum((w - w_new) * (w_new - w_prev)) > 0:
theta = 1.0
w = w_new
else:
theta_new = 0.5 * (1 + np.sqrt(1 + 4 * theta ** 2))
w = w_new + ((theta - 1) / theta_new) * (w_new - w_prev)
theta = theta_new
w_prev = w_new
return None
print(f"ISTA 达到相对次优 1e-6:{steps_to(1e-6, False, 40000)} 步")
print(f"FISTA 达到相对次优 1e-6:{steps_to(1e-6, True, 40000)} 步")输出:
L = 20.5805, 最小特征值 = 0.002155, 条件数 = 9549.5
lambda = 0.247763(lambda_max = 4.955250 的 5%)
坐标下降: 259 轮, 目标 = 10.1670594089, 非零个数 = 34, 真实支撑重合 = 18/30
ISTA:
k = 100: 目标 = 11.0924727466, 相对次优 = 9.102e-02
k = 500: 目标 = 10.1890235637, 相对次优 = 2.160e-03
k = 1000: 目标 = 10.1681372745, 相对次优 = 1.060e-04
k = 4000: 目标 = 10.1670594095, 相对次优 = 6.039e-11
FISTA(自适应重启动):
k = 100: 目标 = 10.1679104407, 相对次优 = 8.370e-05
k = 200: 目标 = 10.1670610703, 相对次优 = 1.634e-07
k = 400: 目标 = 10.1670594089, 相对次优 = 2.207e-13
ISTA 达到相对次优 1e-6:1929 步
FISTA 达到相对次优 1e-6:174 步三组数据的解读如下。
条件数与收敛速度。设计矩阵的条件数为
稀疏性与支撑恢复。参考解有
坐标下降的地位。
3.7.3 算子分裂与 ADMM
两个非光滑项的处理
上一节要求目标中只有一项非光滑。出现两项时(例如 Lasso 加全变差正则、或者带约束的稀疏问题)近端算子本身可能没有闭式解,软化近端算子会引入内层迭代。算子分裂(operator splitting)的思路是不构造复合的近端算子,而是让两个算子交替作用。
两种基本分裂的格式如下(
前向-后向分裂即近端梯度法:
Douglas-Rachford 分裂处理两个都不可微的项,通过反射算子交替:
Peaceman-Rachford 是它的对称版本,收敛条件更严格(要求更强的单侧条件),但通常更快。这一族算法的优点是每步只使用两个简单算子,收敛到
ADMM 的推导
ADMM(alternating direction method of multipliers)是把分裂结构加进增广拉格朗日(3.6.4 节)得到的。考虑两块问题
增广拉格朗日(缩放形式,
对
「交替方向」指的就是前两步在两个方向上依次进行。与 3.6.4 节的增广拉格朗日方法相比,
收敛性与停止判据
停止判据同时检查两类残差(相对形式):
只看一个残差会过早停止:原始残差小而对偶残差大时,解虽然在约束面上,乘子却没有收敛,用它对偶信息(灵敏度、下界)会出错。惩罚参数
数值实验
用一维全变差去噪(一维 fused Lasso)演示 ADMM。问题为
引入差分矩阵
import numpy as np
def soft(v, t):
return np.sign(v) * np.maximum(np.abs(v) - t, 0.0)
rng = np.random.default_rng(1)
n = 200
x_true = np.concatenate([np.zeros(60), 3 * np.ones(40), np.zeros(30),
-2 * np.ones(30), np.zeros(40)])
y = x_true + 0.5 * rng.normal(size=n)
lam = 0.8
D = np.diff(np.eye(n), axis=0)
def admm_tv(rho, maxit=2000, tol=1e-10):
z = D @ y
u = np.zeros(n - 1)
Amat = np.linalg.solve(np.eye(n) + rho * D.T @ D, np.eye(n)) # 预分解,迭代中复用
hist = []
for k in range(maxit):
x = Amat @ (y + rho * D.T @ (z - u))
Dx = D @ x
z_new = soft(Dx + u, lam / rho)
u = u + Dx - z_new
r_pr = float(np.linalg.norm(Dx - z_new))
r_du = float(rho * np.linalg.norm(D.T @ (z_new - z)))
obj = 0.5 * float(np.sum((x - y) ** 2)) + lam * float(np.sum(np.abs(z_new)))
z = z_new
hist.append((k, r_pr, r_du, obj))
if r_pr < tol and r_du < tol:
break
return x, z, u, hist
x, z, u, hist = admm_tv(1.0)
print("ADMM(rho = 1):")
for k, r_pr, r_du, obj in hist:
if k in (0, 5, 20, 50, 100, 200, 400) or k == len(hist) - 1:
print(f" k = {k:>3}: 原始残差 = {r_pr:.3e}, 对偶残差 = {r_du:.3e}, 目标 = {obj:.8f}")
print(f" 迭代总数 = {len(hist)}, 跳变点数 = {int(np.sum(np.abs(z) > 1e-6))}")
# 次微分条件:p = rho * u 应满足 |p_i| <= lambda,且在 z_i != 0 处 p_i = lambda * sign(z_i)
p = 1.0 * u
print(f" 次微分条件: max(|p|) - lambda = {np.max(np.abs(p)) - lam:+.3e}")
act = np.abs(z) > 1e-6
print(f" 活跃跳变处 max|p_i - lambda*sign(z_i)| = "
f"{np.max(np.abs(p[act] - lam * np.sign(z[act]))):.3e}")
# 对偶参考解:max_{|p| <= lambda} p^T D y - 0.5||D^T p||^2,用加速投影梯度
def dual_ref(steps=40000):
p = np.zeros(n - 1); p_old = p.copy(); t = 1.0
for k in range(steps):
gp = -(D @ y) + D @ (D.T @ p)
p_new = np.clip(p - gp / 4.0, -lam, lam)
t_new = 0.5 * (1 + np.sqrt(1 + 4 * t ** 2))
p = p_new + ((t - 1) / t_new) * (p_new - p_old)
p_old = p_new
t = t_new
p = np.clip(p, -lam, lam)
xd = y - D.T @ p
return 0.5 * float(np.sum((xd - y) ** 2)) + lam * float(np.sum(np.abs(D @ xd)))
print(f"对偶参考解(加速投影梯度 40000 步)目标 = {dual_ref():.10f}")
x_sg = y.copy()
for k in range(20001):
step = 0.5 / np.sqrt(k + 1)
x_sg = x_sg - step * ((x_sg - y) + lam * D.T @ np.sign(D @ x_sg))
obj_sg = 0.5 * float(np.sum((x_sg - y) ** 2)) + lam * float(np.sum(np.abs(D @ x_sg)))
print(f"次梯度法(20000 步)目标 = {obj_sg:.8f}(未收敛)")
print("rho 的影响(固定 300 步):")
for rho in [0.05, 0.2, 1.0, 5.0, 50.0]:
z = D @ y
u = np.zeros(n - 1)
Amat = np.linalg.solve(np.eye(n) + rho * D.T @ D, np.eye(n))
for k in range(300):
x = Amat @ (y + rho * D.T @ (z - u))
Dx = D @ x
z_new = soft(Dx + u, lam / rho)
u = u + Dx - z_new
r_pr = float(np.linalg.norm(Dx - z_new))
r_du = float(rho * np.linalg.norm(D.T @ (z_new - z)))
z = z_new
print(f" rho = {rho:>5}: 原始残差 = {r_pr:.3e}, 对偶残差 = {r_du:.3e}")输出:
ADMM(rho = 1):
k = 0: 原始残差 = 7.612e+00, 对偶残差 = 1.288e+01, 目标 = 18.94257790
k = 5: 原始残差 = 4.592e-01, 对偶残差 = 3.106e-01, 目标 = 26.64771574
k = 20: 原始残差 = 6.342e-02, 对偶残差 = 1.308e-02, 目标 = 27.96297719
k = 50: 原始残差 = 1.536e-02, 对偶残差 = 5.831e-03, 目标 = 28.11484130
k = 100: 原始残差 = 1.770e-03, 对偶残差 = 2.320e-04, 目标 = 28.13864793
k = 200: 原始残差 = 8.253e-05, 对偶残差 = 9.880e-06, 目标 = 28.14138526
k = 400: 原始残差 = 1.987e-07, 对偶残差 = 2.375e-08, 目标 = 28.14149412
k = 653: 原始残差 = 9.706e-11, 对偶残差 = 1.160e-11, 目标 = 28.14149437
迭代总数 = 654, 跳变点数 = 32
次微分条件: max(|p|) - lambda = +0.000e+00
活跃跳变处 max|p_i - lambda*sign(z_i)| = 2.220e-16
对偶参考解(加速投影梯度 40000 步)目标 = 28.1414943737
次梯度法(20000 步)目标 = 28.49294137(未收敛)
rho 的影响(固定 300 步):
rho = 0.05: 原始残差 = 9.155e-02, 对偶残差 = 7.448e-05
rho = 0.2: 原始残差 = 9.126e-03, 对偶残差 = 6.873e-05
rho = 1.0: 原始残差 = 4.170e-06, 对偶残差 = 4.985e-07
rho = 5.0: 原始残差 = 6.923e-14, 对偶残差 = 4.700e-12
rho = 50.0: 原始残差 = 1.101e-05, 对偶残差 = 6.887e-02四组数据的解读如下。
收敛过程。两类残差以接近线性的速率下降到
最优性验证。次微分条件 两行给出独立的验证:乘子
与次梯度法的对比。次梯度法用
惩罚参数。固定
预分解与稀疏结构
上面的代码用 np.linalg.solve 显式计算
3.7.4 Frank-Wolfe 与投影的替代
条件梯度
投影梯度与近端梯度都假定投影或近端算子可算。当可行集是核范数球、原子范数球或更复杂的凸集时,投影本身需要一次完整 SVD 或一个内层优化,代价可能超过外层的收益。Frank-Wolfe 方法(条件梯度法)用另一种原语替换投影:线性最小化 oracle(LMO)
然后沿
FW 间隙作为停止判据
对凸目标,定义 FW 间隙
收敛性与迭代点的结构
即
与近端梯度的取舍
两种方法的对比可以用一句话概括:FW 每步便宜但收敛慢,近端梯度与投影梯度每步贵(需要投影或近端)但收敛快(凸光滑情形下线性或近线性)。选择依据是投影与原语的代价比例以及是否要求高精度。
数值实验
在单纯形约束的二次问题上对比 FW 与投影梯度:
import numpy as np
rng = np.random.default_rng(5)
n = 300
rho_c = 0.7
i_idx, j_idx = np.meshgrid(np.arange(n), np.arange(n), indexing="ij")
Q = rho_c ** np.abs(i_idx - j_idx)
x_target = np.zeros(n)
supp = rng.choice(n, 40, replace=False)
x_target[supp] = rng.random(40) + 0.2
x_target /= x_target.sum()
mu = Q @ x_target
def f(x):
return 0.5 * float(x @ (Q @ x)) - float(mu @ x)
def grad(x):
return Q @ x - mu
L = float(np.linalg.eigvalsh(Q).max())
print(f"L = {L:.4f}, 条件数 = {L / np.linalg.eigvalsh(Q).min():.2f}")
def proj_simplex(v):
u = np.sort(v)[::-1]
css = np.cumsum(u)
r = np.nonzero(u * np.arange(1, len(v) + 1) > (css - 1))[0][-1]
theta = (css[r] - 1) / (r + 1)
return np.maximum(v - theta, 0.0)
# 起始顶点取目标函数最小的顶点
i0 = int(np.argmin(0.5 * np.diag(Q) - mu))
x0 = np.zeros(n); x0[i0] = 1.0
print(f"起始顶点 e_{i0 + 1}, 目标 = {f(x0):.6f}")
# 投影梯度:一遍跑完,取出各检查点
x_pg = x0.copy()
pg_hist = {}
for k in range(20001):
if k in (10, 100, 1000, 5000):
pg_hist[k] = f(x_pg)
x_pg = proj_simplex(x_pg - grad(x_pg) / L)
f_ref = f(x_pg)
print(f"投影梯度参考解:目标 = {f_ref:.10f}, 非零个数 = {int(np.sum(x_pg > 1e-10))}")
# Frank-Wolfe
x_fw = x0.copy()
print("Frank-Wolfe(精确线搜索):")
for k in range(20001):
g = grad(x_fw)
i = int(np.argmin(g))
d = np.zeros(n); d[i] = 1.0; d -= x_fw
Qd = Q @ d
fw_gap = float(-g @ d)
if k in (10, 100, 1000, 5000, 20000):
print(f" k = {k:>5}: FW 间隙 = {fw_gap:.3e}, 次优值 = {f(x_fw) - f_ref:.3e}, "
f"非零个数 = {int(np.sum(x_fw > 1e-10))}")
gamma = -float(Qd @ x_fw - d @ mu) / float(d @ Qd)
gamma = min(max(gamma, 0.0), 1.0)
x_fw = x_fw + gamma * d
print("投影梯度:")
for k in sorted(pg_hist):
print(f" k = {k:>5}: 次优值 = {pg_hist[k] - f_ref:.3e}")输出:
L = 5.6620, 条件数 = 32.08
起始顶点 e_14, 目标 = 0.439570
投影梯度参考解:目标 = -0.0202129037, 非零个数 = 40
Frank-Wolfe(精确线搜索):
k = 10: FW 间隙 = 7.952e-02, 次优值 = 1.863e-02, 非零个数 = 11
k = 100: FW 间隙 = 1.060e-03, 次优值 = 5.734e-05, 非零个数 = 40
k = 1000: FW 间隙 = 1.207e-04, 次优值 = 7.445e-06, 非零个数 = 40
k = 5000: FW 间隙 = 1.855e-05, 次优值 = 2.309e-07, 非零个数 = 40
k = 20000: FW 间隙 = 1.042e-07, 次优值 = 8.166e-12, 非零个数 = 40
投影梯度:
k = 10: 次优值 = 9.977e-03
k = 100: 次优值 = 1.727e-06
k = 1000: 次优值 = 1.041e-17
k = 5000: 次优值 = 0.000e+00四行对比说明了两种方法的特征。
收敛速度。投影梯度(条件数
支撑增长。FW 从单个顶点出发,第
代价结构。FW 每步一次梯度求值与一次线搜索(各一次矩阵-向量运算),不需要投影;投影梯度每步除梯度外还要一次排序(
3.7.5 稀疏结构、非凸正则与工程实践
结构化正则与它们的近端算子
组 Lasso 按预先给定的分组
整组的系数同时归零或被同比例缩放。分组结构对应「某个特征组整体不相关」的先验(多个传感器、多个实验批次)。
fused Lasso 与全变差 惩罚相邻系数之差:
原子范数 把「稀疏」写成对原子集合的凸包上的范数:
非凸正则与硬阈值
MCP 与 SCAD 在超过某个阈值之后把惩罚变为常数(不再继续加压),因此对大系数无偏。两者的近端算子都可用分段公式计算,仍然可以嵌入近端梯度框架,代价是非凸目标只保证收敛到局部最优或稳定点。
次模与贪心
离散的稀疏子集选择有一类可证的近似算法。若目标函数
工程实践
把本节方法用于实际问题时,四条经验覆盖大部分情况。
尺度与标准化。
正则参数的选择。
方法的匹配。
精度的合理目标。
3.7.6 本节小结
非光滑问题的四个工具
本节把非光滑优化组织为「光滑部分 + 简单非光滑部分」的框架。近端算子
非光滑优化的工具线到此完成。下一节换一个问题域:前面所有收敛性结论都以凸性为条件,机器学习的目标函数几乎全部非凸,3.8 节讨论非凸情形下哪些结论仍然成立、哪些现象需要新的解释框架。
练习题
第 1 题 概念推导
证明近端算子的最优性条件:
参考答案
最优性条件:
不动点刻画:若
软阈值:
零的产生:
第 2 题 计算推理
计算下列近端算子的取值:(a)
参考答案
(a) 软阈值逐分量:
(b) 块软阈值:组范数
(c) 硬阈值:阈值
(d) Moreau 包络是近端问题的最优值:先算
也可以用解析梯度核对:
第 3 题 代码验证
用 3.7.2 节的代码做三组实验并解释:(a) 把近端梯度步长从
参考答案
import numpy as np
def soft(v, t):
return np.sign(v) * np.maximum(np.abs(v) - t, 0.0)
rng = np.random.default_rng(0)
n, d, s = 200, 150, 30
Z = rng.normal(size=(n, d))
X = Z.copy()
for j in range(1, d):
X[:, j] = 0.9 * X[:, j - 1] + np.sqrt(1 - 0.81) * Z[:, j]
X /= np.sqrt(n)
w_true = np.zeros(d)
idx = rng.choice(d, s, replace=False)
w_true[idx] = rng.normal(size=s) * 2
y = X @ w_true + 0.1 * rng.normal(size=n)
L = float(np.linalg.eigvalsh(X.T @ X).max())
grad = lambda w: X.T @ (X @ w - y)
# (a) 步长扫描
lam = 0.05 * float(np.abs(X.T @ y).max())
def obj(w):
return 0.5 * float(np.sum((X @ w - y) ** 2)) + lam * float(np.sum(np.abs(w)))
for factor in [1.0, 2.0, 2.5]:
w = np.zeros(d)
hist = []
for k in range(201):
if k in (0, 10, 50, 200):
hist.append((k, obj(w)))
w = soft(w - factor * grad(w) / L, factor * lam / L)
print(f"步长 {factor}/L: " + " ".join(f"k={k}: {v:.4e}" for k, v in hist))
# (b) FISTA 不重启动
w = np.zeros(d); w_prev = w.copy(); theta = 1.0
vals = []
for k in range(601):
w_new = soft(w - grad(w) / L, lam / L)
theta_new = 0.5 * (1 + np.sqrt(1 + 4 * theta ** 2))
w = w_new + ((theta - 1) / theta_new) * (w_new - w_prev)
theta = theta_new
w_prev = w_new
vals.append(obj(w))
print(f"FISTA(不重启动): k=100: {vals[100]:.10f}, k=200: {vals[200]:.10f}, "
f"k=400: {vals[400]:.10f}, k=600: {vals[600]:.10f}")
print(f" 上升次数 = {int(np.sum(np.diff(vals) > 0))}, "
f"最大上升量 = {np.max(np.maximum(np.diff(vals), 0)):.3e}")
# (c) lambda 扫描
for ratio in [0.05, 0.2, 0.5]:
lam = ratio * float(np.abs(X.T @ y).max())
w_cd = np.zeros(d); cols = np.sum(X ** 2, axis=0)
for sweep in range(3000):
w_old = w_cd.copy()
for j in range(d):
r = y - X @ w_cd + X[:, j] * w_cd[j]
rho = X[:, j] @ r
w_cd[j] = np.sign(rho) * max(abs(rho) - lam, 0.0) / cols[j]
if np.max(np.abs(w_cd - w_old)) < 1e-13:
break
nz = np.abs(w_cd) > 1e-10
overlap = len(set(np.nonzero(nz)[0]) & set(idx))
print(f"lambda = {ratio:.2f} * lambda_max = {lam:.6f}: 目标 = {obj(w_cd):.6f}, "
f"非零个数 = {int(nz.sum())}, 真实支撑重合 = {overlap}/{s}, 轮数 = {sweep}")输出:
步长 1.0/L: k=0: 5.1418e+01 k=10: 1.6209e+01 k=50: 1.2085e+01 k=200: 1.0495e+01
步长 2.0/L: k=0: 5.1418e+01 k=10: 1.4009e+01 k=50: 1.1089e+01 k=200: 1.0219e+01
步长 2.5/L: k=0: 5.1418e+01 k=10: 3.0796e+02 k=50: 3.2209e+13 k=200: 2.1631e+66
FISTA(不重启动): k=100: 10.1691783668, k=200: 10.1672728877, k=400: 10.1670643186, k=600: 10.1670608225
上升次数 = 193, 最大上升量 = 6.512e-03
lambda = 0.05 * lambda_max = 0.247763: 目标 = 10.167059, 非零个数 = 34, 真实支撑重合 = 18/30, 轮数 = 259
lambda = 0.20 * lambda_max = 0.991050: 目标 = 28.652776, 非零个数 = 22, 真实支撑重合 = 14/30, 轮数 = 187
lambda = 0.50 * lambda_max = 2.477625: 目标 = 44.784009, 非零个数 = 9, 真实支撑重合 = 4/30, 轮数 = 138结论:(a) 近端梯度法对步长的容忍度是
(b) 不重启动的 FISTA 目标值有
(c)
第 4 题 综合应用
一个推荐系统要对
参考答案
算法选择:核范数的近端算子(奇异值软阈值,也称为 SVT)有闭式解,问题结构正好是「光滑项 + 近端可算的非光滑项」,因此近端梯度法与 ADMM 都适用。推荐用近端梯度或它的加速版本:每步只需要一次部分观测上的梯度计算与一次(截断)SVD;ADMM 的另一个子问题是投影到观测约束,收敛快但需要调
每步的核心运算:梯度项
停止判据:用相对目标值变化与近端梯度映射的范数
核范数的近端算子:
三个工程要点:对观测位置做归一化与偏差处理(用户与物品的固定效应先移除,剩下的部分才适合低秩假设);
常见错误
错误 1 · 用次梯度法求解有近端算子的问题
原因:次梯度法对非光滑问题的形式统一,但速率是
解决:只要
错误 2 · 把 FISTA 的目标值当作单调下降
原因:动量外推使 FISTA 的目标值在接近最优解时会上升(练习题中
解决:启用自适应重启动(外推方向与梯度方向不再同向时重置动量),或者维护目标值的历史最小值作为输出与判据。报告结果时同时给出重启动触发的次数,它是问题条件数的一个间接指标。
错误 3 · ADMM 只看原始残差或把
原因:原始残差度量约束违反,对偶残差度量最优性(乘子收敛)。只检查前者会把「解在约束面上但乘子未收敛」的状态当成收敛,此时输出的对偶信息(下界、灵敏度)不可用。
解决:两类残差一起检查,并用相对判据(按数据尺度缩放)。
错误 4 · 忽略
原因:软阈值对超过阈值的分量也做平移(
解决:使用 debiasing 两步法:先用 Lasso(或非凸正则)选支撑集,再在选出的支撑集上做无正则的最小二乘重新估计系数;或者用 MCP、SCAD 这类对大幅值无偏的非凸惩罚。报告结果时区分「选出的支撑集」与「系数估计」两个层面的精度,两者的误差来源与量级不同。