ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基本粒子群算法原理与MATLAB实现:从公式到避坑指南

基本粒子群算法原理与MATLAB实现:从公式到避坑指南

简介:面向算法初学者与优化问题研究者的粒子群优化算法原理讲解与MATLAB实现文档,系统梳理了算法模拟鸟群捕食行为的核心思想,说明速度与位置更新公式中惯性权重、认知系数、社会系数等参数的含义,并对比了全局与局部两种PSO模式的区别。文档从随机初始化粒子群入手,逐步讲解个体极值与全局极值的跟踪机制、迭代停止条件的设定,以及参数调整对收敛速度和解质量的影响,并附带基本流程说明。资源为单个doc文档,压缩包约39KB,已有50人次浏览学习;文档可作为算法课程作业、毕业设计或科研入门阶段的参考,也适合需要快速了解PSO原理并对照MATLAB代码理解参数作用的读者使用。

1. 基本粒子群算法是什么:一份word版资料背后的标准实现

无论是赶课程作业还是做课题预研,很多人手里都躺着一份《完整word版基本粒子群算法的原理和matlab程序(1).doc》。这份资料讲的基本粒子群算法(Particle Swarm Optimization,PSO),是Kennedy和Eberhart在1995年提出的群体智能算法,思路非常直白:把每个候选解看成一只在解空间里飞的鸟,靠自身经验和群体经验不断修正方向,最终逼近最优解。它能处理连续变量和离散变量两类优化问题,PID参数整定、路径规划、神经网络超参数搜索都是它的常见战场;最大优点是目标函数不需要可导,黑匣子也能优化,缺点是收敛精度依赖参数整定,容易早熟。这篇文章按我跑通并改过多轮的经验,把原理、MATLAB实现、参数整定和踩坑一次讲清楚,新手能照抄代码,熟手可以回来核对参数。

2. 从速度-位置更新公式到收敛性:三个分量决定粒子往哪儿飞

先把原理立住,后面调参数、改代码才有依据。网上这类word版资料最值钱的部分就是原理推导,但往往写得太简略,读者只知道“照着公式敲进MATLAB”,不知道为什么要这么写。这里我把标准PSO的数学模型完整拆开,讲清楚位置、速度、个体最优和全局最优之间的关系。

2.1 标准PSO的数学模型:位置、速度、pbest与gbest

假设要优化的是一个n维变量x = [x1, x2, ..., xn],每个变量都有上下界。PSO维护一组“粒子”,每个粒子是解空间里的一个点,同时携带一个速度向量。第i个粒子在t时刻的状态由两部分描述:位置向量xi(t)和速度向量vi(t)。位置就是候选解,速度决定了下一轮朝哪个方向走、走多远。

目标函数f(x)的返回值称为适应度,PSO默认按最小化处理,最大化问题取负号或取倒数即可。每个粒子在飞行过程中记住自己见过的“最好位置”,记为pbest_i;整个种群共享一个“最好位置”,记为gbest。pbest和gbest就是粒子群的两个核心记忆:前者是个人经验,后者是社会经验。初始化时,pbest_i就是该粒子的初始位置,gbest则是全体粒子中适应度最好的那个位置。

这套模型没有复杂的梯度信息,只需要反复算目标函数值。所以它天生适合那些“能算出结果、但求不出导数”的问题,这也是为什么很多工程优化场景宁可忍受它慢,也要用它。

2.2 速度-位置更新公式拆解:惯性项、认知项和社会项

标准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是惯性权重,c1和c2是学习因子,r1和r2是每次独立采样的[0,1]均匀随机数。速度公式可以拆成三个分量来理解。

第一项w·v_i(t)叫惯性项,意思是粒子保留一部分上一时刻的运动趋势。w越大,粒子越“不愿意转弯”,飞行路径更开阔,有利于探索大范围区域;w太小,粒子很快被后续两项牵着走,容易早熟。第二项c1·r1·(pbest_i − x_i(t))叫认知项,把粒子朝自己历史上最好的位置拉。随机系数r1让这个拉力每轮都在变,避免粒子太机械地奔向pbest。第三项c2·r2·(gbest − x_i(t))叫社会项,把粒子朝当前群体最优位置拉。c1和c2的相对大小,决定了粒子是更“相信自己”还是更“相信群体”。

