粒子群优化算法PSO入门:原理、Python实现、调参与排错
粒子群优化算法(PSO)在优化圈子里属于门槛最低的一类启发式方法:不用求导,不用构造复杂的代理模型,几十行代码就能跑起来,还能在黑箱问题上给出够用的解。我第一次真正把它用起来,是为了给一个仿真模型做参数寻优——目标函数没有解析表达式,只能反复跑仿真拿输出,梯度信息完全拿不到。那会儿先试网格搜索,参数维度一上到五六个,组合数就爆炸;又试随机搜索,结果波动大得没法交差。后来换成 PSO,虽然不保证全局最优,但稳定性、可调性和实现成本的平衡让我一直把它留在工具箱里。下面把我这些年踩过的坑、调过的参数、写过的代码整理一遍,从公式每一项的含义讲到可以直接复制的实现,再到调参和排查思路。刚接触启发式优化的读者能顺着走一遍,已经用过但总觉得结果不稳的读者,也能在参数整定和问题排查那几节找到对应的解法。
1. 先把PSO这件事说透:它到底在解什么问题
1.1 从鸟群觅食到参数空间搜索
PSO 的原始灵感来自群体觅食行为:一群鸟不知道食物在哪,但每只鸟知道自己离食物最近时所在的位置,也知道整个群体里谁离得最近,于是下一拍往“自己历史最好位置”和“群体历史最好位置”的合成方向飞。映射到优化问题上,一只鸟就是一个候选解,位置是参数向量,速度是下一次移动的方向和步长,食物就是目标函数的最小值点。
这个类比听着玄,但落到代码里非常直白。假设要优化的参数是三维向量,搜索范围是 [-5, 5],种群三十个粒子。初始化就是在这三个维度上各撒三十个随机点,每个点算一次目标函数值,记下每个粒子自己的历史最优pbest,再从中挑出全局最优gbest。之后每一代,粒子按速度公式移动,移动完重新评估,更新pbest和gbest,如此往复。整个过程没有矩阵求逆,没有导数,只有加减乘和随机数,这也是它在工程现场受欢迎的核心原因——目标函数是黑箱也能用。
需要提醒的是,PSO 求的是“够好的解”,不是“数学上证明的最优解”。它属于随机搜索家族,同一份代码跑两次结果可能不同,这是方法本身的属性,不是实现有 bug。把这一点想清楚,后面调参和评估结果时心态会稳很多。
1.2 PSO与梯度下降、网格搜索的分工边界
选方法之前先看清楚手里的问题长什么样,这比研究算法细节更重要。我把常见的几类方法放在一起对比,方便判断什么时候该拿 PSO 出来。
| 方法 | 是否依赖梯度 | 适用维度 | 结果确定性 | 单次评估成本敏感度 | 典型短板 |
|---|---|---|---|---|---|
| 梯度下降 / 拟牛顿 | 强依赖 | 中高维连续 | 确定 | 高(要算梯度) | 目标不可导直接失效 |
| 网格搜索 | 不依赖 | 低维(一般不超过4维) | 确定 | 极高 | 维度灾难,组合爆炸 |
| 随机搜索 | 不依赖 | 低中维 | 随机 | 中 | 无信息利用,效率低 |
| 贝叶斯优化 | 不依赖 | 低维 | 随机 | 极高(评估昂贵时最优) | 高维表现退化,实现复杂 |
| PSO | 不依赖 | 中低维 | 随机 | 中 | 可能早熟,参数需调 |
这张表里的关键判断点是两条:目标函数能不能求导,以及单次评估贵不贵。能求导、维度又高,老老实实上梯度类方法,别折腾启发式;维度只有两三个、评估一次要几分钟,贝叶斯优化往往比 PSO 更省评估次数;维度在 5 到 50 之间、目标函数不可导、评估一次在毫秒到秒级,这个区间正是 PSO 的舒适区。
还有一个容易被忽略的维度:解的“可解释性”要求。有些场景不只要一个最优点,还要看参数之间的取舍关系,这时候 PSO 只吐出一个gbest就不够用,得配合多次运行收集帕累托前沿,或者换成多目标版本。
1.3 该上PSO和不该上PSO的场景清单
直接给结论,方便对照自己的问题。
适合上手的情况:目标函数是黑箱,比如调用一次仿真软件、跑一次训练、做一次物理实验;参数维度在 5 到 50 之间;评估一次的时间在可接受范围内;对最优性要求是“比人工调的好、比随机搜的稳”,不要求证明全局最优;参数之间有耦合,单变量扫描看不出规律。
不建议硬上的情况:维度超过一百的连续问题,PSO 的搜索效率会明显下降,粒子在高维空间里“飞”的直觉不再成立;评估一次要几十分钟且预算只有几十次,这时候每一次评估都得精打细算,代理模型类方法更合适;目标函数有明确解析形式和梯度,用 PSO 属于绕远路;约束条件极其复杂、可行域窄成一条线,纯 PSO 加罚函数的成功率会低到让人怀疑人生,需要专门的约束处理手段。
我见过不少项目把 PSO 当万能锤,结果在一个光滑可导的凸问题上跑了半天还不如几行梯度下降。判断标准其实很简单:先问自己能不能写出梯度,能写就别用 PSO。
2. 算法骨架拆解:速度更新公式里每一项在干什么
2.1 位置、速度、个体最优、全局最优四个核心量
整个 PSO 就围绕四个量转,把它们的关系理清楚,代码基本就写出来了。
位置x:一个pop_size × dim的矩阵,每一行是一个候选解。速度v:同样形状的矩阵,控制下一次移动的幅度。个体历史最优pbest_x与pbest_f:每个粒子从出生到现在走过的位置里,目标函数值最小的那个位置和对应值。全局历史最优gbest_x与gbest_f:整个种群到目前为止的最好记录。
初始化阶段有个细节值得说:pbest一般直接初始化为初始位置,gbest从pbest里挑。这样做的好处是第一代就有可用的参考点,不会出现“第一代所有粒子都往零向量飞”的怪现象。我早期写的第一版把pbest初始化为全零,结果前几代粒子集体朝原点收缩,白白浪费了评估次数。
更新顺序也要注意:先评估当前位置,再更新pbest,再更新gbest,最后才进入下一代的速度计算。顺序错了会出现gbest落后一代的问题,表现为收敛曲线有轻微滞后,不容易发现但确实影响效果。
2.2 惯性项、认知项、社会项的分工与常见误读
标准速度更新公式是这样的:
v(t+1) = w * v(t) + c1 * r1 * (pbest_x - x(t)) + c2 * r2 * (gbest_x - x(t)) x(t+1) = x(t) + v(t+1)三项各有分工。第一项w * v是惯性项,保留上一拍的运动趋势,作用是维持搜索的“动量”,避免粒子被局部最优一把拽住。第二项c1 * r1 * (pbest - x)是认知项,把粒子往它自己见过的最好位置拉,体现个体经验。第三项c2 * r2 * (gbest - x)是社会项,把粒子往全群最好位置拉,体现信息共享。
r1和r2是每个维度独立采样的均匀随机数,取值范围 [0,1]。这一点常被简化成两个标量,效果会打折扣——逐维随机让不同维度有不同步长,搜索方向更丰富。我实测过标量版本和逐维版本在 Rastrigin 函数上的差异,逐维版本的收敛成功率明显更高,代价只是多生成一个随机矩阵,完全值得。
常见的误读是把惯性权重当成“学习率”来调。两者作用方向相反:学习率大意味着步子大、收敛快但容易越过最优点;惯性权重大意味着更信任上一拍的方向,探索性强但后期收敛慢。调参时这两者要分开考虑,别混在一起试。
2.3 速度钳制与边界处理这两道保险
速度上限v_max是 PSO 里最容易被省略、又最不该省略的东西。没有它,粒子在早期可能获得巨大速度,一步飞出搜索空间,被边界截断后卡在边界上,表现为“粒子全都贴在可行域边缘”。更糟的是飞出去之后目标函数可能返回nan或抛出异常,直接中断整个流程。
v_max的取值有个经验公式:v_max = k * (high - low),其中k通常取 0.1 到 0.2。举个例子,搜索范围是 [-5, 5],跨度是 10,取 k=0.2,那么v_max = 2,意思是单个维度一步最多移动 2,两三代就能横穿整个区间。这个计算过程建议写进代码注释里,后面换问题时照着改,不容易忘。
边界处理有三种常见做法,效果差别不小:
- 截断法:越界就拉回边界值。实现最简单,但粒子会贴在边界抖动,浪费评估次数。
- 反弹法:越界后按相反方向弹回,
x = 2*bound - x,同时速度反向。搜索更自然,适合边界确实代表物理限制的场景。 - 归零法:越界维度位置拉回边界,该维速度置零。贴边抖动少,我目前用得最多。
注意:用了反弹法就不要再叠加速度反向,容易造成两次反向互相抵消,粒子在边界附近来回震荡。选一种,测一下,别混用。
3. 一份可以直接运行的Python实现
3.1 数据结构设计与初始化
我习惯把 PSO 写成一个类,参数在构造函数里一次给全,这样换问题时只改入参不碰逻辑。核心成员包括目标函数、维度、上下界、种群规模、迭代次数、三个权重参数、速度比例和随机种子。
随机种子单独抽出来是有原因的。PSO 本身随机,调试时如果每次结果都不一样,根本没法判断是代码改对了还是运气好。固定种子跑通逻辑,再放开种子看统计分布,这个顺序能省掉大量排查时间。用numpy.random.default_rng(seed)而不是老式的np.random.seed,前者生成独立随机流,多个实验并行时不会互相干扰。
初始化位置用均匀分布在整个搜索区间撒点,初始化速度在[-v_max, v_max]上均匀采样。有人喜欢把初始速度设为零,我不推荐——零速度意味着第一代完全由认知项和社会项驱动,而这两项在初始时都指向pbest,粒子会集体朝同一个方向走,多样性损失得很快。
3.2 迭代主循环逐行说明
主循环里真正需要解释的是几处顺序和形状问题。
评估是逐行调用的,写成[func(x) for x in X],因为大多数工程目标函数只接受一维输入。如果你的函数支持批量输入,可以改成一次性传入矩阵,能省不少时间,这一点在第 7 节会展开。
better = f < self.pbest_f这行生成布尔掩码,然后用花式索引批量更新pbest。这种写法比 for 循环快一个数量级,而且不容易写出下标错误。
惯性权重w在循环开头按代数线性递减,这是最常用的策略,具体公式在第 4 节给。注意除数是max_iter而不是max_iter - 1,这样最后一代的w刚好落在w_min附近,边界干净。
速度更新之后立刻做np.clip钳制,再更新位置,再对位置做边界处理,顺序不能颠倒。位置更新完马上评估,评估完马上更新两个最优,一环扣一环。
3.3 完整代码与测试函数验证
下面这份代码我用了很久,拿去改改就能跑。
import numpy as np def sphere(x): return float(np.sum(x ** 2)) def rastrigin(x): n = len(x) return float(10 * n + np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x))) class PSO: def __init__(self, func, dim, low, high, pop_size=30, max_iter=300, w_max=0.9, w_min=0.4, c1=2.0, c2=2.0, v_ratio=0.2, seed=None): self.func = func self.dim = dim self.low = np.full(dim, low, dtype=float) self.high = np.full(dim, high, dtype=float) self.pop_size = pop_size self.max_iter = max_iter self.w_max = w_max self.w_min = w_min self.c1 = c1 self.c2 = c2 self.v_max = v_ratio * (self.high - self.low) self.rng = np.random.default_rng(seed) self.x = self.rng.uniform(self.low, self.high, size=(pop_size, dim)) self.v = self.rng.uniform(-self.v_max, self.v_max, size=(pop_size, dim)) self.pbest_x = self.x.copy() self.pbest_f = np.full(pop_size, np.inf) self.gbest_x = None self.gbest_f = np.inf self.history = [] def _evaluate(self, X): return np.array([self.func(x) for x in X]) def _update_best(self): f = self._evaluate(self.x) better = f < self.pbest_f self.pbest_x[better] = self.x[better] self.pbest_f[better] = f[better] idx = int(np.argmin(self.pbest_f)) if self.pbest_f[idx] < self.gbest_f: self.gbest_f = float(self.pbest_f[idx]) self.gbest_x = self.pbest_x[idx].copy() def optimize(self): self._update_best() for t in range(self.max_iter): w = self.w_max - (self.w_max - self.w_min) * t / self.max_iter r1 = self.rng.random((self.pop_size, self.dim)) r2 = self.rng.random((self.pop_size, self.dim)) self.v = (w * self.v + self.c1 * r1 * (self.pbest_x - self.x) + self.c2 * r2 * (self.gbest_x - self.x)) self.v = np.clip(self.v, -self.v_max, self.v_max) self.x = self.x + self.v out = (self.x < self.low) | (self.x > self.high) self.x = np.clip(self.x, self.low, self.high) self.v[out] = 0.0 self._update_best() self.history.append(self.gbest_f) return self.gbest_x, self.gbest_f, self.history跑一下验证:
if __name__ == "__main__": for name, fn, bound in [("sphere", sphere, 5.0), ("rastrigin", rastrigin, 5.12)]: pso = PSO(fn, dim=10, low=-bound, high=bound, pop_size=40, max_iter=500, seed=42) best_x, best_f, hist = pso.optimize() print(name, "最优值 =", round(best_f, 6), "前10代 =", [round(v, 3) for v in hist[:10]])十维 Sphere 函数上,这套参数一般能收敛到 1e-3 以下;Rastrigin 有大量局部极小,结果会差一些,通常在个位数到十几之间徘徊,属于正常现象。如果 Rastrigin 上的结果每代都在早期就定死不动,基本可以判定是早熟,第 6 节有对应的排查手段。
4. 参数整定:把默认值调到能交差
4.1 种群规模与迭代次数
种群规模决定了搜索的覆盖面。太小(少于 10)时多样性不足,容易整群被一个局部最优一锅端;太大(超过 100)时每代评估次数暴涨,而收益递减明显。我的经验区间是:
| 参数维度 | 推荐种群规模 | 说明 |
|---|---|---|
| 2 到 5 维 | 15 到 20 | 小种群就够,多出来的粒子纯浪费 |
| 5 到 20 维 | 30 到 50 | 最常用的区间 |
| 20 到 50 维 | 50 到 100 | 需要更多样本覆盖空间 |
| 50 维以上 | 100 起 | 同时认真考虑换方法 |
迭代次数跟评估预算直接挂钩。总评估次数约等于种群规模 × 迭代次数,如果你的目标函数一次要跑两秒,预算半小时,那总评估次数上限就是九百次左右,种群三十、迭代三十,刚好卡住。先算预算再定参数,别反过来。
提示:与其把迭代次数堆到两千看着收敛曲线很漂亮,不如用同样的评估预算跑五次独立的短程优化取最优。随机算法单次长时间运行的边际收益,往往低于多次重启。
4.2 惯性权重的三种策略
惯性权重是 PSO 调参的第一旋钮,常见三种打法。
固定值:w = 0.729,来自收缩因子的等价推导,通用性最好,不知道选什么就用它。
线性递减:w(t) = w_max - (w_max - w_min) * t / T,典型取值从 0.9 降到 0.4。思路是前期重探索、后期重开发,实现只多一行代码,绝大多数场景都吃这一套。
非线性递减:比如按w = w_min + (w_max - w_min) * exp(-k * t / T)衰减,前期保持高位更久,后期掉得更快。适合搜索空间大、需要较长时间全局探索的问题,但多了一个k要调,我一般只有在默认方案效果不佳时才上。
实测感受是:固定值在简单问题上够用,但只要问题稍微复杂一点,线性递减基本都能带来可见的提升,属于性价比最高的一档改进。
4.3 学习因子与收缩因子
c1和c2分别控制个体经验和群体信息的影响权重。经典配置是两者都取 2.0,简单好记;另一种常见配置是c1 = c2 = 1.49445,配合收缩因子使用,理论上保证收敛性。
收缩因子的推导过程值得知道,因为它是“为什么这组数能用”的依据。设φ = c1 + c2,当φ > 4时,收缩系数为:
χ = 2 / |2 - φ - sqrt(φ² - 4φ)|取c1 = c2 = 2.05,φ = 4.1,代进去算出χ ≈ 0.7298。等价地,可以理解为速度公式里的权重取w = 0.7298、学习因子同乘χ得到 1.496。这就是前面那个魔数 0.729 的来源。知道来路之后,就不必把它当玄学常数供着,需要时可以自己算一组。
调c1和c2的相对大小能改变搜索风格:c1偏大,粒子更相信自己的经验,种群分散度高,探索性强;c2偏大,粒子更快向群体最优靠拢,收敛快但早熟风险高。我处理过一个参数耦合严重的模拟项目,把c1调到 2.5、c2降到 1.5,收敛速度慢了一些,但最终结果明显更稳。
4.4 参数速查表
把上面这些整理成一张表,换问题时直接查。
| 参数 | 常用取值 | 调整方向 | 影响 |
|---|---|---|---|
| 种群规模 | 30 到 50 | 多样性不足就加大 | 评估成本线性上升 |
| 迭代次数 | 200 到 500 | 收敛未完成就加大 | 同上 |
w_max/w_min | 0.9 / 0.4 | 后期抖动就降w_min | 探索与开发的平衡 |
c1 | 1.5 到 2.5 | 太集中就加大 | 个体经验权重 |
c2 | 1.5 到 2.5 | 收敛太慢就加大 | 群体信息权重 |
v_ratio | 0.1 到 0.2 | 越界频繁就调小 | 单步最大跨度 |
5. 实战场景拆解:从连续函数到带约束问题
5.1 连续无约束:测试函数上的正确性验证
拿到一份新实现,第一步永远是拿标准测试函数验证。我固定用三个:Sphere 检验基础收敛能力,Rastrigin 检验跳出局部最优的能力,Rosenbrock 检验在狭长谷底中的搜索能力。
验证时别只看最终值,要看三件事。其一,收敛曲线是否单调下降——gbest是历史最优,理论上不会变差,如果曲线出现回升,说明最优更新逻辑写错了。其二,多次运行的方差有多大,固定其他条件跑二十次,看结果的分布区间,区间宽说明稳定性差。其三,粒子是否在早期就挤成一团,可以打印每一代所有粒子到gbest的平均距离,这个指标能直观反映多样性。
这里给一段评估多样性的代码,加在_update_best之后调用即可:
dist = np.linalg.norm(self.x - self.gbest_x, axis=1).mean() self.diversity.append(float(dist))前面二十代这个值快速下降是正常的,但如果它在第十代就掉到接近零并且再也不变,那就是早熟,别犹豫,直接看第 6 节。
5.2 带约束问题:罚函数与修复策略
工程问题几乎都带约束。最简单的处理是罚函数,把约束违反量折算成目标函数的惩罚项:
F(x) = f(x) + λ * Σ max(0, g_i(x))²λ的选取是关键。太小,粒子会大摇大摆地在不可行域里游荡;太大,目标函数被惩罚项主导,等于在优化一个“离约束边界远近”的问题。我的做法是先估一个量级:计算初始种群中不可行解的约束违反量均值,取λ使得惩罚项在初始阶段与f(x)的典型量级相当,然后随着迭代代数递增,前期容忍轻微越界以保持探索,后期收紧逼向可行域。
罚函数不是万能的。可行域极窄时,随机撒点几乎撒不到可行解,粒子群会集体在不可行域里打转。这种场景更适合修复策略:设计一个把任意解映射到可行域的修复算子,先修复再评估。修复算子跟具体问题强相关,需要针对约束结构专门设计,没有通用模板,但思路是一致的——让每个粒子始终代表一个合法解,PSO 只负责在可行域内搜索。
还有一种做法是把约束违反量作为额外的优化目标,转成双目标问题,用多目标 PSO 求解。代价是复杂度上去了,收益是能看到可行性与目标值之间的取舍曲线,适合需要决策依据的场景。
5.3 离散问题:二进制PSO改造
标准 PSO 是连续版的,遇到 0-1 决策问题(比如特征选择、任务分配)需要改造。主流做法是二进制 PSO:速度仍然连续更新,但把速度通过 sigmoid 映射成取 1 的概率。
s(v) = 1 / (1 + exp(-v)) x_d = 1, 若 rand() < s(v_d);否则 0代码上把位置更新那一步换掉就行:
def _binarize(self, v): prob = 1.0 / (1.0 + np.exp(-v)) return (self.rng.random(v.shape) < prob).astype(float)这里有个坑:速度不再代表“移动距离”,而代表“倾向性”,所以速度钳制的范围要改,通常钳在[-6, 6],因为 sigmoid 在这个区间外基本饱和,再大的值没有信息量。另外二进制版本的位置不具备“距离”概念,认知项和社会项里的pbest - x计算出来是 -1、0、1 三值,搜索粒度粗,收敛后期容易在几个解之间反复横跳。常见补救是加一个变异操作,每代以很小的概率随机翻转若干位。
6. 常见问题与排查实录
6.1 早熟收敛的诊断与破局
早熟是 PSO 最典型的毛病:所有粒子在几十代内挤到同一个点附近,而这个点往往是局部最优。诊断方法前面提过,看粒子到gbest的平均距离,掉得太快就是信号。
破局手段按代价从低到高排:第一,加大种群规模,最简单直接;第二,调小c2或加大c1,削弱群体吸引力;第三,加入变异,每代随机重置一小部分粒子的位置,等于强制注入多样性;第四,多群并行,把种群拆成几个子群各自演化,隔若干代再交流最优解,这个思路在复杂问题上效果明显。
我常用的是第三种加第四种的组合,实现不复杂但很管用。变异概率一般取 0.05 到 0.1,重置时位置在搜索空间内均匀采样,速度重新随机。注意别把gbest也一起重置了,那只记录历史最优,不能动。
6.2 结果不可复现与波动过大的处理
同一份代码两次跑出差别很大的结果,先分清是随机性还是 bug。判断方法很简单:固定种子跑两次,结果应该完全一致。不一致说明代码里还有别的随机源或者状态没清干净,比如用了全局的np.random又没重置。
固定种子一致、放开种子差异大,那就属于随机算法的正常波动,处理思路是多次重启。跑十到二十次,取最优的那次作为最终结果,同时记录所有运行的分布。如果分布的长尾特别长,说明偶尔会掉进很差的局部最优,这时候要么增加迭代次数,要么上变异策略。
还有个容易忽略的点:如果目标函数本身带随机性(比如仿真里有随机采样),那评估结果就不是确定的,PSO 会被噪声误导,把偶然偏低的解当成好解。处理办法是每次评估重复多次取均值,或者用公共随机数让不同解在相同噪声条件下比较。这个坑踩过一次就记住了,代价是评估次数翻几倍。
6.3 问题速查表
把实际运行中遇到的典型现象整理成表,方便对照排查。
| 现象 | 可能原因 | 排查动作 | 处理方案 |
|---|---|---|---|
| 前二十代就收敛且结果差 | 早熟,多样性不足 | 打印粒子到 gbest 的平均距离 | 加变异、加大种群、降 c2 |
| 收敛曲线非单调 | 最优更新逻辑有误 | 检查 pbest/gbest 更新顺序 | 先评估再更新,gbest 从 pbest 取 |
| 粒子大量贴在边界 | 速度上限过大或未设 | 统计越界粒子比例 | 设 v_max,贴边维速度归零 |
| 后期在最优点附近抖动 | 惯性权重或速度过大 | 打印每代 w 和平均速度 | 线性递减 w,压低 v_max |
| 目标值出现 nan | 参数越界进非法定义域 | 检查评估函数的输入范围 | 加边界裁剪或罚函数 |
| 多次运行结果差异极大 | 迭代不足或陷入不同局部最优 | 固定种子跑二十次看分布 | 增加迭代,多次重启取最优 |
| 评估次数远超预算 | 种群规模和迭代次数乘积过大 | 统计总调用次数 | 按预算反推参数 |
7. 工程化改进与性能优化
7.1 评估函数是真正的成本中心
PSO 本身的矩阵运算开销微乎其微,时间几乎全花在目标函数评估上。优化方向应该放在减少评估次数和缩短单次评估时间上,而不是去优化速度更新那几行。
减少评估次数的实用技巧有两个。一是缓存,如果参数空间里有重复或高度相似的解,把评估结果存起来直接查表,尤其在离散问题上命中率很高。二是早停,如果某一代之后gbest连续几十代没有改进,而且预算紧张,可以提前终止,把剩余预算留给重启。
缩短单次评估时间则依赖具体场景,比如把仿真模型的计算精度调低一档、用查表替代实时计算、把串行评估改成并行。这些优化带来的收益通常比调 PSO 参数大得多。
7.2 向量化与并行
如果目标函数支持批量输入,把逐行评估改成一次性传入整个矩阵,能省掉大量 Python 层的循环开销。改法很简单:
def _evaluate(self, X): return np.asarray(self.func(X), dtype=float).ravel()对应的目标函数要写成接受二维数组、返回一维数组的形式,用 numpy 的按行运算实现。这种改法在评估本身很轻的时候收益不明显,但在评估包含大量向量运算时提升可观。
目标函数之间完全独立,天然适合并行。用进程池把种群分成若干块分发出去,评估完再汇总,能直接把墙钟时间按核数缩短。注意进程间的数据传输成本,如果单个解很小、评估很快,并行的调度开销可能盖过收益,这时候批量分发更划算。
7.3 混合策略与局部精修
纯 PSO 在后期收敛精度上不如局部搜索方法。一个实用的组合是:先用 PSO 做全局搜索,跑若干代后拿gbest作为初值,接一个局部搜索(比如模式搜索或简单的坐标轮换),在最优解附近精细打磨。这个组合兼顾了全局覆盖和局部精度,实现上只是多调一个函数。
另一个方向是自适应参数。让w、c1、c2随搜索状态动态调整,比如根据种群多样性指标反馈控制:多样性高的时候降低w加快收敛,多样性低的时候提高w或加大变异概率。这类自适应策略的论文很多,实际用起来不用追求复杂,一两条简单的反馈规则就能带来可见改善。
最后说一个务实的判断:如果 PSO 跑了很多次、调了很多参数,结果始终达不到要求,别继续在算法内部打转。先回去看目标函数的定义有没有问题,约束边界是否合理,参数范围是否给对了。我遇到过的情况里,至少有一半的“算法不行”,最后查出来是目标函数写错或者搜索范围设窄了。
我个人在实际操作中的体会是,PSO 这类方法的价值不在于它能找到多精确的最优解,而在于它能在你对问题几乎一无所知的时候,给出一个可用的起点。真正让结果变好的,往往是对问题本身的理解加深,而不是把参数从 2.0 调到 2.05。所以每次调参之前,先花十分钟想清楚这个问题的物理意义和参数之间的耦合关系,比盲试一百组参数管用得多。