ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MATLAB下OPTI优化工具箱:从fmincon到NLP/MILP建模与选型

MATLAB下OPTI优化工具箱:从fmincon到NLP/MILP建模与选型

简介:这份资源是专为MATLAB用户准备的OPTI优化工具箱,集合了求解线性规划、非线性规划、整数规划、多目标优化等问题的函数、脚本与库文件,适合科研人员、工程师以及算法学习者搭建自己的优化环境。压缩包共128.67MB,内含1152个文件,涵盖430个m脚本、214个hpp头文件、44个cpp源码、69个mat数据文件,以及81个dat-s、44个mod、42个nl和23个mps等优化问题格式示例,另有PDF文档、sample样例与DSDP等开源求解器的原始工程文件。其中mexw64_2_28子包提供针对64位Windows编译的MEX接口,可在MATLAB中直接调用C/C++实现的底层求解算法,省去自行编译的麻烦,也便于处理大规模、自定义约束等多类优化场景。资源保留了完整目录结构和开源许可说明,方便按模块查阅和学习算法原理、对比不同求解器或做二次开发。目前已有3495人学习浏览,是一份完整且实用的优化工具箱资源。

1. 当 fmincon 开始不给面子:OPTI 工具箱到底能救什么

如果你搜过「opti matlab」,大概率和我是同一类人:手里有一堆带约束的优化问题,用 MATLAB 自带的 fmincon 写了好几个版本,结果要么初值一换解就飘,要么大规模问题迭代半天不收敛。我最早接触 OPTI 工具箱就是因为一个带非线性约束的参数辨识问题,fmincon 换了三种算法都卡在局部最优,后来把这个工具箱装上,用 IPOPT 重跑同一套建模,收敛速度和稳定性都不是一个量级。简单说,OPTI 是一个 MATLAB 下的开源优化工具箱,它把几十个底层求解器封装成了统一的建模接口,你只要按它的语法描述问题,剩下的求解器选择、参数映射、退化检测都由它处理。这篇笔记按装包、建模、选求解器、避坑的顺序写,适合正在用 MATLAB 做规划、调度、辨识、最优控制的人照着复现。

2. OPTI 工具箱的底细:求解器集合体与安装自检

2.1 它到底是什么:一个求解器「总装车间」

OPTI 的核心思路不是再发明一个求解器,而是把 LP、QP、NLP、MINLP、LMI 这些不同问题类型对应的成熟求解器统一收编,对外暴露同一套问题描述语言。我拿到 .rar 解压后看到的目录结构,一眼就能认出它的组织方式:source里是 MATLAB 源码,solver里是各求解器的 MEX 编译文件,docs里是官方文档,examples里是官方示例。

它解决的一个很现实的痛点是:如果你要对比 IPOPT 和 NLOPT 在同一个问题上的表现,单独去下载两个求解器,要分别处理编译环境、MEX 配置、依赖库,然后还要自己写两套调用代码。OPTI 把这些事封装掉了,你只需要在optiset里改一个solver字段,求解器就换了,问题描述部分一行都不用动。这就是我把它理解成「总装车间」的原因——你描述问题,它负责找合适的机器。

另一个值得注意的特征是,OPTI 的问题定义用的是「区间约束」思想。传统写法里,等式约束、不等式约束是分开处理的;OPTI 把每个约束写成rl <= g(x) <= ru,等式约束就是rl == ru。这个设计在建模阶段非常省事,尤其是面对<=和>=混在一起的问题时,不用来回乘负号。

2.2 在 MATLAB 里完成安装与自检

解压安装这个步骤看起来简单,但有几个细节直接影响后面能不能跑通。我一般会先cd到解压目录,再一次性把整个工具箱加入 MATLAB 路径,然后立刻运行自检脚本确认求解器是否就绪。

% 进入解压后的 OPTI 工具箱根目录 cd('D:\tools\OPTI'); % genpath 会递归把子目录全部加入搜索路径 addpath(genpath(pwd)); % 运行自检,输出当前环境可用的求解器清单 checkopti

这段代码里,genpath(pwd)是把当前目录及所有子目录加入路径,省去手写几十行addpath的麻烦。checkopti是工具箱自带的环境检测函数,它会逐个尝试加载各求解器的 MEX 文件,并以表格形式显示每个求解器是否就绪。我第一次跑的时候,表格里大部分求解器都是绿的,但 SCIP 显示不可用,这通常是因为 SCIP 在部分 MATLAB 版本上需要额外配置,不影响常规使用。

