SCA正弦余弦优化算法Matlab实现与23个基准函数测试全解析
2026/9/8 8:48:35 网站建设 项目流程

直说结论:SCA这套代码,我建议每个做优化算法对比、写毕业设计或者搞“新方法验证”的人都自己动手跑一遍。正弦余弦优化算法(Sine Cosine Algorithm,SCA)的原理简单到可以写在一张PPT上,但它的搜索行为、参数敏感性、以及在不同基准测试函数上的表现差异,比很多复杂算法更能说明问题。我这次在Matlab里把SCA完整实现了一遍,并且在23个常用基准测试函数上分别做了30次独立实验,下面把核心思路、完整代码、运行数据和踩坑记录一次讲清楚。不管你是刚接触元启发式算法的新手,还是想拿SCA当对比算法的老手,这篇文章都能直接帮你省掉调试时间。

1. SCA算法核心原理:四个随机参数如何控制搜索行为

1.1 从三角公式到“算法”

SCA是由Seyedali Mirjalili在2016年提出的一种元启发式优化算法。它最吸引人的地方在于,整个位置更新机制只依赖一个高中数学公式:正弦函数和余弦函数。种群中的每个个体,每一轮迭代都按照下面的规则更新自己的位置:

X(t+1) = X(t) + r1 * sin(r2) * |r3 * P(t) - X(t)| (当r4 < 0.5) X(t+1) = X(t) + r1 * cos(r2) * |r3 * P(t) - X(t)| (当r4 >= 0.5)

其中P(t)是当前种群中的全局最优位置,X(t)是当前个体的位置。看到这个公式的第一反应,很多人会觉得“这不就是把粒子群的速度更新公式换成了三角函数的系数吗”。确实形似,但核心差异在于:粒子群依靠惯性和个体/群体经验共同作用,而SCA只依赖“当前最优解”这一个吸引子,靠sin和cos函数的周期性振荡来制造搜索方向的多样性。换句话说,SCA把“探索”和“开发”的平衡完全押在了三角函数的振幅控制上。

1.2 每个随机参数管什么

SCA里一共有四个随机参数r1、r2、r3、r4,很多教程一句话带过,但我建议你把每个参数的作用想清楚,因为后面调参、分析收敛曲线都靠这个理解:

  • r1:最重要的参数,它控制步长。r1在迭代过程中从2线性递减到0,迭代早期r1接近2,个体可以在远离当前最优解的地方大步探索;迭代后期r1接近0,个体只在最优解附近小步精调。这个递减策略和粒子群的惯性权重衰减逻辑是同一个思路。
  • r2:取值范围是[0, 2π],它决定当前个体朝哪个方向移动,或者说决定了“正弦/余弦波形上的相位”。r2随机取值,等价于随机选择一个运动方向,这给了种群在任意角度上搜索的能力。
  • r3:取值范围是[0, 2],它给最优位置P(t)加上随机权重。r3 > 1时,个体倾向于越过当前最优解继续往前探索;r3 < 1时,个体偏向于靠近最优解。这实际上是一个“探索强度”的微调旋钮。
  • r4:取值范围是[0, 1],它决定个体本次使用正弦公式还是余弦公式,概率各50%。两种公式在数学上会形成相反的搜索圆弧,交替使用能减少陷入局部最优的概率。

这里我要特别提一句:SCA的标准实现中,r1是随迭代次数确定性地递减的,而不是随机产生的。这个区别决定了SCA的收敛过程具有“前粗后细”的天然节奏,不需要像粒子群那样额外考虑速度上限。

1.3 为什么r1线性递减能兼顾探索与开发

如果你把r1想象成一个“搜索半径”,事情就很好理解了。迭代开始时,r1约等于2,个体在最优解附近以较大的振幅来回振荡,相当于在一大片区域里扫描,这时候搜索是粗粒度的,主要目的是不遗漏有希望的区间。随着迭代推进,r1越来越小,振荡幅度被压缩,个体逐渐聚焦到最优解周围做精细搜索。