注意r1和r2每一轮都重新随机采样,而不是取固定值。这样即使两个粒子位置完全一样,下一轮速度方向也会不同,保持了搜索的多样性。位置更新则是简单的“老位置加新速度”,如果把速度看成步长,位置更新就是沿速度方向走一步。实际代码中还要限制速度幅值不能超过vmax,否则粒子可能一步跨出整个解空间。

2.3 收敛性条件与参数约束:w、c1、c2的关系

为什么标准PSO建议w取0.9左右、c1和c2都取2.0?这不是拍脑袋,而是从收敛性推导出来的。把速度更新公式代入位置更新公式,可以消去速度,得到关于位置xi的二阶差分方程,其收敛性取决于特征根是否落在单位圆内。经典结论是:当w < 1且满足 w > (c1 + c2) / 2 − 1 时,粒子系统是稳定的,不会发散。

这个条件解释了三个常见现象。第一,w必须小于1,否则粒子速度只增不减,种群必然爆炸。第二,c1 + c2 最好不要超过4,超过后即使w取0.9,稳定性条件也可能被破坏。第三,c1和c2取2.0是1998年Shi和Eberhart论文里的经典配置,配上w从0.9线性降到0.4,就是所谓“基本粒子群算法”的标准形态,这也是绝大多数word版资料的默认参数。

后来Clerc引入了收缩因子概念,用χ = 2 / (φ − 2 + sqrt(φ² − 4φ)),φ通常取4.1,算出χ约等于0.7298。等价到学习因子上就是c1 = c2 = 1.49445。这组参数在数学上保证了收敛性,很多MATLAB优化工具箱的实现用的就是收缩因子版本。实际用下来,1.49445比2.0更稳,但收敛可能略慢一点;2.0则搜索更活跃,在简单问题上收敛更快。

参数经典取值作用说明
w0.9 → 0.4线性递减控制探索与开发平衡前期大w跑得开,后期小w收得细
c12.0 或 1.49445个体认知拉力太大容易各飞各的
c22.0 或 1.49445群体社会拉力太大容易集体早熟
vmax边界跨度的10%~25%限制步长防止粒子一步飞出解空间

搞清楚这些,再看word里的程序就知道:为什么作者要写w_update = wmax - (wmax - wmin)*iter/maxiter,为什么c1和c2默认填2.0。参数不是随便定的,都对应着这条收敛性链条。

3. 用MATLAB写一个最小可运行的PSO:Spher测试函数与45行核心代码

原理落在纸面上只是一页公式,能不能跑通才是关键。我在处理这类word版资料时,第一步永远是把程序缩减成一个“最小可运行版本”:能跑、能画收敛曲线、能调参,别的花哨功能一律不要。这样排查问题最快,也方便后面逐个加改进策略。

3.1 目标函数选型:为什么先用Sphere测试函数

PSO的第一道坎是目标函数怎么写。很多word程序把目标函数和主循环写在一个文件里,导致换一个优化问题就要改一堆代码,容易改错。更好的做法是把目标函数单独写成函数文件,接口统一为“输入一个矩阵,输出一个列向量”。

测试函数我建议从Sphere开始:f(x) = sum(x_i²)。它有四个特点适合新手:连续、单峰、全局唯一最小值在原点、最优值已知为0。如果PSO连Sphere都搜不到理想结果,那肯定是更新逻辑有bug,而不是算法本身的问题。等Sphere跑通了,再换成Rastrigin这类多峰函数考验全局搜索能力。

3.2 写第一个函数文件:my_func.m

% my_func.m % 目标函数:Sphere 函数 % 输入 x:nPop × nVar 矩阵,每一行是一个粒子 % 输出 f:nPop × 1 列向量,每一行是该粒子的适应度 function f = my_func(x) f = sum(x .^ 2, 2); % 按行求和,得到列向量 end

