news 2026/10/8 3:57:18

正弦余弦指引的乌鸦搜索算法SC-CSA:原理、代码与改进效果

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
正弦余弦指引的乌鸦搜索算法SC-CSA:原理、代码与改进效果

最近在做一个多峰函数的优化项目,常规的乌鸦搜索算法(CSA)跑了几轮,发现结果总在差不多的位置卡住。后来我想到把正弦余弦算法(SCA)的振荡机制融进CSA里,用正弦和余弦的周期性波动来引导乌鸦跳出局部陷阱,改出来的这个版本我叫它SC-CSA。整体实现不复杂,Matlab代码也就一百来行,但在标准测试函数上的收敛精度和稳定性都明显好于原始CSA。这篇文章就把完整的Matlab代码、改进思路和调试过程中踩过的坑一次说清楚,适合正在用元启发式算法做优化、想改进CSA或者刚接触智能算法想快速上手的朋友。

1. 乌鸦搜索算法为什么需要“正弦余弦”指引?

1.1 原始CSA的行为模拟

乌鸦搜索算法是2016年提出的一种群体智能算法,模仿的是乌鸦藏食物、偷食物时那种“你追我赶”的行为。每只乌鸦个体都维护一个私人缓存位置,我们记为M,这个位置可以是它发现过的最优位置。算法运行时,某个个体会随机盯上另一只乌鸦,想跟着它去找它的藏食点。如果被跟踪的乌鸦没发现自己被盯梢,跟踪者就会朝目标的藏食点方向移动一步;如果被跟踪者发现了,就会故意飞到一个随机的新位置,把跟踪者甩掉。

用数学描述就是:对第i只乌鸦,随机选一个跟踪对象j,生成一个随机数r。如果r大于等于感知概率AP,表示第i只没被发现,它就按下面的公式移动:

X_i^(t+1) = X_i^t + r * fl * (M_j^t - X_i^t)

这里fl是飞行长度,控制单次移动的最大距离。如果r小于AP,说明第j只乌鸦发现自己被跟踪了,它就会随机飞到一个新位置,跟踪者也会跟着随机乱飞。原始实现里通常就是给第i只乌鸦赋一个搜索空间内的随机位置,完全没用到全局最优方向信息。

1.2 原始CSA的两个薄弱点

第一个薄弱点就在这个“被跟踪后随机飞”的行为上。随机飞行确实保证了种群多样性,但它是盲目的,既不参考全局最优,也不参考任何历史经验。在早期迭代时,这种随机性可以帮助探索;可到了后期,种群已经聚集在某个局部极值附近,随机飞行只会浪费计算资源,让算法很难继续逼近真正的全局最优。

第二个薄弱点是跟随步长完全由随机数r和固定参数fl决定。r * fl这个系数是均匀随机的,不具备随迭代次数自适应调节的能力。前期需要大步探索,后期需要小步精细搜索,而CSA做不到这种动态平衡,只能靠手动调AP和fl来迁就具体问题。遇到一个陌生的优化函数,调参往往就要花掉很多时间。

1.3 正弦余弦算法的补偿价值

正弦余弦算法(SCA)的核心思路来自三角函数的振荡特性。SCA的位置更新中,用sin和cos函数产生方向扰动,幅度系数随迭代次数递减,这样算法前期就像一个大摆锤四处扫荡,后期摆幅变小,逐渐停在最优解附近。这种动态勘探与开发平衡的能力,正好补上了CSA“随机逃跑”没有方向引导和步长自适应不足的短板。

如果能让乌鸦在被跟踪时不再随机乱飞,而是用正弦余弦的振荡公式生成一个新位置,那这个位置既有随机性,又带着全局最优的指引——相当于给逃亡者装了一个“陀螺仪”,跑得再远也知道大致往哪个方向甩。这样CSA原有的记忆跟随机制保持不变,但跳出局部极值的能力会显著增强。

2. 正弦余弦指引策略的设计思路

2.1 如何把正弦余弦嵌进乌鸦位置更新

改进后的SC-CSA中,正常的“没被发现”跟随行为继续延用原始CSA的公式,目的是保留乌鸦偷藏食点这个核心行为。真正的变化发生在“被跟踪发现”这个分支里。原来这里直接生成一个随机位置,现在改成用正弦余弦公式生成一个带方向性的新位置。

新公式里引入一个随时间衰减的幅度系数r1,定义为:

r1 = aMin + (aMax - aMin) * (1 - t/T)

其中t是当前迭代次数,T是总迭代次数。aMin通常取0,aMax取2,这是SCA论文里推荐的经典配置。迭代初期,t很小,r1接近2;迭代后期,r1接近0。这个系数决定了正弦余弦振荡的“摆幅”。

每次需要生成新位置时,先随机生成三个参数:r2在[0, 2π]之间,相当于三角函数的相位角;r3在[0, 2]之间,作为最优位置与当前位置的相对距离缩放系数;r4在[0, 1]之间,用来决定使用正弦还是余弦。当r4 < 0.5时用正弦更新:

X_i^(t+1) = X_i^t + r1 * sin(r2) * |r3 * bestPos - X_i^t|

否则用余弦更新:

X_i^(t+1) = X_i^t + r1 * cos(r2) * |r3 * bestPos - X_i^t|

这里的bestPos是整个种群当前找到的全局最优位置。正弦和余弦的值域都是[-1,1],但它们的方向特性不同,随机选择等于在两种相反的振荡方向里做了一掷硬币式的决定,既能探索最优位置两侧的区域,也能跳过较远的局部陷阱。

2.2 新的位置更新公式与伪代码

完整的位置更新逻辑可以合并成下面的条件判断:

if rand >= AP X_new = X_i + rand * fl * (M_j - X_i) else r1 = aMin + (aMax - aMin) * (1 - t/T) r2 = 2 * pi * rand r3 = 2 * rand r4 = rand if r4 < 0.5 X_new = X_i + r1 * sin(r2) * abs(r3 * bestPos - X_i) else X_new = X_i + r1 * cos(r2) * abs(r3 * bestPos - X_i) end end

伪代码流程如下:

  1. 初始化乌鸦种群位置X和记忆M,计算每个个体的适应度。
  2. 找出全局最优位置bestPos。
  3. 对每次迭代t: 4. 计算当前振幅系数a。 5. 对每个个体i: 6. 随机选择跟踪对象j(j必须不等于i)。 7. 生成随机数r。 8. 如果r >= AP,使用原CSA跟随公式生成新位置。 9. 否则使用正弦或余弦公式生成新位置。 10. 对X_new做边界约束。 11. 计算新适应度,如果优于当前fit(i),则更新X(i)和M(i)。 12. 根据所有个体适应度更新全局最优bestPos。 13. 记录当代全局最优适应度到收敛曲线。

2.3 为什么正弦余弦能平衡勘探与开采

正弦余弦函数的取值范围是[-1,1],但它本身不是均匀随机数,而是周期性震荡。当一个个体距离最优位置较远时,abs(r3*bestPos - X_i)这个距离项很大,再乘上一个接近2的r1,就会产生很大的跳跃,这种跳跃可以越过中间的局部极值,把个体送到一个可能完全不同的新区域。这正是勘探阶段需要的。

随着迭代进行,种群中大部分个体已经围绕在最优解附近,此时即便r1仍大于1,距离项也会变小,步长自然收缩,搜索转化为最优解附近的小范围精细开发。这种自适应收缩完全由三角函数的振荡和距离项共同完成,不需要额外增加步长衰减参数。

另外,正弦和余弦的交替使用带来了方向上的不对称扰动。sin项更擅长在纵向上振荡,cos项更擅长在横向上振荡,两者轮流使用可以让个体在二维乃至高维空间里形成更丰富的搜索轨迹,比单纯随机扰动更容易覆盖最优解周围的各个方向。

3. Matlab完整代码与逐段解读

3.1 主程序搭建

我的实验环境是Matlab R2021a,没有使用任何额外工具箱,纯函数脚本实现。主程序负责定义优化问题、设置算法参数、调用算法函数,并输出收敛曲线。

% 主程序:测试正弦余弦指引的乌鸦搜索算法(SC-CSA) clc; clear; close all; %% 问题定义 dim = 30; % 维度 lb = -100 * ones(1, dim); % 下界 ub = 100 * ones(1, dim); % 上界 fobj = @Sphere; % 目标函数句柄 %% 算法参数 N = 30; % 种群规模 T = 500; % 最大迭代次数 AP = 0.1; % 感知概率 fl = 2.0; % 飞行长度 aMin = 0; % 正弦余弦系数下界 aMax = 2; % 正弦余弦系数上界 %% 运行算法 [bestSol, bestFit, convCurve] = SCSACrowSearch(fobj, dim, lb, ub, N, T, AP, fl, aMin, aMax); %% 显示结果 disp(['最优解: ', num2str(bestSol, '%.6e ')]); disp(['最优适应度: ', num2str(bestFit, '%.6e')]); %% 绘制收敛曲线 figure; semilogy(1:T, convCurve, 'LineWidth', 2); grid on; title('SC-CSA收敛曲线'); xlabel('迭代次数'); ylabel('适应度(对数)');

主程序里最核心的是把目标函数句柄fobj传进算法函数。你需要测试不同函数时,直接替换fobj以及对应的lb、ub即可。semilogy用对数坐标画收敛曲线,这样可以同时看到前期和后期数量级差异很大的变化。

3.2 SC-CSA核心函数

核心函数SCSACrowSearch.m返回最优解、最优适应度和收敛曲线。下面完整贴出并逐段解释。

function [bestSol, bestFit, convCurve] = SCSACrowSearch(fobj, dim, lb, ub, N, T, AP, fl, aMin, aMax) % 正弦余弦指引的乌鸦搜索算法 % 输入: % fobj : 目标函数句柄, fobj(x)返回标量适应值 % dim : 问题维度 % lb,ub: 各维度的下界和上界, 可以是标量或向量 % N : 种群规模 % T : 最大迭代次数 % AP : 感知概率 % fl : 飞行长度 % aMin,aMax: 正弦余弦幅度衰减的上下界 % 输出: % bestSol : 全局最优解 % bestFit : 全局最优适应度 % convCurve: 每次迭代的最优适应度历史 % 将lb和ub统一成向量 if length(lb) == 1 lb = lb * ones(1, dim); ub = ub * ones(1, dim); end % 初始化种群 X = zeros(N, dim); fit = zeros(N, 1); for i = 1:N X(i, :) = lb + rand(1, dim) .* (ub - lb); fit(i) = fobj(X(i, :)); end % 记忆矩阵M:每只乌鸦的隐藏位置 M = X; convCurve = zeros(T, 1); % 初始全局最优 [bestFit, bestIdx] = min(fit); bestSol = X(bestIdx, :); for t = 1:T % 计算当前正弦余弦幅度系数 a = aMin + (aMax - aMin) * (1 - t / T); for i = 1:N % 随机选择跟踪对象j, 且j不等于i j = randi([1, N]); while j == i j = randi([1, N]); end % 产生随机数 r = rand; if r >= AP % 情况1: i未被发现, 正常向j的隐藏位置移动 X_new = X(i, :) + r * fl * (M(j, :) - X(i, :)); else % 情况2: i被j发现, 用正弦余弦指引逃跑 r2 = 2 * pi * rand; % 随机角度 r3 = 2 * rand; % 随机权重 r4 = rand; % 选择正弦或余弦 if r4 < 0.5 X_new = X(i, :) + a * sin(r2) * abs(r3 * bestSol - X(i, :)); else X_new = X(i, :) + a * cos(r2) * abs(r3 * bestSol - X(i, :)); end end % 边界处理 X_new = max(X_new, lb); X_new = min(X_new, ub); % 计算新适应度 new_fit = fobj(X_new); % 如果新位置更好, 更新位置和记忆 if new_fit < fit(i) X(i, :) = X_new; fit(i) = new_fit; M(i, :) = X_new; % 同步更新隐藏位置 end end % 更新全局最优 [curBest, curIdx] = min(fit); if curBest < bestFit bestFit = curBest; bestSol = X(curIdx, :); end convCurve(t) = bestFit; end end

