☰
Matlab模拟退火与fmincon混合优化:全局搜索+局部精修实战
2026/10/13 1:13:02 网站建设 项目流程

写这篇东西的起因是我前阵子在一个参数标定项目里被Matlab的fmincon折磨得不行:目标函数是仿真系统一跑就是几分钟的黑盒输出,带回声、有噪声,还带着一堆局部极小值。初始点稍微偏一点,结果就稳稳陷在某个盆地不出来。后来把模拟退火算法(Simulated Annealing)和优化工具箱组合起来用,先用全局搜索打底,再用fmincon精修,效果完全不一样。这篇就是我对这套方案的完整复盘,包含工具箱选型、simulannealbnd核心参数、三个能直接跑的案例,以及踩坑后的调参思路。

1. 为什么需要模拟退火这类全局优化算法

1.1 基于梯度的优化器并不万能

先聊点实际的。大部分人在Matlab里接触优化,第一个想到的就是fmincon。它的原理是基于梯度信息,沿着最速下降方向或者拟牛顿方向一步步爬到邻近的极小点。对凸函数、光滑函数、初始点比较靠谱的问题,它又快又准,确实是工程利器。

但工程里的优化问题往往没这么讲武德。目标函数可能是仿真结果直接算出来的,连表达式都拿不到;可能存在噪声,数值微分出来的梯度全是乱跳;更常见的是多峰函数,到处都是局部极小值。这时候fmincon本质上是“站在山腰往山下走”,它只能看到脚下的坡,看不到远处的谷。你换个初值,它就可能爬到另一个完全不同的局部坑里。

模拟退火解决的就是这个痛点。它不依赖梯度,不要求函数光滑,也不会看见局部极小就一头扎进去。核心思路特别朴素:先到处乱逛,逛得差不多了再慢慢收网。实际用下来,它就是给fmincon这类局部优化器打前站的最佳搭档。

1.2 模拟退火的核心机制:允许“坏解”出现

模拟退火这个名字来源于冶金学里的退火工艺:金属加热到高温后慢慢冷却,原子在高温下剧烈运动,冷却过程中逐渐排列成低能量的规则晶格。算法就是把目标函数当成能量函数,把一组参数当成系统状态,通过模拟“加热—冷却”的过程来找能量最低的状态。

关键机制叫Metropolis准则。每次迭代随机扰动当前解,得到一个新解。如果新解比当前解好,直接接受;如果新解更差,也不是直接扔掉,而是以一定概率接受它:

P = exp(-(ΔE) / T)

其中ΔE是新解和旧解的能量差,T是当前温度。温度越高,接受差解的概率越大;温度越低,这个概率越小。也就是说,算法在前期“体温”高的时候允许翻山越岭往坏地方走,借此逃出局部陷阱;后期“体温”降下来,就老老实实做局部精修。

很多初学者不理解为什么要接受差解。你想想登山的场景:你想从A山走到更高的B山,中间隔着一条山谷,如果只允许往上走,你永远到不了B。接受差解,就是允许你先下到山谷,再爬到B。这就是全局搜索和局部搜索的本质区别。

1.3 全局优化工具箱的定位

Matlab里有一个常见的误会:把“优化工具箱”当成所有优化函数的统称。其实官方划分很清晰:

  • Optimization Toolbox:主要是基于梯度的局部优化和线性、二次规划,fmincon、fminunc、linprog、quadprog都在这。
  • Global Optimization Toolbox:专门做全局优化,模拟退火对应的simulannealbnd、遗传算法ga、粒子群particleswarm都在这里。

这两个工具箱一般是配合使用的。模拟退火负责大范围找好区域,fmincon负责在好区域里精确收敛。别一门心思只盯着一个函数,最实用的方案往往是“全局粗搜 + 局部精修”的混合策略。

2. Matlab优化工具箱全景与模拟退火的选型

2.1 常用求解器横向对比

先给一张表,把几个主流求解器的特性摆出来,方便你选型。

求解器搜索机制是否全局约束支持典型适用场景
fmincon梯度下降/拟牛顿否,局部边界、线性、非线性约束凸问题、初值靠近最优解、有精确梯度
fminunc梯度下降/拟牛顿否,局部无约束光滑无约束问题
simulannealbnd模拟退火是只原生支持边界约束黑盒、多峰、不可导、中等维度
ga遗传算法是支持约束,但处理非线性约束较麻烦离散变量、混合整数、复杂约束
particleswarm粒子群是主要支持边界约束连续变量、计算量可控、多峰搜索

