上个月有个做负荷预测的师弟问我,说论文里只写了PSO和GWO的对比,审稿人觉得算法太老,让他补充几个能直接在Matlab里跑、不装第三方工具箱、还能画出收敛曲线的智能优化算法。我从常用算法库里挑了CGO、SCA、GWO、CSA、SSA、HHO、WOA、PSO、TSO这9种,统一加了测试函数、维度、种群规模和迭代次数,整理了一套可以替换目标函数的Matlab代码结构。这篇文章不是把论文搬过来复述公式,而是把我实际跑对比时的算法理解、参数设置和容易翻车的细节写出来。想快速做算法对比、给论文补实验,或者从PSO/GWO往新算法扩展的人,可以照着这套思路搭自己的实验模板。
1. 9种算法放同一张表之前,先把它们拆成三类
很多新手拿到算法清单就开始跑,跑完发现结果乱七八糟,根本不知道问题出在算法本身还是参数设置。我的习惯是先看机制,把9个算法按照“搜索行为来源”分成三类,这样后面分析收敛曲线时才不会瞎猜。
1.1 捕食追踪型:GWO、WOA、HHO
GWO模拟灰狼集群的等级分工。alpha、beta、delta是头狼,其余omega跟随头狼更新位置。每一轮先算所有狼的适应度,把前三名记为alpha、beta、delta,然后用三只头狼的加权位置决定下一代位置,关键参数是A=2arand-a,其中a从2线性降到0。A的绝对值大于1时狼群离散搜索,小于1时向猎物收缩。这个算法优点是结构简单、参数极少,缺点是后期种群多样性下降快,多峰测试函数里很容易所有个体挤到同一个局部峰附近。
WOA模拟座头鲸的气泡网捕食,比GWO多一个策略分支。每轮生成一个随机数p,p小于0.5时走收缩包围,和GWO类似;p大于等于0.5时用对数螺旋绕圈逼近猎物。同时还有一个随机探索机制,避免所有鲸鱼都贴着当前最优走。正因为多了一个螺旋操作,WOA的搜索路径更“绕”,在复杂曲面上有时能绕出局部最优,但也更容易在前中期浪费评价次数。
HHO模拟哈里斯鹰围捕逃跑的兔子,核心是猎物能量E从2迅速掉到0,再叠加随机数q决定软围攻、硬围攻、渐进式快速俯冲等策略。它不是靠单一参数控制探索和开发,而是按E绝对值和随机逃逸概率切换模式。HHO前期会到处飞,后期突然集中围住某个区域,所以收敛曲线经常出现“阶梯式下降”。这既是优点也是隐患,在多峰问题上容易出现结果忽好忽坏的现象。
1.2 群体共享信息型:PSO、SSA、CSA、TSO
PSO不用过多解释,速度-位移模型,pbest和gbest分别提供个体记忆和社会记忆。惯性权重w前期大后期小,c1、c2分别控制“向自己历史最优飞”和“向群体最优飞”。优点是最好理解、实现容易、不容易出bug,缺点是后期gbest不变时,整个粒子群会围着同一个点抖动,基本没有跳出机制。
SSA这里我按樽海鞘群算法来写,也就是Salp Swarm Algorithm。它在代码里的特点是“链式移动”:第一个领导者更新后,后面每个跟随者只参考前一个体的位置,链头由食物源引导,系数c1从2衰减到0控制探索和开发。链式结构的优势是种群移动有方向性,但劣势也很明显:领导者一旦越界,整条链都会被带偏,因此边界处理比别的算法更关键。需要提醒的是,中文论文里也常把SSA当作麻雀搜索算法,那是完全不同的机制,下载代码前先确认你拿到的到底是樽海鞘还是麻雀。
CSA在标题里我按乌鸦搜索算法来展开。每只乌鸦代表一个解,并且会记住自己藏食物的位置。算法随机选另一只乌鸦,决定跟踪还是远离,参数fl控制飞行距离,AP是乌鸦发觉被跟踪的概率。AP越小,乌鸦越容易相信同伴,算法后期过快聚集;AP越大,群体越警惕,搜索越接近随机。我一般把AP放在0.1到0.2,fl取2左右,但这个组合在不同问题上并不通用。如果你在别的资料里看到CSA代表鸡群算法,基本是笔误或另一套代码,使用时务必先确认。
TSO是被囊群优化算法,模拟海洋被囊动物喷流推进和群体交流。喷流推进阶段生成新候选位置,群体交流阶段参考最优个体和当前个体的差异做加权更新。很多论文版本会把控制参数写成随迭代线性递减,效果上接近GWO的a。这个算法的中文资料相对少,Matlab代码风格差异很大,从网上下到版本后,先看它内部到底是最小化还是最大化寻优,否则实验方向直接跑反。
1.3 几何与非生物机制型:SCA、CGO
SCA正弦余弦算法不走生物路线,用sin和cos函数把候选解在新旧位置之间摆动。r1随迭代下降控制摆动幅度,r2决定移动半径方向,r3、r4产生随机性。低维问题上效果尚可,维度一高很容易振荡,属于“理解成本低、实战稳定性一般”的算法,更适合作为教学示例或者对比实验里的“差异性补充”。
CGO的完整名字是混沌博弈优化,源自混沌博弈理论和分形几何。它把当前最优位置、当前个体位置、以及若干随机个体当成“顶点”,每个种子在顶点围成的区域内按概率移动,生成下一代。CGO机制很像分形图形:每次都在缩小范围内选点,最终形成局部密集搜索。实际代码里它至少有四个不同的更新分支,随机性很强,单次运行很难看出真实水平。
| 算法 | 全称/灵感 | 位置更新核心 | 关键参数 | 最容易翻车的地方 |
|---|---|---|---|---|
| GWO | 灰狼捕猎 | alpha/beta/delta加权 | a、A、C | 后期多样性不足 |
| WOA | 座头鲸气泡网 | 收缩包围+对数螺旋 | a、p | 螺旋与随机探索比例固定 |
| HHO | 哈里斯鹰捕兔 | 软硬围攻+俯冲 | E、q、r | 模式跳变导致波动 |
| PSO | 鸟群觅食 | 速度+个体/全局最优 | w、c1、c2 | 全局最优卡死 |
| SSA | 樽海鞘链 | 领导者-跟随者链式 | c1、c2 | 领导者越界带偏全链 |
| CSA | 乌鸦藏食 | 追随/逃跑+感知概率 | AP、fl | 参数敏感度过高 |
| TSO | 被囊群推进 | 喷流推进+群体交流 | 衰减控制参数 | 代码实现差异大 |
| SCA | 正弦余弦摆动 | sin/cos位置摆动 | r1、r2 | 高维振荡 |
| CGO | 混沌博弈/分形 | 四顶点随机投影 | 多种随机分支 | 单次运行极不稳定 |
2. 关键迭代逻辑:位置更新的共性和差异
2.1 统一接口,让9种算法都从同一个入口跑
跑对比实验时最大的错误,是每个算法单独写一个脚本,用到的时候临时改参数。几个月后自己都忘了哪个版本对应哪张图。我后来把代码统一收成一个runOptimizer入口,所有算法返回bestX、bestF、convergence,后续只需要写一个测试脚本循环调用:
function [bestX, bestF, curve] = runOptimizer(optName, objFun, dim, lb, ub, MaxIt, nPop) switch optName case 'PSO' [bestX, bestF, curve] = PSO(objFun, dim, lb, ub, MaxIt, nPop); case 'GWO' [bestX, bestF, curve] = GWO(objFun, dim, lb, ub, MaxIt, nPop); case 'WOA' [bestX, bestF, curve] = WOA(objFun, dim, lb, ub, MaxIt, nPop); case 'HHO' [bestX, bestF, curve] = HHO(objFun, dim, lb, ub, MaxIt, nPop); case 'SSA' [bestX, bestF, curve] = SSA(objFun, dim, lb, ub, MaxIt, nPop); case 'CSA' [bestX, bestF, curve] = CSA(objFun, dim, lb, ub, MaxIt, nPop); case 'TSO' [bestX, bestF, curve] = TSO(objFun, dim, lb, ub, MaxIt, nPop); case 'SCA' [bestX, bestF, curve] = SCA(objFun, dim, lb, ub, MaxIt, nPop); case 'CGO' [bestX, bestF, curve] = CGO(objFun, dim, lb, ub, MaxIt, nPop); end end这种结构的好处是,你后面想换成自己的目标函数,只需要保证objFun接收一个nPop×dim矩阵,返回每个个体的适应度列向量,所有算法代码都不用动。
2.2 种群初始化必须用同一套语法
初始化时我统一这么写:
Positions = rand(nPop, dim) .* (ub - lb) + lb;很多算法代码里直接写成rand(nPop,dim)*ub,把lb丢了。测试函数的下界不是0时,初始种群就全错。表面上看每个算法都“初始化了”,实际起点范围完全不同,对比结果没有意义。另外,目标函数建议写成支持矩阵输入的向量化版本,不要循环单个个体,否则跑高维度时会非常慢。
function f = sphere(x) f = sum(x.^2, 2); % x是nPop×dim矩阵,f是nPop×1 end2.3 参数设置的常用基线
不同算法参数没有一个万能组合,但我整理了一套不容易出大问题的起步值:
| 算法 | 我常设的参数 | 为什么这么设 |
|---|---|---|
| GWO | a: 2→0,A=2arand-a | 线性衰减控制探索开发 |
| WOA | a: 2→0,p∈[0,1] | 让后期螺旋更集中 |
| HHO | E: 2→0,q∈[0,1] | E小于1后才进入围攻 |
| PSO | w: 0.9→0.4,c1=c2=2 | 经典取法,审稿人不会质疑 |
| SSA | c1: 2→0,c2随机 | 链头向食物源,链尾跟随 |
| CSA | AP=0.1~0.2,fl=2 | AP太大就退化成随机搜索 |
| TSO | 衰减系数2→0 | 多数开源版本默认逻辑 |
| SCA | r1: 2→0 | 控制sin/cos摆动幅度 |
| CGO | 随机分支选择 | 算法本身没有固定主参数 |
就算按这些初始值跑,同一组参数在不同函数上的表现差异也很大。真正写论文时,除了默认参数外,最好补一个简单的参数敏感性分析,不然审稿人一句“参数怎么来的”就能把实验结论打穿。
3. Matlab对比实验:真实跑下来应该怎么设计
3.1 测试函数不能只选一个
对比实验只跑一个函数,说服力很低。我通常至少选三个形态不同的函数:单峰Sphere或Rosenbrock,多峰Rastrigin或Ackley,固定维度的CEC基准。维度也要分开,低维用10维,中维用30维,高维可以到100维。注意,这里的维度指决策变量个数,不是测试函数的种群数,很多人会把这两个概念搞混。
测试函数统一写成支持向量化输入的形式。如果拿到的函数是for循环逐个点算的,建议改成矩阵运算,不然9个算法×3个函数×30次独立运行,时间会非常难熬。
3.2 独立运行次数和统计口径
智能优化算法是随机算法,跑1次没有意义。我习惯每个算法每个函数独立运行30次,记录每次得到的最优适应度,再算均值、中位数、标准差。为什么同时看均值和中位数?因为单峰函数上均值接近中位数,但在Rastrigin这种多峰地形上,某一次跳出局部最优会让均值大幅偏移,只看均值会高估算法稳定性。
有条件的话,再做一个Wilcoxon秩和检验,把PSO当Baseline,看新算法相对Baseline的提升是否有显著性。很多审稿人看到你只贴三条收敛曲线,不会说太多,但看到均值好却不给统计检验,就会追问显著性结果。
3.3 记录收敛曲线时,用评价次数而不是迭代次数
这是最容易忽略的公平性问题。PSO一次迭代通常只评价nPop次适应度,但HHO、CGO这类算法内部有多个分支,每个分支里可能又调用了一次目标函数,实际评价次数是迭代次数×nPop×分支倍数。如果你统一设定MaxIt=500,那9个算法的工作量其实完全不一样。
我的做法是在每个算法的适应度评估处统一维护一个FEs(Function Evaluations)计数器,收敛曲线的横坐标用FEs而不是迭代次数。只有这样,不同算法才能在“花了同样计算量”的前提下比较。顺手也把每一轮的gbest记录成curve数组:
convergence(t) = bestF;3.4 实验脚本的标准流程
一个完整的对比实验流程应该是:先确定测试函数和维度,再确定nPop和MaxIt,然后每个算法独立跑30次,每次记录最终bestX、bestF和convergence曲线。最后把所有结果汇总到一个结构体里,用箱线图看分布,用均值标准差做表格,用秩和检验做显著性。不要急着画带阴影的收敛图,先把数据形态看清楚。
4. 我跑Sphere和Rastrigin之后看到的排序和反直觉结论
4.1 单峰Sphere上,老牌算法根本不落下风
在Sphere这种简单单峰函数上,我跑下来的直观感受是:PSO和GWO完全能和新算法打平。PSO收敛快,低维度时几乎每轮都在往下压;GWO稳步下降,曲线很“体面”;WOA前期跳跃明显,后期也能拿到接近较好的值。真正在Sphere上容易出问题的往往是SCA和CSA,它们的后期收敛会出现一段明显的“平台期”,感觉像步子太小,卡在某个量级下不去。
这里有一个反直觉的点:网上很多论文里展示的漂亮曲线,往往只挑了最优的一次运行结果。同一个HHO在Sphere上多跑几次,前期曲线可能有很大差异,有的版本甚至前50次迭代下降很少,后面突然又跳一次。选哪条曲线、怎么说明稳定性,比单纯说“最小适应度更低”更重要。
4.2 Rastrigin上,HHO、WOA和GWO各有各的挣扎方式
Rastrigin函数局部最优极多,10维就已经很容易把算法困住。GWO刚开始下降很快,但进入后期后所有灰狼都集中到同一个局部峰附近,基本没有能力再大幅跳出去。WOA的优势在于螺旋绕行,偶尔能跳到相邻的峰,代价是消耗更多评价次数。HHO如果能量衰减参数E设置得太快,后期所有个体都会进入硬围攻模式,结果被锁在局部区域;但如果E衰减偏慢,它又会浪费大量评价次数在外围瞎飞。
CGO在Rastrigin这类函数上表现很有戏剧性,单次运行有时能挖到很不错的适应度,有时差得离谱。我把它当成“抽奖类”算法,实验时必须增加独立运行次数,否则极容易在报告里得出错误排名。TSO的行为很大程度取决于你下载到哪个实现版本,有的版本做了局部搜索改进,有的版本还是原始喷流推进,代码水平直接决定结果。
4.3 箱线图比收敛曲线更诚实
多跑几次以后,我把9个算法的最终适应度做成箱线图,发现中位数和均值的差距经常被忽视。某个算法平均适应度更小,可能是靠一次偶然的优秀结果拉高,但中位数很差。所以我在报告里固定给出mean±std和中位数,并且画箱线图。收敛曲线看趋势,箱线图看分布,两张图配合使用,才不容易被审稿人挑刺。
下面是我在固定10维、种群30、迭代500次时观察到的大致行为,只反映相对倾向,不写具体数值,因为换一组参数结论就可能变:
| 算法 | Sphere表现 | Rastrigin表现 | 稳定性 | 我的主观建议 |
|---|---|---|---|---|
| PSO | 收敛稳定 | 中位数稳定,极值普通 | 高 | 适合做Baseline |
| GWO | 稳步下降 | 前期快,后期平缓 | 中 | 参数a影响大 |
| WOA | 中后期可能跳出局部 | 探索和开发较均衡 | 中 | 多峰可以试 |
| HHO | 前期剧烈,后期集中 | 波动大,偶发好结果 | 低 | 必须多跑几次 |
| SSA | 链式移动整体性强 | 注意越界问题 | 中 | 边界处理要严格 |
| CSA | 结果依赖AP和fl | 容易早聚 | 低 | 参数单独调 |
| TSO | 实现版本差异大 | 版本决定结果 | 低到中 | 先确认代码逻辑 |
| SCA | 低维可行,高维偏弱 | 摆动明显 | 中 | 教学或对照用 |
| CGO | 单次波动大 | 偶发优秀结果 | 低 | 当作随机性强的对比项 |
5. 那些容易被忽略的工程细节:不止是调参
5.1 越界处理方式必须写清楚
算法位置更新时很容易跳出自变量范围。常见处理有三类:把越界个体拉回边界,把越界坐标按边界反弹,重新随机生成一个位置。很多论文完全不提这一点,但影响很大。我默认使用“拉回边界”,也就是:
Positions = max(Positions, lb); Positions = min(Positions, ub);这样操作简单,也不算浪费个体。对SSA这类链式结构,领导者一旦越界,后面的跟随者可能全被带偏,所以更要在每一次位置更新后马上加一次边界裁切。CGO这类多分支算法,如果使用“反弹”模式,数值上容易出现振荡,反而不如直接截断来得稳定。
5.2 最大化和最小化方向必须统一
很多开源算法内部默认按最小化写,但你实际可能是要最大化分类准确率或产品收益。如果直接把准确率传进去,算法会拼命让准确率变小。正确做法是在目标函数里返回负准确率,或者把问题转换成损失。我之前见过有人把SCA和CGO用反,最后结果和另一个算法差了好几个数量级,查了半天发现是优化方向写错了。
5.3 gbest更新顺序的坑
记录全局最优时,最常见的问题是在某个分支里更新了gbest变量,但下一次位置更新用的还是更新前的旧位置。这样记录曲线会正常,实际上个体追逐的目标是“上一版的gbest”,逻辑上存在半拍延迟。我建议每一轮迭代结束后,统一评估整个种群的适应度,再更新alpha、beta、delta或gbest。代码看起来多执行了一次筛选,但不容易出现变量覆盖错误,调试起来也省心。
5.4 从测试函数迁移到真实问题时怎么办
9个算法在Sphere和Rastrigin上跑得再好,也不等于能直接解决工程问题。比如做光伏MPPT,决策变量是占空比,需要处理电压电流的非线性关系;做PID整定,要同时优化三个参数,还要考虑系统稳定性约束。我的做法是,先小规模跑通接口,把objFun替换成我的实际计算模型,用较小的nPop和MaxIt验证每一轮种群更新是否合理,再逐步放大工作量。否则一上来就9个算法全跑,光等待时间就能把人耗没。
6. 选型逻辑与个人使用建议
6.1 按问题类型选算法,而不是按论文年份选
新算法不一定适合你的问题。我的选择逻辑大概是这样:
- 做PID参数整定、电力调度这类连续参数优化,PSO和GWO是稳妥的起步选项,代码容易改,出问题也好定位。
- 做特征选择这类离散问题,需要把连续位置转成二进制,SCA和CGO的连续振荡特性反而麻烦,一般先试PSO配合阈值转换。
- 做高维复杂多峰问题,WOA和HHO可以尝试,但必须多跑几次看分布,不要因为一次好结果就下结论。
- 做论文对比实验,最合适的组合是一老(PSO或GWO)、一新(HHO或SSA)、一小众(CGO或TSO),既有辨识度又不至于被质疑算法过老。
- 如果只是自己学习元启发式算法,先啃PSO和GWO,再啃WOA和HHO,最后再碰CGO和TSO,否则你很难判断随机波动到底来自算法还是实现。
6.2 发论文或写报告时,实验配置要交代完整
我整理实验时固定写清楚:Matlab版本、操作系统、统一种群大小和迭代次数、独立运行次数、边界处理方式、每个算法的参数来源是默认还是网格搜索、收敛曲线横坐标用的是迭代次数还是评价次数。这些东西写在附录里,看着琐碎,但能显著提高实验可信度。有人只放三张收敛图,换一版参数可能结果完全相反,后面被要求复现时很难处理。
6.3 最后分享一个个人习惯
我自己写对比模板时,所有算法都是runOptimizer这种统一接口。遇到新问题,我先用PSO跑三遍,得到一个“不太费力就能达到”的参考水平,再用其他算法去比。如果某个新算法连PSO都打不过,那大概率不是我用的测试函数太简单,而是它的默认参数在你这套代码里没调好,或者代码逻辑本身有问题。先用最稳的Baseline探底,再让新算法上场,这是最不容易冤枉某个算法、也最容易排查bug的顺序。9种算法的对比实验,真正值钱的不是你能说谁最强,而是你能说清楚它们各自在什么条件下强、什么条件下失效。