☰
七种智能优化算法在23个基准测试函数上的MATLAB对比实现
2026/10/3 14:58:14 网站建设 项目流程

七种算法放在同一套MATLAB平台上跑23个基准测试函数,听起来像流水线机械劳动,但做过对比实验的人都知道,这是智能优化算法研究里最扎实的“双盲测试”。这次我一次性实现了GRO、NOA、MA、PSO、GA、ZOA、SWO七个算法,配套完整的基准测试函数集、统一评价指标和代码框架,全部跑完并整理了结果表与参考文献列表。如果你正在准备算法对比实验,或者想从工程角度客观评价一个优化器的真实水平,这套方案可以直接拿来改,也可以帮你把“这个算法到底行不行”这个问题彻底想清楚。

1. 为什么是这23个函数:基准测试的设计逻辑

1.1 三类测试函数的“地形”差异

23个经典基准测试函数不是随机凑出来的,它们被设计成三组,分别对应三种典型的搜索地形。第一组是单峰函数(F1-F7),整个定义域内只有一个全局最优点,没有局部极小来干扰算法,纯粹考验收敛速度和精度。第二组是多峰函数(F8-F13),地形里布满了大量局部极小值,算法一不小心就会困在某个“假山谷”里,主要考察全局勘探能力和跳出局部陷阱的能力。第三组是固定维度多峰函数(F14-F23),维度不高但地形更贴近真实工程问题,很多函数带有平坦区域、窄长谷地或者欺骗性梯度,用来评估低维条件下的精确搜索能力。

你可以把这23个函数理解成体检套餐中的不同项目:单峰函数查“心肺基础功能”,多峰函数查“压力环境下的应激反应”,固定维度函数则更像“真实生活场景模拟”。只跑一两个函数就下结论,就像只查了血常规就判断全身健康,说服力远远不够。

1.2 23个函数速查表

下面这张表是我在搭建测试平台时一直放在手边的速查表,函数编号、名称、类型和考察点一目了然。

编号函数名类型维度主要难点与考察点
F1Sphere单峰30最简单的光滑凸函数,检验基本收敛能力
F2Schwefel 2.22单峰30含有绝对值与乘积项,梯度变化剧烈
F3Schwefel 1.2单峰30病态二次型,变量之间强耦合
F4Schwefel 2.21单峰30最大值型函数,容易在边界处失效
F5Rosenbrock单峰30经典香蕉谷,窄长弯曲谷底,极难收敛到最优点
F6Step单峰30阶梯状平坦区域,测试算法对离散地形的适应力
F7Quartic with Noise单峰30带随机噪声,考验鲁棒性
F8Schwefel 2.26多峰30全局最优点远离次优点,存在欺骗性
F9Rastrigin多峰30大量规则的局部极小,考验摆脱能力
F10Ackley多峰30中间深坑四周密集局部极小,经典陷阱
F11Griewank多峰30乘积项引入周期性干扰,全局和局部结构并存
F12Penalized 1多峰30带惩罚项的多峰函数,边界处理敏感
F13Penalized 2多峰30同上,地形更复杂
F14Foxholes固定维度2大量均布局部极小点,全局极小埋得深
F15Kowalik固定维度4小规模参数辨识问题,数值条件差
F16Six-Hump Camel固定维度2多局部极小的双峰驼背函数
F17Branin固定维度2三个全局极小点,检验多模态处理能力
F18Goldstein-Price固定维度2多项式复合地形,局部极小密集
F19Hartman 3-D固定维度3三维指数势函数,适合中等复杂度测试
F20Hartman 6-D固定维度6六维指数势函数,维度扩展版
F21Shekel 5固定维度4深坑型函数,坑数可调
F22Shekel 7固定维度4更多深坑,局部极值干扰更强
F23Shekel 10固定维度4坑最多,全局极小隐蔽性最强

我自己在写代码时最大的感受是:千万别小看F7这种带噪声的函数。很多算法在干净函数上表现很好,一遇到噪声就露馅,因为它本质上考察的是算法在随机扰动下不停机的稳定能力。真实工程里的目标函数几乎都带噪声,这比纯数学函数的收敛精度更有现实意义。

1.3 测试平台的意义:让算法说同一种语言

