尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SCA凸优化实战:非凸问题迭代逼近与MATLAB代码解析

SCA凸优化实战:非凸问题迭代逼近与MATLAB代码解析 简介一份专注SCASequential Convex Approximation凸优化算法的MATLAB实现资源包面向需要处理非凸优化问题的学习者、研究者与工程技术人员适用于无线通信、信号处理、能源系统等典型应用场景。压缩包内共2个文件均为.m脚本整体大小仅3KB轻量精炼一个提供SCA算法通用迭代框架另一个给出特定问题的近似示例与调用方式便于读者对照理解“凸近似—凸子问题求解—变量更新”的核心流程。内容还涉及凸函数与凸集判定、凸优化问题标准形式、Taylor展开近似构造技巧及收敛性分析要点帮助读者从原理到代码掌握SCA方法。资源已有2698人学习下载研读后可快速获得可直接运行的MATLAB脚本并理解如何将非凸原问题分解为系列可解的凸子问题避免从零搭建框架适合入门学习与工程实践参考。1. SCA 凸优化非凸问题并非无解关键是找对逼近路径SCA 凸优化Sequential Convex Approximation顺序凸近似在工程优化里出现的频率比想象得高尤其在无线通信功率分配、波束成形、信号处理和能源系统调度这几类场景中。反直觉的一点是很多人以为非凸问题只能靠随机搜索或启发式算法碰运气但 SCA 的做法是把原问题按迭代拆成一串凸子问题每一步用成熟的凸优化工具求解最终收敛到一个满足 KKT 条件的驻点或满意解。这份 sca.zip 里只有两个文件——sca.m 和 xiao_power_beizeng100.m一个负责通用迭代框架一个负责演示具体功率分配问题的建模与求解。适合谁如果你正在复现论文里的迭代算法或者手头有一个能写出数学模型但调不通的非凸优化问题这份代码能给你一条可以直接改着用的路径。2. 先立住优化模型凸函数判定、非凸来源与 SCA 三步迭代2.1 凸函数与凸集先搞清“局部最优等于全局最优”的成立条件凸优化教科书里最常被引用的一句话是凸问题上的任何局部最优解都是全局最优解。但这个结论成立有两个前提一是目标函数是凸函数二是可行集是凸集两个条件缺一不可。凸函数的定义是对任意两个点 x1、x2以及任意 λ∈[0,1]都满足 f(λx1(1−λ)x2) ≤ λf(x1)(1−λ)f(x2)。这个不等式看起来抽象落到图像上就是函数曲线上的任意两点连线不会落在曲线下方。工程上判断一个多维函数是否为凸函数常用的做法是看它的 Hessian 矩阵是否半正定。如果 Hessian 矩阵的所有特征值都大于等于零那函数就是凸的如果特征值里有负数函数就是非凸的。很多人只画一维曲线看“是不是碗形”这在一维情形下没问题但到了多维就不可靠了。我见过不少把非凸问题当成凸问题直接丢给 CVX 的案例结果求解器要么报错要么给出的结果明显不合理。可行集的凸性同样重要。两个可行点之间的连线上的任意一点也必须满足所有约束条件这个集合才是凸集。线性约束自然满足这个性质但非线性约束就不一定了比如二次等式约束 h(x) ‖x‖² c 这样的集合就是一个球面球面上两个点的连线会穿过球体内部并不完全落在球面上所以它是非凸集合。 SCA 的切入点就在这里每轮迭代里用一个凸集合去近似这个非凸集合让凸优化工具能正常工作。2.2 非凸项的三个典型来源分式、乘积与指数嵌套实际工程问题里的非凸项并不是程序员故意制造出来的它们往往来自物理模型的表达方式。第一个典型来源是分式项。通信领域里的 SINR信干噪比就是最典型的分式分子是用户自身信号功率分母是干扰加噪声这个结构在优化变量上天然是非凸的。第二个典型来源是变量乘积比如两个优化变量相乘 x·y或者向量与自身共轭转置构成的外积 wwᴴ。功率分配问题里经常出现变量乘积后还要约束秩为一的情况这种约束几乎都是非凸的。第三个典型来源是指数或对数嵌套比如能效优化里的 log(1SINR) 一旦和分母上的功率相加耦合函数整体就失去了凸性。这些非凸项不会因为我们“希望它是凸的”就自动变凸。处理它们有两个方向一是做变量替换把非凸结构转化成凸结构但很多时候变量替换之后又会冒出新约束等于按下葫芦浮起瓢二是用 SCA在每一轮迭代中把非凸项替换成一个在当前展开点附近成立的凸近似。SCA 的核心思想可以概括成一句话不是直接解原问题而是解一列逐渐逼近原问题的凸子问题。2.3 SCA 的三步循环近似、求解、更新标准 SCA 每一步迭代都包含三个阶段。第一步是近似在当前的迭代点 x(k) 处把目标函数里非凸的部分替换成凸近似函数同时对非凸约束做同样的处理。第二步是求解用内点法、梯度投影法或者现成工具比如 MATLAB 的 quadprog、CVX 求解这个凸子问题得到临时解 x_temp。第三步是更新把 x_temp 作为下一轮迭代的展开点也可以加入阻尼系数避免震荡。整个循环的数学形式大致如下初始化 x(0) for k 0, 1, 2, ...: 构造非凸项的凸近似 f_tilde(x; x(k)) 求解凸子问题: min f_tilde(x; x(k)) s.t. 凸约束集合 得到临时解 x_temp 更新: x(k1) x_temp (或带阻尼的更新) 判断是否收敛 end这里每一步都有讲究。近似构造的质量直接决定收敛速度如果近似函数偏离原函数太远迭代会来回跳动甚至发散。求解阶段必须保证子问题严格凸否则又落回局部最优的老问题。更新阶段如果直接让 x(k1) x_temp在问题条件数很差时容易震荡常见的处理是引入一个步长因子 α∈(0,1]把更新改成 x(k1) x(k) α·(x_temp − x(k))。提示判断 SCA 是否走对方向最直观的方式是看每次迭代后原目标函数值是否呈单调下降或单调上升趋势。虽然 SCA 并不保证每轮都严格单调但在绝大多数设计良好的实现里目标值曲线应该像一条台阶式下降的曲线如果看到锯齿状震荡优先怀疑步长和近似质量。3. 拆解 MATLAB 代码sca.m 的框架与 xiao_power_beizeng100.m 的功率分配示例3.1 从文件名看资源结构两个文件到底怎么分工sca.zip 里只有两个 .m 文件这种精简结构在论文复现包里很常见。sca.m 一看就是主程序或通用函数名它承担的是 SCA 迭代框架初始化、循环、停止判断、结果输出。xiao_power_beizeng100.m 从命名习惯看xiao 是“小”power 是“功率”beizeng100 是“倍增100次”的意思大概率是一个小规模的功率增长或功率分配演示脚本循环 100 次每次让某个功率量做倍增或者步进更新用来观察 SCA 在动态场景下能不能跟踪最优解。这两个文件的设计思路一般是xiao_power_beizeng100.m 调用 sca.m前者定义问题模型后者执行 SCA 迭代。如果直接运行 xiao_power_beizeng100.m 就能出结果说明它已经把问题相关的参数都内置了。如果你要换自己的问题只需要保留 sca.m 的循环骨架重写近似构造和求解那两段。3.2 sca.m 通用框架核心循环与停止条件一个可复用的 sca.m 不会把具体的数学函数写死在代码里而是把“近似构造”“求解”“目标值计算”分别写成子函数或函数句柄这样换问题时不需要动主循环。常见的框架拆成三个函数块具体写成 MATLAB 大概是这个样子function [x_opt, obj_hist] sca(x0, approx_func, solve_func, obj_func, params) % sca.m -- SCA 通用迭代框架 % 输入: % x0 : 初始点(列向量) % approx_func : 函数句柄, 用于在给定点处构造凸近似模型 % solve_func : 函数句柄, 用于求解当前的凸子问题 % obj_func : 函数句柄, 用于计算原问题的真实目标值 % params : 结构体, 包含 max_iter, tol, alpha 等参数 % 输出: % x_opt : 收敛后的最优解 % obj_hist : 每次迭代的原目标函数值, 用于画收敛曲线 x x0(:); n length(x); obj_hist zeros(params.max_iter, 1); for k 1:params.max_iter % 1. 近似:在当前迭代点 x 处构造凸子问题 model approx_func(x); % 2. 求解:把凸子问题交给求解器 x_temp solve_func(model, x); % 3. 更新:加入阻尼系数, 防止迭代点来回跳 alpha params.alpha; x x alpha * (x_temp - x); % 记录真实目标值, 注意这里必须用原目标函数计算 obj_hist(k) obj_func(x); % 停止条件: 相邻两次迭代的变量变化足够小 if norm(x_temp - x, inf) params.tol obj_hist obj_hist(1:k); break; end end x_opt x; if k params.max_iter fprintf([sca] 达到最大迭代次数 %d, 未完全收敛\n, params.max_iter); end end这段代码的逻辑很直接外层 for 循环就是 SCA 三步循环approx_func 和 solve_func 是两个函数句柄这是 MATLAB 里做模块解耦最常用的方式。alpha 这个参数值得细说它通常取 0.5 到 1 之间。alpha 1 时就是完全信任凸子问题的解收敛快但容易震荡alpha 偏小时每步走得保守曲线平滑但迭代次数变多。tol 一般取 1e-6 或 1e-8如果你的变量本身量级很大相对判断会比绝对判断更稳。这里还有一个容易被忽略的细节obj_hist 记录的一定要用原问题的目标函数而不是近似函数的目标值。近似函数每轮都在变它的下降不代表原目标下降只有原目标函数值才是判断收敛的可靠依据。我把这条写进过很多次代码注释里因为确实有朋友拿近似目标值画收敛曲线画出来一条漂亮的单调曲线但实际问题根本没收敛。3.3 xiao_power_beizeng100.m 的功率分配示例变量、约束与监视量xiao_power_beizeng100.m 如果按“小功率倍增 100 次”来理解那它大概率是一个这样的演示初始给每个用户一个很小的功率循环 100 次让功率以固定倍数或步长增长每一步调用 SCA 更新出一个可行功率向量同时记录系统吞吐量或 SINR。这类脚本在无线通信论文复现里出现频率很高它的核心结构往往长这样% xiao_power_beizeng100.m % 演示 SCA 在小规模功率分配问题上的迭代行为 % 场景: N 个用户共享一个信道, 每个用户有一个功率变量 p_i clear; clc; rng(1); N 4; % 用户数 P_max 1; % 归一化总功率上限 p0 0.01 * ones(N,1); % 每个用户初始小功率 % 构造信道增益矩阵, 对角线为用户自身信道, 非对角线为干扰信道 H abs(randn(N) * 0.5 0.5); H(1:N1:end) 1.0; % 迭代参数 max_iter 100; params.alpha 0.8; params.tol 1e-6; % 记录吞吐量和 SINR throughput_hist zeros(max_iter, 1); sinr_hist zeros(max_iter, N); p p0; for k 1:max_iter % 固定当前点, 计算每个用户的干扰功率 interference H * p - diag(H) .* p; % 用当前点构造 SINR 约束的凸近似(分母线性化) sinr_threshold 0.5; % 把 sinr threshold 改写成 信号 threshold * 干扰 % 这是一个线性约束, 是凸的 A_ineq threshold * H; A_ineq(1:N1:end) A_ineq(1:N1:end) - diag(H); b_ineq zeros(N,1); % 用 quadprog 求解最小化 -sum(log(1sinr)) 的凸子问题 % 这里简化为最小化 -sum(log(信号项)) H_diag diag(H); f -log(H_diag); % 线性目标系数 options optimoptions(quadprog, Display, off); p_new quadprog(2*eye(N), f, A_ineq, b_ineq, ... ones(1,N), P_max, zeros(N,1), ones(N,1), p, options); % 阻尼更新 p p params.alpha * (p_new - p); % 计算真实 SINR 和吞吐量 sinr (H_diag .* p) ./ (interference 1e-6); throughput_hist(k) sum(log2(1 sinr)); sinr_hist(k,:) sinr; end这段代码里的核心技巧是 SINR 约束的线性化SINR 大于等于阈值这个非凸约束在固定干扰项后可以改写成信号强度大于等于阈值乘干扰这一步做完约束就变成了线性约束。quadprog 专门求解二次规划问题这里因为目标函数被简化成了线性形式所以代价矩阵用了 2*eye(N)实际项目中你要把真实的目标二阶信息填进去。A_ineq 矩阵的构造是整个例子的核心对角线减去 diag(H) 是为了把自身信号从干扰项里扣除这部分容易写错建议写完之后打印出来逐个检查。注意演示脚本里用了 rng(1) 固定随机种子这意味着每次运行结果可复现。实际调参时我会盯两个监视量throughput_hist 的曲线形态和 p 的逐维变化幅度。如果 throughput 曲线稳步上升最后趋于平缓说明 SCA 的近似方向是对的如果曲线突然跳降大概率是某个约束线性化出了错或者阻尼系数 alpha 太大导致迭代点越过了可行域边界。4. 凸近似怎么构造Taylor 展开、约束松弛与收敛判据4.1 一阶 Taylor 展开构造凸下界的最直接方式SCA 里最常用的近似工具是一阶 Taylor 展开。对于目标函数中的非凸项 g(x)在展开点 x(k) 处做一阶展开得到 g(x) ≈ g(x(k)) ∇g(x(k))ᵀ(x − x(k))这样就把原函数替换成仿射函数而仿射函数既是凸函数也是凹函数可以自由决定它作为上界还是下界。如果是凸函数被下放一阶展开天然就是原函数的全局下界这是凸函数的经典性质。对于非凸项一阶展开不一定是下界也可能是上界取决于具体函数结构。工程上常见的处理方法是把目标函数分解成“凸部分 非凸部分”凸部分原样保留非凸部分做一阶展开。这样得到的近似函数可能不再是原问题的严格下界但仍然是凸函数可以让子问题有唯一最优解。需要注意的是如果展开点附近有二阶曲率信息一阶近似可能偏保守这时候可以加一个正则项 λ‖x − x(k)‖² 来限制迭代步长正则项的系数 λ 相当于隐式控制了步长太大收敛慢太小又防不住震荡。4.2 约束凸化松弛与罚函数目标函数的近似只是 SCA 的一半约束的凸化往往更麻烦。非凸等式约束是最难处理的比如约束 ‖w‖² 1 这个球面约束它的集合是非凸的。常见做法是松弛成不等式 ‖w‖² ≤ 1这一步把约束从等式变不等式后集合变成凸的但问题来了松弛后的解可能落在球内部不满足原等式约束。应对办法是罚函数法。把等式约束的偏差加入目标函数作为惩罚项比如加上 μ(‖w‖² − 1)²然后在外层循环里逐渐增大 μ。这里的 μ 就是罚因子一开始取 10 或 100每轮迭代后乘以 1.5逼迫解逐渐贴近球面。这个技巧在波束成形问题里非常常见代价是会引入额外的非凸罚项需要在 SCA 框架内再次把罚项做线性化相当于两层近似叠加。另一种常用手段是引入辅助变量。一个看似复杂的非凸约束往往可以通过新增变量 t 和一个或多个额外约束改写成凸形式。这个技巧在凸优化里叫变量变换比如把约束 x·y ≥ 1 通过令 u x, v y, 约束 uv 恒定且 uv ≥ 1 来处理uv ≥ 1 在 u, v 非负时不是凸约束但通过取对数变成 log u log v ≥ 0而 log 函数是凹的凹函数求和约束大于等于 0 代表的是一个凸集的补集反而更麻烦。所以变量替换要非常小心不是所有看起来“换元更简单”的变形都是凸的每次替换完都要重新验证集合凸性。4.3 收敛判据与参数选择SCA 的收敛判断通常检查三样东西变量变化量、目标函数变化量、梯度或 KKT 残差。变量变化量最常用即相邻两次迭代满足 ‖x(k1) − x(k)‖∞ ε。目标函数变化量适合处理目标值是“只减不增”的单调下降问题对比前后两轮目标差小于阈值即停机。KKT 残差是理论最严格的做法计算量大工程上见得少。三个判据对应三种不同场景变量判据简洁但容易误判如果目标函数非常平坦变量还能移动但目标值基本不变了变量判据会卡住目标函数判据在非单调场景下容易错过收敛点混合判据同时检查变量和目标差虽然代码多两行但可靠性高得多。参数选择方面alpha 和 tol 是一对组合alpha 越小收敛路径越平滑但最终停留点可能离真最优更远建议先跑一版 alpha1 观察曲线是否震荡如果震荡再降 alpha 到 0.6 左右同时把 tol 从 1e-6 放宽到 1e-5 看目标值是否变化如果变化小于 1e-3精度已经够用。有一类收敛问题值得单独说多次随机初始点跑出来的最优值不同但彼此接近这属于正常现象说明问题本身有多个局部驻点。如果最优值相差很大那就不是收敛判据的问题而是近似模型构造失配需要返回 4.1 节检查近似函数。有一点要记住SCA 并不保证找到全局最优它给出的是满足 KKT 条件的驻点实际项目中把驻点配合多次起点挑选出最好的一个作为工程近似最优解是业界通行的做法。5. SCA 实现避坑五条踩坑记录与排查路径5.1 迭代目标值震荡曲线呈锯齿状现象obj_hist 画出来不是平滑单调下降而是上下跳动甚至几步内目标值反复横跳。原因阻尼系数 alpha 设置过大或者近似函数在展开点附近与原函数偏差过大导致子问题最优解越过了原问题可行域的“安全范围”。解决先把 alpha 降到 0.3 到 0.5 之间观察两轮曲线是否变平滑如果仍震荡检查近似函数是否用了二阶信息而 Hessian 不正定这种情况要退回一阶展开并增加正则项 λ‖x−x(k)‖²λ 从 0.1 开始试。5.2 求解凸子问题时 quadprog 或 CVX 报错提示问题不可行现象在某些迭代轮次求解器直接返回“Problem is infeasible”程序中断。原因这轮里近似约束构造得过于保守使得约束集合变成了空集。常见于惩罚函数法里 mu 取得过大或者线性化约束的系数矩阵写错。解决先打印出错那一轮的约束矩阵检查是否存在某一行所有系数都为零但右侧常数项大于零再把罚因子改成渐进式从 1 开始每轮乘 1.2 而不是直接上大惩罚。5.3 初始点选得太离谱收敛到非常差的结果现象换一个初始点最终目标值差了两倍以上代码逻辑却完全正常。原因SCA 本质上是局部方法初始点决定了它落在哪个驻点附近初始点离可行域太远时内部子问题的凸近似一开始就偏离了原问题的关键区域。解决把初始点投影到可行域内再开始迭代投影可以用 quadprog 先跑一次纯可行解搜索或者采用“热启动”策略先用 50 次粗迭代选一组最优中间点再用这组点作为正式迭代的初始点。5.4 数值量级差异巨大迭代到后期精度崩坏现象目标函数里同时出现 1e-8 量级和 1e6 量级的项收敛后目标值明显偏离理论值。原因MATLAB 默认浮点精度有限大数吃小数导致小量级变量在迭代后期几乎不更新。解决对所有变量和约束做归一化功率问题里把总功率归一到 1信道增益归一化到均值 1如果变量本身跨量级按列做对角缩放效果比归一化更细。缩放之后 tol 也要相应调整原来 1e-6 是绝对量级归一化后 1e-6 变成了相对精度判断标准要重新标定。5.5 收敛了但最终目标值和论文里的结果对不上现象曲线收敛得很漂亮但最终数值和论文或参考实现差了 10% 以上。原因大概率不是循环写错而是目标函数本身的表达有差异比如论文里用的是自然对数 ln代码里写成了 log10或者 SINR 分母里加了噪声项而代码漏掉了还有一种常见情况是约束条件差一个不等号方向松弛方向和收敛结果完全相反。解决逐行对照目标函数和约束表达把论文公式里的每个变量换算关系列成表格用两三个已知点手动算一遍对比代码输出。这步没捷径只能按公式拆开核对。6. 换到自己问题时怎么验证多起点检验与目标函数单调性检查拿到 sca.m 和 xiao_power_beizeng100.m 之后最常遇到的问题是“我怎么知道我改出来的代码是对的”。我自己的验证套路固定分三步第一步跑通原示例确认收敛曲线和目标值和预期一致第二步把自己的目标函数和约束替换进去先不追求性能只验证能跑完整个迭代第三步做多起点检验随机生成 20 到 50 个初始点分别跑 SCA统计最终目标值的分布。第三步里有一个可以重复使用的快捷脚本。% verify_sca.m -- 多起点检验 SCA 稳定性 rng(42); num_starts 20; final_obj zeros(num_starts, 1); for i 1:num_starts x0 params.xmin (params.xmax - params.xmin) .* rand(size(params.xmin)); [x_opt, obj_hist] sca(x0, approx_func, solve_func, obj_func, params); final_obj(i) obj_func(x_opt); if ~isdecreasing(obj_hist) fprintf(起点 %d: 目标值不单调, 注意震荡风险\n, i); end end fprintf(目标值分布: min%.6f max%.6f mean%.6f\n, ... min(final_obj), max(final_obj), mean(final_obj));这个脚本里 isdecreasing 是一个自己写的检查函数判断 obj_hist 序列在最后 20 轮里是否保持单调下降允许每轮变化小于 1e-4 视为平缓。做完这一步如果你的 final_obj 分布集中在很窄的区间里说明问题结构对初始点不敏感结果可信度高如果分布跨度大说明问题本身多峰严重或者你的近似函数构造还有问题需要回去检查近似质量和约束松弛方式。从那以后我每次把 SCA 代码移植到新问题都强制先跑一遍这个多起点检验脚本不通过就不往下继续调参。希望这个验证习惯能帮到你省掉后面因为结果不可复现而返工的大量时间。本文还有配套的精品资源点击获取
返回列表