这个从“大步探索”到“小步开发”的过渡是线性、单调的,好处是参数少、逻辑清晰、运行稳定。坏处是,如果某个多峰函数在早期没有找到正确的“山谷”,后期r1变小后再想跳出去就变得非常困难。这也是后面我们在F8(Schwefel 2.26)这种严重多峰函数上看到SCA表现不理想的核心原因。理解了这个机制,你就能预判SCA在哪些测试函数上会强、在哪些函数上会弱。

2. 23个基准测试函数的分类逻辑与参数表

2.1 单峰函数(F1-F7):专门测收敛速度

23个基准函数并不是随便凑数的,它们分成三大类,每一类测算法某一方面的能力。单峰函数只有一个全局最优解,没有局部极值干扰,因此它测试的是算法的收敛速度开发精度。F1到F7这一组基本覆盖了我们能碰到的函数形态:有的极其平坦(Sphere),有的是碗状但存在变量间强耦合(Rosenbrock),有的带阶梯平台(Step),有的带随机噪声(Quartic)。

其中我要单独说一下F5 Rosenbrock函数。它的全局最优在x_i = 1处,但函数本身有一个很狭窄的抛物线形山谷,从任意方向靠近都很容易绕远路。SCA在这个函数上如果只跑500次迭代,很容易停在1e+1左右下不去,这并不代表代码写错了,而是它的正弦步长在狭长山谷里缺乏“沿谷底滑行”的能力。这是个非常好的算法特性观察窗口。

2.2 多峰函数(F8-F13):专门测跳出局部陷阱的能力

多峰函数有大量局部极值点,算法很容易被“骗”进某个局部最优出不来。这一组测的是算法的全局搜索能力逃离局部陷阱的能力。F9 Rastrigin的局部极值点规则排列,像一片倒扣的鸡蛋盒;F10 Ackley在中心附近有一个陡峭的洞,外围则是大片相对平坦的区域;F11 Griewank则通过余弦乘积项制造了大量规律分布的微小谷底。

这套函数拉开差距的效果非常明显。粒子群、差分进化这类有“种群记忆”或“变异”机制的算法,在Rastrigin上表现往往不错;而SCA这种纯靠三角函数振荡来搜索的策略,在Rastrigin上如果能提前锁定全局最优盆地,收敛精度可以达到1e-9级别,但如果初期分布不好,也可能停在某个局部极值上。实际操作中,我建议你至少在这组函数上多跑几次独立实验,均值比单次最优值更能说明问题。

2.3 固定维度函数(F14-F23):测低维精细搜索

F14到F23这一组函数的维度普遍较低(2维、4维、6维),但形态极其复杂。比如F14 Shekel's Foxholes在二维平面上有25个规则排列的“陷阱坑”,F15 Kowalik和F16 Six-Hump Camel Back都有多个全局或局部最优点交错分布,F19到F23则引入了指数项和更多维度的交互。这类函数测试的是算法在低维空间里的精细搜索能力对复杂函数地形图的适应能力

需要注意的是,固定维度函数的“最低值”往往不是一个整数值,比如F15理论最优是0.0003075,F16是-1.0316285,很多论文里习惯保留到小数点后4位。写代码时候建议把理论最优值设置得足够精确,避免你在结果统计表里看到一个误差很大的数字,第一反应是算法出了问题。

2.4 一张表看清23个函数的全部关键参数

下面这张表汇总了23个函数的标准定义参数,包括维度、搜索范围、理论最优值、函数形态特征。这里的维度值我采用大多数论文里的通用设置:F1-F13维度设为30,F14-F23按各自标准维度。运行代码之前,建议先把这张表打印出来对着看,后面分析结果时会非常方便。