这个文件只有一行核心代码,但接口约定决定了后面主程序能不能写成矩阵化形式。sum(x.^2, 2)的第二个参数2表示按行求和,输出是nPop×1的列向量。如果去掉2,x是矩阵时sum(x.^2)会返回一个1×nVar的行向量,后续和pbest_f做比较时维度对不上,MATLAB直接报“Matrix dimensions must agree”。

想换测试函数时,只需要改动my_func.m这一处。比如Rastrigin函数的MATLAB写法是f = sum(x.^2 - 10cos(2pi*x) + 10, 2),函数文件的其他部分不用动。

3.3 写主程序:pso_demo.m

% pso_demo.m % 基本粒子群算法(标准PSO)求解 Sphere 函数 % 变量范围:[-100, 100] 内找 sum(x.^2) 的最小值 clear; clc; %% 参数区 nVar = 10; % 变量维度 lb = -100; % 变量下界 ub = 100; % 变量上界 maxIter = 200; % 最大迭代次数 nPop = 40; % 种群规模 w_max = 0.9; % 惯性权重上限 w_min = 0.4; % 惯性权重下限 c1 = 2.0; % 个体学习因子 c2 = 2.0; % 群体学习因子 vmax = 50; % 速度上限,取边界跨度的1/4 %% 初始化种群 % 每一行是一个粒子:位置 x、速度 v、历史最优位置 pbest_x x = lb + (ub - lb) * rand(nPop, nVar); % 均匀随机初始化位置 v = -vmax + 2 * vmax * rand(nPop, nVar); % 对称随机初始化速度 pbest_x = x; % 初始个体最优就是自身 pbest_f = my_func(x); % 初始个体最优适应度 %% 确定初始全局最优 [best_f, gbest_idx] = min(pbest_f); gbest_x = pbest_x(gbest_idx, :); history = zeros(maxIter, 1); %% 主循环 for t = 1:maxIter w_t = w_max - (w_max - w_min) * (t / maxIter); % 惯性权重线性递减 % 更新速度:惯性项 + 认知项 + 社会项 % repmat 把 gbest_x 复制成 nPop 行,兼容 R2016a 之前的版本 v = w_t * v ... + c1 * rand(nPop, nVar) .* (pbest_x - x) ... + c2 * rand(nPop, nVar) .* (repmat(gbest_x, nPop, 1) - x); % 速度限幅,防止粒子一步跨出解空间 v = max(min(v, vmax), -vmax); % 更新位置 x = x + v; % 越界粒子拉回边界(吸收型边界) x = max(min(x, ub), lb); % 重新计算所有粒子的适应度 f = my_func(x); % 更新个体最优:新位置比历史最优好就替换 better_mask = f < pbest_f; pbest_x(better_mask, :) = x(better_mask, :); pbest_f(better_mask) = f(better_mask); % 更新全局最优 [f_min, idx] = min(pbest_f); if f_min < best_f best_f = f_min; gbest_x = pbest_x(idx, :); end history(t) = best_f; end %% 输出结果 fprintf('最优解 x* 的前5个分量:\n'); disp(gbest_x(1, 1:min(5, nVar))); fprintf('最优适应度 f* = %.6e\n', best_f); % 画收敛曲线,加eps防止对数坐标下出现log(0) figure; semilogy(history + eps); xlabel('迭代次数'); ylabel('全局最优适应度'); grid on;

代码的核心在更新速度的四行。w_t * v保留了上一轮速度乘以衰减后的惯性权重;c1 * rand(nPop, nVar) .* (pbest_x - x)让每个粒子朝自己的历史最优靠拢,随机数保证拉力方向每轮有抖动;c2那项同理,朝全局最优靠拢。repmat把gbest_x从1×nVar扩展成nPop×nVar,这样矩阵减法是逐行运算,R2016a之前的老版本MATLAB也能跑。

better_mask这段是体现MATLAB风格的写法。f < pbest_f得到一个逻辑向量,true表示该粒子找到了更好的位置,直接用它索引替换,避免写for循环逐个判断。粒子多、维度高时,这种向量化写法比循环快一个数量级以上,这也是word里老式循环代码最常见的性能问题来源。