这张表最关键的信息是:fmincon求的是“局部最优”,simulannealbnd求的是“尽量靠近全局最优”。如果问题本身是凸的,别杀鸡用牛刀,直接fmincon就行。如果问题肉眼可见地多峰、黑盒、不可导,就别在fmincon的初值上反复赌运气了,直接上模拟退火。

2.2 什么场景优先选模拟退火

我自己的判断标准很直接,满足下面两三条就优先考虑SA:

  • 目标函数没有解析表达式,只能通过仿真、实验或者外部程序返回值。
  • 目标函数存在多个极值点,而且你不知道哪个是全局。
  • 函数不可导、不连续,或者对参数变化很敏感。
  • 变量维度不算太高,一般几十维以内,太高的话SA收敛会非常慢。
  • 你愿意用几分钟甚至几小时的全局搜索时间,换一个稳定的最终方案。

遇到过很多项目,目标函数跑一次要好几分钟,这种情况下你不可能让SA像无头苍蝇一样乱撞几万次。我的做法是先用低精度的代理模型或者降采样版本跑SA,确定最优参数大概在哪个区域,再用高精度仿真在那个小范围里做局部优化。这个思路比单纯堆迭代次数管用得多。

2.3 一个必须澄清的误区:工具箱归属

去网上搜“Matlab模拟退火”,很多人会直接输入simulannealbnd,结果报错“Undefined function”。原因不是函数名记错了,而是没有安装Global Optimization Toolbox。

检查工具箱是否存在,可以用:

license('test', 'GADS_Toolbox')

如果返回1,说明有全局优化工具箱;如果返回0,说明只有基础的Optimization Toolbox。另外还可以在Matlab里运行ver,看输出列表里有没有Global Optimization Toolbox。

这个细节特别容易坑到新手。我见过有人折腾了半天代码,结果发现是工具箱都没装。如果你所在的公司或者学校只有Optimization Toolbox,也别灰心:模拟退火算法本身不复杂,自己写一个核心循环也就几十行,后面第4节我会给你一个能跑的手写版本。

3. simulannealbnd核心参数与实操细节

3.1 目标函数怎么写给SA才不踩坑

simulannealbnd对目标函数的要求比fmincon宽松很多,但有几个基础规则必须遵守。第一,输出必须是标量;第二,尽量别返回NaN或Inf;第三,输入x的维度要和初始点x0保持一致。

一个典型的匿名函数写法:

fun = @(x) (x(1)-2)^2 + (x(2)+1)^2 + 5*sin(3*x(1)); x0 = [0, 0]; lb = [-10, -10]; ub = [10, 10]; [x, fval] = simulannealbnd(fun, x0, lb, ub);

这里的x是行向量还是列向量,取决于你的x0。如果你传递的是行向量,SA内部会把x当行向量处理;目标函数里要注意x(1)、x(2)的顺序别写反。

另外一个经验是:如果目标函数内部有报错风险,比如根号下负数、对数里出现0,最好在函数开头加保护逻辑,返回一个大数值而不是让程序崩溃。SA会在搜索过程中频繁试探边界和极端位置,目标函数必须足够健壮。

3.2 边界约束与非线性约束处理

simulannealbnd原生支持的是边界约束lb和ub,这个很简单。没有下界或上界时传[]就行:

[x, fval] = simulannealbnd(fun, x0, [], ub);

但实际工程问题经常有非线性约束,比如某个指标不能超过阈值、某种资源总和不能超限。SA本身不直接支持非线性约束,我通常用外点惩罚函数来处理。基本思路是:把约束违反量放大后加到目标函数上,让算法在搜索时自动避开不可行区域。

惩罚函数的标准形式是:

cost_pen = cost_original + lambda * max(0, g(x))^2

其中g(x) <= 0是不等式约束,lambda是惩罚系数。lambda太小,约束会被无视;lambda太大,会扭曲目标函数形状,让SA的收敛变得很困难。我一般从1e3开始试,逐步增大到1e5量级,直到最优解的约束违反量小到可以忽略。

3.3 温度、冷却率、迭代次数怎么调