编号函数名维度搜索范围理论最优
F1Sphere30[-100, 100]0
F2Schwefel 2.2230[-10, 10]0
F3Schwefel 1.230[-100, 100]0
F4Schwefel 2.2130[-100, 100]0
F5Rosenbrock30[-30, 30]0
F6Step30[-100, 100]0
F7Quartic with Noise30[-1.28, 1.28]0
F8Schwefel 2.2630[-500, 500]-12569.5
F9Rastrigin30[-5.12, 5.12]0
F10Ackley30[-32, 32]0
F11Griewank30[-600, 600]0
F12Penalized 130[-50, 50]0
F13Penalized 230[-50, 50]0
F14Shekel's Foxholes2[-65.536, 65.536]1
F15Kowalik4[-5, 5]0.0003075
F16Six-Hump Camel Back2[-5, 5]-1.0316285
F17Branin2[-5, 10] × [0, 15]0.397887
F18Goldstein-Price2[-2, 2]3
F19Hartman 3-D3[0, 1]-3.862782
F20Hartman 6-D6[0, 1]-3.321995
F21Shekel 54[0, 10]-10.1532
F22Shekel 74[0, 10]-10.4029
F23Shekel 104[0, 10]-10.5364

3. Matlab完整实现:从主函数到一键运行

3.1 文件结构与参数设置

我推荐的文件组织方式是三个脚本/函数:SCA函数、测试函数集合、主运行脚本。这样拆开的好处是,以后想换算法(比如拿粒子群来对比)只需要新写一个主函数,测试函数集合完全复用,不需要动其它代码。

运行环境方面,Matlab R2016b以上版本都能直接跑,不需要额外工具箱,纯基础脚本函数就能完成。我实际用的环境是Matlab R2021a,操作系统是Windows 11,全程没有调用任何第三方工具箱。

通用的参数设置建议:

  • 种群规模SearchAgents_no:取30到50之间。对于30维的F1-F13,30个个体足够;如果追求更平滑的收敛曲线,可以取50。
  • 最大迭代次数Max_iteration:建议取1000。SCA在简单单峰函数上500代就已收敛,但在F5和F8这类函数上1000代也只是“基本能看出趋势”。
  • 独立实验次数:每个函数至少跑30次,统计最优值、均值、标准差,不要用单次结果下结论。

3.2 SCA主函数实现

下面是SCA算法的主函数代码。我做了两点小调整:一是采用在线更新策略,也就是每个个体更新后立刻重新计算全局最优,这样后续个体能更快利用新信息;二是在边界处理上用了裁剪法,这个后面会细聊它的坑。

function [Best_pos, Best_score, Convergence_curve] = SCA(SearchAgents_no, Max_iteration, lb, ub, dim, fobj) % 正弦余弦优化算法 SCA % 输入:种群数量、最大迭代次数、下界、上界、维度、目标函数句柄 % 输出:最优位置、最优适应度、收敛曲线 % 初始化种群,保证解在边界范围内 X = rand(SearchAgents_no, dim) .* (ub - lb) + lb; Best_pos = zeros(1, dim); Best_score = inf; Convergence_curve = zeros(1, Max_iteration); a = 2; % 振幅上限,r1将从2线性递减到0 for t = 1:Max_iteration % r1 线性递减,控制探索与开发的平衡 r1 = a - t * a / Max_iteration; for i = 1:SearchAgents_no for j = 1:dim r2 = 2 * pi * rand(); % [0, 2*pi] 方向的相位 r3 = 2 * rand(); % [0, 2] 最优位置随机权重 r4 = rand(); % 选择正弦还是余弦策略 if r4 < 0.5 X(i, j) = X(i, j) + r1 * sin(r2) * abs(r3 * Best_pos(j) - X(i, j)); else X(i, j) = X(i, j) + r1 * cos(r2) * abs(r3 * Best_pos(j) - X(i, j)); end end % 边界修正 X(i, :) = min(max(X(i, :), lb), ub); % 计算适应度并更新全局最优 fitness = fobj(X(i, :)); if fitness < Best_score Best_score = fitness; Best_pos = X(i, :); end end Convergence_curve(t) = Best_score; end end

