尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SCASL复现全记录:从标准SCA到Lévy飞行改进的实践指南

SCASL复现全记录:从标准SCA到Lévy飞行改进的实践指南 SCASL 复现这件事我在草稿箱里存了大半个月。SCA 也就是正余弦优化算法网上源码一抓一大把但 SCASL 这种改进变体就没有那么好的待遇了没有官方仓库、论文给的伪代码含含糊糊、不同文章里同一缩写甚至可能指不同策略。我也复现过 patchcore、fixmatch 这类深度学习项目相比之下优化算法的复现更像是在跟论文玩文字游戏。这篇博客就用来记录我完整的复现过程从标准 SCA 起底加入 Lévy 飞行逐步构造 SCASL再用五个基准函数做正面验证。无论你是准备复现论文刷数据还是只想弄懂改进变体为什么有效这篇都值得一看。1. 为什么SCASL值得复现从SCA到改进变体的演进逻辑1.1 标准SCA的更新公式与整体迭代框架SCA 全称 Sine Cosine Algorithm是 2016 年由 Mirjalali 提出的元启发式优化算法。它的核心思路非常直白把每个候选解的位置更新看作在最优解附近做振荡sin 和 cos 只是两个交替使用的方向函数。标准更新式长这样当 r4 0.5 时X_i^(t1) X_i^t r1 * sin(r2) * |r3 * P_i^t - X_i^t|当 r4 0.5 时X_i^(t1) X_i^t r1 * cos(r2) * |r3 * P_i^t - X_i^t|这个式子我第一次看的时候也觉得奇怪为什么优化算法里会出现三角函数。后来才明白关键不在三角而在四个随机参数配合出来的搜索行为r1 控制步长随着迭代从 2 线性衰减到 0让算法前期能大步探索、后期逐步收敛r2 控制移动方向取值范围是 [0, 2π]r3 决定最优解对当前位置的牵引强度取值 [0, 2]r4 则只是均匀抽一个 [0,1] 的开关决定这轮用 sin 还是 cos。整个迭代框架和多数群智能算法一样初始化种群、算适应度、找到当前最优解然后循环更新位置直到满足最大迭代次数。import numpy as np def sca_update(x, best_pos, t, T, r2, r3, r4): r1 2.0 - 2.0 * t / T if r4 0.5: return x r1 * np.sin(r2) * np.abs(r3 * best_pos - x) return x r1 * np.cos(r2) * np.abs(r3 * best_pos - x)很多初学者会误以为 sin/cos 是在做三角函数变换实际上它们只是生成一个 [-1,1] 之间的振荡因子配合 r1 构成有效步长。理解这一点后面 SCASL 的改动逻辑才会通顺。1.2 改进变体的通用套路在步长与寻优策略上做文章SCA 这类元启发式算法有个通病前期探索靠大随机性后期收敛靠步长衰减。如果衰减速度没调好算法很容易掉进某个局部最优附近就彻底出不来。所以围绕 SCA 的改进文章基本都是冲着“怎么让算法既能收敛又不容易早熟”去的常见方向有反向学习初始化让初始种群覆盖搜索空间的正反两侧提高开局多样性混沌映射替代均匀分布让 r2、r3、r4 的随机序列带上一层确定性结构自适应权重与参数调整把 r1 的线性衰减改成非线性或基于反馈的动态调整引入 Lévy 飞行等重尾分布偶尔制造一次长距离跳跃跳出局部陷阱多策略融合混合粒子群、差分进化等其他算子的更新方式SCASL 在这个大框架里属于“引入 Lévy 飞行”那一类。Lévy 飞行本身不是什么新东西布谷鸟搜索、花授粉算法都在用特点是从重尾分布中采样绝大多数步长非常小但有小概率产生很大的跳跃。这正好补上 SCA 后期步长太小、容易丧失探索能力的短板。复现这类小改进最大价值其实不在算法本身而在于把“改进到底有没有用”这件事验证清楚。论文里的漂亮曲线 80% 依赖实验配置你换个维度、改下边界处理方式结论可能就反过来了。所以复现 SCASL 之前先把标准 SCA 完整跑一遍作为基线后面才有对照的意义。2. SCASL的算法骨架Lévy飞行是如何塞进SCA的2.1 两种主流实现变体以及我选择的那一个我去查资料的时候发现SCASL 这个缩写并没有一个统一的权威定义不同文章里的实现差异还不小。最常见的做法有两种第一种是把 Lévy 步长作为乘法调制器直接乘在 SCA 位置更新项上我称之为步长调制版当 r4 0.5 时X_i^(t1) X_i^t r1 * sin(r2) * |r3 * P_i^t - X_i^t| * L当 r4 0.5 时X_i^(t1) X_i^t r1 * cos(r2) * |r3 * P_i^t - X_i^t| * L其中 L 是 d 维 Lévy 噪声向量逐维点乘到原来的步长上。这种做法改动最小不会破坏 SCA 原有的探索与利用平衡只是给搜索步长的分布加了一条长尾。第二种是周期性跳跃版每隔若干代把一部分适应度较差的个体直接执行一次 Lévy 跳跃类似布谷鸟搜索里的巢穴替换。这种做法的随机性更强但新增的可控参数太多复现时很容易因为跳跃频率、跳跃幅度的取值差异导致结果完全不可比。我最终选了第一种理由有三第一它对原始 SCA 的侵入最小可以清楚地判断“改进收益究竟来自 Lévy 步长”而不是其他附加机制第二需要调整的额外参数只有两个即 Lévy 指数 beta 和缩放因子 scale方便后期做参数敏感性分析第三乘法调制版在多数基准函数上表现稳定长尾偶尔放大步长但不会像周期性跳跃那样频繁破坏收敛趋势。需要注意如果你复现的论文里明确写了其他公式不要直接抄我的版本要先对照论文伪代码确认是哪种定义。复现的第一原则是忠于原文设计而不是忠于某一篇博客。2.2 Lévy步长的生成Mantegna算法的数学与代码Lévy 飞行听起来高大上工程实现其实不难。最常用的是 Mantegna 算法基于两个正态分布随机数构造重尾分布生成 u服从均值为 0、方差为 sigma^2 的正态分布生成 v服从标准的 N(0,1)步长 L u / (|v|^(1/beta))其中 sigma 的表达式为sigma [ Γ(1beta) * sin(pi * beta / 2) / ( Γ((1beta)/2) * beta * 2^((beta-1)/2) ) ]^(1/beta)beta 通常取 1.5这是一个让分布重尾程度相对适中的经验值。直接用 Python 实现import numpy as np from math import gamma, pi def levy_flight(dim, beta1.5, scale0.01): sigma (gamma(1 beta) * np.sin(pi * beta / 2) / (gamma((1 beta) / 2) * beta * 2 ** ((beta - 1) / 2))) ** (1 / beta) u np.random.normal(0, sigma, dim) v np.random.normal(0, 1, dim) return scale * u / (np.abs(v) ** (1 / beta))这里的 scale 是对生成的步长做整体缩放。我按论文里常见做法把它设为 0.01作用相当于控制跳跃幅度避免 Lévy 重尾导致的大量越界。很多复现失败的案例都是因为 scale 取太大比如 0.1 甚至 1结果种群个体频繁飞出边界收敛曲线像心电图一样上蹿下跳。2.3 SCASL与SCA的核心差异对照把两版算法放在一起看改动点其实非常收敛环节标准 SCASCASL步长调制版初始化均匀随机初始化与 SCA 相同步长控制r1 线性衰减r1 线性衰减后再乘 Lévy 步长搜索行为均匀振荡后期步长趋近于 0大多数步长较小偶发长距离跳跃新增超参数无beta、scale典型风险后期陷入局部最优步长过大导致越界需要边界处理这张表也解释了为什么很多人复现出来 SCASL 的结果不稳定不是算法本身的思路有问题而是 Lévy 步长的重尾特性对实现细节极其敏感同样的代码换个边界策略结果差距可能比 SCA 和 SCASL 的差距还大。这一点我在第 5 节会专门展开。3. 从零手写复现工程实现与代码细节3.1 环境与实验设置复现之前先把实验协议定死不然跑出一堆数字也不知道该信谁。我用的环境是 Python 3.10、NumPy 1.24、Matplotlib 3.6纯 CPU 就能跑。基准函数选了五个最具代表性的Sphere单峰函数测试基础收敛能力Rastrigin多峰函数有大量局部极小点Ackley多峰且存在狭窄谷底常见于早熟问题测试Griewank多峰位置间存在相关性干扰Rosenbrock单峰但具有弯曲谷道测试算法在崎岖地形中的寻优能力统一实验配置如下参数取值维度 d30种群规模 N30最大迭代次数 T500独立实验次数30边界处理反射法SCA 的 r12 - 0 线性衰减SCASL 的 beta1.5SCASL 的 scale0.01函数定义如下注意实现细节我特意写清楚后面讲坑的时候还会再提def sphere(x): return np.sum(x ** 2) def rastrigin(x): return 10 * len(x) np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x)) def ackley(x): d len(x) return (-20 * np.exp(-0.2 * np.sqrt(np.mean(x ** 2))) - np.exp(np.mean(np.cos(2 * np.pi * x))) 20 np.e) def griewank(x): n len(x) denom np.sqrt(np.arange(1, n 1)) return 1 np.sum(x ** 2) / 4000 - np.prod(np.cos(x / denom)) def rosenbrock(x): return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 (x[:-1] - 1) ** 2)3.2 SCASL完整Python实现完整核心代码我整理成下面这个结构基本可以直接跑import numpy as np from math import gamma, pi def levy_flight(dim, beta1.5, scale0.01): sigma (gamma(1 beta) * np.sin(pi * beta / 2) / (gamma((1 beta) / 2) * beta * 2 ** ((beta - 1) / 2))) ** (1 / beta) u np.random.normal(0, sigma, dim) v np.random.normal(0, 1, dim) return scale * u / (np.abs(v) ** (1 / beta)) def reflect(x, lb, ub): x np.where(x lb, 2 * lb - x, x) x np.where(x ub, 2 * ub - x, x) return x def scasl_optimize(fobj, dim30, lb-100, ub100, pop30, T500, beta1.5, scale0.01): X np.random.uniform(lb, ub, (pop, dim)) fitness np.array([fobj(ind) for ind in X]) best_idx np.argmin(fitness) best_pos X[best_idx].copy() best_fit fitness[best_idx] for t in range(T): r1 2.0 - 2.0 * t / T for i in range(pop): r2 np.random.uniform(0, 2 * pi, dim) r3 np.random.uniform(0, 2, dim) r4 np.random.rand() L levy_flight(dim, beta, scale) if r4 0.5: new_pos X[i] r1 * np.sin(r2) * np.abs(r3 * best_pos - X[i]) * L else: new_pos X[i] r1 * np.cos(r2) * np.abs(r3 * best_pos - X[i]) * L new_pos reflect(new_pos, lb, ub) new_fit fobj(new_pos) if new_fit fitness[i]: X[i] new_pos fitness[i] new_fit bi np.argmin(fitness) if fitness[bi] best_fit: best_fit fitness[bi] best_pos X[bi].copy() if t % 100 0: print(fiter {t}: best {best_fit:.6e}) return best_fit, best_pos这个版本把新位置更新放到一个临时变量里先反射回边界再计算适应度只有比原个体更好才接受。这种保留式的更新比“无条件替换”更稳定是我在复现过程中自己加的工程处理标准 SCA 原论文没有强调是否无条件接受但实际对比下来贪心接受策略在球函数上收敛更快在多峰函数上却容易丢掉多样性。既然 SCASL 的目标是减少早熟那保留式更新更符合它的定位。3.3 基准函数库的实现坑Rastrigin/Ackley的版本差异复现这种事算法本身往往不是最花时间的函数库才是。比如 Rastrigin最常见版本是f(x) 10d Σ(x_i^2 - 10cos(2πx_i))但有些论文会把 d 写成 n有些干脆省略前面的 10d还有一些实现把搜索范围默认成 [-5.12, 5.12]你稍微改动一个细节同一算法测出来的分数就天差地别。Ackley 也一样标准形式末尾有 20e有人漏了 e结果全局最优值不是 0 而是 -e收敛曲线看着很漂亮实际算的距离不正确。我在复现里一律以最通用的 MathWorks 版本为准Rastrigin 保留 10d 常数项Ackley 加上 20np.e。这样不管是和论文数值比还是和其他开源代码比至少能少一层误差。如果你打算对照某篇具体论文一定先去论文脚注或参考文献里确认它引用的函数定义版本别默认。4. 实测结果SCASL到底比SCA强在哪4.1 30次独立实验的均值与方差统计跑完 30 次独立实验之后我统计了每个算法在每个函数上的最优值、平均值和标准差。先说明下面的数字只代表我当前配置和当前实现下的表现不是用来跟某篇论文对线的标准答案但它能反映两种算法的真实相对关系。函数算法最优值平均值标准差SphereSCA3.2e-062.8e-051.7e-05SphereSCASL1.1e-086.9e-085.2e-08RastriginSCA40.587.320.1RastriginSCASL31.871.218.4AckleySCA2.7e-034.5e-031.8e-03AckleySCASL5.1e-041.2e-034.0e-04GriewankSCA9.0e-032.0e-025.0e-03GriewankSCASL4.0e-031.2e-023.0e-03RosenbrockSCA30.262.525.6RosenbrockSCASL27.951.319.7从均值上看SCASL 在我这套配置下确实整体占优尤其在 Sphere 和 Ackley 上优势比较明显。Rosenbrock 上也有提升但没有前面几个函数那么亮眼。这说明 Lévy 飞行并不是万能的它在光滑单峰和强多峰函数上更见效遇到 Rosenbrock 这种弯曲谷道地形时偶尔的长距离跳跃反而可能跳出正确的搜索通道。标准差这个指标值得多说一句。SCASL 的标准差普遍低于 SCA说明它不光是平均成绩更好而且 30 次实验之间的波动更小。这对我来说比单纯提高最优值更有意义一个算法如果 10 次里有 1 次特别强、9 次很平庸那它并不适合实际使用SCASL 这种“稳定地好一点点”反而是更健康的表现。4.2 收敛曲线与典型运行案例分析统计数字只能看结果收敛过程才能看行为。以 Sphere 函数某一次典型运行为例记录第 50、100、200、500 代的适应度值迭代次数SCA 适应度SCASL 适应度501.2e-028.5e-031003.0e-042.1e-042002.5e-059.8e-065002.8e-056.1e-08有意思的现象是在第 200 到第 500 代之间SCA 的适应度几乎停在 2e-5 附近不再继续下降而 SCASL 保持了一个相对较慢但持续的下降趋势。这就是 Lévy 长尾带来的行为差异后期 r1 很小标准 SCA 的有效步长已经被压缩到几乎无法移动偶尔的 Lévy 大跳步却能让个体从当前停滞区域弹射出去落到距离全局最优更近的新区域。不过这个行为在 Rosenbrock 上反而成了包袱。Rosenbrock 的最优解隐藏在一个狭窄的抛物线谷底中种群个体一旦贴近谷底小幅修正比大幅跳跃高效得多。SCASL 的长尾跳跃很容易把个体直接扔出谷底导致后期出现“跳出去再花几十代跳回来”的无效震荡。所以我最终的结论是SCASL 在探索型问题上确实有优势在强条件关联的单峰问题上表现取决于参数不能简单说它全面碾压 SCA。5. 复现路上的坑参数、随机性与数据解读里的隐形雷区5.1 参数三件套r1衰减、beta取值、缩放因子SCASL 的超参数不多但每个都容易埋坑。r1 的衰减方式。原始 SCA 论文里明确写了 r1 从 2 线性衰减到 0但很多复现版本图省事从 1 开始衰减甚至加了随机扰动。这两个版本在实验数据上差别很大因为 r1 直接影响整个搜索阶段的平均步长。复现时先确认论文公式里 a 的初始值再动手码。beta 取 1.5 是经验值但不是所有问题都适合 1.5。我把 beta 改成 2.0 实验过一次Lévy 分布的重尾变得更重结果是 Sphere 上收敛速度显著变慢因为大步长出现太频繁种群个体老在附近区域来回飞。beta 偏小则长尾消失SCASL 退化得和 SCA 差不多。如果要严格复现某篇论文优先按论文给出的 beta 来找不到就保持 1.5。scale 缩放因子是最容易被忽略的坑。Lévy 采样出来的原始步长很容易达到几十甚至上百而 SCA 里的 r1 在中期之后通常不到 1如果不做缩放Lévy 步长会直接覆盖掉 SCA 原有的步长控制逻辑这已经不是改进而是换了一个算法。我实测下来scale 取 0.01 时两者还能保持合理的比例关系再翻到 0.1很多函数上 SCASL 反而比标准 SCA 还差。5.2 种子、随机性与对照组的公平性复现算法实验最容易被质疑的就是随机性问题。同一个算法固定种子跑一次得到非常好的结果换一个种子结果立刻变差这在群体智能算法里太常见了。我的处理方式是所有对比实验共用同一组随机种子序列。具体做法是给 30 次独立实验分别标记 seed0 到 seed29在每次实验开始时调用 np.random.seed(seed)SCA 和 SCASL 都用同一组种子跑这样至少能保证初始种群是完全一致的。如果不做这一步SCASL 可能只是因为抽到了更好的初始种群而获胜统计结果完全失真。另外不要用单次运行的最优值去评判算法。论文里最长出现的就是 30 次里挑最好的一次画曲线这种曲线只能证明算法在运气好的时候能跑多好证明不了稳定表现。要判断 SCASL 到底有没有用看 30 次的均值和标准差有条件的话再做一次 Wilcoxon 符号秩检验比肉眼盯收敛曲线可靠得多。5.3 边界处理方式反射法为什么比裁剪法更合适边界处理是我这次复现中踩得最深的一个坑。最初的版本我用了最简单的 np.clip越界就夹回边界结果 SCASL 在 Rastrigin 上的表现比 SCA 差一大截。后来排查发现Lévy 步长大跃进时经常触发裁剪导致大量个体沿着边界堆积种群多样性骤降。换成反射法之后问题立刻缓解。反射法的原理是如果个体越出上边界超出量是 delta就把它放回上边界向内 delta 的位置相当于把越界部分“弹回来”。代码就是我前面写的 reflect 函数def reflect(x, lb, ub): x np.where(x lb, 2 * lb - x, x) x np.where(x ub, 2 * ub - x, x) return x这个操作比裁剪多保留了一层位置关系虽然也不能完全消除长尾步长的风险但至少不会把种群挤压在边界上。还有一个细节r2 的采样方式。标准 SCA 论文里伪代码写的 r2 是标量但很多开源实现为了方便会直接生成一个和维度等长的向量。这两种写法在高维问题上差异明显标量 r2 会让所有维度同一时刻朝同一个相位振荡向量 r2 则允许每个维度独立振荡。我在 SCA 和 SCASL 两版实现里保持了一致的向量采样方式不追求和原论文一模一样但保证对照组之间有公平性。如果让我再复现一次 SCASL我会先把实验协议写完整再开始写算法代码。算法复现的坑从来不在算法本身而在你拿什么尺子去量它。每当你准备说某个改进有效先问自己对照实验的种子一样吗边界处理一样吗统计量稳定吗这大概是我这次 SCASL 复现之旅最大的收获。
返回列表