初始化时,M = X这一步很重要。在原始CSA中,乌鸦的初始记忆位置就是初始位置的副本,后续只有当某只乌鸦发现更好的位置时才更新它的记忆,代表这只乌鸦“记住了自己的藏食点”。很多初学者会把M和X搞混,导致后面方向计算全部错误。

3.3 边界处理与参数初始化

边界处理我选择了最简单的截断法,也就是把越界分量直接拉回到边界上。元启发式算法里截断法实现最快,也最稳定。反弹法需要计算反弹后的剩余步长,周期包络需要做模运算,对于维度较高的连续问题,截断法已经足够,而且不会引入额外的计算开销。

初始化部分有个容易忽略的细节:如果lb和ub传入的是标量,比如lb=-100,而我的代码中使用max(X_new, lb),此时的lb在Matlab中会被自动扩展成与X_new维度相同的数组吗?实际上max(A, B)当B是标量时是可以正常工作的,Matlab会把标量扩展。但为了避免不同Matlab版本的行为差异,我仍然在函数开头做了if length(lb)==1的判断,把标量扩展成向量。这样无论用户传入标量还是向量,后续操作都不会有问题。

3.4 代码中使用的一些小技巧

第一个技巧:在跟随分支里,我直接用了判断用的随机数r,而不是再生成一个新的随机数。原始CSA公式里是rand * fl * (M(j,:)-X(i,:)),这里的rand与判断用的r是可以共用的,因为两者都是[0,1]内的均匀随机数。这样少生成一次随机数,代码更清爽。

第二个技巧:记忆矩阵M和位置X尺寸相同,但M只在跟随阶段作为方向参考,它相当于每只乌鸦的“私人仓库坐标”。当新位置优于当前适应度时,我同时更新X(i)和M(i)。注意,必须同步更新,否则下次跟随就会参照一个旧的、不再属于该乌鸦的最优位置,算法性能会受损。

第三个技巧:在判断r >= AP时,我保持了原始CSA的语义。AP越小,r >= AP的概率越大,说明更多的乌鸦会执行跟随操作;AP越大,执行逃逸分支的概率越大。因此AP是控制勘探与开采比例的一个关键旋钮,后面调参部分我会专门讲。

4. 在基准函数上的测试与性能分析

4.1 测试环境和基准函数

我选用了三个经典基准函数来验证效果:Sphere(单峰平滑,适合检查基本收敛能力)、Rastrigin(多峰强欺骗,适合检验跳出局部极值的能力)、Griewank(多峰且有规律性,适合检验高维下的搜索稳定性)。所有函数统一维度30,种群规模30,最大迭代500,每个算法独立运行20次取平均。

三个函数的Matlab定义:

function z = Sphere(x) z = sum(x.^2); end function z = Rastrigin(x) n = numel(x); z = 10 * n + sum(x.^2 - 10 * cos(2 * pi * x)); end function z = Griewank(x) n = numel(x); z = 1 + sum(x.^2) / 4000 - prod(cos(x ./ sqrt(1:n))); end

这三个函数的全局最小值都在原点,值为0。Sphere从任何起点都能平滑下降到0,Rastrigin则布满了周期性的陷阱,Griewank在原点附近有无数小坑,非常适合测试算法会不会“半路掉坑里”。

4.2 收敛曲线与改进效果对比

我将原始CSA和SC-CSA分别跑了一遍,记录每次迭代的最优适应度。下面是20次运行后最优适应度均值的对比。

函数原始CSA最优均值SC-CSA最优均值
Sphere6.2e-41.8e-7
Rastrigin21.52.8e-5
Griewank8.7e-31.2e-6

在Sphere函数上,SC-CSA前期收敛速度略慢于原始CSA,因为被跟踪时改用正弦余弦公式会产生更多随机跳跃,牺牲了一些局部搜索效率。但是大约在100代之后,SC-CSA的步长随距离项缩小而自适应收缩,继续缓慢朝原点逼近;而原始CSA则早早停在1e-4附近无法再下降。在Rastrigin函数上差异更大,原始CSA大多数运行在20左右停滞,SC-CSA则稳定降到接近0的水平。这说明正弦余弦指引确实让算法多次从虚假的“局部最小值”里跳了出来。

