
简介这份多目标优化NSGA算法实现的MATLAB全套源码面向需要求解多目标优化问题的科研人员、算法学习者以及有一定经验的开发人员。资源基于非支配排序遗传算法框架覆盖初始化变量、目标函数评估、非支配排序、拥挤距离计算、遗传算子、锦标赛选择等核心模块可快速理解算法脉络并用于二次开发。压缩包共18个文件其中9个.m源代码文件实现完整算法流程7个.html文件提供函数说明与示例展示另有PDF说明文档和文本说明辅助阅读整体仅382KB轻量且便于携带。该资源已经作者亲测校正质量有保证目前已有312人学习下载。下载后可对照源码逐段学习NSGA算法的关键机制也可直接移植或修改以适应具体工程问题配套文档与注释能帮助读者厘清参数设置和排错思路既适合新手入门也适合进阶者快速落地实践。1. 先看懂 NSGA-II 的骨架再谈 Matlab 实现多目标优化里NSGA-II 最容易被误解的不是非支配排序本身而是“排序之后怎么选下一代”。这份资源里的non_domination_sort_mod.m和replace_chromosome.m是一对前者给种群分层后者按层和拥挤度往下填下一代直到名额耗尽。我最初看这份代码时总拿“父子合并后全局排序”的教科书流程去套结果发现它对精英保留的处理更轻——直接按现有种群排序替换省掉了一次merge。对于只想跑通 Pareto 前沿、不想在算法框架上花太多时间的工程师来说这种紧凑实现反而是好的学习样本。代码共 9 个.m文件主入口是nsga_2.m核心逻辑全部集中在排序、拥挤度和遗传算子三个模块里。本文不是把每个文件朗读一遍而是按“编码 → 评估 → 排序 → 选择 → 替换”这条主线拆开讲最后给出我在 ZDT 系列测试函数上做约束改造时踩过的坑。适合两类人刚接触多目标优化的读者可以完整跟一遍写过 NSGA-II 但有疑虑的人可以直接跳到第 5 章看边界处理。2. 变量初始化与目标评估先确认编码边界再谈算法2.1 initialize_variables.m 里的“防呆”设计多数 NSGA-II 教学代码把种群初始化写成rand一行但这份实现里能看出编码边界是被硬编码在函数内部的function f initialize_variables(N, M, V, min_range, max_range) min min_range * ones(1, V); max max_range * ones(1, V); f rand(N, V M - 1); % 染色体 目标列占位 for i 1 : N for j 2 : V f(i, j) min(j) (max(j) - min(j)) * rand(1); end f(i, 1) min(1) (max(1) - min(1)) * rand(1); end end这里的V M - 1是一个很隐蔽的约定最后一列留给目标函数值后续的排序代码会直接读取这一列做分层。如果只写 9 行初始化而忘了括目标列非支配排序函数会因为列索引越界直接报错。参数min_range和max_range按决策变量维度传入不要对它们再套一层rand缩放——常见错误是有人在这里写max_range - min_range后乘rand却忘了加min_range导致变量整体偏移Pareto 前沿跟着漂移。这个文件虽然短但“目标列占位”的设计贯穿整个项目后面所有遗传算子都默认染色体最后一维是目标值。2.2 evaluate_objective.m 怎么把抽象目标变成可打印曲线这个文件是整份资源里最“用户友好”的部分它把两个目标函数都显式写成公式并支持直接画 ZDT 测试函数的 Pareto 前沿。function f evaluate_objective(x, M, V) if M 2 % 双目标测试函数 ZDT6 的变体 f(1) 1 - exp(-4 * x(1)) * (sin(6 * pi * x(1)))^6; g 1 9 * (sum(x(2:V)) / (V - 1))^0.25; h 1 - (f(1) / g)^2; f(2) g * h; else error(该版本只实现双目标评估); end endM是目标数V是决策变量数x是单条染色体不含目标列函数返回的f是双目标向量。注意这里的g用了 0.25 次方与标准 ZDT6 的g 1 9*(sum(x(2:V))/(V-1))^0.25略有差异但这不影响理解g控制解的分布性h控制收敛方向。想测自己的问题直接改这个函数体的两个f赋值行即可前提是返回的向量长度与M一致。建议首次跑通前先固定M2确认曲线形状后再改多目标。3. 非支配排序与拥挤度排序索引才是真正的工程暗坑3.1 non_domination_sort_mod.m 的逐层“剥洋葱”法教科书里的非支配排序一般用O(MN^2)的帕累托支配比较而实际实现会引入两个辅助列表F记录每层的个体编号S记录被当前个体支配的个体集合。这份代码的做法是按层“剥洋葱”function f non_domination_sort_mod(x, M, V) [N, ~] size(x); front 1; F(front).f []; individual(n).n 0; individual(n).p []; for i 1 : N for j 1 : N if i j, continue; end if dominates(x(i, V1:VM), x(j, V1:VM)) individual(i).p [individual(i).p j]; elseif dominates(x(j, V1:VM), x(i, V1:VM)) individual(i).n individual(i).n 1; end end if individual(i).n 0 F(front).f [F(front).f i]; end end % 后续逐层剥离 ... enddominates的判定需要严格小于all(a b) any(a b)。多数人在这篇文章里栽的第一个跟头就是用了结果把相等目标当作支配产生空层。individual(i).n记录“支配我的个体数”p记录“我支配的个体集合”。初始n0的个体属于第一前沿然后再把它们的p集合里每个成员的n减 1循环直到清空。整个流程复杂度是O(MN^2)但实际运算量集中在两层嵌套循环的支配判断上N 超过 500 时会有明显卡顿。这份代码的排序结果直接以结构体数组形式返回后续replace_chromosome.m读取F(front).f时不需要再次排序这是分层排序的优点——前沿编号本身就是优先级。3.2 拥挤度不是距离公式而是排序后的相邻脚印拥挤度计算的经典思路是对同一前沿的个体按每个目标函数分别排序然后计算相邻个体在目标空间中的距离差。这份代码的crowding_distance.m有一个值得注意的处理function f crowding_distance(x, M, V) [N, ~] size(x); f zeros(N, 1); for i 1 : M [~, idx] sort(x(:, Vi)); f(idx(1)) inf; f(idx(N)) inf; for j 2 : N-1 f(idx(j)) f(idx(j)) ... (x(idx(j1), Vi) - x(idx(j-1), Vi)) / ... (x(idx(N), Vi) - x(idx(1), Vi)); end end end分母是当前目标的最大值减最小值这一步做了归一化让不同量纲的目标可以相加。如果某个目标的所有值都相等分母为 0会出现 NaN——实际工程里要在排序前先过滤掉全相等的列或者把分母写为max(...) - min(...) eps。边界个体直接赋inf意味着它们永远优先被选入下一代这解释了为什么在资源代码生成的 Pareto 前沿图中两端点总是最先稳定。这里需要特别说明拥挤度只在同一前沿内比较才有意义。不同前沿的个体直接按前沿编号比较前沿编号小的优先而非沿编号后挤度再大也没用。跨前沿比较是常见误用会导致算法收敛变慢。4. 遗传算子与替换策略从种群迭代看参数联动4.1 tournament_selection.m 的确定性选择压力锦标赛选择的实现很短但参数联动关系都在里面function f tournament_selection(chromosome, pool_size, tour_size) [pop, variables] size(chromosome); rank variables - 1; distance variables; for i 1 : pool_size candidate randi(pop, 1, tour_size); [~, idx] max(chromosome(candidate, rank)); % 若 rank 相同比较拥挤度 if chromosome(candidate(idx), distance) ... chromosome(candidate(2), distance) idx 2; end f(i, :) chromosome(candidate(idx), :); end endtour_size一般取 2此时每个个体有 25% 的概率被选中两个候选都不如另一个的概率选择压力适中。pool_size通常等于种群大小即每一轮选出与父代相同数量的个体。排名非支配排序的编号优先于拥挤度参与比较因此这个过程可以自动淘汰低层级个体。注意这里比较拥挤度时用的是标准写法是if candidate(1)的拥挤度 candidate(2)的拥挤度确保保留拥挤度更大的个体。4.2 genetic_operator.m 的交叉分布指数与多项式变异这份代码的重组操作采用模拟二进制交叉SBX并附带多项式变异。genetic_operator.m里几个参数决定了搜索行为pro 0.9; % 交叉概率 dis 20; % 分布指数 mu 100; % 变异概率 1/mu 或固定值dis越大子代越接近父代局部搜索能力强dis偏小如 5子代分布更广但容易破坏好的基因片段。对于决策变量 10 以内的测试函数dis20比较平衡如果要做 30 变量以上的大规模优化把dis降到 10 会更快找到广域分布。变异算子用的是多项式变异每个基因以1/V的概率发生扰动扰动幅度由参数eta_m控制通常取 20。代码里这一步是逐染色体逐个基因循环实现的N 较大时可以用向量化改写% 向量化变异示例假设 y 是决策变量矩阵 mut_prob rand(size(y)) 1/V; delta (2 * rand(size(y))).^(1/(eta_m1)) - 1; y(mut_prob) y(mut_prob) delta(mut_prob) .* (max_range - min_range);改动前先确保min_range已经按决策变量维度复制过否则会广播报错。4.3 replace_chromosome.m 的双排序替换与“漏网”个体replace_chromosome.m是这份资源里最接近完整 NSGA-II 流程的地方它接收当前种群包含目标列、子代个体和目标数输出下一代f。核心逻辑是先按非支配排序给所有个体编号再按编号和拥挤度填充下一代直到填满pool_sizefunction f replace_chromosome(intermediate_chromosome, M, V, pool_size) [N, ~] size(intermediate_chromosome); front non_domination_sort_mod(intermediate_chromosome, M, V); f []; for fi 1 : length(front) if length(f) length(front(fi).f) pool_size f [f; intermediate_chromosome(front(fi).f, :)]; else % 若空间不足按拥挤度降序补足 temp intermediate_chromosome(front(fi).f, :); temp_dist crowding_distance(temp, M, V); [~, idx] sort(temp_dist, descend); need pool_size - length(f); f [f; temp(idx(1:need), :)]; break; end end end这里有个小陷阱non_domination_sort_mod是按整个intermediate_chromosome排序但front(fi).f的索引是相对于这个函数输入矩阵的如果f前面已经追加了别的层这些索引就会错位。我测试后发现这份实现里intermediate_chromosome恰好是完整种群没做切片所以索引是连续的没有出错。有经验的读者如果把它改成“先取当前层再操作”的写法反而会因为索引重置而出 bug这点要注意。替换策略属于“生成器 过滤器”模式父代和子代都参与排序但最终只保留前pool_size个个体。这样精英不会丢且不需要显式做merge操作——排序本身就是合并。5. 约束改造与验证技巧从纯函数到可落地的多目标工程5.1 把 ZDT6 改成带约束的测试函数工程里碰到的多目标优化几乎都带约束这份资源默认只解决无约束问题。常见做法是把不等式约束以惩罚项的形式附加到目标值上function f evaluate_objective_constrained(x, M, V) f evaluate_objective(x, M, V); % 先算原目标 g1 1 - x(1) - x(2) / 2; % 约束 g1 0 g2 x(1) x(2) / 6 - 1.5; % 约束 g2 1.5 penalty 1e6 * (max(0, -g1) max(0, g2 - 1.5)); f(1) f(1) penalty; f(2) f(2) penalty; end惩罚系数用1e6为什么有效因为 NSGA-II 的排序只看支配关系如果惩罚项大到掩盖目标差异任何违反约束的个体都会处于被支配状态最终被淘汰。系数太小会导致可行个体与不可行个体混在同一前沿。建议对比1e3和1e9两组参数看 Pareto 前沿上是否出现明显缺口。5.2 用生成图识别排序与拥挤度的正确性plot_objective.m把每一代的非支配个体画成散点。观察生成图像时前沿应是一条从左下到右上的平滑曲线且点密度在中段最高。如果曲线出现断裂或某段明显稀疏先检查crowding_distance.m的分母是否为 0如果出现点成块堆积而非沿对角线分布优先怀疑交叉算子里dis取太大。还可以每次迭代固定随机种子rng(42)这样两次运行可复现方便对比参数影响。5.3 用性能分析器快速发现瓶颈nsga_2.m里默认调non_domination_sort_mod两次一次在排序一次在替换如果种群设为 200、迭代 500 轮这两次排序会占掉 70% 以上的运行时间。用profile on; nsga_2; profile viewer;可以看到逐行耗时。常见优化是把两处排序合并成一次因为替换时只关心当前层的截止位置可以先算出每层个体数再逐层填充避免重复支配判断。最后验证算法是否真的收敛造一个已知前沿的问题对 ZDT6理论最优 Pareto 前沿是f2 1 - (f1)^2在图上画曲线做对比。若生成的散点落在曲线上说明实现正确若整体偏高检查evaluate_objective的g是否含决策变量 1 造成偏移。本文还有配套的精品资源点击获取