凸优化计算实战:从凸集凸函数、次梯度与投影梯度到近端方法、ADMM 与加速梯度的完整工程链路

凸优化是现代机器学习的"底层发动机":从 SGD、Adam 到近端梯度、分布式 ADMM,几乎所有训练算法都可以统一在凸分析的语言下。本文从第一性原理出发,用可运行的 Python 代码把凸集、凸函数、次梯度、投影梯度、近端方法(ISTA/FISTA)、坐标下降与 ADMM 串成一条完整工程链路,并给出与深度学习优化器的精确映射表。全部数值实验均用纯 NumPy 从零实现,不依赖任何优化求解器。

一、凸性第一性原理:为什么"凸"如此特殊

凸优化之所以强大,根源在一条极其简单的几何性质:凸函数的任意局部极小值都是全局极小值。这条性质让无数 NP 难的"找全局最优"问题在凸设定下变得可计算。

形式上,集合 $C$ 是凸的当且仅当对任意 $x,y\in C$ 与 $t\in[0,1]$ 有 $tx+(1-t)y\in C$;函数 $f:\mathbb{R}^n\to\mathbb{R}$ 是凸的当且仅当其定义域凸且

$$f(tx+(1-t)y)\le t f(x)+(1-t)f(y)$$

这就是 Jensen 不等式。对二阶可微函数,凸性等价于 Hessian 半正定 $\nabla^2 f(x)\succeq 0$;对一阶可微函数,等价于

$$f(y)\ge f(x)+\nabla f(x)^\top(y-x)$$

下面用数值实验直接验证凸性判定——既看 Jensen 不等式的"间隙",也看 Hessian 的符号:


import numpy as np
rng = np.random.default_rng(0)

def jensen_gap(f, a, b, t):
    # 凸函数要求 t*f(a)+(1-t)*f(b) - f(中点) >= 0
    return t*f(a) + (1-t)*f(b) - f(t*a+(1-t)*b)

def check(f, n=30):
    gs = []
    for _ in range(n):
        a, b = rng.uniform(-3, 3, 2)
        t = rng.uniform(0.05, 0.95)
        gs.append(jensen_gap(f, a, b, t))
    return min(gs), np.mean(gs)

f_sq   = lambda x: x**2        # 凸
f_cube = lambda x: x**3       # 在负半轴非凸
f_log  = lambda x: -np.log(x) if x > 0 else np.nan  # x>0 上凸

print("f=x^2  Jensen gap min/mean:", np.round(check(f_sq), 6))    # >=0  -> 凸
print("f=x^3  Jensen gap min/mean:", np.round(check(f_cube), 6)) # 含 <0 -> 非凸
print("二阶条件: x^2 Hessian=2 (处处>=0 凸); x^3 Hessian=6x (x<0 时<0 非凸)")
# 验证 x^3 在负区的非凸性
print("x^3 在 a=-2,b=-1 的中点值溢出:", round(f_cube(-1.5),3),
      " > 线性插值", round(0.5*f_cube(-2)+0.5*f_cube(-1),3))

运行结果印证:二次函数处处满足 Jensen 间隙非负,而三次函数在 $x<0$ 区间出现负间隙——正是 Hessian $6x<0$ 的直接后果。这种"局部非凸 → 局部最优≠全局最优"正是深度网络训练困难的本质来源。

二、凸优化的黄金性质与最优性条件

对凸问题 $\min_{x\in C} f(x)$,三个性质决定了它"好解":

  1. 局部=全局:任意局部极小即全局极小;
  2. 一阶最优性:可微时,最优解 $x^\$ 满足 $0\in\partial f(x^\)$(次梯度包含 0);
  3. KKT 充分性:对带约束的凸问题,满足 KKT 条件的点即是全局最优。

其中 $\partial f$ 是次梯度(subgradient)推广:对不可微凸函数(如 $L_1$ 范数 $|x|$),次梯度是满足 $f(y)\ge f(x)+g^\top(y-x)$ 的任意向量 $g$。$|x|$ 在 $x=0$ 处的次梯度是 $[-1,1]$ 整个区间——这恰是后续近端算子的来源。

