
简介NSGA-II多目标优化算法的MATLAB实现适合进化计算初学者、研究生及需要开展多目标对比实验的开发者。代码按算法流程拆分为多个功能模块涵盖非支配排序、拥挤距离计算、精英策略保留、遗传操作等核心环节模块划分清晰便于按需调用和二次开发。压缩包共27个文件其中包含10个M脚本、10个txt测试数据、7个fig仿真图整体大小约2.41MB。已有3201人浏览学习。代码附带完整的ZDT1—ZDT6与DTLZ1—DTLZ6二维/三维测试函数数据并给出对应仿真图像读者可直接运行复现算法在标准测试集上的表现用于课程设计、论文实验或算法性能对比。 我最早接触NSGA-II是做一个生产排程的两目标优化项目。当时用加权求和法把工期和成本压成一个目标权重试了十几组结果总是顾此失彼——工期短了成本就爆表成本可控了交期又没法看。后来换成NSGA-II一次运行直接给出整条Pareto前沿让项目方自己从候选解里挑平衡点效率完全不一样。这篇文章就把NSGA-II算法的MATLAB代码从原理到实现完整拆开讲一遍包括我实际调试时踩过的坑和参数调整经验给正在做多目标优化的朋友一个可以直接参考的底稿。1. 动手之前先搞懂NSGA-II到底在解决什么问题1.1 多目标优化为什么不能用“加权求和”糊弄单目标优化很好理解一个函数一个最优解梯度下降、遗传算法随便招呼。但工程里碰到的大多是多目标问题比如“成本最小化质量最大化”“交付周期最短资源利用率最高”。这类问题麻烦在目标之间通常互相冲突你想让成本低可能就得牺牲一部分质量你想让交付快可能就得增加资源投入。传统做法是把多个目标按权重加起来变成一个综合指标调一次权重跑一次优化。但这里有个致命问题权重怎么定拍脑袋定出来的权重可能压根覆盖不到真正有意义的解区域。比如成本和工时两个目标量纲都不一样加权之后小的那个目标直接被淹没。更麻烦的是多目标问题的解本来就不是一个点而是一组互有优劣的候选解——目标A好的解目标B可能很差两个解谁也不能说完全碾压对方。这组解就是Pareto前沿也是多目标优化的核心产出。1.2 NSGA-II的三个核心机制NSGA-II全称是Non-dominated Sorting Genetic Algorithm II2002年由Deb等人提出。它能在多目标优化领域横着走二十多年靠的是三个设计精妙的核心机制。第一个是非支配排序。什么叫支配解A在所有目标上都不比解B差而且至少有一个目标比解B好那A就支配B。非支配排序就是把种群里的个体按“被支配程度”分层最外层是没人支配的个体属于第一前沿去掉它们之后剩下的个体再找没人支配的属于第二前沿以此类推。分层的意义在于第一前沿的个体就是当前种群里的“最优候选解集”要优先保留。第二个是拥挤度距离。同一层前沿的解之间也有密集和稀疏之分如果只按排序筛选解很容易全堆在某个区域导致Pareto前沿覆盖不全。拥挤度距离是计算每个解在目标空间里与相邻解之间的“间距”间距大的说明周围稀疏要优先保留这样能让解在目标空间里尽量铺开。第三个是精英保留策略。每一代不是光用父代繁殖子代而是把父代和子代合并统一做非支配排序和拥挤度筛选直接淘汰差的。这保证了优秀个体不会在迭代中被随机性丢掉收敛速度和解质量都有保障。2. 代码架构设计与实现方案2.1 为什么用MATLAB写NSGA-II有人会问现在Python生态这么全为什么还在MATLAB里写NSGA-II我的看法是如果只是跑个标准算例Python的pymoo、Platypus确实省事但如果要做二次开发比如改编码方式、换约束处理策略、接入Simulink仿真MATLAB的优势就出来了——矩阵运算天然适配种群操作一个m文件就能跑完整个流程调试起来非常直观画图看Pareto前沿更是点几个按钮的事。我最推荐的方式是“从零手写一套NSGA-II框架”不要拿现成工具箱直接跑。原因很简单手写一遍之后你对每个算子的作用和每个参数的影响才有体感。工具箱是黑盒出了问题你连排查的方向都没有。下面这套代码是我在多个项目里验证过的底稿结构清晰可以直接改目标函数用于自己的问题。2.2 测试问题选择ZDT1函数写多目标优化代码一定要用有标准参照的测试函数验证否则纯靠自己猜正确性很容易“错得一致”。我这里选ZDT1这是多目标优化领域最经典的二目标测试函数之一Pareto前沿有解析解可以直接画图对比。ZDT1的目标函数定义如下第一个目标 (f_1(x) x_1)第二个目标 (g(x) 1 \frac{9}{n-1}\sum_{i2}^{n}x_i) (f_2(x) g(x)\left(1 - \sqrt{x_1 / g(x)}\right))决策变量 (x_i \in [0,1])变量个数通常取30。真实Pareto前沿满足 (f_2 1 - \sqrt{f_1})。有了这个解析表达式算法跑完之后把结果和理想前沿叠在一张图上好不好一眼就能看出来。3. 核心函数逐段实现与讲解3.1 主函数框架与参数设置先看整个NSGA-II的流程骨架。我不放完整代码防止篇幅爆炸但下面每个子函数的实现逻辑我都会讲清楚照着拼起来就能跑。%% NSGA-II主流程ZDT1示例 clc; clear; close all; % ------------------ 问题参数 ------------------ nVar 30; % 决策变量个数ZDT1标准设置 varMin zeros(1, nVar); % 变量下界 varMax ones(1, nVar); % 变量上界 nObj 2; % 目标个数 % ------------------ 算法参数 ------------------ nPop 100; % 种群规模 maxGen 200; % 最大迭代次数 pCrossover 0.9; % 交叉概率 pMutation 1 / nVar; % 变异概率 etaC 20; % SBX交叉分布指数 etaM 20; % 多项式变异分布指数 % ------------------ 初始化种群 ------------------ pop repmat(struct(Position, [], Cost, [], Rank, [], CrowdingDist, []), nPop, 1); for i 1:nPop pop(i).Position varMin (varMax - varMin) .* rand(1, nVar); pop(i).Cost ZDT1(pop(i).Position); end % ------------------ 初始分层与拥挤度 ------------------ [pop, F] NonDominatedSorting(pop); pop CalcCrowdingDistance(pop, F); % ------------------ 主循环 ------------------ for gen 1:maxGen % 锦标赛选择产生交配池 MatingPool TournamentSelection(pop, nPop); % 交叉变异产生子代 Offspring GeneticOperators(MatingPool, varMin, varMax, pCrossover, pMutation, etaC, etaM); % 父代子代合并 Combined [pop; Offspring]; % 合并后重新分层、计算拥挤度 [Combined, F] NonDominatedSorting(Combined); Combined CalcCrowdingDistance(Combined, F); % 精英保留筛选下一代种群 pop SelectNextPopulation(Combined, nPop); % 打印当前代数最优前沿 fprintf(Generation %d: Front Size %d\n, gen, numel(F{1})); end % ------------------ 结果可视化 ------------------ trueFront 0:0.01:1; plot(trueFront, 1 - sqrt(trueFront), r-, LineWidth, 2); hold on; front [pop(F{1}).Cost]; plot(front(1, :), front(2, :), b., MarkerSize, 10); legend(True PF, NSGA-II, Location, northeast); xlabel(f_1); ylabel(f_2);这里面最关键的参数是种群规模、迭代次数和交叉变异概率。我常用的经验值是决策变量50个以内种群100-200就行迭代次数取决于问题复杂度标准测试函数一般200-500代足够工业问题要看收敛曲线决定。交叉概率0.9是经典设置变异概率取1/变量个数——这个逻辑是让每个变量平均有1次变异机会不会太多也不会太少。3.2 非支配排序快排思想的优化应用非支配排序是整个算法效率的关键。最粗暴的做法是每两个个体都比较一次复杂度 (O(MN^2))种群一上200就很慢。NSGA-II的经典实现里用了一种“快排式”优化对每个个体记录两个信息——被它支配的解集合S和支配它的解数量np。function [pop, F] NonDominatedSorting(pop) nPop numel(pop); % 记录支配关系 for p 1:nPop pop(p).DominatedCount 0; pop(p).DominatedSet []; for q 1:nPop if p q, continue; end if dominates(pop(p).Cost, pop(q).Cost) pop(p).DominatedSet [pop(p).DominatedSet, q]; elseif dominates(pop(q).Cost, pop(p).Cost) pop(p).DominatedCount pop(p).DominatedCount 1; end end end % 分层 F {}; currentFront []; for p 1:nPop if pop(p).DominatedCount 0 pop(p).Rank 1; currentFront [currentFront, p]; end end F{1} currentFront; k 1; while ~isempty(F{k}) nextFront []; for i F{k} for j pop(i).DominatedSet pop(j).DominatedCount pop(j).DominatedCount - 1; if pop(j).DominatedCount 0 pop(j).Rank k 1; nextFront [nextFront, j]; end end end k k 1; F{k} nextFront; end F(end) []; % 删除最后一个空前沿 end function res dominates(cost1, cost2) % cost1 支配 cost2cost1所有目标不差于cost2且至少一个严格更好 res all(cost1 cost2) any(cost1 cost2); end这个实现里最容易写错的是DominatedCount的递减逻辑。每处理完一个前沿下一个前沿的候选解应该是“被当前前沿某个解支配、且除此之外没有其他人支配它”的个体所以每遇到一个前前沿解就把它支配集合里每个解的np减1。等np减到0说明它确实没有其他“尚未分层”的解再支配它了可以归入下一层。3.3 拥挤度距离保持Pareto前沿的均匀性拥挤度距离的计算分三步先按每个目标分别排序然后对每个目标计算相邻解的归一化距离最后把两个目标的距离相加。function pop CalcCrowdingDistance(pop, F) nObj numel(pop(1).Cost); for k 1:numel(F) front F{k}; nFront numel(front); if nFront 2 for i front pop(i).CrowdingDist inf; end continue; end % 提取前沿个体并初始化距离 costs [pop(front).Cost]; dist zeros(1, nFront); for m 1:nObj [~, idx] sort(costs(m, :)); fmin costs(m, idx(1)); fmax costs(m, idx(end)); % 边界个体设为无穷大保证被优先保留 dist(idx(1)) inf; dist(idx(end)) inf; for i 2:nFront - 1 dist(idx(i)) dist(idx(i)) (costs(m, idx(i1)) - costs(m, idx(i-1))) / (fmax - fmin); end end for i 1:nFront pop(front(i)).CrowdingDist dist(i); end end end注意分母 (f_{max} - f_{min})这是归一化处理。不同目标的取值范围可能差好几个数量级如果不归一化数值大的目标会主导拥挤度计算。另外一个细节是边界个体直接给无穷大——因为边界是Pareto前沿的端点无论如何都要保住否则前沿会从两边往里缩。这两个细节是很多初写者容易漏的漏了之后最典型的现象就是最终解集的前沿边界缺一块。3.4 锦标赛选择与遗传算子实现锦标赛选择很简单每次从种群里随机抽两个个体比较它们的Rank和CrowdingDistRank小的胜出Rank相同则拥挤度大的胜出这样能让解往稀疏区域方向更新。重复抽nPop次构成交配池。function MatingPool TournamentSelection(pop, nPop) MatingPool []; for i 1:nPop idx randperm(nPop, 2); a idx(1); b idx(2); if pop(a).Rank pop(b).Rank MatingPool [MatingPool, a]; elseif pop(a).Rank pop(b).Rank MatingPool [MatingPool, b]; else if pop(a).CrowdingDist pop(b).CrowdingDist MatingPool [MatingPool, a]; else MatingPool [MatingPool, b]; end end end end交叉算子我这里用模拟二进制交叉SBX这是遗传算法处理实数编码问题的经典算子。SBX的核心思想是让子代在父代附近生成且离得近的概率更高分布指数(etaC)控制这个“附近”的范围(etaC)越大子代离父代越近。标准的做法是(etaC 20)左右。function Offspring GeneticOperators(MatingPool, varMin, varMax, pCrossover, pMutation, etaC, etaM) nPop numel(MatingPool); nVar numel(varMin); Offspring repmat(struct(Position, [], Cost, []), nPop, 1); for i 1:2:nPop p1 MatingPool(i); if i 1 nPop p2 MatingPool(i1); else p2 MatingPool(1); end child1 MatingPool(i); child2 p2; if rand() pCrossover % SBX交叉 u rand(1, nVar); beta zeros(1, nVar); idx1 u 0.5; idx2 ~idx1; beta(idx1) (2 * u(idx1)).^(1/(etaC1)); beta(idx2) (1 ./ (2 * (1 - u(idx2)))).^(1/(etaC1)); child1.Position 0.5 * ((1 beta) .* MatingPool(p1).Position (1 - beta) .* MatingPool(p2).Position); child2.Position 0.5 * ((1 - beta) .* MatingPool(p1).Position (1 beta) .* MatingPool(p2).Position); child1.Position min(max(child1.Position, varMin), varMax); child2.Position min(max(child2.Position, varMin), varMax); end % 多项式变异 if rand() pMutation child1.Position PolynomialMutation(child1.Position, varMin, varMax, etaM); end if rand() pMutation child2.Position PolynomialMutation(child2.Position, varMin, varMax, etaM); end child1.Cost ZDT1(child1.Position); child2.Cost ZDT1(child2.Position); Offspring(i) child1; if i 1 nPop Offspring(i1) child2; end end end多项式变异是NSGA-II标配的变异算子思路是对每个变量按一定概率加上一个“扰动”扰动大小也由分布指数(etaM)控制。它和高斯变异相比尾部概率更厚偶尔能跳出局部区域但又不至于太激进。3.5 精英保留父代子代合并后如何取舍这是NSGA-II比第一代NSGA提升最大的地方。每一代结束后把父代种群和子代种群合并成2N个个体然后做整体非支配排序从第一前沿开始依次把整层个体加入新种群直到加入某一层会超过N为止——这时用拥挤度从大到小筛选这层个体填满剩下名额。function pop SelectNextPopulation(combined, nPop) [combined, F] NonDominatedSorting(combined); combined CalcCrowdingDistance(combined, F); nextPop []; for k 1:numel(F) if numel(nextPop) numel(F{k}) nPop nextPop [nextPop, F{k}]; else remaining nPop - numel(nextPop); front F{k}; [~, idx] sort([combined(front).CrowdingDist], descend); nextPop [nextPop, front(idx(1:remaining))]; break; end end pop combined(nextPop); end这个策略保证了“好解不会丢”。即使某代子代质量整体很差父代里的好解依然通过合并筛选保住了所以算法不会出现进化过程中的回退。这个机制简单但效果极其显著。4. 运行调试与效果验证4.1 如何验证代码写对了很多新手跑完NSGA-II看到屏幕上画出一堆点就觉得完事了。错。你得先确认这些点是不是真的逼近真实Pareto前沿否则后面所有依赖它的决策都是白搭。验证方法分两步。第一步用ZDT1这种有解析解的测试函数跑一遍把最终得到的Pareto前沿和真实前沿画在一张图上。比如ZDT1的真实前沿是 (f_2 1 - \sqrt{f_1})如果算法正确蓝色散点应该均匀覆盖在红色曲线附近。如果散点偏到曲线下方说明有解“作弊”了——通常是约束处理有bug如果散点集中在某一小段说明拥挤度计算有问题多样性不够。第二步看收敛曲线。每一代记录当前第一前沿的最小拥挤度均值或者前沿个体数理想情况下前沿个体数会先增加后趋稳说明种群逐渐铺满整个前沿。如果前沿个体数一直波动很大说明选择压力不对Rank和拥挤度比较的逻辑需要检查。4.2 参数调优的实战经验参数方面我最常被问到的是“交叉概率、变异概率到底怎么设”。我的经验是交叉概率0.8到0.95都没问题尤其在实数编码下交叉是主要搜索动力设太低收敛会非常慢变异概率不要设大0.05到0.2之间比较合适ZDT1这种连续问题我习惯直接用 (1/nVar)能兼顾探索和稳定。分布指数 (etaC) 和 (etaM) 一般都在15到30之间调小一点搜索步长大大一点局部精细搜索能力强。种群规模和迭代次数是一对需要搭配的参数。种群太小比如50前沿覆盖容易有空洞种群太大比如500每次迭代的排序耗时增长明显。对标准测试函数100乘300代就够了工程问题我一般会先跑一版看收敛趋势如果100代之后前沿还在明显变化就把迭代次数翻倍或者检查是不是交叉概率偏低。记住一个原则在收敛的前提下参数越少越好调不要为了表现算法花哨去堆参数。5. 常见问题与排查技巧实录5.1 初始化种群就报错的三个坑第一个坑是决策变量边界写反或者写成标量。varMin zeros(1, nVar)和varMax ones(1, nVar)是向量但如果直接从别处复制代码容易把nVar写错导致随机初始化向量维度不匹配。第二个坑是目标函数句柄传错。很多初学者在种群初始化循环里写pop(i).Cost ZDT1(pop(i).Position)但ZDT1函数接受的行向量和列向量不一致就会报维度错误。我自己写目标函数时会习惯加一句x x(:);做强制行向量转换省得后面排查半天。第三个坑是结构体数组初始化不对。repmat(struct(...), nPop, 1)是MATLAB里创建结构体数组的标准写法但有人习惯pop []然后在循环里pop(i).Position ...第一次没初始化会直接报错。我建议统一用repmat方式后面读代码也清晰。5.2 结果不收敛或解集分布异常的排查顺序先解决“解全挤在一起”的问题。这种多半是拥挤度计算里没有对边界个体给无穷大或者归一化分母出现0——某个目标的最大最小值相等说明这个目标在种群中根本没变化搜索失效了。排查的时候打印每一代Cost的min和max如果某个目标连续多代没有变化优先检查变异算子里有没有把变量钳位到边界。再解决“前沿离真实曲线很远”的问题。这种一般是选择压力不够或者在精英保留时把整层前沿错误地砍掉了一部分。我调试时常在SelectNextPopulation里打印每一层前沿的个体数如果发现第一前沿本身就很小甚至为空问题大概率出在非支配排序的Rank赋值上尤其是pop(j).DominatedCount递减的循环里可能出现同一个体被重复计数的问题。5.3 跑得太慢怎么优化MATLAB写NSGA-II最容易被诟病的就是慢。如果种群200、迭代500代纯for循环版本可能要跑一两分钟。排查思路很简单用profile on跑一遍看耗时集中在哪个函数。我的经验是九成时间花在非支配排序的两两层比较上。优化手段主要有两个一是向量化目标函数计算把整个种群的Cost一次性算出来而不是循环里逐个调二是减少结构体字段访问次数在排序循环里先把Cost提取到普通矩阵再比较。实测下来向量化之后运行速度能提升5到10倍。另外注意MATLAB的数组预分配所有循环里要增长的数组最好提前初始化否则运行时反复扩容非常慢。写在最后的一些体会这套NSGA-II代码我前前后后改了不下五版最大的感受是多目标优化算法不像单目标光“找到好解”不够还得“均匀地找到一堆好解”。很多人把NSGA-II跑出来就收工但真正有价值的是看Pareto前沿的分布形态——它直接反映了搜索策略的偏好。后续如果你要把这套代码用到实际项目里我建议先从改目标函数开始把ZDT1函数替换成你真实问题的目标函数同时注意决策变量的取值范围一定要和实际问题对齐否则跑出来的“前沿”在工程上可能完全不可用。我自己的习惯是先跑50代看趋势确认目标函数的量纲、变量边界都没问题后再放心跑完整迭代。希望这份代码和踩坑记录能帮你少走点弯路。本文还有配套的精品资源点击获取