安装完成后,验证工作还没结束。我习惯再执行一次which ipopt和which nlopt,如果都能返回 MEX 文件路径,说明核心的 NLP 求解器已经挂到 MATLAB 路径上了。如果which返回空,说明刚才的addpath没生效,检查一下是不是解压路径里还有一层嵌套目录。

2.3 目录结构与版本边界

解压出来的目录里,最需要关注的是solver和examples。solver里的.mexw64文件是求解器本体,它们必须与你的 MATLAB 平台匹配,32 位 MATLAB 用不了 64 位版本,这个在旧版 Windows 上翻车概率很高。examples文件夹里的官方示例值得从头跑一遍,每个例子都对应一种问题类型,能快速验证工具箱在你的环境上是完整的。

这里有个边界必须说清楚:OPTI 并不是所有求解器都开源。比如 SNOPT 这种商业求解器,如果不主动购买许可证,自检表格里它会显示不可用,但工具箱主体照常运行。另外,不要贪图方便把整个 OPTI 目录永久扔在pathdef.m里。我遇到过几次函数名冲突,是 OPTI 的子目录里的某个工具函数和另一个工具箱同名,后来我改成只在需要时addpath,用完就移除,冲突就再没出现过。

3. 用 OPTI 建模求解:从问题定义到跑通 NLP 与 MILP

3.1 Opti 对象的核心语法

OPTI 最常用的是Opti对象这种建模方式,它把目标函数、梯度、约束、边界、变量类型全部塞进一个对象里,最后调用solve完成求解。键值对的写法很长,但可读性比一堆散落的参数列表好得多,而且出问题时容易定位是哪个字段写错了。

% 创建 OPTI 问题对象 Opt = Opti('f', f, ... % 目标函数句柄 'grad', grad_f, ... % 目标梯度句柄(可省略) 'ineq', A, rl, ru, ... % 线性不等式:rl <= A*x <= ru 'nl', nl_f, ... % 非线性约束:rl_nl <= nl_f(x) <= ru_nl 'bounds', lb, ub, ... % 变量上下界 'xtype', xtype, ... % 变量类型:C连续 I整数 B二进制 'options', opts); % 求解器选项 % 求解 [x, fval, exitflag, info] = solve(Opt);

这里的f是目标函数句柄,grad是梯度句柄,能提供梯度时求解器会走解析梯度路径,收敛更快也更稳。ineq接收三个参数:矩阵A以及约束下界rl、上界ru,三者构成rl <= A*x <= ru的区间约束。nl字段类似,但约束函数是非线性的。xtype是字符向量,每个字符对应一个变量:C代表连续变量,I代表整数变量,B代表 0-1 变量。最后options是optiset生成的求解器选项。

3.2 NLP 案例:带约束的 Rosenbrock 问题

Rosenbrock 函数是优化领域的经典测试函数,香蕉形山谷对梯度类算法压力很大,非常适合用来体会 OPTI 和 fmincon 的差异。我在这个案例里加了两个约束:一个线性不等式和一个变量上下界,构成一个完整的 NLP。

% 目标函数:Rosenbrock 函数 f = @(x) 100*(x(2) - x(1)^2)^2 + (1 - x(1))^2; % 解析梯度 grad_f = @(x) [ -400*x(1)*(x(2) - x(1)^2) - 2*(1 - x(1)); 200*(x(2) - x(1)^2) ]; % 线性不等式约束:x1 + x2 <= 1.5,写成区间形式 A = [1, 1]; rl = -Inf; % 下界为负无穷,表示该约束没有下限 ru = 1.5; % 上限 1.5 % 变量上下界 lb = [-2, -2]; ub = [ 2, 2]; % 组装并求解 Opt = Opti('f', f, 'grad', grad_f, 'ineq', A, rl, ru, ... 'bounds', lb, ub); [x, fval, exitflag, info] = solve(Opt); fprintf('x* = (%.6f, %.6f), fval = %.6e, exitflag = %d\n', ... x(1), x(2), fval, exitflag);

这段代码里最值得琢磨的是rl = -Inf的写法。OPTI 要求的不是把约束拆成A*x <= ru这种单边形式,而是统一写成区间形式,所以不需要下界的约束就填-Inf。刚开始用的时候我经常在这里踩反向的坑,写成rl = 1.5, ru = Inf,结果约束方向反了,解直接跑飞。

梯度函数里[ ...; ... ]用分号构造的是列向量,这里必须和x的维度保持一致。IPOPT 这类内点法求解器对梯度非常敏感,手推梯度时一个符号错了,求解器可能直接报「不可行」或者收敛到一个完全不相关的点。我一般会给optiset打开导数检查功能,确认手写的梯度没有笔误,这个参数在第 4 章会细说。

3.3 MILP 案例:混合整数线性规划

混合整数规划在排产、调度、选址这类问题里几乎是日常标配。我拿一个简化版的生产计划问题演示:三种产品,每种有利润贡献,受资源约束,其中一种产品要求产量必须是整数。

% 目标系数:最大化利润等价于最小化负利润 f = [-3; -2; -4]; % x1 每单位利润 3,x2 利润 2,x3 利润 4 % 线性约束矩阵:每行一个约束 A = [ 2, 1, 1; % 资源1:2*x1 + x2 + x3 <= 7 1, 2, 3; % 资源2:x1 + 2*x2 + 3*x3 <= 9 1, 0, 0 ]; % 资源3:x1 <= 3 rl = [-Inf; -Inf; -Inf]; ru = [ 7; 9; 3 ]; % x1 和 x2 连续,x3 是整数 xtype = 'CCI'; % 非负约束 lb = [0; 0; 0]; ub = [Inf; Inf; Inf]; % 指定使用 LPSOLVE 求解 MILP opts = optiset('solver', 'LPSOLVE', 'display', 'final'); Opt = Opti('f', f, 'ineq', A, rl, ru, 'xtype', xtype, ... 'bounds', lb, ub, 'options', opts); [x, fval, exitflag, info] = solve(Opt); disp(x); disp(-fval); % 还原为最大利润值

xtype = 'CCI'的含义是:第一个变量连续,第二个变量连续,第三个变量整数。这里必须和x的排列顺序一一对应,写错一个字符,求解器会把整型约束加到错误的变量上。f向量里我直接取了负号,因为 OPTI 和大多数数值优化工具一样只做最小化,最大化问题一律转成最小化来写。

这个例子跑通之后,你会发现换求解器非常便宜。把solver从LPSOLVE改成GLPK,其他代码一行不用动,结果几乎一致。这就是我前面说的「总装车间」价值:建模逻辑和求解器完全解耦,你可以快速交叉验证多个求解器的结果,而不是为了换求解器重写一遍问题。

4. 求解器选型与参数边界:按问题类型决定用谁

4.1 问题类型到默认求解器的映射

OPTI 会自动根据问题结构猜测合适的求解器,但这个猜测并不总是最优的。我整理了一份我实际使用中比较可靠的对照表,按问题类型、常用求解器、适用规模三列给出。

问题类型OPTI 中的常用求解器适用规模与场景
LP(线性规划)LPSOLVE / GLPK / CLP中等规模,结构标准,稳定性优先
QP(二次规划)OOQP / IPOPT中小规模用 OOQP,大规模稀疏用 IPOPT
NLP(非线性规划)IPOPT / NLOPT / SNOPTIPOPT 是大规模 NLP 默认选择,NLOPT 适合中小规模快速求解
MILP(混合整数线性)LPSOLVE / GLPK / SCIP分支定界类问题,变量数不多时 LPSOLVE 足够
MINLP(混合整数非线性)SCIP 等小规模,依赖具体部署环境

用表里这个映射有个好处:当求解结果异常时,你能第一时间怀疑是「问题类型和求解器不匹配」,而不是先怀疑自己的建模。比如一个带有 0-1 变量的 QP 问题,如果自动选择了 OOQP,它本身不支持整数约束,大概率直接报错。这时手动指到 SCIP 或分支定界类求解器才是正路。

4.2 参数调节:从容差到迭代输出

optiset是 OPTI 所有选项的统一入口。它的设计思路是:只要是 OPTI 认识的标准参数名,它会自动映射到底层求解器的对应参数上。我常用的参数有这么几个,每个的边界都值得说清楚。

% 常用参数组合 opts = optiset('solver', 'IPOPT', ... 'tol', 1e-8, ... % 收敛容差 'maxiter', 1e4, ... % 最大迭代次数 'maxtime', 600, ... % 最大求解时间,单位秒 'display', 'iter', ... % 每次迭代都输出日志 'derivCheck', 'on'); ... % 开启数值梯度校验

