☰
复现SCASL:正余弦优化算法融合Lévy飞行的改进实现与踩坑记录
2026/10/1 22:40:30 网站建设 项目流程

正余弦优化算法(SCA)复现过的人不少,但多数人跑完原版就停了,后面那些改进变体反而没人愿意深挖。我这次复现的SCASL(Sine Cosine Algorithm with Lévy flight)正好补上这块拼图——它是在原版SCA基础上加了Lévy飞行和自适应系数的一套改进方案。复现这事,看着简单,真动手才发现坑比想象中多:收敛曲线画出来不光滑、Lévy步长飞出去了没触发边界修复、多峰函数上改进算法反而不如原版……这些我都踩过。这篇文章就把我的复现过程、代码细节和踩坑记录完整摊开,给想动手复现元启发式算法的读者一条能走通的路。

先说清楚,SCASL在不同论文里有不同写法,我这里按最常见的理解来:基于原版SCA引入Lévy飞行策略,同时改进了位置更新时的系数控制方式,目的是增强全局搜索能力、避免种群陷入局部最优。整体思路就是"原版SCA打底 + Lévy长尾跳跃 + 自适应系数调整"。

复现这种算法类论文,最忌讳的就是一上来就抄代码。你抄得了结果,抄不了对算法设计意图的理解。真正有用的复现流程是:先读懂每个公式为什么这么写,然后用自己的语言把伪代码翻译成可运行程序,最后在标准测试函数上验证行为是否符合预期。我这篇就是按这个流程走的。

1. 先看清SCA的本质:复现之前必须弄懂的事

1.1 原版SCA的四要素:从数学公式到直觉

正余弦优化算法(SCA)是Mirjalili在2016年提出的群智能优化算法。它的核心逻辑听起来很简单:用正弦和余弦函数的周期性波动来驱动搜索代理(Search Agent)在解空间中来回震荡,从而寻找最优解。但就是这种简单的数学构造,反而比很多复杂策略更实用。

SCA的位置更新公式长这样:

[ X_i^{t+1} = \begin{cases} X_i^t + r_1 \cdot \sin(r_2) \cdot |r_3 \cdot P_i^t - X_i^t| & \text{if } r_4 < 0.5 \ X_i^t + r_1 \cdot \cos(r_2) \cdot |r_3 \cdot P_i^t - X_i^t| & \text{if } r_4 \geq 0.5 \end{cases} ]

这四个参数每个都是设计过的:

  • ( r_1 ):控制下一步移动的方向和步幅。它随迭代次数从大致2线性递减到0,决定算法前期多探索、后期多开发。这是SCA平衡全局与局部的核心杠杆。
  • ( r_2 ):决定正弦/余弦函数内部的角度,相当于给搜索移动一个随机的"摆向",驱动解空间内的震荡搜索。
  • ( r_3 ):给当前最优解 ( P_i^t ) 加一个随机权重,目的是既让代理向已知最优解靠拢,又保留一定的随机偏航。
  • ( r_4 ):在0到1之间取随机值,单纯用来在正弦更新和余弦更新之间做五五开的选择。

为什么要用正余弦?直觉上理解,这两种函数天然具有周期性波动,周期性带来的是"环绕"式的搜索路径——既能接近目标点,又能因为相位变化而偏离,防止路径过度重复。这种特性让SCA在低维问题上表现不错,实现成本也低。

但原版SCA有一个天生的短板:( r_1 ) 线性递减策略太粗暴。前期探索的冲击力衰减过快,后期的开发步长又太小,如果种群初始位置不好,很容易卡在局部最优里出不来。SCASL就是冲着这个问题去的。

1.2 SCASL的改进动机:它到底改了什么

SCASL不是推翻SCA重来,而是在SCA的骨架上做两个关键动作。

第一个动作,引入Lévy飞行。Lévy飞行是一种步长服从重尾分布的随机游走模式,它的特点是大多数时候走小步,偶尔会突然跳出很远的距离。这种"少量远跳+大量近步"的组合,正好弥补SCA后期因为 ( r_1 ) 变小而丧失的远程探索能力。你可以把SCA原来那条移动路径想象成一个人在一个圆圈里来回踱步,加上Lévy飞行之后,这个人偶尔会大步跳到圈外,看看别处有没有更好的位置。