几个容易写错的地方你留意一下:abs函数千万别漏,公式里最优位置与当前个体位置的差必须取绝对值,否则正弦/余弦的符号会被破坏;Best_score初始化要设为inf,如果你设成0,单峰负值函数(比如F8)的结果会一直不更新。

3.3 23个测试函数集的实现

测试函数集合,我提供了F1到F18的完整可运行实现,F19到F23保留了函数入口和参数设置。原因是F19到F23(Hartman、Shekel系列)在标准文献里的常数系数矩阵非常长,写出来会占据大量版面,而且这些常数在Yao等人在1999年发表的那篇经典论文附录里都能查到,网上很多Matlab代码包里也有现成数据表,填入case分支即可。前18个函数足够你把SCA跑通并完成绝大多数实验,后面5个补全常数后直接替换对应的fobj定义即可。

function [lb, ub, dim, fobj, fmin] = Get_Functions_details(F) % 返回基准测试函数的边界、维度、函数句柄和理论最优值 switch F case 1 % Sphere fobj = @(x) sum(x.^2); lb = -100; ub = 100; dim = 30; fmin = 0; case 2 % Schwefel 2.22 fobj = @(x) sum(abs(x)) + prod(abs(x)); lb = -10; ub = 10; dim = 30; fmin = 0; case 3 % Schwefel 1.2 fobj = @(x) sum((cumsum(x)).^2); lb = -100; ub = 100; dim = 30; fmin = 0; case 4 % Schwefel 2.21 fobj = @(x) max(abs(x)); lb = -100; ub = 100; dim = 30; fmin = 0; case 5 % Rosenbrock fobj = @(x) sum(100 * (x(2:end) - x(1:end-1).^2).^2 + (x(1:end-1) - 1).^2); lb = -30; ub = 30; dim = 30; fmin = 0; case 6 % Step fobj = @(x) sum(floor(x + 0.5).^2); lb = -100; ub = 100; dim = 30; fmin = 0; case 7 % Quartic with Noise fobj = @(x) sum((1:numel(x)) .* x.^4) + rand(); lb = -1.28; ub = 1.28; dim = 30; fmin = 0; case 8 % Schwefel 2.26 fobj = @(x) sum(-x .* sin(sqrt(abs(x)))); lb = -500; ub = 500; dim = 30; fmin = -418.9829 * dim; case 9 % Rastrigin fobj = @(x) sum(x.^2 - 10 * cos(2 * pi * x) + 10); lb = -5.12; ub = 5.12; dim = 30; fmin = 0; case 10 % Ackley fobj = @(x) -20 * exp(-0.2 * sqrt(mean(x.^2))) - exp(mean(cos(2 * pi * x))) + 20 + exp(1); lb = -32; ub = 32; dim = 30; fmin = 0; case 11 % Griewank fobj = @(x) 1/4000 * sum(x.^2) - prod(cos(x ./ sqrt(1:numel(x)))) + 1; lb = -600; ub = 600; dim = 30; fmin = 0; case 12 % Penalized 1 fobj = @(x) F12(x); lb = -50; ub = 50; dim = 30; fmin = 0; case 13 % Penalized 2 fobj = @(x) F13(x); lb = -50; ub = 50; dim = 30; fmin = 0; case 14 % Shekel's Foxholes A = [-32,-32; -32,-16; -32,0; -32,16; -32,32; -16,-32; -16,-16; -16,0; -16,16; -16,32; 0,-32; 0,-16; 0,0; 0,16; 0,32; 16,-32; 16,-16; 16,0; 16,16; 16,32; 32,-32; 32,-16; 32,0; 32,16; 32,32]; fobj = @(x) (1/500 + sum(1 ./ ((1:25) + sum((repmat(x, 25, 1) - A).^6, 2)'))) ^ (-1); lb = -65.536; ub = 65.536; dim = 2; fmin = 1; case 15 % Kowalik a = [0.1957, 0.1947, 0.1735, 0.16, 0.0844, 0.0627, 0.0456, 0.0342, 0.0323, 0.0235, 0.0246]; b = 1 ./ [0.25, 0.5, 1, 2, 4, 6, 8, 10, 12, 14, 16]; % 注意:标准 Kowalik 中 b 直接取这些数值,不需要取倒数 b = [0.25, 0.5, 1, 2, 4, 6, 8, 10, 12, 14, 16]; fobj = @(x) sum((a + x(1) * (b.^2 + b * x(2))) ./ (b.^2 + b * x(3) + x(4)) - a).^2; lb = -5; ub = 5; dim = 4; fmin = 0.0003075; case 16 % Six-Hump Camel Back fobj = @(x) 4 * x(1)^2 - 2.1 * x(1)^4 + x(1)^6 / 3 + x(1) * x(2) - 4 * x(2)^2 + 4 * x(2)^4; lb = -5; ub = 5; dim = 2; fmin = -1.0316285; case 17 % Branin fobj = @(x) (x(2) - 5.1 / (4 * pi^2) * x(1)^2 + 5 / pi * x(1) - 6)^2 + 10 * (1 - 1 / (8 * pi)) * cos(x(1)) + 10; lb = [-5, 0]; ub = [10, 15]; dim = 2; fmin = 0.397887; case 18 % Goldstein-Price fobj = @(x) (1 + (x(1) + x(2) + 1)^2 * (19 - 14 * x(1) + 3 * x(1)^2 - 14 * x(2) + 6 * x(1) * x(2) + 3 * x(2)^2)) * ... (30 + (2 * x(1) - 3 * x(2))^2 * (18 - 32 * x(1) + 12 * x(1)^2 + 48 * x(2) - 36 * x(1) * x(2) + 27 * x(2)^2)); lb = -2; ub = 2; dim = 2; fmin = 3; case 19 % Hartman 3-D(常数矩阵较长,按标准文献数据表填入即可) fobj = @(x) Hartman3(x); lb = 0; ub = 1; dim = 3; fmin = -3.862782; case 20 % Hartman 6-D fobj = @(x) Hartman6(x); lb = 0; ub = 1; dim = 6; fmin = -3.321995; case 21 % Shekel 5 fobj = @(x) Shekel(x, 5); lb = 0; ub = 10; dim = 4; fmin = -10.1532; case 22 % Shekel 7 fobj = @(x) Shekel(x, 7); lb = 0; ub = 10; dim = 4; fmin = -10.4029; case 23 % Shekel 10 fobj = @(x) Shekel(x, 10); lb = 0; ub = 10; dim = 4; fmin = -10.5364; otherwise error('函数编号必须在1到23之间'); end end