tol是收敛容差,它决定了解的质量上限,但并不是越小越好。我见过有人为了追求高精度把tol压到 1e-12,结果是 IPOPT 在接近解的区域反复震荡,迭代次数暴涨,最后求解时间翻了好几倍。工程上 1e-6 到 1e-8 足够。display有三个常见值:'off'完全不输出、'final'只在结束时输出一行、'iter'每轮迭代都输出。iter模式在问题不收敛时非常有用,能看到目标函数值的下降速度和约束违反对应的变化趋势,但日志量很大,建议只在排查问题时打开。

derivCheck是值得养成习惯打开一次的选项。它会用有限差分法去和你的解析梯度做对比,如果手推的梯度有符号错误或者下标错位,它会直接提示不匹配。我自己的习惯是:手工推导梯度后先开着跑一小规模测试,确认梯度没问题再关掉,因为数值差分在每次迭代里都做一遍,代价很高。

4.3 手动指定求解器:optiSolver 与 solver 字段

问求解器用哪个、参数怎么设的搜索,大多来自这种场景:问题能跑,但结果不理想,想换求解器试试。OPTI 里换求解器只需要两处改动:optiset('solver', 求解器名),以及如果需要更精细的控制,设置底层求解器专属参数。

% 用 NLOPT 求解同一问题,走局部 MMA 算法 opts = optiset('solver', 'NLOPT', 'display', 'iter'); Opt = Opti('f', f, 'grad', grad_f, 'nl', nl_f, ... 'nl_ineq', rl_nl, ru_nl, 'options', opts); [x, fval, exitflag, info] = solve(Opt); % 查看本次求解实际使用的求解器 disp(info.problemType); disp(info.solver);

info结构体是 OPTI 留给我们的「黑匣子记录仪」。每次求解完,info.solver会告诉你是谁真正解了这个问题,info.problemType会说你这个问题在 OPTI 眼里属于什么类型,info.iterations是实际迭代次数。这个结构体在结果异常时是第一个要看的,因为你以为你在用 IPOPT,但 OPTI 可能因为问题里某个特征把你悄悄映射到了别的求解器,看info才能发现真相。

5. 避坑:安装与求解中我踩过的五个常见问题

5.1 装完执行 solve 报错「No solver found for this problem」

这个报错出现得极早,通常发生在刚装完工具箱、照抄官方示例的时候。现象是solve一执行就抛错,无论什么类型的问题都一样。

原因是 OPTI 在求解前会遍历当前 MATLAB 路径上可用的求解器,如果addpath只加了一层目录,solver子目录里的 MEX 文件没进路径,自检就会显示所有求解器不可用。解决办法是重新执行addpath(genpath('你的OPTI根目录')),然后先跑checkopti,看到列表里出现可用的求解器再执行solve。如果checkopti显示 IPOPT 和 NLOPT 都就绪了,但求解仍然报错,检查问题定义里是否用了xtype包含I或B,整数问题需要整数求解器存在,LPSOLVE 不可用时也会报这个错。

5.2 QP 的 H 矩阵传错系数,结果整体偏离

二次规划的目标函数有一个「0.5 倍」的约定,不同求解器的定义不同,这是 MATLAB 优化生态里最老的坑之一。现象是 QP 问题能正常求出来,但最优值和你的手算结果对不上,而且偏差恰好是 2 倍关系。

原因是 fmincon 系列接口用的是0.5*x'*H*x + f'*x的约定,而 OPTI 内部对二次项的处理要和底层求解器对齐,如果建模时把系数习惯性地乘了 0.5,等于把 H 矩阵实际缩小了一半。解决方法是查一下 OPTI 文档中 QP 目标函数的定义式,按它的约定来写 H。我自己记不住的时候,会构造一个单位矩阵 H 的最简单 QP,先求一次看数值,验证约定后再写正式问题。

5.3 同样的初始点,每次求解结果不一样

非线性规划偶尔会出现这种现象:同一个脚本、同一个初值、跑了两次,最后的解却在两个局部最优点之间跳。原因是局部求解器对初值极度敏感,尤其当问题有多个局部最优且目标函数在某个方向上比较平坦时,数值舍入误差都可能导致最终落点不同。

这个问题的解决路径不是调容差,而是做多起点求解。常见做法是把初值写成若干个随机点,循环求解,取目标函数值最小的结果。我在参数辨识问题里一般用 50 到 100 个随机初值做批量扫描,虽然花时间,但能把陷入局部最优的概率降一个数量级。如果这个代价太高,考虑换全局求解器,比如 NLOPT 里的 DIRECT 算法。