第二个动作,改系数生成方式。原版SCA的 ( r_3 ) 是均匀随机生成的,和当前解到最优解的距离没有关系。SCASL通常会把 ( r_3 ) 改为与搜索进程或代理与最优解之间的距离相关联的自适应权重,让算法在早期更偏向于"跟着最优走",中后期则根据种群收敛状态动态调节倾向。不同论文对这部分写法有差异,我复现时采用的是"线性递减 ( r_3 ) 基值 + Lévy步长放大因子"的组合策略。

从整体设计动机来看,SCASL想做的是:保持SCA简单、参数少的优点,同时把全局探索能力提上去,减少对初始种群的过度依赖。

2. 复现前的弹药准备:环境、数据与验证框架

2.1 环境搭建与依赖选择

复现SCASL的环境不用太重,我只用了Python 3.9 + NumPy + Matplotlib,全程不需要额外装任何优化库。有人说可以用现成的Metaheuristic框架,比如mealpy里的SCA实现,但复现实验我强烈建议别用库——你用库调一下输出结果,和自己一行行公式翻译成代码,对算法细节的理解深度完全不一样。

我用纯NumPy的原因有三个:

  1. 省时间,不涉及工程化的版本冲突。
  2. 向量化写法能模拟种群并行,性能足够跑完30次独立重复实验。
  3. 调试时能直接打印每一步的中间变量,容易被库封装掩盖的细节(比如边界处理方式)能完全透明。

Matplotlib只用来画收敛曲线和箱线图,画图代码占不了多少行。

提示:如果你是在Windows上复现,注意NumPy的随机种子在并行线程和单线程下的行为可能不完全一致。建议全程用同一个随机数生成器实例,或者严格设置np.random.seed(),否则不同机器上跑出来的均值和方差统计结果可能有细微差异。

2.2 基准测试函数的选取逻辑

复现不选对测试函数,后面所有结论都站不住脚。我给SCASL选了5个经典基准函数,覆盖单峰、多峰、高维三类场景:

函数名类型维度搜索范围全局最优
Sphere单峰30[-100, 100]0
Rastrigin多峰30[-5.12, 5.12]0
Griewank多峰30[-600, 600]0
Ackley多峰30[-32, 32]0
Rosenbrock单峰(非凸)30[-30, 30]0

选这几个函数的逻辑是:

  • Sphere单峰平滑,测的是算法的收敛速度和精修能力,理论上任何算法应该都能逼近0。
  • Rastrigin和Griewank的搜索空间里布满大量局部最优点,测的是算法跳出局部最优、找到全局最优的能力,这是SCASL改进成效的核心验证场。
  • Ackley在中心附近有大量狭窄的波谷,对种群多样性要求极高,适合测试Lévy飞行"远跳"的实际效果。
  • Rosenbrock虽然名义上是单峰,但它的谷底是一条狭长的抛物线状通道,算法一旦偏离通道就很容易迷路,既能测收敛也能测路径寻找能力。

2.3 实验对照计划

对照实验的设计同样重要。只跑SCASL一个人的结果是没意义的,必须有原版SCA做参照。有条件的话再加一个PSO或DE作为第二对照,用来判断SCASL的改进幅度在整个元启发式算法家族里处于什么水平。

我跑了三组:原版SCA、SCASL、以及经典PSO。每组在5个测试函数上独立运行30次,每次用不同的随机种子,记录每次运行的最优适应度值(收敛精度)、收敛所需的迭代次数(收敛速度)和运行时间(计算开销)。统计指标取30次的均值、标准差、中位数和最优值。

这个统计布局看起来笨重,但它是论文复现的基本盘。尤其是标准差这个指标:如果SCASL的均值很好看但标准差很大,说明它的性能不稳定,对初始种群的依赖依然严重,那么"改进了全局探索"这个结论就要打折扣。

3. 一行一行写代码:SCASL核心实现拆解

3.1 初始化与参数设置的讲究

初始化这块,参数设置有大学问。种群数量N我设置的是30,最大迭代次数T是1000,测试函数维度D按基准要求设定为30。

为什么N取30?这是元启发式领域一个不约而同的经验值:种群太少,搜索覆盖不足;种群太多,每次迭代的计算开销线性增长,但带来的精度提升会迅速饱和。实测N从10涨到30时,收敛精度提升明显;从30涨到100时,精度提升很小,但耗时几乎翻倍。30是一个性价比很高的拐点。

