麻雀搜索算法Python实现:生产者、加入者与警戒者机制及收敛曲线可视化
2026/9/16 8:01:51 网站建设 项目流程

简介:麻雀搜索算法(SSA)是一种受麻雀觅食行为启发的群体智能优化算法,本项目提供完整的Python实现与回归函数优化示例,适合入门智能优化算法、需要对比算法收敛性能的研究者或开发者。代码结构清晰,包含核心算法模块(ssa.py)与可视化主程序(main.py),并附有说明文档(README.md),共3个文件,压缩包仅3KB,轻量易部署。运行后可输出最优变量与最优值,并绘制收敛曲线,直观展示SSA的寻优过程与收敛速度。目前已有1233人学习下载,可作为理解SSA原理、快速上手改进或教学演示的参考脚本。

1. 麻雀搜索算法为什么值得自己实现一遍

很多搞数据分析的人第一次接触 SSA(麻雀搜索算法)是在某篇论文里,看到它把 BP 神经网络或 SVM 的参数调得很好,就想找现成代码跑一下。但真正落到自己的自定义函数上,会发现网上的实现要么耦合了特定目标函数,要么把种群角色简化得看不出原貌。我这篇文章用一份可运行的 SSA 回归示例,把麻雀搜索算法的生产者、加入者、警戒者三类角色的更新逻辑拆开讲,并给出基于 matplotlib 的收敛曲线可视化代码。这样你既能拿它直接优化自己的回归函数,也能在理解参数边界后做二次改造。文章面向想快速复现又不想踩坑的 Python 工程师,也适合准备把群智能算法写进论文实验对比的读者。

2. SSA 的核心机制与种群角色划分

麻雀搜索算法的灵感来自麻雀觅食过程,但它的本质仍是一种基于种群的随机搜索方法。与粒子群算法相比,SSA 增加了发现者与加入者的层级关系,以及警戒者的逃逸行为,这让种群在迭代后期更容易跳出局部最优。理解这三类角色的位置更新规则,是看懂代码和调整参数的前提。

2.1 生产者、加入者与警戒者的位置更新规则

在 SSA 中,种群中一部分适应度较优的个体被标记为生产者,负责引导群体向较优区域搜索。生产者的位置更新公式为:

X_i^{t+1} = X_i^t * exp(-i / (alpha * T_max)) 如果 R2 < ST X_i^{t+1} = X_i^t + Q * L 如果 R2 >= ST

这里 R2 是预警值,ST 是安全阈值,alpha 是随机数,Q 服从正态分布。当预警值小于安全阈值时,生产者进行窄幅搜索;反之迅速飞离危险区域。这个设计让算法在前期有较强的探索性,后期逐渐收敛到局部精修。

加入者会跟随生产者移动,其更新规则为:

X_i^{t+1} = Q * exp((X_worst - X_i^t) / i^2) 如果 i > n/2 X_i^{t+1} = X_best^t + |X_i^t - X_best^t| * A+ * L 否则

当某个加入者排在种群后半段时,说明它位置较差,会跑去其他地方觅食;若在前半段,则在最优个体附近随机搜索。A+ 是元素为 1 或 -1 的随机向量伪逆矩阵,这一步是整个 SSA 里矩阵运算最密集的地方,也是新手容易写错的地方。

警戒者数通常占种群的 10% 到 20%,它们负责发现捕食者。警戒者更新时如果发现自己已经处于最优位置,会逃到安全区附近;否则会向种群中心靠拢。公式为:

X_i^{t+1} = X_best^t + beta * |X_i^t - X_best^t| 如果 f_i > f_g X_i^{t+1} = X_i^t + K * (|X_i^t - X_worst^t| / ((f_i - f_w) + eps)) 如果 f_i == f_g

这里的 beta 是步长控制参数,K 是随机数。注意当适应度相同或收敛到同一水平时,分母加上 eps 避免除零,这个细节在实现时必须处理。

2.2 参数对收敛行为的影响

SSA 的关键参数包括种群规模 N、最大迭代次数 T_max、安全阈值 ST 以及警戒者比例 PD。从工程角度看,ST 的默认值通常设置为 0.8,它的物理含义是「多少比例的麻雀认为当前位置安全」。ST 越大,生产者越倾向小范围移动,收敛快但容易早熟;ST 越小,种群频繁逃逸,全局搜索能力强,但后期震荡明显。

种群规模 N 的选择与目标函数维度直接相关。对于二维或三维的低维函数,N 取 30 到 50 即可;如果回归问题有十几个参数,N 建议设到 100 以上。很多网上的示例代码把 N 固定为 20,用于演示没问题,但实际优化时会导致加入者群体过小,SSA 的层级优势发挥不出来。

警戒者比例 PD 一般取 0.1 到 0.2。PD 过大时,大量个体都在逃逸,会破坏生产者的引导作用,收敛曲线会呈现锯齿状;PD 过小,种群容易陷入局部最优而无人预警。下面章节给出 Python 实现后,你会看到这些参数如何直接体现在更新条件里。

3. 用 Python 实现 SSA 优化自定义回归函数

这一章进入可运行代码。我提供的示例有两个文件:ssa.py封装麻雀搜索算法主体,main.py定义待优化的回归函数并调用 SSA,最后用 matplotlib 绘制收敛曲线。整个实现只依赖 numpy 和 matplotlib,安装命令是pip install numpy matplotlib

3.1 项目结构与算法主体代码

先看ssa.py的核心类实现。麻雀搜索算法的入口可以设计成函数式,也可以设计成类。我这里用类,因为要保存每一代的全局最优适应度,便于后续绘图。

import numpy as np class SSA: def __init__(self, obj_func, lb, ub, dim, pop_size=30, max_iter=100, ST=0.8, PD=0.2): self.obj_func = obj_func # 目标函数,最小化问题 self.lb = np.array(lb) # 下界数组 self.ub = np.array(ub) # 上界数组 self.dim = dim self.pop_size = pop_size self.max_iter = max_iter self.ST = ST self.PD = PD self.convergence = [] # 记录每轮最优适应度 def init_population(self): # 初始化种群位置,均匀分布在边界内 X = np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) return X

注意lbub在这里是数组,而不是标量,这样能处理不同维度有不同边界的情况。如果你要优化的函数所有维度边界一致,也可以传入像[-5, 5]这样的列表,numpy 广播机制会自动扩展。

3.2 位置更新与边界处理

SSA 的生产者更新、加入者更新和警戒者更新需要在一个迭代循环里依次执行。边界处理通常采用两种方式:一是把越界值裁剪到边界上,二是随机重新初始化。我推荐裁剪,因为随机重新初始化会破坏种群已有的收敛方向。

def update_producer(self, X, fitness, t): # 排序并获取索引,适应度越小越优 idx = np.argsort(fitness) best_idx = idx[0] worst_idx = idx[-1] best_pos = X[best_idx].copy() worst_pos = X[worst_idx].copy() # 前 PD * pop_size 个个体作为生产者 producer_num = int(self.PD * self.pop_size) R2 = np.random.rand() for i in range(producer_num): if R2 < self.ST: alpha = np.random.rand() X[idx[i]] = X[idx[i]] * np.exp(-i / (alpha * self.max_iter)) else: Q = np.random.normal(0, 1) X[idx[i]] = X[idx[i]] + Q * np.ones(self.dim) # 边界裁剪 X[idx[i]] = np.clip(X[idx[i]], self.lb, self.ub) return X, best_pos, worst_pos, idx

这段代码里的R2是全局随机数,所有生产者使用同一个 R2 判断是否预警,这是原始 SSA 的定义方式。有些改进版本会对每个个体独立生成 R2,那属于变体,不是标准实现。np.clip直接完成上下界裁剪,避免越界个体导致目标函数计算错误。

加入者的更新逻辑里,A+矩阵需要特别处理。原始公式中的 A 是元素为 1 或 -1 的随机向量,A+ 是 A 的伪逆。这里可以用np.linalg.pinv求伪逆,但更好的做法是直接用下面这种简化写法:

def update_follower(self, X, fitness, best_pos, worst_pos, idx): follower_num = self.pop_size - int(self.PD * self.pop_size) for i in range(follower_num): real_idx = idx[i + int(self.PD * self.pop_size)] if i > follower_num / 2: Q = np.random.normal(0, 1) X[real_idx] = Q * np.exp((worst_pos - X[real_idx]) / (i + 1e-10) ** 2) else: A = np.random.choice([-1, 1], size=self.dim) A_plus = A.T * np.linalg.inv(A @ A.T + 1e-10) X[real_idx] = best_pos + np.abs(X[real_idx] - best_pos) @ A_plus X[real_idx] = np.clip(X[real_idx], self.lb, self.ub) return X

注意A @ A.T的结果是标量,因为 A 是 1 维数组。这里np.linalg.inv里加1e-10是为了防止奇异矩阵。实际运行中,如果维度较大,也可以直接用np.linalg.pinv(A.reshape(-1, 1)),但上面的写法更贴合原论文的向量化表达。

警戒者更新要判断每个警戒者个体是否处于最优位置。在代码层面,可以抽取适应度最小的个体作为最优,然后让警戒者分别走不同的逃逸分支:

def update_watchman(self, X, fitness, best_pos, worst_pos, best_fit, worst_fit): watch_num = int(self.pop_size * 0.1) fitness_flat = fitness.flatten() for i in range(watch_num): if fitness_flat[i] > best_fit: beta = np.random.normal(0, 1) X[i] = best_pos + beta * np.abs(X[i] - best_pos) else: K = np.random.choice([-1, 1]) * 0.5 denom = fitness_flat[i] - worst_fit + 1e-10 X[i] = X[i] + K * (np.abs(X[i] - worst_pos) / denom) X[i] = np.clip(X[i], self.lb, self.ub) return X

这里denom1e-10是必须的,因为当某个警戒者的适应度恰好等于最差适应度时,分母为零,会导致位置爆炸。可能你见过不加这个保护的代码,跑到一半出现 inf,然后整个种群就废了。

3.3 完整的主循环与目标函数接入

ssa.py中还需要一个run方法来串联初始化、迭代、记录收敛曲线。主循环里每次更新完生产者、加入者、警戒者后,都要重新计算所有个体的适应度,再更新全局最优。

def run(self): X = self.init_population() fitness = np.array([self.obj_func(x) for x in X]) best_iter = np.min(fitness) self.convergence.append(best_iter) for t in range(self.max_iter): X, best_pos, worst_pos, idx = self.update_producer(X, fitness.copy(), t) fitness = np.array([self.obj_func(x) for x in X]) X = self.update_follower(X, fitness, best_pos, worst_pos, idx) fitness = np.array([self.obj_func(x) for x in X]) # 更新全局最优位置用于警戒者 best_idx = np.argmin(fitness) worst_idx = np.argmax(fitness) X = self.update_watchman( X, fitness, X[best_idx], X[worst_idx], fitness[best_idx], fitness[worst_idx] ) fitness = np.array([self.obj_func(x) for x in X]) self.convergence.append(np.min(fitness)) best_idx = np.argmin(fitness) return X[best_idx], fitness[best_idx]

需要注意,update_producerupdate_follower里都使用了排序索引,所以在每次更新前必须传入最新的适应度副本。否则排序索引位会错位,导致个体更新张冠李戴。

main.py中定义一个带噪声的非凸回归函数,比如:

import numpy as np from ssa import SSA def obj_func(x): # 自定义回归误差函数,x 为待优化参数 a, b, c = x[0], x[1], x[2] # 构造一个多峰函数,模拟回归参数优化 z = (a - 1.5) ** 2 + (b + 2.0) ** 4 + np.sin(3 * c) + 0.1 * np.random.randn() return z if __name__ == "__main__": ssa = SSA(obj_func, lb=[-5, -5, -5], ub=[5, 5, 5], dim=3, pop_size=40, max_iter=200, ST=0.8, PD=0.2) best_x, best_f = ssa.run() print("最优解:", best_x) print("最优适应度:", best_f)

运行python main.py就能看到输出。这个目标函数故意设置了三次方的sin扰动,并且加了随机噪声,目的是模拟真实回归场景下数据噪声对适应度评估的影响。你会发现 SSA 的收敛曲线不会像纯数学函数那样光滑,这也是正常现象。

4. 收敛曲线可视化:matplotlib 绘图与判读