模拟退火的效果很大程度取决于温度调度。温度高,前期探索范围大;温度低,后期容易陷入局部。降温太快,跟没退火一样;降温太慢,计算时间会爆炸。

在simulannealbnd里,这些因素分散在几个参数中:

参数作用经验值
InitialTemperature初始温度,决定前期接受差解的概率根据目标函数值量级调整,常用10~1000
TemperatureFcn温度下降函数,@temperatureexp、@temperaturefast默认常用指数降温
ReannealInterval多少代后重新升温再降温,防止过早收敛100~1000
MaxIter最大迭代次数500~10000
MaxFunEvals最大目标函数调用次数通常比MaxIter大几倍
TolFun目标函数变化小于该值则停止1e-6~1e-8

如果你完全不知道初始温度怎么设,有一个相对靠谱的校准方法:先固定一个初始解,做若干次随机扰动,统计目标函数增量的平均值Δ_mean,然后设定初始接受率P0,比如0.8,用公式反推:

T0 ≈ -Δ_mean / ln(P0)

意思是刚开始有80%的概率接受那些变差的解。这样算法前期才有足够的探索能力。很多默认参数跑不出好结果,问题往往就出在初始温度和目标函数值量级不匹配。

3.4 saoptimset关键选项速查

老版本用saoptimset设置参数,新版本也可以继续用。我给一个常用的配置模板:

options = saoptimset( ... 'Display', 'iter', ... 'MaxIter', 3000, ... 'MaxFunEvals', 30000, ... 'InitialTemperature', 100, ... 'TemperatureFcn', @temperatureexp, ... 'ReannealInterval', 200, ... 'TolFun', 1e-8); [x, fval] = simulannealbnd(fun, x0, lb, ub, options);

如果用的是较新的Matlab版本,也可以改成optimoptions方式,参数名基本一致:

options = optimoptions(@simulannealbnd, ... 'Display', 'iter', ... 'MaxIterations', 3000, ... 'InitialTemperature', 100);

有一个细节:MaxIter和MaxFunEvals是两套完全不同的终止条件。MaxIter是迭代轮数,每一轮可能调用多次目标函数;MaxFunEvals是目标函数调用次数上限。如果你的目标函数跑一次特别贵,更应该限制MaxFunEvals,而不是MaxIter。

4. 三个可复现案例:从单目标到组合优化

4.1 案例A:多峰函数寻优,和fmincon对比

第一个案例用经典的Rastrigin函数做测试。这个函数的特点是:全局最小值在原点,但周围布满了密密麻麻的局部极小点,专门用来考验全局优化算法。

clear; clc; rng(1); fun = @(x) sum(x.^2 - 10*cos(2*pi*x) + 10); lb = [-5, -5]; ub = [5, 5]; x0 = [3, 4]; % 模拟退火 options = saoptimset('Display', 'off', 'MaxIter', 5000, ... 'InitialTemperature', 50, 'TemperatureFcn', @temperatureexp); [x_sa, f_sa] = simulannealbnd(fun, x0, lb, ub, options); % fmincon做对比 opts_fmin = optimoptions(@fmincon, 'Display', 'off', 'Algorithm', 'sqp'); [x_f, f_f] = fmincon(fun, x0, [], [], [], [], lb, ub, [], opts_fmin); fprintf('SA: x=[%.4f %.4f], fval=%.6f\n', x_sa(1), x_sa(2), f_sa); fprintf('fmincon: x=[%.4f %.4f], fval=%.6f\n', x_f(1), x_f(2), f_f);

在我本地的测试里,SA基本能稳定找到接近(0,0)的位置,fmincon从(3,4)出发则容易停在一个局部极小点,函数值明显更大。这个案例想说明的不是fmincon不行,而是它确实缺乏“跨过山谷”的能力。如果换成凸函数,fmincon的表现反而会更好,所以选求解器一定要看问题形状。

4.2 案例B:带非线性约束的工程参数优化

第二个案例更贴近工程:目标函数很简单,但有一条非线性约束。问题定义如下:

min (x1-3)^2 + (x2-4)^2

约束条件:x1^2 + x2^2 <= 16

这个问题的解析最优解大概在(3, sqrt(7))附近,也就是约束边界上。先用惩罚函数把约束带进目标函数,跑SA,再用fmincon在SA的结果上精修。