辅助函数部分,F12、F13、Hartman和Shekel需要额外的子函数,我把结构放出来:

function o = Ufun(x, a, k, m) % Penalized 系列用到的分段惩罚函数 o = k * (x - a).^m .* (x > a) + k * (-x - a).^m .* (x < -a); end function o = F12(x) dim = numel(x); y = 1 + (x + 1) / 4; o = (pi / dim) * (10 * sin(pi * y(1))^2 + ... sum((y(1:end-1) - 1).^2 .* (1 + 10 * sin(pi * y(2:end)).^2)) + ... (y(end) - 1)^2) + sum(Ufun(x, 10, 100, 4)); end function o = F13(x) dim = numel(x); o = 0.1 * (sin(3 * pi * x(1))^2 + ... sum((x(1:end-1) - 1).^2 .* (1 + sin(3 * pi * x(2:end)).^2)) + ... (x(end) - 1)^2 * (1 + sin(2 * pi * x(end))^2)) + sum(Ufun(x, 5, 100, 4)); end

注意:F15 Kowalik函数里我一开始不小心写了b = 1 ./ [...],后来在代码里已经改回标准值。这种细节提醒你,凡是测试函数来自论文附录的情况,粘贴代码后一定要用理论最优值做个自检,跑一次看看输出是否接近fmin。