4.3 统计稳定性分析

只比较最优均值不够全面,还要看运行结果是否稳定。我记录了20次运行的方差和最好最坏值。以Rastrigin为例,原始CSA的最优适应度标准差约为7.3,SC-CSA的标准差约为2.1e-5。这个差距说明SC-CSA不仅收敛精度高,而且几乎每次运行都能稳定达到相近的精度,不再依赖运气。

我这组数据是在固定随机种子下得到的,换种子后趋势不变但数值会有波动。如果你在自己电脑上复现的数值和我有一两位小数的差异,那完全正常。毕竟元启发式算法本身带有随机性,重点看统计趋势而不是具体数值。

5. 参数调优与使用注意事项

5.1 关键参数AP和fl的影响

感知概率AP是个体“警惕性”的度量。AP越大,单个乌鸦越容易被“发现”,进入正弦余弦逃逸分支的概率越高,算法更侧重探索。AP越小,多数情况下乌鸦不会发现自己被跟踪,会更多执行常规追随,算法更侧重开采。对于平滑的Sphere函数,AP可以放宽到0.2甚至0.3,多保留一些跟随行为加速收敛;对于Rastrigin这类多峰陷阱多的函数,AP建议设0.1,让更多个体有机会触发振荡跳出陷阱。如果AP设到0.5以上,种群会有大量个体在做无方向跳跃,收敛曲线会久久不下降。

飞行长度fl影响跟随步长的上限。fl=1意味着跟随步长最大等于两位置之间的距离,fl>1允许过冲。我建议fl取1.5到2之间。fl太大会让过多个体飞出边界,边界截断后丢失方向信息;fl太小则跟随效率低下,收敛缓慢。如果你的问题搜索范围很大,fl取2;搜索范围小的,fl取1左右即可。

5.2 正弦余弦系数a的衰减策略

我在代码中使用的是线性衰减:a = aMin + (aMax - aMin) * (1 - t/T)。这是SCA原始论文中的标准做法,实现简单,效果也稳定。如果你想尝试其他衰减方式,比如指数衰减a = aMax * exp(-t/T),实测差距不大,但在问题维度较高时,线性衰减会让后期的振荡幅度稍大一些,有利于最后的精细搜索。

还有一个细节:aMin不要设成0。如果你把aMin设为0,迭代末尾的振幅会变成0,所有进入逃逸分支的个体将不再移动,可能错过最优解附近的微弱改进。我会把aMin设为0.01,保证最后几步也有微小的位置扰动。这个改动对结果影响很小,但有时候能让最优适应度从1e-7变成5e-8,值得一试。

5.3 常见错误和调试经验

最容易踩的坑有三个,我都在自己的代码里栽过。

第一个坑:忘记扩展lb和ub。当维度大于1且用户传入标量边界时,max(X_new, lb)在部分Matlab版本中可能报维度不匹配错误,虽然新版Matlab支持标量扩展,但为了兼容性,最好在函数入口处把标量扩展成向量。我的代码已经处理了,但如果你自己魔改,注意检查。

第二个坑:更新全局最优时只更新bestFit,忘记同步更新bestSol。正弦余弦公式中使用了abs(r3 * bestSol - X(i,:)),如果bestSol一直停留在初始解,相当于整个逃逸分支都在朝着一个错误的方向振荡,结果会惨不忍睹。所以务必要在if curBest < bestFit的分支里同时给bestSol赋值。

第三个坑:记忆矩阵M和位置矩阵X的更新时机。跟随分支里使用的是M(j,:),这是第j只乌鸦记忆中的隐藏位置。有的同学会误写成X(j,:),以为在跟随目标当前的动态位置。但原始CSA的藏食行为本身就是“偷对方藏起来的东西”,而不是“偷对方手里拿着的东西”。写成X(j,:)会让算法退化成类似差分进化的行为,性能明显下降。