初始化位置用均匀随机分布生成,保证每个代理均匀覆盖搜索空间:

pop = np.random.uniform(lb, ub, (N, D)) fitness = np.apply_along_axis(obj_func, 1, pop) best_pos = pop[np.argmin(fitness)].copy() best_fitness = np.min(fitness)

这里有个大家最容易忽略的细节:全局最优解的记录要单独保存。很多初学复现的人会把当前迭代中的最优解直接当成全局最优解,但这是错的——当前迭代最优解可能在下一轮被Lévy飞行的远跳移动到一个更差的位置,如果此时把全局最优覆盖更新,算法就丢掉了历史最优信息,收敛曲线会变得一路震荡,永远下不去。正确做法是每一轮更新完所有代理后,再计算当前种群最优,和已保存的全局最优比较,只有更好才覆盖。

3.2 Lévy飞行在SCA里的落地方式

Lévy飞行的标准实现方式是Mantegna方法。生成一个Lévy步长 ( L(\lambda) ) 的数学过程是:

[ L(\lambda) \sim \frac{u}{|v|^{1/\beta}} ]

其中 ( u ) 和 ( v ) 分别服从正态分布 ( N(0, \sigma_u^2) ) 和 ( N(0, \sigma_v^2) ),( \beta ) 通常取1.5,( \sigma_u ) 的计算公式是:

[ \sigma_u = \left[ \frac{\Gamma(1+\beta) \cdot \sin(\pi \cdot \beta / 2)}{\Gamma((1+\beta)/2) \cdot \beta \cdot 2^{(\beta-1)/2}} \right]^{1/\beta} ]

代码实现:

def levy_flight(beta=1.5): sigma_u = (math.gamma(1 + beta) * math.sin(math.pi * beta / 2) / (math.gamma((1 + beta) / 2) * beta * 2 ** ((beta - 1) / 2))) ** (1 / beta) u = np.random.normal(0, sigma_u, size=1) v = np.random.normal(0, 1, size=1) step = u / (np.abs(v) ** (1 / beta)) return step[0]

Lévy步长生成后,怎么融进SCA的更新公式是关键。我在复现中的做法是把它作为位置更新的缩放因子:

[ X_i^{t+1} = X_i^t + \alpha \cdot L(\lambda) \cdot \left( r_1 \cdot \sin(r_2) \cdot |r_3 \cdot P_i^t - X_i^t| \right) ]

这里的 ( \alpha ) 是一个缩放系数。为什么需要它?因为Lévy步长生成出来往往非常大(重尾特性导致),如果直接乘进去,代理会一步飞出搜索边界,边界修复策略就要频繁生效,等于白白浪费了这次移动。( \alpha ) 的作用是把Lévy步长的量级压回和原版SCA步长可比拟的范围。

实测下来,( \alpha ) 取0.01左右在大多数基准函数上表现稳定。这个值不是拍脑袋定的,而是以搜索空间范围的1%为基准调试得到的结果。不同的搜索空间范围最好重新标定,否则在Sphere函数(边界[-100,100])上合适的 ( \alpha ),换到Ackley(边界[-32,32])就可能失灵。

3.3 位置更新与自适应权重

SCASL的位置更新核心代码。我保留了原版SCA的 ( r_4 ) 开关,同时加入了Lévy步长和自适应权重 ( w )。 ( w ) 的设定是随迭代次数从0.9下降到0.4,通过余弦退火方式递减,这样前后期变化比线性递减更平滑:

def scasl_update(pop, best_pos, fitness, T, t, lb, ub, alpha=0.01, w_min=0.4, w_max=0.9): N, D = pop.shape new_pop = pop.copy() # r1: 余弦退火式递减 r1 = 2.0 * (1 - (t / T) ** 2) # 自适应权重 w = w_min + 0.5 * (w_max - w_min) * (1 + math.cos(math.pi * t / T)) for i in range(N): r2 = np.random.uniform(0, 2 * math.pi) r3 = np.random.uniform(0, 2) r4 = np.random.random() # Lévy步长缩放 L = alpha * levy_flight(1.5) * w if r4 < 0.5: new_pop[i] = pop[i] + L * r1 * math.sin(r2) * np.abs(r3 * best_pos - pop[i]) else: new_pop[i] = pop[i] + L * r1 * math.cos(r2) * np.abs(r3 * best_pos - pop[i]) # 边界修复 new_pop[i] = np.clip(new_pop[i], lb, ub) return new_pop