我见过不少算法论文,作者在自家函数集上测试,效果“惊艳”,但换个平台立刻打回原形。原因很简单:每个算法论文都会选择有利于自己的测试场景,这不算造假,但横向可比性极差。23个经典函数作为公共基准,价值恰恰在于大家都在同一套卷子上考试。

这里有个容易踩的坑:有些人不自觉“择菜”,只选取自己算法表现最好的少数函数来报告。做工程选型时可以理解,但如果是学术对比实验,这样做容易翻车。我的习惯是23个函数全部跑完,再按类型分组分析,就算某个函数跑出来不理想也如实记录。那样反而让结论更可信,也更容易发现算法的真实适用边界。

2. 七种算法是什么:从经典双雄到动物行为大军

2.1 PSO和GA:为什么永远是基准测试的对照组

PSO和GA不需要过多介绍,但在这个项目里它们是绝对的“地标”。PSO模拟鸟群觅食行为,每个粒子同时被自身历史最优和全局最优牵引,核心更新公式只有两行:速度更新和位置更新。我习惯用的版本带惯性权重w线性递减,这样前期勘探能力强、后期开发能力强,效果比固定权重稳定得多。GA则模拟自然选择和遗传变异,选择、交叉、变异三种算子缺一不可。实数编码版本里,交叉多采用模拟二进制交叉,变异采用多项式变异,再搭配精英保留策略。

这两个经典算法之所以必须作为对照组,是因为它们覆盖了两种截然不同的搜索哲学:PSO靠群体信息交互,GA靠算子驱动的种群演化。新算法的表现好不好,只有和这两种“体温计”放在一起量,才知道是真创新还是换皮重排。

2.2 五个新算法各自的“核心机制”

GRO、NOA、MA、ZOA、SWO这五个算法,绝大多数是近几年提出的“动物行为启发式算法”。第一次接触时可能会看得眼花缭乱,但拆开看机制后会发现,它们本质上都在回答同一个问题:如何平衡勘探与开发。GRO模拟巨犰狳挖洞觅食的行为,用“螺旋掘进-后退探查”这种动静结合的策略来动态切换搜索阶段,是我认为这一批里比较注重动态切换节奏的算法。NOA模拟星鸦采集、储藏和取回松果的复杂行为,把探索和开发拆成两个大阶段、四个子策略,结构上更精细。

MA这里指蜉蝣优化算法,它把鸟群式的位置-速度模型和遗传交叉变异结合了起来,雄雌蜉蝣分别更新、交配产生子代,本质上是一套混合算法。ZOA则简单直接,模拟斑马的觅食和防御行为,觅食阶段围绕当前最优个体收缩搜索,防御阶段根据随机概率选择逃跑或反击,代码实现门槛很低,特别适合新手读懂新算法的一般套路。SWO蜘蛛蜂优化器的设计更复杂,雌性蜘蛛蜂捕猎蜘蛛、注入毒素、产卵、幼虫成长这些阶段都被建模成不同的搜索算子,全局勘探和局部精细开发交织得很密。

一个很重要的提醒:不要因为一个算法名字新颖就高看一眼。这几年新出的动物行为算法很多,有一部分就是把PSO的公式换个符号重写一遍。判断一个算法的含金量,要看它的算子在哪些搜索阶段真正贡献了多样性,而不是看它模拟的动物有多冷门。

2.3 七种算法速览表

算法全称核心思想主要算子/特点适合方向
GROGiant Armadillo Optimization巨犰狳挖洞觅食螺旋挖掘、后退探查,勘探开发动态切换复杂多峰连续优化
NOANutcracker Optimizer Algorithm星鸦储食取回行为多阶段多策略交替,结构精细高维函数优化
MAMayfly Algorithm蜉蝣求偶交配飞行速度位置更新+交配变异,混合性强中等规模连续优化
PSOParticle Swarm Optimization鸟群/鱼群觅食速度-位置更新,群体信息共享连续光滑问题经典基线
GAGenetic Algorithm自然选择与遗传选择、交叉、变异、精英保留离散/组合优化经典基线
ZOAZebra Optimization Algorithm斑马觅食防御觅食跟随最优,防御随机逃避/反击结构简单,快速验证
SWOSpider Wasp Optimizer蜘蛛蜂捕猎繁殖捕猎麻醉、产卵孵化,两阶段多算子综合复杂度高的优化问题

从这张表也能看出,新算法的设计趋势是“把多个成熟机制缝合到一起”。比如MA是PSO+GA的混合,NOA则在交替执行不同策略,SWO直接把多个阶段串成了一个逻辑链。理解了这一点,再看代码就不会被一堆动物名词绕晕,重点去看每个阶段里的更新公式用了什么经典算子。

3. 实验设计:保证公平的几个关键决策

3.1 参数统一,还是各调各的?

这个问题我在设计实验时纠结了很久。如果所有算法用完全相同的参数,对某些算法来说不公平,因为新算法通常要依赖额外算子,种群规模和最大迭代次数的影响各不相同。如果每个算法都调到最优再比,工作量巨大,而且“最优参数”本身就可能被人为操纵。

最终我选择了文献中最常用的折中方案:能统一的全统一,每个算法自带的特有参数用原始论文推荐值,不额外精细调优。具体来说,种群规模N固定为30,最大迭代次数固定为500,高维函数维度统一设为30,固定维度函数使用各自定义维度。这个配置的好处是总评价次数固定为30×500=15000次,对比双方在计算资源上完全对等,争议最小。

3.2 独立运行次数与评价指标

随机优化算法每次运行结果都可能不同,只跑一次就下结论是最常见的错误。我每个函数独立跑30次,所有随机种子在主脚本里统一生成,保证每个函数每个算法拿到的初始随机序列是同一套。评价指标我记录五样:最优值、最差值、均值、标准差和收敛曲线。

这里我要多说一句:均值不是万能的。某个算法可能在20次运行中达到全局最优,但另外10次完全发散,均值可能和另一个稳定收敛到次优解的算法差不多。真正能反映算法稳定性的指标是标准差,能把异常大偏差暴露出来。我在看实验结果时,习惯先看“均值”和“标准差”这一对“连体婴”,如果均值和标准差都占优,结论才是真的可靠。

3.3 初始化与边界处理的一致性

初始化方式对算法结果的影响经常被忽略。我用统一函数生成初始种群,边界取每个基准函数定义域的标准范围。特别要注意的是,初始种群必须是真正的随机均匀分布,不要用固定矩阵反复复用,否则后面没法分析随机性带来的差异。

边界处理我也做了统一约定:搜索过程中个体越界时,采用“重新初始化到边界内”的方式,而不是简单粗暴地截断到边界上。直接截断会让大量个体堆在边界上,种群多样性瞬间消失,这个问题非常隐蔽,我在跑F4这种最大值型函数时吃过亏,一开始收敛曲线看起来不错,实际上全是边界截断的“假收敛”。

3.4 实验参数速查表

参数项取值说明
种群规模N30中等规模,避免算法差异被群体规模掩盖
最大迭代次数500总评价次数15000次
高维函数维度30F1-F13统一
固定维度函数按定义F14-F23使用原始维度
独立运行次数30每次用不同随机序列
随机数管理主脚本统一rng设置算法内部不重设种子
边界处理重新初始化避免种群堆叠在边界
统计指标最优/最差/均值/标准差/收敛曲线可按需增加Wilcoxon检验

这套参数表格建议直接贴在你的实验记录里。写论文的时候专家经常会追问实验配置,有这张表就能应对大部分提问,也不会出现“跑完了才发现忘了记录”的尴尬。

4. MATLAB代码实现:搭建一套可复用的测试框架

4.1 顶层主脚本:函数句柄数组驱动一切

这套代码框架的核心思路是“统一接口 + 数据驱动”。所有算法对外只暴露同一个函数签名,所有基准函数都通过函数句柄传递。这样主脚本可以写得很短,新增算法时只需要往algorithms列表里加一行。