clear; clc; rng(2); obj = @(x) (x(1)-3)^2 + (x(2)-4)^2; con = @(x) x(1)^2 + x(2)^2 - 16; lambda = 1e4; penObj = @(x) obj(x) + lambda * max(0, con(x))^2; lb = [-10, -10]; ub = [10, 10]; x0 = [0, 0]; options = saoptimset('Display', 'off', 'MaxIter', 4000, ... 'InitialTemperature', 100); [x_sa, f_sa] = simulannealbnd(penObj, x0, lb, ub, options); % 用fmincon做约束精修 nonlcon = @(x) deal(con(x), []); [x_refine, f_refine] = fmincon(obj, x_sa, [], [], [], [], lb, ub, nonlcon); fprintf('SA粗搜索: x=[%.4f %.4f], 约束值=%.4f\n', x_sa(1), x_sa(2), con(x_sa)); fprintf('fmincon精修: x=[%.4f %.4f], 约束值=%.4f\n', x_refine(1), x_refine(2), con(x_refine));

这里要注意,fmincon精修时使用的是原始目标函数,不再带惩罚项,而是把约束正式传给nonlcon。这样SA负责找到边界附近的可行区域,fmincon负责把解精确落到边界上,两者分工明确。

我带过一个项目,最初只靠惩罚函数跑SA,结果总是差那么一点,后来改成这种“SA找区域,fmincon找精确解”的两段式,精度瞬间就上去了。

4.3 案例C:手写SA求解TSP,自定义邻域是关键

simulannealbnd并不是为离散组合优化设计的,比如旅行商问题(TSP),它的变量是城市访问顺序,是置换编码,不是连续向量。强行套simulannealbnd会很别扭。这种情况下我更推荐直接用Matlab手写一个SA核心循环,代码量也不大。

TSP的SA思路:

  1. 用随机排列表示一条路径。
  2. 邻域操作采用“交换两个城市的位置”或者“反转一段路径”。
  3. 新路径长度更短就接受,更长则以exp(-Δ/T)概率接受。
  4. 温度按指数冷却:T = T * alpha。

核心代码框架:

nCity = 30; rng(3); cityPos = rand(nCity, 2) * 100; distMat = pdist2(cityPos, cityPos); path = randperm(nCity); path = [path, path(1)]; % 首尾相连 cost = pathCost(path, distMat); T = 1000; alpha = 0.995; while T > 1e-3 newPath = swapTwo(path); newCost = pathCost(newPath, distMat); delta = newCost - cost; if delta < 0 || exp(-delta / T) > rand path = newPath; cost = newCost; end T = T * alpha; end fprintf('最终路径长度: %.2f\n', cost); function len = pathCost(p, D) len = 0; for i = 1:length(p)-1 len = len + D(p(i), p(i+1)); end end function p2 = swapTwo(p) p2 = p; idx = randperm(length(p)-2, 2) + 1; % 不交换首尾 p2(idx) = p2(fliplr(idx)); end

这个案例的价值在于:当你面对的不是标准连续优化问题,而是特殊编码的组合优化问题时,理解算法原理远比赛会背工具箱接口更重要。SA的核心就三件事:解的表达、邻域扰动、温度调度。

5. 常见问题与调优技巧实录

5.1 结果重复性差怎么处理

很多用户第一次跑SA,会惊讶地发现每次结果都不一样。这是正常现象,因为SA本质是随机算法。但如果每次结果天差地别,说明算法没有充分收敛。

我处理重复性问题有一个标准流程:先固定随机种子rng(1),保证每次实验可复现;然后增加MaxIter和MaxFunEvals;最后如果计算资源允许,多次运行SA,取目标函数值最小的解作为最终结果。这个“多跑几次取最优”的策略虽然简单,但在实际工程里非常实用。

5.2 SA收敛太慢,除了等待还能做什么

如果跑了几千次迭代还在原地转,大概率是温度策略不适合当前问题。常用的加速手段:

  • 降低InitialTemperature,减少前期无效的乱逛。
  • 把TemperatureFcn换成temperaturefast,降温更激进。
  • 增大ReannealInterval,减少重新升温的次数,让算法更专心收敛。
  • 检查目标函数是否有大量平坦区域,如果有,SA在平坦区里会瞎转,这种问题更适合加一点局部梯度信息。