关于边界处理,我最初用的是"重新随机初始化"策略:代理飞出边界后,在搜索空间内重新随机生成位置。跑完发现结果很差,收敛曲线一直在跳。仔细分析发现原因:Lévy飞行的重尾特性会导致远跳频繁发生,如果飞出去就重新随机初始化,等于每轮都有大量代理被扔回起点,种群完全失去了连续搜索的势头,实际在"重新开局"而不是"搜索优化"。

换成"边界吸收"策略(即np.clip,超出边界的值直接截断到边界上)后,代理飞出去后会停驻在边界上,下一步可以从边界继续向内搜索,连续性保住了。实测在Rastrigin函数上,边界吸收比重新初始化提升了约两个数量级的收敛精度。

每个代理更新完后要立刻计算新位置的适应度,如果优于当前个体历史最优就更新。不要等到整个种群全部更新完再统一评估,否则会丢失"中途改进"的机会。整个迭代流程结束后,最终保存的全局最优位置和适应度就是SCASL的最终复现输出。

4. 复现中的关键实验与结果对照

4.1 跑实验:怎么设置随机种子才不会被人喷

关于随机种子,这是复现实验里最容易被忽视但最容易出问题的环节。我见过太多太随意的做法:每个函数固定用一个种子跑十次,取平均,然后宣称"算法优于对比对象"。这种实验设计有不少风险——环境因素、局部收敛、初始化偏差都可能让某一个种子恰好带来优势,其结果稳定性不足。

我在复现中坚持的规则是:每个函数、每个算法、30次独立重复,每次使用不同种子。种子从1到30连续编号,30个种子的范围内保证随机性,同时保证其他研究者能精确复现我的实验序列。

for func_name in benchmark_functions: for seed in range(1, 31): np.random.seed(seed) # 初始化种群、运行算法、记录结果

这个"30次独立重复"是论文级实验的及格线,不是一个可有可无的经验。标准差直接反映算法稳定性——如果SCASL的均值比SCA低但标准差反而大得多,说明它的性能提升是靠极少数运气好的运行拉起来的,不能简单下结论说"SCASL优于SCA"。

4.2 收敛曲线的正确打开方式

收敛曲线是复现论文时最直观的展示工具,也是用户最容易画错的地方。

我先踩过一个典型错误:直接取单次运行的最优适应度随迭代次数的变化画曲线。这条曲线毛刺很多,参考价值有限。后来改成画"30次运行的平均最优适应度"曲线,同时用透明色画出每次运行背后的个体曲线作为背景,视觉效果立刻清晰了,也更能说明算法的典型行为特征。

另一个坑是对数坐标。Sphere函数的适应度收敛过程可能跨越20个数量级,如果画线性坐标,前期的大数值会把后期的小数值全部压在接近0的位置,曲线看起来就像在第50次迭代后直接"撞到地板"了。解决办法很简单:纵轴用对数坐标。

plt.plot(mean_convergence, label='SCASL') plt.yscale('log')

但用对数坐标时要小心:如果算法有适应度为0的完美收敛(U型函数上可能发生),对数会报错。需要加一个小到忽略不计的偏移量,比如np.maximum(convergence_curve, 1e-300),避免最大值、最小值出现极端差异。(注:实际画图时通常np.log10(convergence + 1e-300)即可)

4.3 结果解读:好看不代表有效

数据跑出来了,SASL在Rastrigin、Griewank上的收敛精度平均比SCA提升了一到两个数量级,收敛曲线的下降速度也更快。但真正的挑战在Rosenbrock上——这是一个相当"平"的谷地,几乎水平的谷底使得收敛机制容易被浪费,数值改进不如其他函数立竿见影。

