简介:第三代非支配排序遗传算法(NSGA-III)的MATLAB完整实现代码,源自开源项目Yarpiz,主要面向需要处理高维、多目标复杂优化问题的科研人员、工程技术人员及相关专业学生。该算法借助非支配排序、拥挤距离与参考点机制来平衡收敛性和种群多样性,尤其适合存在多个相互冲突目标的工程决策场景。压缩包共19个文件,包含14个m格式源码文件(完整算法逻辑)、3个txt说明文档(使用说明与版权信息)以及2个原始项目链接,整体大小仅15KB,代码极为凝练。目前该资源已有2399人学习下载。实现代码完整覆盖主程序迭代流程、非支配排序、拥挤距离计算、参考点生成、交叉与变异等核心环节,每个函数均配有细致注释,可清晰理解NSGA-III的分层选择、精英保留及适应度分配过程。基于现有框架,只需替换目标函数接口或调整种群规模、迭代次数等参数,便能快速迁移到自己的多目标优化课题中,既适合作为算法教学演示,也可作为进一步扩展开发其他进化算法的轻量基础模板。
1. 多目标优化与遗传算法:NSGA-III 能在高维目标空间里做什么
当目标数量超过三个的时候,经典遗传算法NSGA-II里的拥挤度距离就会告诉你什么叫歇菜——高维解空间中几乎所有个体都被判定为不拥挤,算法直接退化成随机搜索。这是我在做某工程调度优化时踩过的大坑。多目标优化这件事,两个目标可以画Pareto前沿,三个目标还能看三维图,到了四个五个目标,需要的是让算法在目标空间里均匀撒点保持多样性。NSGA-III就是为了解决这个问题出现的,它用一组预定义的参考点替代拥挤度距离做多样性维持,让解集在高维目标空间里依然分布均匀。这份MATLAB工程里,参考点生成、非支配排序、归一化、关联选择、遗传算子全部带注释,能直接跑通并迁移到你的业务优化问题上。
2. 项目架构与模块设计:从主循环到函数拆分的完整链路
2.1 NSGA-III 与 NSGA-II 的本质区别:拥挤度退场,参考点登场
NSGA-III 不是全新的算法,它沿用了遗传算法的主框架:初始化种群、交叉变异产生子代、合并父子种群、非支配排序、环境选择。和 NSGA-II 真正的分水岭在最后一步环境选择。
NSGA-II 用拥挤度距离来保证解集多样性,做法是把同一个非支配层里的个体按目标函数值排序,计算每个个体两侧的矩形周长。这个机制在 2 目标、3 目标时非常好用,但目标数量一涨,每个个体的邻近个体变少,拥挤度值迅速趋同,选择压力被稀释,种群漂移严重,解集没法均匀覆盖整个前沿。
NSGA-III 的思路是彻底换掉多样性度量:在运行前预先在目标空间生成一组均匀分布的参考点,环境选择时把每个个体关联到离它最近的参考点上,然后按"每个参考点附近应该保留多少个个体"来做名额分配。这样解集的分布性是由参考点网眼决定的,而不是由解之间的相对距离决定的,目标数量增加时,只需要加密参考点网格,算法行为保持稳定。这也是为什么 NSGA-III 被称为"基于参考点的非支配遗传算法"。
2.2 MATLAB 目录与函数职责
这份代码的文件组织非常清爽,每个.m文件只干一件事,属于典型的模块化单文件设计。核心结构如下:
NSGA3_Code/ ├── main.m # 主入口:参数定义、种群初始化、迭代主循环 ├── generate_reference_points.m # 参考点生成:Das-Dennis 组合数枚举 ├── evaluate_objective.m # 目标函数:默认 DTLZ2 三目标测试问题 ├── fast_non_dominated_sort.m # 快速非支配排序:返回层级与前沿集合 ├── adaptive_normalization.m # 自适应归一化:极值点、超平面、截距 ├── associate.m # 关联参考点:垂直距离计算与最小距离匹配 ├── environmental_selection.m # 环境选择:小生境计数与精英保留 ├── sbx_crossover.m # 模拟二进制交叉 SBX ├── polynomial_mutation.m # 多项式变异 └── genetic_operators.m # 调用交叉与变异,生成完整子代种群每个函数的输入输出都是标准矩阵:种群统一用 N×n_var 矩阵表示,目标值统一用 N×M 矩阵表示,参考点统一用 R×M 矩阵表示。模块之间没有全局变量传递,所有数据通过函数参数显式流动。这种设计的好处是你可以单独抽出任意一个函数做单元测试,比如只跑generate_reference_points.m观察参考点分布,再决定要不要调整划分密度。
2.3 主程序 main.m:参数初始化与迭代主循环
主循环的代码结构决定了整个算法的运行节奏,先把骨架帖出来,后面每一章再深入拆里面的模块。
%% 参数设置 M = 3; % 目标函数个数 p = 12; % 参考点每个维度的划分数 R = nchoosek(M + p - 1, p); % 参考点数量,Das-Dennis 组合数 N = R; % 种群大小,一般取参考点数或整数倍 max_gen = 250; % 最大迭代代数 n_var = 30; % 决策变量个数 lb = zeros(1, n_var); % 决策变量下界 ub = ones(1, n_var); % 决策变量上界 %% 初始化 population = lb + rand(N, n_var) .* (ub - lb); ref_points = generate_reference_points(M, p); %% 迭代主循环 for gen = 1:max_gen % 生成子代:SBX 交叉 + 多项式变异 offspring = genetic_operators(population, lb, ub); % 合并父子两代种群 combined = [population; offspring]; % 计算全部个体的目标函数值 obj_vals = evaluate_objective(combined); % 快速非支配排序,得到 rank 向量 [rank, ~] = fast_non_dominated_sort(obj_vals); % 自适应归一化:更新理想点,求截距,得到 f_norm z_min = min(obj_vals, [], 1); [f_norm, ~, ~] = adaptive_normalization(obj_vals, z_min); % 每个个体关联到最近的参考点 [match_idx, match_dist] = associate(f_norm, ref_points); % 环境选择:保留 N 个精英个体进入下一代 selected_idx = environmental_selection(combined, rank, match_idx, match_dist, ref_points, N); population = combined(selected_idx, :); % 打印当前最优前沿信息 if mod(gen, 50) == 0 fprintf('gen=%d \\n', gen); end end这段代码有几个参数值得细说。p是参考点划分份数,直接决定参考点数量 R,当 M=3、p=12 时 R=91,种群一般就设成 91 或者 182 这样的整数倍。lb、ub是决策变量的上下界,必须与你要优化的实际问题匹配。主循环里的关键步骤顺序是固定的:先合并种群,再做非支配排序,然后归一化,最后关联选择。前两步搞清"个体优不优",后两步解决"多样性够不够",缺一不可。
3. 核心代码拆解:参考点生成与自适应归一化
3.1 Das-Dennis 参考点生成:组合数学的递归实现
参考点是 NSGA-III 的基石,它决定了算法期望解集在目标空间里的分布形态。Das-Dennis 方法生成的是单纯形上的均匀网格点:要求每个参考点坐标分量之和等于 1,且每个分量是 p 等分中的整数倍。参考点数量由组合数C(M+p-1, p)决定,比如 M=3、p=12 时就是 C(14,12)=91 个点。
function ref = generate_reference_points(M, p) % M : 目标函数个数,一般 3~15 % p : 每个维度上的划分份数,正整数 % ref: 大小为 C(M+p-1, p) × M 的参考点矩阵,每行和为 1 if M == 1 % 单目标退化:参考点就是 1 ref = 1; return; end container = []; % 递归枚举所有满足 x1 + x2 + ... + xM = p 的非负整数组合 for i = 0:p sub = generate_reference_points(M - 1, p - i); for j = 1:size(sub, 1) container = [container; i, sub(j, :)]; end end % 除以 p,让每一行分量和为 1 ref = container / p; end这段递归的实现思路是固定第一个维度的取值 i,然后把剩余 p-i 的划分交给低一维的同样问题递归处理。当 M=2 时递归到基准条件,返回p(即[p]除以 p 后变成 1),回溯拼接后得到所有整数分区。最终除以 p 后,每一行的分量都在 0 到 1 之间且总和为 1。需要说明的是:参考点本身不是解,只是目标空间里的虚拟锚点,它不参与进化,只是在环境选择时用来标定"哪个区域该保人"。
这个函数在参数选择上的一个常见疑问是 p 取多少。p 越大参考点越密,解集分布越细,但计算量也会按组合数增长。工程上我一般这样定:M=3 时 p=12 或 13 得到 91 或 105 个参考点,M=5 时 p=6 得到 210 个点,M=8 时 p=3 得到 120 个点。这个经验值能平衡覆盖密度和计算代价。
3.2 理想点与极值点:归一化前的两个关键输入
归一化是整个 NSGA-III 中最容易翻车的一步。先说为什么要归一化:多目标问题里不同目标的量纲往往不一致,比如成本是元、时间是小时、风险是百分比,直接用原始值算距离,量级大的目标会完全压过量级小的目标。归一化的目的是把每个目标都变换到接近 0 到 1 的范围内,让关联参考点的计算不偏向任何一维。
归一化的第一步是平移,用每个目标的历史最小值构成理想点 z_min,从所有个体的目标值里减掉它,让理想点落到原点。第二步是找极值点,即每个目标轴方向上最突出的个体。这一步用的是加权切比雪夫函数 ASF,它的核心思想是:给目标 i 一个很大的权重 1,其它目标给一个极小的权重(比如 1e-6),然后对每个个体计算 max(f_j / w_j),使这个值最小的个体就是沿目标 i 方向最极端的解。
function [f_norm, z_min, intercepts] = adaptive_normalization(f, z_min) % f : 种群目标值矩阵 N × M % z_min : 理想点,各维目标的历史最小值 % f_norm : 归一化后的目标值 % intercepts: 超平面在每个目标轴上的截距 N = size(f, 1); M = size(f, 2); % 1. 平移:让理想点落到原点 f_shifted = f - repmat(z_min, N, 1); % 2. 用加权切比雪夫函数找每个目标轴的极值点 extreme = zeros(M, M); for i = 1:M w = 1e-6 * ones(1, M); w(i) = 1; % 计算 ASF 值,取最小对应的个体作为极值点 asf = max(f_shifted ./ repmat(w, N, 1), [], 2); [~, idx] = min(asf); extreme(i, :) = f_shifted(idx, :); end % 3. 构造超平面 extreme * a = 1,解出截距 if rcond(extreme) < 1e-10 % 矩阵接近奇异:改用伪逆,避免数值爆炸 a = pinv(extreme) * ones(M, 1); else a = extreme \ ones(M, 1); end intercepts = 1 ./ a; % 4. 基于截距做归一化 f_norm = f_shifted ./ repmat(intercepts, N, 1); end代码里f_shifted ./ repmat(w, N, 1)中的w是行向量,MATLAB 隐式扩展会用每个个体的目标值逐项除以权重向量。因为 w(i)=1 而其它维度权重极小,ASF 值主要由目标 i 分量主导,所以min(asf)找出来的个体就是该目标轴方向上最极端的那个解。rcond是条件数评估函数,值太小表示矩阵接近奇异,此时普通除法会得到不可靠结果,改用pinv是稳妥做法。实际应用中极值点矩阵奇异很常见,比如两个目标的极值点是同一个个体,几乎每次跑都会遇到,所以这个判断不是防御性编程,是必需品。
3.3 超平面截距求解:从线性方程到数值稳定性
第三步的数学含义值得单独说清楚。拿到 M 个极值点后,要构造一个穿过这些极值点的超平面,然后求出这个超平面与每个目标轴的交点,也就是截距。超平面方程可以写成extreme * a = 1,其中extreme是 M×M 矩阵(每行是一个极值点的坐标),a是待求系数向量,截距就是1./a。
实际运行中这个方程组经常会病态。常见情况是某些极值点坐标非常接近,导致矩阵行向量近似线性相关,rcond很小,解出来的 a 有的分量接近零甚至为负。一旦截距为负,归一化后该维目标值全部变负,关联参考点算出来的距离全是错的,迭代下去整个种群就废了。所以我在代码里做了两道防线:第一道是用rcond判断是否改用伪逆;第二道是后续在环境选择时对截距做合理性检查。经验来看,如果目标函数本身存在强相关性,或者种群早期极值点还没站稳,这一步最容易出 NaN。
4. 核心代码拆解:非支配排序与精英选择机制
4.1 快速非支配排序:两两比较的支配链
非支配排序和 NSGA-II 完全一致,逻辑是:如果一个个体 p 的所有目标值都不劣于另一个个体 q,且至少一个目标严格优于 q,那么 p 支配 q。把所有不被任何人支配的个体划为第一层,去掉它们之后再找出新的不被支配个体划为第二层,依此类推。层级越小,个体越优秀。
function [rank, fronts] = fast_non_dominated_sort(obj_vals) % obj_vals : 种群目标值矩阵 N×M % rank : 每个个体的非支配层级,1 为最优层 % fronts : 元胞数组,每个元素是该层个体索引向量 N = size(obj_vals, 1); dominated_set = cell(N, 1); % p 所支配的个体集合 n_dominated = zeros(N, 1); % 支配 p 的个体数量 for p = 1:N for q = 1:N if p == q, continue; end if all(obj_vals(p, :) <= obj_vals(q, :)) && any(obj_vals(p, :) < obj_vals(q, :)) dominated_set{p}(end + 1) = q; elseif all(obj_vals(q, :) <= obj_vals(p, :)) && any(obj_vals(q, :) < obj_vals(p, :)) n_dominated(p) = n_dominated(p) + 1; end end end % 第一层:不被任何个体支配的个体 fronts = {}; current_front = find(n_dominated == 0); layer = 1; % 循环:去掉当前层后,下一层的 n_dominated 减到 0 即为新层 while ~isempty(current_front) fronts{layer} = current_front; rank(current_front) = layer; next_front = []; for i = current_front for j = dominated_set{i} n_dominated(j) = n_dominated(j) - 1; if n_dominated(j) == 0 next_front(end + 1) = j; end end end layer = layer + 1; current_front = next_front; end end这段代码的时间复杂度是 O(M·N²),M 是目标数,N 是种群大小。两层循环做两两比较,之后每个个体的被支配计数逐层递减。当种群规模是 200、目标数是 3 时,单次排序要做 4 万次比较,在 MATLAB 里完全可接受。但如果目标数上到 10 且种群上千,这个排序会成为瓶颈,优化方向是把内层比较向量化,用矩阵运算替代 for 循环。
需要留意的是all(obj_vals(p,:) <= obj_vals(q,:)) && any(...)的组合条件不能写错。只写all不判断any会把两个完全相同的个体也判成互相支配,导致排序结果错乱。这是个非常容易忽略的细节,我在写第一版时就踩过,种群里重复个体一多,所有层都乱套了。
4.2 关联参考点:垂直距离计算
归一化完成后,每个个体在目标空间里是一个点,每个参考点是从原点出发的一条射线。关联操作要回答的问题是这个个体最靠近哪条射线。距离度量用的是点到射线的垂直距离,不是点到点的欧氏距离。差别在于:参考点是射线方向,不是固定坐标点,如果算欧氏距离,所有个体都会涌向靠近原点的参考点,分布性会彻底失效。
function [match_idx, match_dist] = associate(f_norm, ref) % f_norm : 归一化目标值矩阵 N×M % ref : 参考点矩阵 R×M % match_idx : 每个个体关联的参考点编号(1~R) % match_dist : 个体到参考点射线的垂直距离 N = size(f_norm, 1); R = size(ref, 1); match_idx = zeros(N, 1); match_dist = zeros(N, 1); for i = 1:N f_i = f_norm(i, :); best_d = inf; best_r = 0; for j = 1:R w = ref(j, :); w2 = dot(w, w); if w2 < 1e-12 % 参考点为原点时,垂直距离就是个体到原点的模长 d = norm(f_i); else % 垂直距离公式: |f - dot(f,w)/dot(w,w) * w| proj = dot(f_i, w) / w2 * w; d = norm(f_i - proj); end if d < best_d best_d = d; best_r = j; end end match_idx(i) = best_r; match_dist(i) = best_d; end endproj = dot(f_i, w) / w2 * w是向量投影的标准公式,把个体坐标向量投影到参考点方向向量上,再用原始向量减去投影得到垂直分量,它的模长就是垂直距离。当参考点落在原点(即参考点向量模长为零)时,投影公式分母为零,需要兜底处理。实际运行中这种极端情况很少发生,但如果划分份数 p=0 或者目标数为 1 时就会触发。我习惯在计算前先用norm(w)判断一下,防止除零警告刷屏。
这个嵌套循环在种群大、参考点多的时候是性能热点。优化手段通常是先用矩阵批量算出所有个体到所有参考点方向的投影参数,再一次性求距离矩阵,可以把耗时降一到两个数量级。代码里保留双循环是为了可读性,性能敏感时值得重构。
4.3 小生境计数与精英选择:最后一道筛选
环境选择的目标是从合并种群(大小为 2N)中留下 N 个精英。策略是逐层纳入非支配层,如果加入某一层后总数正好等于 N,直接结束;如果会超过 N,就需要对这一层的个体做小生境筛选。小生境的核心思想是:先统计已选个体在各个参考点附近的密度,然后优先从密度最小的参考点附近挑人。
function selected = environmental_selection(combined, rank, match_idx, match_dist, ref, N) % combined : 合并后的种群个体(或索引) % rank : 每个个体的非支配层级 % match_idx : 每个个体关联的参考点编号 % match_dist : 每个个体到参考点的垂直距离 % ref : 参考点矩阵 % N : 需要保留的种群大小 selected = []; layer = 1; % 逐层纳入个体,直到下一层会超过 N while length(selected) + length(find(rank == layer)) <= N selected = [selected, find(rank == layer)]; layer = layer + 1; end % 最后一层:需要从中挑选 K 个 K = N - length(selected); last_layer = find(rank == layer); % 统计已选个体在各参考点上的小生境计数 R = size(ref, 1); rho = zeros(1, R); for i = 1:length(selected) rho(match_idx(selected(i))) = rho(match_idx(selected(i))) + 1; end chosen = []; while length(chosen) < K % 找小生境计数最小的参考点(取第一个) [min_rho, j] = min(rho); % 该参考点关联的、还在最后一层中的候选个体 linked = last_layer(match_idx(last_layer) == j); if isempty(linked) % 没有候选个体,把该参考点的计数设为无穷大,跳过 rho(j) = inf; continue; end % 从候选个体中选垂直距离最小的 [~, idx] = min(match_dist(linked)); chosen(end + 1) = linked(idx); % 移除已选个体,更新小生境计数 last_layer(last_layer == linked(idx)) = []; rho(j) = rho(j) + 1; end selected = [selected, chosen]; end这个选择逻辑体现了 NSGA-III 的核心策略:它不按个体的绝对适应度来抢名额,而是优先保证每个参考点附近都有人。rho记录的是已选个体在各参考点的分布密度,min(rho)选出的参考点意味着该区域的人还很少,那么从这一层里挑一个离它最近的个体补进去,就有助于让解集均匀铺满前沿。
实现细节上有个容易出问题的点:当某参考点没有候选个体时,如果直接continue,会陷入死循环,因为 rho 最小值一直不变。把该参考点的 rho 设为inf是常用的处理技巧,相当于把这块区域从排队中移除。此外,last_layer(last_layer == linked(idx)) = []删掉的是匹配索引的第一个匹配项,如果候选集合里正好有两个相同索引的个体,这种方式是安全的,因为个体索引本身是唯一的。
5. 跑通与避坑:参数调优和常见问题排查
5.1 换掉默认测试函数:把DTLZ2改成你自己的问题
默认的evaluate_objective.m实现的是 DTLZ2 三目标测试函数,这段代码的价值在于它示范了多目标测试函数的写法。换成实际问题时,只需要改这一个文件。首先定义决策变量维度和目标个数,然后写清从决策变量到目标值的计算过程。如果目标是成本和时间这种最小化指标,直接返回函数值即可;如果是最大化收益,取负值或在外部做转换。
以 DTLZ2 为例,三目标版本的实现是每个个体都要算一个距离函数 g,然后根据角度变量投影到三个目标轴上:
function f = evaluate_objective(x) % x : 决策变量矩阵,每一行是一个个体 % f : 目标值矩阵,每一行对应一个个体的 M 个目标 M = 3; % 目标个数 n = size(x, 1); f = zeros(n, M); for i = 1:n % 距离函数 g:对下标 >= M 的决策变量求平方和 g = sum((x(i, M:end) - 0.5).^2); % 角度投影 f(i, 1) = (1 + g) * cos(0.5 * pi * x(i, 1)) * cos(0.5 * pi * x(i, 2)); f(i, 2) = (1 + g) * cos(0.5 * pi * x(i, 1)) * sin(0.5 * pi * x(i, 2)); f(i, 3) = (1 + g) * sin(0.5 * pi * x(i, 1)); end end替换成实际问题时,g的部分换成你的约束处理和损失计算,x(i, M:end)这种取子集的操作换成你的变量映射。需要特别注意的是决策变量的范围:如果实际问题里变量下限是 -5、上限是 5,必须同时改main.m里的lb和ub,否则种群初始化和遗传操作的边界裁剪会不一致,子代很容易跑到不可行区域。
5.2 参数匹配:目标数、参考点数、种群规模的关系
NSGA-III 的参数不是各调各的,它们之间有强约束关系。参考点数量由目标数 M 和划分数 p 通过组合数决定,种群大小一般直接取参考点数或它的整数倍。如果种群大小和参考点数差距太大,会出现两个问题:种群比参考点少很多时,有些参考点附近永远没人,解集覆盖不全;种群比参考点多数倍时,计算量浪费在过多的个体上,收敛速度反而变慢。
| 目标数 M | 划分数 p | 参考点数量 R | 推荐种群大小 N |
|---|---|---|---|
| 3 | 12 | 91 | 91 或 182 |
| 3 | 30 | 496 | 496 |
| 4 | 10 | 286 | 286 或 572 |
| 5 | 6 | 210 | 210 |
| 8 | 3 | 120 | 120 或 240 |
max_gen的设定主要看问题复杂度。测试函数通常 250 到 500 代收敛,工程问题建议先跑 500 代观察 Pareto 前沿是否还在移动,如果最后几十代的解集变化很小,说明可以提前停止;如果变化明显,继续加代数。迭代代数的判断没有玄学可言,多跑几次对比 HV 指标或前沿分布就能找到合适的值。
5.3 避坑记录:5个真实踩过的坑
第一个坑:参考点数量与种群大小不匹配时,环境选择死循环。现象是程序跑到选择阶段卡住不动,CPU 占用 100%,几十分钟不退出。原因是某参考点始终没有候选个体,rho(j)一直是最小值,代码不停重试同一个空集合。解决办法是给空的参考点设inf,同时限制最大重试次数,两种手段我都加上才彻底稳定。
第二个坑:归一化截距解出 NaN。现象是种群进化到某一代后目标值全是 NaN,后续所有距离计算失效。原因是极值点矩阵接近奇异,用普通除法得到了不可靠结果。我的解决方法是先判断rcond(extreme),小于阈值改用伪逆,再对截距做一次合法性检查,出现负值或 NaN 就回退为 1,相当于不归一化该维度。
第三个坑:把垂直距离写成欧氏距离。现象是解集集中在少数几个参考点附近,其它区域完全空白。网上不少简化实现确实把associate写成了sqrt(sum((f_i - w).^2)),这算的是点到点的距离,不是点到射线的距离。正确的做法是用投影公式,让我吃了一次亏后,现在每次检查别人的实现,第一件事就是看associate函数里有没有除法dot(f_i,w)/dot(w,w)。
第四个坑:高维目标下参考点数量爆炸。现象是 M=8、p=10 时参考点数量 C(17,10)=19448,内存直接崩溃。这不是算法 bug,是组合数增长的本质代价。解决思路是降低 p 或用两层参考点生成策略,边界层用大 p、内部层用小 p,总的参考点数能压到原来的十分之一。
第五个坑:目标量级悬殊时归一化失效。现象是两个目标一个范围在 0 到 1,另一个在 0 到 1e6,截距完全被大目标主导,归一化后小目标的区分度接近零。解决方法是归一化前先对每个目标做独立缩放,把量级拉到同一个水平线,再进入自适应归一化流程。
6. 二次开发:把 NSGA-III 改造成自己的优化引擎
6.1 从测试函数到实际工程问题:约束处理与变量替换
拿到代码后最典型的改造路径是把 DTLZ2 换成一个真实的调度问题或参数优化问题。第一步改evaluate_objective.m,定义好输入输出的矩阵维度;第二步改main.m里的lb、ub和n_var。如果你的问题带约束条件,常见做法是在目标函数里加惩罚项,罚函数系数从 1e3 开始调,不要让惩罚项彻底压过真实目标,否则早期种群全是不可行解,算法探索效率极低。
6.2 性能验证:用超体积指标检查改造效果
改造后不要只看前沿图,超体积指标 HV 是更可靠的量化验证手段。HV 计算的是解集在目标空间中覆盖的体积,值越大说明解集的收敛性和分布性越好。在 MATLAB 里可以用pareto函数画二维前沿,三维用plot3,四维以上就要靠 HV 了。每次改完代码,我习惯固定相同随机种子,跑 NSGA-II 和 NSGA-III 各 10 次对比平均 HV,这样能量化出参考点带来的多样性收益。
6.3 进阶改造方向:并行化与热启动
当评估函数计算成本很高时,主循环里的evaluate_objective可以用parfor并行替换for,在并行池开启的情况下性能提升接近核数倍率。热启动是另一个实用的改造技巧:把上一轮进化得到的最终种群作为下一轮参数调整后的初始种群,避免重复冷启动的收敛开销。从那以后我每次接手新的优化项目,都会强制走一遍这套流程:先跑通测试函数验证代码正确性,再替换评估函数,然后用 HV 量化基线,最后才动手并行化和调参。希望帮到你。
本文还有配套的精品资源,点击获取