MATLAB算法优化全解析:fminunc、遗传算法与模拟退火用法与选型
简介一份面向 MATLAB 用户的算法优化方法讲解文档适合需要解决非线性规划、极值求解、工程参数寻优等问题的学生、工程师与科研人员也可作为优化工具箱的快速入门参考。内容按算法类别展开先从总体上区分确定性算法与随机算法再逐一聚焦牛顿法、拟牛顿法BFGS/DFP、遗传算法和模拟退火算法讲清每种方法的数学背景、适用条件及在 MATLAB 中对应的典型函数帮助读者避免盲目选算法。每个算法都附有可直接运行的示例代码涵盖 fminunc 的 quasi-newton 选项、ga 的变量数与边界约束设置、simulannealbnd 的初始点与上下界设定等并对 options 结构体的常用字段进行说明读者不仅能理解优化选项的实际含义还能将其迁移到自己的计算任务减少重复调试时间。资源包共包含 1 个 docx 文件整包约 37KB体积小巧、内容紧凑便于在电脑或手机上快速查阅。目前已有 57 人学习适合需要系统掌握 MATLAB 优化方法并希望结合示例快速上手的开发者。1. MATLAB技术算法优化方法四种函数两类思路一次说透MATLAB里的算法优化不是一门玄学关键是把接受梯度的确定性方法与靠随机采样跳出局部极值的随机方法分开来看待。这份整理收拢了牛顿法、拟牛顿法、遗传算法ga与模拟退火simulannealbnd四类调用套路分别落在fminunc、ga、simulannealbnd三个工具箱函数上。无论你是做参数标定、曲线拟合还是给自己的目标函数找最小值最需要想清楚的是问题适合局部优化还是全局优化要不要给边界要不要固定随机种子。它能帮你快速复现代码、跑通实验也适合把示例改造成自己的工程脚本。下面按从局部到全局的顺序逐一拆解。2. 确定性算法fminunc 背后的牛顿法与拟牛顿法从 BFGS 到 DFP2.1 牛顿法与拟牛顿法的分工Hessian 矩阵的价值与代价在无约束优化里极小值点的必要条件是一阶梯度为零∇f(x)0。但光有这个条件还不够还依赖二阶信息判断该点附近的曲率方向。真正的牛顿法把目标函数在当前点做二阶泰勒展开利用Hessian矩阵的逆修正搜索方向所以在极小值附近迭代收敛得非常快。代价也很直接Hessian矩阵要算n维二阶偏导变量一多解析推导和数值差分都变得昂贵机器精度下的二阶差分噪声也会放大。拟牛顿法绕开了这个成本。它不显式计算Hessian而是用一阶梯度的连续变化量去逼近Hessian矩阵或其逆核心是BFGS与DFP两种更新公式。MATLAB把这条路线收敛进了fminunc的quasi-newton算法里也是fminunc默认采用的算法。实际工程里BFGS比DFP更稳因为它的更新能更好地保持矩阵正定性对舍入误差的容忍度更高所以工具箱默认选项基本都以BFGS为基准。注意一个容易误读的点文档里“3.牛顿法”那段示例实际写的就是quasi-newton算法不要把两者直接画等号。下列表格可以直观说明两种算法模式的区别算法模式是否显式需要梯度是否利用二阶信息典型适用场景quasi-newton默认可选不提供也能用有限差分通过一阶梯度近似Hessian目标函数连续且可微变量规模中等trust-region必须提供梯度使用真实Hessian或近似能解析推导梯度追求高精度收敛2.2 fminunc 的可复现调用x0、options 与四种典型参数fminunc的调用并不复杂真正影响结果的是x0和options这两块。x0代表搜索起点options决定搜索的耐性MaxIterations控制迭代上限FunctionTolerance控制收敛粒度Display设为iter能在命令行看到每一轮的目标值变化。为了有一个能直接运行的最小例子把经典的Rosenbrock函数装进匿名函数它的最小值点在(1,1)目标值为0形状是一条深谷很适合验证局部优化器的行为。fun (x) 100*(x(2) - x(1)^2)^2 (1 - x(1))^2; x0 [-1.2, 1]; opts optimoptions(fminunc, ... Algorithm, quasi-newton, ... HessUpdate, bfgs, ... Display, iter, ... MaxIterations, 200); [x, fval, exitflag, output] fminunc(fun, x0, opts);逻辑说明Rosenbrock函数的谷底很窄从(-1.2,1)出发如果迭代方向掌握不好容易在谷壁上反复震荡。fminunc会在每轮迭代中计算搜索方向并做线搜索quasi-newton算法用BFGS公式维护一个随迭代更新的近似Hessian矩阵从而在谷壁之间找到合适的前进方向。最终x会落到(1,1)附近fval接近0。参数说明Algorithm指定拟牛顿算法HessUpdate只在quasi-newton模式下生效bfgs与dfp二选一Display设为iter会在命令行打印每轮迭代次数、函数值和一阶最优性MaxIterations设200是给足余量多数情况下几十次迭代就已经收敛了。如果自己想看看DFP的效果把HessUpdate改成dfp再跑一遍对比迭代日志就能发现BFGS在数值稳定性上更占优。2.3 需要真正二阶信息时trust-region 与梯度的配合当你的目标函数能写出解析梯度且希望利用精确二阶信息时quasi-newton就不够用了。fminunc还有一条trust-region路线它要求你通过SpecifyObjectiveGradient参数显式告知“我提供了梯度”。这里还是用Rosenbrock函数它的梯度可以手写成一个匿名函数传入。funGrad (x) [400*x(1)^3 - 400*x(1)*x(2) 2*x(1) - 2; ... 200*(x(2) - x(1)^2)]; trOpts optimoptions(fminunc, ... Algorithm, trust-region, ... SpecifyObjectiveGradient, true, ... Display, final, ... MaxIterations, 200); [xT, fvalT] fminunc(fun, x0, trOpts);逻辑说明fminunc把梯度函数作为可选的第二个输入接受trust-region算法在每个迭代步内构建一个局部信赖域模型再通过模型求解候选步长并判断是否接受因此它对梯度的准确性更敏感。Rosenbrock函数梯度表达式中第一行的400x(1)^3项来自对100(x(2)-x(1)^2)^2这一项求偏导时产生的交叉项写错任何一个符号都会让收敛方向错乱。参数说明SpecifyObjectiveGradient设为true后fminunc不再用有限差分估计梯度省掉大量函数求值次数并显著降低截断误差。如果读者用的是更高版本的MATLAB且提示选项名不识别检查是否属于Optimization Toolbox的版本差异。这里给出的梯度表达式是按Rosenbrock函数标准形式推出的换成自己的问题时要重新推一遍不能直接套。3. 随机算法ga 遗传算法与 simulannealbnd 模拟退火的完整调用3.1 梯度法之外为什么要给自己留一条全局搜索的退路牛顿法和拟牛顿法本质上是局部优化器它们的搜索结果严重依赖初始点。目标函数一旦存在多个局部极小值梯度下降就会把解“吸”进最近的谷底而你无法确定那个谷底是不是全局最低点。更麻烦的是工程里经常遇到不可微函数、噪声函数或者带离散参数的问题这时候梯度信息要么不存在要么数值差分出来全是震荡。我一般会建议凡是目标函数形态不明、或者初始点猜不准的问题先跑一轮随机算法摸底。遗传算法和模拟退火都属于随机优化它们不依赖梯度而是通过“种群进化”或“退火采样”在大范围搜索空间里保留全局探索能力。代价是收敛速度相对慢、结果带随机性——这正说明为什么需要把随机种子固定下来以及为什么最终往往还要接一轮局部优化做精修。3.2 ga 的调用骨架种群规模、边界约束与停止条件MATLAB的ga函数签名比较长许多初学者第一次看到十几个参数就发怵。完整形式是ga(fun,nvars,A,b,Aeq,beq,lb,ub,nonlcon,options)其中A/b、Aeq/beq是线性约束lb/ub是边界nonlcon是非线性约束占位。需要给Rosenbrock函数设一个搜索盒确保遗传算法不会跑到远离合理区间的区域。fun (x) 100*(x(2) - x(1)^2)^2 (1 - x(1))^2; lb [-3, -3]; ub [3, 3]; gaOpts optimoptions(ga, ... Display, iter, ... PopulationSize, 60, ... MaxGenerations, 150, ... PlotFcn, {gaplotbestf}); rng(42); [xg, fvalg, exitflagg, outputg] ga(fun, 2, [], [], [], [], lb, ub, [], gaOpts);逻辑说明ga先随机生成一个规模为PopulationSize的初始种群每个个体代表一个[x1,x2]候选解接着通过选择、交叉、变异不断生成后代每一代保留适应度高的个体。PlotFcn会在迭代中实时绘制最佳函数值曲线方便肉眼判断是否还有下降趋势。参数说明nvars2对应两个决策变量lb与ub定义搜索盒[-3,3]×[-3,3]遗传算法会在这个盒内初始化种群所有后代也会被边界约束限制在盒内MaxGenerations150是停止条件之一代数跑满就结束rng(42)固定随机数流保证第二次运行结果与第一次完全一致这在写报告和对比实验时是必需的。提示ga返回的exitflag主要有1满足收敛条件、0达到最大代数、-2找不到可行点等几种。结果不佳时先看exitflag再决定是加种群规模还是加迭代代数。3.3 simulannealbnd 的温度调度与重退火机制模拟退火的工程直觉来自金属退火高温时原子运动剧烈系统有能量跳出本地势阱温度慢慢降低后系统逐渐稳定到低能状态。对应到优化里“温度”控制算法接受差解的概率——温度越高越能容忍目标值变得更差的候选点从而跳出局部极小温度越低接受差解的概率越小最终收敛。MATLAB的simulannealbnd把退火过程实现成一个可直接调用的求解器同时支持边界约束。x0 [-1.2, 1]; lb [-3, -3]; ub [3, 3]; saOpts optimoptions(simulannealbnd, ... Display, iter, ... InitialTemperature, 10, ... ReannealInterval, 50, ... MaxIterations, 2000); [xs, fvals, exitflags, outputs] simulannealbnd(fun, x0, lb, ub, saOpts);逻辑说明simulannealbnd从x0出发每一步在当前解附近随机生成一个候选点按Metropolis准则决定接受还是拒绝。InitialTemperature设得越高初期接受坏解的概率越大搜索范围越广ReannealInterval50表示每完成50次迭代就提升一次温度重新加热给算法机会逃离已经陷入的局部谷。参数说明MaxIterations是退火过程的总迭代步数2000是这次示例的上限实际收敛可能在几百步内完成。saOpts里还能加MaxFunctionEvaluations限制函数计算总量以及FunctionTolerance让fval变化小于阈值时提前退出。与ga不同模拟退火不需要种群只跟踪一个当前解因此内存开销很小适合变量规模较大的连续优化问题。4. 选型与对比同一个 Rosenbrock 函数四种算法跑到什么程度4.1 四种算法的输入条件与收敛特征同一个Rosenbrock函数分别用fminunc、ga、simulannealbnd跑行为差异非常明显。下表是我在本地跑这类实验时总结的经验参照具体数值会受MATLAB版本与初始点影响但趋势不变算法类型需要梯度全局搜索能力收敛精度最容易翻车的点fminuncquasi-newton确定性可选弱局部搜索高初始点离谷太远时不收敛fminunctrust-region确定性必须提供弱依赖初始点很高梯度写错直接走偏ga随机不需要强受种群与代数限制种群太小会早熟结果随机simulannealbnd随机不需要较强受初温与重退火影响温度参数不当会长时间空转确定性算法的精度优势在光滑问题上很明显给一个合理初始点fminunc几十步就能收敛到1e-8量级而ga和simulannealbnd往往只能收敛到1e-4量级甚至更差因为它们的主要任务是“找到正确的盆地”而不是在盆地底部做精密定位。这就是为什么我会说随机算法是敲门砖局部优化器才是精度保障。4.2 先随机后局部ga 粗搜加 fminunc 精修一个颠扑不破的工程套路是分两阶段跑先用ga做全局粗搜找到一组“看起来在正确区域”的解再把它当作fminunc的初始点做局部精修。这样既借到了随机算法的全局探索能力又拿回了确定性算法的高收敛精度。rng(42); globalOpts optimoptions(ga, ... Display, off, ... PopulationSize, 80, ... MaxGenerations, 120); [xGlobal, ~] ga(fun, 2, [], [], [], [], lb, ub, [], globalOpts); localOpts optimoptions(fminunc, ... Algorithm, quasi-newton, ... HessUpdate, bfgs, ... Display, off); [xFinal, fvalFinal, eflag, out] fminunc(fun, xGlobal, localOpts);逻辑说明第一阶段把Display关掉不让遗传算法的逐代日志刷屏只需要它返回一个落点第二阶段把这个落点作为fminunc的x0等于告诉局部优化器“我已经在正确的谷里了替我把精确位置找出来”。这样xFinal通常能逼近(1,1)fvalFinal接近0。参数说明ga的PopulationSize提到80比默认50更能覆盖Rosenbrock函数的深谷结构MaxGenerations120足以让种群收敛到正确区域。fminunc的quasi-newton与BFGS组合是精修阶段的默认配置不需要额外提供梯度就能跑。如果目标函数能提供解析梯度第二阶段换成trust-region收敛更快。4.3 用 OutputFcn 把迭代过程拉出来看很多情况下光看最终结果没法判断算法是“稳步下降”还是“碰巧撞对”。MATLAB为多数求解器提供了OutputFcn机制每次迭代都会调用你的回调函数你可以在这里记录迭代次数、目标值、搜索步长等信息。下面是一个可以同时被fminunc和ga调用的回调函数把每次迭代的数据存进基础工作区。function stop myIterLogger(x, optimValues, state) persistent history stop false; switch state case init history []; case iter history(end1, :) [optimValues.iteration, optimValues.fval]; case done assignin(base, iterHistory, history); end end逻辑说明state字段是MATLAB回调的统一约定init表示第一次调用iter表示每次迭代done表示求解结束。persistent变量history在函数内部跨调用保留数据每次迭代追加一行迭代号与目标值。assignin把最终数据写到base工作区变量iterHistory方便后续分析。参数说明使用该回调时在optimoptions里加一行OutputFcn, myIterLogger即可。注意局部函数在脚本中定义有版本限制最省事的方法是把这段代码单独保存成myIterLogger.m放到当前目录。拿到iterHistory后画一条semilogy曲线就能直观看出收敛是平滑下降还是反复震荡。5. 避坑与常见问题五个倒在半路的优化案例5.1 fminunc 中途出现 NaN 或 Inf现象迭代日志里fval突然显示NaN或Inf随后求解器报错退出exitflag返回负数。原因目标函数在搜索路径上进入了未定义区域。常见于目标函数内部包含log、sqrt、除法或指数运算候选点越过边界后触发负数开方、零除或溢出。Rosenbrock函数本身不会有这个问题但换成实际工程函数后很容易碰到。解决在目标函数入口处加一步参数防抖把超出定义域的点映射回安全区间或者直接返回一个很大的惩罚值。同时检查初始点x0是否落在可行域内。fminunc是无约束优化器它不会主动避开你函数里的隐性禁区。5.2 ga 每次运行结果相差很大现象同一份代码连续跑三次最优目标值fval从0.01到0.8都有解的位置也漂移。原因ga默认使用随机数流每次初始化种群和变异操作都不同。种群规模太小时早期几代就可能把优秀基因丢掉造成“早熟收敛”每次都收敛到不同的局部谷底。解决在调用ga之前执行rng(一个固定整数)固定随机数流并调大PopulationSize。如果论文对比实验结果需要完全复现还可以通过InitialPopulationMatrix显式指定初始种群。固定种子后的结果仍然可能不是全局最优但至少让实验可复现、可复核。5.3 ga 的线性约束与边界参数占位混乱现象调用ga时报“Not enough input arguments”明明参数顺序和你记着的一样。原因ga的完整签名是ga(fun,nvars,A,b,Aeq,beq,lb,ub,nonlcon,options)中间每一段都要按顺序占位。很多新手只用边界约束却不给A、b、Aeq、beq占位把lb直接放在第三个参数位置求解器自然无法识别。解决养成写全位置参数的习惯即使没有线性约束也写上[]。比如ga(fun, 2, [], [], [], [], lb, ub, [], opts)这样代码的可读性最高别人接手也不会误会。遇到nonlcon不需要时同样用[]占位。5.4 simulannealbnd 一直不收敛现象迭代数跑了几百步fval几乎没有变化函数计算次数却涨得飞快求解器迟迟不退出。原因InitialTemperature设置过高或ReannealInterval过密导致算法长时间维持高温状态接受大量差解搜索行为接近纯随机游走另一种情况是MaxIterations设得过大算法在已经稳定后还继续空转。解决先调低InitialTemperature让前期探索保持足够但不过度再把ReannealInterval从默认值调大减少不必要的重加热。同时给FunctionTolerance和MaxStallIterations设置合适阈值让目标函数值在连续若干代没有改善时提前终止。5.5 optimset 与 optimoptions 混用导致选项失效现象代码能用但运行时警告“option names might not be compatible”或者某个选项明明设置了却不起作用。原因老版本代码习惯用optimset构造options结构体而新版MATLAB推荐optimoptions。两个结构的字段命名规则不完全一致比如Display、MaxIter在optimset里沿用旧名在optimoptions里则需要用完整拼写。ga和simulannealbnd属于Global Optimization Toolbox选项名与fminunc所属的Optimization Toolbox也不尽相同。解决每个求解器都用optimoptions(求解器名, ...)独立构造选项不要跨求解器复制options结构体。遇到“invalid option name”报错时在命令行敲optimoptions(求解器名)列出当前版本支持的全部选项逐个对照检查。6. 用多起点脚本与 output 结构体给优化结果验货局部优化器最怕的不是迭代慢而是初始点给错了都不知道。我习惯在正式跑优化之前先写一个多起点冒烟测试随机生成十几组初始点逐个丢给fminunc把所有结果收集起来看是否收敛到同一个极值点。如果十几个起点最终落点差异很大说明目标函数存在多个局部极小直接使用单一初始点得出的结果是不可信的。rng(7); starts [-3 6*rand(10, 2); 0, 0]; bestX []; bestF inf; for i 1:size(starts, 1) [xi, fi, ef] fminunc(fun, starts(i, :), localOpts); if fi bestF bestF fi; bestX xi; end fprintf(起点 %2d: ef%d, fi%.6f\n, i, ef, fi); end fprintf(最优解: [%.6f, %.6f], fval%.8f\n, bestX(1), bestX(2), bestF);这段脚本的逻辑很简单rand生成10个分布在[-3,3]内的随机起点再加上(0,0)作为对照每个点都跑一遍局部优化记录最优结果。输出里的ef即exitflag如果出现负数说明某个起点下求解失败这个信息本身就有诊断价值。多起点收敛到同一个点的次数越多结果的置信度越高。得到最终解后还要看output结构体里的几个关键字段iterations表示实际迭代次数funcCount是目标函数总计算次数firstorderopt是一阶最优性指标。对平滑函数来说firstorderopt降到1e-6以下可以认为梯度已接近零解已经稳定。fminunc的exitflag含义也建议记牢1表示一阶最优性满足收敛条件0表示达到最大迭代次数-3表示目标函数返回NaN或Inf。看到0和-3都要回头查原因。另外如果最终结果只是用来做对比实验建议把上面这段多起点脚本存成一个独立.m文件连同每个起点的日志一起保留。我自己的习惯是每接一个优化问题都先跑一遍多起点加上output诊断两关都过了才敢把结果写进报告。从那以后优化这类看似黑匣子的问题我心里基本有底了希望帮到你。本文还有配套的精品资源点击获取