我踩过的一个坑是:为了追求全局最优,把初始温度设得特别高,结果前一半计算时间全在随机乱跳,真正的局部精修反而不够。

5.3 惩罚函数把结果带偏了

惩罚函数几乎是处理非线性约束的标配,但它有个副作用:惩罚系数太大,目标函数表面会变得特别陡峭,SA在边界附近很难稳定;惩罚系数太小,约束会被无视。

我的建议是不要一次性定死惩罚系数,而是跑一轮SA,检查最终解的约束违反量。如果违反量大,就把lambda乘以10再跑;如果结果已经在可行域内,但精度不够,再用fmincon精修。实际项目中,我经常用SA+惩罚函数找到边界区域,然后靠fmincon去满足严格的约束。

5.4 SA+fmincon混合优化模板

综合前面的经验,我目前最常用的优化流程是这样的:

  1. 先用模拟退火做全局搜索,得到一个候补解。
  2. 检查候补解的目标函数值和约束违反量。
  3. 以候补解为初值,调用fmincon做局部精修。
  4. 如果fmincon结果不符合预期,回到第1步,换一个随机种子再跑一次。

这段逻辑可以封装成一个函数,核心代码如下:

function [x_best, f_best] = hybrid_optimize(objFun, x0, lb, ub, nonlcon) saOptions = saoptimset('Display', 'off', 'MaxIter', 3000); [x_sa, ~] = simulannealbnd(objFun, x0, lb, ub, saOptions); fminOptions = optimoptions(@fmincon, 'Display', 'off'); if nargin < 5 nonlcon = []; end [x_best, f_best] = fmincon(objFun, x_sa, [], [], [], [], lb, ub, nonlcon, fminOptions); end

这种混合策略几乎成了我做中型优化项目的默认起手式。它既保留了SA的全局搜索能力,又利用了fmincon在局部的高精度收敛,两个工具箱才算真正打出了配合。

5.5 问题速查表

现象常见原因解决办法
每次运行结果都不同随机算法未充分收敛固定rng、增加迭代次数、多次运行取最优
收敛到明显很差的解初始温度过低或降温过快提高InitialTemperature,更换temperatureexp
搜索过程极其缓慢初始温度过高、退火区间过长降低初始温度、增大ReannealInterval
约束始终不满足惩罚系数太小增大lambda,或改用fmincon处理约束
目标函数返回NaN搜索到无效区域在目标函数内加保护逻辑,返回大数
结果停留在初始点附近邻域扰动幅度太小检查x0向量范围,调整初始温度

6. 我的实操心得与后续扩展

6.1 踩坑后总结的习惯

我现在做任何一个优化项目,都会先花时间把目标函数的行为摸一遍。能画图就画图,不能画图就随机采样若干组参数,看一眼目标函数的量级和粗糙程度。这个习惯帮我省了无数调参时间。

第二个习惯是永远不要把SA的结果当最终答案。SA擅长的是“找到好区域”,而不是“给出极高精度的解”。除非你的问题对精度要求不高,否则建议在SA结果后面再接一个局部优化器。哪怕是fminunc这种无约束的局部求解器,也够用。

第三个习惯是记录每一次SA运行的轨迹。Matlab里可以通过自定义输出函数或绘图函数,把每次迭代的当前最优值记录下来,后期分析收敛过程非常有用。但自定义绘图函数在新版本里有时兼容性一般,我直接保存变量,画图自己来。

6.2 还可以怎么扩展

模拟退火的灵活性在于温度调度和邻域操作都可以自定义。我在一个项目里给温度函数加入“阶段性升温”策略:每跑固定轮数后,如果检测到连续多轮最优值没有变化,就自动升温重新搜索。效果比固定降温曲线好不少。

如果你对Matlab的全局优化工具箱已经很熟,还可以进一步把SA与其他方法组合:先用SA做变量筛选,再用线性回归或响应面模型精修;或者在SA的目标函数中嵌入仿真模型,实现真正的黑盒优化。只要理解了“全局粗搜+局部精修”这个核心思想,工具箱就只是工具,真正解决问题的是你的调度策略。

模拟退火不是万能的,但在我处理过的多峰、黑盒、不可导问题里,它几乎是最稳的兜底方案。配合Matlab优化工具箱里的局部求解器,这一套组合拳值得成为你优化武器库里的常备选项。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询