
简介MOPSO多目标粒子群优化算法的MATLAB实现源码面向需要学习多目标进化计算的研究生、工程师及竞赛爱好者可用于求解帕累托前沿等典型优化问题。该程序结合粒子群全局搜索与帕累托支配机制能够同时优化多个冲突目标并获得一组分布均匀的非支配解。压缩包大小仅5KB由10个.m脚本文件组成除主程序外还包含网格创建、个体支配判断、外部档案去重等辅助模块该资源已有517人浏览学习。源码覆盖初始化、适应度评价、速度与位置更新、外部档案维护等核心环节并附有fun.m测试函数便于读者替换为自定义目标函数直观观察非支配排序和网格法对多样性的影响。整体代码结构清晰、模块化程度高适合初学者逐段调试也适合在此基础上扩展改进进行多目标优化实验与研究。1. 为什么MOPSO不是多跑几次PSO那么简单多目标粒子群优化这些年常被当成“PSO加个权重”来用实际效果往往只在凸前沿上过得去凹前沿直接漏掉一片解。MOPSO的核心变化在于把“唯一最优”换成“一组互不支配的解”用支配关系决定粒子保留方向再用外部档案维护整个帕累托前沿。这套 MATLAB 程序不是玩具代码它把非支配排序、网格划分、去重和个体选择拆成了独立函数适合做结构优化、资源配置、参数标定这类需要多个冲突目标平衡的人。对刚接触多目标优化的工程师看完主循环就能把 PSO 的直觉平移过来想深入研究的人也可以拿 ZDT 系列测试函数逐行验证。2. 从粒子群到帕累托MOPSO的数学基础与支配判断2.1 PSO的更新公式在多目标里卡在哪经典粒子群在单目标问题中的迭代核心是 v_i^(t1) w v_i^t c1 r1 (pBest_i - x_i^t) c2 r2 (gBest - x_i^t) x_i^(t1) x_i^t v_i^(t1)w是惯性权重c1/c2是学习因子r1/r2是[0,1]随机数。这个式子里只有一个gBest所以每次迭代所有粒子都往同一个最优位置拉。把多目标问题加权成单目标再跑多次不同权重看起来像是“多跑几次PSO”但它只能得到凸Pareto前沿上的解凹前沿里的点无论取什么权重都不会成为单目标加权和的最优解。所以MOPSO必须在算法内部保留一组解而不是外部枚举权重。MOPSO把gBest从单一变量换成了外部档案中的一个引导者。每个粒子在每一代从档案中按概率选出一个gBest于是不同粒子可以飞向不同区域前沿才能铺开。pBest的定义也在变当新目标向量支配旧pBest时直接替换当两者互不支配时常见做法是保留离当前粒子更近的那个或者随机保留一个目的是维持粒子个体方向的稳定性。2.2 支配关系与非支配排序最小化问题里解A支配解B的条件是对每个目标都有A的目标值小于等于B且至少有一个目标严格小于。代码里最基础的就是Domination.m它一次只判断两个解function d Domination(x, y) % 输入 x, y两个解的目标函数值例如 [f1, f2] % 输出 d 1 表示 x 支配 yd 0 表示不支配 better x y; % 严格更优的目标 worse x y; % 严格更差的目标 if any(better) ~any(worse) d 1; else d 0; end end这里默认所有目标都是最小化如果你的问题里有最大化目标可以直接取负号再传入避免在函数里反复判断。any(better) ~any(worse)的意思是至少在一个目标上严格更优同时没有任何一个目标更差这才是支配。两个条件缺一不可。JudgePopDomination.m 的作用是把这层判断放到整个种群上。它的常见实现是遍历每个粒子A与其余所有粒子B逐个调用Domination如果B支配A就把A标记为“被支配”如果A不支配所属的集合那就不写入非支配层。更规范的做法是会做一次非支配排序分出第一层、第二层前沿但这套代码里只需要“是/不是非支配解”两步判断所以JudgePopDomination这个名字足够说明问题它只返回每个粒子的支配状态而不是完整排序。为了保证档案里的解彼此不重复uniqueRep.m 会把目标函数向量完全一样的粒子合并。这里有个工程细节用浮点数比较相等会出问题因为0.99999999和1.00000001明明应该算同一个点。我一般会引入一个容忍度比如round(cost*1e6)/1e6之后再比对或者把目标值量化到网格精度。DeleteOneRepMember.m 则在档案容量超过设定值时从拥挤网格中删除冗余成员目标也很直白别让相同的解占满档案。2.3 网格法与外部档案的均匀性控制只有一个放满非支配解的数组还不够gBest到底从数组里哪一项选直接决定前沿是否均匀。这套程序使用了自适应网格CreateGrids.m 先根据当前档案的目标值范围把目标空间切成若干个格子FindGridIndex.m 再计算每个粒子落在第几个格子。Selectzbest.m 在选全局引导时优先从粒子数量少的格子里随机挑一个解作为gBest。这样做的直觉是稀疏区域的解更容易得到引导粒子会陆续飞过去把前沿的空洞补起来。网格选择不是唯一方案几种常见策略可以放在一起对比gBest选择策略典型实现分布效果额外成本随机选择从档案中均匀随机取一般最低拥挤距离按每个解到近邻的距离排序取距离大的较均匀需要计算距离矩阵自适应网格CreateGrids FindGridIndex均匀性可控实现直观网格边界敏感网格法最大的优点是不用算距离矩阵计算量集中在网格索引上在大档案场景下更划算。缺点集中在两个地方一是网格数量nGrid设置过小格子太粗选出来的gBest差异性差二是nGrid设置过大很多格子只有一两个解稀疏判断会失效。实际操作里两个目标取5到15个格子三个目标取4到8个格子后续跑ZDT1时会更快看到差别。另外CreateGrids 在取目标上下界时常见做法是在min和max基础上扩大一个alpha*span的边界。原因是一旦某个解恰好落在边界上找邻居格子时可能会越界。有的实现会把alpha设成0.1或者0.5具体要看目标范围是否归一化不扩边界也经常会跑错只是偶尔报错不够明显结果前沿少了一块。3. 逐文件拆解这套MATLAB版MOPSO程序函数职责与调用链当主程序 PSO_Multipleob2j.m 一打开扑面而来的是一堆.m文件。这套代码和教学版PSO最大的差别在于它把每个可复用的判断都拆成了独立函数。对学习来说这很友好调通之后你完全可以只改fun.m和一个参数段就能解决另一个领域的问题。3.1 文件清单与职责先把整个项目的文件对应关系列清文件核心职责关键输入输出PSO_Multipleob2j.m主程序组织初始化、迭代、绘图读参数输出rep与Gridfun.m目标函数定义多目标就返回多列输入决策变量输出代价向量Domination.m判断两个解是否支配输入两个代价向量输出0/1JudgePopDomination.m对整个种群做支配标记输入种群代价矩阵输出非支配标记uniqueRep.m去除完全重复的粒子输入档案输出去重后的档案DeleteOneRepMember.m删除重复或冗余粒子控制档案大小输入档案和容量上限输出精简档案CreateGrids.m建立自适应网格输入档案目标值输出网格上下界FindGridIndex.m计算粒子所在网格编号输入单个解和网格输出网格索引Selectzbest.m从档案中选出全局引导粒子输入档案和网格输出一个粒子Plotfitness.m绘制Pareto前沿或迭代曲线输入rep输出图窗主程序并不直接调用每个函数调用关系是主循环 → Selectzbest/CreateGrids/FindGridIndex/JudgePopDomination/uniqueRep/DeleteOneRepMember。fun.m 和 Domination.m 在最底层几乎被其他函数共用。3.2 主循环结构与速度更新主程序里最值得抄的部分是迭代主循环。我把它简化成可读版本% PSO_Multipleob2j.m 主流程简化后可照抄骨架 nVar 30; % 决策变量维度 VarMin zeros(1, nVar); VarMax ones(1, nVar); N 100; % 粒子数量 MaxIt 200; % 最大迭代次数 w 0.5; c1 1.5; c2 1.5; nGrid 10; nRep 100; % 初始化 particle(N).Position []; particle(N).Velocity []; for i 1:N particle(i).Position unifrnd(VarMin, VarMax); particle(i).Velocity zeros(1, nVar); particle(i).Cost fun(particle(i).Position); particle(i).Best.Position particle(i).Position; particle(i).Best.Cost particle(i).Cost; end % 外部档案初始化为当前所有非支配解 rep particle; repCosts vertcat(rep.Cost); keep JudgePopDomination(repCosts); rep rep(logical(keep)); rep uniqueRep(rep); rep DeleteOneRepMember(rep, nRep); Grid CreateGrids(rep, nGrid); for it 1:MaxIt for i 1:N gBest Selectzbest(rep, Grid); r1 rand(1, nVar); r2 rand(1, nVar); particle(i).Velocity w * particle(i).Velocity ... c1 * r1 .* (particle(i).Best.Position - particle(i).Position) ... c2 * r2 .* (gBest.Position - particle(i).Position); particle(i).Position particle(i).Position particle(i).Velocity; particle(i).Position min(max(particle(i).Position, VarMin), VarMax); particle(i).Cost fun(particle(i).Position); if Domination(particle(i).Cost, particle(i).Best.Cost) particle(i).Best.Position particle(i).Position; particle(i).Best.Cost particle(i).Cost; end end % 把个体最优并入档案再统一做支配筛选、去重、容量控制 rep [rep, particle]; repCosts vertcat(rep.Cost); keep JudgePopDomination(repCosts); rep rep(logical(keep)); rep uniqueRep(rep); rep DeleteOneRepMember(rep, nRep); Grid CreateGrids(rep, nGrid); end这份代码里有两个容易出错的地方一是unifrnd(VarMin, VarMax)在两个输入都是向量时直接生成的是对应维度的随机矩阵要保证VarMin/VarMax的维度对齐二是更新位置后的min(max(...))只是硬边界粒子在边界处速度不归零容易反复撞边界实际中我会在边界处让速度乘以 -0.5避免粒子一直贴在边界上。3.3 Selectzbest、CreateGrids与FindGridIndex的配合Selectzbest 每次被调用时传入的是外部档案rep和当前网格Grid。它先调用 FindGridIndex 算出每个档案粒子的网格编号再统计每个网格里的粒子数。比如三维目标空间里nGrid10时理论上有1000个格子但实际只有少数格子有解。Selectzbest 会找出包含非支配解且粒子数最少的格子在其中一个解里随机选一个返回。这段逻辑保证了稀疏区域有更高的概率被选为全局最优粒子也就更容易往那里飞。CreateGrids 则承担一个容易被忽略的任务每个维度上设置nGrid等分。两个目标时它返回两个向量下边界和上边界或者返回一个包含网格边界与编号规则的结构体。FindGridIndex 内部常见写法是function idx FindGridIndex(cost, Grid) % 假设 Grid.lb 和 Grid.ub 是每维边界向量的 cell 数组 nObj numel(cost); idx zeros(1, nObj); for j 1:nObj id find(cost(j) Grid.lb{j} cost(j) Grid.ub{j}, 1); if isempty(id) idx(j) -1; % 超出边界说明网格没有外扩 else idx(j) id; end end end这里麻烦的地方在于当某个目标值恰好等于上边界时会把最后一个点排除在网格外。所以 CreateGrids 里一定要把最后一格的上边界比实际最大值放大一点常见做法是ub(end) ub(end) eps或者用alpha*range外扩。遇到前沿呈长尾分布时这个细节直接影响FindGridIndex能否正确索引。可以顺带提一句如果自己改问题目标数量从2变成3时CreateGrids和FindGridIndex的维度逻辑要同步调整否则索引维度对不上。这套代码的目标函数fun.m如果返回两列那nGrid可以设成10如果返回三列nGrid要降下来否则网格数量指数增长大部分格子都是空的。4. 跑通并检验ZDT系列测试函数上的参数设置与结果可视化MOPSO写得好不好不能光看能不能跑要看它能不能在标准测试函数上逼近真实帕累托前沿。ZDT1是最常用的入门测试函数有两个优化目标、变量维度可以自由设置。拿它来验证这套MATLAB程序半小时内就能得到一张可以写进报告的前沿图。4.1 如何把ZDT1写进fun.mZDT1的数学定义是决策变量x1到xn都取值[0,1] f1(x)x1 g(x)19/(n-1)sum(x2..xn) f2(x)g(1-sqrt(f1/g))它的真实Pareto前沿是f21-sqrt(f1)对应的条件是x2到xn全等于0。代码实现可以这样写function z fun(x) % ZDT1 测试函数两个目标均为最小化 n numel(x); f1 x(1); g 1 9 / (n - 1) * sum(x(2:end)); f2 g * (1 - sqrt(f1 / g)); z [f1, f2]; end这段代码返回的 z 是一个1×2的行向量。主程序里判断支配关系时用的就是这种目标向量。变量n取多少会影响求解难度n30是文献常用设置但收敛慢适合正式对比n10能快速看到趋势适合调参数。第一次跑建议先用n10。4.2 主参数设置与影响MOPSO不像单目标PSO那样只有“收敛”一个指标它要同时看收敛性和分布性。所以参数表格比普通PSO多出nGrid和nRep两行。参数常见范围对结果的作用N粒子数50 - 200越大种群分布越广但每代计算量线性增加MaxIt迭代次数100 - 500ZDT1通常200代能到近似收敛复杂问题要500以上w惯性权重0.4 - 0.9大权重强化全局搜索小权重强化局部收敛c1/c2学习因子1 - 2c2相对c1越大粒子越依赖档案引导nGrid网格数5 - 20决定前沿均匀性分辨率过大会导致格子过空nRep档案容量50 - 200最终输出解的最多个数也影响选择压力这里我最常遇到的问题是 nGrid 和 nRep 不匹配nGrid很小比如5nRep却设到200最后会有大量粒子被塞进同一个格子Selectzbest 的选择压力失效反过来 nGrid很大、nRep只有30档案里每个格子只有一两个解稀疏判断会变成随机选择。先按 nGrid 10、nRep 100 起步再看前沿图调整。4.3 运行步骤与可视化对比一套可复现的运行步骤是解压整个MOPSO程序包确认所有.m文件在同一个目录下路径上不要出现中文文件夹名否则MATLAB读取容易出问题。用上面的ZDT1代码覆盖fun.m。打开PSO_Multipleob2j.m把nVar设为10MaxIt设为200其余按默认参数。运行脚本主循环完成后在工作区里应看到rep变量。执行Plotfitness(rep)观察Pareto前沿是否从左下角延伸到右上角。如果主脚本运行完直接结束没留下rep就在末尾追加一行保存命令save(MOPSO_result.mat, rep);然后用单独的画图脚本对比真实前沿load(MOPSO_result.mat); costs vertcat(rep.Cost); % rep中的Cost字段组装成 N×2 矩阵 real_x linspace(0, 1, 200); real_y 1 - sqrt(real_x); plot(real_y, real_x, k--, LineWidth, 1.5); hold on; scatter(costs(:,2), costs(:,1), 15, filled, MarkerFaceAlpha, 0.6); xlabel(f2); ylabel(f1); legend(真实 Pareto 前沿, MOPSO 结果, Location, best);注意我画图时把f1放在纵轴、f2放在横轴是为了和ZDT1的常见图形保持一致。如果你习惯f1横轴就把real_x、real_y和scatter里的列对调。重点是MOPSO求出的点应该落在真实前沿附近而不是聚在某一小段。如果点全部挤在左侧说明w太大或者MaxIt不够如果点分散但离前沿很远优先检查fun.m的g表达式里的除法是否写成了矩阵除法。热搜词里常出现“mopso优化zdt1的pareto前沿”其实就是在做这一件事把标准测试函数跑出接近真实前沿的一组解再用图形把收敛性和分布性展示出来。运行这一步时会发现档案里有些点的f1分布并不均匀。这不一定代表算法错了如果nGrid太小网格选择无法区分稀疏区域前沿中央会缺一段。把nGrid调到15再看一次通常能看到明显变化。5. 把MOPSO用到自己的问题目标函数改写、网格扰动与性能验证5.1 替换fun.m时的三个检查点换掉ZDT1改成自己的工程问题只需要动fun.m和变量范围。三个地方最容易出错第一目标函数必须返回一行向量内部有多少个目标就返回多少列MOPSO不自动识别向量长度第二VarMin和VarMax的长度必须和决策变量维度一致否则unifrnd会报错第三如果有约束条件不要试图把约束写进支配判断里常见做法是在fun.m返回值上叠加惩罚项让违反约束的解在目标空间中处于支配劣势。5.2 网格扰动实验我看一个MOPSO实现是否稳定会先做网格扰动实验固定其他参数把nGrid从7改成15再改成25观察同一问题下的Pareto前沿变化。nGrid25时常常出现“前沿看起来很散但IGD反而变好”的情况这是因为细网格让更多粒子去填补稀疏区域虽然输出点数量不变位置更贴前沿。这个扰动实验也能用来判断nGrid是否过大如果nGrid从15到25结果几乎没变化说明再细分已经没有信息量。5.3 用IGD指标代替肉眼判断视觉对比只能定性定量验证可以算IGDInverted Generational Distance。IGD衡量MOPSO解集到真实前沿的逼近程度值越小越好。真实前沿通常用密集采样点表示ZDT1可以直接用linspace生成function igd IGD(PF, PFtrue) % PFMOPSO求得的非支配解目标值矩阵N行nObj列 % PFtrue真实Pareto前沿密集采样点M行nObj列 d 0; for i 1:size(PFtrue, 1) dist sqrt(sum((PF - PFtrue(i, :)).^2, 2)); d d min(dist); end igd d / size(PFtrue, 1); end调用时传入真实前沿点和MOPSO结果得到的是一个标量。我通常跑10次取中位数避免单次随机性误导结果。调参时有个简单技巧固定其他参数只改变nRep观察IGD曲线多数情况下nRep越大IGD先变好后变差因为档案过大时网格选择压力被分散所以可以把nRep设为三倍目标解数量作为起点再用这个小实验找到拐点。如果你发现前沿某一段始终缺失优先检查CreateGrids的边界放大系数我一般把alpha从0.1加到0.3缺失段往往会自己修复。本文还有配套的精品资源点击获取