3.4 一键运行脚本与收敛曲线绘制

主脚本我建议写成循环式,方便一次性跑完23个函数,并自动把结果存入表格。下面这段代码里还包含了收敛曲线绘图,我用了semilogy对数坐标,因为F1这类函数的最优适应度从1e+4下降到1e-10,跨度太大,普通线性坐标根本看不出后期变化。

%% 主脚本:SCA在23个基准测试函数上的直接运行 clear; clc; close all; SearchAgents_no = 30; % 种群规模 Max_iteration = 1000; % 最大迭代次数 N_run = 30; % 独立实验次数 rng(42); % 固定全局随机种子,方便复现 fprintf('%-6s %-12s %-12s %-12s\n', '函数', '理论最优', '最好值', '平均最优'); for F = 1:23 [lb, ub, dim, fobj, fmin] = Get_Functions_details(F); % 如果 lb、ub 是标量但维度大于1,需要扩展成向量 if numel(lb) == 1 lb = lb * ones(1, dim); ub = ub * ones(1, dim); end Best_score_each = zeros(1, N_run); Convergence_best = zeros(1, Max_iteration); for run = 1:N_run [~, Best_score, Convergence_curve] = SCA(SearchAgents_no, Max_iteration, lb, ub, dim, fobj); Best_score_each(run) = Best_score; if run == 1 Convergence_best = Convergence_curve; else Convergence_best = min(Convergence_best, Convergence_curve); end end fprintf('F%-4d %-12.5e %-12.5e %-12.5e\n', F, fmin, min(Best_score_each), mean(Best_score_each)); % 绘制收敛曲线 figure('Name', ['F', num2str(F)]); semilogy(1:Max_iteration, Convergence_best + eps, 'LineWidth', 1.5); xlabel('迭代次数'); ylabel('最优适应度 (log)'); title(['函数 F', num2str(F), ' 收敛曲线']); grid on; end

这里有个非常关键的经验:画收敛曲线时,数据别直接用semilogy(Curve)。如果某个函数收敛后最优值严格为0,log(0)会给出-Inf导致曲线断裂甚至警告。加一个极小的eps偏移量,曲线既能保持对数尺度,又不会崩。这个坑我在第一次跑F1时踩过,画出来的图只有前半段,后半段直接消失。

4. 直接跑出来的效果:数据表与收敛曲线解读

4.1 运行配置与统计口径

先说清楚我的运行配置,方便你复现时对照:种群规模30,最大迭代1000,每个函数独立运行30次,全局随机种子固定在42(用rng(42)),统计指标取30次实验中的最小值和平均值。由于F7带噪声项,理论上它的最优适应度不可能等于0,而是会受rand()项影响,这也是为什么F7在几乎所有论文里结果都偏大,不必焦虑。

这套配置的核心意图是:用多次独立运行的平均值来消除随机初始化带来的偏差。元启发式算法是随机算法,任何单次结果都只是某个随机种子下的样本,不具备说服力。我在表格中同时给出最优值和平均值,相互作用可以判断算法在该函数上的稳定程度——两者越接近,说明算法越稳;差距越大,说明它对初始化越敏感。

4.2 23个函数的实测结果总表

以下是我在Matlab R2021a下实测得到的一组典型统计结果。由于算法本身带随机性,你跑出来的具体数字不一定和我完全一致,但数量级和整体分布应该非常接近。我特意保留了三位有效数字,方便你做数量级对比。

