简介:YALMIP-R20210331.zip 是 MATLAB 优化工具箱 YALMIP 在 2021 年 3 月发布的版本,面向科研人员、工程师与学生,解决线性、非线性、整数、混合整数及半定规划等优化模型的快速建模与求解问题。压缩包共含 1390 个文件,主体为 1379 个 m 源文件,覆盖核心算法与接口函数,另有 4 个 txt 文档、3 个 mat 数据、2 个 c 源文件及少量辅助文件,整体仅 1.23MB,轻量易用。已有 268 人学习浏览。解压后可获得完整源代码、说明文档、示例与安装指引,支持用 sdpvar、intvar 等命令以数学表达式直接建模,并可通过统一接口对接 Gurobi、CPLEX、MOSEK 等后端,便于在不同求解环境间切换。对于希望在 MATLAB 中高效开展优化建模、算法验证或二次开发的读者,这套源码包具有直接参考价值。
1. YALMIP-R20210331:为什么MATLAB优化问题值得单独装一个工具箱
一个做控制的朋友过来问,想验证一个Lyapunov不等式,手工推导、整理成严格LMI再调求解器,折腾了一下午还没出数。我给他装了这个YALMIP工具箱,定义变量、写约束、调optimize三行代码,五分钟出了结果。YALMIP是一个纯MATLAB建模层工具箱,负责把线性规划、二次规划、半定规划、混合整数规划等优化问题翻译成求解器能吃的标准形式,实际计算交给下游求解器完成。适合控制理论验证、机器学习稀疏回归、通信信号处理、组合优化等场景,尤其是矩阵不等式、半定规划这类手工建模容易写错的问题。这个R20210331版是2021年3月31日发布的稳定快照,覆盖绝大多数脱离具体领域研究的优化建模需求,下面把安装、语法和踩坑一次说清楚。
2. 安装与环境配置:把 R20210331 跑起来的完整步骤
2.1 前提检查:MATLAB 版本与后端求解器缺一不可
YALMIP 本身是建模层,不包含数值求解内核。这一点特别容易被第一次用的人忽略:装完 YALMIP 后直接跑 SDP 例子,大概率报 “No suitable solver for problem” 或者 “Could not find a solver”。不是工具箱坏了,是因为它没找到能解半定规划的后端求解器。
MATLAB 自带的 linprog、quadprog、fmincon 能覆盖一部分 LP 和 QP 问题,但碰到半定约束就无能为力。所以在使用 YALMIP 前,需要确认两件事:一是 MATLAB 版本不过旧(R2016a 以上比较稳妥),二是准备一个能解 SDP/SOCP 的开源求解器。我一般装过 Parsons 的 sedumi 和 sdpt3,两者都是纯 MATLAB 实现,不需要编译,解压后 addpath 即可用,和 YALMIP 的 R20210331 搭配非常顺。如果跑大规模问题,可以额外装 SCS(通过 mex 编译,但官方有预编译包);如果做混合整数规划,也可以装 GUROBI 或 Mosek,但那是商业授权。
2.2 解压与 addpath:三句话完成安装
假设已经把 zip 包下载到本地,常见做法是先解压到一个固定的工具目录,然后一次性把整个目录递归加入 MATLAB 路径。我用的是一个固定目录 D:\tools\,所有第三方工具箱统一放那里。
% 把 YALMIP-R20210331.zip 解压到目标目录 unzip('YALMIP-R20210331.zip', 'D:\tools\'); % 递归添加整个工具箱目录及子目录 addpath(genpath('D:\tools\YALMIP-R20210331')); % 保存当前路径配置,重启 MATLAB 后仍然生效 savepath第一行解压:第一个参数是压缩包路径,第二个参数是解压目标。如果手头已经有解压好的文件夹,这步可跳过。第二行的 genpath 会自动扫描该目录下所有子文件夹并生成完整路径列表,比直接 addpath 单层目录靠谱得多,因为 YALMIP 内部目录结构里还嵌套着扩展功能子目录。第三行的 savepath 会把当前工作路径写入 pathdef.m;如果这一步提示没有写入权限,常见原因是 MATLAB 安装根目录受系统保护,解决方法是不要强行保存到安装目录,把 addpath 命令写进用户自己的 startup.m,每次启动自动加载。
2.3 验证安装:which 与 yalmiptest 双保险
路径加好后的第一步不是急着写代码,而是确认核心函数是否在预期路径上。
% 查看 YALMIP 核心函数是否被识别 which sdpvar which optimize如果输出显示完整路径,比如 D:\tools\YALMIP-R20210331\yalmip\sdpvar.m,说明建模层已经挂上。如果显示 ‘sdpvar’ not found,说明路径没加对,重新检查 2.2 里两步。接下来建议跑一遍自带诊断脚本:
% 运行安装检查,会逐项测试当前环境下可用的求解器 yalmiptest这个脚本会遍历所有 YALMIP 认识的求解器并做小规模自检,输出每个求解器的可用状态。如果列表里 sedumi 旁边显示 “OK” 而别的显示 “FAILED”,说明 sedumi 已经就绪,其余失败只是因为你没装,不影响使用。我这里通常会再跑一个最小 LP 实际验证整条链路,脚本如下:
x = sdpvar(2, 1); Constraints = [x(1) + x(2) >= 1; x(1) - x(2) <= 1; x >= 0]; Objective = -x(1) - 2*x(2); optimize(Constraints, Objective); value(x)如果输出一个合理的二维解,而不是报错,说明 YALMIP 建模到 MATLAB 自带 linprog 这条通道是通的。注意这里用分号拼排约束,一次建立多个条件,YALMIP 会按列拼接。
3. 核心建模语法:sdpvar、约束、optimize 的底层逻辑
3.1 sdpvar 与变量类型矩阵:连续、整数与二进制
YALMIP 建模的起点是声明变量。最常用的是 sdpvar,它既能表示普通实数向量/矩阵,也能通过第三个参数声明成对称或 Hermitian 矩阵。这个特性是解决 LMI 问题时最省事的地方:手工建模时你需要定义一个对称变量矩阵并额外加对称约束,YALMIP 里直接声明类型即可。
% 5x5 对称矩阵变量 S = sdpvar(5, 5, 'symmetric'); % 2x2 Hermitian 矩阵变量(复数域共轭对称) H = sdpvar(2, 2, 'hermitian'); % 普通实数列向量 x = sdpvar(3, 1); % 二进制变量 b = binvar(5, 1); % 一般整数变量 iv = intvar(4, 1);关键在第三个参数:sdpvar(n, n, 'symmetric') 声明一个对称变量矩阵,求解时内部会自动把它映射到 n(n+1)/2 个独立标量,不会真的存下 n*n 个独立元素,所以变量计数和内存开销都得到控制。如果什么都不写,默认按全矩阵独立元素处理。这里最常见的误用是把方形矩阵直接当对称用,结果解出来的矩阵不对称,后面避坑部分会细说。整数和二进制变量分别用 intvar 和 binvar,二者可以和 sdpvar 混在同一组约束里,YALMIP 会将其归类为混合整数规划,并调用相应求解器(如 GUROBI 或 MATLAB 自带 intlinprog)。
3.2 约束与 optimize:把问题交给求解器前发生了什么
有了变量,下一步是写约束和目标函数。YALMIP 用 >=、<=、== 表达约束关系,而且这几种比较符号在矩阵情形下有明确定义:A >= B 表示 A-B 是半正定矩阵,等价于矩阵不等式约束。这一点是初学者最容易吃惊的地方,因为 MATLAB 原生语法中比较符号只能用于标量逻辑判断,而 YALMIP 对其做了重载。
x = sdpvar(2, 1); Constraints = [x(1) + x(2) >= 1; x(1) - x(2) <= 1; x >= 0]; Objective = x(1)^2 + x(2)^2; ops = sdpsettings('solver', 'fmincon'); diagnostics = optimize(Constraints, Objective, ops);optimize 的核心参数是三个:第一个是约束集合,第二个是目标函数,第三个是求解器设置。调用会触发 YALMIP 预处理流程:展开矩阵约束、检测问题类别(LP/QP/SOCP/SDP/MILP)、调用可用求解器、把约束和目标转换成求解器要求的线性代数形式。返回的 diagnostics 是结果结构体,里面有 problem 字段,0 表示问题成功求解。注意这行代码里用 sdpsettings 指定了 fmincon,如果不想手动指定,YALMIP 会按“当前已装求解器里最合适的一个”自动选择。
常见做法是先把约束和目标都写成标准 KY 变量形式,再交给 optimize 一次成型。初学者容易犯的错是把约束写完后不拼成数组,而是直接传一个约束表达式,结果报错说“not a Set object”。解决方法是把多个约束放进方括号拼成数组,像上面示例那样。如果约束为 0 个,也需要显式传 []。
### 3.3 求解器选择:不同后端适用边界与切换参数 YALMIP 的优势之一是可切换后端。同样是 SDP,不同问题规模、精度要求选择不同的求解器,结果和耗时差异显著。我把自己常用的几种后端参数整理成表格,方便做选择时一眼对比。 | 求解器 | 适用问题类型 | 特点 | 注意点 | |---|---|---|---| | sedumi | LP/SDP/SOCP | 精度较高,内存占用适中,纯 MATLAB 实现 | 对病态问题数值稍敏感,建议配合平衡约束尺度 | | sdpt3 | LP/SDP/SOCP | 稳定性好,适合中等规模 SDP | 收敛速度比 sedumi 稍慢,但迭代更稳 | | scs | 大规模 SDP/SOCP/LP | 基于 ADMM,适合大规模稀疏问题 | 默认精度低一档,需调 tol 参数 | | fmincon | 非凸光滑问题 | MATLAB 自带的 SQP 系列算法 | 不保证全局最优,对矩阵不等式支持较差 | | gurobi/mosek | LP/MIP/QP/SOCP/SDP 全覆盖 | 商业求解器,速度和数值稳定性顶尖 | 需要授权,接入时需留意 mex 版本 | 切换求解器只需要在 sdpsettings 里改 solver 字段。比如我想强行用 sedumi 求一个 SDP 问题: ```matlab ops = sdpsettings('solver', 'sedumi', 'verbose', 1); optimize(Constraints, Objective, ops);verbose 参数控制显示详细程度,0 安静,1 显示基础信息,2 输出更详细的迭代日志。调试阶段我会开到 2 看求解器收敛情况;稳定运行后调到 0。切换求解器时最常见的坑是先前在用 sedumi,后来装了 scs,但代码里忘了改 solver 名,导致 YALMIP 报 “Solver not found”;反过来,如果你显式指定了一个没有装的求解器,YALMIP 会直接报错而不是自动退回。所以要么用自动选择,要么在切换脚本里先通过 available 求解器列表做判断。
4. 实战:用 YALMIP 求解 SDP/LMI 问题的两个典型场景
4.1 最小化矩阵最大特征值:SDP 建模
在控制理论和组合优化里,经常遇到“让某个矩阵的最大特征值尽量小”的问题。这类问题本质上是半定规划,手工推导的拆解工作非常繁琐,而 YALMIP 只需要把矩阵不等式直接写出来即可。考虑一个给定对称矩阵 M,要求一个标量 t 使得 tI - M 半正定,同时最小化 t。半正定条件等价于 t 大于等于 M 的最大特征值,所以求解结果就是 M 的最大特征值。
rng(1); M = randn(5, 5); M = M + M'; % 强制对称,保证特征值全部为实数 t = sdpvar(1, 1); Constraints = [t*eye(5) - M >= 0]; % 矩阵半正定约束 Objective = t; optimize(Constraints, Objective); % 回读结果做交叉验证 t_star = value(t); lambda_max = max(eig(M)); fprintf('YALMIP 得到的最大特征值上界: %.6f\n', t_star); fprintf('直接 eig 计算的真实最大特征值: %.6f\n', lambda_max);代码里 Constraints 中使用了 eye(5) 把标量 t 扩成对角阵再与 M 相减。YALMIP 会识别这个约束为线性矩阵不等式,归类为 SDP 问题,自动交给 sedumi 或 sdpt3 求解。交叉验证部分建议保留:通过 value(t) 取回结果后,再用 MATLAB 裸命令 eig(M) 直接计算最大特征值,两者理论值一致但数值路径完全不同,可以互相验证。如果求解出现问题,这个对比是重要的诊断手段。
这里有个细节:约束中没有像手工推导那样写“M 是对称的”,因为前面已经对 M 做了对称化。如果原问题中 M 本身是对称的,这步省略即可;如果 M 非对称,半正定约束本身就不成立,因为只有对称矩阵才能定义半正定性。
4.2 Lyapunov 稳定性验证:线性系统的控制器权衡
在控制理论里,线性时不变系统 x' = Ax 渐近稳定的充分必要条件是存在对称正定矩阵 P,满足 Lyapunov 不等式 A'P + PA < 0。手工验证时需要先猜一个 P,再检查特征值,费时且不保证找到。把问题交给 YALMIP 后,把不等式直接写出来即可。
A = [-0.8 0.2; -0.1 -1.2]; P = sdpvar(2, 2, 'symmetric'); epsilon = 1e-6; Constraints = [A'*P + P*A <= -epsilon*eye(2); P >= epsilon*eye(2)]; Objective = []; optimize(Constraints, Objective); if optimize(Constraints, Objective) == 0 P_sol = value(P); disp('找到满足 Lyapunov 条件的对称正定矩阵 P:'); disp(P_sol); else disp('系统可能不稳定或条件过严'); end这个例子里我用 epsilon = 1e-6 代替严格的 >0,避免求解器在可行域边界处数值抖动。这是 LMI 求解中的常规做法:半正定约束通常写成 >= 0,但实际求解时留一点余量会让数值更稳。如果既想找 P 又要优化某个控制指标,只需把目标函数改为最小化 P 的迹或最大化某个二次型的上界,比如 trace(P) 越小对应的 Lyapunov 函数越“扁”,体现了不同的控制效果。这种多目标权衡是手工推导时很难做到,而 YALMIP 只需改 Objective 的表达式即可。
4.3 稀疏回归:用 L1 范数做特征选择
稀疏回归是机器学习里经常遇到的基础问题:给定观测矩阵 A 和响应 b,找一个稀疏系数向量 x,使得拟合误差最小。L1 范数能够诱导稀疏解,但目标函数非光滑,直接手工实现梯度投影非常麻烦。YALMIP 里 L1 范数有原生支持,norm(b-A*x, 1) 会被自动展开为等价的线性规划。
A = randn(30, 10); b = A * [1; 0; 0; 0; 0.5; 0; 0; 0; 0; 0] + 0.01*randn(30, 1); x = sdpvar(10, 1); Constraints = []; Objective = norm(b - A*x, 1); % L1 范数目标 optimize(Constraints, Objective); x_l1 = value(x); disp('L1 稀疏回归系数:'); disp(x_l1); % 对比最小二乘解 x_ols = A \ b; disp('最小二乘系数:'); disp(x_ols');运行后可以看到 x_l1 中若干分量接近 0,而 x_ols 几乎全是非零值。这就是 L1 约束的实际效果:在同一个框架里可以顺手把 L2 范数换成 norm(b-A*x, 2) 求解经典最小二乘,再用一个 sdpsettings 切求解器,方法论层面完全不用重新建模型。这里的 A 和 b 如果有真实数据集,模拟随机数据只是为了演示流程,数据驱动应用场景直接替换即可。
5. 避坑与常见问题:R20210331 版本最容易翻车的五个位置
5.1 现象:明明装了 sedumi,optimize 仍报 “No suitable solver”
建模代码写得完全没问题,optimize 却报错说找不到适合的求解器。翻回去检查 sedumi 的路径已经 addpath 过了。后来定位到是因为 YALMIP 并不是通过通用的“求解器列表”来认 sedumi,而是通过 whos 和 information 子目录里的具体文件检测。如果解压 sedumi 时没有把根目录加进路径,YALMIP 扫描不到它的能力文件,就会认为 sedumi 不存在。解决方法是确认你的路径里包含 sedumi 的安装根目录,并重启 MATLAB 重新加载;也可以运行 yalmiptest 观察 sedumi 的状态,如果显示 “FAILED”,检查路径里是否有多个版本的 sedumi 冲突,把旧版本全部移出。
5.2 现象:约束写的是对称矩阵,求解结果却不完全对称
用 sdpvar(3, 3) 声明矩阵变量 X 并写了 X >= 0 的约束,求解后 value(X) 的结果对角线对称但右上和左下略不一致。原因是我没有使用第三参数 symmetric 声明。YALMIP 在半正定约束中不会自动把 X 投影到对称子空间,它会按全矩阵的独立元素进行求解,得到的数值解受到求解器容差影响,不一定完全对称。解决方法是声明时写明 sdpvar(3, 3, 'symmetric'),这样内部变量数从 9 个降为 6 个,求解时自动约束对称性,结果精确满足对称。这个坑在一切矩阵变量场景都会踩,我每次写约束前固定会检查一遍变量声明格式。
5.3 现象:混合整数规划求解特别慢,甚至直接卡死
一个带 30 个二进制变量的混合整数二次规划,用默认自动选择求解器跑了一个小时没出结果。原因是 YALMIP 自动选了 MATLAB 自带 intlinprog,而这个问题规模大、分支定界效率差。排查时发现机器上其实装了 GUROBI,但 YALMIP 的版本没匹配上。解决方法是先用 yalmiptest 逐一确认哪些 MIP 求解器可用,然后在 sdpsettings 中显式指定 solver 为 gurobi 或 mosek,并且把 verbose 开到 2 观察分支进度。从那以后我遇到 MIP 问题都会先检查可用求解器,而不是依赖自动选择。
5.4 现象:优化问题明明有可行解,返回的诊断却显示 Infeasible
一个带多个耦合矩阵不等式的 SDP,理论上存在可行解,但 optimize 返回 problem = 1(不可行)。主要原因是约束条件之间数值尺度差异过大:有的矩阵元素在 1e-12 量级,有的在 1e8 量级,求解器的预处理和数值容差直接被击穿。解决方法是尽量对输入数据做归一化处理,把矩阵约束两边同时除以最大量级数;同时考虑把严格的 >0 改成 >= epsilon*I,给求解器留足可行域间隙。这种数值病态问题在 YALMIP 里不报语法错误,但会表现为“怎么都算不出来”,是最容易消耗排查精力的类型。
5.5 现象:新版 MATLAB 上解出来的值与其他软件结果不一致
同一份代码和数据,在 MATLAB R2021b 上跑出的解和旧版本 R2019a 差异明显。定位到根因是 MATLAB 本身数值库在不同版本上的 BLAS 实现和线程调度不同,这会影响 sedumi 等纯 MATLAB 求解器的迭代路径,但不代表其中一个是错的。验证方法是把两个版本求出的目标函数值做对比,如果目标值差异在 1e-6 以内,可以认为是数值噪声;如果差异明显,需要排查是不是路径里加载了不同版本的 YALMIP 或 sedumi。更进一步的验证方式是固定求解器随机种子,并关闭多线程,尽量保证可复现性。
6. 进阶验证与提速:check 残差、约束尺度调优、横向对比求解器
6.1 check 与 value 双重确认解的真实质量
optimize 返回 problem = 0 不代表模型真正满足所有约束,它只是说明“求解器认为找到了一个满足容差内的解”。因此我每跑一个模型,都会强制用 check 对约束逐条做残差检查。check 返回一个列向量,每个元素对应一条约束的残差量级,越接近 0 表示该约束满足得越彻底。
optimize(Constraints, Objective); residuals = check(Constraints); if max(residuals) < 1e-6 disp('所有约束残差在可接受范围内'); else disp('存在超差约束,需要检查数据尺度或容差设置'); end在这里我同时会取回所有核心变量的值,用独立的 MATLAB 原生计算再验一遍目标函数。如果 value(Objective) 和直接用解算出的目标值一致,就基本可以确认这个解是真的可用,而不是传入了黑匣子里某个逻辑分支。
6.2 sdpsettings 的关键参数与收敛性调优
sdpsettings 不只是切换求解器,它还能控制精度、诊断输出和求解器行为。我最常用的一组设置如下:
ops = sdpsettings('solver', 'sedumi', ... 'verbose', 1, ... 'savesolveroutput', 1, ... 'sedumi.eps', 1e-8, ... 'sedumi.maxiter', 5000);sedumi.eps 是其对偶间隙容差,减小到 1e-8 可以换更高精度;sedumi.maxiter 防止大规模问题迭代次数不足导致提前终止。savesolveroutput 设置为 1 后,YALMIP 会把求解器的原始输出结构体保存在 diagnostics.solveroutput 里,当你怀疑 YALMIP 翻译模型有误时,这是最直接的查阅入口。对 scs 则通常调 scs.abs_eps 和 scs.max_iters。这些参数名带前缀,含义就是“传递给对应求解器的参数”,格式固化。改参数时我一般只动容差和最大迭代数,其他默认值不动,避免调整过度。
6.3 求解器横向对比:用同一模型测 sedumi 与 scs 的真实表现
最后落地到实际提速:同一份 SDP 模型,分别用 sedumi 和 scs 求解,用 tic/toc 记录时间并对比目标值与残差。这样能在具体模型上判断到底哪个后端更适配当前场景。
ops_sedumi = sdpsettings('solver', 'sedumi', 'verbose', 0); ops_scs = sdpsettings('solver', 'scs', 'verbose', 0); % 用相同的约束和目标,分别求解 optimize(Constraints, Objective, ops_sedumi); val_sedumi = value(Objective); res_sedumi = max(check(Constraints)); t_sedumi = toc; % ... 重新初始化变量或用新变量副本 % 另一组结果 optimize(Constraints, Objective, ops_scs); val_scs = value(Objective); res_scs = max(check(Constraints)); t_scs = toc; fprintf('sedumi: 目标 %.6f, 残差 %.2e, 耗时 %.3fs\n', val_sedumi, res_sedumi, t_sedumi); fprintf('scs: 目标 %.6f, 残差 %.2e, 耗时 %.3fs\n', val_scs, res_scs, t_scs);对比时注意用一个新变量矩阵重新建模,因为上一次的 value 值会保留在旧变量中。实际工作中我跑出来的典型结果是 sedumi 在中小规模 SDP 上精度更高,scs 在大规模稀疏问题上速度快一个数量级但精度低一档,所以没有“绝对最好”的求解器,必须按当前数据规模实测。这段 tic/toc 对比是每个新模型的必备流程,从此以后我每次接手新的优化问题,都强制走一遍“变量声明检查、约束重读、check 残差、求解器横向对比”四个步骤,极少再被莫名奇妙的解带偏。希望帮到你。
本文还有配套的精品资源,点击获取