资讯详情 正弦余弦指引的乌鸦搜索算法SC-CSA:原理、代码与改进效果
📅 2026/10/8 3:57:20
最近在做一个多峰函数的优化项目常规的乌鸦搜索算法CSA跑了几轮发现结果总在差不多的位置卡住。后来我想到把正弦余弦算法SCA的振荡机制融进CSA里用正弦和余弦的周期性波动来引导乌鸦跳出局部陷阱改出来的这个版本我叫它SC-CSA。整体实现不复杂Matlab代码也就一百来行但在标准测试函数上的收敛精度和稳定性都明显好于原始CSA。这篇文章就把完整的Matlab代码、改进思路和调试过程中踩过的坑一次说清楚适合正在用元启发式算法做优化、想改进CSA或者刚接触智能算法想快速上手的朋友。1. 乌鸦搜索算法为什么需要“正弦余弦”指引1.1 原始CSA的行为模拟乌鸦搜索算法是2016年提出的一种群体智能算法模仿的是乌鸦藏食物、偷食物时那种“你追我赶”的行为。每只乌鸦个体都维护一个私人缓存位置我们记为M这个位置可以是它发现过的最优位置。算法运行时某个个体会随机盯上另一只乌鸦想跟着它去找它的藏食点。如果被跟踪的乌鸦没发现自己被盯梢跟踪者就会朝目标的藏食点方向移动一步如果被跟踪者发现了就会故意飞到一个随机的新位置把跟踪者甩掉。用数学描述就是对第i只乌鸦随机选一个跟踪对象j生成一个随机数r。如果r大于等于感知概率AP表示第i只没被发现它就按下面的公式移动X_i^(t1) 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通常取0aMax取2这是SCA论文里推荐的经典配置。迭代初期t很小r1接近2迭代后期r1接近0。这个系数决定了正弦余弦振荡的“摆幅”。每次需要生成新位置时先随机生成三个参数r2在[0, 2π]之间相当于三角函数的相位角r3在[0, 2]之间作为最优位置与当前位置的相对距离缩放系数r4在[0, 1]之间用来决定使用正弦还是余弦。当r4 0.5时用正弦更新X_i^(t1) X_i^t r1 * sin(r2) * |r3 * bestPos - X_i^t|否则用余弦更新X_i^(t1) 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伪代码流程如下初始化乌鸦种群位置X和记忆M计算每个个体的适应度。找出全局最优位置bestPos。对每次迭代t 4. 计算当前振幅系数a。 5. 对每个个体i 6. 随机选择跟踪对象jj必须不等于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从任何起点都能平滑下降到0Rastrigin则布满了周期性的陷阱Griewank在原点附近有无数小坑非常适合测试算法会不会“半路掉坑里”。4.2 收敛曲线与改进效果对比我将原始CSA和SC-CSA分别跑了一遍记录每次迭代的最优适应度。下面是20次运行后最优适应度均值的对比。函数原始CSA最优均值SC-CSA最优均值Sphere6.2e-41.8e-7Rastrigin21.52.8e-5Griewank8.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.3SC-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影响跟随步长的上限。fl1意味着跟随步长最大等于两位置之间的距离fl1允许过冲。我建议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和flSC-CSA由于正弦余弦自带动态探索和开发平衡参数敏感性降低了不少。当然它也不是万能的如果你碰到极度欺骗性的多模态问题还是需要和局部搜索算子比如模式搜索结合使用。最后分享一个调试小技巧画收敛曲线时一定要用semilogy对数坐标否则那些毫微级别的提升根本看不出趋势。很多时候你以为算法没收敛其实它已经在逼近最优只是坐标尺度不合适而已。希望这篇代码和经验能帮你少走弯路快点把算法用到自己的问题上。如果你也是在搞元启发式算法改进欢迎交流思路。