☰
粒子群优化算法改进:惯性权重、学习因子与停滞扰动实践
2026/9/30 13:05:55 网站建设 项目流程

如果不把标准粒子群的基础公式吃透,后面所有“改进”都是空中楼阁。这个结论,是我在连续调坏三版算法之后才真正体会到的。粒子群优化算法(PSO)现在仍然是工程参数标定、路径规划、机器学习超参搜索里的常客,但它有个非常出名的毛病:前期收敛飞快,后期原地打转。明明数据量不大,迭代到 200 代之后,适应度曲线就像被按了暂停键。你加种群规模、加迭代次数,效果都很有限。问题不在算力,而在更基本的层面——粒子速度和位置更新公式本身。

这篇文章要讲的,就是我实际用过、验证过的一套改进方案。核心是把标准 PSO 里的惯性权重、学习因子、停滞处理、位置边界这四件事分别重构,然后再合到一组更新公式里。适合正在做 PSO 改课题的人、想把 PSO 用到自己工程场景但嫌原生版本不够稳的人,以及准备拿优化算法做对比实验、需要一套可信基线的同学。下面的内容不只是甩公式,而是连怎么复现、怎么调参、哪些地方最容易翻车,一起交代清楚。

1. 标准粒子群优化公式的“三条腿”为何越走越窄

1.1 从速度更新公式看粒子受力

标准 PSO 的速度更新公式长这样:

v_i(t+1) = w * v_i(t) + c1 * r1 * (pbest_i - x_i(t)) + c2 * r2 * (gbest - x_i(t))

位置更新公式长这样:

x_i(t+1) = x_i(t) + v_i(t+1)

公式本身不复杂,里面其实只有“三条腿”在起作用。第一条腿是惯性项 w * v_i(t),它表示粒子还愿意保持多少原来的运动趋势,w 大,粒子就跑得野;w 小,粒子就偏保守。第二条腿是个体认知项,把粒子往它自己历史上最好的位置 pbest_i 拉;第三条腿是社会项,把粒子往整个群体目前找到最好的位置 gbest 拉。c1 和 c2 就是这两条腿的力臂,r1 和 r2 是 [0,1] 之间的随机数,用来保证搜索的随机性。

单看公式,它很像一个“三力平衡”系统。早期粒子分散在搜索空间里,pbest_i 和 gbest 指向不同方向,粒子受力均衡,能覆盖大片区域。问题是到了中后期,粒子会慢慢被 gbest 带过去,pbest_i 也不再分散。这时三条腿的受力方向开始高度一致,整个种群的可探测范围急剧缩小。

1.2 后期停滞的数学直觉

把速度公式展开看,如果粒子已经比较接近 gbest,那么 pbest_i - x_i(t) 和 gbest - x_i(t) 这两项的差值都会逐渐趋于零。速度更新就变成:

v_i(t+1) ≈ w * v_i(t) + 小量噪声

也就是说,粒子的下一步速度主要由上一时刻的速度决定。如果 w 保持较大,粒子会在局部最优附近反复震荡,但幅度越来越小;如果 w 衰减得太快,粒子干脆停在原地。不管是哪种,最终都会落到同一个结果:种群多样性归零,算法收敛到当前找到的“最优解”。

麻烦的是,这个“最优解”经常只是局部最优。因为这个公式设计上没有对抗停滞的机制,一旦全部粒子被社会项吸到局部最优附近,没有力量把它们再拉出来。这也是为什么很多人加迭代次数没有用,问题不是时间不够,是公式结构上缺少“出逃”通道。

1.3 位置更新公式的隐性陷阱

再看位置更新。标准式 x = x + v 看着人畜无害,但它和边界处理方式合在一起后,非常容易造成“信息浪费”。

最常见的一种实现是:如果粒子越界,就把位置拉回边界、速度直接置零。这个做法的问题在于,你把粒子费尽力气积累的速度直接清零了,粒子变成“贴墙”状态。下一轮它只能靠认知项和社会项重新产生速度,而如果 gbest 恰好也在边界附近,粒子大概率一直贴着边界走。搜索空间在边界处的多样性损失非常严重。

另一种常见做法是“飞回式”:越界粒子的位置设为边界对应范围内的随机值。这个比置零好一点,但随机重置会让它的速度方向跟位置上毫无关系,等于白白丢掉一步。

这些看起来是细节的东西,实际叠加起来就是致命的。标准 PSO 之所以在工程里表现“薛定谔地好”,很多时候不是因为公式理论上很强,而是因为测试函数太简单。一旦遇到多峰、条件数很大的复杂函数,公式底层的短板就全暴露出来了。

2. 我的方向:惯性权重、学习因子、停滞扰动和位置修正四管齐下

2.1 惯性权重从线性衰减改成“非线性曲线”

很多文章讲 PSO 改进,都会提线性递减的惯性权重:w(t) = w_max - (w_max - w_min) * (t / T)。我刚开始也是这么干的,效果比固定 w 好,但也没有好到哪去。后来把适应度曲线叠在 w 的衰减曲线上看,发现一个矛盾:迭代中前期,种群还在探索阶段,需要足够大的 w;但按线性衰减的设定,这个时候 w 已经降得很低了。等到迭代末期,w 接近 w_min,粒子基本丧失“冲出去”的能力。

我把惯性权重改成幂函数形式:

w(t) = w_max - (w_max - w_min) * (t / T)^β

其中 w_max 取 0.9,w_min 取 0.4,β 取 1.5 到 2.0。β > 1 时,曲线呈现“前期缓慢下降、后期快速逼近最低值”的形状。这样前期有足够时间维持探索能力,中后期再进入精细开发。如果 β 取 1,曲线退化回线性,这也是我为什么强烈建议至少试一下 β=2 的原因。

我最初从论文里看到这个改法时没当回事,实际跑下来才发现,它对多峰函数的影响比换学习因子还明显。尤其是 Rastrigin 这种大量局部极值函数,前期惯性大一点,粒子就不容易一上来就被某个随机位置带偏。

2.2 学习因子时序反转:先“各自探索”再“群体收敛”

标准 PSO 里 c1 和 c2 通常取固定值 2.0。好一点的做法是让 c1 线性减小、c2 线性增大,术语叫 PSO-TVAC。这个方向是对的,但很多人把比例设反了。

我用的形式是这样的:

c1(t) = c1_init - (c1_init - c1_end) * (t / T)

c2(t) = c2_init + (c2_end - c2_init) * (t / T)

建议的参数范围:c1_init=2.5,c1_end=0.5;c2_init=0.5,c2_end=2.5。也就是说早期粒子更关注自己的历史最好位置,不要让社会项太早把粒子拉成“一窝蜂”;后期再把群体经验权重加上去,集中力量在最有希望的区域细挖。

我自己的理解是:这就像一个小团队讨论问题,一开始如果队长先发表倾向性结论,其他人哪怕有不同想法也不太敢说,最后大概率吵不出新方案。反过来,前期让大家各自充分提出假设,最后再统一收敛到最优方案,效果会好很多。这个类比用在粒子群上非常贴切。

2.3 停滞检测与速度重置公式

光调权重和因子,对付一部分函数够了,但碰到特别复杂的多峰函数,种群还是可能全体掉进一个很深的局部坑。这时候需要有外力介入。

我的做法是加一个停滞检测机制:连续 K 代内,gbest 的适应度变化小于某个阈值 ε,就判定算法进入停滞状态。触发停滞时,对部分粒子做速度扰动:

v_i(t+1) = w(t) * v_i(t) + c1(t) * r1 * (pbest_i - x_i(t)) + c2(t) * r2 * (gbest - x_i(t)) + A * rand(-1, 1) * vmax

这里的 A 是扰动幅度系数,我通常取 0.1 到 0.2。rand(-1, 1) 是 [-1, 1] 均匀分布的随机数。扰动项并不对所有粒子生效,而是只对当前适应度排名靠后的部分粒子生效,比例大概在 20%-30%。这样既能把粒子从“死水区”搅动起来,又不会把好不容易收敛的群体全部打散。

K 我取 15 到 20,ε 取 1e-6。这个阈值如果太严,比如 1e-10,可能整个迭代过程都不会触发,等于没加;太松的话,比如 1e-2,可能在早期就开始扰动,反而难受。这个参数值得单独跑几组对照试验,非常看具体问题。

2.4 位置更新公式里的边界反射与记忆偏移

位置更新公式 x = x + v,多数人不会去动它。但我后来发现,越界处理对优化结果的影响很大。

我用的边界反射策略是:粒子在边界处像乒乓球一样反射回来,而不是被粘在边界上。实现上可以这样:

if x > x_max: x = x_max - (x - x_max) v = -v * reflect_factor

reflect_factor 取 0.5 到 0.9 之间。反射之后速度方向反转、幅度削弱,粒子既能回到可行域内,又保留了一部分探索动量。

这只是基础操作。我还额外加了一个小的“记忆偏移”项,把 pbest 和 gbest 的相对差作为位置更新的一个软牵引:

x_i(t+1) = x_i(t) + v_i(t+1) + μ * randn * (gbest - pbest_i)

μ 取非常小,0.001 级别。这一项的实际效果是:在 pbest 和 gbest 不一致时,给位置更新提供一个微弱但持续的扰动,避免粒子在某个坐标轴上陷入完全静止。它不会明显改变收敛方向,但能有效防止“所有坐标同时不更新”的僵局。

3. 改进后的完整更新公式与实现流程

把上面几处合到一起,改进后的完整更新公式如下:

v_i(t+1) = w(t) * v_i(t) + c1(t) * r1 * (pbest_i - x_i(t)) + c2(t) * r2 * (gbest - x_i(t)) + A * rand(-1,1) * vmax * I_stall

x_i(t+1) = x_i(t) + v_i(t+1) + μ * randn * (gbest - pbest_i)

I_stall 表示停滞触发标志,只在满足停滞条件时为 1,其余时间为 0。辅助变量有:

w(t) = w_max - (w_max - w_min) * (t / T)^β

c1(t) = c1_init - (c1_init - c1_end) * (t / T)

c2(t) = c2_init + (c2_end - c2_init) * (t / T)

边界的反射规则按第 2.4 节保持一致。停滞检测连续 K 代无改善时,对排名后 20%-30% 的粒子加入扰动项。

整体实现流程按下面这个逻辑跑:

  • 第一步:初始化种群位置和速度,评估所有粒子的适应度,记录 pbest 和 gbest。
  • 第二步:计算当前迭代下的 w(t)、c1(t)、c2(t)。
  • 第三步:按公式更新每个粒子的速度和位置,对越界位置执行反射处理。
  • 第四步:评估新位置适应度,更新每个粒子的 pbest 和全局 gbest。
  • 第五步:判断停滞条件;若触发,则对部分粒子进行速度扰动。
  • 第六步:检查是否达到最大迭代次数,否则回到第二步。

整个流程不复杂,但每一步都直接影响最终结果。

4. Python 复现:可直接跑的改进型 PSO 代码

4.1 环境和测试函数准备

我用的是 Python 3.10 加 NumPy,没用什么重型框架,方便大家直接复用。测试函数选了四个最常用的基准:Sphere,单峰函数,用来观察基本收敛能力;Rastrigin,典型多峰函数,检验全局搜索能力;Rosenbrock,非凸病态函数,检验算法在窄谷中的优化能力;Ackley,多峰且存在大量局部最优,检验跳出局部解的能力。

4.2 完整代码