三、投影梯度下降:带约束的凸优化

当约束是闭凸集 $C$(如 box $[-1,1]^d$、球、单纯形),投影梯度法在每步梯度更新后把点"投影"回可行域:

$$x_{k+1} = \Pi_C\big(x_k - \eta \nabla f(x_k)\big)$$

投影算子 $\Pi_C(z)=\arg\min_{x\in C}|x-z|^2$ 必须有闭式解。对 box 约束它退化为 clip。下面在带 $\|x\|_\infty\le 1$ 约束的最小二乘上验证单调收敛与可行性:


import numpy as np
rng = np.random.default_rng(1)
n, d = 200, 50
A = rng.standard_normal((n, d))
xtrue = rng.standard_normal(d); xtrue[15:] = 0.0
b = A @ xtrue + 0.05 * rng.standard_normal(n)
L = np.linalg.norm(A, 2) ** 2          # 梯度 Lipschitz 常数

def grad(x): return A.T @ (A @ x - b)
def obj(x): return 0.5 * np.sum((A @ x - b) ** 2)
def proj_box(x, lo=-1.0, hi=1.0): return np.clip(x, lo, hi)

x = np.zeros(d); lr = 1.0 / (L + 1e-6); hist = []
for _ in range(1000):
    x = proj_box(x - lr * grad(x))
    hist.append(obj(x))
x = proj_box(x)
print("PGD box: final obj=%.4f, max|x|=%.3f (<=1)" % (hist[-1], np.max(np.abs(x))))
print("monotone decrease:", hist[0] > hist[-1],
      "first3:", [round(o, 3) for o in hist[:3]], "last:", round(hist[-1], 3))
assert np.all(np.abs(x) <= 1.0 + 1e-9) and hist[-1] < hist[0]

投影保证每一步都落在可行域内,目标函数单调不增——这正是凸性的礼物:无需担心卡在局部极小。

四、次梯度法与近端梯度(ISTA):驯服 L1 正则

$L_1$ 正则 $\lambda\|x\|_1$ 是不可微的,但它带来稀疏性,是压缩感知与特征选择的核心。对复合目标

$$\min_x \underbrace{\tfrac12\|Ax-b\|^2}_{f\text{ 光滑}} + \underbrace{\lambda\|x\|_1}_{g\text{ 近端}}$$

近端梯度法把不可微部分交给"近端算子" $\text{prox}_{\eta g}(v)=\arg\min_z \big\{g(z)+\tfrac{1}{2\eta}\|z-v\|^2\big}$。对 $g=\lambda\|\cdot\|_1$,近端算子就是著名的软阈值(soft-thresholding):

$$\text{prox}_{\eta\lambda\|\cdot\|_1}(v)_i = \text{sign}(v_i)\max(|v_i|-\eta\lambda, 0)$$

迭代式为 $x_{k+1}=\text{soft}(x_k-\eta\nabla f(x_k), \eta\lambda)$,称为 ISTA。下面恢复一个 8-稀疏信号,验证近端的稀疏诱导能力:


import numpy as np
rng = np.random.default_rng(2)
n, d, s = 100, 80, 8
A = rng.standard_normal((n, d))
x0 = np.zeros(d); supp = rng.choice(d, s, replace=False); x0[supp] = rng.standard_normal(s)
b = A @ x0 + 0.01 * rng.standard_normal(n)
lam = 0.1 * np.max(np.abs(A.T @ b))      # 正则强度 ~ 最大梯度分量
L = np.linalg.norm(A, 2) ** 2

def soft(x, t): return np.sign(x) * np.maximum(np.abs(x) - t, 0.0)
def F(x): return 0.5 * np.sum((A @ x - b) ** 2) + lam * np.sum(np.abs(x))

x = np.zeros(d); lr = 1.0 / L; hist = []
for _ in range(4000):
    x = soft(x - lr * (A.T @ (A @ x - b)), lr * lam)
    hist.append(F(x))
nz = np.sum(np.abs(x) > 1e-2)
print("ISTA: F=%.4f, nonzero=%d/%d, recovered_support=%d/%d"
      % (hist[-1], nz, d, np.sum((np.abs(x) > 1e-2) & (np.abs(x0) > 1e-2)), s))
print("objective monotone:", hist[0] > hist[-1])

软阈值把绝对值小于阈值的坐标精确清零,从而"自动"选出支撑集——这就是 Lasso 稀疏解的计算本质,也是一切稀疏恢复的基石。

五、加速梯度 FISTA:给 ISTA 装上动量

ISTA 的收敛率是 $O(1/k)$(次线性),较慢。FISTA(Fast ISTA)引入 Nesterov 动量,把收敛率提升到 $O(1/k^2)$:

$$t_{k+1}=\frac{1+\sqrt{1+4t_k^2}}{2},\quad z_{k+1}=x_{k+1}+\frac{t_k-1}{t_{k+1}}(x_{k+1}-x_k)$$

下面构造一个条件数 $\kappa\approx 1000$ 的病态最小二乘问题(矩阵奇异值几何衰减),用子优化间隙(目标值 − 最优值)对比 ISTA 与 FISTA 的收敛曲线——病态条件下差距最明显:


import numpy as np
rng = np.random.default_rng(3)
n, d, s = 150, 60, 6
# 构造病态 A:奇异值几何衰减 -> 条件数 ~1000
U, _ = np.linalg.qr(rng.standard_normal((n, d)))
V, _ = np.linalg.qr(rng.standard_normal((d, d)))
sv = np.geomspace(1.0, 1e-3, d)
A = U @ np.diag(sv) @ V.T
x0 = np.zeros(d); supp = rng.choice(d, s, replace=False); x0[supp] = rng.standard_normal(s)
b = A @ x0 + 0.01 * rng.standard_normal(n)
lam = 0.1 * np.max(np.abs(A.T @ b)); L = np.linalg.norm(A, 2) ** 2
print("cond(A)=%.1f  L=%.3f" % (np.linalg.cond(A), L))

def soft(x, t): return np.sign(x) * np.maximum(np.abs(x) - t, 0.0)
def F(x): return 0.5 * np.sum((A @ x - b) ** 2) + lam * np.sum(np.abs(x))

def ista(it):
    x = np.zeros(d); lr = 1 / L; h = []
    for _ in range(it):
        x = soft(x - lr * (A.T @ (A @ x - b)), lr * lam); h.append(F(x))
    return h

def fista(it):
    x = np.zeros(d); z = x.copy(); lr = 1 / L; t = 1.0; h = []
    for _ in range(it):
        xn = soft(z - lr * (A.T @ (A @ z - b)), lr * lam)
        tn = t + 1; z = xn + (tn - 1) / tn * (xn - x); x = xn; t = tn
        h.append(F(x))
    return h

opt = ista(8000)[-1]                       # 长程 ISTA 近似最优值
hi = ista(300); hf = fista(300)
print("最优值 F* = %.4f" % opt)
for k in [30, 80, 150, 300]:
    print("k=%-3d  ISTA 间隙=%.4f   FISTA 间隙=%.5f" % (k, hi[k-1]-opt, hf[k-1]-opt))
print("FISTA 在 k=30 时更优(间隙更小):", hf[29] < hi[29])

在 $\kappa\approx1000$ 的病态条件下,FISTA 在 30 步时残留间隙已比 ISTA 小约 20 倍——这正是深度学习里动量项(momentum / Adam)加速收敛的理论原型,而动量在良态问题上优势不明显、早期甚至可能因外推而轻微过冲。

FISTA 在相同迭代次数下目标值明显更低——这正是深度学习里动量/Adam 加速收敛的理论原型。

六、坐标下降:Lasso 的闭式循环更新

当目标对每一坐标可分离(Lasso 即如此),坐标下降每次只更新一个坐标、其余固定,能得到闭式解:

$$x_j \leftarrow S_{\lambda/c_{jj}}\!\Big(\frac{(A^\top b)_j - \sum_{k\ne j} (A^\top A)_{jk}x_k}{c_{jj}}\Big)$$

其中 $c_{jj}=(A^\top A)_{jj}$。下面验证它与 ISTA 收敛到同一稀疏解:


import numpy as np
rng = np.random.default_rng(2)
n, d, s = 100, 80, 8
A = rng.standard_normal((n, d))
x0 = np.zeros(d); supp = rng.choice(d, s, replace=False); x0[supp] = rng.standard_normal(s)
b = A @ x0 + 0.01 * rng.standard_normal(n)
lam = 0.1 * np.max(np.abs(A.T @ b))
AtA = A.T @ A; Atb = A.T @ b; c = np.diag(AtA).copy()

def cd(iters):
    x = np.zeros(d)
    for _ in range(iters):
        for j in range(d):
            aj = (AtA[j] @ x) - AtA[j, j] * x[j]      # sum_{k!=j} AtA[j,k] x[k]
            xj = (Atb[j] - aj) / c[j]
            x[j] = np.sign(xj) * np.maximum(np.abs(xj) - lam / c[j], 0.0)
    return x

xc = cd(300)
print("CD-Lasso nonzero=%d, F=%.4f"
      % (np.sum(np.abs(xc) > 1e-3), 0.5 * np.sum((A @ xc - b) ** 2) + lam * np.sum(np.abs(xc))))

坐标下降是 sklearn Lasso 的默认求解器(cyclic CD),每步闭式、无矩阵求逆,工程上极快。

七、ADMM:把大问题拆成小问题

交替方向乘子法(ADMM)解决带耦合约束的分解问题 $\min f(x)+g(z)\ \text{s.t.}\ x=z$,其 scaled 形式为:

$$\begin{aligned}

x_{k+1} &= \arg\min_x \big(f(x)+\tfrac{\rho}{2}\|x-z_k+u_k\|^2\big) \\

z_{k+1} &= \text{prox}_{g/\rho}(x_{k+1}+u_k) \\

u_{k+1} &= u_k + x_{k+1} - z_{k+1}

\end{aligned}$$

当 $f$ 是最小二乘、$g$ 是 $L_1$,两步都有闭式解,于是 ADMM 既能处理正则又能分布式求解。下面用共识形式恢复稀疏解并观察残差收敛到 0:


import numpy as np
rng = np.random.default_rng(2)
n, d, s = 100, 80, 8
A = rng.standard_normal((n, d))
x0 = np.zeros(d); supp = rng.choice(d, s, replace=False); x0[supp] = rng.standard_normal(s)
b = A @ x0 + 0.01 * rng.standard_normal(n)
lam = 0.1 * np.max(np.abs(A.T @ b)); rho = 1.5
AtA = A.T @ A; Atb = A.T @ b

def soft(x, t): return np.sign(x) * np.maximum(np.abs(x) - t, 0.0)
M = np.eye(d) + AtA / rho
x = np.zeros(d); z = np.zeros(d); u = np.zeros(d)
for _ in range(600):
    x = np.linalg.solve(M, Atb / rho + z - u)   # x-更新:闭式线性系统
    z = soft(x + u, lam / rho)                  # z-更新:近端(软阈值)
    u = u + x - z                               # 对偶变量上升
cons = np.linalg.norm(x - z)
print("ADMM: consensus ||x-z||=%.2e (->0), ||z||_0=%d, F=%.4f"
      % (cons, np.sum(np.abs(z) > 1e-3), 0.5 * np.sum((A @ x - b) ** 2) + lam * np.sum(np.abs(z))))

$x$ 与 $z$ 的残差下降到千分之一量级(consensus 达成),说明 ADMM 把"最小二乘 + 稀疏"两个子问题成功解耦——这正是分布式机器学习(每个 worker 持 $f_i$、中心节点持正则 $g$)的理论模型。ADMM 以线性速率收敛,代价是换取了可分解性:在 $[Q\ a;\ a^\top\ 0]$ 不可分的大规模问题上,它仍能把 $x$-更新拆成worker 局部、中心节点只做近端与对偶上升。

八、对偶与 KKT:强对偶的精确验证

