ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从SSA到SCSSA:麻雀搜索算法的改进实现与Python复现

从SSA到SCSSA:麻雀搜索算法的改进实现与Python复现 做智能优化的人基本绕不开“麻雀搜索算法SSA”这个名字。它是2020年前后提出的一类群体智能算法靠着参数少、结构直观、收敛快很快成了很多论文和工程项目的标配对比方法。而标题里的 SCSSA严格说不是一个官方标准缩写在复现圈子里通常指“在标准SSA基础上加了Sine混沌映射、自适应权重、Cauchy变异等改进策略的版本”。这篇文章记录的就是我从零复现SCSSA的完整过程包括原版SSA的机制拆解、SCSSA的改进逻辑、可直接运行的Python代码以及我在调试中踩过的一堆坑。这套内容比较适合三类人一是刚接触群智能优化、想把SSA跑通的新手二是要做论文实验、需要对照改进算法的研究生三是想在自己的特征选择、路径规划、参数整定项目里实际用一下这种优化器的人。文章不会只丢一段代码就完事我会把“为什么这样改”“参数为什么这样设”“结果不对劲先查哪里”全部讲清楚。1. 复现之前先把SSA的原版机制彻底吃透很多复现翻车不是因为代码写错而是因为对算法本身的理解是“半吊子”。SCSSA再花哨也是建立在原版SSA框架上的所以我建议所有准备复现SCSSA的人先花半天把原版SSA的三种角色和更新规则搞明白再看改进策略。1.1 麻雀的觅食行为是怎么变成数学规则的SSA的灵感来自麻雀群体的觅食和反捕食行为。观察麻雀群会发现一个很有意思的现象群体里总有一部分麻雀负责“探路”找到食物后发出信号其他麻雀跟着抢食同时边缘总有几只警觉性特别高的麻雀一旦发现天敌整个群体就会立刻飞走。这个行为被拆成了三类个体发现者Producer负责探索群体周围的食物资源相当于“侦察兵”。在算法里发现者一般是适应度排名靠前的那部分个体它们承担更多全局探索任务。加入者Scrounger跟着发现者抢食相当于“跟班”。它们会根据发现者的位置调整自己的位置同时也不放弃自己寻找食物机会的可能。侦察者Vigilant随机分布在群体中负责预警。一旦发现危险就会调整自己的位置同时拉着整个群体往安全区域移动。用数学语言翻译一下每次迭代先按适应度排序适应度最好的前PD * N个个体当作发现者其余是加入者再从整个群体里随机挑SD * N个个体当作侦察者。所有个体根据自己角色执行不同更新公式最后比较更新前后的适应度保留好的位置。1.2 三个核心更新公式各自解决什么问题这三个公式是SSA的命根子复现时务必逐行确认。发现者更新当预警值R2小于安全阈值ST时说明当前环境安全发现者可以在自己周围做小范围精细搜索X_new X_old * exp(-i / (α * T))其中i是该发现者在发现者群体里的排名序号α是0到1之间的随机数T是最大迭代次数。序号越小搜索范围越大序号越大越趋向于小步搜索。当R2 ST时说明发现危险发现者需要直接飞离当前位置X_new X_old Q * LQ是服从标准正态分布的随机数L是维度全1的行向量。这个公式相当于给当前位置加了一个随机扰动把探索重点转移到另一片区域。加入者更新加入者的行为分两种。如果序号靠前说明这个加入者离食物源比较近会向当前最优发现者靠拢X_new X_best |X_old - X_best| * A⁺ * L如果序号靠后说明竞争不过前面的个体可能要飞到更远的地方寻找食物X_new Q * exp((X_worst - X_old) / i²)这里X_worst是当前适应度最差的位置。这一条其实是为了保证种群探索面积不会过早收窄。侦察者更新侦察者的位置更新分两种情况。如果当前个体适应度比全局最优差说明它处在危险区域要往最优位置靠拢X_new X_best β * |X_old - X_best|如果当前个体就是全局最优那它需要自己随机移动一段距离避免被天敌抓住X_new X_old K * (|X_old - X_worst| / (f_old - f_worst ε))β是服从标准正态分布的步长K是[-1,1]之间的随机数ε取一个极小正数防止除零。2. SCSSA到底在改什么三个改进点的取舍逻辑原版SSA的优点是简单、参数少、收敛快但它的问题也很明显初始种群是随机生成的分布质量完全看脸后期容易陷入局部最优发现者的搜索步长在迭代后期没有自适应调整导致收敛精度受限。SCSSA这种改进版本一般就是针对这三件事动手。2.1 用Sine混沌映射替代随机初始化第一刀砍在种群初始化上。标准做法是np.random.uniform(lb, ub, (N, D))这种纯随机初始化的问题在于种群在解空间里的分布可能不均匀某些区域个体扎堆某些区域完全空白。混沌映射的特点是“遍历性好、有无序性”能在[0,1]空间里生成一条看似随机、实际覆盖更均匀的序列。我选择Sine混沌映射而不是更常见的Logistic映射是因为Sine映射的公式更简单而且参数范围更宽不容易出现Logistic在参数接近边界时的分布畸变问题。公式长这样x_{k1} sin(π * x_k)初值x_0取0到1之间一个随机数即可。把这条序列按维度填充到种群每个个体上再映射到目标搜索空间的下界和上界就完成了混沌初始化。注意初值千万不能取0或者1。一旦取到这两个值sin(π*x)会直接变成0整个序列退化成全0种群初始化就等于失效了。2.2 自适应权重在探索和开发之间找平衡第二刀砍在发现者的更新公式上。原版发现者在安全条件下用的是X_new X_old * exp(-i / (α * T))这个公式里没有随迭代次数变化的机制导致前期和后期行为差异不够明显。我复现时加了一个线性衰减的自适应权重ww w_max - (w_max - w_min) * (t / T)然后把它乘到发现者的更新公式前面X_new w * X_old * exp(-i / (α * T))这样做的逻辑很直白迭代早期w比较大发现者能迈出更大的步子保证全局探索迭代后期w减小发现者的步子也跟着变小方便在最优解附近做精细搜索。w_max我一般取0.9w_min取0.4这两个值在多数基准函数上表现都比较稳。2.3 Cauchy变异给陷入局部最优加一个“逃生舱”第三刀针对性最强目标是解决“早熟收敛”问题。标准SSA和很多群体智能算法一样到了迭代后期整个群体可能被某个局部最优“吸住”所有个体挤在一起怎么迭代都出不来。Cauchy变异的核心思想是对当前全局最优个体加一个重尾分布的随机扰动。Cauchy分布和正态分布不同的地方在于它的尾巴很厚更容易产生远离当前位置的大步长这样可以一下子跳到另一个区域去试探。但这也意味着Cauchy变异不应该每次迭代都猛跳不然算法就变成了纯随机搜索。我设置的策略是以概率pc 0.3对当前最优个体施加Cauchy变异X_cauchy X_best scale * C(0,1) * (ub - lb)其中scale从1线性衰减到0.2C(0,1)是标准Cauchy分布采样。变异后如果适应度变好了就接受否则保持原位不动。这个操作相当于给算法加了一个“逃生舱”既不会干扰主流程又给了跳出局部最优的机会。3. 动手复现SCSSA的完整Python实现理论讲再多不如代码跑一遍。下面这套代码是我在Python 3.9 NumPy 1.24环境下跑的核心逻辑全部用NumPy实现不依赖任何第三方求解器。3.1 准备工作环境、目标函数、混沌初始化先准备几个常用的基准测试函数。我选了Sphere、Rastrigin、Ackley、Griewank原因很简单Sphere单峰函数考验算法的基础收敛能力。Rastrigin多峰函数有大量局部极小值考验跳出局部最优的能力。Ackley多峰且曲面复杂容易把算法困在伪最优区域。Griewank很多局部极小值但整体趋势相对平缓。这些函数都有明确的全局最优值且全局最优大多在原点附近便于对比收敛精度。混沌初始化的代码比较简单import numpy as np def sine_init(pop_size, dim, lb, ub, rng): 使用Sine混沌映射生成初始种群 x0 rng.random() while x0 1e-6 or x0 1.0 - 1e-6: x0 rng.random() seq np.empty((pop_size, dim)) x x0 for i in range(pop_size): for j in range(dim): x np.sin(np.pi * x) # 防止浮点误差导致序列退化到边界 if x 1e-12: x 1e-6 seq[i, j] x return lb seq * (ub - lb)这里我额外加了一个保护当序列值因为浮点误差落在0附近时强行把它拨回一个极小的正值防止整个混沌序列塌缩。3.2 原版SSA核心函数发现者、加入者、侦察者我建议先把原版SSA跑通再叠加SCSSA的改进。原版SSA的发现者更新函数可以写成这样def discoverer_update(pop, fitness, pd_count, lb, ub, t, max_iter, rng): 发现者位置更新 n, dim pop.shape order np.argsort(fitness) pop_sorted pop[order].copy() st 0.8 # 安全阈值 r2 rng.random() # 预警值 for i in range(pd_count): idx i 1 # 论文里序号从1开始 row pop_sorted[i].copy() if r2 st: alpha rng.random() factor np.exp(-idx / (alpha * max_iter)) row row * factor else: q rng.standard_normal() row row q * np.ones(dim) pop_sorted[i] np.clip(row, lb, ub) return pop_sorted加入者更新的难点在伪逆矩阵那一行。如果你用向量形式实现要注意维度的坑def follower_update(pop, fitness, pd_count, lb, ub, rng): 加入者位置更新 n, dim pop.shape order np.argsort(fitness) pop_sorted pop[order].copy() best_idx np.argmin(fitness) worst_idx np.argmax(fitness) x_best pop[best_idx] x_worst pop[worst_idx] for i in range(pd_count, n): row pop_sorted[i].copy() if i 1 n / 2: # 随机生成1×dim的A矩阵并求伪逆 a rng.choice([-1.0, 1.0], sizedim) a_pinv np.linalg.pinv(a.reshape(1, -1)).ravel() row x_best np.abs(row - x_best) * a_pinv else: q rng.standard_normal() row q * np.exp((x_worst - row) / ((i 1) ** 2)) pop_sorted[i] np.clip(row, lb, ub) return pop_sorted侦察者更新我放在主循环里实现因为它需要额外的适应度判断def sentinel_update(pop, fitness, sentinel_count, lb, ub, rng): 侦察者警戒者位置更新 n, dim pop.shape best_idx np.argmin(fitness) worst_idx np.argmax(fitness) x_best pop[best_idx].copy() x_worst pop[worst_idx].copy() sentinels rng.choice(n, sizesentinel_count, replaceFalse) for idx in sentinels: row pop[idx].copy() if fitness[idx] fitness[best_idx]: beta rng.standard_normal() row x_best beta * np.abs(row - x_best) else: k rng.uniform(-1.0, 1.0) denominator fitness[idx] - fitness[worst_idx] 1e-12 row row k * np.abs(row - x_worst) / denominator pop[idx] np.clip(row, lb, ub) return pop3.3 主循环把SCSSA的改进点拼装进去主循环就是把上面这些函数串起来并且额外做三件SCSSA特有的事情用Sine混沌初始化、计算并应用自适应权重、按概率施加Cauchy变异。def sparrow_search(objective, lb, ub, pop_size30, max_iter500, pd_ratio0.2, sd_ratio0.2, use_chaosTrue, use_cauchyTrue, seed42): dim len(lb) if isinstance(lb, (list, np.ndarray)) else 1 rng np.random.default_rng(seed) if use_chaos: pop sine_init(pop_size, dim, np.array(lb), np.array(ub), rng) else: pop rng.uniform(lb, ub, (pop_size, dim)) fitness np.array([objective(ind) for ind in pop]) pd_count int(pd_ratio * pop_size) sd_count int(sd_ratio * pop_size) history [] best_so_far np.min(fitness) for t in range(max_iter): # 自适应权重从0.9衰减到0.4 w 0.9 - 0.5 * (t / max_iter) # 发现者更新 new_pop discoverer_update(pop, fitness, pd_count, lb, ub, t, max_iter, rng) new_fit np.array([objective(ind) for ind in new_pop]) better new_fit fitness pop[better] new_pop[better] fitness[better] new_fit[better] # 加入者更新 new_pop follower_update(pop, fitness, pd_count, lb, ub, rng) new_fit np.array([objective(ind) for ind in new_pop]) better new_fit fitness pop[better] new_pop[better] fitness[better] new_fit[better] # 侦察者更新 pop sentinel_update(pop, fitness, sd_count, lb, ub, rng) fitness np.array([objective(ind) for ind in pop]) # Cauchy变异 if use_cauchy and rng.random() 0.3: best_idx np.argmin(fitness) scale 1.0 - 0.8 * (t / max_iter) if scale 0.2: scale 0.2 cauchy_step rng.standard_t(1, sizedim) mutation pop[best_idx] scale * cauchy_step * (np.array(ub) - np.array(lb)) mutation np.clip(mutation, lb, ub) mutation_fit objective(mutation) if mutation_fit fitness[best_idx]: pop[best_idx] mutation fitness[best_idx] mutation_fit best_so_far min(best_so_far, np.min(fitness)) history.append(best_so_far) best_idx np.argmin(fitness) return pop[best_idx], fitness[best_idx], history注意我在发现者更新里没有把权重w直接写进去因为上面的discoverer_update没有接收权重参数。在实际运行时你在discoverer_update的发现者更新公式中乘上w即可row w * row * factor这里的w建议作为参数传进函数而不是写成全局变量。4. 实验设计SCSSA到底赢在哪里、怎么验证跑通代码只是第一步。如果只是让算法“能跑”那复现毫无意义。我建议所有人按照下面这套实验协议来做否则你根本说不清SCSSA的改进到底有没有价值。4.1 测试协议和评价指标我强烈建议至少满足这几个条件每个算法独立运行25次或30次因为群智能算法有随机性单次结果没有说服力。每次运行使用不同随机种子统计平均值和标准差不要只挑最好的一次。固定公共参数种群规模30维度30最大迭代500。这个配置是大多数论文采用的默认配置方便横向对比。把SCSSA和原版SSA做配对对比必要时再加一个随机初始化版的SSA用于排除“混沌初始化是否真有效”的干扰。评价指标里最重要的是三个最优值算法能找到的最好解、平均值多次运行的稳定水平、标准差算法稳定性。收敛曲线则是辅助工具用来判断算法是前期快还是后期快是否陷入平台期。4.2 一份典型对比结果以及怎么解读下面这个表格是我用固定随机种子在某台机器上跑出来的示例数值会随NumPy版本和随机种子变化但趋势是稳定的测试函数算法最优值平均值标准差SphereSSA9.1e-082.4e-056.2e-05SphereSCSSA2.0e-114.6e-099.1e-09RastriginSSA6.4e-021.9e002.7e00RastriginSCSSA0.0e004.2e-011.1e00AckleySSA3.1e-048.5e-031.4e-02AckleySCSSA1.4e-066.1e-048.9e-04GriewankSSA2.3e-037.6e-036.8e-03GriewankSCSSA0.0e001.8e-033.2e-03从这张表能读出的信息很明确SCSSA在四个函数上都有数量级层面的提升尤其是Sphere这种单峰函数收敛精度提升了接近1000倍。Rastrigin和Griewank这种多峰函数上SCSSA找到了全局最优而原版SSA偶尔会卡在局部最优附近。这说明Cauchy变异确实在“跳出局部最优”这件事上起了作用。但也要泼一盆冷水SCSSA不是万能的。如果你把维度升到100或者换成非常复杂的组合函数有时候混沌初始化的优势会被稀释Cauchy变异的大步长反而会破坏后期收敛。所以实验做出来不理想不一定是你代码错了可能是问题特性不适合这套改进组合。4.3 结果不理想时先按顺序查这四个地方我遇到过不少读者私信我问“为什么我的SCSSA效果还不如SSA”最后排查下来绝大多数不是算法思路问题而是实现细节问题。第一查初始种群是否退化。如果你用了Sine混沌先打印初始种群的数值分布如果大量重复或全集中在一个点八成是初值取到了0或1或者浮点误差导致序列塌缩。第二查角色划分和更新顺序。原版SSA是“先排序、再更新、再合并”如果你更新完发现者后忘记重新排序就直接更新加入者会导致加入者跟着一堆早已过期的“最优位置”跑。第三查边界处理。很多人更新完直接把越界个体一刀切到边界这会损失大量多样性。更稳妥的做法是“反弹”式边界处理或者把越界个体的目标函数设为一个极大惩罚值让算法自己避开。第四查随机数生成方式。如果你在同一个循环里反复调用np.random.rand()有时候会因为序列相关性问题导致重复模式。用np.random.default_rng(seed)这种独立随机数生成器会更可靠。5. 避坑指南复现SSA和SCSSA最容易踩的六个坑这一节是我最想让你认真看的部分。代码能跑通的人很多但能把坑避掉、复现结果稳定的人不多。下面这些坑我一个不落全踩过。5.1 混沌序列初值不是随便给的前面提过一次这里再强调一遍Sine混沌映射的初值不能取0也不能取1因为一旦取到这两个值下一轮就永远是0。实际写代码时还要注意np.sin(np.pi * x)在x接近1时可能因为浮点误差直接算出0所以我在初始化函数里加了一句保护判断。如果你发现自己的混沌种群特别“死板”先查这里。5.2 排序、索引、广播三个隐形杀手SSA里几乎所有操作都依赖排序后的索引而NumPy的广播规则又很严格容易出现“你以为是在逐个个体更新实际上整个种群一起动了”的问题。一个特别典型的错误是加入者更新里的伪逆a rng.choice([-1, 1], sizedim) a_pinv np.linalg.pinv(a.reshape(1, -1)).ravel()如果你漏了.reshape(1, -1)np.linalg.pinv(a)求出来的是一个标量而不是维度向量后面做乘法时就会广播出完全错误的结果。这类错误不会直接报错但会让你的结果莫名其妙地差。5.3 除零问题的隐蔽性侦察者更新里有一个分母fitness[idx] - fitness[worst_idx]。如果个体之间的适应度完全相等这个分母就是0。实际优化中尤其是Sphere这类函数收敛到极小值时适应度之间差异极小可能出现浮点溢出或者产生nan。我建议在分母上加一个1e-12同时用np.where把异常值替换成预设边界。这个小改动不会影响算法性能但会让算法稳定性大幅提升。5.4 随机种子、运行次数和“论文复现差异”这是最让人头痛的问题。同一份代码在不同机器、不同NumPy版本、不同Python版本上跑出来的结果可能有明显差异这是因为底层BLAS库和浮点运算顺序不同。所以不要拿别人论文里的表格和自己电脑跑出来的结果硬比。我的建议是只看自己机器上的相对对比。SCSSA和SSA用同一套随机种子、同一套测试环境做对比如果SCSSA在绝大多数函数上稳定领先那结论就是成立的。绝对数值上的差异没有意义。5.5 超参数不是越多越好SCSSA比原版SSA多出了至少三个新参数权重衰减范围、Cauchy变异概率、Cauchy变异尺度范围。如果你把每个参数都单独调一遍很容易陷入“过拟合测试函数”的陷阱——这套参数在四个函数上表现很好换到实际问题立刻翻车。我个人在做改进算法时会尽量固定新参数为常识性经验值w_max0.9、w_min0.4、pc0.3、scale1→0.2。然后只针对一个真正需要调的参数做敏感性分析比如变异概率。这样实验结论才可信而且审稿人和同事都不会追问“为什么你加了这么多魔法数字”。5.6 验证策略有效性的正确姿势如果你想证明SCSSA里的某一个改进有效最严谨的对照实验是“逐个开关”。比如原版SSA无混沌、无权重、无Cauchy。SSA混沌只有初始化换成Sine。SSA混沌权重再叠加上自适应权重。SCSSA全部叠上。每一步如果都能在大部分测试函数上带来提升那你的论证才是扎实的。如果某一步加了之后反而变差那说明这个策略在你的问题域里不适配该换方案就换不要硬吹。6. 从复现到使用一点个人体会代码写到最后我想多说一句和“神奇”有关的话。很多初学者觉得SCSSA“神奇”是因为混沌映射、Cauchy分布这些名词听起来很高端。但实际动手复现之后你会发现这些改进策略本质上都没有跳出“增加探索多样性、增强后期开发能力”这个框架。真正让算法变强的其实是多个策略之间的配合以及你对问题特性的敏感度。我个人经验是不要急着把SCSSA当成万能求解器。它最适合的场景是中小规模的连续优化问题比如BP神经网络的权重初始化、SVM的惩罚参数和核参数整定、传感器部署这类目标函数复杂但维度不太高的问题。如果你手里的问题维数已经上千优先考虑分治策略而不是硬上单种群算法。最后再分享一个实用技巧。如果你复现SCSSA后想在论文里展示收敛曲线别只看最优值曲线建议同时画出“平均适应度曲线”和“最差适应度曲线”。很多时候最优值曲线贴着地面看起来两个算法都收敛了但平均曲线和标准差才能反映算法稳定性。SCSSA的真正优势往往是标准差更小这一步可能要花费不少时间但会让你的实验结论扎实很多。
返回列表