
简介这份资料提供NSGA-II算法的Matlab完整实现面向多目标优化研究者、研究生及竞赛参与者可用于掌握非支配排序、拥挤距离、精英策略等核心思想并基于ZDT1-6二维与DTLZ1-6三维标准测试函数进行仿真验证。压缩包共27个文件包含10个M脚本、10个TXT测试数据文件与7个Fig图形文件M脚本涵盖主程序、遗传算子、子代生成、拥挤距离计算、非支配排序、精英策略等功能模块TXT数据提供不同测试问题的输入点集Fig图形直观展示算法在ZDT及DTLZ系列上的Pareto前沿收敛情况。资源包仅2.41MB下载与运行十分便捷。目前已有3208人学习下载。借助该代码读者既能逐模块剖析NSGA-II的完整算法流程也能一键运行获得标准测试问题上的优化结果代码采用模块化设计便于替换测试函数、调整种群规模与迭代次数等参数实验数据完整适合用作算法对比实验、毕业设计或课程作业的可靠参考与二次开发基础。1. NSGA-II的MATLAB代码一次运行拿回一整条帕累托前沿做多目标优化最「翻车」的体验是把成本和效率用加权和压成一个单目标权重调三天老板说换一组参数再来一遍。NSGA-II不干这事——它用非支配排序和拥挤距离两个指标直接筛选解跑一次就给出一整套帕累托前沿。这份资源就是NSGA-II的MATLAB完整实现从种群初始化到精英策略收尾每段代码都能直接在MATLAB里跑。适合做调度排产、结构优化、路径规划以及任何需要同时权衡两个以上目标的人。看完你不仅能跑通还能把测试函数换成自己的业务目标。2. 核心算法拆解非支配排序与拥挤距离怎么用MATLAB写先解决一个基础问题NSGA-II和普通遗传算法差在哪普通遗传算法靠适应度打分一个个体一个分数NSGA-II靠「支配关系」分层。一个解支配另一个解是指它在所有目标上都不落下风且至少在一个目标上严格更优。这一章把三个核心部分拆开每段代码都可以单独贴进MATLAB验证。2.1 非支配排序把解集分成一层一层的前沿先看最核心的一小段。任意两个解 a 和 b判断谁支配谁只需要一条规则a 的所有目标都不差于 b且至少一个目标严格优于 b。MATLAB 里用逻辑向量实现非常干净function d dominates(a, b) % d true 表示 a 支配 b % 默认所有目标都是最小化方向 d all(a b) any(a b); endall(a b)检查 a 在所有目标上都不劣于 bany(a b)确保至少一个目标严格更优。两个条件同时满足才算支配。这里有个约定如果做的是最大化问题记得在外面对目标取负号或者把所有目标转成最小化否则排序逻辑会反过来。非支配排序做的事情就是给整个种群分层第一层是没有被任何解支配的个体第二层是去掉第一层后再看没有被支配的个体一直剥到最后一层。下面的函数沿用原始论文里的计数思路每个解记两个量被多少个解支配记为 n以及它支配了哪些解记为 S。function [rank, front] fast_non_dominated_sort(fitness) N size(fitness, 1); n zeros(N, 1); % n(i): 支配个体 i 的解数量 S cell(N, 1); % S{i}: 被个体 i 支配的个体索引集合 front cell(N, 1); % 每一层前沿的个体索引 nf 0; % 当前前沿层数 for i 1:N for j 1:N if i j, continue; end if dominates(fitness(i,:), fitness(j,:)) S{i} [S{i}, j]; elseif dominates(fitness(j,:), fitness(i,:)) n(i) n(i) 1; end end if n(i) 0 nf nf 1; front{nf} [front{nf}, i]; end end f 1; while f nf for k 1:numel(front{f}) i front{f}(k); for q S{i} n(q) n(q) - 1; if n(q) 0 nf nf 1; front{nf} [front{nf}, q]; end end end f f 1; end rank zeros(N, 1); for i 1:nf for k 1:numel(front{i}) rank(front{i}(k)) i; end end front front(1:nf); end逻辑说明第一次遍历找出所有不被任何人支配的个体n(i) 0它们天然属于第一层。然后遍历第一层的每个解把它所支配的个体的 n 减一减到零说明这个个体只被第一层支配于是归入第二层。循环往复所有个体都会被分层。rank(i)存的是个体 i 属于第几层front{f}存的是每一层包含哪些个体。参数说明fitness是 N×M 矩阵N 为个体数M 为目标数。返回的rank是 N×1 列向量front是元胞数组。这段代码的时间复杂度是 O(N²)500 个体内跑起来很轻松如果要跑几千个体建议换基于分类树的实现否则每代光排序就要等半天。2.2 拥挤距离同一层里怎么保证多样性分层只解决优先级不解决分布。如果只按 rank 选种群会慢慢聚到某一小段区域帕累托前沿就不完整了。拥挤距离的思路很直白在同一层里把解按某个目标排序看它前后相邻的两个解在这个目标上差多远。差距越大说明这个解周围越「空」越值得保留。function dist crowding_distance(fitness, idx) n numel(idx); dist zeros(1, n); n_obj size(fitness, 2); for m 1:n_obj f fitness(idx, m); [~, order] sort(f); % 边界解的距离设为无穷大永远优先保留 dist(order(1)) inf; dist(order(n)) inf; fmin f(order(1)); fmax f(order(n)); % 该目标上所有解完全相同跳过避免除零 if fmax - fmin eps continue; end for i 2:n-1 dist(order(i)) dist(order(i)) ... (f(order(i1)) - f(order(i-1))) / (fmax - fmin); end end end逻辑说明每个目标单独算一遍把相邻距离除以当前目标的范围做归一化最后累加。这样多个目标的范围差异不会影响排序。边界解排序后第一个和最后一个距离直接被置为 inf相当于边缘解永远优先于内部解被保留这能保证前沿的两个端点不被弄丢。参数说明idx是某一层前沿的个体索引向量注意它是局部索引还是全局索引取决于调用位置。一个常见的血泪经验是dist的下标是idx的局部顺序不是全局种群下标。在主函数里调用时要写成dist(front{f}) crowding_distance(fit, front{f})直接传全局下标进去排序会错位最后选出来的解完全不对。eps判断很有必要如果这一层所有解在某个目标上数值完全一样fmax - fmin为 0不拦截就会算出 NaN。而 NaN 一进锦标赛选择比较逻辑全乱跑出来的前沿会莫名其妙缺一块。2.3 精英策略父代子代合并保住最好的解普通遗传算法是父代生成子代然后直接用子代替换父代。这样风险在于每代最好的解可能被交叉变异毁掉。NSGA-II 的精明之处是先合并后裁剪pop_size 个父代和 pop_size 个子代合成一个 2×pop_size 的大池子对这个大池子做非支配排序按层往里装新种群装不下某一层时按拥挤距离从大到小把这一层的人挤进来。function [new_pop, new_fit] elitism(pool, pool_fit, rank, dist, pop_size) N size(pool, 1); new_pop zeros(pop_size, size(pool, 2)); new_fit zeros(pop_size, size(pool_fit, 2)); count 0; f 1; while count pop_size layer find(rank f); if isempty(layer), break; end if count numel(layer) pop_size % 这一层能完整装下直接全部保留 new_pop(count1:countnumel(layer), :) pool(layer, :); new_fit(count1:countnumel(layer), :) pool_fit(layer, :); count count numel(layer); else % 装不下按拥挤距离降序取前 need 个 need pop_size - count; [~, order] sort(dist(layer), descend); take layer(order(1:need)); new_pop(count1:pop_size, :) pool(take, :); new_fit(count1:pop_size, :) pool_fit(take, :); count pop_size; end f f 1; end end逻辑说明这个函数是精英策略的具体落地。它保证每一代的最优解一定不会被交叉变异冲掉因为父代和子代是放在一起竞争的父代中 rank 靠前的个体天然占据优势。当某一层塞不下时sort(dist(layer), descend)把该层里拥挤距离最大的个体优先保留前沿的分布性就靠这一步维持。参数说明pool和pool_fit是合并后的种群rank和dist是对这个大池子算出的非支配层和拥挤距离pop_size是最终想保留的种群规模。输出new_pop和new_fit就是下一代的父代。3. 工程结构从main入口到各算子的完整调用链这一章把源码的文件组织方式和主循环讲清楚。拿到代码第一步不是急着跑而是先看懂每个文件在链路里的位置后面改自己的问题时才知道动哪里。3.1 文件职责一览这份 MATLAB 工程按功能拆成了七个文件每个文件只干一件事改动成本很低。我整理了一张表文件名职责nsga2_main.m主入口定义种群规模、迭代次数、变量边界驱动整个循环evaluate_zdt1.m目标函数评估默认内置 ZDT1 双目标测试函数fast_non_dominated_sort.m非支配排序返回 rank 与分层索引crowding_distance.m计算某一层个体的拥挤距离tournament_select.m二元锦标赛选择返回选中的父代索引genetic_operators.m交叉与变异内部含 SBX 交叉和多项式变异elitism.m父代子代合并后的精英保留文件之间的依赖是单向的nsga2_main.m调用其余所有文件tournament_select.m和genetic_operators.m不依赖排序模块所以如果你只想复用一个算子直接拷贝对应文件就行不用整套搬走。3.2 主循环初始化、评估、排序、选择、交叉变异、精英保留主循环是整个项目的骨架。我挑核心部分展开省略了打印和绘图逻辑function nsga2_main() pop_size 100; max_gen 250; n_var 30; lb zeros(1, n_var); % 变量下界 ub ones(1, n_var); % 变量上界 % 随机初始化种群每行一个个体 parent rand(pop_size, n_var); fit evaluate_zdt1(parent); for g 1:max_gen % 1. 对当前父代做非支配排序 [rank, front] fast_non_dominated_sort(fit); dist zeros(1, pop_size); for fi 1:numel(front) dist(front{fi}) crowding_distance(fit, front{fi}); end % 2. 锦标赛选择父代索引 sel tournament_select(rank, dist, pop_size); % 3. 交叉变异生成子代 child genetic_operators(parent, sel, lb, ub); child_fit evaluate_zdt1(child); % 4. 合并父代子代统一排序后裁剪回 pop_size pool [parent; child]; pool_fit [fit; child_fit]; [merge_rank, merge_front] fast_non_dominated_sort(pool_fit); merge_dist zeros(1, 2 * pop_size); for fi 1:numel(merge_front) merge_dist(merge_front{fi}) crowding_distance(pool_fit, merge_front{fi}); end [parent, fit] elitism(pool, pool_fit, merge_rank, merge_dist, pop_size); end % 最后一轮取出第一前沿就是帕累托解集 [last_rank, last_front] fast_non_dominated_sort(fit); pareto_x parent(last_front{1}, :); pareto_f fit(last_front{1}, :); save(pareto_result.mat, pareto_x, pareto_f); end代码块里的四步对应四个算子顺序不能乱。第一步对当前父代排序得到 rank 和 dist 供选择用第二步用二元锦标赛选出 pop_size 个索引注意这里选的是索引不是个体下一步要从 parent 里取对应行做交叉第三步交叉变异后子代规模也是 pop_size第四步把父代和子代合并成 200 个个体重新排序再裁剪。最后保存的是last_front{1}也就是第一前沿这就是我们真正想要的帕累托解集。参数说明pop_size取 100 在大多数双目标问题上是够用的三目标以上建议提到 150200。max_gen取决于问题的收敛速度ZDT1 这种平滑函数 200 代能看到完整前沿实际工程问题可能要 500 代以上。n_var是决策变量个数ZDT1 标准形式是 30 个换成实际问题后按你的变量数改。3.3 遗传算子SBX交叉和多项式变异的参数怎么调交叉变异是 NSGA-II 里最容易被当成「玄学」的部分。先说 SBX模拟二进制交叉的核心逻辑生成一个随机数 u映射出一个 beta 值beta 控制子代偏离父代的幅度。beta 接近 0 时子代接近父代beta 远离 0 时子代偏向两边。function beta sbx_beta(u, eta_c) if u 0.5 beta (2 * u)^(1 / (eta_c 1)); else beta (1 / (2 * (1 - u)))^(1 / (eta_c 1)); end endeta_c是交叉分布指数默认取 20。这个值越大beta 越集中靠近 1子代和父代越像值越小子代探索范围越大。实际调参时eta_c在 1030 之间都是安全区别小于 5否则子代会频繁越界。多项式变异相对简单对每个基因位以 1/n_var 的概率扰动扰动幅度由eta_m控制通常取 20100。eta_m越大变异越细微适合后期精细收敛越小变异越激进适合早期探索。我习惯把eta_m固定在 20遇到陷入局部前沿的情况再把eta_m降到 10 以下试试。4. 避坑指南跑NSGA-II最容易踩的五个现场这个项目的代码量不大但它属于「跑通容易跑好难」的类型。下面五条是我实际调试中踩过的坑按发生频率排序。4.1 拥挤距离一上来全是 inf种群边界被塞满现象跑完第一代第一前沿里所有解的拥挤距离都是 inf后续选择时边界解反复胜出种群多样性很差。原因多数是目标函数写得有问题导致某一层里不同解在某个目标上取值完全一样。比如两个目标高度相关或者测试函数的第一目标被固定成常数fmax - fmin eps提前跳过后边界解以外的距离计算不完整。解决先在 crowding_distance 里加断点检查fmax - fmin是否过小。如果确实相关性强考虑换目标函数或者在计算距离前对目标做一次归一化。另外检查是否把inf赋给了局部下标而不是全局下标这也是边界解异常的常见来源。4.2 种群收敛到单点前沿变成一根短棒现象迭代到 100 代以后所有解挤在帕累托前沿的一小段上距离拉不开。原因锦标赛选择里比较 rank 和 dist 的逻辑写反了或者拥挤距离根本没算进选择。还有一个隐蔽原因交叉概率设成了 1.0每一代所有个体都参与交叉把一些好的基因结构彻底打散了。解决先确认 tournament_select 里是「rank 小优先同 rank 比 dist 大」再确认交叉概率pc在 0.80.95 之间不要设成 1。最后检查精英保留环节是不是真的从合并池里选而不是直接拿子代覆盖父代。4.3 目标矩阵方向搞反非支配关系全乱现象跑出来的「帕累托前沿」是一条从左下到右上的单调上升曲线明显不对。ZDT1 的真实前沿是一条凹曲线。原因ZDT1 是最小化问题但有人习惯写最大化目标。比如把成本取倒数当成目标方向没统一dominates里面的和就全部失效。解决养成习惯所有目标在进排序之前统一转成最小化。最大化目标就取负号保证all(a b) any(a b)判定正确。在 evaluate 函数里写一行注释标明每个目标的优化方向避免几周后自己都忘了。4.4 目标数一多每代耗时直线上升现象从双目标改成三目标后运行时间从几秒变成几分钟。非支配排序和拥挤距离的计算量都随目标数增加。原因fast_non_dominated_sort 是 O(N²) 的逐对比较目标数变多不影响排序复杂度但拥挤距离每个目标都要排序一次目标数从 2 变 3这部分计算量增加 50%。更关键的是目标数增加后种群必须扩大否则前沿覆盖不住整体耗时自然上去。解决三目标以上建议把种群控制在 200 以内迭代次数控制在 300 以内。如果必须大规模计算把排序函数里的双重循环改成向量化判断用 bsxfun 或 repmat 一次比较整个矩阵能省掉一半时间。4.5 初始化边界给错算法在无效空间里白跑现象加了几个变量的上下界后跑出来全是边界值或者目标函数报 NaN。原因遗传算法生成的子代可能越过边界而 genetic_operators 里没有做限幅处理。另一个常见情况是lb、ub的维度写成标量和 n_var 不匹配MATLAB 的隐式扩展把错误再放大。解决在交叉变异函数末尾统一做一次child min(max(child, lb), ub)并确认 lb 和 ub 是 1×n_var 的行向量。初始化用rand(pop_size, n_var)后按边界缩放到实际区间不要假设随机数范围就是变量范围。5. 二次开发把ZDT1换成你自己的业务目标这套代码最有价值的部分在于排序、选择、精英保留都是与具体问题无关的换目标函数就能换一个场景。我总结三个最常用的改造方向。5.1 替换目标函数假设你要优化「配送路径的里程和碳排放」只需要重写 evaluate 函数输入一个决策变量行向量路径方案的编码输出一行两列的目标值。注意几个约定输入是 pop_size×n_var 的矩阵用向量化写法算目标别写 for 循环逐行算输出列的顺序决定了后续排序方向统一最小化。改完后主函数里调用处替换成你的评估函数即可。5.2 加约束条件NSGA-II 原始论文处理约束的方法是约束支配一个可行解永远支配不可行解两个都可行时按目标比较两个都不可行时约束违反程度小的优先。在代码里你可以给每个个体额外算一个约束违反总量cv在锦标赛选择和精英保留时先比 cv再比 rank 和 dist。5.3 调整变量类型如果决策变量里混着整数和连续变量在 genetic_operators 的变异步骤末尾对整数基因位做一次 round 就行。更规范的做法是让交叉变异都在实数空间进行评估前把整数位取整这样既保证遗传算子的连续性又满足整数约束。从那以后我每次改目标函数都会强制走一遍完整流程先拿 ZDT1 跑通基线确认前沿形状和论文一致再替换成自己的评估函数检查目标值范围和方向最后跑 50 代看中间结果确认没踩到边界或方向问题再放开跑完整迭代。这套流程帮我避开好几次「跑了一整夜出来结果全不可用」的尴尬。希望帮到你。本文还有配套的精品资源点击获取