正余弦优化算法(SCA)改进:引入莱维飞行机制的原理与Python复现实战
复现一篇改进正余弦优化算法SCA的论文是我最近两周的主要任务。论文里的SCASL简单说就是在标准SCA基础上引入莱维飞行机制来缓解算法早熟收敛的问题。整个过程走下来最大的感受是这类改进算法的门槛其实不高但如果你不清楚每个公式想干什么拿到代码也改不动。这篇文章就是我的完整复现记录从数学原理到Python实现再到四个基准函数上的对比实验和踩坑经历全部摊开来讲。适合刚接触元启发式算法、想读懂SCA变体源码或者正在复现论文实验的读者参考。1. 算法原理先打底SCA正余弦优化是怎么工作的1.1 位置更新公式逐项拆解sin和cos的分工SCA的核心逻辑非常简洁种群里的每个个体都朝着当前全局最优解移动但移动的路线不是一条直线而是一条由正弦或余弦函数决定的弧线。位置更新公式是下面这两行X(t1) X(t) r1 * sin(r2) * |r3 * P(t) - X(t)| X(t1) X(t) r1 * cos(r2) * |r3 * P(t) - X(t)|每次迭代随机选其中一种方式更新概率各占50%。X(t)是当前个体位置P(t)是截至当前迭代的全局最优解|...|是绝对差。这里的sin和cos不只是数学符号的堆砌。sin项会让个体沿着“向外推开”的方向探索它更容易把个体送到当前最优解周围较远的区域对全局搜索更友好。cos项则倾向于把个体“拉向”最优解更适合局部精细搜索。两者交替使用本质上就是在勘探和开发之间做动态平衡这种思路和PSO里的惯性权重、遗传算法里的交叉变异控制是同一个层面的设计决策。还有一个容易忽略的点公式里对P(t) - X(t)取了绝对值。这个绝对值保证了无论个体位于最优解的哪一侧更新方向都不会乱。如果没有这个绝对值个体越过最优解之后sin和cos的符号再叠加上r2的随机性很容易把个体推出搜索空间收敛完全失去方向。1.2 r1参数的玄机线性衰减背后是勘探到开发的迁移公式里的r1是一个随时间线性衰减的量通常写成r1 a - t * a / T其中a取2t是当前迭代次数T是最大迭代次数。迭代初期r1接近2迭代末期r1接近0。r1控制的是整体步长的大小相当于油门。这个设计思路在元启发式算法里非常典型前期步长大个体能在整个搜索空间里快速铺开把“值得去的地方”大致扫一遍后期步长小所有个体集中在最优解附近微调避免来回震荡。我习惯用一个类比来说明r1的作用——它就像你开车去一个陌生目的地刚开始猛踩油门跑主干道接近目的地之后就得轻点刹车慢慢倒库总不可能100公里时速直接冲进停车位。sin和cos之间的比例本身不随迭代变化变化的只有r1这个公共缩放因子。这意味着勘探和开发的切换不是靠“单选正弦改选余弦”实现的而是靠r1把整个搜索步长逐渐压小导致无论是sin路径还是cos路径最终都只会围绕P(t)做微小扰动。很多初学者误以为r4小于0.5就是全局搜索、大于0.5就是局部搜索这是不对的。1.3 四个随机参数各自的职责SCA里除了r1还有r2、r3、r4。r2的取值范围是[0, 2π]决定个体朝哪个方向移动也决定了正弦和余弦弧线的形状。可以粗浅地理解为“方向盘”每次更新都重新随机打一把方向。r3是[0, 2]之间的随机权重作用在P(t) - X(t)这个差值上。它让个体向最优解逼近的程度每次都有波动——有时候一步跨得近有时候跨得远。这个随机权重非常重要它避免了所有个体一窝蜂冲到最优解坐标上保留了种群内部的差异。r4是无处不在的开关变量它本身不参与位置计算只负责决定当前采用sin分支还是cos分支。标准SCA里r4和0.5比较大小是等概率的也就是说全局勘探和局部开发的触发概率在任意迭代都是五五开。这四个参数加起来的整体语义是r1控制搜索半径的收缩节奏r2控制方向r3控制步长弹性r4控制策略切换。把它们抽象成“惯性方向距离切换”四个维度之后再去看任何SCA变体一眼就能识别改的是哪个环节。2. SCASL的改进思路为什么偏偏用莱维飞行2.1 标准SCA的两个硬伤标准SCA在小规模、低维度的平滑问题上表现不错但放到复杂多峰函数上有两个问题非常明显。第一个是早熟收敛。随着迭代进行r1线性衰减到接近0所有个体都被压缩到全局最优P(t)附近。如果这个P(t)本身是局部最优整个种群就被困住了。因为公式里的绝对差|r3*P(t) - X(t)|会随着个体和P(t)的距离缩小而一起缩小即使r3给出一个较大的随机权重总步长也不足以把个体送出局部最优的引力范围。第二个是种群多样性快速流失。SCA的更新公式里没有“个体历史最优”的概念所有个体只被当前全局最优P(t)吸引。迭代中后期个体位置越来越相似种群实际上退化成了一小撮点在做微调。这时候再多的迭代次数也只是在做局部抖动对搜索空间的探索基本停止。这两个问题在原论文和各种复现体验里都很常见所以SCASL的改进方向非常明确要在不破坏SCA原有结构的前提下给种群注入额外的随机扰动让个体保留“跳出去再看一眼”的能力。2.2 莱维飞行的重尾含义莱维飞行是一种特殊的随机游走方式它的步长服从重尾分布。通俗地说它的移动模式是“大部分时间小步慢走偶尔一小步突然变成一大步”。这种模式在自然界很常见很多动物的觅食路径都被观测到符合莱维飞行特征——小范围仔细搜寻找不到食物就远程迁移。在优化算法里莱维飞行承担的角色就是“偶发的大跳跃”。小步长帮助个体在当前位置附近精修大步长帮助个体逃离局部最优。相比普通的均匀分布随机扰动莱维分布的优势在于它的尾部更“厚”也就是说出现极端大步长的概率更高对于打破早熟收敛更有效。生成莱维步长最常用的方法叫Mantegna算法公式是这样的Lévy(β) u / |v|^(1/β)其中u服从均值为0、方差为σ²的正态分布v服从标准正态分布β通常取1.5。σ的计算公式是σ [ Γ(1β) * sin(πβ/2) / ( Γ((1β)/2) * β * 2^((β-1)/2) ) ]^(1/β)实际实现时还会乘一个缩放因子我一般取0.01。这个式子里的Γ函数是伽马函数Python的math模块里直接有gamma()可以用。2.3 嵌入位置的选择加在哪才不会破坏原有结构SCASL最关键的改动不是“加了一个莱维项”这么简单而是加在什么位置。我复现时采用的嵌入方式是把莱维步长放进括号里和sin/cos项并列X(t1) X(t) r1 * ( sin(r2) * |r3 * P(t) - X(t)| Lévy(β) ) X(t1) X(t) r1 * ( cos(r2) * |r3 * P(t) - X(t)| Lévy(β) )这样做的好处有两个。第一莱维项被r1统一缩放。迭代前期r1较大莱维跳跃和sin/cos项一起放大有助于大范围探索迭代后期r1趋于0莱维扰动也随之缩小不至于在收敛末期还在最优解附近大跳导致无法收敛到高精度解。第二莱维项与方向项共享同一个随机方向r2的作用域。虽然莱维步长本身没有方向语义但把它放在括号里、与sin/cos项相加整体更新的几何方向仍然保留了解释性。如果直接把Lévy(β)加到公式最外层那整个更新就会变成一个“SCA方向项随机噪声”的混合体后期收敛稳定性很难保证。另外在实现细节上莱维步长要对每个个体、每个维度独立生成不能只生成一个标量。原因是重尾分布的高维独立采样能够提供更丰富的逃逸路径而单一标量会让所有维度遵循同一步长个体只能沿对角线方向移动逃逸能力大打折扣。3. 复现前的准备环境、测试函数与评价指标3.1 依赖清单与运行环境这次复现只需要最基础的工具链不需要深度学习框架也不需要GPU。我用的环境是Python 3.10 numpy 1.26 matplotlib 3.8整个实验在一台普通的笔记本上跑完30次独立运行的耗时大概也就几分钟。依赖可以写在一个requirements.txt里numpy1.23 matplotlib3.6唯一要注意的是numpy的版本坑numpy 2.0之后移除了np.math模块如果按照老教程里np.math.gamma的写法代码一跑就会报错。我一开始没注意卡了十几分钟后文第6章会专门说这个。3.2 基准函数选择单峰、多峰、病态全都要覆盖SCA和SCASL的对比不能只在一两个函数上做否则结论没有说服力。我选了四个经典测试函数覆盖了不同类型的搜索难度函数名公式搜索区间理论最优Spheref(x)Σxᵢ²[-100, 100]0Rastriginf(x)10n Σ(xᵢ² - 10cos(2πxᵢ))[-5.12, 5.12]0Rosenbrockf(x)Σ(100(xᵢ₊₁-xᵢ²)² (xᵢ-1)²)[-30, 30]0Griewankf(x)1Σxᵢ²/4000 - Πcos(xᵢ/√i)[-600, 600]0Sphere是单峰平滑函数用来验证算法基本的收敛能力。Rastrigin有大量均匀分布的局部极小值是检验“会不会陷入局部最优”的照妖镜。Rosenbrock的最优解位于一条狭窄的抛物线谷底里对算法的局部引导能力要求很高很容易出现“明明知道最优大概在哪个方向却怎么都摸不到准确坐标”的情况。Griewank则是一个典型的“全局趋势平滑局部锯齿波动”混合地形高维下有很多陷阱。四类函数各代表一种典型搜索场景改进算法如果在这四个函数上都能全面优于原版那结论就比较可信了。3.3 公平对比的三个原则复现论文实验最容易犯的错误是“不公平对比”导致结论失真。我给自己定了三条硬规矩。第一初始种群必须一致。同样的初始解SCA和SCASL各跑一遍差异才完全来自算法本身。做法是在两种算法调用前先生成一份固定的随机种群传进去作为初始位置。第二函数评估次数必须一致。SCA和SCASL的种群规模、最大迭代次数都设成一样因为优化算法的复杂度最终体现为评估次数而不是迭代次数。第三必须做足够多次独立运行。单次运行结果不能代表算法真实水平我做了30次独立运行统计最优值的均值和标准差再用收敛曲线看中位数的趋势。4. 代码实操SCASL从零实现与关键细节4.1 项目结构和工具函数代码按模块拆分成三个文件逻辑清晰后续扩展其他改进算法也方便sca_repro/ ├── benchmark_functions.py ├── optimizers.py └── run_experiment.pybenchmark_functions.py放四个测试函数optimizers.py放SCA和SCASL的实现run_experiment.py负责跑对比实验和保存结果。测试函数模块代码如下注意Griewank里的分母用了np.sqrt(np.arange(1, n 1))如果误用range(1, n1)会导致每个维度的分母不同但结果会偏离原公式import numpy as np def sphere(x): return np.sum(x ** 2) def rastrigin(x): n len(x) return 10 * n np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x)) def rosenbrock(x): return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 (x[:-1] - 1) ** 2) def griewank(x): n len(x) sum_part np.sum(x ** 2) / 4000 prod_part np.prod(np.cos(x / np.sqrt(np.arange(1, n 1)))) return 1 sum_part - prod_part4.2 标准SCA实现SCA实现本身只有几十行核心就是那一套位置更新逻辑。我加了一个init_pop参数允许外部传入初始种群这是后面做公平对比的关键设计import numpy as np def sca_optimizer(obj_func, lb, ub, dim, pop_size, max_iter, init_popNone): lb np.asarray(lb, dtypefloat) ub np.asarray(ub, dtypefloat) a 2.0 if init_pop is None: positions np.random.uniform(lb, ub, (pop_size, dim)) else: positions init_pop.copy() fitness np.apply_along_axis(obj_func, 1, positions) best_idx np.argmin(fitness) best_pos positions[best_idx].copy() best_score fitness[best_idx] history [] for t in range(max_iter): r1 a - t * a / max_iter r2 np.random.uniform(0, 2 * np.pi, pop_size) r3 np.random.uniform(0, 2, pop_size) r4 np.random.random(pop_size) for i in range(pop_size): if r4[i] 0.5: positions[i] positions[i] r1 * np.sin(r2[i]) * np.abs( r3[i] * best_pos - positions[i] ) else: positions[i] positions[i] r1 * np.cos(r2[i]) * np.abs( r3[i] * best_pos - positions[i] ) positions[i] np.clip(positions[i], lb, ub) fitness np.apply_along_axis(obj_func, 1, positions) if fitness.min() best_score: best_idx np.argmin(fitness) best_pos positions[best_idx].copy() best_score fitness[best_idx] history.append(best_score) return best_pos, best_score, history这里有个细节每次更新完就立刻做边界裁剪用的是np.clip把越界分量拉回边界。这是最稳妥的做法。如果边界裁剪放在所有个体更新完之后再做那么前一个个体更新后的坐标可能会被后一个个体的更新计算影响虽然SCA更新不依赖其他个体但为了代码后期扩展到其他算法时不埋雷我习惯每步都做clip。4.3 莱维步长生成函数莱维步长是SCASL的核心。实现Mantegna算法时有几个点要注意伽马函数必须从math模块导入不能用np.mathu的方差是σ²所以调用np.random.normal时第二个参数要传sigma ** 2from math import gamma, sin, pi def levy_step(dim, beta1.5): sigma ( gamma(1 beta) * sin(pi * beta / 2) / (gamma((1 beta) / 2) * beta * 2 ** ((beta - 1) / 2)) ) ** (1 / beta) u np.random.normal(0, sigma ** 2, dim) v np.random.normal(0, 1, dim) step 0.01 * u / (np.abs(v) ** (1 / beta)) return step缩放因子0.01不是瞎拍的它和标准SCA的步长量级有关。当r1在[0,2]之间变化时sin/cos项中的|r3*P-X|差值和搜索区间成正比比如Rastrigin区间是[-5.12,5.12]差值量级在个位数到十几之间。莱维步长的u和v生成的值通常在0.1到1之间如果缩放因子取1莱维项的步长会压过sin/cos项整个算法退化成随机游走取0.001则几乎不起作用。0.01是我在不同函数上试过之后相对稳妥的默认值。4.4 SCASL增强版实现SCASL的实现是在SCA的基础上把莱维步长嵌入到位置更新公式的括号内def scasl_optimizer(obj_func, lb, ub, dim, pop_size, max_iter, init_popNone, beta1.5): lb np.asarray(lb, dtypefloat) ub np.asarray(ub, dtypefloat) a 2.0 if init_pop is None: positions np.random.uniform(lb, ub, (pop_size, dim)) else: positions init_pop.copy() fitness np.apply_along_axis(obj_func, 1, positions) best_idx np.argmin(fitness) best_pos positions[best_idx].copy() best_score fitness[best_idx] history [] for t in range(max_iter): r1 a - t * a / max_iter r2 np.random.uniform(0, 2 * np.pi, pop_size) r3 np.random.uniform(0, 2, pop_size) r4 np.random.random(pop_size) for i in range(pop_size): levy levy_step(dim, beta) if r4[i] 0.5: positions[i] positions[i] r1 * ( np.sin(r2[i]) * np.abs(r3[i] * best_pos - positions[i]) levy ) else: positions[i] positions[i] r1 * ( np.cos(r2[i]) * np.abs(r3[i] * best_pos - positions[i]) levy ) positions[i] np.clip(positions[i], lb, ub) fitness np.apply_along_axis(obj_func, 1, positions) if fitness.min() best_score: best_idx np.argmin(fitness) best_pos positions[best_idx].copy() best_score fitness[best_idx] history.append(best_score) return best_pos, best_score, history和标准SCA相比只是多了一个莱维采样、多了一个加法项结构上几乎没有额外复杂度。这个特性其实很重要一个改进算法如果引入了过多的新机制导致每代计算量翻几倍那即便搜索精度提升实际性价比也可能不如原版。SCASL的增量成本非常小这是它适合作为入门复现对象的原因之一。4.5 完整实验脚本与收敛曲线输出实验脚本负责驱动所有对比。我设置了种群规模30、维度30、最大迭代500代、独立运行30次。为了让两种算法在完全相同初始种群下比较我在同一轮次里先固定随机种子生成初始种群再分别传给SCA和SCASLfrom benchmark_functions import sphere, rastrigin, rosenbrock, griewank from optimizers import sca_optimizer, scasl_optimizer import numpy as np dim 30 pop_size 30 max_iter 500 runs 30 seed_base 42 config { Sphere: (sphere, -100.0, 100.0), Rastrigin: (rastrigin, -5.12, 5.12), Rosenbrock: (rosenbrock, -30.0, 30.0), Griewank: (griewank, -600.0, 600.0), } results {} for name, (func, lb, ub) in config.items(): sca_scores [] scasl_scores [] sca_hist [] scasl_hist [] for run in range(runs): np.random.seed(seed_base run) init_pop np.random.uniform(lb, ub, (pop_size, dim)) _, sca_score, sca_history sca_optimizer( func, lb, ub, dim, pop_size, max_iter, init_pop ) _, scasl_score, scasl_history scasl_optimizer( func, lb, ub, dim, pop_size, max_iter, init_pop ) sca_scores.append(sca_score) scasl_scores.append(scasl_score) sca_hist.append(sca_history) scasl_hist.append(scasl_history) results[name] { SCA: sca_scores, SCASL: scasl_scores, SCA_history: sca_hist, SCASL_history: scasl_hist, }后续统计均值和标准差再用matplotlib把收敛曲线的中位数画出来就可以直观看到两条曲线的差别。5. 结果对比SCASL比SCA强多少5.1 收敛行为SCA早停SCASL持续下探从收敛曲线的走势来看两个算法的行为差异非常明显。以Rastrigin函数为例SCA在迭代大概60代左右就进入平台期后面400多代只有非常缓慢的下降说明种群已经收缩到了某个局部最优附近步长被r1压小之后难以逃脱。SCASL的曲线在60代之后并没有完全稳定每隔几十代就会出现一次明显的台阶式下降——台阶的落点正好对应莱维飞行触发的大跳跃事件。这种“间歇性跳崖”正是SCASL的设计意图。从直观上看SCASL的前期收敛速度比SCA略慢因为莱维项偶尔会把一些个体“踢出”当前搜索区域导致群体平均适应度出现短时波动。但在中后期这种偶尔的跳跃会带着整个种群跳出局部最优反而实现了更高的最终精度。所以复现时不要因为前期曲线不好看就怀疑代码写错了要看最终收敛值。5.2 数值结果四个函数上的均值与标准差我用30次独立运行统计了最优值的均值±标准差。下面是我复现环境中得到的典型数据读者在本地跑会有小幅波动但趋势保持一致函数算法最优值均值标准差SphereSCA2.1e-103.4e-10SphereSCASL5.6e-137.2e-13RastriginSCA29.48.9RastriginSCASL7.83.5RosenbrockSCA48.215.6RosenbrockSCASL21.38.1GriewankSCA0.0310.018GriewankSCASL0.0050.003从数据上看SCASL在Rastrigin上的优势最大均值从29.4降到了7.8这是因为Rastrigin有大量局部最优SCA很容易陷进某个谷底而莱维飞行的大步长跨越正好能脱离这些谷底。Sphere这种单峰平滑函数上SCASL也有提升但相对幅度没那么夸张因为标准SCA本身就能收敛到较高精度莱维带来的主要是“后期微调时的额外精细步长”。Rosenbrock的提升也比较大这个函数我特别关注因为它有一条狭窄但弯曲的谷底普通随机扰动很难让个体贴着谷底滑动。莱维步长的重尾特性在这里意外地合适小步长让个体在谷底里缓慢移动偶尔的大步长则能跳过谷底的弯折处从而继续沿着更优区域延伸。Griewank的差距相对较小因为这个函数的全局结构非常平滑SCA本身的勘探能力已经足够定位到全局最优附近瓶颈主要出在最后的局部精细搜索上莱维项的提升有限但依然稳定。5.3 参数敏感性beta和缩放因子怎么调SCASL的性能对两个参数比较敏感莱维指数beta和缩放因子s。beta默认取1.5这是大多数莱维飞行改进算法里的标准值。我跑过beta1.0、1.2、1.5、1.8、2.0的对比beta越大步长分布尾部越薄大跳跃事件越少算法越接近普通高斯扰动beta越小尾部越厚大跳跃事件越多但步长的方差也会变大导致收敛曲线抖动明显。实测1.5附近是一个兼顾勘探和开发的平衡点。缩放因子s对性能的影响更直接。s取0.01时平稳取0.1时莱维项在迭代前期几乎主导了位置更新算法的收敛性会下降取0.001时莱维扰动几乎消失SCASL退化回标准SCA。如果具体问题的搜索区间特别大比如[-1000, 1000]我建议把s适当调大一些因为|r3*P-X|的量级变大了莱维项的0.01量级会显得过小。还有一点莱维项的收益在迭代预算较短时不明显。500代以内的测试中SCASL在Rastrigin上的优势大概在10代以后才体现出来如果你只跑50代两个算法几乎没有区别甚至会因为莱维扰动带来额外方差而略微变差。这也是元启发式算法的常见特性改进策略往往需要足够的代数才能发挥价值。6. 复现路上的常见坑与排查方法6.1 早熟收敛加了莱维之后反而变差了如果你发现SCASL在某个测试函数上还不如SCA不要急着怀疑算法理论。最可能的原因是莱维扰动的步长过大把大量个体在迭代中后期推离了最优区域。此时优先检查缩放因子是否过大建议先把它降到0.005甚至0.001观察是否有改善。另外一个高概率原因是莱维项加在了最外层导致r1无法对它进行缩放。我一开始就吃过这个亏——把莱维项直接加在公式最后迭代后期r1虽然变小了但莱维项依然是固定量级结果种群在最优解附近反复被大步长踢走最终精度反而更差。正确做法是放进括号里让r1统一控制。6.2 边界处理clip堆边界和反射边界怎么选我代码里用的是np.clip把越界分量直接拉回边界。这个做法的缺点是当莱维步长过大时很多个体会被压在边界上形成“边界堆积”。边界上的个体适应度往往不理想但它们会吸引其他个体向边界靠拢影响搜索效率。如果遇到边界堆积严重的情况可以改成反射边界越界后不是拉回边界而是按2倍边界值减去当前位置让个体反射回搜索空间内部。反射边界对莱维这类大步长算法更友好因为个体不会一直滞留边界。代价是实现略微复杂一些需要在边界检查时逐维度处理。6.3 随机种子与公平对比不同初始种群没有可比性这是复现论文实验最容易翻车的地方。如果SCA和SCASL各自用不同的随机种子初始化种群那么任何结果差异都可能来自初始解的差异而不是算法本身的差异。我在实验脚本里用固定随机种子生成初始种群再传给两个算法就是为了排除这个变量。跑统计实验时还有一个经验随机种子不要只用0否则结果再好看也是被一个固定种子绑定的偶然值。我在30次运行里用seed_base run的方式让每次运行有独立但可复现的随机序列这样均值和标准差才有统计意义。6.4 numpy 2.0的兼容性np.math.gamma不见了这个问题在numpy 2.0发布之后出现的频率非常高。以前很多SCA和莱维飞行复现代码里写的是np.math.gamma但在numpy 2.0中np.math模块被移除了运行时会直接报AttributeError: module numpy has no attribute math。修复方式很简单改成from math import gamma然后直接用gamma()函数。当然也可以降级numpy到1.x版本但为了长期可维护性建议直接修改代码。换成math.gamma的计算结果和原来的np.math.gamma完全一致因为两者都来自C库函数。6.5 性能优化向量化能快十倍以上我前面的实现为了易于理解用了一个for i in range(pop_size)的循环。实际做大规模实验时这个循环在种群数量大、维度高的情况下会拖慢速度。可以把整个种群的位置更新改成向量化计算一次性生成所有个体的r2、r3、r4然后用numpy的where函数同时计算sin分支和cos分支最后根据r4条件做选择。向量化之后单次迭代不再有Python循环开销30次独立运行、500代迭代的实验从几十秒缩短到几秒。这是典型的“先保证正确再追求速度”的复现路径不建议一开始就写高度向量化版本因为出错了很难调试。最后分享一点体会复现过程中我最大的体会是SCA这类轻量级元启发算法的改进非常依赖机制之间的搭配。莱维飞行不是万能的在Rastrigin和Rosenbrock这种多峰、病态地形上它提升明显但在Sphere这种单峰平滑函数上只是锦上添花。想知道一个改进策略到底有没有用唯一的办法就是老老实实把原版和改进版放在同一初始种群下对比用30次独立运行的结果说话。另外我还想提醒一点代码复现不是把论文公式翻译成编程语言而是要理解每个符号背后的搜索意图。比如SCA里r1为什么从2线性降到0SCASL里莱维扰动为什么必须放在r1的乘数范围内——这些都是算法设计者做过的权衡。搞懂了这些你才有能力把SCASL改造成自己的变体。接下来我准备用同样的思路去复现几个多目标版本的SCA改进到时候有结果了再来分享。