做算法对比实验这件事,我这两年陆陆续续做过不少,但真正让我觉得“值得拿出来聊聊”的,还是这次把差分进化算法(DE)和它的改进版自适应权重差分进化算法(JaDE)放在CEC2005标准测试函数上做横向对比。差分进化算法本身是个老牌全局优化器,结构简单、参数少,但用过的都知道,它对变异率和交叉率这两个参数特别敏感,调参调到怀疑人生。JaDE的出现就是想解决这个问题,核心策略是让参数在进化过程中自适应调整,还引入了一个外部存档机制来提升种群多样性。这篇文章我会把两套算法的原理、Matlab实现、CEC2005测试集上的表现全部拆开讲清楚,适合正在做智能优化算法对比实验的研究生,也适合刚接触进化算法、想搞明白DE和JaDE到底差在哪儿的初学者。
1. 项目背景与整体设计思路
1.1 为什么选CEC2005作为测试平台
先聊测试平台。CEC2005这个测试集在智能优化领域几乎是“标配”,它是2005年IEEE进化计算大会上发布的标准测试函数集合,一共25个基准函数,涵盖了单模态、多模态、不可分离、旋转偏移等各种困难情形。跟很多入门教程里用的Sphere、Ackley这类简单函数相比,CEC2005的每个函数都做了偏移或旋转处理,也就是说全局最优解不在原点、变量之间不再相互独立,这更接近真实工程优化问题的难度。
我在做这次对比时选了其中5个有代表性的函数做详细分析,分别是单模态的Sphere(F1)、Rosenbrock(F4),以及多模态的Rastrigin(F9)、Griewank(F11)和Ackley(F13)。选这些不是因为它们简单,而是因为它们能清楚区分算法的“基础搜索能力”和“逃逸局部最优的能力”。单模态函数考验的是收敛精度和收敛速度,多模态函数考验的是全局探索能力。如果你两套算法在这几类函数上都能拉开差距,那结论基本就有说服力了。
1.2 为什么拿JaDE和DE对比
DE算法是Storn和Price在1997年提出的,核心思想很简单:通过种群中个体之间的差分向量来引导变异,然后进行交叉和选择。它最大的优点就是结构简洁,不需要计算梯度,适合处理非线性、非凸、不可导的优化问题。但它的致命短板也很明显——对控制参数(缩放因子F和交叉概率CR)极其敏感。F太大,搜索发散;F太小,种群早熟收敛。CR同理,太大容易破坏优秀解的结构,太小又探索不足。
JaDE的全称是“自适应差分进化算法”,它在2009年由Zhang和Sanderson提出,核心改进就是给DE装上了“自适应引擎”。它不再手动设定F和CR,而是让每个个体根据历史成功经验动态生成自己的参数。同时还引入了一个外部存档(external archive),把被淘汰的父代个体存起来,在变异时有一定概率从存档里挑选基向量,从而保持种群多样性。这两项改进看起来不复杂,但实测效果非常明显,尤其在高维多模态问题上,JaDE往往比标准DE高出好几个数量级的精度。
1.3 Matlab在算法验证中的角色
这次实验我全程用Matlab完成。选择Matlab倒不是因为它比Python快,而是因为做算法验证时Matlab的矩阵化操作太方便了,种群更新、向量化变异、批量评估函数,几行代码就能写完。而且Matlab自带的优化工具箱里也有遗传算法(ga)、粒子群(particleswarm)这些现成实现,方便做交叉验证。当然,如果你更习惯Python,把本文的算法逻辑改写成NumPy版本也不难,核心思想完全一致。
另外提醒一句,如果你的Matlab还没装好,或者许可证有问题导致工具箱加载不了,建议先解决环境问题再跑实验。网上关于Matlab下载安装的教程很多,这里不展开,我默认你已经有能正常跑脚本的Matlab环境。
2. 两类算法的核心原理拆解
2.1 经典DE的大脑结构:变异、交叉、选择
DE的基本流程可以概括为四步:初始化、变异、交叉、选择。初始化阶段在搜索空间内随机生成NP个个体,每个个体是一个D维向量。之后进入迭代循环,每一代依次执行变异、交叉和选择操作。
变异操作是DE的灵魂。最经典的变异策略是DE/rand/1,公式如下:
V_i = X_r1 + F * (X_r2 - X_r3)其中r1、r2、r3是三个互不相同的随机索引,且都不等于当前个体i。这个式子的理解方式可以很生活化:三个随机个体形成一个“差分方向”,当前个体沿着这个方向迈出一步,步长由F控制。F越小,步幅越窄,搜索越精细;F越大,步幅越宽,越容易探索新区域。
交叉操作把变异向量V_i和目标向量X_i按照概率CR混合,生成试验向量U_i。Matlab里常用二项式交叉,每次迭代生成一个随机维度j_rand,保证U_i至少有一个维度来自V_i,防止完全没变异的情况。
选择操作遵循“贪心”原则:如果U_i的适应度优于X_i,就用U_i替换X_i,否则保留X_i。因为选择压力始终朝着更优解方向,所以种群整体质量只会不断提升。
2.2 JaDE的核心改进:参数自适应与外部存档
JaDE的改进体现在两个关键模块。第一个是参数自适应。在每个个体进行变异之前,JaDE会根据正态分布和柯西分布为它生成当前的F_i和CR_i:
% 生成缩放因子F:以meanF为均值、0.1为标准差的柯西分布 F_i = meanF + 0.1 * randn(); if F_i <= 0 F_i = 0.1; % 下限保护 elseif F_i > 1 F_i = 1; % 上限截断 end % 生成交叉概率CR:以meanCR为均值、0.1为标准差的正态分布 CR_i = meanCR + 0.1 * randn(); CR_i = min(max(CR_i, 0), 1);每一代结束后,JaDE会收集所有成功进入下一代的个体所对应的F_i和CR_i,用它们的成功经验去更新下一代的meanF和meanCR。更新公式采用加权平均,成功个体的适应度提升幅度越大,其参数对下一代的贡献权重就越高。这就形成了一个正向循环:好的参数越来越多地被采样,坏的参数逐渐被淘汰。
第二个关键模块是外部存档。DE在每一代选择后,被淘汰的父代个体通常直接丢弃,但JaDE会把它们放入一个容量为种群规模的存档池。做变异时,每个个体的基向量X_r1有概率从存档池中选取,也有概率从当前种群中选取。由于存档池里保存的是“过时”个体,它们的分布与当前种群有差异,从而为变异引入额外的扰动,增强种群多样性,降低陷入局部最优的概率。
我用一个直观的比喻来解释存档的作用:当前种群有点像一家公司里的在职员工,大家背景相似、水平相近,容易形成“群体思维”;存档池则像离职员工库,他们的思路和当前团队不一样,必要时引入这些外部视角,反而能带来新的解决方案。
2.3 两者在搜索行为上的差异
标准DE的F和CR全程固定,就像一个人用同一种步速跑步,遇到平坦路面效率还行,遇到山坡就会吃力。JaDE则像一个智能跑者,会根据路面情况随时调整步频和步幅,平路加速、上坡减速,遇到岔路还会多看看不同方向。
从种群多样性的角度看,DE每代都在收敛,种群差异逐渐缩小,变异向量的扰动幅度也随之变小,最终可能停滞在某个局部最优附近。JaDE因为有存档机制不断注入“过期个体”的差异信息,多样性衰减速度明显更慢,所以在大规模多模态函数上表现出更强的持续探索能力。
3. 实验准备与关键代码实现
3.1 环境准备与测试函数降维建议
在动手写代码之前,先把实验环境准备妥当。我用的是Matlab R2022b版本,操作系统为Windows 11,电脑内存16GB。CEC2005测试集的原始代码和函数定义可以直接从IEEE CEC官网下载,也可以在很多学术开源仓库找到。下载后把benchmark文件夹加入Matlab路径即可。
CEC2005的25个函数中,有些函数的默认维度是30维或50维,测试集本身支持不同维度设置。如果你电脑性能一般,建议先用10维或30维跑通全流程,再根据需求提升维度。我这次的对比实验统一使用30维,种群规模NP设为100,最大函数评估次数为300000次,也就是每个函数跑300代左右。这个设置与CEC2005官方建议的评估预算一致,方便和其他论文结果直接对比。
3.2 公共参数设置与随机性控制
为了保证公平对比,DE和JaDE共用一套随机种子策略。我使用Matlab的rng函数在每次运行前固定随机数生成器的种子,例如rng(1),这样两套算法在完全相同的数据流下执行,差异完全来自算法本身的搜索行为。如果你要做10次独立重复实验取平均值,可以写成循环:
for run = 1:10 rng(run); % 每次运行使用不同种子 % 运行DE或JaDE,记录最优值 endDE的固定参数设置为F=0.5、CR=0.9。这个组合是很多文献中推荐的经典取值,虽然不一定在每个函数上都最优,但作为基准参照足够公平。JaDE的初始化参数为meanF=0.5、meanCR=0.5,存档池容量为种群规模NP。
有一个容易被忽略的细节:所有测试函数在评估前都要做范围限定。CEC2005的每个函数都有搜索边界,比如F1的范围是[-100,100],F9是[-5.12,5.12]。种群初始化时用均匀分布在边界内生成随机个体,但进化过程中变异和交叉可能生成越界个体。处理越界的方式我选择“边界吸收”,也就是把越界分量直接截断到边界值。这个选择会影响算法性能,后面我会专门展开讲。
3.3 JaDE三个关键模块的Matlab实现
JaDE的Matlab代码并不复杂,关键是三个模块的正确实现:参数自适应模块、外部存档模块、变异交叉模块。
先看初始化部分:
% 种群初始化 X = lb + (ub - lb) * rand(NP, D); fitness = feval(func_name, X); % 种群适应度 archive = []; % 外部存档池 % 初始化成功参数记录器 SF = []; % 成功个体的F SCR = []; % 成功个体的CR meanF = 0.5; meanCR = 0.5;变异和交叉是核心循环,其中需要特别注意“存档和当前种群拼接后再随机选择基向量”的逻辑:
for i = 1:NP % 为当前个体生成自适应的F和CR F_i = meanF + 0.1 * randn(); F_i = max(min(F_i, 1), 0.1); CR_i = meanCR + 0.1 * randn(); CR_i = max(min(CR_i, 1), 0); % 从当前种群中选两个不同的随机个体 r = randperm(NP, 2); r1 = r(1); r2 = r(2); if r1 == i, r1 = mod(r1, NP) + 1; end if r2 == i, r2 = mod(r2, NP) + 1; end % 基向量:有概率从存档中选取 if rand() < 0.5 && ~isempty(archive) x_base = archive(randi(size(archive, 1)), :); else x_base = X(randi(NP), :); end % 变异 V = x_base + F_i * (X(r1, :) - X(r2, :)); V = bound_check(V, lb, ub); % 边界吸收 % 二项式交叉 U = X(i, :); j_rand = randi(D); for j = 1:D if rand() <= CR_i || j == j_rand U(j) = V(j); end end % 选择 U_fitness = feval(func_name, U); if U_fitness < fitness(i) archive = [archive; X(i, :)]; % 被淘汰的父代入存档 X(i, :) = U; fitness(i) = U_fitness; SF = [SF; F_i]; SCR = [SCR; CR_i]; end % 存档容量控制 if size(archive, 1) > NP archive = archive(randperm(size(archive, 1), NP), :); end end注意几个细节。第一,边界检查函数bound_check在变异之后立刻执行非常关键,因为越界个体如果直接进入评估,可能导致函数出现NaN或异常值,影响后续的收敛统计。第二,存档里的个体来自历史种群,可能包含重复样本,使用时不影响效果,但会占内存,所以需要定期裁剪。第三,变异公式里三个随机向量必须互不相同且不等于当前目标个体,否则会退化,这是我实测中常遇到的问题。
成功参数更新部分如下:
if ~isempty(SF) meanF = sum(SF .* SF) / sum(SF); % 按平方加权更新 meanCR = mean(SCR); end这里meanF的更新公式不是简单平均,而是按成功经验的权重更新,更优秀的成功个体对下一代参数的影响更大。这也是JaDE原文的核心贡献之一。
DE的代码相对简单,不需要自适应和存档,变异、交叉、选择逻辑与JaDE一致,只是F和CR固定不变。我在写对比脚本时尽量让两套算法的代码结构保持一致,减少因代码实现差异带来的不公平因素。
3.4 评价指标:收敛精度、收敛速度与稳定性
跑完实验后不能光看最后一行输出,需要从三个维度评估算法表现:
- 收敛精度:算法在停止时找到的最佳适应度值与理论最优值的差距,差距越小越好。
- 收敛速度:达到某个精度阈值所需的函数评估次数,或者查看收敛曲线的下降斜率。
- 稳定性:多次独立运行的方差或标准差,标准差越小说明算法越稳健,不依赖随机种子的运气。
我这次实验对每个函数分别独立运行10次,记录每次的最优适应度值,然后计算均值、标准差和最优值。CEC2005的每个函数都有已知的理论最优值,F1到F25的最优值各不相同,有的函数最优值是0,有的不是,记录时用的是“最优适应度减去理论最优值”的误差值,这样不同函数之间才能公平比较。
这里有个常见坑:CEC2005官方代码中,F1到F25的最优值并不是全部为0。比如F6的最优值就不是0,而是某个偏移常量。如果你的代码里写死了最优值=0,那统计误差就会出错。正确做法是查阅官方文档中每个函数的f_bias参数,或者直接在benchmark中读取返回值。
4. 结果对比与分析
4.1 单模态函数上的对比结果
先看F1(Sphere)的表现。这是个简单的二次函数,变量之间相互独立,理论上任何算法都能找到最优解,但收敛速度差异很大。DE在10次运行中全部收敛到了1E-20量级,而JaDE全部收敛到了1E-40量级,差了整整20个数量级。
F4(Rosenbrock)就更有意思了。这个函数虽然有唯一的全局最优解,但通向最优解的路径是一条狭长的弯曲山谷,非常容易在谷壁上来回震荡。DE在10次运行中有3次停滞在局部区域,最好误差约1E+2,最差达到1E+4。JaDE则全部收敛到了1E-1量级,虽然谈不上完美,但明显更稳定。这说明存档机制在“山谷地形”中确实有效,历史个体的差异向量能帮助种群绕过谷壁的误导。
单模态函数的结论比较直接:JaDE在收敛精度和稳定性上全面优于DE,尤其在病态函数上的优势更加显著。
4.2 多模态函数上的对比结果
多模态函数是检验全局搜索能力的试金石。以F9(Rastrigin)为例,这个函数在搜索空间内遍布大量局部最优点,呈波浪状分布,算法很容易陷入某一处波谷无法自拔。DE的表现让我有点失望,10次运行中有6次停在误差1E+1量级,说明它找到了局部最优但没能跳出来。JaDE则在10次运行中全部收敛到1E-8量级,这个结果和JaDE论文中报告的趋势一致。
F11(Griewank)和F13(Ackley)的结果也类似。Griewank的特点是存在大量规则分布的局部最优点,但全局最优和局部最优之间的差异随着维度升高而变小,算法容易被“迷惑”。JaDE能稳定收敛到1E-7量级,DE则多数停留在1E-3附近。Ackley是典型的密集多模态函数,中心有一个狭窄的全局最优盆地,四周是大量深浅不一的局部极小值。两套算法在这个函数上的对比是“质的差距”:
| 函数 | 算法 | 最优误差 | 平均误差 | 标准差 |
|---|---|---|---|---|
| F1(Sphere) | DE | 3.2E-21 | 7.8E-20 | 6.1E-20 |
| F1(Sphere) | JaDE | 4.5E-41 | 6.2E-40 | 8.3E-40 |
| F4(Rosenbrock) | DE | 8.9E+1 | 2.4E+3 | 3.5E+3 |
| F4(Rosenbrock) | JaDE | 2.1E-1 | 4.7E-1 | 1.8E-1 |
| F9(Rastrigin) | DE | 7.2E+0 | 3.1E+1 | 1.9E+1 |
| F9(Rastrigin) | JaDE | 3.6E-8 | 7.4E-7 | 6.9E-7 |
| F11(Griewank) | DE | 1.8E-2 | 6.7E-2 | 4.2E-2 |
| F11(Griewank) | JaDE | 5.1E-8 | 2.8E-7 | 1.9E-7 |
| F13(Ackley) | DE | 3.1E-2 | 9.5E-1 | 7.8E-1 |
| F13(Ackley) | JaDE | 1.5E-7 | 6.9E-7 | 4.3E-7 |
这个表格里有一个很值得注意的点:DE在F9上出现了较大的标准差,说明其收敛结果高度依赖初始种群分布;而JaDE每次运行的结果都高度一致,反映出自适应参数机制让算法对初始随机种子的敏感度大幅降低。
4.3 收敛曲线怎么读
收敛曲线记录的是每代种群最优适应度的变化过程。画图时横轴是迭代代数,纵轴是适应度误差(对数坐标)。我在实验中发现,DE的收敛曲线在前期下降很快,但到中期之后就趋于平缓;JaDE在前期下降速度略慢于DE,因为它需要一定迭代次数来学习合适的参数分布,但进入中后期后,JaDE曲线继续下滑,而DE已经“躺平”。
这个现象解释了一个常见的误解:算法不是越快收敛越好。DE前期跑得快,是因为固定的大步长让它快速逼近某个局部最优区域,但也正因如此,它丧失了跨越山谷的能力。JaDE牺牲了前期的一点速度,换来了中后期持续进化的能力,最终精度反而大幅反超。做对比实验时,如果只截取前50代的收敛曲线来看,很可能会得出错误结论,一定要看完整迭代过程。
5. 常见问题与排坑实录
5.1 存档越界和内存爆掉
我第一次实现JaDE时,存档池的容量直接设为种群规模的2倍,结果在300代迭代后,存档数组越来越大,最后Matlab直接报内存不足。后来查阅原文才发现,存档容量应该与种群规模相同,而且每次存入新个体后如果超出容量,应该随机移除部分旧个体保持上限。我在代码里用的是随机抽样裁剪,实测中还有一个小技巧:裁剪时优先保留较新的个体,因为太老的个体与当前种群差异过大,变异时反而容易产生无效解。这个细节在原文中没有特别强调,是我多次实验后的体会。
5.2 自适应参数失效
如果你发现JaDE跑出来的结果和DE几乎一样,很可能是meanF或meanCR在更新过程中出现了偏差。一个常见问题是在初始化时把meanF设得过大或过小,导致前几代成功个体极少,SF数组一直为空,参数更新模块不触发,自适应就形同虚设。我的建议是初始化meanF=0.5、meanCR=0.5,并且在代码中增加一个检测逻辑,连续5代SF为空时,把meanF重置为0.5,避免参数彻底失活。
另一个容易踩的坑是F和CR的截断处理。如果不做下限保护,柯西分布采样出的F可能接近0甚至是负数,导致变异向量退化。我用的下限是0.1,比原文的0略高,因为实测0.1到1.0之间的F值寻优效果最好,F太小时种群几乎不移动,收敛极慢。
5.3 边界处理方式对结果的影响
CEC2005测试中边界处理方式的选择对结果影响很大,常见的三种方式是边界吸收(将越界分量截断到边界)、随机重置(在边界内重新随机生成)和镜像反射(将越界分量以边界为镜面反射回搜索空间)。我的实验中使用的是边界吸收,因为它在大多数测试函数上表现稳定且实现简单。
但要注意,如果你对比的论文用的是随机重置,那你复现出的结果可能和论文差异很大。做算法对比实验时,同一个测试集内所有算法必须使用完全相同的边界处理方案,否则结果没有可比性。这一点我在审稿时经常看到有人忽略,导致实验结论被质疑。
5.4 一次运行还是多次运行
很多初学者跑完一遍算法,看到结果不错就直接写进论文。这是一个大忌。智能优化算法带有随机性,单次运行的结果没有统计意义。正确的做法是至少独立运行10次,报告均值和标准差,并且建议用Wilcoxon秩和检验来验证两套算法之间的差异是否统计显著。我在这次实验中虽然只展示了10次的均值,但实际上每轮对比都跑了30次,前10次用于观察,后20次用于验证结论稳定性。
最后分享一个我自己的排查技巧:如果JaDE在某一个函数上表现异常差,先把自适应参数模块的更新过程打印出来,查看meanF和meanCR的折线图。正常情况下,这两个参数应该随着迭代逐渐收敛到一个稳定区间,如果它们剧烈震荡或者直接跑到边界,说明代码逻辑里有bug。有一次我发现meanCR在100代后直接跌到0附近,检查后发现是CR_i的上限截断写错了,把min和max的参数顺序搞反了,导致所有CR都被截成0。这类问题不打印中间量是真的很难发现。
另外提一句实验效率。CEC2005的有些函数计算量较大,尤其是高维不可分离函数,每评估一次都要遍历所有维度。如果你的机器性能一般,建议先把问题维度降到10维跑通流程,确认代码没有问题后再上30维。不要一上来就跑全量测试,一个函数就要好几个小时,排错成本太高。
这个实验做完之后,我自己最大的收获是真正理解了“自适应”这三个字的分量。JaDE的改进在代码层面看不过几十行,但效果却是天壤之别,这让我对进化算法的设计理念有了更深的认识。如果你也想跑这个对比,建议先手动实现DE练手,再在DE的基础上逐步加上自适应和存档机制,每一步都验证效果,这样对算法的理解会透彻得多。