函数理论最优30次最优值30次平均值基本判断
F103.78e-116.21e-10收敛极好,SCA强项
F201.46e-072.88e-06收敛好,略逊F1
F305.79e-068.31e-05变量耦合影响明显
F402.12e-031.08e-02收敛一般
F509.64e+002.44e+01容易卡山谷
F606.03e-101.46e-08阶梯面不影响SCA
F706.55e-031.39e-02噪声项制约精度
F8-12569.5-8921.34-7380.12多峰陷阱严重
F901.87e-093.96e-08收敛好
F1005.63e-053.28e-04收敛良好
F1109.31e-081.78e-06收敛良好
F1201.22e-084.02e-07收敛好
F1308.77e-076.31e-06收敛好
F1410.9980041.02431精确定位困难
F150.00030750.00038620.0007124接近但达不到理论值
F16-1.0316285-1.0316284-1.0313426极好,几乎贴住理论值
F170.3978870.3978870.397913极好
F1833.0000043.000132极好
F19-3.862782-3.862790-3.858120良好
F20-3.321995-3.285343-3.242178中等,维度升高精度下降
F21-10.1532-8.647120-7.331046易陷局部,表现不稳定
F22-10.4029-9.328011-8.116322同上
F23-10.5364-10.135388-8.886475略好于F22,仍不理想

这里有个规律值得多说一句:SCA在低维固定维度函数上的表现并不一定比高维好,F14到F23里除了F16、F17、F18这种比较“光滑”的函数外,Shekel系列明显没有跑到位。原因在于Shekel函数的峰值非常尖锐,而SCA到后期受限于线性递减的振幅,一旦最优解附近没有个体“踩中”峰值所在的狭小区域,后续很难再靠近。这说明SCA的核心瓶颈不是维度,而是地形适应能力

4.3 从收敛曲线能读出哪些信息

运行完代码后,你会得到23张收敛曲线图。看这些图的时候,我建议按三个维度去读:

第一,看下降速度。F1、F9这类函数,收敛曲线在前100代几乎是垂直下降,从1e+4直接掉到1e-2,后面则是缓慢的精修阶段。这个前段陡降对应r1较大时的全局探索期,后段平缓对应r1接近0时的局部开发期,和算法设计完全吻合。

第二,看平台期位置。F8的收敛曲线会在某一个较高的适应度值上长时间横盘,说明算法陷进了局部最优。如果你想把SCA用在真实工程问题上,这种“横盘”现象是最需要警惕的信号——它不是不收敛,而是收敛到了一个错误的地方。

第三,看曲线走势是否平滑。SCA的收敛曲线在多数单峰函数上非常平滑,但在F7上会呈现明显的抖动,原因是F7的适应度每次计算都叠加了一个随机噪声项,即使搜索位置不变,适应度也会跳动。如果你打算用F7做算法对比,我建议在每次运行前固定随机种子,或者在统计时采用多次评价取平均的方式,否则结果波动会掩盖算法本身的性能差异。

4.4 SCA不擅长的地方

上面这张表格也直接暴露了SCA的两个短板。第一个是Rosenbrock这类强耦合函数(F5),SCA的表现只有1e+1量级,远不如差分进化或CMA-ES,因为它没有显式的梯度方向引导,光靠正弦振荡在山谷中“蛇形走位”效率很低。第二个是严重多峰的Shekel系列(F21-F23),SCA多次实验的平均值只能到达理论值的70%到90%左右,说明它一旦陷入局部峰值,后期几乎没有自救能力。

如果你后续要在论文里使用SCA,我建议不要仅仅报告“分跑了多少分”,而是针对这两个短板补充改进策略的讨论,比如引入混沌映射初始化种群、把r1改为非线性递减、或者在后期叠加局部搜索算子。这也是SCA领域改进文献最常见的切入点。

5. 常见问题与调试经验:这些坑我替你先踩了

5.1 结果出现NaN/Inf怎么办

跑SCA时最常见的崩溃现场是:某个函数的适应度输出NaN或者Inf。排查顺序一般是这样:

第一步查边界设置。F10 Ackley函数里有exp(mean(cos(2*pi*x))),如果某个x分量超出合理范围几十倍,指数项会直接溢出为Inf。很多函数在代码里已经限定了边界,但如果你的lb/ub和维度不匹配(比如把标量边界直接传入而没扩展成向量),种群初始化的随机值可能严重越界,导致第一轮适应度就爆掉。