怎么解读这个现象?Rosenbrock最优解位于一条狭窄的抛物线形谷道中,谷道内部数值变化非常平缓,算法需要很长时间才能沿着谷道滑向最优点。Lévy飞行的"远跳"在这种地形上反而是负面的——一个远跳很可能直接跳出谷道,掉进两侧的高数值区域,然后被迫用大量迭代重新寻找谷道。这解释了为什么SCASL在Rosenbrock上最好成绩相比原版SCA优势不大,甚至在部分种子下更差。

另一个需要注意的是统计显著性。不同算法在30次重复中的均值差是否显著,靠肉眼不可靠。我在复现中用了Wilcoxon秩和检验来做统计对比,p值小于0.05才认定为显著改善。这不是学术界的八股文,而是防止自己误读实验结果的有效工具。

5. 复现时踩过的坑和排查思路

5.1 结果跟论文对不上?先从这几个地方查

复现论文最常见的困惑:看了论文里的表格,觉得不该是这个水平,或者和自己在别处看到的复现结果差距悬殊。这种时候,先检查几个系统性的隐患:

第一,搜索边界。很多基准函数的边界写错一位数,看起来影响不大,实际影响很大。比如Rastrigin的边界是[-5.12, 5.12],如果写成[-5, 5],搜索空间直接损失了2.3%,而且最优解位置不受影响,但陷入边缘局部最优的概率变了。

第二,迭代次数。论文里如果写了T=500,你跑T=1000,结果不会更好,反而可能更差。因为在某些多峰函数上,算法可能在前期已经找到比较好的局部区域,后期迭代在Lévy飞行的远跳下反而跳出好区域,最终收敛精度倒挂。跑实验前一定要先看论文的参数设置表。

第三,位置更新公式里的符号。SCA里 ( r_3 ) 乘的是 ( P_i^t ) 和 ( X_i^t ) 差的绝对值。如果你写成了 ( |r_3 \cdot P_i^t + X_i^t| ),算法行为会彻底改变,而且还不一定报错——只是收敛曲线略差一点,很容易漏掉。

5.2 Lévy飞行为什么不生效

我一开始跑出来的SCASL和原版SCA性能几乎一模一样,第一反应是"改进个寂寞"。排查后发现,我生成的Lévy步长太大,大多数代理一次远跳后就飞出边界,被边界修复拉回边界,然后又飞出去,形成"边界震荡"。

怎么排查?打印每一步的Lévy步长分布和远跳成功率(即跳出边界的代理数占比)。如果你的远跳成功率超过50%,说明 ( \alpha ) 缩放系数太小,Lévy步长被边界处理机制吞掉了,真实有效的远跳反而很少。

把 ( \alpha ) 从1.0调到0.01之后,远跳成功率降到了10%左右,收敛精度立刻上来了。这个细节是我复现过程中踩得最深也最有价值的坑之一——改进策略本身没问题,但策略和边界机制的交互方式不对,改进效果就被隐藏了。

还有一个隐蔽问题:我在生成Lévy步长时用了size=D一次性生成向量,然后每个代理的D个维度用同一个步长。这会破坏Lévy飞行的"各向异性"特性——真实Lévy飞行应该每个维度独立生成步长。改成每个维度都调用levy_flight()后,远跳成功率进一步改善。

5.3 收敛曲线像一条直线?大概率是"种群趋同"

早期实验中出现过一张很诡异的收敛曲线:前50次迭代下降很快,之后就变成一条死水平线,完全不动。查了代码发现,所有代理在几十次迭代后全部聚集到同一个小区域,种群多样性基本消失。此时Lévy飞行的远跳因为边界截断太频繁,已经无法产生有效的大步长探索,剩下的小步长在一个局部区域里互相映照,完全丧失了逃逸能力。

解决种群趋同,可以用两个办法:

一是给Lévy步长加一个"变异触发概率"。每次迭代以某个小概率(比如0.1)强制让几个代理跳过Lévy飞行更新,直接执行一个大幅度的随机重新初始化,给种群注入新鲜多样性。这相当于给算法加了"重置机制",和Lévy飞行的远跳构成了双层多样性保护。

二是增大 ( \alpha ) 的初始值。把 ( \alpha ) 设置成随时间递减的变量,前期大,后期小。前期充分探索,保持种群的分散度;后期步长变小,专注局部精修,也顺势缓解了趋同问题。

5.4 代码复现的及格线:指标表和曲线图怎么才算完整