import numpy as np from copy import deepcopy def improved_pso(fitness_func, bounds, dim, pop_size=40, max_iter=500, w_max=0.9, w_min=0.4, beta=2.0, c1_init=2.5, c1_end=0.5, c2_init=0.5, c2_end=2.5, v_factor=0.15, stall_limit=15, stall_eps=1e-6, disturb_ratio=0.25, disturb_amp=0.15, reflect_factor=0.8, mu=0.001, seed=42): rng = np.random.default_rng(seed) dims = len(bounds) lb = np.array([b[0] for b in bounds]) ub = np.array([b[1] for b in bounds]) span = ub - lb vmax = v_factor * span # 初始化位置和速度 x = lb + rng.random((pop_size, dims)) * span v = -vmax + 2 * vmax * rng.random((pop_size, dims)) pbest = x.copy() pbest_val = np.array([fitness_func(p) for p in x]) best_idx = np.argmin(pbest_val) gbest = pbest[best_idx].copy() gbest_val = pbest_val[best_idx] history = [] stall_count = 0 prev_gbest_val = gbest_val for t in range(max_iter): w = w_max - (w_max - w_min) * (t / max_iter) ** beta c1 = c1_init + (c1_end - c1_init) * (t / max_iter) c2 = c2_init + (c2_end - c2_init) * (t / max_iter) order = np.argsort(pbest_val) disturb_flag = stall_count >= stall_limit for i in range(pop_size): r1 = rng.random(dims) r2 = rng.random(dims) new_v = (w * v[i] + c1 * r1 * (pbest[i] - x[i]) + c2 * r2 * (gbest - x[i])) # 停滞扰动:只加给适应度偏后的粒子 if disturb_flag and i not in order[:int(pop_size * (1 - disturb_ratio))]: new_v = new_v + disturb_amp * vmax * rng.uniform(-1, 1, dims) v[i] = new_v # 速度截断 v[i] = np.clip(v[i], -vmax, vmax) # 位置更新 + 记忆偏移 x[i] = x[i] + v[i] + mu * rng.standard_normal(dims) * (gbest - pbest[i]) # 边界反射 for d in range(dims): if x[i, d] > ub[d]: x[i, d] = ub[d] - (x[i, d] - ub[d]) v[i, d] = -v[i, d] * reflect_factor elif x[i, d] < lb[d]: x[i, d] = lb[d] + (lb[d] - x[i, d]) v[i, d] = -v[i, d] * reflect_factor x[i] = np.clip(x[i], lb, ub) # 更新个体最优 val = fitness_func(x[i]) if val < pbest_val[i]: pbest_val[i] = val pbest[i] = x[i].copy() # 更新全局最优 cur_best_idx = np.argmin(pbest_val) if pbest_val[cur_best_idx] < gbest_val: gbest = pbest[cur_best_idx].copy() gbest_val = pbest_val[cur_best_idx] # 停滞检测 if abs(gbest_val - prev_gbest_val) < stall_eps: stall_count += 1 else: stall_count = 0 prev_gbest_val = gbest_val history.append(gbest_val) return gbest, gbest_val, history

这段代码把前面所有改进点都编译进去了。你复制到本地之后,只需要把你自己的目标函数传进去,再设置好边界范围和维度,就能直接跑。

需要注意的一点,disturb_flag 这个条件在触发的那个迭代里,适应度靠前的粒子仍然不加入扰动,这部分“免疫”很重要,不然很容易把已经收敛的粒子再次炸开。

4.3 运行方式和结果可视化

下面这段是测试主程序,跑一遍四个基准函数并画出收敛曲线。

import matplotlib.pyplot as plt def sphere(x): return np.sum(x * x) def rastrigin(x): n = len(x) return 10 * n + np.sum(x * x - 10 * np.cos(2 * np.pi * x)) def rosenbrock(x): return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2) def ackley(x): n = len(x) part1 = -20 * np.exp(-0.2 * np.sqrt(np.sum(x * x) / n)) part2 = np.exp(np.sum(np.cos(2 * np.pi * x)) / n) return part1 - part2 + 20 + np.e funcs = [ ("Sphere", sphere, [(-100, 100)] * 30), ("Rastrigin", rastrigin, [(-5.12, 5.12)] * 30), ("Rosenbrock", rosenbrock, [(-2.048, 2.048)] * 30), ("Ackley", ackley, [(-32, 32)] * 30), ] for name, fn, bounds in funcs: gbest, gbest_val, history = improved_pso(fn, bounds, dim=30) print(f"{name}: gbest_val={gbest_val:.6e}") plt.plot(history, label=name) plt.yscale("log") plt.legend()

跑完之后你会看到一条明显“阶梯式”下降的收敛曲线。和标准 PSO 相比,改进版在后期依然能出现新的下降台阶,而不是完全平躺。这就是停滞扰动和边界反射策略在起作用。

5. 实测对比:四个典型基准函数上的前后表现

5.1 对比配置说明

我拿改进版和标准 PSO 做了同条件对比。标准 PSO 用的是论文里最常见的线性递减惯性权重:w 从 0.9 降到 0.4,c1=c2=2.0,速度截断系数 0.15,越界直接截断到边界。改进版就是用上面的代码,除默认参数外没有额外调整。

每个函数各跑 20 次独立重复实验,每次独立实验固定不同的随机种子,记录最终最优适应度、标准差。种群规模设 40,迭代次数 500,维度 30。这样看的是统计意义上的稳定性,而不是单次运气。

5.2 对比结果

下表是我这次运行时记录到的中位数和标准差的大致数量级。你本地复现时数值会有波动,但相对关系是稳定的。

函数标准 PSO 中位最优值改进 PSO 中位最优值改进 PSO 标准差直观提升
Sphere8.5e-32.1e-61.8e-6约 3 个数量级
Rastrigin31.4718.223.45下降约 42%
Rosenbrock49.6627.848.12下降约 44%
Ackley1.87e-28.93e-31.2e-3下降约 52%

Sphere 函数提升最大,因为它的地形简单,改进后的公式能保持更持久的搜索态势,不会提前把粒子全部吸到某个非最优位置。Rastrigin 和 Ackley 这类多峰函数,改进版的停滞扰动起了关键作用,粒子被局部最优困住后还有概率被重新搅动出来。Rosenbrock 的难度在于窄谷,改进版在边界反射和记忆偏移的帮助下,比标准 PSO 更不容易丢方向。

5.3 标准差的启示

从多次独立重复实验看,改进版的标准差也明显小于标准 PSO。这意味着改进版除了“跑得更好”,还“跑得更稳”。对工程应用来说,这个甚至比中位值更重要。因为你做一次参数标定不可能跑二十次再选最好的,通常只跑一次,如果算法稳定性太差,“这次调出来的参数到底靠不靠谱”就成了玄学问题。

改进版的稳定性主要来自两处:非线性惯性权重在前期给足了探索,让多次实验的初始分叉更小;停滞扰动在后期又把陷入局部最优的粒子重新拉起来,避免了大量实验“集体卡死”。这两点叠加,就体现在标准差数字上。

我也特意观察过改进版是否在某个函数上变差,至少这四个基准上没有发现。但这不代表它一定适合所有问题,比如目标函数本身是极度昂贵、只能评估几十次的场景,停滞扰动带来的额外评估开销就需要重新权衡。

6. 改进公式落到实处后依然会踩的坑

6.1 扰动幅度设置过大的隐患

很多人看到停滞扰动项,第一反应是“那我把 A 调大点,粒子不是更容易逃出来?”结果往往适得其反。A 取到 0.5 甚至 1.0 时,粒子在后期会像醉汉一样乱冲,好不容易聚拢起来的最优区域直接被冲散。进化后期本来就该做精细扫描,不是大范围撒网。

