
做资源分配、参数拟合、路径规划、排产调度、投资组合这类问题只要本质是“在限制条件下求最优值”都可以放进 MATLAB 的优化算法工具箱Optimization Toolbox里跑。很多人一上来就搜“matlab 最优解函数”或者直接抄一段遗传算法代码结果要么不收敛要么算出来的结果连约束都不满足。这篇文章把工具箱最常用的几个求解器、建模方式、参数含义和排查顺序完整拆一遍重点不是复述官方文档而是告诉你实际跑通一个优化问题到底依次要做什么。和传统视频那种“演示完就结束”的节奏不同我更想让你拿到手的是判断标准。比如 linprog 返回的 exitflag 到底说明什么fmincon 迭代日志里哪个数值最值得盯批量跑数据时为什么单个样例正常、循环起来却报错。如果你之前已经看过遗传算法工具箱那篇可以把本文和它配合起来用先理解确定性求解器能解决什么再理解什么时候要切到 ga 这类启发式算法。1. 优化算法工具箱解决什么问题和遗传算法工具箱有什么区别1.1 先判断你的问题属于哪一类使用优化算法工具箱之前第一步不是写代码而是判断问题类型。因为不同类型对应不同求解器用错了会在起点就埋下隐患。按目标函数和约束条件的形式可以分成下面几类问题类型典型数学形式对应求解器线性规划目标函数和约束都是线性linprog整数规划线性目标 部分变量要求整数intlinprog二次规划目标函数含二次项quadprog非线性规划目标或约束存在非线性fmincon最小二乘目标是残差平方和lsqnonlin、lsqcurvefit多目标多个目标需要折中gamultiobj、fgoalattain为什么要先分类因为求解难度完全不同。线性规划是最成熟的一类大规模也能快速求解整数规划因为需要做组合搜索规模一上去速度会明显下降非线性规划更麻烦可能收敛到局部最优点还依赖初值。把问题归类清楚后面设置参数才不会乱。实际工作里线性规划多用于资源分配、运输调度、生产计划整数规划多用于排班、选品、选址二次规划常见于投资组合优化、最小二乘类问题非线性约束优化则覆盖了大量工程设计、参数反演、结构优化场景。工具箱把这几个方向都收进去了但很多人只记得一个 fmincon遇到线性问题也用 fmincon 硬算其实没必要。1.2 和遗传算法工具箱的分工这里要特别说明一个容易混淆的点。MATLAB 的优化算法工具箱准确的名称是 Optimization Toolbox里面主要是确定性数值优化算法靠梯度、二阶导数、内点法这类数学工具推进搜索。而遗传算法 ga、模拟退火、粒子群这些启发式算法放在另一个工具箱 Global Optimization Toolbox 里也就是很多人常说的“遗传算法工具箱”。两者不是替代关系。确定性算法速度快、精度高、结果稳定适合目标函数连续、可导、约束清晰的优化问题但它对初值敏感容易陷入局部最优。遗传算法不依赖梯度可以在不可导、非连续、离散变量很多的搜索空间里做全局探索但代价是计算量大、结果有随机性不同次运行可能得到不同结果。所以更合理的用法是配合先用遗传算法找到一块有希望的初值区域再把这个初值交给 fmincon 做精细局部优化。这个流程后面单独讲。文章标题里说“结合上期遗传算法工具箱一起食用效果更佳”其实就是这个意思把启发式算法当探路器把确定性算法当精加工工具。1.3 动手前先确认工具箱是否可用很多初学者会忽略环境检查结果代码复制进去后直接报错看到 “未定义函数或变量 linprog”第一反应是代码写错了其实是工具箱没装或者许可没生效。在 MATLAB 命令窗口输入ver输出结果里会列出已安装的工具箱。看有没有 Optimization Toolbox 这一项。如果列表太长也可以直接执行license(test, Optimization_Toolbox)返回 1 表示授权可用返回 0 表示当前环境没有这个工具箱的许可。还有一个更直接的办法which linprog能返回完整路径说明工具箱已安装如果返回 “linprog not found”说明函数不在当前环境里。这一步花不了十秒钟但能避免后续所有排查都建立在错误前提上。尤其是你在实验室电脑、虚拟机、远程服务器上跑 MATLAB 时可能不同机器装了不同组件集确认过环境再进入建模环节。2. 先跑通最简单的线性规划linprog 建模、命令和结果判断2.1 一个最小可运行的 linprog 示例线性规划是优化算法工具箱里最基础也最常用的一类。它的标准形式是min f * x 满足 A * x b Aeq * x beq lb x ub注意工具箱默认是求最小值约束默认是不等式小于等于。如果是大于等于要改写约束矩阵和向量的正负号或者对约束整体取负。举个例子。假设一个生产计划问题两个产品产量分别为 x1、x2目标是最大化利润也就是最小化负利润f [-2; -3]; % 目标函数系数取负号 A [1, 1; 2, 1]; % 约束系数矩阵 b [10; 15]; % 约束上限 lb [0; 0]; % 变量下界 [x, fval, exitflag, output] linprog(f, A, b, [], [], lb, []);这里没有等式约束所以 Aeq 和 beq 传空数组没有变量上界所以 ub 也传空数组。运行后x 是最优解fval 是最优目标值。如果要把这个例子改成基于问题的写法可以用 optimproblem 和 optimvarprob optimproblem(ObjectiveSense, minimize); x optimvar(x, 2, LowerBound, 0); prob.Objective -2 * x(1) - 3 * x(2); prob.Constraints.c1 x(1) x(2) 10; prob.Constraints.c2 2 * x(1) x(2) 15; sol solve(prob);基于问题写法的好处是不用记忆各种空占位参数目标函数和约束直接写数学表达式。坏处是当你处理大规模变量、复杂自定义函数时底层转换会多一层开销而且可调的高级参数不如基于求解器写法直观。我的建议是学习阶段两套都试实际做工程时固定一套主要用法。2.2 如何判断求解结果是否可信很多人只关心 x 和 fval忽略了 exitflag。这是最大的坑点。exitflag 是求解器给的结果状态码用来告诉你这次求解是否真正成功。linprog 返回的 exitflag 常见值可以按下表理解exitflag含义1函数收敛到解结果可用0达到最大迭代次数结果可能不可靠-2未找到可行点-3问题无界目标值没有下限-4目标函数或约束出现 NaN 或 Inf-7搜索方向过小但无法继续如果 exitflag 不是 1建议先不要直接使用结果。比如 exitflag 为 0说明迭代到了上限可能临界收敛也可能离最优解还很远你需要调大 MaxIterations或者检查初值和约束。exitflag 为 -2问题更严重约束之间可能存在矛盾也就是找不到任何一组变量同时满足全部限制条件。除了 exitflag还要看 output.iterations。它告诉迭代次数。如果一次线性规划迭代次数上百甚至上千说明问题规模不小或者约束矩阵有退化现象。正常情况下大多数中小规模的 linprog 问题迭代次数都在几十次以内。2.3 最容易踩的输入格式坑linprog 五个参数占位是最容易漏的地方。常见错误是把约束矩阵写到 A把等式也折算进去结果约束数量对不上。记住一点小于等于约束放 A、b等式约束放 Aeq、beq变量边界放 lb、ub互不混用。第二个常见问题是最大值和最小值。工具箱默认求最小值想最大化利润时把目标系数取负得到最优值后再取负得到真实最大利润。这个操作很简单但越简单越容易忘记。第三个问题是稀疏矩阵。当决策变量很多比如几百个甚至几千个时约束矩阵通常非常稀疏。直接构造稠密矩阵会占用大量内存求解速度也会变慢。建议用 sparse 存储约束矩阵A sparse(A);linprog、intlinprog、quadprog 都支持稀疏矩阵输入。这个细节在模型规模达到几千个变量时非常关键。3. 从线性到整数和二次限制变多但模型更贴近业务3.1 intlinprog 处理整数变量和逻辑选择线性规划只能处理连续变量但业务场景里大量决策本质上是离散的。比如一个工厂要不要启动生产线这是 0-1 决策排班表里的人数必须是整数车辆调度里每次派多少辆车不能是小数。这时候要用 intlinprog。intlinprog 的输入格式和 linprog 很像只是多了一个 intcon 参数用来指定哪些变量必须是整数。看一个例子f [-1; -1; -1]; % 三个变量 intcon [1, 2, 3]; % 三个变量都要求整数 A [2, 3, 1; 4, 2, 3]; % 线性约束 b [12; 10]; lb [0; 0; 0]; [x, fval, exitflag] intlinprog(f, intcon, A, b, [], [], lb, []);如果模型里既有整数变量又有连续变量intcon 只写整数变量对应的索引即可。比如第 1 和第 3 个变量是整数第 2 个是连续变量就写intcon [1, 3]。整数规划比连续线性规划难解得多。同样是几十个变量linprog 可能瞬间完成intlinprog 可能要几秒、几分钟变量更多时可能要几小时。不是工具箱实现不好而是整数组合搜索本身就属于 NP 难问题这是数学层面的现实。所以实际建模时不要把所有连续变量都声明成整数。能保留为连续变量的部分尽量保留整数变量越少求解越快。如果变量数量很大可以设置求解时间上限和节点上限options optimoptions(intlinprog, MaxTime, 60, MaxNodes, 1e5); [x, fval, exitflag] intlinprog(f, intcon, A, b, [], [], lb, [], options);MaxTime 按秒设置到点后返回当前最好可行解虽然可能不是全局最优但很多业务场景够用了。3.2 quadprog 处理带二次目标的问题当目标函数不是一次函数而是包含变量的平方项或交叉项时就需要二次规划。工具箱里对应的是 quadprog。quadprog 标准形式min 0.5 * x * H * x f * x为什么前面有 0.5因为二次项的梯度计算会自然带出 2 倍系数写成 0.5 倍可以消掉这个系数优化问题里约定俗成。举个最小例子H [2, 0; 0, 4]; % 二次项系数H 要对称 f [-1; -1]; % 一次项系数 A [1, 1]; % 线性不等式约束 b 1; [x, fval, exitflag] quadprog(H, f, A, b, [], [], [], []);二次规划在投资组合优化里出现频率很高。目标函数里承担风险的部分通常是收益方差的二次项约束条件是资金分配比例、预期收益下限等。做这类问题时H 矩阵必须是对称矩阵。如果非对称quadprog 可能报错或者结果不符合预期。还要注意 H 矩阵的正定性。如果 H 是半正定二次规划是一个凸问题局部最优就是全局最优quadprog 能稳定求解。如果 H 不定问题变得非凸quadprog 结果可能只是局部最优而且求解过程可能不稳定。遇到这种情况要么调整模型表达式要么把初始搜索点改成多个点逐一尝试。3.3 性能与结果判断整数规划和二次规划都有相对固定的判断思路。整数规划先看 exitflag。intlinprog 返回 1代表找到整数最优解返回 0 往往表示达到时间限制此时返回的是当前最好可行解未必全局最优返回 -2 表示找不到可行整数解需要检查约束之间有没有冲突。用 MaxTime 限制时间时建议额外检查sol.bound或output.gap看看当前解和最优边界之间还有多大差距。这个 gap 越小说明当前解越接近最优。二次规划判断结果时先确认 H 的维度、对称性和正定性再看 exitflag。quadprog 返回 1 通常表示局部最小返回 0 表示迭代达到上限。低阶问题可以直接求解析解做对照高阶问题就看是否满足 KKT 条件。对大多数学习者来说能把约束写对能得到稳定解就已经通过了最基础的验证。4. fmincon非线性约束问题里最常用的求解器4.1 目标函数和约束函数怎么写fmincon 是工具箱里被问得最多的求解器因为它能处理带非线性约束的优化问题。它的输入不仅是矩阵还包括目标函数句柄和非线性约束函数句柄。我们用一个经典的 Rosenbrock 函数做示例加一个非线性不等式约束fun (x) 100 * (x(2) - x(1)^2)^2 (1 - x(1))^2; x0 [-1; 1]; % 初始点 A []; b []; Aeq []; beq []; lb []; ub []; nonlcon (x) deal(x(1)^2 x(2)^2 - 1, []); % 非线性不等式约束c(x) 0 % 非线性等式约束ceq(x) 0这里为空 options optimoptions(fmincon, Algorithm, sqp, Display, iter); [x, fval, exitflag] fmincon(fun, x0, A, b, Aeq, beq, lb, ub, nonlcon, options);这里最容易出错的点有两个。一个是 nonlcon 的返回格式。MATLAB 要求非线性约束函数返回两个输出第一个是 c表示非线性不等式约束要求 c 0第二个是 ceq表示非线性等式约束要求 ceq 0。如果没有某种约束就返回空数组。上面例子我用 deal 函数把两个返回值组合成一行。实际编写时也可以写成完整函数文件function [c, ceq] mycon(x) c x(1)^2 x(2)^2 - 1; ceq []; end第二个是检查约束不等式方向。很多人习惯把约束写成 g(x) 0但 fmincon 要求 c(x) 0。所以写约束函数时先把数学约束整理成左侧减右侧的形式再判断是否满足小于等于零。4.2 算法选择interior-point、sqp、active-set 的作用fmincon 支持多种算法常见的有 interior-point、sqp、active-set、trust-region-reflective。不同算法的网格逻辑不一样适合的问题类型也不一样。作为入门不需要把每个算法都搞清楚。你只要记住一个简单判断默认算法通常是 interior-point适合大规模问题约束处理柔和稳定性较好。sqp 算法在中小规模问题上收敛更快迭代行为更直观适合学习调试。active-set 适合有大量等式约束的问题但对初值更敏感。下面是设置 sqp 的方式options optimoptions(fmincon, Algorithm, sqp);如果换算法后结果变化很大往往说明目标函数或约束存在明显的非凸性求解过程更容易落入不同局部最优。不要寄希望于某个算法一劳永逸多做初值测试更靠谱。当问题规模很大或者目标函数计算昂贵时可以进一步设置梯度函数。给 fmincon 提供解析梯度能显著减少函数调用次数。目标函数返回两个输出function [f, gradf] objfun(x) f 100 * (x(2) - x(1)^2)^2 (1 - x(1))^2; gradf [400 * x(1)^3 - 400 * x(1) * x(2) 2 * x(1) - 2; 200 * (x(2) - x(1)^2)]; end然后在 options 里开启梯度options optimoptions(fmincon, SpecifyObjectiveGradient, true);如果手算梯度容易出错可以用 MATLAB 自带符号计算或者差分方式验证。开发后端的同学可能更关心加速效果但普通学习者先不用在这上面花太多时间等模型跑通了再回头优化。4.3 迭代日志里到底要看什么把 Display 设置成 iter 后每次迭代都会打印一行信息。新手往往不知道看什么。需要重点盯三个量f(x)目标函数值整体趋势应该是下降的偶尔出现波动也不用紧张。Norm of step当前迭代的步长。如果这个值变得非常小比如 1e-8说明算法可能已经停在某个点附近。First-order optimality measure一阶最优性度量是收敛判断的核心。这个值越小说明当前点越接近驻点理想情况是降到 1e-6 以下。fmincon 的 exitflag 也要理解exitflag含义1满足收敛条件结果可用0达到迭代次数限制2步长小于容差但最优性条件未完全满足-1被输出函数中断-2未找到可行点-3目标函数在迭代点处无界当 exitflag 为 1 时结果可用为 2 时要谨慎可能只是卡住了为 -2 时基本可以确定约束不可行。正常情况下多花一点时间把日志跑一遍比盲目调参数有用得多。5. 批量跑优化任务不要只写 for 循环要把实验设计一起做了5.1 为什么单次能跑通、循环却报错很多人学会单条优化命令后下一步就是批量跑数据。比如有几组不同的 A、b、f要依次求解。最常见的做法是写一个 for 循环结果第一个点正常第二个点开始报错或者结果输出错乱。原因很多。可能是某一组数据里约束矩阵维度不一致可能是某些问题本身不可行exitflag 返回 0 或 -2但你的代码里没有处理这些状态直接把 x 拿来继续计算导致后续操作出错。也可能是变量初始化位置不对循环里某一步把上一轮的解带入下一轮改变了问题定义范围。所以我会建议先做一个最小样例集跑通流程再扩展到全量数据。最小样例集至少包含三类数据一组非常简单的可行样例、一组边界约束较强的样例、一组明显不可行的样例。这样写代码时你会被迫考虑成功和失败两条路径。5.2 用结构体保存每次结果并记录失败原因批量优化时不建议把所有结果一股脑存进普通数组。因为每次优化可能返回不同长度的解向量、不同含义的 fval混合在一起非常容易出错。更稳妥的方式是用结构体或者 cell 数组保存。一个典型的批量循环dataList { ... }; % 每个元素包含 f,A,b 等输入 n length(dataList); results struct(x, cell(1, n), fval, cell(1, n), ... exitflag, cell(1, n), errMsg, cell(1, n)); for i 1:n try f dataList{i}.f; A dataList{i}.A; b dataList{i}.b; [x, fval, exitflag] linprog(f, A, b, [], [], [], []); results(i).x x; results(i).fval fval; results(i).exitflag exitflag; results(i).errMsg ; catch ME results(i).errMsg ME.message; results(i).exitflag NaN; end end这里的关键点是使用 try-catch 包住求解器调用。任何一步异常都不能让整个循环中断而是把错误信息记下来等循环结束后统一分析。如果某个结果 exitflag 不是 1就不要把 x 当作有效解参与后续计算。把失败原因集中过滤出来也方便排查failedIdx find(arrayfun((s) ~strcmp(s.errMsg, ), results));5.3 输出命名、日志和断点续跑批量任务规模一旦变大还要关注输出文件的命名和日志记录。很多人把结果存成 mat 文件但文件名只写 result.mat第二次运行时直接把第一次的结果覆盖了。更合适的做法是文件名包含输入参数的关键标识比如save([result_, num2str(i), .mat], x, fval, exitflag);如果需要比较不同参数组合还可以把所有 option、fval、exitflag、运行时间写入 CSV 或表格方便后续分析T struct2table(rmfield(results, x)); writetable(T, optim_summary.csv);如果任务量大到需要中断后继续建议每处理完一个样例就立即保存该样例的结果而不是等全部跑完再保存。这样中断后可以从失败列表里重新执行未完成的部分不需要重新从头跑。这个习惯和学习阶段无关但一旦进入真实项目会节省大量时间。6. 报错、无解、跑太慢三组高频问题的排查链路6.1 遇到错误消息先复制完整文本很多报错看起来吓人但第一行只是简要信息。真正有用的是后面那一段完整错误尤其是函数名、文件路径、错误行号和 cause。排查第一步不是改代码而是把完整错误复制出来看它指向哪个函数、哪个参数。如果错误信息里出现 “Input arguments must be ...” 这类描述多半是参数格式不对。比如矩阵维度不一致、A 的行数和 b 的长度不一致、lb 的长度和变量个数不同。如果出现 “NaN or Inf” 相关提示说明约束矩阵或目标函数里出现了非法数值需要回到生成这些矩阵的上游代码去查而不是在求解器这里徘徊。6.2 “No feasible solution found” 怎么排查这是非常常见的运行结果不是报错但更让人头疼。它表示在给定约束下找不到满足条件的解。排查顺序我一般按下面几步来先检查约束方向。是不是把大于等于约束写成了小于等于导致所有变量都被压在一个不可能满足的区域内。再检查边界。lb 和 ub 是否存在上下界冲突比如要求 x 10 同时 x 5。检查约束之间是否矛盾。比如两个不等式互相排斥。如果问题规模小可以手动把所有约束代入验证。检查是否存在数值异常。约束矩阵里如果出现 NaN 或 Inf求解器无法正常判断可行域。最后考虑约束是否过强。真实业务场景里几个指标同时达到最优可能不现实这时可以放宽某个约束观察可行域是否恢复。还有一个处理技巧是先把所有非线性约束去掉只保留线性约束看能不能得到可行解。如果能说明问题出在非线性约束本身如果仍然无解说明线性约束就没写对。逐层剥离约束能快速定位到罪魁祸首。6.3 “求解时间过长” 先降什么遇到优化问题跑很久不要第一反应就是换电脑。先看几个关键设置。检查是否设置了 MaxIterations。默认上限可能对某些问题太大可以在 options 里设置一个合理上限比如 1000 或 5000。检查问题规模和稀疏度。变量个数从几十膨胀到上万求解时间可能呈非线性增长。能用线性约束替代非线性约束能用稀疏矩阵都会明显提速。检查初始点。fmincon 对初值很敏感一个靠近可行域的初值能减少很多迭代。如果你有历史求解结果把它作为当前初值。检查求解器选择。整数规划问题如果变量数量大intlinprog 会慢可以用启发式算法配合限制条件先得到一个可行解作为目标边界。检查约束中是否存在冗余。很多约束条件其实是等价的或包含的在建模时把冗余约束删掉会减少矩阵大小和迭代难度。如果任务本身是大规模非线性规划可以进一步考虑用并行计算、减少函数调用成本、或者把模型拆分成多层优化。这一步属于进阶优化等前面的参数调整做完再去处理。6.4 结果不满足预期时优先怀疑局部最优最隐蔽的问题是求解器没有报错exitflag 也是 1但结果和业务预期相差很远。这种情况常见于非线性问题也就是求解器收敛到了一个局部最优而旁边可能存在更优的解。处理思路不是立刻换算法而是做多初始点测试。你可以随机生成多个 x0或者用经验值设置几个不同起点的组合逐个调 fmincon比较最终结果。工具箱还内置了全局搜索 MultiStart 和 GlobalSearch它们在 Optimization Toolbox 里可用适合做全局探索。当函数形状比较复杂、局部最优很多时遗传算法 ga 的优势就体现出来了。ga 通过种群搜索不容易被单个局部最优困住。这也是把它作为全局初值搜索工具的原因。7. 和遗传算法工具箱配合先用 ga 找全局趋势再用 fmincon 精优化7.1 什么时候必须改用 ga前面讲的 linprog、quadprog、fmincon 都属于确定性算法对目标函数有比较严格的前提。当出现下面几类情况时建议把遗传算法工具箱用起来目标函数不连续或者存在大量非线性、不可导区域目标函数噪声很大梯度信息不可靠变量包含大量离散值直接用整数规划建模很困难问题存在大量局部最优fmincon 每次跑出的结果都不同你需要多目标优化希望看到 Pareto 前沿。遗传算法的优点是不需要梯度不做可导性假设只要你能计算目标函数值它就能搜索。缺点是收敛慢结果有随机性也不保证每次找到全局最优但它通常能覆盖到比单点搜索更宽广的区域。7.2 混合优化流程示例一个很实用的做法是先用 ga 得到一组较好的解再把这个解作为 fmincon 的初始点。这样的好处是既利用了 ga 的全局搜索能力又利用 fmincon 的局部收敛精度。以 Rosenbrock 函数为例fun (x) 100 * (x(2) - x(1)^2)^2 (1 - x(1))^2; A []; b []; Aeq []; beq []; lb [-3; -3]; ub [3; 3]; nonlcon []; options_ga optimoptions(ga, PopulationSize, 100, ... MaxGenerations, 200, Display, iter); [x_ga, fval_ga] ga(fun, 2, A, b, Aeq, beq, lb, ub, nonlcon, options_ga); options_fmin optimoptions(fmincon, Algorithm, sqp, Display, final); [x_opt, fval_opt] fmincon(fun, x_ga, A, b, Aeq, beq, lb, ub, [], options_fmin);注意 ga 属于 Global Optimization Toolbox执行前先按照前文方法确认该工具箱可用。另外ga 的输入顺序和 fmincon 略有不同第二个参数是设计变量个数所以即使约束为空也要把 2 传进去。这个细节很容易写漏写漏之后会报参数数量错误。混合优化不是万能。如果目标函数计算一次就要好几秒ga 的种群规模大、代数多时会非常耗时。可以先减小种群规模和最大代数得到一个粗略的可行初值再用 fmincon 精修。7.3 建议的落地顺序我做这类任务时一般按下面顺序推进先拿确定性求解器试跑比如 fmincon 加多初始点。如果结果稳定、够用就不用上遗传算法。如果多初始点仍然结果不一致或者目标函数明显不可导、非连续再切换到 ga。先用 ga 找几组有希望的初值再用 fmincon 精优化。最终把整套流程做成函数支持批量输入、结果记录、日志输出。这个顺序能避免一上来就把所有问题都交给 ga节省大量计算资源。优化算法工具箱的核心价值不只是某个函数而是让你学会如何在“确定性方法”和“启发式方法”之间做选择然后把它们组合成一套可落地的流程。真正的生产级任务不是靠某一次运行“跑出最优”就结束而是需要重复测试、参数调整和结果验证。先把单任务跑稳再处理批量最后才考虑混合策略。按这个节奏走你会发现大多数优化问题并没有想象中那么难。