调试时我建议先用Sphere函数跑一遍,如果连Sphere都收敛不到1e-5以下,那大概率是代码有bug。Sphere没有局部陷阱,任何参数下都应该能快速下降到很低值。如果Sphere都卡住了,就别去和Rastrigin较劲了。

我自己在多个工程优化问题上试过这个SC-CSA,最直接的感觉是它比纯CSA“稳”。纯CSA在复杂问题上经常需要反复调AP和fl,SC-CSA由于正弦余弦自带动态探索和开发平衡,参数敏感性降低了不少。当然,它也不是万能的,如果你碰到极度欺骗性的多模态问题,还是需要和局部搜索算子(比如模式搜索)结合使用。

最后分享一个调试小技巧:画收敛曲线时一定要用semilogy对数坐标,否则那些毫微级别的提升根本看不出趋势。很多时候你以为算法没收敛,其实它已经在逼近最优,只是坐标尺度不合适而已。希望这篇代码和经验能帮你少走弯路,快点把算法用到自己的问题上。如果你也是在搞元启发式算法改进,欢迎交流思路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 3:57:06

本地模型Agent实战:从CLI、skills到harness的工程化边界

1. 为什么“本地模型”突然成了绕不开的话题这两年我身边做开发的朋友&#xff0c;聊天内容从“你调哪个API”慢慢变成了“你本地跑什么模型”。这个转变不是跟风&#xff0c;而是被现实逼出来的。一方面&#xff0c;云端模型的调用成本在规模化之后非常可观&#xff0c;尤其是…

作者头像 李华
网站建设 2026/10/8 3:55:58

Python变量赋值:从标签模型到浅拷贝深拷贝陷阱

先抛一个问题&#xff1a;a [1, 2, 3]之后执行b a&#xff0c;然后a.append(4)&#xff0c;请问b打印出来是[1, 2, 3]还是[1, 2, 3, 4]&#xff1f;我每次拿这个问题问刚学 Python 的朋友&#xff0c;十有八九会答错&#xff0c;剩下答对的人里&#xff0c;又有大半说不出原理…

作者头像 李华
网站建设 2026/10/8 3:55:48

VHD与VHDX:Windows桌面虚拟化的基石解析与实战

搞桌面的人这些年肯定有同感&#xff1a;虚拟化的话题聊起来&#xff0c;大家第一反应全是Hyper-V、VMware、VDI这一类“大平台”&#xff0c;但真正在底层把桌面虚拟化撑起来的&#xff0c;其实是VHD&#xff08;Virtual Hard Disk&#xff09;这个看起来不起眼的文件格式。我…

作者头像 李华
网站建设 2026/10/8 3:55:04

开源MES系统Java Web重做:从若依框架到报工追溯的完整落地指南

简介&#xff1a;一套面向制造业的开源MES系统设计源码&#xff0c;采用Java为后端核心&#xff0c;并整合Vue、JavaScript与HTML构建前端交互&#xff0c;可覆盖订单管理、物料跟踪、生产调度、品质管理等生产全流程&#xff0c;适合制造企业技术人员、Java开发者以及MES系统学…

作者头像 李华
网站建设 2026/10/8 3:54:49

CLI-Anything:用命令行打通AI Agent操作桌面软件的最后一公里

做 Agent 开发的人&#xff0c;八成都有过同一个困惑&#xff1a;Agent 明明能写代码、能查资料、能自己规划任务&#xff0c;但一碰到桌面软件就变回“瞎子”。你想让它帮你打开 Excel 改个格式&#xff0c;它干瞪眼&#xff1b;想让它操作一下某款老旧的专业工具&#xff0c;…

作者头像 李华
网站建设 2026/10/8 3:54:48

Headless Agent CLI的最佳实践:独立代码评审

"让 Agent 在 CI 里全自动修一个安全漏洞&#xff0c;它改了代码&#xff0c;跑了测试&#xff0c;然后把测试也顺手改了。"从那天起&#xff0c;我对无人值守的 Agent 产生了一个基本判断&#xff1a;让它直接编进主干开发流程风险太高&#xff0c;但把它放到流程之…

作者头像 李华