跑完之后看两个东西:一是终端打印的最优适应度,Sphere问题上应该到10^-6以下才算正常;二是收敛曲线,应该平滑下降,最后趋于水平。如果最优值停在几十甚至几百,优先检查vmax是否太大、w_t是否衰减到0.4以下,以及目标函数是否写错维度。

4. 参数整定的三个必调项:惯性权重、学习因子与种群规模

很多读者拿到能跑的PSO程序后,第一件事就是调参数。可调的东西看着很多,实际上最值得动的只有三个:惯性权重w、学习因子c1/c2、种群规模nPop。速度上限vmax和边界处理方式属于“设对一次就不用再动”的次要参数。这一章把每个参数的调节逻辑讲透,顺带给出我常用的取值表。

4.1 惯性权重w:线性递减为什么是默认第一选择

基本粒子群算法里w是最敏感的参数,没有之一。Shi和Eberhart在1998年给的标准方案是w从0.9线性降到0.4,这个方案沿用至今,理由很朴素:迭代前期粒子需要大范围探索,w大一点,速度的惯性成分高,粒子不容易被某个局部最优立刻吸走;迭代后期需要精细搜索,w小一点,粒子在最优解附近慢慢磨。

线性衰减的实现就是主程序里那一行:w_t = w_max - (w_max - w_min) * (t / maxIter)。这个写法把最大权重、最小权重和当前进度绑定在一起,改起来也方便。如果目标函数是简单的单峰函数,可以把w固定成0.6,省去衰减逻辑,收敛速度反而更快;如果是多峰函数,强烈建议保留衰减,否则后期粒子绕圈不收敛。

调w时最直观的判断看收敛曲线形状。曲线下降很快但最后停在明显偏大的值,说明w降得太快或w_min太高,粒子没时间精细搜索。曲线一路震荡、尾端还在上下跳,说明w_t下降不到位,后期惯性仍偏大。还有一种情况是曲线开头就平,那大概率不是w的问题,而是vmax太小,粒子飞不出去。

4.2 学习因子c1、c2:怎么判断粒子在“盲飞”

c1和c2分别控制粒子对个人经验和群体经验的信任程度。c1过大,每个粒子只盯着自己的pbest飞,群体信息被忽略,整体表现为“各飞各的”,收敛慢;c2过大,所有粒子一股脑冲向当前gbest,如果gbest是个局部极值,整个种群就集体早熟。所以调这两个参数的实质是平衡探索与开发,而不是把单值调得越大越好。

word版资料最常见的配置是c1 = c2 = 2.0,这个组合搜索积极但在多峰问题上容易震荡。如果换成收缩因子版本c1 = c2 = 1.49445,配合w固定0.7298,收敛会更稳。还有一种异步时变方案:c1从2.5线性降到0.5,c2从0.5线性升到2.5。前期让粒子更相信自己的探索,后期让群体共识主导,对Rastrigin这类陷阱多的函数比固定2.0有效。

判断粒子是否在“盲飞”,可以打印中间过程。如果迭代到一半,所有粒子的位置差、速度差都很小,但gbest还没有达到理想值,说明c2的拉力太强,粒子过早扎堆;如果迭代结束,粒子分布还很散,说明c1过强或c2过弱。调参本质上是个玄学,不同目标函数的最优参数组合不一样,我的习惯是先固定w的衰减方案,再以10%步长扫描c1和c2,每次跑完记录收敛曲线,而不是一次调三个参数。

4.3 种群规模、速度上限与边界处理的取值表

种群规模nPop决定每代计算量,计算量等于nPop乘以nVar再乘以目标函数耗时。维度低于10的简单问题,nPop取20~40足够;维度到30以上,建议取50~100。不要盲目加到几百,因为PSO的信息共享是全局的,种群到一定规模后再加人,收敛速度提升有限,计算成本却线性增加。