% run_experiments.m clear; clc; % 统一实验配置 config.N = 30; config.MaxIter = 500; config.Dim = 30; config.Runs = 30; % 算法列表:函数句柄 + 算法名 algorithms = { @(f,lb,ub,dim,N,iter) GRO(f,lb,ub,dim,N,iter), 'GRO'; @(f,lb,ub,dim,N,iter) NOA(f,lb,ub,dim,N,iter), 'NOA'; @(f,lb,ub,dim,N,iter) MA(f,lb,ub,dim,N,iter), 'MA'; @(f,lb,ub,dim,N,iter) PSO(f,lb,ub,dim,N,iter), 'PSO'; @(f,lb,ub,dim,N,iter) GA(f,lb,ub,dim,N,iter), 'GA'; @(f,lb,ub,dim,N,iter) ZOA(f,lb,ub,dim,N,iter), 'ZOA'; @(f,lb,ub,dim,N,iter) SWO(f,lb,ub,dim,N,iter), 'SWO'; }; funcs = loadBenchmarks(config.Dim); results = cell(size(algorithms,1), numel(funcs)); for a = 1:size(algorithms,1) for f = 1:numel(funcs) fprintf('运行算法: %s 函数: %s\n', algorithms{a,2}, funcs(f).name); records = zeros(1, config.Runs); curves = []; for r = 1:config.Runs initPop = initPopulation(funcs(f).lb, funcs(f).ub, funcs(f).dim, config.N); [bestCost, history] = algorithms{a,1}(... funcs(f).handle, funcs(f).lb, funcs(f).ub, ... funcs(f).dim, config.N, config.MaxIter, initPop); records(r) = bestCost; curves = [curves; history']; %#ok<AGROW> end results{a, f} = struct('best', min(records), 'worst', max(records), ... 'mean', mean(records), 'std', std(records), 'curves', curves); end end save('results_all.mat', 'results', 'algorithms', 'funcs', '-v7.3');

这套主脚本跑起来会输出进度信息,避免长时间无用等待。我通常会加一个tic/toc统计每个算法在每个函数上的耗时,如果某个函数明显比别的慢很多,就要排查是不是代码里出现了死循环或者数据复制爆炸。

4.2 基准函数集的封装

基准函数集我用结构体数组封装,每个函数固定包含名称、维度、边界、理论最优值和函数句柄。这样主脚本遍历起来非常干净。

function funcs = loadBenchmarks(D) funcs = []; funcs(end+1) = struct('name','Sphere', 'type','单峰', ... 'dim',D, 'lb',-100, 'ub',100, 'optimal',0, ... 'handle',@(x) sphere_func(x)); funcs(end+1) = struct('name','Rosenbrock', 'type','单峰', ... 'dim',D, 'lb',-30, 'ub',30, 'optimal',0, ... 'handle',@(x) rosenbrock_func(x)); % ... 其余函数按同样方式补全 end

这里有一个细节很容易被代码新手忽略:Rosenbrock函数的全局最优点在(1,1,...,1),不在定义域中心零点。如果沿用大部分函数“对称边界”的惯性,很可能会把搜索域设成[-1,1],那就把真正的最优点排除在外了。我建议每添加一个函数,先手动在MATLAB里用理论最优值点算一次函数输出,确认返回值和文献一致后再放进测试集。

4.3 算法接口统一化

所有算法的接口我统一写成[bestCost, history] = algorithm(fobj, lb, ub, dim, N, MaxIter, initPop)。输入参数依次是目标函数句柄、下界、上界、维度、种群规模、最大迭代次数和初始种群。initPop可以传,也可以传空让算法自己初始化,但为了公平实验,我在主脚本里统一生成后传给每个算法。

function [bestCost, history] = GRO(fobj, lb, ub, dim, N, MaxIter, initPop) % 统一接口实现示例 if isempty(initPop) initPop = initPopulation(lb, ub, dim, N); end pop = initPop; bestCost = inf; history = zeros(1, MaxIter); for t = 1:MaxIter % 计算适应度 costs = zeros(N, 1); for i = 1:N costs(i) = fobj(pop(i,:)); end % 更新最优解 [cmin, idx] = min(costs); if cmin < bestCost bestCost = cmin; bestX = pop(idx,:); end history(t) = bestCost; % 算法核心更新算子(不同类型算法在此替换) % pop = ... end end

我把最重要的经验放在这里:写新算法实现时,第一步不是“把论文公式翻译成代码”,而是先把上面这个骨架跑通,确保目标函数、边界、初始化和最优解记录都正常,再往里面填论文里的搜索算子。否则你花两天写完整个算法,最后发现是初始化把维度都传错了,排查成本叠加好几倍。

4.4 收敛曲线绘制与结果存档

收敛曲线是整个实验最直观的展示方式。绘制时我有两个实操经验:一是对多次运行取平均后再画,单次运行的曲线噪声太大,看不出趋势;二是很多函数收敛到0或者极接近0,画对数坐标时会出现log(0)导致曲线断掉。我处理的办法是记录历史值时统一加一个极小量1e-300,绘图时只显示大于0的点。

function plotConvergence(results, funcs, algNames, funcIdx) figure('Color','w', 'Position',[100 100 700 500]); hold on; for a = 1:numel(results(:, funcIdx)) avgCurve = mean(results{a, funcIdx}.curves, 1); iters = 1:numel(avgCurve); mask = avgCurve > 0; semilogy(iters(mask), avgCurve(mask) + 1e-300, 'LineWidth', 1.6); end hold off; grid on; legend(algNames, 'Interpreter','none'); xlabel('迭代次数'); ylabel('平均最优目标值(对数坐标)'); title(['函数 ' funcs(funcIdx).name]); saveas(gcf, ['convergence_' funcs(funcIdx).name '.png']); end

另外,结果存档不要只存一张图。我把results_all.mat里的均值标准差表额外导出成CSV,写论文时可以直接粘贴到LaTeX表格里。导出方法很简单:循环结果数组,用writetable写CSV即可。

5. 高频报错与排查记录

5.1 索引和维度不一致

这个错误在七种算法里我都遇到过多轮。典型场景是目标函数期望输入是1 x dim的单个个体,算法内部却传进来N x dim的整个种群;或者在初始化时把dim和N搞反,生成出个30 x 30的矩阵然后在运算时又当作50维。排查方法很笨但有效:在每个算法入口加一行disp(size(pop)),用最简单的F1函数先跑一遍,确认每个循环体内变量的size都符合预期。

5.2 收敛曲线“躺平”不动

如果你的收敛曲线前几代下降很快,然后几十代完全水平,大概率不是算法找不到更好解,而是边界处理出了问题。个体越界后被直接截断到边界,种群多样性急剧下降,算法陷入“伪停滞”。另一种常见原因是适应度计算出现了NaN,NaN参与比较时不会更新最优解,而min函数遇到NaN经常静默返回。我建议在所有目标函数返回值出口统一检查~isfinite(cost),有异常就打印可疑个体。

5.3 随机数种子管理不当

实验开始复现性出问题,九成是因为rng设置的位置不对。有些算法内部会调用rng('shuffle')或者randn,这会悄悄改变全局随机数流,导致后续算法拿到完全不同的初始种群。我做实验时的强制约定:全局种子只在主脚本设置,算法内部一律不调用任何重置随机数流的函数;如果要保证每个函数都能用同一套初始种群,就在主循环里先预先缓存所有函数的初始种群矩阵,再传给算法。

5.4 结果复现困难

除了随机数,复现问题还可能来自MATLAB版本差异和浮点计算顺序。我的做法是把运行环境信息一并保存到结果文件:MATLAB版本、算法代码版本号、rng种子、每个函数的初始种群矩阵。这样即使三个月后再回看数据,也能精确复现当时的实验。

5.5 常见错误速查表

错误现象可能原因解决方案
维度报错或矩阵拼接失败dim和N混用算法入口打印size,用F1函数最小化排查
收敛曲线前段好、后段平坦边界截断导致多样性丢失改用边界内重新初始化,减少直接截断
结果跑几次都不一致算法内部重置随机种子全局种子统一在主脚本设置,算法内禁用rng
目标值出现NaN或Inf函数定义域外计算溢出越界重初始化,并在目标函数返回值处检查有限性
绘图时log曲线断掉收敛到0或负值记录时加1e-300,绘图时过滤非正点
某算法特别慢代码内向量化不彻底或死循环用tic/toc定位耗时环节,检查是否有重复计算

跑完这批实验,我最想说的是:新算法层出不穷,23个函数就像一面照妖镜。比起总排名,我建议你重点关注某个算法在哪些函数上失败,再回看它的代码,搞清楚失败原因到底是勘探不足还是开发过度,这种分析带来的认知提升远大于看一张“七个算法排名表”。这套测试框架的扩展性很好,想接真实工程问题也很方便,把目标函数换成实际模型就行,比如路径规划、参数辨识这类场景,只需要改边界、维度和目标函数句柄,其他部分完全不用动。我个人的体会是,真正可靠的结论从来不是“哪个算法最强”,而是“什么样的地形适合什么样的机制”,这才是做基准测试实验最大的收获。

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

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

立即咨询