资讯详情

资讯详情

NSGA-II多目标优化MATLAB代码实现:非支配排序与拥挤距离详解

简介这份资源是NSGA-II多目标优化算法的MATLAB实现代码面向正在学习智能优化算法、准备课程设计或科研实验的高校学生与研究人员。代码围绕非支配排序、拥挤距离计算、精英策略与基因操作等核心模块展开并针对二维测试函数ZDT1-6与三维测试函数DTLZ1-6提供了完整的测试数据与仿真图像便于读者理解算法在标准测试集上的收敛性与分布性表现。压缩包共27个文件包含10个m脚本、10个txt测试数据文件与7个fig仿真结果图整体约2.41MB目录结构清晰主程序与各功能模块分离方便按需查阅与二次修改。目前已有3204人学习下载适合作为算法入门复现、实验对比或论文写作的参考素材读者可借助现成脚本快速跑通流程并对照仿真图像验证非支配解集的分布效果。1. 从一次“跑不通”的选型说起NSGA-II 的 MATLAB 代码到底能解决什么如果你手头有一个多目标优化问题——比如同时压低成本和能耗、同时拉高精度和吞吐——大概率绕不开 NSGA-II。它属于遗传算法家族里的经典多目标版本核心是三件事非支配排序、拥挤距离、精英策略。这三件套决定了它能在一次运行里给出一整条帕累托前沿而不是像加权求和那样只能拿到一个折中点。这份 MATLAB 代码包就是把这三件事完整落地的实现适合两类人一类是课程设计或论文里需要跑出帕累托前沿、但不想从零手写排序逻辑的另一类是已经用过工具箱函数、想拆开看内部机制到底怎么转的。它不依赖额外工具箱纯 MATLAB 脚本加函数文件拿到就能在本地跑通改目标函数也只需要动一个文件。下面按“先立住原理、再动手复现、最后说坑”的顺序拆开讲。2. 非支配排序与拥挤距离NSGA-II 的两根支柱怎么在代码里落地2.1 非支配排序把种群切成一层一层的“优劣梯队”多目标优化里没有单一最优解只有“我不比你差”的支配关系。解 A 支配解 B意思是 A 在所有目标上都不劣于 B且至少有一个目标严格更优。非支配排序就是反复找出当前种群里不被任何其他解支配的那一批标为第一层剔除后再找第二批依此类推。代码里这一步通常是一个双层循环外层遍历每个个体内层拿它和其余个体逐一比较支配关系统计被支配次数和它支配了谁。被支配次数为 0 的个体就是当前层的成员。% 非支配排序核心逻辑简化示意 front 1; F(front).f []; individual []; for i 1:N individual(i).n 0; % 被支配计数 individual(i).p []; % 支配的解集合 for j 1:N dom_less 0; dom_equal 0; dom_more 0; for k 1:M if x(i).cost(k) x(j).cost(k) dom_less dom_less 1; elseif x(i).cost(k) x(j).cost(k) dom_equal dom_equal 1; else dom_more dom_more 1; end end if dom_less 0 dom_equal ~ M individual(i).n individual(i).n 1; % j 支配 i elseif dom_more 0 dom_equal ~ M individual(i).p [individual(i).p j]; % i 支配 j end end if individual(i).n 0 x(i).rank 1; F(front).f [F(front).f i]; end end这段逻辑里M是目标个数N是种群规模。dom_less、dom_equal、dom_more三个计数器分别记录第 k 个目标上“更小、相等、更大”的情况。判断支配的条件是没有任何一个目标更差dom_less 0且不是所有目标都相等dom_equal ~ M。参数上最容易出问题的是目标方向——如果你的问题是最大化得先把目标取负否则排序结果会完全反过来。常见做法是在目标函数里统一转成最小化排序逻辑就不用改。2.2 拥挤距离同一层里怎么再分出个先后非支配排序把种群分了层但同一层里可能有几十个解选谁进下一代NSGA-II 用拥挤距离来衡量一个解周围有多“空”。距离越大说明它周围解越稀疏越该保留这样帕累托前沿才能铺得开、不会全挤在一处。计算方式是对每个目标排序边界解的距离设为无穷大必须保留中间解取相邻两个解在该目标上的差值归一化后累加。% 拥挤距离计算对某一层 front 内的解 costs reshape([x(F(front).f).cost], M, []); % 取出该层所有解的目标值 n numel(F(front).f); distance zeros(n, 1); for k 1:M [~, idx] sort(costs(:, k)); distance(idx(1)) inf; % 边界解距离无穷大 distance(idx(end)) inf; f_max max(costs(:, k)); f_min min(costs(:, k)); if f_max - f_min 0 continue; % 该目标全相等跳过避免除零 end for t 2:n-1 distance(idx(t)) distance(idx(t)) ... (costs(idx(t1), k) - costs(idx(t-1), k)) / (f_max - f_min); end endf_max - f_min是归一化分母这一步很关键如果某个目标量纲是几千、另一个是零点几不归一化的话大量纲目标会完全主导拥挤距离。代码里对分母为 0 的情况做了跳过处理这是血泪经验——早期版本没加这个判断遇到所有解在该目标上相等时直接出 NaN整个选择环节就崩了。边界解赋inf保证它们一定被选中这是维持前沿两端不被截断的常规做法。2.3 精英策略父代和子代合并后再选为什么更稳很多遗传算法的做法是父代产生子代、子代直接替换父代。NSGA-II 不一样它把父代和子代合并成 2N 个个体一起做非支配排序和拥挤距离计算再从中挑 N 个进下一代。这个“精英策略”的好处是已经找到的好解不会因为一次交叉变异就被丢掉收敛更稳。选择顺序是先按 rank 从小到大整层整层地取取到某一层放不下时用拥挤距离从大到小补齐。% 精英选择合并种群后截断到 N combined [pop_parent, pop_child]; [combined, F] non_dominated_sort(combined); new_pop []; front 1; while numel(new_pop) numel(F(front).f) N new_pop [new_pop, F(front).f]; front front 1; end % 最后一层按拥挤距离排序补齐 last F(front).f; dist crowding_distance(combined, last); [~, order] sort(dist, descend); need N - numel(new_pop); new_pop [new_pop, last(order(1:need))];combined的长度是 2Nwhile循环负责整层接收need是还差多少个名额。这里有个容易翻车的点如果某一层的解数量刚好等于剩余名额while条件用的是会整层收下不会触发后面的补齐逻辑这是对的但如果写成就会多走一轮把本该整层收下的层拆开破坏前沿结构。参数上N一般取 100 到 200太小前沿稀疏太大单代计算量明显上升非支配排序是 O(MN²)N 翻倍计算量约四倍。3. 把代码跑起来目标函数、参数设置与一次完整运行3.1 目标函数怎么写以两个经典测试问题为例拿到代码包后第一个要改的就是目标函数。代码通常把目标函数单独放在一个文件里输入是决策变量向量输出是目标值向量。以 ZDT1 为例它有两个目标决策变量 30 维帕累托前沿是一条已知的凸曲线常用来验证算法是否正确。function f evaluate_objective(x) % ZDT1: 两个目标x 为 1×30 决策变量 f(1) x(1); g 1 9 * sum(x(2:end)) / (numel(x) - 1); f(2) g * (1 - sqrt(x(1) / g)); endf(1)直接取第一个变量f(2)由g和x(1)共同决定。这个函数的好处是帕累托前沿解析已知当g 1时f(2) 1 - sqrt(f(1))f(1)在 0 到 1 之间。跑完之后把结果和这条曲线对比就能判断代码有没有问题。如果你要换成自己的问题保持输入输出格式不变把内部计算替换掉即可目标个数M在参数文件里同步改。3.2 参数文件怎么配种群、代数、交叉变异率参数一般集中在一个脚本或结构体里常见字段如下表。这些值不是拍脑袋定的交叉率 0.9、变异率 1/决策变量数 是文献里的常用起点。参数含义常用取值调整影响N种群规模100太小前沿稀疏太大计算量平方增长max_gen最大代数200看收敛曲线前沿不再移动即可停pc交叉概率0.9太低探索不足太高破坏好解pm变异概率1/n_var按变量数自适应太大退化成随机搜索eta_c交叉分布指数20越大子代越靠近父代eta_m变异分布指数20控制变异步长越大扰动越小pm取1/n_var是经验做法变量越多单个变量被变异的概率越低整体变异强度保持稳定。eta_c和eta_m是模拟二进制交叉和多项式变异里的分布指数取值越大子代越集中在父代附近探索能力下降但局部搜索更细。我一般先用默认值跑一遍看收敛曲线再决定要不要调。3.3 一次完整运行从初始化到输出帕累托前沿主循环的结构是初始化种群、评估、非支配排序、进入迭代、每代做选择交叉变异、合并父子代、精英选择、更新。下面是一次运行的骨架。% 主流程骨架 pop initialize_population(N, n_var, lb, ub); pop evaluate_population(pop, evaluate_objective); [pop, F] non_dominated_sort(pop); pop calculate_crowding_distance(pop, F); for gen 1:max_gen child tournament_selection(pop, N); child crossover(child, pc, eta_c, lb, ub); child mutation(child, pm, eta_m, lb, ub); child evaluate_population(child, evaluate_objective); combined [pop, child]; [combined, F] non_dominated_sort(combined); combined calculate_crowding_distance(combined, F); pop truncate_population(combined, N); % 精英选择截断 fprintf(第 %d 代第一层解数%d\n, gen, numel(F(1).f)); end % 输出第一层作为帕累托前沿 pareto pop([pop.rank] 1); scatter([pareto.cost(1,:)], [pareto.cost(2,:)], filled);tournament_selection是二元锦标赛随机抽两个先比 rankrank 小的赢rank 相同比拥挤距离大的赢。truncate_population就是 2.3 节讲的精英选择。跑完后pareto里就是第一层解画出来应该是一条平滑曲线。如果画出来是一团散点或者只有一两个点多半是排序或截断环节出了问题下一章专门说。4. 避坑与排查五个让代码“看起来对但结果不对”的坑4.1 帕累托前沿只有一两个点现象跑完几百代第一层解数始终是个位数散点图几乎看不出曲线。原因通常是变异概率过低或种群多样性在早期就丢失也可能是目标函数里某个目标恒为常数导致支配关系退化。解决先把pm调到1/n_var确认不是变异问题再检查目标函数打印每代各目标的取值范围如果某个目标方差接近 0说明它没起作用需要重新设计目标或检查变量映射。4.2 结果每次跑都不一样且差异很大现象同一份代码、同一组参数两次运行的前沿形状差很多。原因遗传算法本身是随机的但如果差异大到前沿覆盖范围都变了通常是没固定随机种子或者种群规模太小导致随机性被放大。解决在初始化前加rng(1)固定种子方便复现和调试正式跑的时候把N提到 100 以上多跑几次取包络不要用单次结果下结论。4.3 拥挤距离出现 NaN 或 Inf 传播现象迭代到某一代选择环节报错或结果全变成 NaN。原因某个目标上所有解取值相等归一化分母为 0除法产生 NaNNaN 参与比较后污染整个距离数组。解决在拥挤距离计算里对分母为 0 的目标直接continue跳过该目标的距离累加代码 2.2 节已经给了这个判断。另外边界解赋inf是正常的但如果inf出现在中间解上说明排序索引越界检查idx的取值范围。4.4 最大化问题直接套用导致前沿反向现象目标明明是越大越好跑出来的前沿却集中在目标值很小的区域。原因非支配排序默认所有目标都是最小化最大化问题不转换方向支配关系就反了。解决在目标函数里对最大化目标取负f(k) -raw_value排序逻辑不动输出时再取回负号。这是最常见的翻车点尤其是从别人代码改过来的时候很容易漏掉方向转换。4.5 代数够了但前沿还在移动现象设了 500 代跑到最后前沿还在缓慢变化不确定该不该停。原因收敛判据不能只看代数要看前沿是否稳定。解决每隔若干代记录第一层解的目标均值或超体积指标画收敛曲线当曲线在若干代内波动小于阈值就可以停。常见做法是连续 20 代超体积变化小于 1e-4 就提前终止比死磕固定代数更省时间。5. 进阶技巧用超体积指标验证前沿质量而不是只看图跑出前沿只是第一步怎么判断这次运行比上次好肉眼看散点图太主观我一般用超体积指标。它衡量的是帕累托前沿和参考点围成的区域面积值越大说明前沿越靠前、铺得越开同时反映收敛性和多样性。计算时先定一个参考点取各目标上比最差解还差一点的值然后对前沿解按第一目标排序逐段累加矩形面积。function hv hypervolume(points, ref) % points: n×M 的前沿目标值ref: 1×M 参考点 [~, idx] sort(points(:,1)); points points(idx, :); hv 0; for i 1:size(points,1)-1 width points(i1,1) - points(i,1); height ref(2) - max(points(i,2), points(i1,2)); if height 0 hv hv width * height; end end hv hv (ref(1) - points(end,1)) * (ref(2) - points(end,2)); end这段是二维简化版ref要选得比所有解都差否则算出来偏大。width是相邻解在第一目标上的间隔height是参考点到该段较高那个解的距离。二维情况下这个逻辑够用三维及以上需要做切片或调用专门库代码包里如果带了这个函数直接传前沿和参考点即可。我习惯每跑完一次就记一个超体积值调参时对比这个数比盯着散点图靠谱得多。从那以后我每次改完参数都强制走一遍超体积对比不再凭感觉判断“好像好了一点”。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →