3.6 约束优化与对偶理论
前五节的算法都假设变量可以取遍整个空间:最优性条件是梯度为零,迭代方向由模型的梯度或曲率决定。实际问题中变量几乎总带限制:概率单纯形上的权重和为
约束的处理有两条路线。第一条把约束吸收进目标函数:罚函数在违反约束时增加代价,对数障碍在接近边界时趋于无穷,增广拉格朗日把罚函数与乘子估计组合起来。转化之后可以用无约束方法求解,代价是引入的参数需要控制。第二条把约束保留在算法内部:单纯形沿多面体的棱移动,积极集方法在边界面之间切换,内点法在可行域内部沿中心路径逼近边界。
两条路线由对偶理论统一起来。约束的数量决定对偶问题中变量的个数,每个约束配一个乘子(multiplier),乘子度量约束对最优值的边际影响。对偶问题给出最优值的下界,这一下界既用于算法终止判据,也用于灵敏度分析;对偶问题的目标函数始终是凹的,即便原问题非凸。本节按这一顺序展开:先给出拉格朗日函数与 KKT 条件,再建立对偶理论,然后讨论三类把约束转化为无约束的方法,最后回到线性规划与锥规划这两个结构最清楚的问题类。
3.6.1 约束问题与拉格朗日函数
问题的写法
约束优化问题的标准形式为
可行域
变量的取值限制可以写成显式约束,也可以在算法中隐式处理。概率单纯形
拉格朗日函数
把约束加权并入目标函数,得到拉格朗日函数(Lagrangian):
其中
乘子的意义通过一个可以手算的例子显出。考虑
拉格朗日函数为
于是
对偶函数与对偶问题
固定乘子,对
对偶函数给出的值是原问题最优值的下界,对偶问题是在乘子上取这个下界的最大值:
在上面的例子中,把
这是
约束的几何意义
最优性条件可以用梯度表达。在最优解处,目标函数沿任何可行方向的导数非负(否则可以沿该方向下降)。可行方向构成一个锥:所有与活跃约束的梯度满足
其中
投影是约束优化的特例
投影问题
3.6.2 KKT 条件
一阶必要条件
把上节的几何条件写成代数形式,得到 Karush-Kuhn-Tucker 条件(KKT 条件):
定理的表述:
互补松弛(complementary slackness)把积极性与乘子绑定:
约束规范
必要条件成立的前提是约束规范(constraint qualification)。常用的三个:
Slater 条件适用于凸问题:
LICQ(线性无关约束规范):活跃约束与等式约束的梯度
MFCQ:存在方向使所有活跃不等式约束的梯度严格减少(
约束规范失效时 KKT 条件可能找不到乘子。典型例子是
凸情形的充分性
对凸问题,KKT 条件从必要转为充分:
最后一步用了
数值验证
下面的代码在二维问题上验证 KKT 条件,并实现积极集枚举。问题为
两个约束写成
import numpy as np
# 问题:min (x1-2)^2 + (x2-2)^2 s.t. x1 + x2 <= 2, x1 <= 0.5
def f(x):
return float((x[0] - 2) ** 2 + (x[1] - 2) ** 2)
def grad(x):
return np.array([2 * (x[0] - 2), 2 * (x[1] - 2)])
def g(x):
return np.array([x[0] + x[1] - 2, x[0] - 0.5])
# 在解析解 x* = (0.5, 1.5)、lambda* = (1, 2) 处检验 KKT 四组条件
x_star, lam = np.array([0.5, 1.5]), np.array([1.0, 2.0])
A = np.array([[1.0, 1.0], [1.0, 0.0]]) # 各约束的梯度
print("平稳性残差:", np.linalg.norm(grad(x_star) + A.T @ lam))
print("原始可行性 (max g):", g(x_star).max())
print("对偶可行性 (min lambda):", lam.min())
print("互补松弛 (max |lambda_i g_i|):", np.abs(lam * g(x_star)).max())
# 积极集枚举:对每个候选积极集解 KKT 系统,检查可行性
H, c = 2 * np.eye(2), np.array([-4.0, -4.0])
b_con = np.array([2.0, 0.5])
for S in [(), (0,), (1,), (0, 1)]:
if len(S) == 0:
x = -np.linalg.solve(H, c)
print(f"积极集 {S}: x = ({x[0]:.4f}, {x[1]:.4f}), 可行 = {(g(x) <= 1e-12).all()}")
else:
As = A[list(S)]
K = np.block([[H, As.T], [As, np.zeros((len(S), len(S)))]])
sol = np.linalg.solve(K, np.concatenate([-c, b_con[list(S)]]))
x, mu = sol[:2], sol[2:]
mu_str = ", ".join(f"{v:.4f}" for v in mu)
print(f"积极集 {S}: x = ({x[0]:.4f}, {x[1]:.4f}), 乘子 = ({mu_str}), "
f"可行 = {(g(x) <= 1e-12).all()}, 乘子非负 = {(mu >= -1e-12).all()}")输出:
平稳性残差: 0.0
原始可行性 (max g): 0.0
对偶可行性 (min lambda): 1.0
互补松弛 (max |lambda_i g_i|): 0.0
积极集 (): x = (2.0000, 2.0000), 可行 = False
积极集 (0,): x = (1.0000, 1.0000), 乘子 = (2.0000), 可行 = False, 乘子非负 = True
积极集 (1,): x = (0.5000, 2.0000), 乘子 = (3.0000), 可行 = False, 乘子非负 = True
积极集 (0, 1): x = (0.5000, 1.5000), 乘子 = (1.0000, 2.0000), 可行 = True, 乘子非负 = True四组 KKT 残差全为零,确认
注意积极集
3.6.3 对偶理论
弱对偶
对偶函数在可行域上不超过原问题最优值:对任意可行
第二个不等号用到
对偶函数的凹性与次梯度
无论原问题是否凸,
右端关于
强对偶与 Slater 条件
强对偶(strong duality)指
鞍点与极小极大
若
则称该三元组是
左端就是
灵敏度与影子价格
把约束右端视为参数,定义扰动问题的最优值
读法:把第
对偶上升与数值实验
对偶问题的最直接解法是对偶函数的投影梯度上升。由于
其中
import numpy as np
# 沿用上例:min (x1-2)^2 + (x2-2)^2 s.t. x1 + x2 <= 2, x1 <= 0.5
H, c = 2 * np.eye(2), np.array([-4.0, -4.0])
A = np.array([[1.0, 1.0], [1.0, 0.0]])
b_con = np.array([2.0, 0.5])
def f(x):
return float((x[0] - 2) ** 2 + (x[1] - 2) ** 2)
def g(x):
return np.array([x[0] + x[1] - 2, x[0] - 0.5])
def dual_fun(lam):
lam = np.asarray(lam, dtype=float)
x = -np.linalg.solve(H, c + A.T @ lam) # argmin_x L(x, lambda)
return float(f(x) + lam @ g(x)), x
# 投影梯度上升:lambda <- [lambda + alpha * g(x(lambda))]_+
lam = np.zeros(2)
for k in range(201):
val, x = dual_fun(lam)
if k % 50 == 0:
print(f"k = {k:>3}: lambda = ({lam[0]:.4f}, {lam[1]:.4f}), q = {val:.6f}, "
f"f(x) = {f(x):.6f}, 与最优值的差 = {2.5 - val:.3e}")
lam = np.maximum(0.0, lam + 0.5 * g(x))
print(f"最终: lambda = ({lam[0]:.8f}, {lam[1]:.8f}), q = {dual_fun(lam)[0]:.10f}")
# 对偶函数在网格上的取值(展示凹性与峰值位置)
print("对偶函数网格(行 lambda1,列 lambda2):")
print("lambda1\\lambda2 " + "".join(f"{v:>10.4f}" for v in [0, 1, 2, 3]))
for l1 in [0.0, 0.5, 1.0, 1.5, 2.0, 3.0]:
print(f"{l1:>13.1f} " + "".join(f"{dual_fun((l1, l2))[0]:>10.4f}" for l2 in [0, 1, 2, 3]))
# 约束规范失效的例子:min x s.t. x^2 <= 0
print("min x s.t. x^2 <= 0:")
for lam_v in [1.0, 10.0, 100.0, 1e4, 1e8]:
print(f"lambda = {lam_v:>8.1e}: q = {-1 / (4 * lam_v):.8f}, 与最优值的差 = {1 / (4 * lam_v):.2e}")输出:
k = 0: lambda = (0.0000, 0.0000), q = 0.000000, f(x) = 0.000000, 与最优值的差 = 2.500e+00
k = 50: lambda = (1.0041, 1.9934), q = 2.499994, f(x) = 2.498259, 与最优值的差 = 5.777e-06
k = 100: lambda = (1.0000, 2.0000), q = 2.500000, f(x) = 2.499988, 与最优值的差 = 2.529e-10
k = 150: lambda = (1.0000, 2.0000), q = 2.500000, f(x) = 2.500000, 与最优值的差 = 1.066e-14
k = 200: lambda = (1.0000, 2.0000), q = 2.500000, f(x) = 2.500000, 与最优值的差 = 0.000e+00
最终: lambda = (1.00000000, 2.00000000), q = 2.5000000000
对偶函数网格(行 lambda1,列 lambda2):
lambda1\lambda2 0.0000 1.0000 2.0000 3.0000
0.0 0.0000 1.2500 2.0000 2.2500
0.5 0.8750 1.8750 2.3750 2.3750
1.0 1.5000 2.2500 2.5000 2.2500
1.5 1.8750 2.3750 2.3750 1.8750
2.0 2.0000 2.2500 2.0000 1.2500
3.0 1.5000 1.2500 0.5000 -0.7500
min x s.t. x^2 <= 0:
lambda = 1.0e+00: q = -0.25000000, 与最优值的差 = 2.50e-01
lambda = 1.0e+01: q = -0.02500000, 与最优值的差 = 2.50e-02
lambda = 1.0e+02: q = -0.00250000, 与最优值的差 = 2.50e-03
lambda = 1.0e+04: q = -0.00002500, 与最优值的差 = 2.50e-05
lambda = 1.0e+08: q = -0.00000000, 与最优值的差 = 2.50e-09三段输出可以分别读出三件事。第一段是对偶上升的收敛过程:乘子从零出发,
第二段的网格展示了对偶函数的凹性:沿每一行、每一列,相邻差分单调递减,峰值在
第三段是约束规范失效的例子。
3.6.4 罚函数与增广拉格朗日
二次罚函数
最直接的转化方式是在目标函数中加上违反约束的代价。对等式约束,二次罚函数为
不等式约束用
代价有多大可以用本节的小例子量出。考虑
最优解
精确罚函数
把二次惩罚换成不可微的绝对值惩罚,可以在有限罚参数下得到精确解:
当
增广拉格朗日方法
增广拉格朗日(augmented Lagrangian,也称乘子法)在二次罚中加回乘子项:
迭代分两步:固定
与纯二次罚的差别在乘子项的修正作用:
在例子中,
import numpy as np
# min x1^2 + x2^2 s.t. x1 + x2 = 2,最优解 (1, 1),最优乘子 nu* = -2
print("二次罚函数:min x1^2 + x2^2 + mu/2 (x1+x2-2)^2")
for mu in [1.0, 10.0, 100.0, 1000.0, 10000.0]:
t = mu / (mu + 1) # 由对称性与一阶条件得到
Hp = np.array([[2 + mu, mu], [mu, 2 + mu]])
print(f"mu = {mu:>8.0f}: x = ({t:.6f}, {t:.6f}), 违反量 = {abs(2 * t - 2):.3e}, "
f"条件数 = {np.linalg.cond(Hp):.3e}")
print("L1 精确罚:min x1^2 + x2^2 + sigma |x1+x2-2|")
for sig in [1.0, 1.5, 2.0, 3.0]:
t = min(1.0, sig / 2) # sigma <= 2 时解偏离可行域
print(f"sigma = {sig:>4.1f}: x = ({t:.4f}, {t:.4f}), 违反量 = {abs(2 * t - 2):.3e}")
print("增广拉格朗日(mu = 1 固定):")
mu, nu = 1.0, 0.0
for k in range(51):
t = (2 * mu - nu) / (2 + 2 * mu)
h_val = 2 * t - 2
if k % 10 == 0 or k == 50:
print(f"k = {k:>2}: nu = {nu:>+.6f}, x = ({t:.6f}, {t:.6f}), 违反量 = {h_val:+.3e}")
nu = nu + mu * h_val输出:
二次罚函数:min x1^2 + x2^2 + mu/2 (x1+x2-2)^2
mu = 1: x = (0.500000, 0.500000), 违反量 = 1.000e+00, 条件数 = 2.000e+00
mu = 10: x = (0.909091, 0.909091), 违反量 = 1.818e-01, 条件数 = 1.100e+01
mu = 100: x = (0.990099, 0.990099), 违反量 = 1.980e-02, 条件数 = 1.010e+02
mu = 1000: x = (0.999001, 0.999001), 违反量 = 1.998e-03, 条件数 = 1.001e+03
mu = 10000: x = (0.999900, 0.999900), 违反量 = 2.000e-04, 条件数 = 1.000e+04
L1 精确罚:min x1^2 + x2^2 + sigma |x1+x2-2|
sigma = 1.0: x = (0.5000, 0.5000), 违反量 = 1.000e+00
sigma = 1.5: x = (0.7500, 0.7500), 违反量 = 5.000e-01
sigma = 2.0: x = (1.0000, 1.0000), 违反量 = 0.000e+00
sigma = 3.0: x = (1.0000, 1.0000), 违反量 = 0.000e+00
增广拉格朗日(mu = 1 固定):
k = 0: nu = +0.000000, x = (0.500000, 0.500000), 违反量 = -1.000e+00
k = 10: nu = -1.998047, x = (0.999512, 0.999512), 违反量 = -9.766e-04
k = 20: nu = -1.999998, x = (1.000000, 1.000000), 违反量 = -9.537e-07
k = 30: nu = -2.000000, x = (1.000000, 1.000000), 违反量 = -9.313e-10
k = 40: nu = -2.000000, x = (1.000000, 1.000000), 违反量 = -9.095e-13
k = 50: nu = -2.000000, x = (1.000000, 1.000000), 违反量 = -8.882e-16三组输出对应三种方法的特征。二次罚的违反量按
罚参数的标度
三个例子的罚参数都在
3.6.5 内点法
对数障碍
罚函数与增广拉格朗日处理等式约束很自然,处理不等式约束要在边界处判别积极性。另一种思路是把不等式约束的边界做成一道排斥墙。对数障碍(logarithmic barrier)为
定义在严格可行域
中心路径与对偶界
中心点提供了对偶可行点与间隙的显式估计。障碍问题的一阶条件为
与拉格朗日函数的平稳性条件对照,令
对一般的凸不等式约束,间隙被
路径跟踪与复杂度
由于障碍函数是自协调的(3.2 节),障碍问题的牛顿法有可预测的行为:从上一个中心点出发,对新的
复杂度由外层轮数乘以每轮牛顿步数给出。固定倍增因子时外层轮数为
与 3.2 节、3.5 节的联系
障碍函数是凸分析中自协调函数的主要例子,其自协调参数为
数值实验
用一个三维线性规划演示中心路径。决策问题为
其中
import numpy as np
A = np.array([[2.0, 3.0, 1.0], [4.0, 1.0, 2.0], [3.0, 4.0, 2.0]])
b = np.array([5.0, 11.0, 8.0])
c = np.array([5.0, 4.0, 3.0]) # 最大化 c^T x
def barrier_center(x, t, tol=1e-16, maxit=200):
x = x.copy()
steps = 0
for _ in range(maxit):
s = b - A @ x
gb = -t * c + A.T @ (1 / s) - 1 / x # 障碍问题梯度
Hb = A.T @ np.diag(1 / s ** 2) @ A + np.diag(1 / x ** 2) # 障碍问题 Hessian
d = -np.linalg.solve(Hb, gb)
dec = -gb @ d # 牛顿减量
if dec / 2 < tol:
break
step = 1.0
while (b - A @ (x + step * d)).min() <= 0 or (x + step * d).min() <= 0:
step *= 0.5 # 回退到严格可行域
phi0 = -t * c @ x - np.log(b - A @ x).sum() - np.log(x).sum()
while True:
xn = x + step * d
phi1 = -t * c @ xn - np.log(b - A @ xn).sum() - np.log(xn).sum()
if phi1 <= phi0 + 1e-4 * step * gb @ d:
break
step *= 0.5
x = x + step * d
steps += 1
return x, steps
x = np.array([0.5, 0.5, 0.5]) # 严格可行初始点
total = 0
for t in [1.0, 10.0, 100.0, 1000.0, 1e4, 1e5, 1e6]:
x, st = barrier_center(x, t)
total += st
s = b - A @ x
lam = 1 / (t * s) # 对偶恢复
profit, bound = c @ x, b @ lam
print(f"t = {t:>9.0f}: 利润 = {profit:.9f}, 对偶界 = {bound:.9f}, "
f"间隙 = {bound - profit:.3e} (6/t = {6 / t:.1e}), 牛顿步 = {st}, 累计 = {total}")
print(f" lambda = ({lam[0]:.6f}, {lam[1]:.6f}, {lam[2]:.6f}), "
f"x = ({x[0]:.6f}, {x[1]:.6f}, {x[2]:.6f})")输出:
t = 1: 利润 = 10.283360952, 对偶界 = 16.283360947, 间隙 = 6.000e+00 (6/t = 6.0e+00), 牛顿步 = 6, 累计 = 6
lambda = (1.189729, 0.246352, 0.953106), x = (0.816872, 0.275637, 1.698818)
t = 10: 利润 = 12.662500412, 对偶界 = 13.262500412, 间隙 = 6.000e-01 (6/t = 6.0e-01), 牛顿步 = 8, 累计 = 14
lambda = (0.739358, 0.068670, 1.101293), x = (1.746001, 0.037148, 1.261301)
t = 100: 利润 = 12.969579040, 对偶界 = 13.029579040, 间隙 = 6.000e-02 (6/t = 6.0e-02), 牛顿步 = 7, 累计 = 21
lambda = (0.961257, 0.009636, 1.014662), x = (1.982274, 0.003387, 1.014886)
t = 1000: 利润 = 12.996995828, 对偶界 = 13.002995828, 间隙 = 6.000e-03 (6/t = 6.0e-03), 牛顿步 = 7, 累计 = 28
lambda = (0.996012, 0.000996, 1.001497), x = (1.998323, 0.000334, 1.001349)
t = 10000: 利润 = 12.999699958, 对偶界 = 13.000299958, 间隙 = 6.000e-04 (6/t = 6.0e-04), 牛顿步 = 7, 累计 = 35
lambda = (0.999600, 0.000100, 1.000150), x = (1.999833, 0.000033, 1.000133)
t = 100000: 利润 = 12.999970000, 对偶界 = 13.000029999, 间隙 = 6.000e-05 (6/t = 6.0e-05), 牛顿步 = 7, 累计 = 42
lambda = (0.999960, 0.000010, 1.000015), x = (1.999983, 0.000003, 1.000013)
t = 1000000: 利润 = 12.999997000, 对偶界 = 13.000003002, 间隙 = 6.002e-06 (6/t = 6.0e-06), 牛顿步 = 7, 累计 = 49
lambda = (0.999996, 0.000001, 1.000001), x = (1.999998, 0.000000, 1.000001)数据验证了理论中的三条。间隙与
3.6.6 线性规划与锥规划
标准形与对偶
线性规划(LP)是目标与约束都为线性的问题,形式上可以有多种等价写法。常用的两种是
两者通过引入松弛变量互相转化:
单纯形法
线性规划的可行域是多面体(3.2 节)。单纯形法(simplex method)沿多面体的棱从一个顶点移动到相邻顶点,每次移动使目标值改善,直到没有改善方向。实现上用表格形式跟踪一个基:把
每次迭代(枢轴)包含三步:选一个价格系数为负的列作为入基变量(使目标下降);在该列的正元素中按最小比值法则选出基变量(保证新顶点可行);做行变换把入基列变成单位列。比值平局与价格系数为零的退化情形可能引起循环,Bland 规则(入基取最小下标、出基取对应基变量下标最小者)可以保证终止。
表格的最后一行还免费给出对偶解。目标行在松弛变量下的元素是
单纯形法的复杂度与对偶单纯形
单纯形法的最坏情形复杂度是指数的(Klee 与 Minty 在 1972 年构造了让所有枢轴规则走遍全部顶点的多面体),但实践中的平均性能很好,平滑分析解释了这一点:对数据加微小随机扰动后,期望枢轴次数是多项式量级的。对偶单纯形法在对偶可行(价格系数全非负)而原始不可行处出发,枢轴过程中保持对偶可行性,适用于增加约束或改变右端后的热启动,也是整数规划中割平面法与分支定界求解子问题的标准工具。
多项式时间算法的历史值得记住顺序:椭球法(Khachiyan,1979)首次证明 LP 存在多项式时间算法,但实际性能远不如单纯形法;Karmarkar 的投影尺度法(1984)给出
锥规划
线性规划的对偶结构可以推广到一般的锥规划(conic programming):
SOCP 的实用性可以用一个例子说明。鲁棒最小二乘考虑数据矩阵
用三角不等式与算子范数的界
这条约束是两个范数之和的不等式,可以写成二阶锥形式(引入辅助变量
数值实验
下面用表格形式的单纯形法求解本节的 LP,并读出影子价格。
import numpy as np
A = np.array([[2.0, 3.0, 1.0], [4.0, 1.0, 2.0], [3.0, 4.0, 2.0]])
b = np.array([5.0, 11.0, 8.0])
c = np.array([5.0, 4.0, 3.0]) # 最大化 c^T x
def simplex_tableau(A, b, c, maxit=200):
m, n = A.shape
T = np.zeros((m + 1, n + m + 1))
T[:m, :n] = A
T[:m, n:n + m] = np.eye(m)
T[:m, -1] = b
T[m, :n] = -c # 最大化问题:目标行初始化为 -c
basis = list(range(n, n + m))
hist = []
for _ in range(maxit):
cand = [j for j in range(n + m) if T[m, j] < -1e-10]
if not cand:
break
e = min(cand) # Bland 规则:入基取最小下标
col = T[:m, e]
ratios = [(T[i, -1] / col[i], i) for i in range(m) if col[i] > 1e-10]
if not ratios:
return T, basis, hist, "unbounded"
rmin = min(r for r, _ in ratios)
tie = [i for r, i in ratios if abs(r - rmin) < 1e-12]
lv = min(tie, key=lambda i: basis[i])
T[lv] /= T[lv, e]
for i in range(m + 1):
if i != lv:
T[i] -= T[i, e] * T[lv]
hist.append((e, basis[lv], rmin, T[-1, -1]))
basis[lv] = e
return T, basis, hist, "optimal"
T_fin, basis, hist, status = simplex_tableau(A, b, c)
print("状态:", status, " 枢轴次数:", len(hist))
for k, (e, lv, ratio, z) in enumerate(hist, 1):
print(f"枢轴 {k}: 入基 x{e + 1}, 出基 x{lv + 1}, 比值 = {ratio:.4f}, 目标 = {z:.4f}")
np.set_printoptions(precision=4, suppress=True, linewidth=120)
print("最终表格:")
print(T_fin)
x_s = np.zeros(6)
for i, j in enumerate(basis):
x_s[j] = T_fin[i, -1]
print("最优解:", np.round(x_s[:3], 6), " 松变量:", np.round(x_s[3:], 6))
print("最优值(表格末列):", T_fin[-1, -1], " 直接计算 c^T x =", c @ x_s[:3])
print("影子价格(松变量列):", T_fin[-1, 3:6])输出:
状态: optimal 枢轴次数: 2
枢轴 1: 入基 x1, 出基 x4, 比值 = 2.5000, 目标 = 12.5000
枢轴 2: 入基 x3, 出基 x6, 比值 = 1.0000, 目标 = 13.0000
最终表格:
[[ 1. 2. 0. 2. 0. -1. 2.]
[ 0. -5. 0. -2. 1. 0. 1.]
[ 0. -1. 1. -3. 0. 2. 1.]
[ 0. 3. 0. 1. 0. 1. 13.]]
最优解: [2. 0. 1.] 松变量: [0. 1. 0.]
最优值(表格末列): 13.0 直接计算 c^T x = 13.0
影子价格(松变量列): [1. 0. 1.]两次枢轴之后目标行没有负元素,算法终止。最优解
用灵敏度检验影子价格的读数:把第一个约束右端从
3.6.7 本节小结
约束的三种处理方式
本节围绕约束的处理展开。KKT 条件给出可验证的最优性判据:平稳性、原始可行性、对偶可行性、互补松弛四组条件,在凸问题中是充要条件,在非凸问题中需要约束规范(Slater、LICQ、MFCQ)才能作为必要条件。对偶理论把每个约束转化为一个乘子变量:对偶函数是约束违反量的反馈通道,对偶问题始终凸,弱对偶无条件成立,强对偶在凸性与 Slater 条件下成立,鞍点、极小极大与灵敏度分析是它的三种表达。三类把约束转为无约束的方法各有取舍:二次罚简单但精度与条件数绑定(违反量
约束优化到此完成。下一节处理的问题在结构上与本节的罚函数直接衔接:非光滑的正则项与精确罚如何高效求解,答案是把惩罚的绝对值项交给近端算子,把约束交给算子分裂与 ADMM。3.6 节的增广拉格朗日在那里会以 ADMM 的形式再次出现,对偶上升与障碍法的位置则由近端梯度与分裂算法承担。
练习题
第 1 题 概念推导
证明对偶函数
参考答案
凹性:固定
次梯度:设
按次梯度的定义(凹函数版本:
弱对偶:对任意可行
凸性:对偶问题的约束
第 2 题 计算推理
求解问题
参考答案
写成标准形式:
积极集判断:无约束最优
两个约束都活跃:
第一分量:
灵敏度:把
一阶变化为
第 3 题 代码验证
用 3.6.4 与 3.6.5 节的代码做两组实验并解释:其一,把增广拉格朗日的
参考答案
import numpy as np
# 实验一:增广拉格朗日的 mu 与迭代次数(违反量降到 1e-12)
for mu in [0.1, 1.0, 10.0]:
nu = 0.0
for k in range(5000):
t = (2 * mu - nu) / (2 + 2 * mu)
h_val = 2 * t - 2
nu = nu + mu * h_val
if abs(h_val) < 1e-12:
break
print(f"mu = {mu:>4}: {k} 步达到违反量 1e-12, nu = {nu:.10f}")
# 实验二:障碍法的倍增因子与牛顿步总数(3.6.5 节的 A、b、c)
A = np.array([[2.0, 3.0, 1.0], [4.0, 1.0, 2.0], [3.0, 4.0, 2.0]])
b = np.array([5.0, 11.0, 8.0])
c = np.array([5.0, 4.0, 3.0])
def barrier_center(x, t, tol=1e-16, maxit=200):
x = x.copy()
steps = 0
for _ in range(maxit):
s = b - A @ x
gb = -t * c + A.T @ (1 / s) - 1 / x
Hb = A.T @ np.diag(1 / s ** 2) @ A + np.diag(1 / x ** 2)
d = -np.linalg.solve(Hb, gb)
if -gb @ d / 2 < tol:
break
step = 1.0
while (b - A @ (x + step * d)).min() <= 0 or (x + step * d).min() <= 0:
step *= 0.5
phi0 = -t * c @ x - np.log(b - A @ x).sum() - np.log(x).sum()
while True:
xn = x + step * d
phi1 = -t * c @ xn - np.log(b - A @ xn).sum() - np.log(xn).sum()
if phi1 <= phi0 + 1e-4 * step * gb @ d:
break
step *= 0.5
x = x + step * d
steps += 1
return x, steps
def run_path(sigma):
x = np.array([0.5, 0.5, 0.5])
t, total = 1.0, 0
while t < 1e6 * 0.5:
x, st = barrier_center(x, t)
total += st
t *= sigma
x, st = barrier_center(x, t)
total += st
s = b - A @ x
gap = b @ (1 / (t * s)) - c @ x
return t, gap, total
for sigma in [2.0, 10.0]:
t_fin, gap, total = run_path(sigma)
print(f"因子 {sigma}: 最终 t = {t_fin:.3e}, 间隙 = {gap:.3e}, 累计牛顿步 = {total}")输出:
mu = 0.1: 297 步达到违反量 1e-12, nu = -2.0000000000
mu = 1.0: 40 步达到违反量 1e-12, nu = -2.0000000000
mu = 10.0: 11 步达到违反量 1e-12, nu = -2.0000000000
因子 2.0: 最终 t = 5.243e+05, 间隙 = 1.144e-05, 累计牛顿步 = 70
因子 10.0: 最终 t = 1.000e+06, 间隙 = 6.002e-06, 累计牛顿步 = 49实验一:理论递推为
实验二:倍增因子为
第 4 题 综合应用
软间隔支持向量机(SVM)的原始问题为
推导其拉格朗日对偶问题,写出 KKT 条件,并说明三件事:对偶问题为什么适合核化;哪些样本在最优解处起决定作用;对偶问题适合在样本数与特征维数处于什么关系时使用。
参考答案
拉格朗日函数:引入乘子
平稳性条件:对
对偶问题:把
KKT 条件:平稳性(上面三式)、原始可行性(约束成立)、对偶可行性(
核化:对偶目标只通过内积
哪些样本起决定作用:
适用规模:对偶变量数为样本数
常见错误
错误 1 · 混用两套不等号约定
原因:约束可以写成
解决:在一个项目内固定一套约定并写进注释(本教程统一用
错误 2 · 把 KKT 条件当作普遍成立的必要或充分条件
原因:两端的误用都常见。必要方向忽略了约束规范:梯度退化的约束(如
解决:先判断问题类别再使用 KKT。凸问题加 Slater 条件时 KKT 是充要条件,可以放心求解;非凸问题中把 KKT 解视为候选点,按目标值筛选,并检查约束规范是否成立。数值实现中检查 KKT 残差的量级(平稳性范数、违反量、乘子负部),而不是只看求解器返回的收敛标志。
错误 3 · 认为二次罚函数只要增大罚参数就能得到精确解
原因:二次罚的违反量按
解决:需要精确可行解时改用增广拉格朗日(固定
错误 4 · 障碍法从边界点或不可行点出发
原因:对数障碍在边界上取值为无穷、在可行域外无定义,牛顿方向的计算依赖当前点严格可行。从松弛量为零的点出发时,
解决:初始点取严格可行点(松弛量与变量都严格正),构造方法是从零向量出发做一个可行性阶段,或用相一(phase I)问题;回退过程显式检查严格可行性,不满足就折半。外层倍增因子取