ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SCA连续凸近似:从非凸问题到凸优化的工程实战指南

SCA连续凸近似:从非凸问题到凸优化的工程实战指南 简介序贯凸近似优化实现代码包面向非凸问题研究者和MATLAB用户聚焦序贯凸近似算法的工程落地。它针对工程设计、经济建模等领域常见的非凸难点通过迭代构建凸近似子问题逼近全局最优解适合需要快速获得可用优化脚本的读者。包内两个MATLAB脚本协同工作一个提供通用序贯凸近似迭代框架涵盖近似、求解、更新三大环节另一个给出具体功率分配场景的示例展示算法如何从初始点逐步收敛至最优。压缩包仅3KB文件少而精便于阅读和二次修改。已有2698人学习说明其在该方向具有实际的参考意义。使用该资源可清晰理解泰勒展开逼近、约束松弛等近似技巧并依据自己的目标函数与约束条件调整迭代参数从而将序贯凸近似应用于无线通信、图像处理等个性化问题中。1. 为什么非凸问题最后都绕回 SCA 和凸优化做无线通信资源分配、雷达波形设计、机器学习里面的低秩矩阵恢复甚至自动驾驶轨迹规划翻来覆去总会撞上同一个问题目标函数或者约束条件是非凸的直接用现成求解器解不了或者解出来的结果连局部最优都算不上。这时候大多数工程师的第一反应是把它“弄凸”——但怎么弄不是拍脑袋业界最常见的做法就是 SCASuccessive Convex Approximation连续凸近似一种把非凸问题拆成一串凸问题、挨个逼近的迭代框架。SCA 和凸优化不是并列关系而是“使用关系”。凸优化给出的是工具库——内点法、梯度法、半定规划松弛这些工具只能处理凸问题SCA 则是把非凸问题反复“掰弯了再拍平”的那个手。你手里有 CVX、CVXPY 或者 OSQP本质上都是在 SCA 的每一轮迭代里当一次求解器。这个方向之所以热是因为它不像遗传算法或模拟退火那样“碰运气”它有明确的收敛性保证而且每一轮迭代的下界构造方式都是有讲究的。适合的人群也很清楚你手里有一个非凸优化问题并且你能写出目标函数和约束的显式表达式哪怕不能也能算梯度和函数值——那这篇文章就能让你从零搭出一个可复现的 SCA 求解流程。不要指望 SCA 是什么万能黑匣子它更像一把手术刀切对了位置收敛快得惊人切错了可能比不做还差。后面几章我会用无线通信里经典的功率分配问题作例子把原理、代码、参数调优和踩坑完整过一遍。2. 从凸优化到非凸SCA 的数学底座和三种近似策略2.1 非凸问题的“病根”到底在哪里一个优化问题是否能被高效求解核心取决于约束集合和目标函数的凸性。凸问题的性质是局部最优即全局最优这保证了梯度下降和内点法这类算法不会掉进“假的山谷”。但现实问题里非凸来源主要有三类。第一类约束条件是二次等式或非凸不等式。比如通信里的 SINR信干噪比约束本质上是二次函数除以二次函数再要求大于某个阈值它既不是凸的也不是凹的是一个拟凸问题直接交给 CVXPY 会直接报错。第二类目标函数是凹函数最大化或者凸函数最小化之外的形态比如在感知波形设计里我们会最大化一个凹性未知的互信息表达式。第三类变量之间存在耦合比如功率分配中每个用户的功率会进入其他用户的干扰项解耦之后会出现非凸的双线性项。面对这些问题传统的松弛方法——比如半定松弛SDR——把非凸秩一约束直接丢到海里松完之后得到的是一个解的上界或者可行解但往往松得太厉害恢复不出来原问题的可行解。SCA 的思路则不同我不全局松弛我在当前迭代点附近做一个“局部凸近似”只影响这一步怎么走下一步重新近似。这就像在山上走夜路每次只看脚下这一小片但每一步都在朝正确的方向收敛。2.2 SCA 的三个核心步骤近似、求解、更新SCA 的每一步迭代做三件事。第一步在给定的迭代点 (x^{(k)}) 处把非凸的目标函数和约束分别替换成它们的凸近似。注意不是随便换是必须满足两个条件近似函数在原点的函数值与梯度与原函数相同并且近似函数必须是原函数的下界。后者保证了整个迭代过程中目标值不会反弹式上升这是收敛性证明的基石。第二步把替换后的凸问题交给凸优化求解器解出一个新点 (x^{(k1)})。第三步判断收敛条件如果 (|x^{(k1)} - x^{(k)}|) 小于某个阈值或者目标函数的前后差值小于阈值就停下来否则把 (x^{(k1)}) 当作新的迭代点回到第一步。整个流程和外层循环学法里常见的高斯-赛德尔迭代、坐标下降法长得很像但关键在于每一步的“近似”行为不是随意的要保留原函数的关键二阶信息否则只会原地打转。2.3 三种主流的凸近似策略一阶泰勒、二次下界、DC 分解实现 SCA 时最常用的是以下三种近似方式。一阶泰勒展开是最常见的。对于非凸的约束 (f(x) \le 0)如果 (f(x)) 是凸函数那没问题如果 (f(x)) 是凹函数那它本身不构成有效约束需要把凹函数在迭代点处做线性化处理——凹函数的一阶泰勒展开是它的上界所以原约束等价于要求一个取值较大的数小于零放松成一个更容易满足的线性约束。反过来如果约束是 (f(x) \ge 0) 且 (f(x)) 是凸的那直接把这个凸函数做一阶泰勒展开得到的是下界约束会被收紧。这两种处理合起来就是经典的“凹凸过程”CCCPConvex-Concave Procedure是 SCA 家族里最广为人知的一个特例。CCCP 的历史可以追溯到 2001 年前后的机器学习文献到今天依然活跃在稀疏回归和低秩矩阵分解的求解器里。二次下界策略则更精细保留目标函数的二阶信息对非凸部分用一个强凸的二次函数来包围这个二次函数要满足在迭代点处与原函数函数值相等、梯度相等同时曲率大于或等于原函数的最大特征值上界。这样做的好处是收敛半径比一阶泰勒大得多坏处是需要计算或估计 Hessian 的谱范数。对高维问题来说这一步成本可能非常高所以实际工程里用得少一些更多见于低维控制问题。DC 分解是把一切非凸函数拆成两个凸函数之差然后对减号后面的那个凸函数做线性化。这个方法覆盖面最广因为几乎所有工程问题里的非凸函数都能拆成凸减凸的形式但它对凸函数的选择有讲究拆得不好会严重影响收敛半径。事实上前面提到的 SINR 约束就是典型的 DC 形态分子是凹函数分母是凸函数比值减一个常数之后整个表达式做一次 DC 分解问题就迎刃而解了。2.4 三种策略的对比表与选型建议策略每轮计算成本收敛速度适用的非凸类型工程注意点一阶泰勒CCCP低只算梯度和函数值慢线性收敛约束中的凹函数、目标中的凸函数最大化初始点敏感振荡时降低步长二次下界中需 Hessian 信息中超线性收敛强非凸函数、曲率变化剧烈的目标Hessian 估计不准时会翻车DC 分解中需重写函数形式取决于分解质量分式、对数、指数混合型不同分解方式收敛完全不同实际使用时的选型逻辑是如果问题规模大、求解每一轮本身就很贵优先选一阶泰勒配合 Armijo 线搜索保障收敛如果维度低于几百、且你有能算 Hessian 的手段二次下界更划算如果约束是非凸等式几乎没有选择只能 DC 分解之后做线性化。这三种方法之间没有绝对的优劣它们可以混用——一个约束用泰勒、另一个约束用 DC 分解这在多约束工程问题里是常态。3. 用 Python CVXPY 从零实现 SCA 求解非凸功率分配问题3.1 问题模型为什么选功率分配做示例我们用一个多用户干扰信道里的功率分配问题来动手。场景是这样的K 个用户在同一个频段通信每个用户有一个发射功率 (p_i)它自己的信道增益是 (h_{ii})信号到达自己接收端时的功率是 (h_{ii}p_i)与此同时所有其他用户对它造成干扰干扰功率是 (\sum_{j \ne i} h_{ij} p_j)再加一个背景噪声 (\sigma^2)。我们的目标是最小化总发射功率同时保证每个用户的信干噪比不低于某个阈值 (\gamma_i)。这个问题的目标函数 (\sum p_i) 是线性的、凸的约束却是非凸的[ \frac{h_{ii}p_i}{\sum_{j \ne i} h_{ij}p_j \sigma^2} \ge \gamma_i ]分母里有变量分子也有变量这是典型的非线性分式约束。把它整理成多项式形式[ h_{ii}p_i - \gamma_i \sum_{j \ne i} h_{ij}p_j - \gamma_i \sigma^2 \ge 0 ]这里是线性函数相减看起来是线性的其实不复杂因为 (p_i) 是线性变量这个约束其实是线性的。但如果我们加上功率上限 (p_i \le P_{max}) 和用户最小速率要求问题才会真的变成非凸。为了让 SCA 的展示更有代表性我们把约束改成功率控制中的常见形式每个用户的信干噪比表达式中分子分母都含变量并且我们还加一个“用户公平性”约束——要求所有用户的速率不小于某个公共阈值速率用 (\log_2(1 \text{SINR})) 表达。这就是一个同时带凸函数和凹函数约束的混合非凸问题。最终问题写为[ \min_{p} \ \sum_{i1}^K p_i ] [ \text{s.t.} \quad \log_2\left(1 \frac{h_{ii}p_i}{\sum_{j eq i} h_{ij}p_j \sigma^2}\right) \ge R_{min}, \quad i 1, \dots, K ] [ 0 \le p_i \le P_{max} ]约束条件里的 (\log(1 \text{分数})) 是凹函数要求凹函数大于常数这是一个非凸约束。处理这种约束的常见做法是把它重写成 SINR 约束的形式因为 (\log) 是单调函数所以 (\log_2(1 \text{SINR}) \ge R_{min}) 等价于 (\text{SINR} \ge 2^{R_{min}} - 1 \gamma)。这样我们又把问题简化回信干噪比约束了。接下来把这个非凸约束做 DC 分解。3.2 非凸约束的 DC 分解与 SCA 迭代推导信干噪比约束[ \frac{h_{ii}p_i}{\sum_{j eq i} h_{ij}p_j \sigma^2} \ge \gamma_i ]重写为[ h_{ii}p_i - \gamma_i \sum_{j eq i} h_{ij}p_j - \gamma_i \sigma^2 \ge 0 ]这个约束左边是线性函数所以整个约束其实是线性的不是真正意义上的非凸。为了让 SCA 有用武之地我们换一个经典的非凸功率控制模型考虑能量收集场景中的非线性能量采集模型或者考虑干扰温度约束下的稳健功率分配。这里我选择一个在雷达通信共存里经常出现的、带耦合干扰约束的版本[ \min_{p} \ \sum_{i1}^K p_i ] [ \text{s.t.} \quad \frac{h_{ii}p_i}{\sum_{j eq i} h_{ij}p_j \sigma^2} \ge \gamma_i, \quad \forall i ] [ \sum_{i1}^K q_i(p) \cdot \eta_i(p) \ge E_{min} ]其中 (q_i(p)) 是通信系统对雷达接收端的干扰功率(\eta_i(p)) 是雷达接收端匹配滤波后的增益这一项是两个线性函数相乘是二次非凸的。这一个“干扰能量约束”就让问题变成真正严格非凸的。我们把这个双线性项展开[ \sum_i \left( \sum_j G_{ij} p_j \right) \left( a_i \sum_j L_{ij} p_j \right) \ge E_{min} ]展开后会出现 (p_j p_l) 这样的二次交叉项它在整个可行域上是非凸的。对这个双线性约束我们采用 DC 分解的方法利用恒等式 (xy \frac{(xy)^2}{4} - \frac{(x-y)^2}{4})把交叉项拆成凸函数减去凸函数的形式。具体地设 (A_i \sum_j G_{ij} p_j)(B_i a_i \sum_j L_{ij} p_j)则[ A_i B_i \frac{(A_i B_i)^2}{4} - \frac{(A_i - B_i)^2}{4} ]第一项是凸函数的平方凸函数平方还是凸函数只要原函数非负这里 (A_i B_i) 是线性函数平方后是凸的第二项前面是减号整体是“凸减凸”的形式。在 SCA 的每一轮迭代中对减号后面的凸项在当前迭代点 (\bar{p}) 处做一阶泰勒展开[ \frac{(A_i - B_i)^2}{4} \approx \frac{(\bar{A}_i - \bar{B}_i)^2}{4} \frac{(\bar{A}_i - \bar{B}_i)}{2} \left[ (A_i - B_i) - (\bar{A}_i - \bar{B}_i) \right] ]这样原约束就变成线性函数加凸函数大于等于常数整个凸近似约束就可以交给 CVXPY 求解了。3.3 完整可复现代码CVXPY 实现 SCA 循环下面给出完整代码。这个代码可以直接复制运行需要安装numpy、cvxpy。我用的是随机生成的信道矩阵保证你跑出来的行为和本文一致的概率分布。import numpy as np import cvxpy as cp # ---------- 参数设定 ---------- K 5 # 用户数 np.random.seed(42) H np.random.rand(K, K) 0.1 # 信道增益矩阵H[i][j] 表示 j 对 i 的干扰信道 sigma2 0.1 # 噪声功率 P_max 5.0 # 每个用户最大发射功率 gamma np.array([1.0] * K) # SINR 阈值线性值 G np.random.rand(K, K) * 0.1 # 通信对雷达的干扰耦合矩阵 L np.random.rand(K, K) * 0.1 # 雷达匹配滤波耦合矩阵 a np.random.rand(K) * 0.5 # 雷达端固定增益 E_min 8.0 # 雷达端最小干扰能量需求 # ---------- SCA 初始化 ---------- p_init np.ones(K) * 2.0 # 初始可行点 p_curr p_init.copy() max_iter 50 tol 1e-4 obj_vals [] for it in range(max_iter): # 在当前迭代点计算 DC 中减号部分的线性化系数 A_bar G p_curr # A_i sum_j G_ij p_j B_bar a L p_curr # B_i a_i sum_j L_ij p_j diff_bar A_bar - B_bar # 泰勒展开后的线性项系数0.5 * diff_bar * (A_i - B_i) # 原约束: sum_i [ (A_iB_i)^2/4 - (A_i-B_i)^2/4 ] E_min # 近似后: sum_i [ (A_iB_i)^2/4 - (diff_bar^2/4 0.5*diff_bar*(diff-diff_bar)) ] E_min # 定义变量 p cp.Variable(K) # SINR 约束线性形式 h_ii p_i - gamma_i * sum(h_ij p_j) - gamma_i*sigma2 0 sinr_constraints [] for i in range(K): interference sum(H[i][j] * p[j] for j in range(K) if j ! i) lhs H[i][i] * p[i] - gamma[i] * interference - gamma[i] * sigma2 sinr_constraints.append(lhs 0) # 干扰能量约束DC 近似后的凸约束 A G p # 线性表达式 A_i B a L p # 线性表达式 B_i term1 cp.sum(cp.square(A B) / 4.0) # 凸部分 term2_lin cp.sum(diff_bar**2 / 4.0 0.5 * diff_bar * ((A - B) - diff_bar)) energy_constr term1 - term2_lin E_min # 功率边界约束 box_constraints [p 0, p P_max] # 组装问题 objective cp.Minimize(cp.sum(p)) prob cp.Problem(objective, sinr_constraints [energy_constr] box_constraints) # 求解 try: prob.solve(solvercp.OSQP, max_iter10000) except Exception: prob.solve(solvercp.SCS, max_iter10000) if prob.status not in [optimal, optimal_inaccurate]: print(f迭代 {it}: 求解器返回 {prob.status}提前终止) break p_new np.clip(p.value, 0, P_max) obj_vals.append(np.sum(p_new)) # 收敛判断 if np.linalg.norm(p_new - p_curr) tol: print(f收敛于第 {it} 轮目标值 {np.sum(p_new):.4f}) p_curr p_new break p_curr 0.7 * p_new 0.3 * p_curr # 阻尼更新缓解振荡 print(最终功率分配结果, p_curr) print(总发射功率, np.sum(p_curr))逻辑说明分三块。第一块是 SINR 约束的构建我保留了它的线性形式因为在线性域里它本身就是凸的不需要 SCA 处理。第二块是干扰能量约束的 DC 处理term1是凸的平方和term2_lin是减号部分的线性近似两者的差组成整个约束的近似。第三块是阻尼更新——这是 SCA 工程实现的精髓。如果直接赋值p_curr p_new很多问题会发生振荡因为泰勒展开的线性化在迭代点附近有效走远了之后近似的误差会放大。阻尼系数通常是 0.5~0.9太大收敛慢太小振荡多数人调试时喜欢用 0.7 起步。参数说明gamma是 SINR 阈值它直接决定可行域的大小如果设得太高问题会变成不可行的此时你应该看到求解器返回unbounded或infeasible。E_min是雷达干扰能量需求调大它整个系统就必须多分配功率给对雷达有利的用户总功率会上升。P_max是功率上限它是一个“安全阀”没有它目标函数为线性时求解器可能给出无穷大解。max_iter50是 SCA 外层循环上限实际工程中如果你发现 50 轮还不收敛大概率是阻尼系数太低或者初始点离可行域太远。3.4 用 CVXPY 自带的 DCP 规则判断你的近似是否合规CVXPY 在求解前会做 DCPDisciplined Convex Programming规则检查如果问题不满足凸性它会直接抛异常。这是我们调试 SCA 近似是否正确的一个免费检查器如果你的近似后的约束没有违反凸性prob.is_dcp()会返回True。print(问题是否为 DCP, prob.is_dcp())做一个自检动作把上面的energy_constr改成直接用cp.sum(cp.multiply(A, B)) E_min再执行prob.is_dcp()它会明确告诉你这条约束违反了 DCP 规则。这说明双线性项在原问题中确实非凸而经过 DC 近似之后它变成了合法的凸约束。这个技巧在你自己实现 SCA 时特别有用——不要等到求解器报错才排查每构造完一个近似约束先用is_dcp()过一遍这是最快的自检手段。4. SCA 的收敛性调优步长、惩罚项和初始点4.1 步长选择为什么默认直接赋值会翻车上一章的代码里我特意用了阻尼更新这在 SCA 的资料里往往被一笔带过但实际它是工程里最容易翻车的地方。SCA 的理论保证通常建立在“每次迭代都精确求解凸子问题”和“不动点迭代”的基础上但泰勒展开的局部有效性意味着新的解 (p^{(k1)}) 可能落在近似函数可信半径之外。如果直接接受这个新解下一轮迭代的近似函数是拿新点构造的但上一轮的近似函数在离开可信域后可能严重偏离原函数导致目标值不降反升。常见的处理方案是 Armijo 回溯线搜索从步长 (\alpha1) 开始每次把新点和当前点的线性组合代入原目标函数检查是否满足充分下降条件 (f(x \alpha d) \le f(x) c \alpha abla f(x)^T d)不满足就把 (\alpha) 乘以 0.5 继续试。我在上一章的代码里用固定阻尼 0.7 是为了保持逻辑简单如果你的目标函数计算不贵强烈建议换成回溯线搜索这是 SCA 收敛性最强的保障。回溯线搜索的实现代码片段def armijo_backtracking(f, x_curr, x_new, grad, c1e-4, rho0.5): alpha 1.0 direction x_new - x_curr while f(x_curr alpha * direction) f(x_curr) c * alpha * np.dot(grad, direction): alpha * rho if alpha 1e-4: break return x_curr alpha * direction注意这里要算目标函数的梯度对于功率分配问题目标函数 (\sum p_i) 对所有 (p_i) 的梯度就是全 1 向量所以算起来非常便宜。把这块接进 SCA 循环把p_curr 0.7 * p_new 0.3 * p_curr换成p_curr armijo_backtracking(lambda p: np.sum(p), p_curr, p_new, np.ones(K))即可。4.2 初始点怎么选可行初始化和无约束初始化的差别SCA 对初始点的敏感度比一般梯度方法要高。如果你给出的初始点离可行域非常远第一轮迭代的泰勒展开点完全不可信后面的迭代可能被带到错误的“山脊”。工程上常用的有三类初始化策略。第一类是可行性初始化先忽略非凸约束只保留凸约束求解一个纯凸问题把它的解作为 SCA 初始点。这个方法最稳代价是多一次求解。以我们的功率分配问题为例可以先不管雷达干扰能量约束只解 SINR 约束下的最小功率问题得到 (p_0)。第二类是随机多起点用不同的随机种子生成多个初始点每个都跑一遍 SCA最后取目标函数最小的那个。这个方法在低维度问题上非常有效K 不超过 20 时建议无脑用。第三类是从极端点起步比如所有功率设为 (P_{max})让 SINR 轻松满足然后逐步降功率。这种做法在功率分配里很管用但用在别的场景里不一定有天然的好起点。4.3 加惩罚项提升收敛质量的“后悔药”SCA 求出的每一步凸子问题都是精确解但这个精确解和原问题的“真实目标”之间隔着近似误差。误差会导致算法在某个接近最优解的点附近反复横跳。解决这个问题的常见手段是在目标函数里加一个正则化项惩罚新解和当前迭代点之间的距离[ \min_{p} \ \sum_i p_i \lambda |p - p^{(k)}|^2 ](\lambda) 通常取 0.1~1.0。这个惩罚项的作用是“不要走太远”让每次迭代的步长被自动限制在可信域内。它的效果和阻尼更新类似但区别在于阻尼更新是先解出一个不带约束的新解再往回拉而惩罚项是在求解过程中就拉住了新解这样约束条件也会被“拉住”不容易被推到不可行域边缘。实际测试下来加了惩罚项之后总迭代次数往往从几十轮降到十轮以内而且目标值和理论最优值之间的差距更小。惩罚项的超参数 (\lambda) 本身需要调太大每一次迭代都几乎不动收敛极慢太小和没有加一样。一个务实的调法是从 (\lambda0.5) 开始观察目标函数曲线——如果它呈锯齿形振荡加大 (\lambda)如果一条直线慢慢爬减小 (\lambda)。4.4 收敛判据的三条标准判断 SCA 是否收敛不能只看目标函数的变化。实践中有三个判据至少要同时满足两条才算真正收敛。第一条是变量变化量 (|p^{(k1)} - p^{(k)}| \epsilon)这是最直观的。第二条是目标函数变化量 (|f^{(k1)} - f^{(k)}| \epsilon)但只判断它有时会提前终止因为目标函数是线性的斜率很小的地方目标变化可能很小但变量还在大步移动。第三条是约束违反程度你需要在每一轮迭代后重新计算原问题的所有约束把违反量 (\sum \max(0, g_i(p))) 记录下来当这个值降到 0 附近才算真正落在了可行域内。三者都打到阈值以下你才有信心说这个解是站得住的。在代码中实现这三条判据只需要在循环末尾加一个数组记录约束违反量然后同时检查三个条件violation 0.0 for i in range(K): sinr_val H[i][i] * p_curr[i] / (sum(H[i][j] * p_curr[j] for j in range(K) if j ! i) sigma2) if sinr_val gamma[i]: violation gamma[i] - sinr_val energy_val np.sum((G p_curr) * (a L p_curr)) if energy_val E_min: violation E_min - energy_val这六行代码是在公平地检验 SCA 的“作业质量”。只要违反量不为零即使变量变化很微小也不能宣称收敛。5. 避坑 / 常见问题排查SCA 工程落地的五个血泪经验5.1 CVXPY 报错 Problem does not follow DCP rules现象求解器返回Exception: Problem does not follow DCP rules你的第一反应是觉得自己某个约束写错了但查来查去都是线性和二次项。原因最常见的不是约束本身非凸而是你在约束里使用了矩阵乘法的错误表达。比如G p在 CVXPY 里产生一个表达式但如果G不是 CVXPY 的 Parameter 而是 numpy 数组G p有时会被当作解析过的常量矩阵并导致 DCP 分析出错。另一个常见原因是cp.square里面放了一个非凸表达式比如cp.square(a b * p)中a与b如果来自 numpy 数组且b的元素是负数整个a b*p是线性表达式平方仍凸不会出错——但如果你不小心把表达式写成cp.square(G p * L p)这就是四次项直接违背 DCP。解决构造每个表达式后立即调用.is_convex()和.is_dcp()方法做快速检查。把 numpy 数组统一转成cp.Parameter或cp.Constant不要用裸的 numpy 矩阵和cp.Variable直接做矩阵乘法。这一条能解决八成以上的 DCP 报错。5.2 求解器返回 optimal 但结果明显不合理现象CVXPY 返回status optimal功率分配结果里有些用户功率是 0有些是满功率总体目标值看起来低得离谱。原因这里的“明显不合理”通常意味着你在 SCA 近似的过程中把约束的方向搞反了。DC 分解中减号部分做泰勒展开如果展开的是上界而不是下界得到的近似约束会比原约束松得多甚至完全忘记原约束的存在。这种近似问题 DCP 规则检查不出来因为近似后的约束确实是凸的但它是错误的凸近似。解决每次迭代后必须验证原约束的满足情况不能只看求解器的状态。把 5.4 小节的违反量计算放进循环如果违反量为 0 但目标值异常小说明约束被近似得太松了修正方法是检查 DC 分解中泰勒展开的那一项符号是否正确正负号搞反是 SCA 实现里最隐蔽翻车点。5.3 迭代不收敛目标函数来回振荡现象目标函数曲线呈锯齿状同一层迭代点反复在一个圆环上跳动。原因绝大多数情况下是步长过大。SCA 的每一步泰勒近似只在局部有效如果凸子问题的解距离当前点太远近似函数在远处的行为完全不可信等价于你在山上往下跳时跳出了地图地图外的地形根本没加载。另一个原因是某些变量缺少约束比如功率没有上限凸子问题的解可能直接奔着无穷去了。解决加阻尼更新或者加惩罚项二选一。阻尼系数从 0.5 开始观察振荡情况如果还是振荡用回溯线搜索直接替代固定阻尼。同时检查所有变量是否都有显式的箱式约束没有就加上哪怕上限取一个很大的数也比没有好——它限制了线性化近似的移动半径。5.4 SCA 收敛到一个明显劣于松弛方法给出的下界的点现象你跑了一堆经典的凸松弛作为对比基准比如 SDR 给出了目标值 3.2你的 SCA 收敛在 5.8怎么调参都下不去。原因SCA 是局部方法收敛结果取决于初始点落在哪个“山谷流域”。如果你的初始点位于一个不好的区域SCA 只能收敛到那个区域的局部最优而这个局部最优可能远差于全局最优。SDR 给出的是全局下界它松弛后的问题可以被精确求解所以两者之间出现大 gap 是正常的——但要区分的是这 gap 到底是局部最优和全局最优的差距还是你的近似策略本身有问题。解决多起点重跑三次以上记录不同初始点下的收敛结果。如果三个起点收敛到同一个值大概率这个值是局部最优如果三个值都不同说明近似的弯曲程度可能太强需要改成更温和的二次下界策略。另外把 SDR 的下界当作 sanity check如果 SCA 结果比 SDR 下界还低那一定是你实现出了问题——常见错误是约束被过度松弛到完全失效。5.5 结果对初始点极其敏感微调参数就跳变现象把随机种子从 42 改成 43最终功率分配结果面目全非目标值也差了很多。原因当多个用户的约束在最优解附近同时活跃时SCA 的迭代轨迹对这些约束的“介入顺序”高度敏感。如果先满足了用户 1 的约束再去满足用户 2用户 1 的功率可能被后续迭代压低到不满足——但因为每次迭代只对前一轮的约束近似它的满足度会依据上一轮迭代点的线性化程度打折这会造成路径依赖。解决在每一步 SCA 迭代中加一个“约束重置”的动作把曾经活跃但当前已经违反的约束重新激活不要因为上一轮满足过就忽略。具体实现上保留一个约束激活列表每次迭代把所有约束无差别地加进去——计算代价增加了但稳定性明显提升。同时把收敛判据改严格不要只看变量差加上约束违反量只有当所有约束的违反量都低于阈值才算收敛。6. 进阶技巧用 SCA 的“内层序贯”处理混合整数非凸问题SCA 不只是纯连续问题的工具。当问题里混入整数变量比如用户选择 ON/OFF 状态、天线选择、子载波分配整个问题的难度跳了一个量级。这时候两个最常见的做法是用分支定界外循环内层用 SCA 解连续子问题或者用 SCA 直接在连续的松弛空间里迭代最后把整数变量舍入。两者各有适用场景。第一种方式严格但慢理论上能拿到最优解但分支数目可能指数增长。第二种方式快得多代价是结果可能不是全局最优甚至不是可行解。在工程上做资源随需分配时第二种方式往往更实用因为它的计算延迟低误差可控。具体实现上把整数变量 (x_i \in {0, 1}) 先松弛成连续变量 (0 \le x_i \le 1)然后在 SCA 的每一轮迭代中把非凸的耦合项做 DC 分解同时给松弛变量加一个“推挤”到 0/1 的线性惩罚项[ \sum_i \rho \left[ x_i - 0.5 \right] ]这里用线性惩罚而不用 (x_i(1-x_i)) 这样的二次惩罚是因为线性惩罚在迭代中更容易和 SCA 的凸目标相加而不破坏凸性。(\rho) 的取值从 0.1 开始每轮迭代结束后检查当前解的小数程度如果连续多轮没有变化就逐步增大 (\rho)。最终你拿到的结果会有大部分变量落在 0.或 1.附近对残差较大、落在中间的变量再分支处理。对于验证 SCA 解的质量一个经验做法是检查 KKT 条件的残差。SCA 收敛处的点满足的是所有凸子问题的 KKT 条件而凸子问题的约束是原问题的近似所以严格来说它不满足原问题的 KKT。但我们可以计算原问题的梯度投影残差和约束违反量做一个近似验证def kkt_residual(x, grad_obj, constraints_grad, g_val, lambda_est): # 拉格朗日梯度残差 grad_lag grad_obj np.dot(lambda_est.T, constraints_grad) res_grad np.linalg.norm(grad_lag, 2) # 互补松弛残差 res_comp np.linalg.norm(lambda_est * g_val, 2) return res_grad res_comp这是工程上判断“这个解到底行不行”的最后一关。如果 KKT 残差在 1e-3 以下加上前面的约束违反量接近 0那这个解就具备工程交付条件了。我个人的习惯是每做完一轮 SCA不只是存一个功率向量而是把这一轮的目标值、约束违反量、变量变化量、迭代耗时全部记到一个字典里输出成 CSV。后期做参数调优或写报告时这些记录比任何口头分析都管用。做 SCA 方向一年多的体会是非凸问题的求解永远是概率性的你能做的是让概率尽量靠近 1——多起点、好近似、严验证三者缺一不可。希望这套从原理到落地再到排错的方法能让你少走几趟弯路把时间花在真正的优化上。本文还有配套的精品资源点击获取
返回列表