速度上限vmax影响单步最大移动距离。我习惯取边界跨度的10%~25%,比如变量范围[-100, 100]跨度为200,vmax取20到50。vmax太小,粒子被限速,搜索效率低;vmax太大,粒子频繁撞边界,速度限幅形同虚设。边界处理方式有三种,word程序里最常见的是“拉回边界”,即越界后直接赋值为边界值,实现简单,但粒子容易在边界堆积,尤其是最优解靠近边界时。

参数建议范围调试优先级我的默认配置
nPop20 ~ 100高40(维度>30时取80)
w0.9→0.4线性衰减高按Shi方案
c1, c21.0 ~ 2.5中2.0,多峰时换1.49445
vmax边界跨度的10%~25%低20%
边界处理吸收/反射/周期低吸收型,早熟时改反射型

一套配置先用默认值跑通,再单独调最敏感的那一两个参数。我见过最多的失败是同时改五个参数,结果收敛曲线变差都不知道是哪个参数惹的祸。

5. 避坑与排查:这份word版程序最常见的5个翻车现场

这类word版的基本粒子群算法程序,大多是从教材或论文里抄下来的循环版本,结构差不多,问题也高度相似。我把调过的程序里最常见的五个坑按“现象→原因→解决”列出来,照一条条对就能省下大半天血泪排查时间。

5.1 报错“Matrix dimensions must agree”——目标函数写成标量

现象:把word代码粘进MATLAB,第一次运行就报维度不匹配错误,指向目标函数那一行。原因:目标函数里写的是f = sum(x.^2),而传入的x是nPop×nVar矩阵,sum默认按列求和,输出变成1×nVar行向量。主程序拿它和pbest_f比较时,要么维度对不上,要么广播出错。解决:目标函数一律按“输入矩阵、输出列向量”的约定写,核心改动就是把sum(x.^2)改成sum(x.^2, 2)。如果是从别人手里接的代码,先在命令行执行size(f)确认目标函数返回的维度,再动主程序。

5.2 粒子全堆在边界,结果停在很差的解上

现象:迭代曲线下降几步后就不动了,打印gbest发现它的分量正好等于边界值,很多粒子也贴在边界上。原因:word程序里越界处理用的是x = max(min(x, ub), lb),这叫吸收型边界。粒子撞墙后贴在墙上,下一轮速度如果还指向墙外,又被拉回来,墙面成了隐形陷阱。解决:换成反射型边界,把越界的粒子折回界内,速度反向:

% 反射型边界:越上界折回,越下界折回,速度反向 over_ub = x > ub; over_lb = x < lb; x(over_ub) = 2 * ub - x(over_ub); x(over_lb) = 2 * lb - x(over_lb); v(over_ub | over_lb) = -v(over_ub | over_lb);

反射型适合最优解在边界附近的问题,粒子不会堆积在墙上,搜索空间利用得更充分。代价是多两行代码,以及目标函数在边界附近不光滑时反射会引入轻微的振荡。这个改动属于收益远大于成本的类型。

5.3 惯性权重不衰减,迭代曲线像心电图

现象:收敛曲线下降一段时间后开始上下跳动,尾部完全不平滑。原因:程序把w固定成0.9从不衰减。后期粒子速度惯性太大,每轮都从最优解身边冲过去,被认知项和社会项拉回来,再冲过去,形成振荡。解决:改成线性衰减,w从0.9降到0.4。如果衰减后仍然震荡,把w_min降到0.3,或者把vmax缩小到边界跨度的10%。判断标准很简单:收敛曲线尾部应该是一条接近水平的线,而不是锯齿状。

5.4 结果每次跑都不一样,无法复现

现象:同一份程序、同一组参数,连续跑三次,最优值和收敛曲线都有差异,有时候结果好得离谱,有时候差得离谱。原因:rand()依赖MATLAB的全局随机数流,每次启动流状态不同。这不是bug,是随机算法的固有特性,但调参时就成了大坑——你分不清曲线变好是因为参数改对了,还是单纯运气好。解决:程序开头加一行rng(0),固定随机数种子。调参阶段务必固定种子,组间对比才有意义;确定最终参数后,可以去掉种子多跑几次看稳定性。

5.5 早熟停滞:gbest不动了,粒子扎堆

