简介:NSGA-III是一种面向2到3目标优化的多目标进化算法,本资源是在MathWorks开源版本基础上逐行添加中文注释的Matlab实现,适合正在学习多目标优化、想读懂算法内部机制的初学者和研究人员。包内共16个文件,以14个Matlab脚本为主,从主程序、测试函数定义到非支配排序、参考点生成、归一化、关联与选择、变异交叉等核心步骤均有对应实现;另附原始下载链接与license说明,便于回溯对照。资源仅11KB,轻量易用,已有5056人学习。作者坦承并未完全吃透代码,部分难懂处保留空白或问号注释,读者可结合源码和阅读笔记一起排查,也可在讨论区交流印证理解。对想从调用工具转向理解NSGA-III原理的读者来说,这份带问题标记的中文注释版是很好的起点,便于按模块逐段精读。
1. 当单目标优化只能给你一个“正确答案”,你反而不知道怎么选了
我之前独立负责某个调度系统的参数优化时,需求方说“成本要低,时间要短,质量还要过得去”,我一度把三个目标加权成单目标去跑遗传算法,每次只能拿到一个解,换一组权重又得出一个不同的“最优”,汇报时根本讲不清哪个方案更合理。后来我把多目标进化算法引入进来,一次运行直接得到一整组互不支配的候选方案,让需求方在帕累托前沿上挑。NSGA-III正是这类算法里最值得研究的实现之一,而这份Matlab版本的最大特点是它在每个关键函数里都写满了中文注释,把非支配排序、参考点选择、归一化这些原本像黑匣子的环节全部摊开,适合想真正读懂并改成自己工程问题的开发者。无论你是第一次接触多目标进化算法,还是已经在用NSGA-II想升级,这份带中文注释的工程代码都能直接落地。
2. 算法定位与演进:为什么有了NSGA-II还要NSGA-III
2.1 从拥挤距离到参考点:NSGA-II在高维目标下的困境
NSGA-II大家应该不陌生,它的核心思路是快速非支配排序加上拥挤距离比较算子,在2到3个目标上表现非常好。但我第一次把NSGA-II拿去做一个5目标的工业调度优化时,问题立刻暴露出来:拥挤距离在高维目标空间里的分辨能力急剧下降,因为帕累托前沿在高维空间中的分布极度稀疏,基于邻居距离的密度估计几乎失效,种群会过早聚成一团,解集的均匀性和覆盖度都变得很差。这就像几个人在空旷广场上量彼此距离,怎么量都差不多,根本分不出谁周围更拥挤。
NSGA-III要做的事,就是用一组事先均匀分布的参考点来替代拥挤距离,引导搜索朝向理想点(即各目标最优值的组合)附近的多个方向同时推进。它并不直接计算密度来淘汰个体,而是通过“每个参考点周围有几个个体、哪些个体关联到该参考点”来判断当前进化种群是否在某一块区域过密。目标数量变多时,参考点在单位超平面上依然能保持相对均匀,这就是它能够扩展到许多个目标的原因。
2.2 这份Matlab实现里最重要的四个机制
这份代码本身是对NSGA-III标准流程的忠实实现,我拆完之后认为它是由四个核心机制撑起来的:
第一,快速非支配排序。把种群中的个体划分层级,第一层的个体不被任何个体支配,第二层只被第一层支配,以此类推。注意快速非支配排序与遗传算法里适应度排序完全是两件事,前者比较的是多目标向量之间的支配关系,后者通常只看单个数值大小。
第二,参考点生成。在标准NSGA-III里采用Das-Dennis方法在单位单纯形上生成数量均匀分布的参考点,参考点个数一般与种群规模保持接近,或者略小于种群规模。参考点的两组关键参数是目标个数M和每个目标方向上的等分份数H,我用的这份代码里参考点数量通过组合公式直接算出来,这个细节在后面调试时非常重要。
第三,自适应归一化。每次进化迭代时,根据当前种群在目标空间的极值点构造一个超平面,把所有个体的目标值归一到同一个范围内。这一步如果不做,量纲差异大的目标(比如成本和时间的数值完全不在一个数量级)会让参考点的关联计算失效,种群会被量纲大的目标牵着跑。
第四,基于参考点的环境选择。在“锦标赛选择”后产生的后代与父代合并,先照常做非支配排序,再从第一层开始往里填,填到某一层时放不下全部个体,就在这一层里借助参考点选择“走哪几个,丢哪几个”,目标是让每个参考点附近都尽量保留一个个体,得到分布均匀的帕累托前沿。
2.3 面向2目标的定位:这份代码为什么仍然适合从低维入门
标题里提到“2目标”这个关键词,在很多人的印象里NSGA-III是为高维目标设计的,拿它做2目标似乎大材小用。实际读代码你会发现,NSGA-III在2目标下收敛速度快,分布均匀性也好,而且因为参考点数量可控,运行过程比NSGA-II更容易观察和调试。对于刚开始接触多目标进化算法的开发者,从一个2目标的ZDT测试函数入手,把参考点机制和种群进化过程一行一行读懂,之后再扩展到3目标、5目标,会顺畅得多。
还有一个实际原因:很多教材和公开资料里NSGA-II的源码已经泛滥,但NSGA-III的Matlab实现质量参差不齐。这份代码给这个经典算法提供了一个“能跑、能改、注释全”的范本,尤其适合作为课程设计或论文验证代码的基线版本。
3. 核心模块拆解:一个种群从初始化到输出帕累托前沿的完整流程
3.1 项目文件结构与初始化入口
拿到这份Matlab源码,先把目录看清楚。常见的工程结构是几个核心脚本加上一个测试函数文件,目录结构大致如下:
NSGAIII_MATLAB/ ├── NSGAIII.m % 主脚本:参数设置 + 主循环 ├── initialize_pop.m % 种群初始化 ├── non_dominated_sort.m % 快速非支配排序 ├── generate_reference_points.m % 参考点生成 ├── adaptive_normalization.m % 自适应归一化 ├── associate_to_reference.m % 个体-参考点关联 ├── niching_selection.m % 小生境选择(环境选择) ├── crossover_mutation.m % SBX交叉 + 多项式变异 ├── evaluate_objective.m % 目标函数定义(默认ZDT1) └── plot_pareto.m % 结果绘图我一般建议先看NSGAIII.m主脚本,它把算法流程拉成一条线:初始化种群,进入最大代数循环,循环内部依次执行交叉变异、合并父代子代、非支配排序、归一化、关联参考点、环境选择,最后输出best_pareto。
主循环的核心段是这样的:
% 主循环:generation从1到max_gen for gen = 1 : max_gen % 通过锦标赛选择 + 交叉变异生成子代种群 offspring = crossover_mutation(population, params); % 将父代与子代合并,合并后的种群是两倍大小 combined = [population, offspring]; % 对合并种群做快速非支配排序,得到分层结果 [fronts, ~] = non_dominated_sort(combined); % 自适应归一化:计算理想点、极值点、超平面截距 normalized_pop = adaptive_normalization(combined); % 将每个个体关联到距它最近的参考点 [associate_idx, distance] = associate_to_reference(normalized_pop, params.ref_points); % 环境选择:从合并种群中挑出pop_size个个体进入下一代 population = niching_selection(combined, fronts, associate_idx, distance, params); end这个主循环本身没有复杂语法,但每一句背后都是一整个模块。params.ref_points是在主脚本开头用generate_reference_points预生成好的,整个进化过程里参考点固定不变,这是NSGA-III标准做法的关键——参考点只用于引导个体分布,不会随着进化而移动。如果你想让它在特定问题上表现更好,可以考虑以参考点作为固定锚、让种群逼近它们,这是后话,第三章后面几节会逐个模块深入。
参数设置部分通常是这样的:
params.pop_size = 100; % 种群大小,建议与参考点数量接近 params.max_gen = 200; % 最大进化代数 params.num_obj = 2; % 目标个数,这份代码默认配置为2目标 params.num_var = 30; % 决策变量维度,ZDT1默认30维 params.crossover_prob = 1.0; % 交叉概率 params.mutation_prob = 1/30; % 突变概率,通常取1/变量维度 params.ref_H = 99; % 参考点在各目标方向上的分割份数参数里ref_H最容易被忽略,它是Das-Dennis方法里的等分份数H。对2目标问题,参考点数量等于H+1,而对3目标问题,公式变成组合数C(H+M-1, M-1)。也就是说,如果你把目标数改成3,但忘了改H,参考点数量会突然增加很多,种群大小却不变,后面的小生境选择行为就会和2目标时完全不同。这是做多目标进化算法调试时最典型的“参数联动”陷阱之一。
3.2 快速非支配排序:把“支配关系”打成层级
多目标里的“支配”是一个严格偏序关系:解A支配解B,当且仅当A在所有目标上都不比B差,并且至少在一个目标上严格优于B。这是整个算法选择压力的来源,也是困扰新手最多的地方:单目标遗传算法里适应度是一个标量值,多目标里没有单一的“适应度”,取而代之的是一层一层分出来的等级。
这段代码采用经典的两两比较方案:
function [fronts] = non_dominated_sort(pop_obj) % 输入pop_obj: 种群的目标向量矩阵,size为[种群大小, 目标个数] % 输出fronts: cell数组,fronts{1}是第一层个体的索引集合 pop_size = size(pop_obj, 1); S = cell(pop_size, 1); % S{i}存储被个体i支配的个体集合 n = zeros(pop_size, 1); % n(i)记录支配个体i的个体数量 fronts = {}; current_front = []; for i = 1 : pop_size for j = 1 : pop_size if i == j continue; end dom_i = all(pop_obj(i,:) <= pop_obj(j,:)) && any(pop_obj(i,:) < pop_obj(j,:)); dom_j = all(pop_obj(j,:) <= pop_obj(i,:)) && any(pop_obj(j,:) < pop_obj(i,:)); if dom_i S{i} = [S{i}, j]; elseif dom_j n(i) = n(i) + 1; end end if n(i) == 0 current_front = [current_front, i]; end end front_no = 1; fronts{front_no} = current_front; % 逐层剥离:处理当前层支配的个体,更新计数,形成下一层 while ~isempty(current_front) next_front = []; for i = current_front for j = S{i} n(j) = n(j) - 1; if n(j) == 0 next_front = [next_front, j]; end end end front_no = front_no + 1; current_front = next_front; fronts{front_no} = current_front; end end这个实现的时间复杂度是O(M * N^2),其中M是目标个数,N是种群大小。虽然比基于排序的优化版本慢一些,但胜在逻辑直观,对阅读代码的开发者来说,先把“谁支配谁、第几层、每层有哪些个体”这三个问题看清楚比什么都重要。第一层就是当前种群里的非支配个体集合,也就是我们最终要的帕累托前沿候选。
当你在这份代码基础上做扩展时,注意这里处理的是“最小化问题”。如果某个目标需要最大化,常见做法是在计算前对目标值取负号,而不是去改这段排序逻辑。我在拿到一批工程测试函数时,就曾因为一个目标是“越大越好”,直接代入排序代码,导致结果完全反向,这是低级但常见的错误。
3.3 参考点生成与自适应归一化:NSGA-III的“均匀性灵魂”
参考点生成模块用的是Das-Dennis方法,核心思想在2目标下很简单:把连接两个轴点的线段等分H份,取等分点作为参考点。3目标及以上时,扩展为单纯形上的网格点。
function [ref_points] = generate_reference_points(M, H) % M: 目标个数 % H: 每个目标方向的等分份数 % 输出ref_points: 参考点矩阵,行数为组合数C(H+M-1, M-1) ref_points = []; if M == 2 for i = 0 : H ref_points = [ref_points; i/H, 1 - i/H]; end else % 递归生成单纯形上的均匀网格,见Das-Dennis(1998) ref_points = recursive_generate(M, H, []); end end参考点有个隐含属性容易被忽略:它在目标空间里不需要归一化,因为初始参考点的每个坐标分量加在一起等于1,相当于落在单位单纯形上。后续归一化操作就是把种群的目标值也映射到同样尺度的空间里去比较。如果把参考点直接生成在原始目标空间会产生问题,因为每个目标的数值范围不同,参考点与个体的距离计算会被大范围目标主导。
归一化模块要处理真正的数值坑。标准流程是先算出理想点,也就是每一维目标的最小值,将目标值减去理想点得到偏移后的坐标。然后找出极值点,构造一个仿射超平面,计算截距,最后把每个个体目标值除以截距:
function [norm_pop] = adaptive_normalization(pop_obj, ideal_point) % 将每个个体的目标值映射到[0,1]区间附近的单位超平面 % 输入pop_obj: 原始目标值矩阵 % 输入ideal_point: 每个目标维度上的历史最小值集合 % 输出norm_pop: 归一化后的目标值矩阵 % 1. 减去理想点,保证所有维度的最优值平移为0 shifted = pop_obj - repmat(ideal_point, size(pop_obj,1), 1); % 2. 计算每维目标的最大值,用于辅助截距求解 max_values = max(shifted, [], 1); % 3. 找到极值点:对每个维度i,找出使该维目标最小化同时其他维受约束的个体 extreme_points = zeros(size(pop_obj,2), size(pop_obj,2)); for i = 1 : size(pop_obj,2) % 用ASF(增广标量函数)找权值方向的极值个体 [~, idx] = min(sum(shifted ./ max_values, 2) - shifted(:,i) ./ max_values(i)); extreme_points(i,:) = shifted(idx,:); end % 4. 解线性方程组得到截距,并做归一化 intercepts = extreme_points \ ones(size(pop_obj,2), 1); intercepts = 1 ./ intercepts; % 5. 防止除零:截距过小时加一个极小量 intercepts(intercepts < 1e-6) = 1e-6; norm_pop = shifted ./ repmat(intercepts, size(shifted,1), 1); end这里最容易翻车的有两处。第一处,如果某个目标维度上所有个体取值完全相同,max_values有可能为0,导致后面shifted ./ max_values出现NaN,需要用max(..., eps)保护,这是从优化测试函数切换到工程函数时极易触发的问题。第二处,求解截距的方程组可能奇异,也就是多个极值点落在同一平面内,常见做法是捕获矩阵求逆的报错,把截距全部置为1,直接退回“只做平移”的状态。
3.4 关联与小生境选择:决定“最后谁能进下一代”
个体与参考点关联的过程分两步:先计算每个个体到所有参考点的欧氏距离,找到最近的一个作为其关联参考点,然后统计每个参考点关联了多少个体。小生境选择时,优先挑选关联个体数少的参考点,确保这些区域至少被覆盖到一个解。这个机制非常像是“一个萝卜一个坑”:先保证每个坑里都有一个萝卜,坑不够用了再择优填第二个。
function [selected] = niching_selection(combined, fronts, assoc_idx, dist, params) % 从合并种群中选出pop_size个个体 % fronts: 非支配分层结果 % assoc_idx与dist: 每个个体关联到的参考点编号和距离 selected = []; remain = params.pop_size; % 按层依次添加个体,直到一层无法完全容纳 for i = 1 : length(fronts) if numel(fronts{i}) < remain selected = [selected, fronts{i}]; remain = remain - numel(fronts{i}); else % 进入到临界层,使用小生境策略 selected = [selected, niche_select(fronts{i}, remain, assoc_idx, dist)]; break; end end endniche_select里的逻辑比这个片段复杂一些,核心是统计每个参考点当前的关联计数,然后迭代执行:从关联量最小的参考点集合里挑距离最近的个体进入下一代,选中后该参考点的计数加1。重复此过程直到选满为止。这意味着处于临界层的个体能否保留,取决于“它所在的参考点是不是冷门区域”。如果一个参考点附近已经有五六个个体了,第六个即使这一层里排名靠前,也会被淘汰掉,以维持解集在空间里的均匀。
这个“宁可舍弃局部更优解也要保分布均匀”的设定,让NSGA-III在真实工业问题里很有用。我当时做调度方案时,最需要的不是一两个尖峰解,而是能覆盖多种成本与时间组合的方案,给小决策者选,分布均匀比单点最优重要得多。
4. 运行与改造:把ZDT测试函数换成你自己的工程问题
4.1 跑通基础示例:从2目标ZDT1开始
下载这份Matlab代码之后,最稳的路线是先原封不动跑一遍。打开NSGAIII.m,确认evaluate_objective.m里默认是ZDT1测试函数,在Matlab命令窗口直接运行:
NSGAIII运行结束后会弹出一个二维散点图,横纵轴分别是两个目标函数值,若干分散的点形成一条向原点凸的稀疏曲线,那就是ZDT1理论帕累托前沿的一个近似。如果你的机器配置一般,也可以先把params.pop_size降到50,params.max_gen降到100,先验证整个流程能跑通,再逐步恢复参数。
关键参数表如下:
| 参数 | 默认值 | 调整建议 |
|---|---|---|
| pop_size | 100 | 与参考点数量相当为宜,不要远大于参考点数 |
| max_gen | 200 | 工程场景建议300以上观察收敛性 |
| num_obj | 2 | 改成3时,优先调整ref_H,参考点数量会剧增 |
| num_var | 30 | ZDT1为30;自定义问题按决策变量数量设置 |
| ref_H | 99 | 2目标时参考点数=100,3目标时参考点数会变成5000多,务必同步调整pop_size |
跑通之后,有两个观察点别跳过:打开工作区的population变量,看最终种群的目标值是不是都集中在帕累托前沿附近;再打开fronts{1},数一数第一层个体数量是不是接近pop_size。这两个观察能帮助你快速判断种群是否收敛,也能发现参考点参数是否设置合理。
4.2 替换目标函数:从ZDT到自定义调度问题
修改方式是所有二次开发的基础:打开evaluate_objective.m,这个函数接收决策变量向量x,返回一个目标向量f。改成你的工程问题时,注意保持目标统一为最小化形式。比如我之前做的一个制造车间调度简化版,定义了两个目标:总完工时间和总延迟惩罚,那么对应函数可以写成:
function [f] = evaluate_objective(x) % x: 决策变量向量,例如两道工序的时间参数 % f: 目标向量,默认全部是最小化目标 proc_time = x(1) * 20 + 10; % 由决策变量映射出实际加工时间 delay_cost = x(2) * 8 + 2; % 延迟惩罚系数 % 目标1:完工时间,越小越好 f1 = proc_time; % 目标2:延迟惩罚,越小越好 f2 = delay_cost; f = [f1, f2]; end这里有个重要细节:决策变量的取值范围由params.lower_bound和params.upper_bound控制,也就是在NSGAIII.m里设置。修改自定义问题时,务必同步调整边界,避免决策变量在初始化阶段就越界。还有一点是我自己踩过的:evaluate_objective函数的输入x是行向量,如果某些Matlab函数返回列向量,目标向量的维度会无缘无故变成[1, 2]和[2, 1]两种不同形态,后续矩阵运算报错时你会摸不着头脑,所以第一行可以统一加一句强制转换。
另外,如果你的工程问题里混合了“越大越好”的目标,比如利润、吞吐量,不要直接改排序模块,而是在evaluate_objective里对该目标取负值,然后在输出结果解读时把符号转回来。这种做法最省事,也不容易破坏非支配排序的通用性。
4.3 观察结果:从目标空间看进化过程
跑完算法后,除了plot_pareto.m之外,我建议手动写一段代码跟踪每一代的最优前沿变化:
% 记录每代第一层非支配个体的目标值,画动态轨迹 best_front_each_gen = zeros(max_gen, num_obj); for gen = 1 : max_gen fronts = non_dominated_sort(population_obj{gen}); first_front = population_obj{gen}(fronts{1}, :); % 取第一层中离原点最近的点作为代表 best_front_each_gen(gen, :) = min(first_front, [], 1); end plot(best_front_each_gen(:,1), best_front_each_gen(:,2), '-o'); xlabel('目标1: 完工时间'); ylabel('目标2: 延迟惩罚'); title('每代最优解收敛轨迹');看这张图的趋势能了解算法收敛速度:前几十代目标值下降很快,中后期进入精细搜索阶段,曲线趋于平缓。如果曲线走到后期还在剧烈波动,说明种群多样性保持得太强,或者参考点分布和你实际前沿形状差异过大,可以适当调大交叉概率或参考点数量再看一轮。调参本身不是玄学,就是要在“多样性”和“收敛性”之间找平衡。
5. 避坑指南:NSGA-III在Matlab里跑偏的六种典型现场
5.1 参考点数量暴涨,程序突然变慢
现象:把目标数从2改成3,运行时间从几十秒变成几分钟甚至更久,且内存占用异常。
原因:ref_H没有随目标数调整。ref_H=99时,2目标的参考点数是100个,3目标直接变成C(99+3-1, 3-1) = 5050个。种群大小为100时,每个个体要做5000多次距离计算,关联阶段的计算量直接爆炸。
解决:3目标问题时建议设置ref_H为12到15之间,对应参考点数量在100以内(ref_H=12时参考点数约91个,ref_H=15时约136个)。一个合理的参考点数量,通常在种群大小的0.8到1.2倍之间。对更多目标数,还可以改用两层级联参考点生成来避免组合爆炸。
5.2 归一化过程报错或出现NaN
现象:运行到adaptive_normalization时报矩阵奇异错误,或最终绘图里出现NaN目标值。
原因:工程函数替换ZDT之后,目标值的数值范围变化很大,某些极端情况下所有个体在某一个目标维度上取值完全相同,导致归一化里的max_values为0,或者极值点方程解不出唯一截距。
解决:推荐方案是在adaptive_normalization.m里给max_values加一个下限保护,用max(max_values, eps)。同时用try...catch捕获求解截距的奇异情况,一旦发现矩阵接近奇异,就把截距全部设为1,让算法退化为“只平移不伸缩”的模式。这笔防御代码成本很低,但能避免大量排查时间。
5.3 第一层个体比种群还大
现象:运行结束后统计fronts{1}的个体数,发现比pop_size还大,或者接近全是第一层。
原因:目标数很少但问题简单时,大量个体互为非支配关系,层级结构扁平化,第一层几乎容纳全部个体,导致环境选择里“临界层选谁”的压力几乎全部落到小生境策略上,算法退化为随机挑。
解决:把目标数量增加到2到3个以上,让支配关系更有区分度;或者引入离散决策变量,增加问题的约束特征。如果问题本身实在简单,建议换成更复杂的测试函数验证算法实现是否正确,再回到目标问题上调试。
5.4 替换目标函数后,决策变量维度对不上
现象:evaluate_objective报错索引超出范围,或者结果明显不收敛。
原因:params.num_var仍保持ZDT1的30维,但自定义函数里只读取了x(1)和x(2),得到的结果不会报错,只是一大半决策变量完全没参与目标计算,算法实际上在做无效搜索。反过来,如果自定义函数里读取了x(5)但num_var设置成3,则会直接索引越界。
解决:每次替换目标函数后,强制检查三个数字是否一致:params.num_var、初始化函数里的随机数维度、目标函数内部读取的最大x(i)下标。这个检查我后来形成了习惯,每次改完测试函数先跑一轮弹出结果才放心。
5.5 参考点固定但前沿形状不规则,结果分布不均匀
现象:最终帕累托前沿在某个区域密集,另一个区域几乎空白。改了pop_size也没改善。
原因:NSGA-III的参考点是固定的均匀分布,它假设帕累托前沿在目标空间里足够规则。如果你的工程问题前沿形状严重不规则,比如存在非凸区域或断崖式边界,均匀参考点无法贴合前沿形状。
解决:很自然的做法是改成“自适应参考点”,每若干代根据当前前沿形状重新生成参考点;或者在关联阶段对参考点做局部微调。我一般先验证问题本身的前沿形状,把第一层个体目标值直接绘制成散点图,看看是否连续规则,再决定是否要修改参考点策略。对于大多数调度、路径规划类问题,固定参考点已足够。
5.6 Matlab脚本相互调用时函数名冲突
现象:报错“函数名重复”或调用了错误版本的同名函数。
原因:Matlab会优先调用当前工作目录下的函数。如果你把这份代码目录加入路径后,又放了另一个版本的non_dominated_sort.m在同级目录,调用时会随机命中其中之一,排查起来很难发现。
解决:把所有代码文件放在同一个独立目录下,用cd切换到这个目录再运行NSGAIII;不要使用addpath把多个包含同名函数的目录混入路径。这是我吃过一次亏的教训:有一次调试结果莫名其妙,半天才发现是路径里有两个不同的排序函数,改完顺序后一切恢复正常。
6. 二次开发技巧:把这份源码改造成自己的“多目标求解器”
拿到一份注释齐全的源码,最大的价值不是跑通示例,而是改成自己的工具。我会从三个层面去做这件事。
第一步,把测试函数和算法框架解耦。目前evaluate_objective.m是全局函数,建议改成回调函数模式:在主脚本里定义目标函数句柄,传入各模块。改造之后,参数集和目标函数都通过一个配置结构体传入,以后切换问题只需要改一行配置,而不需要去碰算法核心。这种松耦合的设计对后续维护和复现实验都非常关键。
第二步,加一个“多代前沿指标”的统计模块。单纯看散点图只能靠感觉判断收敛性。建议在每一代记录三项指标:第一层个体数量、前沿点到理想点的平均距离、以及相邻解在目标空间的间距方差。前两项反映收敛性,第三项反映均匀性。有了这三个指标,你在调参时就不再是“看运气”,而是能判断某个参数改动的方向。
第三步,做一个与NSGA-II的对比验证。保留这一份NSGA-III的同时,找一套NSGA-II的Matlab实现,在同一组测试函数上、同样的种群规模和代数下分别运行多次,比较前沿的覆盖范围和均匀性。在多目标领域里,算法好坏本质是经验比较,多跑几个测试函数比在理论上空谈有意义得多。我在某实验室做课程项目时,就是用这种对比方式给某跨平台系统选了合适的多目标优化方案。
从那以后,我拿到任何进化算法源码,都会先强制自己走一遍“跑通示例、替换问题、记录指标、对比基线”四步流程,确认四个步骤都完成后,才敢把结果拿到评审会上。这个过程帮我避开了很多“算法看似正常但其实在瞎搜”的尴尬情况。希望这份带中文注释的NSGA-III实现也能帮你少踩一些坑,把精力花在解决自己实际问题的那一步上。
本文还有配套的精品资源,点击获取