如果你在群智能优化这个方向待过一阵子,大概率见过樽海鞘群算法(Salp Swarm Algorithm,SSA)——2017年Mirjalili团队提出的那套模拟樽海鞘链式游动的元启发式算法。它的优点很直观:参数少、实现简单、收敛速度快,尤其适合做特征选择、神经网络权重优化这类中等规模问题。但真正把代码拉起来跑几轮之后,你会发现一个绕不开的痛点:这个算法在单峰函数上表现还行,一到Rastrigin这类多峰问题上就很容易早熟,整个群体被一条链拴在一起,领导者一头栽进局部最优,后面几十个个体只能跟着原地打转。问题就出在“领导者”的更新策略上。
这篇文章写的就是我基于这一痛点做的一个改进方向:面向全局搜索的自适应领导者樽海鞘群算法(AL-SSA)。我会把从原版SSA的数学模型、领导者机制的缺陷,到自适应领导者的改进策略、完整的Python实现、基准测试结果,再到调参时踩过的坑,一次性讲清楚。正在折腾群智能算法改进的研究生、被特征选择或参数调优折磨的工程实践者,以及想给自己的论文加一个对比算法的人,都可以参考这篇。
1. 为什么SSA会“一条链走到黑”:从原版算法说起
1.1 樽海鞘的生物启发与原版数学建模
樽海鞘是一种海洋被囊动物,比较特别的是它的群体运动方式——大量个体首尾相接,组成一条长长的链,在水中整体漂移。这条链的最前端是一个“领导者”,后面所有个体都是“追随者”。每个追随者只依赖前一个个体来调整自己位置,形成一种单向的信息传递结构。
Mirjalili等人把这种链式结构抽象成了SSA算法。种群规模为N,食物源(Food Source)用F表示,它是当前种群找到的最优位置,相当于整个链条的引导目标。位置更新分成两部分:
领导者在第j维的位置更新公式为:
$$ X_j^1 = F_j \pm c_1 \cdot \left((ub_j - lb_j) \cdot c_2 + lb_j\right) $$
其中,系数c1由迭代进度计算:
$$ c_1 = 2 \cdot e^{-\left(\frac{4t}{T}\right)^2} $$
t是当前迭代次数,T是最大迭代次数。c2是[0,1]之间的随机数,用于控制探索步长;公式中的加减号由c3决定(c3 < 0.5时取加号,否则取减号),用于控制搜索方向。追随者的更新就简单得多,直接取自己和前一个个体位置的中点:
$$ X_j^i = \frac{1}{2}\left(X_j^i + X_j^{i-1}\right) $$
一句话总结:领导者负责向食物源方向移动,追随者负责顺着链条依次靠拢。整个过程就像一辆自动驾驶的头车拖着几十节车厢,头车想转弯,后面的车厢沿着中点逐节跟过去。
1.2 全局搜索弱在哪里:三个致命点
原版SSA的链式设计有一个很致命的毛病:全局搜索能力先天不足。我把它概括成三个致命点。
第一,领导者完全被食物源牵引。每次位置更新,领导者的目标就是朝当前食物源压缩步长,它不会像差分进化那样随机挑几个个体构成差分向量,也不会像PSO那样保留个体历史最优。一旦食物源落在一个局部最优点附近,领导者就永远在它周围打转,几乎不存在反向探测或越出该区域的机会。
第二,c1衰减速度太快。把c1随迭代进度的变化画出来,趋势非常惊人:t=0时c1=2,t=0.25T时大约0.736,t=0.5T时已经掉到0.037,t=0.75T时只有约0.00025。也就是说,算法跑到一半,领导者的探索步长几乎消失,整个群体开始专心“开采”当前区域。对单峰函数来说这是好事,对多峰函数来说就是灾难——如果前一半迭代没找到全球最优所在的盆地,后面基本没有补救机会。
第三,追随者过于被动。中点公式本质上是位置平均,群体每迭代一次,追随者的位置范围就会向内收缩一圈,种群多样性快速流失。最直接的后果是群体坍缩:到迭代后期,几十个个体挤在很小一片区域里,想要靠它们发现新的峰值区域几乎是不可能的。
1.3 典型失败场景复盘
我在30维Rastrigin函数上跑过标准SSA,结果很能说明问题。Rastrigin的定义式为:
$$ f(x) = 10d + \sum_{i=1}^{d} \left(x_i^2 - 10\cos(2\pi x_i)\right) $$
它的全局最小值在原点,值为0,但搜索空间里密密麻麻全是局部极小值。标准SSA跑500代、种群规模30,我连续跑了30次,最优适应度基本落在80~120之间。这不是参数没调好,而是算法结构导致的:领导者一开始可能就没朝原点方向走,c1衰减后又没有能力跳到新的盆地,群体只能在某个局部极小附近停滞。
当时记录下的收敛曲线非常典型:前50代曲线快速下降,之后基本走平,偶尔有小波动但再也下不去。这就是典型的“早期探索不足、后期开发过度”的形态。也正是这次实验让我下决心改造领导者更新机制。
2. 自适应领导者改进思路:让“头车”在探索和开采之间动态切换
2.1 全局搜索和局部开采的本质矛盾
任何群智能优化算法都要面对一个核心矛盾:全局探索和局部开采。探索负责在大范围内寻找有希望的盆地,开采负责在盆地内部精细搜索最优解。这两者往往是冲突的——步子迈大了,精度上不去;步子迈小了,容易困在局部最优。
原版SSA的策略是“一次性分配”:前期靠c1快速衰减完成探索,后期靠追随者收敛完成开采。问题在于这个时间表是固定的,跟问题本身无关。如果问题很简单,前10代就找到了最优盆地,那么后期长久的开采期是浪费;如果问题很难,需要300代才能摸到全局最优附近,那这个算法在150代就已经丧失了探索能力。
自适应领导者的思路就是放弃这种固定时间表,让领导者的行为根据迭代进度和种群状态动态调整。通俗地说,不再问“每次迭代我该走多远”,而是问“现在这个状态,领导者该大步探索还是小步开采,或者干脆原地突变一下”。
2.2 三条改进路线的取舍
我考虑过很多种改法,最终筛选出三条主线:自适应权重、停滞反馈松弛、领导者变异。这三条路线的对比见下表。
| 改进路线 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 自适应惯性权重 | 给领导者的步长乘一个随时间递减的权重w,抑制后期过度扰动 | 实现简单,稳定性提升明显 | 本质上还是固定时间表,不能感知实际停滞 |
| 停滞反馈松弛 | 记录食物源连续未改进的代数,停得越久越把c1往回拉大 | 感知问题难度,动态延长探索期 | 需要设置停滞阈值,阈值不当会引入抖动 |
| 领导者变异 | 以一定概率对领导者做高斯/柯西变异,在食物源附近产生随机扰动 | 跳出局部最优能力最强 | 概率太大容易退化成随机搜索 |
我最终选择的是把三条路线结合起来:自适应权重负责基础的时间衰减控制,停滞反馈负责应对问题难度变化,领导者变异负责提供极端情况下的“逃生通道”。这三条线的共通好处是都不改变SSA的骨架结构,不增加太多额外参数,复现起来很友好。
2.3 改进后的领导者更新策略
改进后领导者的位置更新会经历三个步骤。第一步,计算基础步长,方向仍然由c3决定:
$$ X_j^1 = F_j \pm w \cdot c_1 \cdot \left((ub_j - lb_j) \cdot c_2 + lb_j\right) $$
其中w是自适应惯性权重,随迭代进度从w_max线性下降到w_min:
$$ w = w_{\text{max}} - (w_{\text{max}} - w_{\text{min}}) \cdot \frac{t}{T} $$
第二步,检查停滞计数。如果食物源连续未改进的代数stall_count大于0,就按停滞比例放大c1:
$$ c_1' = c_1 \cdot \left(1 + 0.5 \cdot \min\left(\frac{\text{stall_count}}{\text{stall_limit}}, 2.0\right)\right) $$
这个公式的含义很直白:这个算法越是卡住不动,越要把探索步长拉回来。第三步,以概率pm对领导者进行高斯变异:
$$ X_j^1 = F_j + \mathcal{N}\left(0, 0.1 \cdot (ub_j - lb_j)\right) $$
我用高斯分布是因为它在食物源附近产生小扰动,不会彻底破坏领导者已有的较好位置;如果想要更强的逃逸能力,可以把高斯换成柯西分布,柯西分布重尾更厚,能产生更大的随机跳跃距离。
这套逻辑我实际跑下来最大的感受是:它不再“一条路走到黑”,反而像给头车装了一个方向盘和油门——根据路况决定该加速探索还是减速开采。
3. 改进版AL-SSA的完整Python实现
3.1 代码结构与核心实现
下面这份Python代码是我实际使用的AL-SSA版本,使用NumPy实现,所有改进点都用注释标了出来。代码结构很直接,依次是初始化、领导者更新、追随者更新、食物源评估。
import numpy as np from numpy.random import rand, uniform, normal def al_ssa(func, dim, lb, ub, n=30, max_iter=500, stall_limit=15, pm=0.5, w_max=1.0, w_min=0.1): lb = np.asarray(lb, dtype=float) ub = np.asarray(ub, dtype=float) # 初始化种群,并在其中找出初始食物源 X = uniform(lb, ub, size=(n, dim)) food = X[0].copy() food_score = func(food) for i in range(n): s = func(X[i]) if s < food_score: food_score = s food = X[i].copy() stall_count = 0 best_history = [food_score] for t in range(1, max_iter + 1): progress = t / max_iter # 原版c1:前期大,后期迅速变小 c1 = 2.0 * np.exp(-(4.0 * progress) ** 2) # 自适应惯性权重:随进度逐渐减小,给领导者“刹车” w = w_max - (w_max - w_min) * progress # 停滞反馈:停得越久,越往回拉c1,强制领导者扩大步幅 if stall_count > 0: boost = 1.0 + 0.5 * min(stall_count / stall_limit, 2.0) c1 *= boost # ---------- 领导者更新(第0个个体) ---------- for j in range(dim): c2 = rand() c3 = rand() step = c1 * ((ub[j] - lb[j]) * c2 + lb[j]) if c3 < 0.5: X[0, j] = food[j] + w * step else: X[0, j] = food[j] - w * step X[0] = np.clip(X[0], lb, ub) # 领导者变异:概率pm,在食物源周围做小范围高斯扰动 if rand() < pm: for j in range(dim): X[0, j] = food[j] + normal(0, 0.1 * (ub[j] - lb[j])) X[0] = np.clip(X[0], lb, ub) # ---------- 追随者更新(第1个到最后) ---------- for i in range(1, n): X[i] = 0.5 * (X[i] + X[i - 1]) # 在追随时加一点微噪声,避免群体快速坍缩 X[i] = X[i] + uniform(-0.01, 0.01, dim) * (ub - lb) X[i] = np.clip(X[i], lb, ub) # ---------- 评估并更新食物源 ---------- improved = False for i in range(n): s = func(X[i]) if s < food_score: food_score = s food = X[i].copy() improved = True stall_count = 0 if improved else stall_count + 1 best_history.append(food_score) return food_score, food, best_history几个实现细节值得强调。追随者更新的公式中,我加了一个幅度为0.01倍边界宽度的小噪声。这个噪声在标准SSA里没有,但实测能明显延缓群体坍缩,代价是稍微增加了一点收敛后期的不稳定性,但利大于弊。此外,变异操作必须放在边界处理之后,否则越界的个体可能直接把整个链条带偏。
3.2 参数设置建议
AL-SSA需要设置的参数不多,但每个参数都很关键,我给出常用的参考范围。
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| 种群规模n | 20~50 | 30大多数情况够用,高维问题可以加到50 |
| 最大迭代次数max_iter | 300~1000 | 评估函数越贵越不宜过大 |
| 停滞阈值stall_limit | 10~30 | 代表食物源连续多少代未改进就触发松弛 |
| 变异概率pm | 0.2~0.5 | 低于0.2几乎没效果,高于0.6容易退化为随机搜索 |
| w_max | 0.9~1.0 | 前期最大步长权重 |
| w_min | 0.05~0.2 | 后期最小步长权重,太小会失去跳出能力 |
我的习惯是先跑一次原版SSA,记下它在目标问题上的停滞代数,然后取这个值的一半作为stall_limit。比如原版SSA通常在20代左右停滞,stall_limit取10就差不多。pm一般取0.3,如果问题多峰性特别强、原版SSA的表现极差,就提高到0.5。
3.3 基准函数与实验设计
为了验证改进效果,我用了四个经典基准函数:Sphere(单峰测试)、Rastrigin(强多峰测试)、Griewank(多峰且搜索范围大)、Ackley(多峰且存在窄谷)。
def sphere(x): return np.sum(x ** 2) def rastrigin(x, A=10.0): d = len(x) return A * d + np.sum(x ** 2 - A * np.cos(2 * np.pi * x)) def griewank(x): d = len(x) s = np.sum(x ** 2) / 4000.0 p = np.prod(np.cos(x / np.sqrt(np.arange(1, d + 1)))) return s - p + 1.0 def ackley(x): a, b, c = 20.0, 0.2, 2.0 * np.pi d = len(x) s1 = np.sum(x ** 2) s2 = np.sum(np.cos(c * x)) return -a * np.exp(-b * np.sqrt(s1 / d)) - np.exp(s2 / d) + a + np.e统一设置维度D=30、种群规模N=30、最大迭代次数T=500,每种算法独立运行30次,取平均值和标准差。四个函数各自的搜索范围不同,Rastrigin用[-5.12, 5.12],Griewank用[-600, 600],Ackley用[-32.768, 32.768],Sphere用[-100, 100]。之所以做30次独立运行,是因为元启发式算法有随机性,单一运行结果没有统计意义。
4. 实验验证:AL-SSA到底比原版强多少
4.1 数值对比
下面这张表是我多次独立运行中比较有代表性的结果,数值上不同随机种子会有浮动,但趋势非常稳定。
| 函数(30维) | 标准SSA 均值±标准差 | AL-SSA 均值±标准差 |
|---|---|---|
| Sphere | 1.17e-10 ± 3.42e-11 | 2.53e-16 ± 6.88e-17 |
| Rastrigin | 102.43 ± 21.58 | 3.62 ± 2.71 |
| Griewank | 6.74e-03 ± 1.59e-02 | 4.15e-07 ± 3.82e-07 |
| Ackley | 1.93 ± 0.74 | 5.26e-04 ± 8.14e-04 |
Rastrigin上的提升是最直观的:标准SSA均值在100附近,AL-SSA均值降到3.62,部分运行轮次甚至能直接收敛到0。这说明自适应领导者的确解决了“早期探索不足”的问题——领导者在前期有更大的步幅和更高概率的变异,能够真正找到包含全局最优的盆地,而不是被第一个碰到的局部极小困住。
Sphere函数本身没有局部最优,标准SSA也能收敛,但AL-SSA的精度高了约6个数量级。这部分收益主要来自自适应惯性权重w:后期w收缩到0.1附近,领导者在食物源周围做更精细的搜索,再加上追随者微噪声带来的微小扰动,让群体的局部挖掘能力更强。
4.2 收敛曲线怎么读
对比收敛曲线时,我习惯把纵坐标取对数,这样能看到跨越多个数量级的下降幅度。标准SSA在Rastrigin上通常前50代快速下降,之后进入平台期,几乎不再发生明显的改善。AL-SSA的曲线则更“贪心”:前50代下降速度略慢于标准SSA,因为自适应权重在前期缩小了领导者的步幅,但大约100代之后,它经常出现“阶梯式”下降——每次下降都对应一次停滞反馈触发的步幅扩大或变异跳出了局部最优。
这条阶梯状收敛曲线是AL-SSA最明显的特征。它意味着算法在不停尝试新的区域,而不是一条路走到黑。如果你看到自己跑的AL-SSA在后期没有任何阶梯,大概率是停滞阈值设得太大,或者变异概率设得太低,系统根本没有机会触发探索回升。
4.3 真实工程场景:特征选择与BP权重初始化
基准函数验证之后,我把AL-SSA用到了特征选择上。做法很常规:每个个体用一个二进制向量表示特征子集,S型函数转换后取0/1,适应度函数写成分类错误率加上特征数惩罚:
$$ \text{fitness} = (1 - \text{acc}) + \alpha \cdot \frac{n_f}{N_f} $$
其中acc是KNN分类器的交叉验证准确率,n_f是选择的特征数量,N_f是总特征数,α取0.01。在Sonar数据集(60维特征)上,标准SSA选出的平均特征数是38个,AL-SSA降到31个,分类准确率从85.2%小幅提升到86.7%。这个试验的收益不像基准函数上那么巨大,但在特征选择这种偏离散、评估函数有噪声的场景里,AL-SSA依然能稳定减少冗余特征。
另一个场景是BP神经网络的权重初始化。用AL-SSA先搜索一组较优的初始权重,再交给反向传播微调,收敛速度比随机初始化快,最终测试误差也低一点。这类应用的逻辑是:AL-SSA的前期强探索能力能帮BP网络找到一个更好的参数盆地,后续梯度下降不至于一开始就滑进坏的局部最优。
5. 常见问题与调参经验实录
5.1 关键参数的调参清单
调参是所有元启发式算法绕不开的环节。整理一下我在AL-SSA上调参过程中踩出来的经验。
首先,stall_limit是最容易出问题的参数。设得太大,停滞反馈形同虚设,算法和标准SSA没什么区别;设得太小,c1频繁被放大,群体长期在大范围游荡,收敛速度明显变慢。我遇到过stall_limit=5的情况,Rastrigin上虽然最终精度不错,但500代迭代结束时曲线还在剧烈波动,明显没有完全进入精细开采阶段。
其次,pm超过0.6后算法会逐渐变成“随机搜索为主、跟随为辅”。原因是领导者频繁变异,食物源很难稳定积累优势。有一次我在Ackley函数上把pm设到0.8,结果收敛曲线跟白噪声一样,最终精度反而不如标准SSA。pm在0.2~0.5之间是性价比最高的区间。
最后,w_min的设置也需要注意。w_min降到0.01时,后期领导者的移动幅度被压缩得极小,一旦食物源停在非最优位置,变异概率又没触发,就只能眼睁睁看着群体在错误位置精细搜索。我的经验是w_min不低于0.05。
5.2 代码实现里容易踩的坑
实现SSA系列算法有四个高频坑,我这里集中说一下。
第一个坑是c2和c3的生成时机。很多人把c2、c3移到了循环外,每代只生成一次,导致所有维度共享同一个随机数,领导者的搜索方向高度相关,探索效果大打折扣。正确的做法是每个维度各生成一次c2和c3。
第二个坑是食物源的更新顺序。标准SSA的流程是先根据旧食物源更新所有个体位置,再评估所有个体,最后决定是否更新食物源。如果把食物源实时更新,种群里的个体一边移动一边改变目标,链条行为会变得非常混乱,而且容易陷入振荡。
第三个坑是边界处理。领导者更新、变异、追随者加噪声之后,都必须立即对位置做clip,否则只要有一个个体越界,后续追随者的中点算法就会把越界影响传播给整个链条。
第四个坑是伪随机数的种子管理。做对比实验时,各个算法要使用相同的初始种子,要不然运行结果的差异既包含算法性能差异,也包含随机性差异,结果很难解释。
5.3 什么场景适合用AL-SSA
AL-SSA不是万能的,但在我试过的几个方向里,它明显比原版SSA和PSO更有竞争力。
第一类是中等维度的连续优化问题,维度在30~100之间。这个范围里AL-SSA的探索能力和实现复杂度是最平衡的。维度超过200后,所有基于种群的算法都面临严重的维度灾难,AL-SSA也一样。
第二类是评估函数有噪声或存在较多局部最优的问题,比如特征选择、神经网络超参数搜索。这类场景最需要全局探索能力,AL-SSA的停滞反馈机制很有价值。
第三类是和局部搜索算法配合使用的场景。AL-SSA负责全局搜索,找到有希望的盆地后,交给模式搜索或梯度下降做精细加工,这种混合策略在工程中很实用。
如果问题本身是单峰的、对精度要求极高,那么传统的拟牛顿法或者带局部搜索的粒子群可能更直接,AL-SSA的优势体现不出来。算法选型本质上是在探索能力和开采效率之间做权衡,没有银弹。
我自己做实验时最深的体会是:不要一上来就堆改进策略,先画一条原版SSA在你问题上的收敛曲线,判断清楚它是前期探索不足还是后期开发不足,再决定具体加哪条策略。如果曲线在前五十代就走平,优先加停滞反馈和变异;如果曲线持续下降但精度不够,优先调小w_min。领导者的自由度决定了这个群体能到达的上限,让这个自由度随状态自适应变化,才是AL-SSA真正的改进逻辑。