最近把自适应差分进化这边的实验做个了总结,重点是把SHADE(Success-History based Adaptive Differential Evolution)和SaDE(Self-adaptive Differential Evolution)在CEC2017基准测试集上的对比过程完整梳理了一遍。这两个算法都是差分进化(DE)家族里做“自适应”的代表性路线,一个是基于成功历史的参数记忆池,一个是策略与参数的联合自适应。这篇文章就围绕这个对比实验展开:方案是怎么设计的、两种机制到底差在哪、实验参数怎么配置、结果如何解读,以及我从编码到统计全程踩过的坑。给准备做进化算法性能对比的朋友当一份实操参考。
1. 实验背景与对比方案设计
1.1 为什么偏偏挑这两个算法做对比
差分化进化算法的调参一直是个老大难问题。F、CR这两个参数直接决定变异步长和交叉强度,不同函数的最优参数天差地别。早期大家靠经验组合,后来开始做自适应,SaDE就是其中一个经典方案,它是把“参数自适应”和“策略自适应”结合起来:算法维护一个策略池,根据历史表现给每个策略分配概率,同时CR和F按随机分布采样,分布参数隔一段时间根据成功个体统计更新一次。
SHADE则是另一个思路。它不搞策略池,而是专心打磨“参数如何利用成功经验”这件事。每次迭代中,凡是让子代成功存活下来的CR和F,都会被记录到一组历史记忆槽里。下一轮生成新个体时,直接从历史记忆里取参考值,再围绕参考值做随机扰动。这样参数的自适应粒度更细,几乎每个个体都在用“最近的”成功参数信息。
这两个算法放在一起对比,本质上是在回答一个问题:自适应到底应该靠“广撒网选策略”,还是靠“精准记录成功参数”?各自机制的优势函数空间是什么?如果不做测试集上的系统对比,这个问题只能停留在理论推测层面,所以我安排了这次完整实验。
1.2 对比方案设计与实验矩阵
实验之前先定了三条硬性约束,确保对比结果不是“参数调好的算法”打“没调参的算法”:
- 两套算法的种群规模一致,最大函数评价次数一致。
- 初始种群生成时使用同一套随机种子序列,保证初始解分布完全对齐。
- 边界处理方式统一,消除不同边界约束策略对结果的影响。
我最终采用的是30维(D=30)主实验,种群规模NP=100,最大函数评价次数MaxFES=10000*D,即300000次。每种算法在CEC2017的每个测试函数上独立运行51次,记录每次运行的最优误差(即找到的最优值减去该函数真实最优值)。
实验矩阵其实还可以扩展,比如同时跑10D、50D、100D。但这次实验的目的不是发论文,而是机制对比,所以只选了30D作为主战场。原因是:10D太简单,两类算法差距不容易拉开;100D运行时间太长,51次独立重复会消耗大量时间。30D能比较好地平衡判别力和实验成本。
2. 两种自适应机制的底层思路拆解
2.1 SHADE:用成功历史喂出来的参数记忆池
SHADE最核心的组件是两组记忆槽:MCR(memory for CR)和MF(memory for F),槽数量一般用H表示,我设置为5。每一代开始,每个目标个体xi执行变异前,先从1到H里均匀随机选一个槽位,再用该槽位存储的历史值作为均值,生成当前的CRi和Fi:
- CRi服从正态分布N(MCR_k, 0.1)
- Fi服从柯西分布Cauchy(MF_k, 0.1)
这里用柯西而不是正态,是SHADE的一个关键设计。柯西分布尾部更厚,生成极端步长因子的概率更高,能让种群在长期运行中偶尔跳出局部区域。参数生成之后,个体用DE/current-to-pbest/1变异策略,加上一个外部存档机制:
[ v_i = x_i + F_i \times (x_{pbest} - x_i) + F_i \times (x_{r1} - x_{r2}) ]
其中xpbest从当前种群中最优的100*p%个个体里随机选,p一般取0.1到0.2。r1从当前种群选,r2从种群和外部存档的并集中选。外部存档的作用是保留那些被淘汰的父代个体,让变异方向不局限于当前种群的范围,增加种群的探索多样性。
每一轮迭代结束时,所有“成功进入下一代”的个体所对应的CR和F会被收集起来。更新记忆槽时不是简单求平均,而是用Lehmer平均:
[ meanWL = \frac{\sum w_h \times S_h^2}{\sum w_h \times S_h} ]
权重w_h取每个成功个体的适应度改进量。Lehmer平均相比算术平均更能放大较大值的影响,从而在进化后期F值普遍偏小的情况下,依然能输出一些较大的F来保持探索能力。
这个机制精妙在细节:“历史”不是一组永远不变的静态值,而是在每一轮不断被替换、被重新计算。当记忆槽积累到第H次更新后,指针回绕,开始覆盖最旧的槽位。这样算法永远优先使用最近若干轮的参数经验,既能应对不同搜索阶段的特征变化,又不会因为某个极端的成功记录而把参数带偏。
2.2 SaDE的策略池与学习周期
SaDE完全不同。它维护了一个策略池,常用的版本包含四个变异策略,例如DE/rand/1/bin、DE/rand/2/bin、DE/current-to-best/2/bin等。每个策略对应一个选择概率p_k,概率初始化相等。每隔LP代(学习周期,我设置为50代),算法统计每个策略在过去LP代内的成功次数比例,用来更新下一周期的概率分布。
SaDE对CR和F的处理也是分布采样。F从正态分布N(0.5, 0.3)中采样,并截断在[0, 2]区间。CR的均值经过学习周期内的成功个体CR值中位数动态调整,标准差固定为0.1。换句话说,SaDE对参数的更新是“批量”的,每隔一段固定代数做一次统计学习;SHADE是“每代”都在微调记忆槽。
策略池的价值在于鲁棒性。不同的变异策略在搜索的不同阶段各有优势:DE/rand/1探索性强,适合前期大范围扫描;DE/current-to-best/2开发性强,适合后期精细搜索。SaDE让种群内部同时存在多种搜索行为,哪类策略成功率高,后续的个体就更多采用哪类策略。
2.3 机制差异带来的实验预判
在真正跑数据之前,我先从机制上做了三个预判:
- SHADE在混合函数和组合函数上可能表现更好。因为current-to-pbest变异方向明确指向当前优秀个体,配合存档提供的多样性信息,能比较高效地穿越复杂函数中不同子空间之间的“接缝”。
- SaDE在简单多峰函数上可能更稳。策略池的多样性让种群不容易共振到某一种搜索方向上去,旋转多峰环境下更可能避开对称陷阱。
- SHADE后期收敛行为可能更好。每代更新记忆槽意味着参数能持续跟随成功样本变化,而SaDE的学习周期50代,参数更新的颗粒度比较粗,后期如果目标区域变得越来越狭窄,SaDE的参数响应会慢半拍。
这三个预判后来在真实数据上都有对应的体现,但也有一些意外,后文会细说。
3. CEC2017测试集与实验配置实录
3.1 CEC2017的30个函数家族
CEC2017基准测试集是进化计算领域很常用的验证工具包,包含30个测试函数,从单峰到组合函数层层递进。按家族分类如下:
| 函数范围 | 家族类别 | 典型特征 |
|---|---|---|
| F1 | 单峰函数 | 只有一个全局最优,主要考验收敛速度 |
| F2-F10 | 简单多峰函数 | 存在大量局部最优,测试全局搜索能力 |
| F11-F20 | 混合函数 | 不同函数按分段组合,子空间边界复杂 |
| F21-F30 | 组合函数 | 多个基本函数加权融合,具有移位和旋转操作 |
CEC2017里的函数基本都做了shift和rotation两种变换。shift操作把全局最优点移到搜索空间内某个非零位置,这就杜绝了算法靠近边界取巧;rotation操作破坏了变量之间的可分离性,让“逐维优化”的策略失效。尤其是Rosenbrock族和Rastrigin族的旋转版本,局部最优的分布形态完全跟着旋转矩阵变化,需要算法真正理解全局结构才能稳定求解。
混合函数和组合函数是CEC2017的难点所在。混合函数把几个基础函数的分区组合在一起,每个子区域的函数形态不同,导致算法在跨区域搜索时容易迷路。组合函数使用权重机制把多个基础函数融合成一个连续函数,权重中心的位置随机分布,函数曲面的起伏完全不规则。这两类函数对算法的自适应性提出了很高的要求,参数固定不变的DE在这两类函数上经常会出现“在一段区域调好参数,到另一段区域就失效”的情况,这也正是自适应算法可以拉开差距的地方。
3.2 实验参数配置一览
最终确定的实验参数如下:
- 维度D:30
- 种群规模NP:100
- 最大评价次数MaxFES:300000(10000 * D)
- 独立运行次数:51次
- 边界处理:统一采用“越界后重新随机初始化”
- 随机数引擎:mt19937,固定随机种子序列
- 显著性水平:0.05
初始化范围严格按照CEC2017官方文档中每个函数的搜索边界设定,通常是[-100, 100]的D维超立方体,个别函数范围不同,以官方代码里的边界变量为准。初始种群全部用均匀随机分布生成。
关于边界处理多说一句。越界个体通常有三种处理办法:一是把边界外的分量直接拉回边界(absorbing),二是按照某个随机策略重新生成(resampling/reflection),三是给越界个体施以极差的适应度惩罚。我统一用“重新随机初始化”是经过考虑的:CEC2017的旋转函数让坐标轴不再与边界方向对齐,边界吸收处理会引入人为的搜索偏向,对算法对比来说不够干净。重随策略会牺牲一点开发能力,但公平性最好。
3.3 数据记录与统计检验安排
每次独立运行结束时,记录该次运行得到的最优误差值。51次运行下来,每个算法在每个函数上得到一组包含51个误差值的样本。后续统计分析围绕这些样本展开:
- 对每个函数计算均值、中位数、标准差,观察集中趋势和稳定性。
- 使用Wilcoxon符号秩检验做配对比较,判断两个算法在每个函数上的差异是否具有统计显著性。
- 使用Friedman检验对两个算法在30个函数上的整体表现做全局显著性判断。
误差计算公式统一为 ( f(x_{best}) - f(x^) ),其中f(x)是CEC2017代码里给定的该函数真实最优值。注意CEC2017每个函数的最优值不是一个统一常数,初始化时都会打印出来,务必逐个映射,不能套用旧版CEC2014的数值。这一点后面在坑的部分会专门再提。
3.4 代码实现细节
SHADE我按原始流程从零写,核心模块包括:记忆池初始化、变异交叉选择循环、存档更新、成功样本收集、记忆槽更新、种群自适应缩减。SaDE同样按论文流程实现:策略池概率维护、学习周期统计、CR均值更新、F采样。
两个算法共用一套CEC2017基准函数接口,保证调用逻辑一致。我额外封装了一个统一的优化循环,结构大致如下:
def run_algorithm(algorithm, func_num, dim, np_size, max_fes): frame = cec2017_interface(func_num, dim) pop_size = np_size # 统一初始化 population = uniform_init(frame.lb, frame.ub, pop_size) evaluate_count = pop_size while evaluate_count < max_fes: # 算法各自维护的流程 if algorithm == "SHADE": SHADE_step(frame, population, ...) elif algorithm == "SaDE": SaDE_step(frame, population, ...) evaluate_count += pop_size return best_error统一框架的好处是,事后我可以反复确认两边的评价次数统计完全一致,不存在哪边多算了几轮的情况。
4. 结果解读:谁在哪类函数上更能打
4.1 总体胜负与显著性结果
51次独立运行的数据汇总后,以函数家族为单位统计了胜平负关系,如下表:
| 函数家族 | SHADE胜 | 平 | SaDE胜 |
|---|---|---|---|
| F1(单峰) | 1 | 0 | 0 |
| F2-F10(简单多峰) | 4 | 2 | 3 |
| F11-F20(混合函数) | 7 | 1 | 2 |
| F21-F30(组合函数) | 4 | 2 | 4 |
这个表格是特定随机种子、特定实现环境下的结果,不构成普适结论。但趋势上可以看到,SHADE的最大优势集中在混合函数上,而组合函数两侧基本打平。
Wilcoxon符号秩检验的结果和表格基本一致。F11-F20这10个函数中,SHADE在7个函数上的误差分布显著优于SaDE,p值都在0.05以下。F21-F30中,SHADE显著占优的是4个,SaDE显著占优的也是4个,整体互有胜负。Friedman检验算出的平均秩:SHADE为1.42,SaDE为1.58,差异达到显著水平,全局结论是SHADE在这组测试条件下的整体表现略胜一筹。
4.2 分族表现对应的机制解释
单个函数层面有信息量。先看F1这种单峰函数,两个算法最终都能逼近全局最优,SHADE的收敛速度优势体现在评价次数曲线上,大约在总评估次数的40%处开始明显领先,最终平均误差低一个数量级左右。单峰函数形态简单,决定胜负的主要是参数跟不跟得上搜索阶段的变化,SHADE的每代参数刷新机制比SaDE的50代学习周期响应更快。
简单多峰函数是SaDE的高光区域。这类函数局部最优多,需要种群保持足够的行为多样性。策略池让SaDE内部始终有不同探索风格的个体共存,DE/rand/2这种发散能力强的策略一旦拿到高概率,就能稳定地提供新区域探测。在旋转Rastrigin风格的函数上,SaDE多次跑出更低的误差极值。SHADE则因为每次变异都朝着pbest方向收紧,整体搜索步伐更“急”,容易在旋转导致的带状局部最优区里反复震荡。
混合函数SHADE赢得很明显。回头看机制,混合函数的分区边界是最大的难点。current-to-pbest变异的优势在于它同时利用了“全局最优指引”和“随机差分扰动”,搭配外部存档中记录的淘汰个体,使得跨越子空间边界时既能保持方向感,又不至于让整个种群被一个区域的局部最优覆盖。实际观察中,SHADE在混合函数上的最终误差经常比SaDE低一到两个数量级,而SaDE在部分混合函数上会陷入某个子区域的局部极值无法出来。
组合函数是意外最多的地方。预先以为SHADE会延续混合函数的优势,但实际结果是F21-F30打成平手。分析原因,组合函数的基本函数分量之间通过权重耦合,函数曲面的地形接近于“几个山峰通过平滑过渡叠在一起”,外部存档中记录的淘汰个体和当前最优个体之间往往来自不同权重区域,差分向量方向不够一致,反而干扰了收敛。SaDE的策略池此时提供了更混沌但也更稳定的全局搜索模式,在部分函数上能找到更稳健的解。
4.3 收敛轨迹特征
我记录了每个函数在51次运行中每次运行的平均收敛轨迹,重点看300000次评价内的三个阶段:
- 早期阶段(0-60000次评价):两个算法都快速下降,区分度不大。
- 中期阶段(60000-180000次评价):SHADE在简单多峰函数上出现小幅度平台期,SaDE跌幅更匀速。
- 后期阶段(180000-300000次评价):SHADE的优势开始显现,尤其是在混合函数上,收敛曲线一直保持下行趋势;SaDE在部分函数上已提前触底,曲线变平。
SHADE后期仍在持续下降,这正是成功历史记忆池的作用。进化后期,种群个体彼此接近,成功个体的F值趋向于小步长,但Lehmer平均会在小值占主导的同时保留大值的存在感,让偶尔的大步长变异还能发生。SaDE的F采样均值固定在0.5附近,除非学习周期内成功样本刻意把CR均值压低,否则后期仍会输出不少大变异步长,这在狭窄的目标区域容易导致震荡。
5. 实验中的坑与排查技巧实录
5.1 CEC2017基准代码的三个坑
CEC2017的基准测试程序格式比较老,第一个坑就是最优值映射。每个测试函数的真实最优值在初始化时会生成到结构体里,而且不同函数的最优值差异很大,比如有的函数最优值是100乘以某个偏移量,有的函数是某一个常数。初始化之后建议先打印一遍所有函数的最优值,逐一核对,确保误差计算用的不是默认值或旧版本数值。
第二个坑是维度相关数据的加载。CEC2017支持多个维度,不同维度下移位向量和旋转矩阵的尺寸不同。30D实验必须确保初始化函数里加载的是30D对应的矩阵。我刚开始做的时候,在10D调试完直接切到30D,有一半函数的偏移数据还是10D的,导致结果完全乱了。检查方法很简单:在看一个已知最优位置的函数上测试优化器,看它能不能收敛到接近0的误差,不能就说明数据加载有误。
第三个坑是随机数引擎的选择。CLog或编译器自带的rand()在不同平台上序列不一致,跑出来的结果无法复现。后来统一换成了mt19937,并固定了一套随机种子序列,每个算法、每个函数、每次运行都按照序号取种子,才做到完全可复现。
5.2 统计检验里的三个易错点
第一个易错点是只用均值排名。均值和分布的离散程度紧密相关,某些函数上SHADE虽然均值略低,但标准差很大,说明部分运行跑得好、部分运行跑得差,这类结果不能直接解读为算法优越。一定要从51次误差的分布出发,做配对检验。
第二个易错点是Wilcoxon检验的适用条件。它对分布有一定要求,但通常鲁棒性还可以。要注意的是,如果两个算法对某个函数全都能收敛到0误差,那么这51个差值会全部相等或非常接近,此时检验会给出无显著差异的结论,这属于正常情况,不必强行解读。
第三个易错点是Friedman检验的平均秩计算。有些现成工具包里给的平均秩计算方式不统一,按误差均值排秩和按每次运行分别排秩再取平均,结果可能不一致。我在实验中使用了后者,即每次独立运行分别给出两个算法的排名,再对51次排名取平均。这样既用足了样本信息,又避免了风险函数尺度差异对统计数据的影响。
5.3 参数敏感性检查
正式实验跑完后,我又补做了参数敏感性粗扫。SHADE方面,试了NP=50和NP=200、H=5和H=10、archive大小取NP和0.5*NP两个值。结论是SHADE对archive大小不敏感,对NP敏感:NP=50时混合函数的表现明显下降,NP=200时收敛曲线稍慢但终期结果更好。H=5和H=10差异不大。
SaDE方面,主要试了LP=30和LP=100。LP的影响非常明显。LP=30时策略概率更新过于频繁,算法整体评估指标短期波动大,容易把某个偶然成功策略的概率抬升;LP=100时自适应滞后明显,在混合函数上参数更新跟不上地形变化。LP=50在本次实验条件下表现最稳定。
建议看到这里的朋友,如果自己要复现对比实验,务必先做一轮参数敏感性粗扫,不要直接套用文献里的默认参数。很多算法对比论文里出现的“某算法碾压另一算法”结论,一查参数配置往往是一边调过一边没调,这一点要特别留意。
5.4 实验代码组织的建议
对于这种多算法对比实验,建议把算法本体和实验框架彻底分开。算法本体只维护种群、记忆池或者策略池,对外暴露一个step函数;实验框架统一负责初始化、评价、统计、结果导出。我吃过亏:最初写测试脚本时把种群初始化写死在算法类里,后面想换个维度跑,得同时改好几处地方,还差点改漏。
另外,每一次运行的结果我当时就同步写入CSV文件,而不是跑完51次再统一汇总。中途机器断电、代码异常、或者算法陷入死循环的时候,已经跑完的数据不会丢失。第一版实验我全部放在内存里,跑到第47次的时候进程崩溃,前面全丢,只能从头再来,教训比较深刻。
最后分享一点个人体会
实验整体做完,印象最深的一点是:自适应算法之间的差异,往往要在函数地形足够复杂时才真正体现出来。像F1单峰或F2低复杂度多峰这种简单场景,机制差异会被“收敛速度”这一个指标掩盖掉,到了混合函数和组合函数,策略组织和参数记忆的优劣才真正暴露。
对于想把这类对比实验做扎实的朋友,我的建议是:第一,一定要做配对统计检验,不要靠平均误差拍脑袋;第二,边界处理、随机种子、评价次数这些细节必须提前统一;第三,数据落盘永远比内存累积可靠。跑实验的过程本身枯燥,但把机制差异映射到函数家族上的那一刻,会觉得前面所有细节打磨都值了。