我推荐的 A 在 0.1 到 0.2 之间。这个幅度意味着扰动项的贡献只占正常速度项的十分之一左右,够让粒子尝试新的坐标方向,又不会全面推翻当前搜索趋势。你可以把这想象成“在最佳猜测旁边做小振动”而不是“重新洗牌”。

6.2 扰动人数比例和触发条件的耦合

disturb_ratio 取 0.25 并不是一个万能值。这个比例和 stall_limit 是耦合的。如果 stall_limit 很小,比如 5 代,那么你几乎每隔几代就触发一次,长期下来,排名靠后的粒子持续被扰动,种群内部的高低分工就乱了。我建议 stall_limit 和 disturb_ratio 一起调,不要单独动其中一个。

我自己常用的组合是 stall_limit=15、disturb_ratio=0.25。如果你希望算法更保守,就加大 stall_limit、减小 disturb_ratio;如果问题特别容易陷坑,就反过来。

6.3 边界反射和反射系数的交互

reflect_factor 取 0.5 和取 0.9 的效果差异很大。取 0.5,粒子碰到边界后只剩一半速度,容易在边界附近反复来回;取 0.9,粒子几乎没有损失速度,会很快弹回搜索空间深处。但过高的 reflect_factor 也可能导致粒子在边界区域高频弹跳,浪费迭代步数。

我的体会是:如果搜索空间边界本身是真是可行的物理边界,比如某个参数的取值范围不能越出物理常识,那就用 0.8 左右;如果边界只是随便设的一个较宽范围,并不是真实约束,那反射系数可以低一点,让粒子快速离开边缘。

6.4 固定随机种子和实验对比的基本功

给改进 PSO 做实验时,最忌讳的是每次跑都用不同的随机种子,然后拿单次结果说事。随机优化算法本质上有随机性,单次对比没有任何统计意义。我所有的对比都是固定同一套随机种子列表,标准 PSO 和改经版在同一个种子列表下分别跑,再统计均值和标准差。

代码里写好 seed 参数之后,改成循环遍历多组种子并不是难事,重点是把“对比实验”当成一个规范动作来做。否则你可能花了一个星期调公式,最后被评审老师或者同事一句话问倒:这个效果是运气还是稳定性?

6.5 惯性权重指数 β 不是越高越好

β=2.0 是平衡点。β=5 时,曲线前面几乎不掉权,后一百代突然断崖下降到最低值,等于把“后期探索”和“后期开发”强行压缩到很短的时间内,很容易引起收敛大震荡。β=0.5 时,权重快速下降,早期探索不够充分。多数情况下 β 在 1.5 到 2.5 之间是安全区间,超过 3 就要重新审视你的参数设计。这个问题的本质是:你想要的不是“某个时刻看起来平滑”,而是整个搜索过程中各阶段的节奏都合理。

7. 关于“再往前一步”:这个版本还能怎么改

代码里其实还有很多可以继续扩展的插槽。比如,停滞检测目前只盯着全局最优 gbest,但实际上种群多样性的下降往往比 gbest 停更更早发生。如果你提前算一下种群位置分布的方差,当方差低于阈值时就触发扰动,有可能比只看 gbest 更灵敏。这个思路我做过一些实验,效果在部分离散优化问题上更好,但还没完全验证完,欢迎大家拿去试。

另一个可以改的点是停滞时的扰动范围。现在是只对适应度靠后的粒子做相同幅度的扰动。你也可以改成“按适应度排名线性衰减扰动幅度”:适应度越差,扰动越大;适应度接近最优,扰动越小。这样更加平滑,不容易把粒子拍到完全不可控的区域。

最后提醒一句,所有改进公式的参数都应该绑定到你的具体问题上去调,千万不要拿我这组参数当免检结论。不同维度、不同适应度函数地形、不同边界约束,参数的敏感区间差别还挺大。每次改公式之后,重新跑一遍小规模网格搜索才是正经做法。

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

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

立即咨询