1. 求解器选型:找到你的“最佳拍档”
刚接触CVX的朋友,最常问我的一个问题就是:“我该用哪个求解器?” 这感觉就像去修车,师傅问你用国产扳手还是进口扳手,你一脸懵。其实,选求解器没那么玄乎,核心就看你手里的“活儿”(优化问题)是什么类型,以及你对“活儿”的质量(精度、速度)有什么要求。
CVX本身是个“翻译官”,它把你的数学模型(用优雅的数学语言写的)翻译成求解器能听懂的“机器语言”。而求解器,才是真正干活的“计算引擎”。引擎有好几种,有的擅长跑高速(线性规划),有的越野能力强(半定规划),有的则是全能选手但价格不菲。选错了,要么问题解不出来,要么算得慢如蜗牛,甚至可能得到错误的结果。
我刚开始用的时候,也犯过懒,一直用默认的SDPT3。直到有一次处理一个规模稍大的二次锥规划(SOCP)问题,算了快一个小时还没出结果,急得我直挠头。后来在同事提醒下换成了MOSEK,好家伙,30秒就搞定了。那次经历让我明白,没有“最好”的求解器,只有“最合适”的。下面这张表,是我根据多年踩坑经验整理的,你可以把它当成“选型速查手册”,贴在电脑旁边。
| 求解器 | LP(线性) | QP(二次) | SOCP(二次锥) | SDP(半定) | GP(几何) | 整数规划 | 自带许可 | 特点简述 |
|---|---|---|---|---|---|---|---|---|
| SeDuMi | Y | Y | Y | Y | E | N | 是 | 元老级,免费,对标准锥规划问题稳定,但有时数值上比较“娇气”。 |
| SDPT3 | Y | Y | Y | Y | E | N | 是 | CVX默认,稳健性不错,速度和精度比较均衡,是可靠的“备胎”。 |
| Gurobi | Y | Y | Y | N | N | Y | 需专业版 | 商业求解器中的“快枪手”,尤其擅长线性和混合整数规划,速度快到飞起。 |
| MOSEK | Y | Y | Y | Y | Y | Y | 需专业版 | “全能王”,支持的锥类型最全,数值鲁棒性极佳,商业求解器中的瑞士军刀。 |
| GLPK | Y | N | N | N | N | Y | 否 | 免费的线性与整数规划求解器,功能单一,可作为特定场景下的轻量级选择。 |
注意:表中的“E”代表实验性支持,意味着功能可能不完全稳定或效率不高,建议优先考虑标“Y”的求解器。
怎么用这张表呢?很简单,分三步走。第一步,看问题类型。如果你的模型里有 semidefinite() 这样的半定约束,那Gurobi就直接出局了,得在SeDuMi、SDPT3和MOSEK里选。如果你的问题是个混合整数规划(变量带 integer 或 binary 声明),那SeDuMi和SDPT3也帮不上忙,只能在Gurobi、MOSEK和GLPK里挑。第二步,看资源与许可。SeDuMi和SDPT3是CVX自带的“免费午餐”,开箱即用。Gurobi和MOSEK虽然强大,但需要CVX专业版许可,这通常意味着公司采购或者学术申请。GLPK免费,但需要你额外安装并配置MATLAB路径。第三步,实战测试。对于关键问题,我强烈建议你用两三个候选求解器都跑一遍。有时候,一个求解器报“Infeasible”(不可行),换一个可能就“Solved”(已解决)了,这未必是你的模型真有问题,可能是不同求解器的数值预处理策略不同导致的。
举个例子,我做过一个天线阵列设计的优化,里面既有二次约束又有秩约束(可转化为半定规划)。第一次用SDPT3,迭代了很久最后报“Failed”。我以为是模型建错了,检查了半天。后来换成MOSEK,虽然慢点,但稳稳地求出了解。再回过头用SeDuMi,发现也能解,但给出的对偶间隙稍微大一点。所以你看,对于复杂问题,多试几个求解器,不仅是备份方案,更是验证模型正确性的好方法。
1.1 安装与配置:让求解器“听你指挥”
选好了求解器,下一步就是把它“请”进你的MATLAB环境里。对于SeDuMi和SDPT3,这一步最简单,因为CVX安装包里已经自带,你啥也不用管。但如果你想用更强大的商业求解器,或者免费的GLPK,就需要动动手了。
Gurobi/MOSEK(商业求解器):如果你有CVX专业版许可,安装通常是一键式的。运行CVX的安装脚本(cvx_setup)时,它会自动检测并绑定这些求解器的二进制文件。但有时候,特别是公司内网环境,可能需要手动指定求解器库的路径。比如MOSEK,你需要确保它的MATLAB工具箱目录(例如 mosek/10.0/toolbox/r2017a)被添加到MATLAB的搜索路径中。我常用的检查命令是:
which mosekopt
如果返回了路径,说明MOSEK配置好了。Gurobi类似,可以用 which gurobi 来检查。
GLPK(免费求解器):这个稍微麻烦点。首先,你需要去GNU官网下载GLPK的源代码并编译,或者找预编译好的二进制文件。然后,你需要一个叫glpkmex的MATLAB接口包(这个CVX不提供)。把glpkmex编译好并放到MATLAB路径后,才能在CVX里调用。说实话,除非你的问题纯粹是线性或整数规划,且对免费有执念,否则配置GLPK的功夫,可能已经用SeDuMi把问题解完了。
所有求解器都准备好后,在CVX里切换它们就像换电视频道一样简单。在建模之前,用一句命令就能指定:
cvx_solver sedumi % 切换到SeDuMi
cvx_solver sdpt3 % 切换到SDPT3 (默认)
cvx_solver gurobi % 切换到Gurobi
cvx_solver mosek % 切换到MOSEK
cvx_solver glpk % 切换到GLPK
这里有个小技巧:cvx_solver 命令不区分大小写。你可以在脚本的最开始全局设定一次,也可以针对某个特定的模型临时切换。我习惯在调试模型时,在 cvx_begin 和 cvx_end 之间临时指定求解器,这样不会影响其他代码部分的设置。
cvx_begin
cvx_solver mosek % 仅这个模型用MOSEK求解
variable x(n)
minimize( norm(A*x-b) )
subject to
x >= 0;
cvx_end
如果你想一劳永逸,把某个求解器设为默认,可以在设置好后执行 cvx_save_prefs。这样下次重启MATLAB,它还是你的首选。
1.2 问题类型与求解器匹配实战
光知道理论不行,我们得来点实际的。我结合几个最常见的优化问题类型,说说我的选型经验。
场景一:经典的线性/二次规划(LP/QP) 比如投资组合优化(方差最小化)、资源分配问题。这类问题最成熟,几乎所有求解器都支持。如果你的问题规模很大(变量数上万),并且是纯线性规划,Gurobi通常是速度冠军。它的单纯形法和内点法实现得非常高效。如果是二次规划,MOSEK和Gurobi不相上下。但如果你没有商业许可,SDPT3是免费选择里最靠谱的。我做过一个包含几千个变量的供应链调度QP问题,用SDPT3需要2分钟,换成Gurobi只用了15秒,差距明显。
场景二:带二阶锥约束的规划(SOCP) 这是工程中非常常见的一类,比如鲁棒优化、滤波器设计、金融中的VaR约束。SeDuMi、SDPT3、MOSEK都原生支持。我的经验是,对于中小规模的、数值条件较好的SOCP,SeDuMi往往最快。但如果问题“病态”(比如系数矩阵条件数很大),SeDuMi可能会数值崩溃,这时SDPT3的鲁棒性更好。MOSEK则介于两者之间,速度和稳定性都很均衡。我曾经处理过一个无线通信中的功率控制SOCP问题,SeDuMi秒解,SDPT3用了3倍时间,但两者结果在小数点后10位都一致。
场景三:半定规划(SDP)
这是求解器能力的分水岭。像矩阵秩最小化、某些组合优化问题的松弛,都会用到SDP。目前,对SDP支持最全面、最稳定的是MOSEK。SeDuMi和SDPT3也支持,但当矩阵维度变大(比如超过100×100)时,它们可能会遇到内存问题或收敛困难。Gurobi完全不支持SDP。所以,一旦你的模型里出现 semidefinite(),基本就锁定MOSEK了(如果追求免费,可以试试SeDuMi,但要做好调参准备)。
场景四:混合整数规划(MIP) 如果你的变量有一部分必须是整数(比如选择哪条路径、是否启动某个设备),那就进入了整数规划的领域。免费求解器GLPK可以处理小规模的MIP,但一旦问题变复杂,求解时间会指数级增长。商业求解器Gurobi和MOSEK都内置了强大的分支定界、割平面法等算法。通常,Gurobi在纯整数线性规划上更快,而MOSEK在混合整数二次锥规划(MIQCP)上更有优势。记住,整数规划求解时间波动很大,多给求解器一些时间,或者尝试调整它们的启发式策略参数。
2. 求解过程控制:从“黑盒”到“白盒”
选好求解器,把问题丢进去,然后祈祷它给出一个“Solved”的状态——这是很多新手的状态。但当你真正要做产品、发论文时,可不能这么“佛系”。你得知道求解器在干嘛,它遇到了什么困难,以及你如何帮它一把。这就需要对求解过程进行观察和干预。
2.1 读懂求解器的“状态报告”
CVX求解完成后,最重要的输出除了最优解 cvx_optval,就是状态变量 cvx_status。它就像医生的诊断书,告诉你这次求解是否健康。我刚开始的时候,看到除了“Solved”之外的状态就心慌,现在则学会了从容应对。
- Solved:皆大欢喜。解是可靠的,可以拿去用。
- Unbounded(无界):通常意味着你的目标函数设置有问题。比如在一个最小化问题中,目标函数值可以趋向负无穷。检查一下你的约束条件是否写全了,是不是漏掉了某个关键的变量非负约束
x >= 0。 - Infeasible(不可行):模型里的约束条件互相“打架”,找不到一个点能同时满足所有约束。这不一定是你模型错了,有时是数据导致的。一个实用的调试技巧是:逐步放松或移除一些约束,看模型是否变得可行,从而定位矛盾的约束。
- Inaccurate/Solved(不精确/已解决):这个状态很常见,也最值得关注。它意味着求解器找到了一个解,但这个解没有完全达到预设的高精度标准,不过在一个更宽松的误差范围内是可接受的。对于很多工程应用,这个解已经足够好了。你可以检查一下
cvx_optval的值是否合理,并把求出的解代入原约束,看看违背程度。如果误差在可接受范围(比如1e-6),就大胆用。 - Failed(失败):求解器在求解过程中“卡住”了,无法取得有效进展。这可能是问题数值性质太差,或者求解器参数设置不当。遇到这个,首先考虑换一个求解器。如果还不行,可能需要回头检查模型的数学形式,看看是不是可以等价转换一下,让问题变得更“友好”。
- Suboptimal(次优):只出现在整数规划中。求解器找到了一个可行的整数解,但还没证明它是全局最优的(可能因为时间到了,或者你手动中断了)。这个解仍然是可行的,只是可能不是最好的。你可以根据
cvx_optval判断当前解的质量是否满足要求。
养成查看 cvx_status 的习惯,能让你快速定位问题所在,而不是对着错误的结果干瞪眼。
2.2 控制屏幕输出:让日志更清爽
默认情况下,CVX和求解器会在命令窗口输出一堆迭代信息。这对于调试单个问题很有用,但当你把CVX嵌入一个大的循环或算法中,这些刷屏的信息就非常烦人了。这时,你需要让它们“安静”下来。
最简单的方法是在 cvx_begin 后面加上 quiet 关键字:
cvx_begin quiet
% ... 你的模型 ...
cvx_end
这样,求解过程就不会有任何输出。如果你还想保留一些关键信息(比如最终状态和最优值),但又不想看迭代过程,可以用更精细的控制命令 cvx_quiet。
cvx_quiet true; % 全局关闭输出
cvx_begin
% 这个模型求解时不会有输出
cvx_end
cvx_quiet false; % 全局恢复输出
我个人的习惯是,在写脚本做大规模批量计算时,一开始就 cvx_quiet(true)。等到最终结果出来,如果发现某个问题异常,再针对性地关闭quiet模式,单独运行那个问题,查看详细的迭代日志来调试。这能保持工作环境的整洁。
3. 精度调优:平衡“快”与“准”
数值优化没有“绝对精确”,只有“在某个误差容忍度内的精确”。求解器的精度设置,直接决定了它要在计算速度和结果准确性之间做怎样的权衡。调精度,是CVX进阶使用的必修课。
3.2 理解三层精度与 cvx_precision
CVX管理着三层精度,这比直接调求解器参数更友好:
- 求解器精度:告诉求解器“你可以停下来了”的阈值。
- 标准精度:CVX认为问题被“完全解决”的阈值。
- 降低的精度:CVX认为问题被“不精确地解决”的阈值(此时状态为
Inaccurate/xxx)。
默认设置(cvx_precision default)对大多数问题都适用。但有些特殊情况需要你手动干预:
- 你的问题对精度要求极高:比如某些理论证明或高精度数值模拟。你可以使用
cvx_precision high甚至cvx_precision best。best设置会尽可能压榨求解器,直到它无法再改进为止,速度会慢很多,但能逼近机器精度极限。 - 你只想要一个快速的粗略解:比如在一个大算法的初期阶段,只需要一个大概的搜索方向。可以用
cvx_precision low或cvx_precision medium,求解速度会显著提升。 - 你总是得到
Inaccurate/Solved:这可能意味着默认精度对你的问题来说太严格了。你可以尝试稍微放宽标准精度。例如,将标准精度从默认的1e-8放宽到1e-6:
注意上面代码中保存和恢复精度设置的模式,这是避免影响后续其他模型的好习惯。cvxp = cvx_precision([1e-6, 1e-8]); % 设置标准精度为1e-6,降低的精度自动计算 cvx_begin % ... cvx_end cvx_precision(cvxp); % 恢复原有精度设置
精度调整是一把双刃剑。设得太紧,求解慢甚至失败;设得太松,结果不可靠。我的一般原则是:先用默认精度跑,如果结果合理且速度可接受,就不要动它。如果失败了或者太慢,再考虑调整。调整时,每次只改变一个量级(比如从1e-8调到1e-7),并观察求解状态和结果的变化。
3.3 高级参数微调:与求解器直接对话
当你对某个求解器(特别是SeDuMi、SDPT3)非常熟悉,或者从求解器官方文档、社区获得了针对某类问题的特定参数建议时,你可以使用 cvx_solver_settings 这个“终极武器”。这是一个高风险、高回报的操作,用好了能大幅提升性能,用错了可能导致求解器行为异常。
比如,SeDuMi有一个控制其中心路径参数 mu 更新策略的选项 eps。默认是0.9,更激进的值(如0.99)可能让它在某些问题上收敛更快,但也可能更不稳定。你可以这样设置:
cvx_begin
cvx_solver sedumi
cvx_solver_settings('eps', 0.99) % 设置SeDuMi的eps参数
% ... 你的模型 ...
cvx_end
又比如,对于SDPT3,你可以调整最大迭代次数 maxit,如果默认的100次不够:
cvx_solver_settings('maxit', 200)
在使用这个功能前,务必、务必、务必去查阅对应求解器的官方文档,了解每个参数的含义。MOSEK和Gurobi有海量的参数可以调,从预处理策略到并行线程数。一个常见的性能调优是开启多线程计算(如果你的CPU核心多):
% 对于MOSEK
cvx_solver_settings('MSK_IPAR_NUM_THREADS', 4); % 使用4个线程
% 对于Gurobi
cvx_solver_settings('Threads', 4);
设置完后,可以用 cvx_solver_settings(无参数)查看当前求解器的所有自定义设置,用 cvx_solver_settings -clear 来清除它们。
4. 实战排坑与性能优化指南
理论说再多,不如实际踩几个坑。下面是我总结的几个典型场景和优化技巧,希望能帮你少走弯路。
4.1 问题尺度化:让求解器“算得舒服”
很多数值问题求解失败,根源在于问题的“数字尺度”太糟糕。比如你的变量 x 预期值在1e-6量级,而系数矩阵 A 的元素却在1e+9量级,这种巨大的数量级差异会导致求解器内部的数值计算出现严重舍入误差,轻则精度下降,重则直接报错。
解决方案是尺度化。简单说,就是通过变量替换,让所有变量和约束的数值范围都在1附近。例如,如果你知道变量 x 大概在1e-3量级,可以定义新变量 y = 1e3 * x,然后在模型里都用 y 来表示,最后再把解 x = y / 1e3 换回来。对于约束 A*x <= b,如果 A 的某一行特别大,可以考虑两边同时除以一个常数来缩放。
CVX本身有一些自动预处理,但对于极端尺度的问题,手动尺度化是必要的。我处理过一个电路设计问题,电阻值在欧姆级,电流在毫安级,电压在伏特级,直接建模求解器直接“炸了”。后来我对每个物理量都做了归一化(除以其典型值),问题立刻变得稳定可解。
4.2 利用问题结构:加速求解的“秘籍”
不同的求解器内部算法不同,对问题结构的利用能力也不同。将你的问题用最“自然”、最“紧凑”的形式表达出来,往往能获得最佳性能。
- 使用向量化操作:避免在CVX模型中使用循环来添加约束,尽量用矩阵和向量运算。CVX的解析器能更好地识别向量化形式的结构。
% 不推荐 for i = 1:n A(i,:) * x <= b(i); end % 推荐 A * x <= b; - 为二次锥规划使用
norm:如果你要表达形式为||Ax+b||_2 <= c^T x + d的约束,直接使用norm(A*x+b, 2) <= c'*x + d。CVX能将其高效地识别为二阶锥约束,这比你自己写成一系列二次约束要快得多,因为SOCP求解器有专门的高效算法。 - 对称性与稀疏性:对于半定规划中的大矩阵变量,如果它是对称的,确保你声明的是
variable X(n,n) symmetric。如果约束矩阵A非常稀疏,在构建问题时尽量保持其稀疏格式(使用MATLAB的sparse矩阵),这能极大减少内存占用和计算量。
4.3 混合整数规划求解技巧
MIP问题是出了名的难解,求解时间可能天差地别。除了选对求解器(Gurobi/MOSEK),还有一些策略可以尝试:
- 提供初始可行解:如果你能通过启发式方法猜到一个比较好的可行解,可以将其作为“热启动”提供给求解器,能显著缩短搜索时间。
cvx_begin cvx_solver gurobi variable x(n) integer % ... 目标与约束 ... % 假设 x0 是一个已知的可行解(列向量) cvx_solver_settings('Start', x0) cvx_end - 设置合理的时间限制与间隙容忍度:对于大规模MIP,可能不需要追求绝对的全局最优,一个在1%或5%最优间隙内的解也许就能接受。你可以设置
cvx_solver_settings('TimeLimit', 600)来限制求解时间为10分钟,或者设置cvx_solver_settings('MIPGap', 0.01)来在找到距离最优值1%以内的解时就停止。这能在有限时间内得到一个可用的优质解。 - 分解问题:如果可能,看看你的MIP问题是否能分解成更小的子问题,或者是否有某些特殊的结构(如网络流、集合覆盖)可以利用,这有时比直接丢给通用求解器更有效。
最后,保持耐心和实验精神。优化求解,尤其是复杂问题的求解,很多时候是一门“实验科学”。没有一个固定的“银弹”参数组合。我的工作流程通常是:先用默认设置快速跑一遍;如果失败或太慢,就换一个求解器;如果还不行,检查模型尺度和表述;最后,再考虑针对性的精度和高级参数调整。每次调整只变一个因素,并做好记录,这样才能积累起对自己领域内问题求解的直觉和经验。

677

被折叠的 条评论
为什么被折叠?