很多人在社区里问"我这个复现算不算成功",得到最多回答是"看结果符不符合预期"。但这个标准很模糊。我自己总结了一套复现的验收清单:

  1. 每个基准函数至少跑30次独立重复,给出均值±标准差,只报一次最好的结果是耍流氓。
  2. 收敛曲线必须是对数纵轴,否则量级跨度大的函数完全看不出细节。
  3. 至少画一张多种算法的对比图,单独看单条曲线说明不了任何问题。
  4. 面对结果的偶然性,补充统计检验。复现代码最忌讳"晒一张最好的图就说有效",必须给出检验结果。
  5. 公布随机种子。其他人拿去跑,至少在统计水平上能复现你的结果,而不是只在某一颗特定的种子上成立。

这不是为难自己,而是只有在这种标准下复现,才能真正验证算法的改进点是否落在了实处,而不是巧妙地发生在某一次随机的正面偏差上。

6. 从复现到延伸:SCASL还能怎么玩

6.1 给SCASL做小改动的方向

复现SCASL不只是为了跑完基准函数交差,更大的价值在于后续扩展。我基于这个复现框架做了几个小改动,都收到了不错的效果:

一个是把 ( r_1 ) 的递减策略从余弦退火改成自适应反馈式递减——当种群多样性低时自动增大 ( r_1 ) 以重新激活探索。实现不难,就是在每轮迭代后计算种群个体之间的平均距离,归一化后作为多样性指标,反向映射到 ( r_1 ) 的调整系数上。

另一个改动是把边界处理从"吸收"改成"反射":飞出边界的代理按镜面反射方式折回搜索空间。这在带有狭窄可行域的实际问题上能显著提升边界附近的搜索密度。实测在Ackley函数上,反射处理比吸收处理的收敛精度提升了接近一个数量级。

6.2 和其他算法的混合使用

SCASL的Lévy飞行框架其实是一个很好的"全球探索器",我自己尝试过把SCASL和差分进化(DE)做混合:前期用SCASL负责全局勘探,后期切换DE的差分变异做局部开发。在CEC2017的部分函数上,混合策略比单独使用SCASL提高了约两个数量级的精度,代价是每轮迭代多了一次适应度评估。

这种"混合配方"的实践难度不高,关键是找到两种算法各自擅长的阶段。SCASL擅长多样性高时的大范围扫描,DE擅长在已定位的最优区域附近做精细的差分扰动。切换时机设在总迭代次数的60%附近通常是个不错的起点,具体需要针对函数族做微调。

6.3 应用场景:识别哪些实际问题适合SCASL

复现基准函数测试的最终目的是把算法迁移到实际工程问题上。SCASL适用场景的特征是:搜索空间高维、目标函数多峰、对全局最优的定位要求高。典型的方向包括:

特征选择问题就可以直接用SCASL——每个代理的位置向量代表一个特征子集选择方案,维度是原始特征数,适应度函数是分类器交叉验证精度减去特征数量的惩罚项。这跟基准函数测试时的维度、边界处理逻辑非常接近,迁移成本低。

还有一个我实测过的场景是无线传感器网络覆盖优化(属于网络规划类问题),可以把代理位置映射成传感器坐标,用SCASL查找目标区域覆盖率最大的部署方案。因为规划问题的解空间有大量局部最优,SCASL的全局搜索特性正好有用武之地。这类问题上,SCASL比原版SCA的收敛精度提升了约一个数量级——和基准函数上的趋势一致。

写在最后

复现SCASL这次经历,给我最深的感触是:改进型算法最容易被低估的价值,不在那个"改进点"本身,而在它逼着你去思考原版算法的边界失效在哪。如果你只看原版SCA的公式,很难直观体会到"线性递减 ( r_1 )"在后期究竟牺牲了什么;只有当你亲手把Lévy步长接进更新公式,被边界问题、趋同问题狠狠教育一遍之后,才会真正理解什么叫"探索与开发的平衡"。

如果你也在复现某个元启发式算法的改进变体,我的建议是:别急着优化结果,先把流程跑通,把每个子模块的行为用图表量化清楚。哪怕一开始结果很差,只要你能说清楚"差在哪个环节、为什么差",复现就已经成功了一大半了。后面那些调优手段,无非是在你理解的框架上叠加经验值而已。

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

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

立即咨询