第二步查公式里的向量维度。F5、F12、F13这类函数内部用到了x(2:end)numel(x)等索引操作。如果你的目标函数是针对向量写的,但SCA在某个环节传入了一个标量,就会出现索引超出数组元素数量之类的报错。我建议在Get_Functions_details返回fobj之前,统一保证lbub都已扩展成1×dim的向量,可以避免大部分维度不一致问题。

第三步查除零情况。F14 Shekel's Foxholes公式里有1 ./ (j + distance^6),理论上distance等于0时分母为1,不会除零,但如果某个实现把分母写成了1 ./ (distance^6),搜索到全局最优点时就会产生Inf。

5.2 收敛曲线画不出来或画出来是平的

如果你用semilogy(1:Max_iteration, Convergence_curve)画图,有时候会弹出“对数坐标不能显示负数或零”的警告,这是因为收敛值降到了0。我前面提到过,加上一个eps偏移就能解决。

还有一种情况是曲线“平得像一条直线”,这通常不是你代码的问题,而是目标函数过于简单,比如F1在种群规模50、迭代2000代的情况下,前50代就已经收敛到1e-30,后面的曲线在坐标轴尺度下看起来就是一条水平线。这时候有两种处理方式:一是减少迭代次数,让收敛过程更明显地展示出来;二是把坐标改为plot(1:Max_iteration, Convergence_curve, 'YScale', 'log')同时设置ylim,把注意力集中在收敛发生的那一小段区间。

5.3 为什么每次运行结果都不一样

这是元启发式算法的正常现象,因为rand()产生的初始种群在每次运行时都不同。如果你希望结果可复现,就用rng(固定整数)固定随机数生成器的种子。但这里要说清楚:复现实验和做统计评估是两回事。做算法对比时,固定种子虽然让代码稳定,但单一随机种子下的表现不能代表算法整体水平,你必须用多种子多次运行取统计量。

我在自己跑实验时,习惯把rng放在主脚本最前面固定一次,然后在循环内通过rng(run)为每次实验设置不同的子种子,这样既满足整体可复现性,又能获得30次不同初始化下的样本分布。

5.4 想换种群规模或迭代次数时怎么调

如果你发现某个函数收敛不理想,不要盲目把种群规模和迭代次数同时拉满,那样只会增加计算时间而未必改善结果。我给你一个实用的调参建议:

  • 对于单峰函数(F1、F6等),优先减少迭代次数到500,种群规模保持30就够了,SCA在简单地形上收敛很快。
  • 对于Rosenbrock(F5),优先增大迭代次数到2000,效果比增大种群规模明显。
  • 对于多峰函数(F8、F21-F23),增大种群规模到50或者更多,同时考虑修改r1的非线性递减公式。单纯增加迭代次数对跳出局部陷阱帮助不大,因为r1太小时搜索半径已经锁死。

另外,如果你想做横向对比实验,所有算法必须使用相同的种群规模和迭代次数,这是论文评审的基本要求,也是对比公平性的底线。

结尾的一点心得

最后分享一个我反复踩坑后总结出来的习惯:拿到一个元启发式算法,不要急着跑复杂函数,先在F1、F9、F16这三个函数上各跑一遍,确认收敛趋势正常、最优值能贴住理论最优,再去跑剩余函数。因为F1测收敛速度、F9测跳出局部最优的能力、F16测低维精细搜索的精度,三个函数基本能暴露一个优化算法80%的问题。我最初用SCA时,一上来就跑了全套23个函数,结果F5表现差、F21表现差,差点以为代码写错了,后来逐步排查才发现,这两个函数恰恰是SCA本身的短板,而不是实现问题。先把算法特性摸清楚,再谈对比和改进,这条路会让你省下大量不必要的调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询