简介:YALMIP是MATLAB环境下一款广泛使用的优化建模工具箱,专为科研、工程和控制领域的开发者设计,能以接近数学表达的方式快速构建并求解线性规划、二次规划、整数规划、非线性规划以及动态优化与最优控制等复杂问题,且支持从建模、求解到结果分析的全流程工作。压缩包提供2023年6月发布的YALMIP-R20230609版本,内含1307个文件,绝大多数为m脚本(1295个),另有少数txt说明、mat数据、yml配置、c源文件和gitignore文件,整体大小约1.08MB,体积较小,方便下载后直接布置到MATLAB路径中;其中m脚本负责核心算法与求解器接口,txt和yml提供环境配置信息,mat数据用于测试验证,c源文件则为底层效率优化服务。目前已有1285人学习/下载,常被用于教学演示、学术研究与算法对比。包内除了完整的主程序、辅助函数、示例文档外,还涉及分支定界等高级求解逻辑,并包含动态系统最优控制与状态估计模块,帮助用户从基础语法学习到高级求解器配置,衔接Gurobi、CPLEX等第三方优化引擎,可直接服务于预测控制、参数辨识和鲁棒优化等实际场景。
1. YALMIP资源下载包:在做优化的人手里到底能干什么
YALMIP资源下载包,是MATLAB生态里做优化建模绕不开的一个工具箱。它不是求解器,而是一层“翻译层”:你用接近数学表达式的语法把优化问题写出来,它负责把这个问题翻译成背后某个具体求解器能够吃的标准形式。很多人第一次搜到这个下载包,是为了解一个线性矩阵不等式或半定规划,因为手写内点法不现实,直接用求解器原生的数据格式又容易出错。装上YALMIP之后,从线性规划、二次规划到半定规划、混合整数规划,描述问题的成本都会大幅下降,你只需要关心模型本身,不用关心求解器接口细节。这篇文章会把从下载、安装、建模、对接求解器到排查问题的完整链路过一遍,适合刚接触该工具箱的研究生和工程师,也适合装过但一直没跑顺的熟手。
2. 把YALMIP资源下载包装进MATLAB:渠道、路径与第一个线性规划
2.1 从哪下载:官方仓库、File Exchange与第三方打包的取舍
先说结论:我一般只会从两个地方拿YALMIP资源下载包,一是官方托管仓库,二是MATLAB的File Exchange官方发布页。
官方仓库的好处是版本最全,代码时刻跟最新提交同步,新模型语法、新求解器适配都是先在这里出现。File Exchange是经过平台审核的发布通道,下载量有保障,适合不太想盯着版本迭代的人。需要留意的是,File Exchange上的包有时会滞后于仓库版本,如果你要用某个刚出的特性,可能还是得去仓库拉最新。
第三方打包,比如某些论坛帖或网盘分流的“一键安装版”,看着方便,但问题很多:打包时间不明,内部文件可能被改过,解压后目录结构残缺,更麻烦的是可能混进旧版本的残留文件,一旦和你的MATLAB环境里的既有路径冲突,排查起来非常折磨人。
判断一个资源包靠不靠谱,有几个简单的习惯:看文件夹里有没有README和CHANGELOG这类说明文件;看主目录下是不是直接就是yalmip相关的一堆.m文件;看解压出来的日期。基本满足这些的,大概率是干净版本。
| 获取渠道 | 优点 | 风险 | 适合人群 |
|---|---|---|---|
| 官方托管仓库 | 版本最新、更新快 | 偶尔有小瑕疵 | 需要新特性的熟手 |
| File Exchange发布页 | 审核过、稳定性好 | 版本可能滞后 | 刚入门的同学 |
| 第三方打包 | 看似省事 | 文件被改、目录残缺、版本不明 | 不建议使用 |
2.2 解压后的路径配置addpath与savepath:装完不生效八成在这
下载解压只是第一步,真正让YALMIP进到MATLAB里的是路径配置。YALMIP本质上是一堆.m文件和少量配套文件,只有MATLAB的搜索路径包含它的主目录,你在命令行敲sdpvar才不会报“未定义函数或变量”的错误。
我习惯的做法是把解压后的整个目录统一放到一个不会随便动的路径下,比如D:\tools\yalmip-master,然后在MATLAB里执行:
% 递归添加YALMIP主目录及所有子目录到MATLAB搜索路径 addpath(genpath('D:\tools\yalmip-master')); % 把当前路径配置持久化保存,下次启动MATLAB仍生效 savepath;参数说明:genpath的意思是把指定目录下的所有子目录一并展开,这是必须的,因为YALMIP的功能分散在多个子目录里。如果只用addpath不带genpath,只能加顶层目录,运行时会漏掉一堆内部函数。savepath则是把当前路径写入MATLAB的路径定义文件,不加这一句的话,当前会话能用,关掉MATLAB再打开就失效了,等于没装。
装完不生效的原因,十有八九是路径没保存:你以为装了,其实每个新会话都要重新addpath。其次是路径里有空格或中文,部分旧版本对这类路径处理有怪问题,尽量用纯英文路径。还有一个容易被忽略的点:如果你把YALMIP目录放在同步网盘里,云同步没完成时MATLAB可能读到半截文件,出现莫名其妙的语法错误。所以本地固定目录是最稳的选择。
2.3 用第一个LP验证安装:optimize的返回与诊断信息
路径配置好之后,先用一个最小的线性规划确认识别有效。我把下面这段当作“安装自检”脚本:
% 定义二维决策变量 x = sdpvar(2, 1); % 约束:两个分量非负,且和为1 constraints = [x >= 0, sum(x) == 1]; % 目标:最小化 x1 + 2*x2 objective = [1 2] * x; % 调用optimize完成建模翻译并求解 diagnostics = optimize(constraints, objective); % 如果求解成功,输出最优解 if diagnostics.problem == 0 disp(value(x)); else % 失败时查看诊断信息 disp(diagnostics.info); end这个脚本里,sdpvar声明的是“待求解的符号变量”,不是数值;constraints用方括号把多条约束拼成一个约束对象;objective是线性的目标表达式。optimize是YALMIP当前的主求解入口,传入约束、目标和可选的求解设置,返回一个诊断结构体。diagnostics.problem为0表示求解成功,这个字段是排查一切问题的第一入口。
跑出来如果能看到两个x1、x2的值并且和为1,就说明YALMIP本身已经能用了。注意这段脚本默认走YALMIP内置的求解器选择逻辑,如果机器上没装任何外部求解器,它会把模型转给自身自带的基础求解器兜底。线性规划这种简单问题一般都能算出来,所以这里卡住基本就是路径配置问题。
2.4 版本兼容性:MATLAB版本与YALMIP新旧版本的匹配
YALMIP是纯MATLAB实现的工具箱,对MATLAB版本的最低要求其实不算苛刻,但新版本确实会逐渐放弃过老的MATLAB。我遇到过最典型的坑是:电脑上装的是新版本YALMIP,但MATLAB是很久以前的版本,一跑就报语法错误,因为新YALMIP用了一些新语法特性。
反过来,太老的YALMIP在较新的MATLAB上跑,偶尔会碰到对某些内置函数行为变化的兼容问题。所以我的建议是:能用新版就用新版,同时留意官方说明里写的MATLAB版本下限;如果你的MATLAB版本确实很老,就去下载当时的较旧发布版,别硬上最新。这个“版本匹配”看起来是小事,但一旦不匹配,排错成本远大于重新下载一个合适版本的资源包。
另外,YALMIP对MATLAB自带工具箱的依赖很少,不需要额外装优化工具箱就能做基础建模。但如果要跑特定求解器,仍然需要单独安装那个求解器的包,这放到第4章细说。
3. 用YALMIP建模:从线性规划到半定规划的三种写法
3.1 sdpvar声明决策变量:维度、类型与命名习惯
凡是YALMIP模型,起点都是sdpvar。它和MATLAB自带的sym变量不一样,sym面向符号数学,sdpvar面向优化建模。你声明sdpvar之后,可以像操作普通矩阵一样做加减乘除、转置、求迹,最后丢给optimize去求数值。
声明方式很直观:
% 列向量:n行1列 x = sdpvar(10, 1); % 矩阵:m行n列 Y = sdpvar(4, 4); % 对称矩阵变量:半定规划里最常用 Z = sdpvar(3, 3, 'symmetric'); % 二元变量:0/1混合整数规划 b = binvar(5, 1); % 整数变量 i = intvar(3, 3);参数说明:第三个参数'symmetric'告诉YALMIP这个矩阵是对称的,只有对角线及上三角部分是自由变量,能显著减少变量数量,半定约束里几乎都会用到。binvar和intvar分别对应0/1变量与整数变量,是混合整数规划的基础。这里“减少变量数量”不是小事情,半定规划求解器对变量数非常敏感,少几个自由变量往往能让求解时间从小时级降到分钟级。
命名上我没有特别的规矩,但建议一个模型里的变量名和论文符号保持一致,比如控制器问题里用P表示李雅普诺夫矩阵,用K表示反馈增益。这样写约束时能直接对照公式,减少“代码里这个符号是啥”的认知负担。还有一个容易被忽略的点:sdpvar声明的变量在求解前不会给你任何数值,如果误把它当成普通数组去调试,打印出来全是符号对象,容易误判为“YALMIP坏了”。
3.2 约束和目标函数的书写:比较运算与求和、trace的用法
YALMIP的约束语法就是MATLAB的比较运算。你写A*x <= b,它不会立刻报错,而是记录下来,等你把所有约束拼成一个对象,再整体交给optimize。
x = sdpvar(3, 1); A = [1 2 3; 2 3 4; 3 4 5]; b = [5; 6; 7]; c = [1; -1; 0.5]; constraints = [x >= 0, A*x <= b, c'*x >= 2]; objective = c'*x; optimize(constraints, objective);这里有两个容易糊涂的点。一是YALMIP默认做最小化,所以如果论文里是最大化收益,你要么在目标前加负号,要么用maximize函数。我个人的习惯是统一写成最小化形式,少记一套API。二是constraints里的逗号其实是“并且”的意思,多个约束写在一个数组里,等价于逻辑与。这个语法在排查时尤其重要:你想去掉某一条约束,直接把那一项从方括号里删掉就行。
对于矩阵变量,求迹用trace,这是半定规划里最常见的目标函数形式。约束里同样可以对矩阵做整体比较,比如Z >= 0对sdpvar矩阵的解释取决于变量声明方式:对声明为symmetric的变量,>= 0会被解释为半正定约束,这是LMI建模的基础;对普通矩阵变量,>=则是逐元素不等式。这两者差别很大,写之前一定要确认变量声明方式。如果你想对非对称矩阵做逐元素约束,就把它向量化,写成Z(:) >= 0,这样语义明确,不容易被版本差异坑到。
3.3 半定约束与LMI:YALMIP相对其他建模工具的核心优势
YALMIP在工程圈里传播开,很大程度是因为线性矩阵不等式。很多控制理论问题最终都归结为求解一组LMI,而手写LMI的求解流程非常痛苦。用YALMIP写LMI,几乎和论文里写的公式一一对应:
% 对称矩阵变量 P P = sdpvar(2, 2, 'symmetric'); % 已知矩阵 A = [-1 0; 0 -0.5]; Q = eye(2); % 李雅普诺夫不等式:A'P + PA < 0, P > 0 constraints = [P >= 0, A'*P + P*A <= -Q]; % 目标:最小化P的迹,得到低增益解 objective = trace(P); diagnostics = optimize(constraints, objective); if diagnostics.problem == 0 P_val = value(P); end参数说明:A'*P + P*A是控制里最常见的李雅普诺夫表达式,<= -Q保证了矩阵不等式严格负定,用非严格形式做近似。constraints里第一条P >= 0,因为P是对称sdpvar,所以这里是半正定约束,不是逐元素非负。这是YALMIP建模时最关键的语义差异,啃下这一点,LMI的大门就打开了。
跑这个模型需要一个能处理半定规划的求解器,比如SeDuMi或SDPT3。如果你的机器上装了其中之一,optimize会自动找到。如果没装,会提示找不到适用求解器,这是正常的,问题不在模型,在于求解器后端没配齐。这种“建模语言和求解器分离”的设计,既是YALMIP的优势也是入门时的困惑来源:你可能模型写对了,但缺一个后端求解器,就会误以为是自己代码错了。
4. 求解器对接与参数设置:让模型从“写出来”到“算出来”
4.1 自动探测求解器和手动指定求解器:sdpsettings的solver参数
YALMIP本身不负责数值优化,它负责把模型翻译成求解器能吃的格式,翻译完交给后端。默认情况下,它会按“已安装且适用”的原则自动挑一个求解器。这个自动探测机制对新手非常友好,但对老手来说经常是个玄学:你永远不知道它选的是哪个,性能波动也说不清。同一份模型在不同机器上跑出不同速度,多半就是自动选到了不同求解器。
所以我在项目里几乎总是指定求解器:
% 设置使用SeDuMi求解半定规划 ops = sdpsettings('solver', 'sedumi'); diagnostics = optimize(constraints, objective, ops); % 或者指定Gurobi,适合大规模LP/QP/MIQP ops = sdpsettings('solver', 'gurobi', 'verbose', 0); diagnostics = optimize(constraints, objective, ops);参数说明:sdpsettings返回一个设置结构体,solver字段指定求解器名字。名字必须是YALMIP认识的,常见的有sedumi、sdpt3、gurobi、cplex、mosek、scs、osqp。指定之后,YALMIP会先做可用性检查,如果求解器没装,会在diagnostics.info里给出提示,而不会悄悄换另一个。
指定求解器的意义在于可复现:同一个模型,不同求解器给出的解可能有微小差异,尤其遇到数值敏感问题时。写论文跑数据时,固定求解器和参数,别人才能复现你的数字。我见过不止一次,两个人用同一份数据,一个人用SeDuMi一个人用SDPT3,结果最优值在小数点后第四位开始不一样,然后互相怀疑对方代码错了。其实谁都没错,是求解器数值行为不同。
| 求解器 | 适用问题类型 | 许可证 | 我的使用习惯 |
|---|---|---|---|
| SeDuMi | SDP/SOCP/LP | 免费开源 | 默认半定求解器 |
| SDPT3 | SDP/SOCP/LP | 免费开源 | 与SeDuMi交叉验证 |
| Gurobi | LP/QP/MIQP/QCP/MIQCP | 商业授权 | 大规模混合整数 |
| MOSEK | 连续优化/整数/锥规划 | 商业授权 | 锥规划首选 |
| SCS | 大规模锥规划 | 免费开源 | 超大模型兜底 |
4.2 三个必调参数:verbose、savesolveroutput和savedebug
sdpsettings里的参数非常多,但我一般只调三个,其他参数用到再查。这三个参数分别解决“看不看过程”“细节在哪”和“怎么复现”三个问题。
ops = sdpsettings(... 'solver', 'gurobi', ... 'verbose', 2, ... 'savesolveroutput', 1, ... 'savedebug', 1); diagnostics = optimize(constraints, objective, ops);verbose控制求解过程的输出详细程度。0是静默,1是简略,2及以上会打印每轮迭代信息。模型跑不出来时,把verbose开到2能让你看到求解器内部的迭代日志,这是判断“模型不可行”还是“求解器数值出问题”的最直接手段。很多新手遇到求解失败,第一反应是检查模型代码,但日志里往往已经写明了原因,比如“infeasible”或“unbounded”。
savesolveroutput设置为1之后,optimize返回的diagnostics结构体里会多出一个solveroutput字段,里面是求解器原生返回的完整信息。很多时候YALMIP包装层的报错比较笼统,真正的细节藏在solveroutput里。savedebug则会把当前模型导出成调试文件,方便你把问题最小化后单独复现。这个习惯在模型很大时会救你一命:不用每次都从头构造数据,直接加载调试文件就能复现问题。
4.3 数值缩放与稀疏性:模型量级如何影响求解器表现
这是最容易被忽略的坑。很多优化模型在数学上完全正确,但算法跑出来结果一塌糊涂,因为数值尺度差太多。举个典型场景:目标函数里既有1e-6量级的项,又有1e6量级的项,求解器内部要同时处理这两个尺度,数值稳定性会急剧下降。
YALMIP本身做了一部分自动预处理,但依赖它不如自己先做模型缩放。我习惯把决策变量和约束都规范化到接近1的量级,单位不一致时提前统一。目标函数乘一个常数不会改变最优解,但会显著改变求解器的数值表现,所以看到结果异常时,先检查模型量级是第一步,不要急着怀疑算法。
稀疏性同样影响效率。同样是几千个变量的线性规划,如果矩阵是全稠密生成的,求解速度会慢很多倍。在YALMIP里构造约束时尽量用稀疏矩阵参与运算,避免把零元素显式填充成大矩阵。对从文件读进来的数据,先处理成稀疏格式再进模型,是低成本高收益的习惯。一行sparse(A),就能让求解器的内存占用和迭代时间都有肉眼可见的改善。
5. YALMIP使用中的常见问题排查:从安装到结果的避坑记录
5.1 安装后仍报错“未定义函数或变量”:路径没保存或目录放错
现象:执行sdpvar时MATLAB提示无法识别该函数。
原因:最常见的是addpath执行了但没savepath,新会话里路径丢失;其次是解压后的目录层级不对,sdpvar所在文件不在你加入的路径里;还有一种是MATLAB当前工作目录被其他同名文件干扰,比如某个子目录里恰好也有一个sdpvar.m,路径优先级把它顶掉了。
解决:先执行which sdpvar,看能否找到文件位置。如果返回为空,重新执行addpath(genpath(...))再savepath;如果找到的位置不对,用pathtool手工调整搜索顺序,把YALMIP目录挪到靠前的位置。最后,确认没有把YALMIP目录放在只读路径或同步网盘目录下,同步未完成时MATLAB可能读到半截文件,报出奇怪的语法错误。
5.2 明明装了求解器却提示探测不到:求解器没加进MATLAB路径
现象:YALMIP能建模,但optimize提示没有可用求解器,或者指定了solver仍然报not found。
原因:求解器是独立安装的,很多人只装了YALMIP,以为自带了求解器。YALMIP的核心包本身不带任何外部求解器,它只是一个建模层。即使单独装了求解器,求解器的.m文件路径没加进MATLAB路径,YALMIP也探测不到。
解决:用which sedumi确认求解器是否在路径里。不在就参照YALMIP的安装方式,把求解器目录同样addpath。另外,检查求解器版本与YALMIP的兼容性,太老的求解器可能不在YALMIP的探测列表里,这时候要么更新求解器,要么在sdpsettings里强制指定名称去调用。
5.3 求解结果全是NaN或inf:不可行、无界还是数值爆炸
现象:optimize返回的problem不为0,value(x)得到NaN,或某些分量是inf。
原因:模型本身不可行、无界,或者虽然存在最优解但数值条件太差。三种原因对应表现不一样:不可行通常诊断信息里会提示infeasible;无界会提示unbounded;数值问题则会看到求解器迭代数异常或残差不收敛。
解决:先用diagnostics.info确定问题类型。如果是不可行,把约束逐个放开,定位哪一条约束和已有约束冲突,这是最花时间的步骤。如果是无界,检查目标方向是否写反,最大化问题写成最小化很可能导致目标无下界。如果是数值问题,按第4章的缩放方法重新整理模型量级,或者换一个数值稳定性更好的求解器,比如把SeDuMi换成SDPT3交叉验证。
5.4 结果与手算不一致:目标符号、约束方向与check残差
现象:模型求解成功,但某些变量值和手工计算对不上,甚至目标函数符号相反。
原因:YALMIP默认最小化,目标写反符号是最常见的翻车原因。另一个常见点是把约束方向写反,比如原问题是A*x <= b,写成A*x >= b,结果完全变样。这两个问题在代码里非常隐蔽,因为YALMIP语法接近自然语言,人往往按论文里的公式直接抄,却不小心把不等号反转了。
解决:求解后用check(constraints)查看每个约束的残差。这个函数返回一个向量,每个元素对应一个约束的违约程度,理论上的值应该在容差范围内。如果某个约束残差明显偏大,先检查那一行是不是写反了方向。另外,对比目标函数值,判断是最大化还是最小化,必要时用maximize或统一加负号。我自己的习惯是每次跑完模型,都顺手把check的结果打印出来看一眼。这是最便宜的成本,能拦住一大半“结果看起来合理但实际是错的”的暗坑。
6. 把YALMIP当实验平台:几个我一直在用的验证习惯
6.1 随机数据与固定种子:做可复现实验的基本功
优化实验最怕两个问题:一是随机生成的数据每次不一样,导致前后两组结果没法对比;二是没有固定求解器,今天跑出来和明天跑出来差一点。我的做法是在脚本开头固定随机种子,同时把求解器固定住,这样生产出来的结果才是可复现的。
% 固定随机种子,保证每次实验数据一致 rng(20240411); A = randn(50, 30); b = randn(50, 1); c = randn(30, 1); % 构建并求解 x = sdpvar(30, 1); constraints = [x >= 0, A*x <= b]; objective = c'*x; ops = sdpsettings('solver', 'gurobi', 'verbose', 0); diagnostics = optimize(constraints, objective, ops);这里rng里的种子值本身没有特殊含义,只是一个固定数,能让你在不同时间得到同一份随机数据。数据生成之后,建议先存成.mat文件,后续实验直接加载,避免种子被中间步骤打乱。我就吃过这个亏:脚本里有一处用了随机数生成,忘了固定种子,结果每次跑出来的实验对比都轻微漂移,浪费了整整半天时间。
6.2 用check和diagnostics做断言:让脚本自己报错
手动看输出容易漏,我会在脚本里加断言,跑完自动判断。比如要求diagnostics.problem为0,约束最大残差小于1e-6,否则直接error。这样批量跑实验时,哪个模型挂了会立刻暴露,不用等最后人工翻数据。
% 断言求解成功 assert(diagnostics.problem == 0, diagnostics.info); % 断言所有约束残差在容忍范围内 residual = check(constraints); assert(max(residual) < 1e-6, '约束残差过大');这两个断言组合起来,既检查了求解状态,又检查了约束满足度,基本能覆盖日常90%的模型质量问题。再加一个输出最优值的习惯,整个实验流程才闭环。我现在每次跑完模型都固定走这三步:看problem、看check、看value。这个习惯帮我避开了很多“自认为正确但实际有隐患”的坑,也让我在对比不同求解器结果时有个统一标准。希望这些经验对你有用,少走几步弯路。
本文还有配套的精品资源,点击获取