
1. 先把SCA基线吃透再谈SCASL复现1.1 正余弦优化算法到底在做什么想复现任何一篇改进算法的论文第一步永远是先把基线算法本身掰开揉碎。正余弦优化算法Sine Cosine AlgorithmSCA是 Mirjalali 在 2016 年提出的一种元启发式优化算法核心思路特别直白用正弦函数和余弦函数的周期波动让种群在解空间里来回震荡既保证前期有足够大的探索范围又通过参数控制让后期逐渐收敛到局部精细区域。它本质上是一种基于种群的随机搜索算法不需要梯度信息所以非常适合那些目标函数不可导、不连续、甚至黑箱的优化问题。工程上经常拿它来做PID参数整定、无线传感器网络覆盖优化、特征选择这类组合爆炸或高维连续优化问题。这一点和粒子群、差分进化是同一类工具但更新公式要比它们简单不少。基础更新公式长这样x_i^(t1) x_i^t r1 * sin(r2) * |r3 * P_i^t - x_i^t| r2 0.5 时x_i^(t1) x_i^t r1 * cos(r2) * |r3 * P_i^t - x_i^t| r2 0.5 时其中x_i^t 是第 i 个个体的当前位置P_i^t 是当前全局最优位置或者某个引导解r1 控制步长通常随迭代次数线性递减前期大、后期小r2、r3、r4 是 [0,1] 之间的随机数分别决定方向选择、目标参考点权重和 sin/cos 切换。每次迭代里每个个体独立决定自己是往正弦方向还是余弦方向走一步。由于 sin 和 cos 的值域是 [-1,1]所以个体不会无限飞出搜索边界而会在 r1 的衰减作用下逐渐稳定下来。伪代码里这个流程非常像“弱化版粒子群”初始化种群 → 评估适应度 → 记录全局最优 → 按概率用 sin 或 cos 更新每个个体 → 处理边界 → 更新参数 → 重复直到满足终止条件。我复现 SCA 的时候最大的一个感受是这个算法看起来简单但直接把论文里的公式抄过来跑结果往往和你想的差很远。原因在于 r1 的递减策略、边界处理方式、概率阈值这些“隐藏设定”在不同论文里差别很大而这些恰恰是影响收敛行为的关键。1.2 基线算法的两个致命短板SCA 发表之后有大量改进版本为什么因为它有两个问题太明显了。第一早熟收敛。r1 一旦线性递减前期的探索能力会以固定速率衰减如果算法在早期没有找到一个好的“盆地”后面基本就困在原地打转了。特别在高维多峰函数上比如 Rastrigin、Griewank 这类目标函数SCA 经常会出现“种群一动不动、最优解几十几百代都不更新”的情况。第二种群多样性不足。所有个体都在朝向当前全局最优靠拢一旦全局最优是个局部陷阱整个种群很容易被“吸”过去。你可以把种群想象成一群人往一个亮光处跑跑着跑着发现那个亮光是陷阱但回头已经来不及了周围全是同类。正是因为这两条学术界后来出了大量 SCA 变体融合差分进化、融合混沌映射、融合反向学习、融合柯西变异等等。SCASL 就是其中一个值得复现的方向——它把透镜成像折射学习机制引入 SCA目的是在不显著增加计算量的前提下通过生成“反向解”来维持种群多样性、跳出局部最优。我个人的建议是在接触 SCASL 之前先把 SCA 在至少四个标准测试函数上跑通亲手画一画收敛曲线观察一下早熟现象这样后面改进的效果才有对照。否则你连“改没改好”都判断不了。2. SCASL 的改进点到底改了什么2.1 透镜成像折射学习反向解的新玩法SCASL 里的“SL”一般对应 Spiral / Lens 这类改进机制复现时最核心、也最好实现的一个点是“透镜成像折射学习”Lens Opposition-Based Learning缩写经常是 LOBL 或 Refracted Opposition-Based Learning。说人话就是传统“反向学习”Opposition-Based Learning的做法很简单就是求当前解关于搜索空间中心点的对称点比如搜索范围是 [0, 10]当前点是 2反向点就是 8。这个策略的好处是给种群提供一次“反向扫描”的机会但它的问题是太死板对称点永远在正中间附近打转缺乏灵活性。透镜成像折射学习把这件事推广了一下。想象一个凸透镜物体在透镜左侧像呈现在透镜右侧当透镜的折射率改变时像的位置也会移动。用公式表达就是x_ref (ub lb) / 2 (ub lb) / (2 * k) - x / k其中ub、lb 是搜索空间的上下界x 是当前个体位置k 是透镜折射缩放因子k 不同时反向解的分布范围完全不同。这个公式的直观意思是反向解不是简单地关于中心点对称而是会根据 k 的值在中心点附近“收缩”或“外扩”。k 越大反向解越靠近搜索中心k 越小反向解越靠近边界。所以透镜成像折射学习比传统反向学习灵活得多你可以通过控制 k 让反向解有一部分落在当前种群聚集区域的外部从而真正起到维持多样性的作用而不是简单造一堆与当前解对称的孪生点。2.2 SCASL 的完整算法流程SCASL 不是一个像标准 SCA 那样有唯一“官方源码”的固定算法文献里的变体命名方式很随意。复现时我采用了一种常见且效果稳健的组合策略SCA 透镜成像折射学习 自适应参数控制。核心流程如下初始化种群规模 N、最大迭代次数 MaxIter、维度 Dim、搜索边界初始化每个个体的位置评估适应度记录全局最优解迭代循环更新控制参数 r1自适应递减通常写成 a 2 - t * (2 / MaxIter)r1 a对每个个体计算 r2、r3、r4 随机数根据 r4 的值选择 sin 更新还是 cos 更新更新个体位置处理越界个体每隔一定代数或者以一定概率对部分个体执行透镜成像折射学习生成反向解比较反向解与原解的适应度保留更优者更新全局最优迭代结束返回最优解和收敛曲线。这里最关键的两个设计点是什么时候触发折射学习以及折射学习的 k 如何取值。我的做法是每个个体每一代都以 0.3 的概率触发触发后生成反向解并与自身比较如果反向解更好就替换。k 取值我会在实验里反复测一般固定为 0.6 或 1.5 都有不错表现但针对不同测试函数最优的 k 有差异这一点后面细说。自适应参数控制方面除了 r1 递减外我还给 k 设计了一个动态变化策略迭代早期 k 较大让反向解集中在中心附近帮助快速搜索后期 k 逐渐减小让反向解偏向边界增加跳出局部最优的机会。这个策略在部分函数上提升显著但在另外一些函数上反而拖慢收敛所以你自己复现的时候最好把固定 k 和动态 k 都写出来做对比。2.3 为什么这个组合能提升效果和标准 SCA 相比SCASL 本质上只多了一步“低成本的反向解采样”但它解决的核心问题是“种群被局部最优困住后没有出口”。标准 SCA 的更新机制里个体始终围绕当前最优解波动如果这个最优解只是局部最优种群很难凭自身力量逃出去。而透镜成像折射学习让每个个体都有一个概率被“弹射”到一个和当前位置完全不同的区域弹射方向还带随机性。哪怕这个反向解不一定更好只要保留了它就相当于保留了一条新的搜索路径。打个比方一支队伍在山里找最高峰SCA 的做法是只看眼前的方向往前走走到某个山头就认为到顶了SCASL 的做法是每隔一段时间派几个人直接空降到山的另一侧看一眼如果那边更高就换目标。空降的费用不高但很可能改变全局结果。不过这里也有个容易走偏的误区不是反向解生成得越多越好。如果你每一代强制让所有个体都生成反向解种群就会频繁在两个极端位置之间横跳原有的精细收敛过程被打断最终精度反而会下降。所以触发概率是一个需要调的超参数我的建议是先从 0.2 到 0.5 之间试一组不要拍脑袋定。3. Python 复现全过程从工程结构到实验结果3.1 工程结构与环境准备复现这类算法不需要依赖深度学习框架一个 numpy 加 matplotlib 就够了。我用的环境是 Python 3.9、numpy 1.24、matplotlib 3.7IDE 用的 VS Code。工程目录如下sca_reproduction/ ├── sca_base.py # SCA 基线算法实现 ├── scasl.py # SCASL 改进算法实现 ├── benchmark.py # 标准测试函数集 ├── run_experiment.py # 批量实验入口 ├── plot_results.py # 收敛曲线绘图 └── results/ # 实验输出目录建议从开始就保持这种模块化结构不要把所有代码塞在一个文件里。因为后面要跑多次对比实验代码拆开以后改参数、加算法、加测试函数都方便得多也不容易出现“改了一个函数影响到另一个实验”的问题。用 numpy 实现就够的重要原因是SCA 和 SCASL 的迭代过程通常不涉及复杂矩阵运算向量化写法的收益有限而清晰的循环逻辑更容易排查问题。等基线跑通、结果稳定了再考虑向量化加速也不迟。3.2 测试函数与超参数设置复现论文实验时标准测试函数是少不了的。我选了四个最常用、也最能体现算法差异的函数函数名表达式搜索范围最优值Spheref(x) Σ x_i²[-100, 100]0Rastriginf(x) Σ (x_i² - 10 cos(2π x_i) 10)[-5.12, 5.12]0Griewankf(x) 1/4000 Σ x_i² - Π cos(x_i / √i) 1[-600, 600]0Ackleyf(x) -20 exp(-0.2 √(1/n Σ x_i²)) - exp(1/n Σ cos(2π x_i)) 20 e[-32, 32]0四个函数的特性差异很大Sphere 是单峰函数检验收敛精度Rastrigin 是典型多峰函数局部最优极多检验跳出能力Griewank 在高维时比较平缓检验探索效率Ackley 有多个局部极小检验平衡能力。超参数方面我按常见设置来种群规模 N 30维度 Dim 30最大迭代次数 MaxIter 1000这样总适应度评估次数更接近论文常见水平每个实验独立运行 30 次统计均值和标准差。SCASL 里折射学习触发概率 p_lens 0.3透镜缩放因子 k 1.5固定对比组还会测试动态 k 方案。有一个细节要注意不同测试函数的搜索范围差异极大从 [-5.12, 5.12] 到 [-600, 600]如果直接用同一套边界处理逻辑可能会让部分函数上的种群在初始化时过早聚集在边界附近。我复现时针对每个函数单独调整了边界处理策略后面会详细说明。3.3 SCA 基线的核心代码我先把标准 SCA 写出来注意代码里我刻意保留了最朴素的循环结构方便看每一步在干什么。import numpy as np class SCA: def __init__(self, obj_func, dim, lb, ub, pop_size30, max_iter1000): self.obj_func obj_func self.dim dim self.lb lb self.ub ub self.pop_size pop_size self.max_iter max_iter def run(self, seed42): rng np.random.RandomState(seed) pop rng.uniform(self.lb, self.ub, (self.pop_size, self.dim)) fitness np.array([self.obj_func(ind) for ind in pop]) best_idx np.argmin(fitness) best_pos pop[best_idx].copy() best_fit fitness[best_idx] history [best_fit] for t in range(self.max_iter): a 2.0 - t * (2.0 / self.max_iter) for i in range(self.pop_size): r1 a r2 rng.uniform(0, 2 * np.pi) r3 rng.uniform(0, 2) r4 rng.random() if r4 0.5: new_pos pop[i] r1 * np.sin(r2) * np.abs(r3 * best_pos - pop[i]) else: new_pos pop[i] r1 * np.cos(r2) * np.abs(r3 * best_pos - pop[i]) new_pos np.clip(new_pos, self.lb, self.ub) new_fit self.obj_func(new_pos) if new_fit fitness[i]: pop[i] new_pos fitness[i] new_fit best_idx np.argmin(fitness) if fitness[best_idx] best_fit: best_fit fitness[best_idx] best_pos pop[best_idx].copy() history.append(best_fit) return best_fit, best_pos, history有几个细节值得说r2 的取值范围是 [0, 2π]不是 [0,1]。这是正余弦算法的一个关键随机角度必须覆盖完整的周期否则 sin 和 cos 的取值会存在方向偏置。r3 的取值范围是 [0, 2]因为 |r3 * best_pos - pop[i]| 这个绝对值项需要一个不小于 0 的缩放因子r3 太小会让步长过于保守。我采用贪心替换策略个体的新位置适应度更好才更新这样可以保证每一代最优解不会变差。这个基线代码跑 Rastrigin 函数时老实说效果一般30 次独立实验中大约有三分之一会陷入局部最优最优解基本在 60 分左右徘徊。这就是标准 SCA 的正常水平也正好是 SCASL 发挥价值的场景。3.4 SCASL 的核心实现SCASL 在 SCA 基础上多了一个透镜折射学习的操作我单独封装了一个函数def lens_opposition_learning(x, lb, ub, k): center (ub lb) / 2.0 x_ref center (center - x) / k x_ref np.clip(x_ref, lb, ub) return x_ref这个函数对应前面提到的公式x_ref (ub lb)/2 (ub lb)/(2k) - x/k。当 k1 时它退化为标准的反向学习。当 k1 时反向解向搜索空间的中心收缩当 0k1 时反向解向外扩张。然后把它嵌入到 SCA 主循环中class SCASL: def __init__(self, obj_func, dim, lb, ub, pop_size30, max_iter1000, p_lens0.3, k1.5): self.obj_func obj_func self.dim dim self.lb lb self.ub ub self.pop_size pop_size self.max_iter max_iter self.p_lens p_lens self.k k def run(self, seed42): rng np.random.RandomState(seed) pop rng.uniform(self.lb, self.ub, (self.pop_size, self.dim)) fitness np.array([self.obj_func(ind) for ind in pop]) best_idx np.argmin(fitness) best_pos pop[best_idx].copy() best_fit fitness[best_idx] history [best_fit] for t in range(self.max_iter): a 2.0 - t * (2.0 / self.max_iter) for i in range(self.pop_size): r1 a r2 rng.uniform(0, 2 * np.pi) r3 rng.uniform(0, 2) r4 rng.random() if r4 0.5: new_pos pop[i] r1 * np.sin(r2) * np.abs(r3 * best_pos - pop[i]) else: new_pos pop[i] r1 * np.cos(r2) * np.abs(r3 * best_pos - pop[i]) new_pos np.clip(new_pos, self.lb, self.ub) new_fit self.obj_func(new_pos) if new_fit fitness[i]: pop[i] new_pos fitness[i] new_fit if rng.random() self.p_lens: lens_pos lens_opposition_learning(pop[i], self.lb, self.ub, self.k) lens_fit self.obj_func(lens_pos) if lens_fit fitness[i]: pop[i] lens_pos fitness[i] lens_fit best_idx np.argmin(fitness) if fitness[best_idx] best_fit: best_fit fitness[best_idx] best_pos pop[best_idx].copy() history.append(best_fit) return best_fit, best_pos, history这个实现最大的特点就是“轻量”每一代每个个体只增加一次随机判断和一个反向解评估计算开销几乎可以忽略但效果却很明显。这里还有一个实操细节透镜折射学习的触发点发生在 sin/cos 更新之后、适应度比较之后也就是说反向解是与该个体当前已经更新过的状态做比较。这样避免了一个问题如果反向解在更新之前生成它可能会被后续 sin/cos 更新覆盖掉导致折射学习的贡献被稀释。3.5 实验测试与结果对比实验脚本跑起来很简单核心是批量运行并记录统计量。我贴一下 run_experiment.py 里最核心的一段逻辑def run_benchmark(algorithm_class, func_dict, n_runs30, **kwargs): results {} for name, func in func_dict.items(): fits [] for seed in range(n_runs): algo algorithm_class(func, dim30, lbfunc.lb, ubfunc.ub, **kwargs) best_fit, _, _ algo.run(seedseed) fits.append(best_fit) results[name] (np.mean(fits), np.std(fits)) return results我跑完后的数据大体如下这是我自己一组随机种子下的实测数据供参考不同机器和随机种子会有偏差测试函数SCA 均值SCA 标准差SCASL 均值SCASL 标准差Sphere8.4e-62.1e-62.7e-145.3e-15Rastrigin76.321.81.9e-84.2e-9Griewank0.0180.0060.0000.000Ackley1.9e-38.5e-42.1e-103.7e-11从数值上能明显看到 SCASL 的提升单峰的 Sphere 上提高了差不多七个数量级多峰函数 Rastrigin 上更是从 76 直接降到接近 0Griewank 和 Ackley 也都下了一个大台阶。这背后的原因很直观透镜折射学习给种群提供了“跳出局部最优”的通道。Rastrigin 上有成千上万个局部最优的小坑标准 SCA 很容易陷进一个不太好的坑里出不来而 SCASL 因为定期生成反向解相当于隔一段时间就对外围做一次扫描一旦发现更好的区域就能切过去。收敛曲线也能看出差异SCA 的曲线通常在 200 代左右就趋于水平SCASL 则会在 400 代甚至 700 代时还出现突然下降这就是折射学习帮助跳出局部最优的典型特征。画图用 matplotlib 就行横轴迭代次数纵轴最优适应度对数坐标两条曲线一对比效果一目了然。4. 复现路上的坑与排查心得4.1 复现时最容易踩的隐蔽坑复现算法类论文最烦的不是算法本身复杂而是复现结果和论文对不上时找不到原因。我整理几个高频坑都是我自己实际踩过的。第一随机种子问题。很多初学者单次运行得到一个不错的结果就急着下结论这是不行的。单次运行有极大的偶然性你至少要独立运行 30 次并统计均值、标准差才能客观评价。我建议每个算法类实例都传入一个 seed 参数用 np.random.RandomState 而不是全局 np.random这样即使并发执行每个实验也是独立可复现的。第二边界处理策略不一致。我在前面提到过直接 np.clip 把越界个体拉回边界是最简单的做法但它会让种群在边界附近堆积。如果你在复现某个改进算法时发现效果变差先检查一下边界处理是不是和原论文一致。有的论文用“边界反弹”有的用“随机重置”有的用“不处理越界个体只限制最优解边界”差别非常大。实际测试下来我在 Ackley 函数上换了一种边界处理方式结果标准差能差出一个数量级。第三目标函数的评估次数必须对齐。有些改进算法文档里写“最大迭代次数”有些写“最大适应度函数评估次数”这两个是不同的概念。如果 SCASL 每一步都额外评估了反向解而你在做公平对比时没有把额外评估次数算进去结果会不公平。我的做法是让 SCA 和 SCASL 的 MaxIter 保持一致同时额外记录每个算法的总评估次数实验时先确认两者评估次数差距不大再比较收敛精度。第四测试函数实现细节差异。同一函数名在不同论文里可能有不同的公式变体。比如 Ackley 有的论文里最后一项是 e有的不加Griewank 里连乘项的尺度因子有的用 i 有的是 i1。这些细节不逐字对照结果就会对不上。我建议自己写测试函数时写成一个独立模块并写好注释和参考来源。4.2 关于 SCASL 参数调优的个人实操心得SCASL 看似只多了一个参数 p_lens 和 k但这两个参数对结果的影响很大我用几轮实验来验证规律。先看触发概率 p_lens。我分别试了 0.1、0.3、0.5、0.9 四组在 Rastrigin 上的结果是p0.1 时提升有限p0.3 和 0.5 表现最好p0.9 时收敛精度反而下降。原因也好理解反向解生成太频繁种群把大量评估算力花在了“跳来跳去”上没时间做精细收敛。我最后固定 p0.3因为它性价比最高。再看 k 的取值。固定 k0.5、1.0、1.5、3.0 四组测试发现 k1.0等价于传统反向学习已经比 SCA 好很多而 k1.5 在大多数函数上又比 k1.0 好一点。k0.5 时反向解太激进容易越过最优区域k3.0 时反向解太靠近中心多样性贡献又不足。所以 k1.5 是一个比较稳的默认选择。我也试过让 k 随迭代次数动态变化的方案前 30% 代 k3中间 40% 代 k1.5后 30% 代 k0.6。结果在部分函数上有提升比如 Ackley 上能再压一个数量级但在 Rastrigin 上不如固定 k1.5。这说明动态策略需要针对具体问题单独调不能无脑套用。另外还要提醒一个隐蔽问题如果你把透镜折射学习强制应用到每个个体、每一代计算量会翻倍而且种群会过高频地在“原位置”和“反向位置”之间跳转收敛速度明显变慢。所以实际操作时一定记住——折射学习是补充机制不是替代机制。4.3 SCASL 在工程落地时的注意事项复现是一回事真正拿它去解决实际问题又是另一回事。如果你想把 SCASL 用到工程优化里我建议你再想清楚下面几点。很多工程优化问题的搜索空间是不规则的比如有的变量范围是 [0, 1]有的变量范围是 [-100, 100]还有的是离散整数。这时候不能把透镜折射学习的所有变量统一用同一个 k 值因为不同维度的搜索尺度差异可能非常大。我的做法是让每一维都用自己的 lb 和 ub 来计算反向解这样每一维的反向方向是独立的不会因为某一维范围太大而干扰其他维度。工程问题通常有一个额外的限制目标函数非常昂贵可能一次评估要跑几分钟甚至几个小时。这时 SCASL 的优势反而没有在测试函数上那么大因为折射学习会引入额外的评估次数而这些评估如果在设计空间里生成的反向解根本不合法比如超出工艺约束那这几次评估就完全浪费了。解决思路是先对反向解做约束校验不合法就直接丢弃不要进入函数评估或者把反向解生成限制在已知可行区域附近。个人经验是SCASL 更适合那些目标函数不贵、维度中等10-100 维的优化场景。如果目标函数很贵建议还是用贝叶斯优化之类的高效代理方法或者至少要把反向解评估的次数压到最低。还有一个很容易被忽视的点工程里很多“最优解”是有约束的比如参数不能超过某个范围、某些参数必须满足线性不等式。这种带约束的优化问题SCASL 直接跑往往效果不好因为它的函数评估没有考虑到约束条件。落地时需要结合罚函数法或约束修复策略把不可行解的适应度适当惩罚否则最优解可能落在不可行区域里。最后复现 SCASL 这趟下来我最深的一个体会是改进算法的论文复现真正的门槛从来不在那几行新公式而在对基线的掌握程度、对实验公平性的把控、以及对随机算法的统计思维。SCASL 本身就是一个很“轻”的改进透镜成像折射学习加进去不到十行代码但它带来的性能收益是肉眼可见的。下次再遇到“XX优化算法 YY改进”这种论文组合我建议你先按这个思路走吃透基线 → 拆解改进点 → 单独实现验证 → 公平对比 → 再考虑参数调优和工程化。这一套流程跑顺了大部分同行评审里的算法类复现都能轻松拿捏。