
简介面向进化计算与优化算法研究者这份材料围绕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 是组合函数靠距离权重让多个子函数「轮流当家」。这个分组直接决定了你实验表格该怎么分块展示。编号函数名类型主要考察点F1Shifted and Rotated Bent Cigar单峰沿单一方向的收敛效率F2Shifted and Rotated Sum of Different Power单峰高次幂数值极易溢出F3Shifted and Rotated Zakharov单峰病态条件下的稳定性F4Shifted and Rotated Rosenbrock多峰狭长山谷里的路径跟随F5Shifted and Rotated Rastrigin多峰大量规则局部最优F6Shifted and Rotated Expanded Schaffer多峰强非线性耦合F7Shifted and Rotated Lunacek Bi-Rastrigin多峰双峰盆地的欺骗性F8Shifted and Rotated Non-Continuous Rastrigin多峰不连续造成的搜索断层F9Shifted and Rotated Levy多峰平坦区与陡峭区混合F10Shifted and Rotated Schwefel多峰最优解靠近定义域边界F11~F20Hybrid Function 1~10混合分块异构局部搜索策略切换F21~F30Composition 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|^(i1))。这个定义在高维下必然溢出。function f sum_diff_pow_func(x, D, idx) % F2: 高次幂累加D 较大时极易溢出 f 0; for i 1:D f f abs(x(i))^(i1); % 第 i 维的指数是 i1 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 的溢出在小维度下不明显。D10 时最大指数是 11100^11 1e22完全在范围内所以很多人先在 10 维上跑通了换到 100 维才发现满屏Inf还以为是自己的算法发散。建议一开始就用 D100 做一次冒烟测试。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 现象D10 时一切正常D100 时误差爆表甚至出现 NaN大概率是x传成了列向量或者移位向量没按维度截断。y x - o在x是1×D、o是1×100时会触发广播得到一个D×100的矩阵后续sum出来的值完全不对如果x是D×1x * M直接维度报错。解决在入口函数第一行统一写x x(:);所有内部函数都用行向量。移位向量统一取o(1:D)旋转矩阵统一取M(1:D,1:D)。改完之后用 D10 和 D100 各跑一次同一函数看返回值是否都在合理量级两次差异过大就说明还有地方没对齐。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、没有一群一模一样的数再动算法代码。这个习惯帮我省掉过好几次「调了两天参数最后发现是测试集没接对」的后悔药。希望帮到你。本文还有配套的精品资源点击获取