SSA 输出的收敛曲线能直观反映算法有没有收敛、是否早熟、是否存在剧烈震荡。这一章讲如何用 matplotlib 把每一代的最优适应度画出来,以及如何从图形上判断改进方向。

4.1 用 matplotlib 绘制收敛曲线

SSA类中self.convergence已经记录了每一轮的最优适应度值。在main.py中加入绘图逻辑:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False plt.figure(figsize=(8, 5)) plt.plot(ssa.convergence, color='#2E86AB', linewidth=1.8, label='SSA 收敛曲线') plt.xlabel('迭代次数') plt.ylabel('最优适应度') plt.title('麻雀搜索算法优化回归函数收敛过程') plt.grid(alpha=0.3) plt.legend() plt.tight_layout() plt.savefig('ssa_convergence.png', dpi=150) plt.show()

如果想把六张图合在一个图上做多组对比实验,可以用plt.subplot(2, 3, i)的方式循环绘制。比如固定种群规模 40,分别改变最大迭代次数 50、100、200,再看不同 ST 取值的曲线差异。这种可视化方式在论文实验里很常见,也比单条曲线更有说服力。

4.2 三种典型曲线形态与原因分析

第一种是快速下降后趋于平缓,说明算法在前期找到较好区域,后期在局部精修,属于正常收敛。第二种是曲线长期保持同一水平线没有变化,大概率是种群提前收敛,所有麻雀都挤在同一个局部最优附近,此时应调大 ST 或增加警戒者比例。第三种是曲线上下剧烈波动,最优适应度忽高忽低,说明种群在最优区域附近来回跳跃,通常因为生产者数量过少或警戒者逃逸步长过大。

从 Python 实现上看,震荡还有一个常见来源:update_follower中前半部分的加入者使用best_pos + |X_i - best_pos| @ A_plus更新,当A_plus求解不稳定时,更新后的位置会跳到很远。如果你画图发现相邻两代最优值差距超过几个数量级,优先检查np.linalg.inv是否出现数值异常,改用np.linalg.pinv通常能缓解。

5. 调参与验证的实战技巧

最后一章分享我在实际使用 SSA 时积累的几个验证和调试方法,这些技巧能帮你快速定位是算法实现问题还是参数选择问题。

5.1 先用简单函数验证算法正确性

接手任何 SSA 代码,第一步先拿 Sphere 函数(求和平方)跑一遍。Sphere 函数是单峰凸函数,理论上任何合理的智能算法都能在几十代内收敛到接近零。如果你在 Sphere 函数上都无法达到1e-5以下的适应度,说明代码实现有 bug,而不是参数问题。验证代码可以这样写:

def sphere(x): return np.sum(np.square(x)) sa = SSA(sphere, lb=[-10, -10], ub=[10, 10], dim=2, pop_size=30, max_iter=100) best_x, best_f = sa.run() assert best_f < 1e-5, f"Sphere 函数未收敛: {best_f}"

这个断言脚本可以作为回归测试,配合 CI 使用。很多开源优化库都用这种思路做单元测试,非常直接。

5.2 收敛曲线加 Log 尺度看早熟

当适应度值跨度很大时,线性坐标的收敛曲线会把前 20 代的快速下降压扁,后面缓慢下降的部分看不到细节。我一般会把纵轴改成对数坐标:

plt.yscale('log')

如果使用对数坐标后曲线仍然是一条水平线,说明算法确实没有进展。此时检查种群多样性:打印每隔 10 代所有个体位置的标准差,如果标准差趋近于 0,说明种群聚集,早熟已经发生。一个常见做法是在SSA类里增加diversity_history列表,每次迭代结束后计算np.mean(np.std(X, axis=0)),画双纵轴图同时显示适应度和多样性,能更快定位问题。

5.3 边界处理与随机数种子

最后强调两点。第一,目标函数内部如果用了np.random.randn()作为噪声,一定要在外部固定全局随机数种子(例如np.random.seed(42)),否则每次运行得到的最优值都不一样,很难判断算法改进效果。第二,SSA 的边界处理我统一用np.clip,但如果你对某个参数有特殊的边界约束(比如必须为正数),还需要在目标函数里加惩罚项,而不是单纯依赖算法边界。这两个小细节在写进论文对比实验时尤其重要,否则审稿人会质疑实验可重复性。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询