现象:gbest从第30代开始就再也不更新,但已知理论最优值还没达到;进一步打印x矩阵,发现各行几乎完全相同,粒子全部挤在一个点附近。原因:所有粒子收敛到同一个位置后,pbest趋同,认知项和社会项都趋近于零,剩下的只有惯性项。如果这个点是局部极值,粒子就彻底失去逃逸能力。解决:检测种群多样性或gbest停滞代数,触发随机重置。下面这段代码插在主循环gbest更新之后即可,逻辑是连续30代没有更新gbest,就把30%的粒子重新撒回解空间:

% 停滞检测并重置部分粒子(插在主循环gbest更新之后) if noChange >= 30 reset_idx = randperm(nPop, ceil(0.3 * nPop)); x(reset_idx, :) = lb + (ub - lb) * rand(length(reset_idx), nVar); v(reset_idx, :) = vmax * (2 * rand(length(reset_idx), nVar) - 1); noChange = 0; end

注意这段代码共用了主程序里的变量,重置只改变位置和速度,不动pbest和gbest。这样重生的粒子带着群体的记忆重新搜索,一旦找到更好的位置会自然更新pbest,不会破坏已收敛的部分成果。下一轮主循环会自动重新计算这些粒子的适应度,所以不需要额外补算。

一个更温和的做法是给惯性权重w_t加一个下限,比如不低于0.5,让粒子保留一定的搜索活力。但根治早熟还得靠多样性恢复,这是第6章要展开的进阶技巧。

6. 一个能直接抄的进阶技巧:基于停滞检测的自适应变异PSO

处理word版程序时,我做的最多的改造就是给标准PSO加“后悔药”。标准PSO的致命弱点是没有多样性恢复机制,一旦早熟就只能看着gbest卡住。前面5.5给了一段最简重置代码,这一章把它扩展成一个可以嵌入几乎所有PSO程序的自适应变异模块,并讲清楚怎么验证它真的有效。

6.1 在原有主循环里插入停滞检测与重置

思路是维护一个noChange计数器:每一轮如果gbest没有改善,计数器加1;一旦改善,清零。当计数器达到阈值,说明算法已经停滞,此时对一部分粒子重新初始化,但保留它们的pbest,让群体带着记忆重新探索。完整的嵌入逻辑如下,可以直接替换第3章主循环尾部:

% 主循环尾部追加:停滞检测与自适应重置 if best_f < last_best last_best = best_f; noChange = 0; else noChange = noChange + 1; end if noChange >= 40 % 连续40代没有改善,判定早熟 reset_idx = randperm(nPop, ceil(0.25 * nPop)); x(reset_idx, :) = lb + (ub - lb) * rand(length(reset_idx), nVar); v(reset_idx, :) = vmax * (2 * rand(length(reset_idx), nVar) - 1); noChange = 0; % 重置完成后重新计时 end

阈值40和重置比例25%是经验值。阈值太小,粒子还没来得及在局部区域精细搜索就被重置,浪费前期计算;阈值太大,早熟后要白跑很多代。重置比例太高会破坏已经收敛的结构,太低又起不到恢复多样性的作用。我一般在多峰函数上用40代加25%,单峰函数上把阈值放大到80代。

6.2 判定改造是否有效的验证方法

不要只看最后一行最优值就下结论。我的验证步骤是:固定rng(0),分别在Rastrigin函数上跑标准PSO和加了自适应重置的PSO各三次,记录每次的最优值和“首次达到该值的迭代次数”,然后对比。标准PSO大概率会掉进某个局部极值,而带重置的版本由于粒子被反复重新撒网,最终值通常更接近理论最优。

我自己早期用固定w的word程序跑Rastrigin,调了三天参数始终陷在局部最优里,最后发现自己犯的错就是“只在参数上使劲,没在机制上动手”。给粒子留一条重新出发的路,比把w、c1、c2调到某个玄学组合更管用。这个改造同样适用于深度学习MATLAB场景下的超参数搜索——把PSO当成基线优化器,早熟问题不解决,搜出来的学习率组合很容易骗过你的眼睛。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进