简介:面向进化计算与优化算法研究者,这份材料围绕2017年CEC测试函数集展开,提供可运行的MATLAB实现、函数说明与配套文档,适合用于算法验证、性能对比、课程实验及论文复现。压缩包共337个文件,约8.4MB,以329个txt数据与说明文件为主,另含3个m脚本、2个docx总结文档、1个cpp源文件、1个mexw64编译组件和1个pdf官方说明,覆盖函数定义、测试数据与调用入口。已有6661人学习,说明其在优化社区具有一定参考价值。读者可据此快速搭建CEC2017单目标优化实验,理解20个基本函数的结构、搜索空间与多模态特征,并借助示例脚本完成算法调试、结果复现和不同维度下的性能分析,减少从零整理测试环境的时间。
1. 从「第一次跑出一堆 Inf」说起:这套 2017CEC 测试函数到底能干什么
很多人第一次把自研的优化器接到 2017CEC 测试函数上,跑出来的不是漂亮收敛曲线,而是一串 Inf、NaN,或者 30 个函数误差几乎一模一样的结果——这不是算法坏了,是测试集没接对。这份 2017CEC 测试函数与 MATLAB 代码,把 30 个标准函数的移位、旋转、混合、组合以及已知最优值全部封进一套可直接调用的.m文件里,搜索范围统一在[-100,100]^D,维度支持 10/30/50/100。它适合三类人:要跑对比实验凑实验表格的、想验证自己新算子到底有没有用的、以及第一次接触 CEC 测试函、不知道从哪下手的开发者。下面按「能不能跑通 → 核心怎么实现 → 怎么改造成自己的实验台」的顺序拆。
2. 套件骨架:30 个函数是怎么被组织成一份可调用代码的
拿到压缩包先别急着run,先搞清楚编号和函数类型的对应关系。2017 版和 2014 版最大的差别在混合函数和组合函数的配比上,编号错一位,整张实验表格的结论就是错的。这一章把函数清单、目录结构和调用约定先梳理清楚,后面调试才有参照系。
2.1 四组函数的分工:单峰、多峰、混合、组合
30 个函数不是随便凑数,而是按「难度阶梯」分组的:F1~F3 是单峰,用来测收敛速度;F4~F10 是简单多峰,测跳出局部最优的能力;F11~F20 是混合函数,把不同形态的子函数拼在一个定义域里;F21~F30 是组合函数,靠距离权重让多个子函数「轮流当家」。这个分组直接决定了你实验表格该怎么分块展示。
| 编号 | 函数名 | 类型 | 主要考察点 |
|---|---|---|---|
| F1 | Shifted and Rotated Bent Cigar | 单峰 | 沿单一方向的收敛效率 |
| F2 | Shifted and Rotated Sum of Different Power | 单峰 | 高次幂,数值极易溢出 |
| F3 | Shifted and Rotated Zakharov | 单峰 | 病态条件下的稳定性 |
| F4 | Shifted and Rotated Rosenbrock | 多峰 | 狭长山谷里的路径跟随 |
| F5 | Shifted and Rotated Rastrigin | 多峰 | 大量规则局部最优 |
| F6 | Shifted and Rotated Expanded Schaffer | 多峰 | 强非线性耦合 |
| F7 | Shifted and Rotated Lunacek Bi-Rastrigin | 多峰 | 双峰盆地的欺骗性 |
| F8 | Shifted and Rotated Non-Continuous Rastrigin | 多峰 | 不连续造成的搜索断层 |
| F9 | Shifted and Rotated Levy | 多峰 | 平坦区与陡峭区混合 |
| F10 | Shifted and Rotated Schwefel | 多峰 | 最优解靠近定义域边界 |
| F11~F20 | Hybrid Function 1~10 | 混合 | 分块异构,局部搜索策略切换 |
| F21~F30 | Composition Function 1~10 | 组合 | 权重随位置变化,全局最优分散 |
按这张表准备实验时,一个常见做法是:先只在 F1、F3 上验证算法本身没写错,再扩展到 F4~F10 看跳出局部最优的能力,最后才上 F11~F30。反过来直接全跑,一旦某处维度对不上,30 个函数一起报错,排查成本会高很多。
2.2 目录结构与数据文件:真正决定函数长什么样的是 txt
代码本身很薄,重的是input_data里那些纯文本文件。移位向量、旋转矩阵、打乱序列全部存在 txt 里,.m只负责读取和做运算。所以这套代码能不能复现别人的结果,取决于你有没有把input_data完整拷过去。
cec17/ ├── cec17_func.m % 统一入口,按 func_num 分发 ├── bent_cigar_func.m % 各基础函数实现 ├── sum_diff_pow_func.m ├── zakharov_func.m ├── rosenbrock_func.m ├── ... ├── hybrid_func*.m % 混合函数 ├── cf*.m % 组合函数 └── input_data/ ├── shift_data_1.txt % 1×D 或 K×D 的移位向量 ├── rotate_data_1.txt % D×D 或 K*D×D 的旋转矩阵 ├── shuffle_data_11.txt % 混合函数的分块与打乱顺序 └── ...说几个读文件时容易忽略的点。shift_data_*.txt对基础函数通常只有一行,长度可能比你要跑的维度长,所以取前 D 个元素就行;组合函数的移位文件是 K 段拼在一起的,要按(i-1)*D + (1:D)去切。rotate_data_*.txt对组合函数是K*D × D,对基础函数是D × D,如果直接load完就拿去乘,维度不报错但结果是错的。shuffle_data_*.txt只在混合函数里用,作用是把维度打乱,保证各子函数的变量不是简单的前后相邻。
提示:判断自己引用的到底是 2017 版还是 2014 版,最快的办法是看
input_data里shuffle_data的最大编号——2017 版混合函数有 10 个,编号能到 20 附近;版本搞混了,F11 之后的结果全都对不上。
2.3 统一入口的调用约定:x 的行列、维度、返回值
整个套件对外只暴露一个函数,其余都是内部实现。结构上大致是这样一个分发表:
function f = cec17_func(x, func_num) % 统一入口:x 为 1×D 行向量,func_num 取 1~30 x = x(:)'; % 强制行向量,避免列向量参与矩阵乘时维度错位 D = numel(x); % 维度不单独传参,从 x 的长度推断 switch func_num case 1 f = bent_cigar_func(x, D, 1); case 2 f = sum_diff_pow_func(x, D, 2); case 3 f = zakharov_func(x, D, 3); % ... F4~F10 同构 case 11 f = hybrid_func1(x, D, 11); % ... F12~F20 case 21 f = cf01(x, D, 21); % ... F22~F30 otherwise error('func_num 必须在 1~30 之间'); end end这里的几个参数含义值得说清。x必须是行向量,因为旋转那一步是y * M的写法,列向量乘矩阵会直接报错;D不显式传入,靠numel(x)推断,好处是同一份代码能跑 10/30/50/100 四个维度,代价是你如果传了个1×50的向量去跑本来只定义了 30 维的函数,代码不会拦你,只会算出一个没人验证过的数。func_num是函数编号,同时被用作偏置量,官方约定第 i 个函数的全局最优值是100*i,所以误差计算统一写成f - 100*func_num。
返回值f是标量。如果你在算法里习惯用列向量存种群(pop是NP × D),每次调用前记得转置,或者写个包装函数一次性处理掉,别在 30 个函数里各改一遍。
3. 把环境跑通:从 addpath 到批量出结果表
骨架清楚了,接下来是实操。这一章按「路径 → 单点 → 批量 → 统计」四步走,每一步都给出能直接抄的代码,跑完之后你应该能拿到一张算法 × 30 个函数的误差矩阵。
3.1 把路径一次性铺对
MATLAB 里最常见的翻车不是算法写错,是addpath只加了顶层目录,导致input_data里的 txt 找不到。函数里load('input_data/shift_data_1.txt')用的是相对路径,相对的是「当前工作目录」,不是「函数所在目录」。
% 在工程根目录执行一次即可 root = fileparts(mfilename('fullpath')); % 若在脚本里,直接用 pwd addpath(genpath(fullfile(root, 'cec17'))); % genpath 会把 input_data 也带上 which cec17_func % 确认解析到的是哪一份genpath比addpath多做的事是递归包含所有子目录,这样不管你在哪个脚本里调用,函数文件都能被找到。which cec17_func这一步别省,尤其是在你本地同时存了好几份不同来源的测试集时,它能立刻告诉你现在生效的是哪一份——这个习惯能省掉大量「明明改了代码却没生效」的玄学时间。
如果习惯用cd切目录跑脚本,那更稳妥的做法是在函数内部拼绝对路径:用mfilename('fullpath')拿到函数自身路径,再向上找input_data。改动量不大,但从此不再依赖调用者的工作目录。
3.2 单点冒烟测试:先确认函数是「活的」
正式跑实验之前,花两分钟做一次冒烟测试:在定义域内随机取点,看 30 个函数是不是都能返回有限值。
D = 30; lb = -100; ub = 100; rng(2024); % 固定随机源,方便复现 for i = 1:30 x = lb + (ub - lb) * rand(1, D); % 定义域内随机点 try f = cec17_func(x, i); fprintf('F%-2d f = %+.6e 理论最优 = %d\n', i, f, 100*i); catch ME fprintf('F%-2d 调用失败: %s\n', i, ME.message); end end这段代码不追求数值有多好看,只验证两件事:有没有抛异常,返回值是不是isfinite。如果某个函数返回Inf,八成是 F2 那种高次幂溢出,后面 4.4 节会专门讲;如果直接报「未定义函数或变量」,回到 3.1 检查路径;如果一堆函数返回同一个常数,那基本可以确定switch分支没进去,或者func_num传成了字符串。
再做一次边界测试会更稳:把x全设成ub跑一遍,再全设成lb跑一遍。边界点是最容易暴露裁剪策略问题的地方,尤其是 F10 这类最优解贴近边界的函数。
3.3 批量实验脚本:把 30 个函数 × N 次运行跑成矩阵
真正出数据靠的是批量脚本。下面这个模板把「算法句柄 + 函数编号 + 维度 + 评价次数 + 重复次数」拆成参数,换算法只需要换一个函数句柄。
function R = run_benchmark(alg, D, maxFEs, runs) % alg : 算法句柄,签名必须是 @(fhd, D, lb, ub, maxFEs) -> [best_f, best_x] % D : 维度,取 10 / 30 / 50 / 100 % maxFEs : 评价次数预算,常见取 10000*D % runs : 独立重复次数,常见取 30 或 51 lb = -100; ub = 100; R = zeros(30, runs); % 行=函数编号,列=第几次运行 for i = 1:30 fhd = @(x) cec17_func(x, i); % 把编号固化成单参数句柄 parfor r = 1:runs % 无并行工具箱时改成 for rng(i * 1000 + r); % 每次运行的种子可复现且互不相同 [bf, ~] = alg(fhd, D, lb, ub, maxFEs); R(i, r) = bf - 100 * i; % 统一转成误差 end end end几个参数值得展开。maxFEs用10000*D是这类基准实验的常规预算,维度越高给得越多,这样不同维度之间的结果才有可比性;如果你只给固定次数,高维下所有算法都会「没收敛完」,曲线全糊在一起。runs取 30 或 51 是为了后面做统计检验,样本少于 20 的话 Wilcoxon 秩和检验的结论不太站得住。rng(i*1000 + r)这个写法保证「同一函数的不同次运行之间种子不同,不同函数之间种子也不同」,避免某些函数因为共用随机流而产生意外的相关性。误差统一减100*i,是因为后面画收敛曲线、算平均值都要用误差而不是原始函数值,原始值动辄几千,混在一起看不出差别。
如果算法内部有persistent缓存(很多实现为了省时间是这么干的),parfor下每个 worker 各有一份,结果不会互相污染;但如果你改成普通for又忘了clear缓存,第二次运行就会拿到上次的残留状态——这一点在 5.3 节还会提到。
3.4 结果落盘与统计口径
R拿到手之后,至少要落三个东西:误差矩阵原始值、每个函数的 mean/std、以及和对照组做检验的 p 值。
save(sprintf('res_%s_D%d.mat', alg_name, D), 'R'); meanErr = mean(R, 2); % 30×1,每个函数的平均误差 stdErr = std(R, 0, 2); % 30×1,标准差 % 与基线算法对比(R_base 同尺寸) p = zeros(30, 1); for i = 1:30 p(i) = ranksum(R(i, :), R_base(i, :)); % Wilcoxon 秩和检验 end统计口径上有两个细节容易被忽略。一是标准差该用样本标准差还是总体标准差,MATLAB 的std默认是样本标准差(n-1),和大多数论文一致,别手动改成n。二是显著性标记,p < 0.05标+/-/=这套三符号体系在优化领域用得多,但一定要在表注里写清+表示你的算法显著更好还是更差,方向写反了审稿人一眼就能看出来。
表格建议按 F1~F3、F4~F10、F11~F20、F21~F30 四段分开,每段末尾加一行小计(比如该段内显著更优的函数个数)。这样读者一眼就能看出你的算法是「在简单函数上强」还是「在组合函数上强」,比一张 30 行的流水表信息量大得多。
4. 核心实现拆解:移位、旋转、混合、组合四层变换
这一章进入代码内部。2017CEC 的所有函数都是「基础函数 + 变换」的组合,看懂这四层变换,你就能自己造新函数,也能在结果异常时快速定位是哪一层出的问题。
4.1 移位加旋转:一个函数值的完整生成链路
以 F1 为例,从输入向量到一个标量,中间要过三步:减移位向量、乘旋转矩阵、加偏置。
function f = bent_cigar_func(x, D, idx) % F1: Shifted and Rotated Bent Cigar persistent o1 M1 if isempty(o1) % 只读一次盘,后续复用 o1 = load(['input_data/shift_data_' num2str(idx) '.txt']); M1 = load(['input_data/rotate_data_' num2str(idx) '.txt']); end o = o1(1:D); % 只取前 D 个,文件可能更长 M = M1(1:D, 1:D); % 旋转矩阵截成 D×D y = x - o; % 1) 移位:把最优解从原点挪走 z = y * M; % 2) 旋转:制造维度间的耦合 f = z(1)^2 + 1e6 * sum(z(2:end).^2) + 100*idx; % 3) 基础函数 + 偏置 end三步各自的作用很明确。移位让最优解不在原点,防止那些「默认从原点附近开始搜」的算法白捡便宜;旋转把各维之间的耦合关系打乱,让坐标轴方向不再是有意义的方向,靠单维变异的算法会明显吃亏;偏置让最优值变成100*idx,避免多个函数的最优值都是 0 时表格里一堆 0 分不清谁是谁。
参数上要盯住两个地方。o = o1(1:D)这个截断不能省,shift_data文件是按最大维度 100 准备的,你跑 10 维却拿了 100 个元素去减,MATLAB 会直接报维度不匹配。y * M的乘法顺序也不能改,写成M * y'在数学上等价,但如果你某天把x换成了列向量却没改这一行,得到的仍然是一个合法数值——只是这个数值和官方结果对不上,属于最难查的那类 bug。
基础函数本身的形态也值得记一下。Bent Cigar 是x1^2 + 1e6*sum(x_i^2),第一维权重是 1,其余维权重是 1e6,所以它极度病态:算法必须在第一维上做精细搜索,其余维只能粗调。Zakharov 是sum(x_i^2) + (sum(0.5*i*x_i))^2 + (sum(0.5*i*x_i))^4,多了两个高阶项,梯度在不同区域差别很大。Rosenbrock 是经典的山谷函数,最优点在一条弯曲的窄谷里,路径跟随能力差的算法会在谷壁上反复横跳。
4.2 混合函数:分块、独立变换、再拼回去
混合函数的思路是「一个定义域里塞多个不同形态的子函数」。F11 通常由三个子函数按 0.3/0.3/0.4 的比例切分维度,每块用自己的移位向量和旋转矩阵,最后拼成一个整体。
function f = hybrid_func(x, D, idx, ratios, fhandles) % 通用混合函数骨架:按比例切块,每块独立变换后累加 persistent sh o M if isempty(sh) sh = load(['input_data/shuffle_data_' num2str(idx) '.txt']); o = load(['input_data/shift_data_' num2str(idx) '.txt']); M = load(['input_data/rotate_data_' num2str(idx) '.txt']); end perm = sh(1:D); % 固定的重排顺序,保证函数确定性 n = floor(ratios * D); % 例如 [0.3 0.3 0.4] × D n(end) = D - sum(n(1:end-1)); % 余数全部塞给最后一块 xs = x(perm); % 先按固定顺序重排维度 f = 0; start = 1; for i = 1:numel(fhandles) seg = xs(start : start + n(i) - 1); seg = seg - o(start : start + n(i) - 1); % 该块专属的移位 seg = seg * M(1:n(i), 1:n(i)); % 该块专属的旋转 f = f + fhandles{i}(seg); % 累加各块贡献 start = start + n(i); end f = f + 100 * idx; % 全局偏置 end这段代码里最关键的一行是perm = sh(1:D)。打乱顺序必须来自文件、必须是固定的,绝不能写成randperm(D)。原因很直接:如果每次调用都重新打乱,函数本身就带随机性,同一个算法跑两次结果不同,你根本分不清是算法的随机性还是测试集的随机性。这也是shuffle_data文件存在的全部意义。
n(end) = D - sum(n(1:end-1))这行也不能省。按比例算出来的块长度取整之后,各块加起来往往不等于 D,差的这几个维度如果不补到最后一块,seg的总长度会小于 D,最后几个维度相当于被静默丢弃了——函数照样返回数值,只是那个数值和官方定义的不是同一个函数。
在算法层面,混合函数的难点在于「不同区域的性格不一样」。有的块是单峰,适合快速下降;有的块是多峰,需要保持多样性。如果你的算法全局只用一个步长策略,通常会在某一块上表现极好、在另一块上完全停滞,最终误差被最差的那块拖住。
4.3 组合函数:距离权重决定谁说了算
组合函数比混合函数更「狡猾」:它不是一个定义域切几块,而是让多个完整函数同时存在,靠距离权重决定当前点听谁的。
function f = cf_generic(x, D, idx, funcs, sigma, lambda, bias) % 组合函数骨架:K 个子函数按距离加权 persistent o M if isempty(o) o = load(['input_data/shift_data_' num2str(idx) '.txt']); % K*D×1 M = load(['input_data/rotate_data_' num2str(idx) '.txt']); % K*D×D end K = numel(funcs); w = zeros(1, K); val = zeros(1, K); for i = 1:K oi = o((i-1)*D + (1:D)); % 第 i 个子函数的移位向量 Mi = M((i-1)*D + (1:D), 1:D); % 第 i 个子函数的旋转矩阵 y = (x - oi) / lambda(i) * Mi; % 按 lambda 伸缩后再旋转 d2 = sum((x - oi).^2); % 到第 i 个子函数中心的距离平方 w(i) = exp(-d2 / (2*D*sigma(i)^2)) / (d2^(1/8) + eps); % 距离权重 val(i) = funcs{i}(y); end w = w / sum(w); % 归一化,保证权重和为 1 f = sum(w .* val) + bias; % 加权求和再加全局偏置 end参数的作用可以这样理解。sigma(i)控制第 i 个子函数的「势力范围」,值越大,它在更远的地方仍有话语权;lambda(i)控制定义域的伸缩,把子函数原来定义在[-5,5]上的形态拉伸到[-100,100]这个统一搜索空间里。d2^(1/8)这一项是让距离越近权重越大的关键,指数取 1/8 而不是 1,是因为 8 次方根能显著放缓衰减速度,避免出现「离得稍微远一点权重就变成 0」的硬切换。
w = w / sum(w)归一化不能漏。少了这行,函数值会随着距离整体缩放,最优点附近的值可能小于偏置,误差算出负数,看起来像是你的算法「超越了理论最优」——这种情况百分之百是实现问题,不是算法突破了。
组合函数的难点在于最优解不唯一。它通常有多个局部最优分别落在不同子函数附近,全局最优只是其中「偏置最小的那个」。算法如果只盯着一个盆地挖,很容易挖到另一个子函数的局部最优就停住。测试这类函数时,观察种群在多峰之间的迁移行为比看最终误差更有信息量。
4.4 F2 的溢出问题:为什么很多人直接把它剔除
F2 是 Sum of Different Power,形如sum(|x_i|^(i+1))。这个定义在高维下必然溢出。
function f = sum_diff_pow_func(x, D, idx) % F2: 高次幂累加,D 较大时极易溢出 f = 0; for i = 1:D f = f + abs(x(i))^(i+1); % 第 i 维的指数是 i+1 end f = f + 100 * idx; end问题出在指数上。第 100 维的指数是 101,只要|x(100)| = 100,这一项就是100^101 ≈ 1e202,再加上前面 99 项,超出double的表示范围就是Inf;如果中间出现Inf - Inf或者0 * Inf,就变成NaN。而NaN一旦进入种群,后续所有比较都会失效,算法会静默地「卡住不动」。
处理方式一般有三种,选哪种取决于你打算怎么写实验说明。一是直接把 F2 从统计里剔除,并在论文或报告里注明原因——官方技术报告本身也提到过这个函数的数值问题,剔除是被接受的做法。二是在算法层做保护:把x裁剪到[-10,10]再传给 F2,代价是它和其余 29 个函数的定义域不一致,跨函数比较时要单独说明。三是在函数内部检测~isfinite(f)并按一个极大的惩罚值返回,好处是实验流程不中断,坏处是惩罚值的选择会影响收敛曲线形状。
注意:不管选哪种,都别默默改掉却不在实验记录里写。别人拿你的代码复现不出同样的数,第一个怀疑的就是这里。
另外提醒一句,F2 的溢出在小维度下不明显。D=10 时最大指数是 11,100^11 = 1e22,完全在范围内,所以很多人先在 10 维上跑通了,换到 100 维才发现满屏Inf,还以为是自己的算法发散。建议一开始就用 D=100 做一次冒烟测试。
5. 避坑与排查:维度、边界、随机种子这些翻车点
这套代码本身的逻辑并不复杂,问题几乎都出在「用」的环节。下面五条是我在实际跑实验时反复遇到的,按现象、原因、解决三段写,遇到对得上的直接照着排查。
5.1 现象:所有函数都报「未定义函数或变量」,或者返回值全是同一个数
原因是路径没铺全,或者switch分支没进去。cec17_func.m里用load('input_data/shift_data_1.txt')这种相对路径,如果你的工作目录不在cec17的父目录下,load直接失败;如果外层套了try-catch,异常被吞掉,函数可能返回一个未初始化的f,看起来就是「所有函数值一样」。
解决分两步:先addpath(genpath(...))把子目录全带上,再which cec17_func确认解析路径唯一。然后在冒烟测试里把try-catch的ME.message打出来,别只打「调用失败」。相对路径改成mfilename('fullpath')拼绝对路径是更彻底的做法。
5.2 现象:D=10 时一切正常,D=100 时误差爆表甚至出现 NaN
大概率是x传成了列向量,或者移位向量没按维度截断。y = x - o在x是1×D、o是1×100时会触发广播,得到一个D×100的矩阵,后续sum出来的值完全不对;如果x是D×1,x * M直接维度报错。
解决:在入口函数第一行统一写x = x(:)';,所有内部函数都用行向量。移位向量统一取o(1:D),旋转矩阵统一取M(1:D,1:D)。改完之后用 D=10 和 D=100 各跑一次同一函数,看返回值是否都在合理量级,两次差异过大就说明还有地方没对齐。
5.3 现象:rng固定了,同一个算法跑两次结果还是不一样
原因通常有两个。一是混合或组合函数内部若用了randperm之类没被固定的随机源;二是函数里用了persistent缓存,脚本之间共享工作区时缓存没清,第二次跑拿到的是上次的状态。
解决:先在脚本开头加clear functions清掉所有持久变量,或者把你的算法和测试集都包成函数而不是脚本,让工作区自然隔离。然后dbstop if naninf挂个断点,跑一次看NaN出现在哪个函数里。如果确认是测试集本身带了随机性,那就说明你拿到的版本被别人改过,回去核对shuffle_data是不是被替换成了随机生成。
5.4 现象:算法在某个函数上完全不动,误差曲线是一条水平线
多半是边界裁剪策略和函数形态打架。搜索范围是[-100,100]^D,但 F10 这类函数的最优解靠近边界,如果你的算法把越界个体直接裁到边界上,种群会在边界堆积,失去多样性。反过来,如果用的是「越界就重新随机初始化」,在边界附近反而会不断把好解扔掉。
解决:把裁剪策略写成可切换的参数(裁剪 / 反射 / 随机重置 / 惩罚),在 F10 上分别跑一次看差异。这个差异往往比你调算法参数带来的提升还大,所以实验记录里必须写明用的是哪种,否则别人复现出来的曲线会完全不同。
5.5 现象:算出来的误差出现负值
这说明你用的偏置和官方约定不一致。理论最优值是100*i,误差f - 100*i应该是非负的(数值误差级别的小负数可以接受,比如-1e-12)。如果出现-300这种量级,基本可以确定是版本搞混了,或者是从别的年份的代码改了个名字。
解决:在 3.2 的冒烟测试里把100*i一起打出来对照。更稳的办法是找一组「接近最优」的解代进去看f是否略大于100*i。如果整体偏移了一个常数,那就是偏置项写错了;如果不是常数偏移而是随机波动,那就是某个变换层没生效。
6. 二次开发:把自研优化器接进标准实验流程
接入这件事,我建议只改一个地方:给算法定一个统一签名,测试集那边一律不动。签名写成[best_f, best_x, curve] = my_alg(fhd, D, lb, ub, maxFEs),fhd是@(x) cec17_func(x, i)这样的单参数句柄。这样跑 30 个函数只是一个for循环,换算法只换句柄,实验脚本一行不改。
% 算法侧的统一签名,curve 记录每次评价后的最优值,方便后期画收敛图 function [best_f, best_x, curve] = my_alg(fhd, D, lb, ub, maxFEs) curve = zeros(1, maxFEs); % ... 初始化、迭代 ... for t = 1:maxFEs % 每次评价后更新 best_f / best_x curve(t) = best_f; end end想加自己的函数,有两条路。一条是改官方入口:在cec17_func的switch里加case 31,同时准备input_data/shift_data_31.txt和rotate_data_31.txt,偏置按100*31给。另一条是不动官方代码,单独写一个my_func.m做包装,实验脚本里用@(x) my_func(x, i)调用。后者更适合需要和别人对比的场景——官方文件保持原样,别人拿到你的包能直接验证。
| 接入方式 | 改动范围 | 可复现性 | 适用场景 |
|---|---|---|---|
改cec17_func加case | 动官方入口 | 需要连数据文件一起发 | 自己长期用,函数集固定 |
独立包装my_func.m | 只新增文件 | 官方文件可原样核对 | 要发论文、要给别人复现 |
直接替换整个input_data | 动数据层 | 最难核对 | 造私有基准,不对外 |
验证改造是否成功,有个笨但有效的办法:拿 F1 做标定。F1 是单峰,一个正常的算法在10000*D次评价内应该能把误差压到很低;如果连 F1 都压不下去,别急着怀疑测试集,先检查算法的评价次数是不是真的花在了fhd上——常见的错误是在循环里算了fhd但没计入maxFEs,导致预算虚高。再拿 F10 做一次,看误差曲线是不是在边界附近反复震荡,这能侧面验证你的边界策略有没有生效。
还有一个容易忽略的技巧:把curve按log10(error + eps)存,而不是存原始误差。30 个函数的最优值从 100 到 3000,原始误差量级跨度太大,画在一张图上根本看不出形状;取对数之后,收敛速度的差异一目了然,也方便直接算 AUC 这类指标。
从那以后我每次接入新的测试集,都会先跑一遍原点附近的冒烟测试,把 30 个函数的返回值连同理论最优值一起打印出来看一眼,确认没有Inf、没有NaN、没有一群一模一样的数,再动算法代码。这个习惯帮我省掉过好几次「调了两天参数,最后发现是测试集没接对」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取