5.4 display 设为 iter 却看不到迭代日志

现象是明明设置了display = 'iter',求解过程屏幕上却什么都没有,看起来像卡死了。其实求解器一直在正常迭代,只是输出被 MATLAB 的命令窗口缓冲机制吞掉了。

原因一方面是部分底层求解器如 IPOPT,它的日志输出走的是标准输出接口,在某些 MATLAB 版本上不会实时刷新;另一方面是先调用了evalc或某些脚本把输出重定向了。解决方法是把display改成'final',至少能看到结束状态;或者用solve之后检查info里的iterations和objective字段,确认求解过程确实发生了。想实时监控迭代过程的话,第 6 章的回调函数是更可靠的办法。

5.5 旧版 MATLAB 里 xtype 报「字符串无法识别」

MILP 建模时xtype = 'CCI'是 OPTI 教程里常见的写法,但某些旧版 MATLAB 环境下,直接传入字符串字面量会触发错误。现象是问题对象创建失败,指向xtype参数附近的解析错误。

原因是 OPTI 的早期 MEX 版本对字符向量的类型检查比较严格,而新版 MATLAB 引入了string类型,如果用的工具箱版本和 MATLAB 版本跨度太大,类型转换会出现不兼容。解决方法很简单,把xtype显式转成char:xtype = char('CCI'),或者把字面量写成双引号之前旧式单引号向量并确认它确实是字符数组。另外一个配套检查是确认solve返回的info里整数变量数量是否和你预设一致,避免「声称是整数解,实际却是松弛解」的隐性故障。

6. 进阶:回调监控与 KKT 结果验证的通用套路

6.1 用回调函数把迭代过程拉出来看

当求解器规模变大之后,凭display输出判断求解状态是不够的,我更习惯写一个回调函数,在每一轮迭代结束的时候把关键指标打印或绘图出来。OPTI 的optiset提供了callback选项,传入一个函数句柄即可。

% 定义回调函数,每次迭代记录目标值和迭代数 function stop = myCallback(x, fval, iter) fprintf('iter %4d: fval = %.6e, x = [%.4f, %.4f]\n', ... iter, fval, x(1), x(2)); stop = false; % 返回 false 表示继续迭代 end % 挂在求解选项上 opts = optiset('solver', 'IPOPT', 'callback', @myCallback, ... 'maxiter', 200); Opt = Opti('f', f, 'grad', grad_f, 'ineq', A, rl, ru, ... 'bounds', lb, ub, 'options', opts); [x, fval, exitflag, info] = solve(Opt);

回调函数里的stop返回值是一个很方便的机制,如果你发现目标值多轮不下降,可以直接在回调里返回true强制中断,省掉干等多轮迭代的时间。这个技巧在长耗时求解里非常实用,我在做最优控制参数扫描时就是靠回调提前剪掉明显不收敛的初值点。

6.2 用 KKT 条件给解做体检

求解器返回的exitflag只能说明求解过程的状态,不能证明解本身没问题。我每次得到一个 NLP 解之后,都会用 KKT 条件做一次独立校验:判断解是否满足梯度投影为零、约束可行、互补松弛三个条件。这个习惯帮我抓出过一次 IPOPT 在边界解上「假收敛」的问题。

% 用数值梯度估算解处的目标梯度 g_num = grad_f(x); % 提取约束矩阵和乘子 lam = info.lambda; % 检查可行性的最大违反对数量级 viol = max([ru - A*x; A*x - rl; 0]); fprintf('max constraint violation: %.3e\n', viol); fprintf('gradient magnitude at solution: %.3e\n', norm(g_num));

info.lambda是 OPTI 返回的拉格朗日乘子估计,配合约束矩阵可以算最优性残差。整个校验逻辑是:如果这个解的约束违反对在 1e-6 量级、梯度范数也在 1e-6 量级,那这个解是可信的;如果梯度范数很大但约束刚好满足,多半卡在了边界奇异点。从那以后,我每次换数据集跑优化,都会强制走一遍这个 KKT 体检,再决定要不要记录结果。这套习惯配合 OPTI 的快速换求解器能力,能让你少走很多白费功夫的弯路,希望帮到你。

本文还有配套的精品资源,点击获取

返回列表