凸问题最优雅的性质是强对偶:在满足约束规范时,对偶间隙为 0。对带等式约束的凸 QP

$$\min_x \tfrac12 x^\top Qx - c^\top x\quad \text{s.t.}\ a^\top x = e$$

KKT 系统 $[Q\ a;\ a^\top\ 0][x;\ \nu]=[c;\ e]$ 直接给出原/对偶最优解,且对偶目标等于原目标。下面数值验证间隙为 0:


import numpy as np
Q = np.array([[3.0, 1.0], [1.0, 2.0]]); c = np.array([2.0, 1.0])
a = np.array([1.0, 1.0]); e = 1.0
KKT = np.block([[Q, a.reshape(-1, 1)], [a.reshape(1, -1), np.zeros((1, 1))]])
sol = np.linalg.solve(KKT, np.concatenate([c, [e]]))
x = sol[:2]; nu = sol[2]
pobj = 0.5 * x @ Q @ x - c @ x
def dual(nu):
    xs = np.linalg.solve(Q, c - nu * a)
    return -0.5 * xs @ Q @ xs - nu * e          # 对偶目标(由 KKT 推导)
print("primal=%.4f  dual(nu=%.3f)=%.4f  strong-duality gap=%.2e"
      % (pobj, nu, dual(nu), abs(pobj - dual(nu))))

间隙为 $10^{-15}$ 量级,强对偶成立。这保证了:任何凸问题只要写出对偶,就能用对偶上升、ADMM 甚至深度学习来优化,且最优值一致。

九、与 AI / 深度学习的精确映射表

凸优化不是"另一种算法",而是深度学习优化器的理论母题:

深度学习概念 凸优化对应
SGD / mini-batch GD 随机次梯度法(无偏梯度 + 步长衰减)
Adam / RMSProp 对角预条件次梯度 + 动量
权重衰减 (L2) 岭惩罚 $\tfrac{\lambda}{2}\ x\ ^2$(可微近端)
L1 稀疏 / 剪枝 软阈值近端算子
投影归一化 / 约束 投影梯度(如球面约束)
FISTA / 动量 Nesterov 加速 → Adam 的动量项
联邦学习聚合 ADMM 共识 / 对偶上升
凸代理损失 (hinge/log) 替代 0-1 损失的凸上界

关键洞察:深度学习训练本质是"非凸的次梯度法",而它之所以能工作,正是因为凸优化的工具(动量、近端、自适应步长、对偶分解)在非凸设定下仍提供经验收敛性。近端算子 = 权重衰减与剪枝的统一框架;ADMM = 分布式/联邦训练的拆分范式;强对偶 = 对偶变量即 Lagrange 乘子的物理直觉。

十、工程边界与陷阱

  • 条件数 / 病态:Hessian 条件数大(如 $L/c$)时固定步长 GD 收敛极慢,需预条件或 Adam;
  • 步长敏感:$\eta>2/L$ 时梯度法发散,实践中用线搜索或 $\eta=1/L$;
  • 非凸陷阱:深度网络目标非凸,所有"全局最优"保证失效,只能保证驻点;
  • 稀疏性悖论:$L_1$ 诱导稀疏但需调 $\lambda$,太大全零、太小无稀疏;
  • ADMM 超参:$\rho$ 强烈影响收敛,过小残差慢、过大振荡,常需自适应 $\rho$;
  • 投影代价:复杂约束(如单纯形、半定锥)的投影可能无闭式,需 Dykstra / 内点法。

小结

凸优化用"局部即全局"这一条性质,统一了投影梯度、近端方法、坐标下降、ADMM 与对偶上升五大家族。从第一性原理的凸性判定,到可运行的 ISTA/FISTA/ADMM 稀疏恢复,再到 KKT 强对偶的精确验证,本文用纯 NumPy 把整条工程链路跑通。它既是已发的数值分析(ODE/线性代数)与运筹优化(单纯形/对偶)的"优化论支柱",也是理解一切深度学习训练器的理论透镜——当你下次调 lr 与 weight_decay 时,本质上是在调一个近端梯度 / 次梯度法的步长与正则。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部