资讯详情

资讯详情

Python实现NSGA-II算法:从CEC-2021课设到多目标优化实战

简介这份资源是CUG智能优化课程设计配套的Python实现NSGA-Ⅱ算法完整代码包面向需要完成多目标优化课设或学习进化算法的本硕学生同样适合准备算法竞赛的开发者参考。压缩包共185个文件整体约969KB包括12个Python源码、100个mat格式的种群迭代快照、50个txt结果记录以及png可视化图表和xml工程配置等其中算法主程序实现非支配排序、拥挤距离计算与遗传算子赛题问题定义文件涵盖CEC-2021测试问题mat格式种群快照可用于还原每次迭代的种群状态便于对比验证。已有125人浏览学习。通过阅读代码可掌握NSGA-Ⅱ的初始化、选择交叉变异、非支配排序及精英保留等完整流程并理解如何将算法应用于多目标优化问题资源还附带HV等性能指标计算脚本方便量化评估解的收敛性与分布性。代码将问题定义、算法主体与结果分析分离结构清晰易扩展既可作为课设直接参照也能帮助学习者将NSGA-Ⅱ迁移到工程调度、路径规划等实际场景是智能优化算法入门的实用素材。1. 当课设题目是 NSGA-IICUG 智能优化课设与 CEC-2021 到底在考什么我第一次看到“Python 实现 NSGA-II 算法解决 CEC-2021 竞赛中的问题”这个题目时第一反应是这不是个普通的排序题也不是单纯调个库就能交差的作业。它要求你亲手写一遍 NSGA-II 的核心流程——快速非支配排序、拥挤度计算、锦标赛选择、精英保留、SBX 交叉和多项式变异再把它丢到 CEC-2021 的基准测试函数上跟已知最优解比一比。CUG 的智能优化课设选这个题其实是想让你把“多目标进化算法不是黑匣子”这件事真正搞清楚。很多人拿到这个题的第一直觉是去 GitHub 上搜现成的 NSGA-II 实现然后改个函数名交差。但 CEC-2021 的测试函数不是随便写个目标函数就能糊弄过去的它里面既有单峰也有多峰既有变量可分也有变量不可分的情况有的函数还用移位和旋转制造了欺骗性。你的算法要是只会在简单问题上跑一上竞赛函数就原形毕露。这篇文章我按自己做这套课设的完整思路来写从算法核心流程怎么拆到 Python 代码怎么写再到 CEC-2021 测试函数怎么用、参数怎么调、实验报告要放哪些数据最后把最容易翻车的地方一次说清。新手跟完这套流程能交出一份拿得出手的课设熟手也可以对照着查一下自己的实现哪里埋了雷。这套东西的价值不在“跑通”而在你知道每个算子在干什么、为什么这样设计、失败了应该去查哪个环节。2. 从“会调库”到“会写算法”NSGA-II 的五个核心模块是怎么在 Python 里落地的想用 Python 写一个能用的 NSGA-II首先得把算法拆成几个能独立实现、独立验证的模块。很多课设翻车就是因为一上来就把整个算法糊在一个大循环里出了问题根本不知道是交叉算子的 bug 还是选择环节的 bug。我一般会按五个模块来拆种群初始化、快速非支配排序、拥挤度计算、锦标赛选择、交叉变异。每个模块单独写完先跑一个小测试最后再拼成主循环。“会写”的标志不是你背下了伪代码而是你能解释为什么快速非支配排序要把种群分成一层一层的 Pareto 前沿、为什么拥挤度距离要在同一前沿内算、为什么锦标赛选择的压力参数 k 要取 2。这些设计决定不是拍脑袋拍出来的每个都对应一个具体的问题场景。下面我先讲清楚为什么要这么设计再给你能直接改着用的最小实现。2.1 快速非支配排序为什么把种群分成“一层一层”而不是直接比较所有个体多目标优化里最核心的问题就是“两个解谁更好”。单目标比大小就行多目标没有绝对的大小关系只有支配关系解 A 在所有目标上都不差于解 B且至少在一个目标上严格优于解 B那 A 支配 B。快速非支配排序要做的事情就是把种群里的所有个体按照支配关系分成若干层第一层是不被任何个体支配的个体集合第二层是去掉第一层以后不被剩余个体支配的以此类推。分层有什么用因为后续的选择操作需要优先保留更靠近真实 Pareto 前沿的个体。如果所有个体混在一起选多样性是有了但收敛性没法保证。NSGA-II 的核心思想就是先保证收敛前沿层越靠前越优先保留再保证多样性同一层内用拥挤度拉开距离。这两句话是整个 NSGA-II 的算法灵魂。用 Python 实现的时候最常见的做法是先用一个列表记录每个个体被哪些个体支配、又支配哪些个体同时记录每个个体被支配的次数。被支配次数为 0 的放进第一层然后把第一层移除后剩下个体的被支配次数相应减一再找出新的 0 支配次数个体作为第二层。这个过程用一个队列或者循环就能完成。真实实现代码如下def fast_non_dominated_sort(population, objectives): # population: 个体列表, 每个个体是一个 dict, 至少包含 vector 和 fitness # objectives: 目标函数列表, 每个函数接受一个向量返回一个标量 n len(population) S [[] for _ in range(n)] # S[i] 存被 i 支配的个体索引 Np [0] * n # Np[i] 存支配 i 的个体数量 rank [0] * n fronts [[]] for i in range(n): fi [f(population[i][vector]) for f in objectives] population[i][fitness] fi for j in range(n): if i j: continue fj [f(population[j][vector]) for f in objectives] if dominates(fi, fj): S[i].append(j) elif dominates(fj, fi): Np[i] 1 if Np[i] 0: rank[i] 0 fronts[0].append(i) k 0 while fronts[k]: next_front [] for i in fronts[k]: for j in S[i]: Np[j] - 1 if Np[j] 0: rank[j] k 1 next_front.append(j) k 1 fronts.append(next_front) for i in range(n): population[i][rank] rank[i] return fronts[:-1]这段代码的逻辑说明先对每个个体计算全部目标值并存入自身 fitness 字段然后两两比较支配关系构建“被谁支配”和“支配谁”两张表。被支配次数为 0 的个体天然属于第一层之后逐层剥离。fronts[:-1]是去掉最后那个空列表的小技巧。这里有一个我踩过的坑在循环里对每个 j 都重新计算一遍目标函数值会导致计算量翻好几倍。更好的做法是先把所有个体的目标值一次性算好存成矩阵比较时直接查表。CEC-2021 的函数计算量不小尤其是维度拉到 30 甚至 50 的时候每多算一遍都是实打实的等待时间。dominates函数是我自己写的辅助函数判断 a 是否支配 b条件是 a 的每个目标值都不大于 b 且至少一个严格小于。注意 CEC-2021 的测试函数基本都是最小化问题所以这里默认是越小越好。如果你的题目要求最大化直接把目标值取负号就行不用改排序逻辑。2.2 拥挤度距离同一层前沿里怎么定义“哪个解更稀有”分层只解决了收敛问题还没解决多样性问题。如果同一层前沿上的个体都挤在一个区域内比如全都集中在 Pareto 前沿的左端那这个算法虽然收敛了但得到的解分布很差决策者拿不到多样化的方案。拥挤度距离就是用来衡量一个个体在目标空间里被周围个体“挤”得有多紧的指标。拥挤度距离的计算思路并不复杂对同一个非支配层内的所有个体按每个目标分别排序然后看每个个体在相邻两个个体之间的目标值跨度有多大。跨度越大说明这个个体周围的区域越“空旷”越值得保留。两个极端边界个体在某一维上缺少邻居通常直接给无穷大的距离值保证边界个体一定被保留下来。实现时最容易出错的是目标值相同的情况。如果两个个体在某个目标维度上值完全一样排序后距离就是 0这本身没问题但如果你用max-min归一化时最大值等于最小值就会出现除零错误。我常用的解决办法是给分母加一个小 epsilon或者直接判断最大值等于最小值时这一维上所有人距离都设为 0。def crowding_distance(front, population, objectives): # front: 某一非支配层的个体索引列表 dist {i: 0.0 for i in front} if len(front) 2: for i in front: dist[i] float(inf) return dist for m in range(len(objectives)): front_sorted sorted(front, keylambda i: population[i][fitness][m]) dist[front_sorted[0]] float(inf) dist[front_sorted[-1]] float(inf) f_max population[front_sorted[-1]][fitness][m] f_min population[front_sorted[0]][fitness][m] if f_max - f_min 1e-12: continue for k in range(1, len(front_sorted) - 1): idx front_sorted[k] prev_idx front_sorted[k - 1] next_idx front_sorted[k 1] dist[idx] (population[next_idx][fitness][m] - population[prev_idx][fitness][m]) / (f_max - f_min) return dist这段代码的逻辑说明先给边界的两个个体无穷大拥挤度保证其存活然后对内部个体按每个目标维度累加归一化后的距离跨度。如果你跑出来的解总是集中在一片区域优先检查这个函数有没有写对因为我见过不少课设代码把f_max - f_min写成了f_max f_min导致拥挤度计算完全失真。一个容易被忽略的细节是拥挤度距离要在每个前沿层内部单独计算不能跨层。因为跨层比较没有意义——第一层的解和第二层的解在 Pareto 意义上根本不在一个档次上。有些人在实现时图省事把所有个体拉在一起算拥挤度这样会让优秀前沿的信息完全被稀释掉。2.3 锦标赛选择与精英保留控制种群规模的同时锁住优质解有了排序和拥挤度选择就顺理成章了。NSGA-II 用的是二元锦标赛选择每次从种群中随机挑两个个体优先选非支配层号小的层号相同选拥挤度距离大的。这样反复挑直到选满需要的个体数量。锦标赛选择的优势是它不需要全局排序信息操作随机性带来一定的探索能力。二元锦标赛的压力参数是一个很值得讨论的事情。k2 是经典配置选择压力适中。如果 k 调大比如 k4选择压力变大收敛速度可能更快但种群多样性容易崩如果你发现算法收敛到某个局部区域彻底不动了可以先看看是不是 k 设太大了。精英保留是 NSGA-II 的另一个关键设计子代生成以后把父代和子代合并成一个大种群然后按照“层号小的优先层号相同拥挤度大的优先”排序截取前 N 个个体作为下一代种群。这保证了最优解永远不会丢失——即使子代整体变差了父代里的优秀个体还在合并池里兜底。def tournament_selection(population, k2): import random a random.sample(population, k) best min(a, keylambda ind: (ind[rank], -ind[crowd_dist])) return best def elitist_merge(parents, offspring, pop_size): combined parents offspring # parents 和 offspring 里的个体都已经算好了 rank 和 crowd_dist combined.sort(keylambda ind: (ind[rank], -ind[crowd_dist])) return combined[:pop_size]选择代码里的-ind[crowd_dist]是个小技巧Python 的 sort 默认从小到大想要“层号从小到大、拥挤度从大到小”就利用元组排序时第二项取负来反转方向。这个写法简洁但如果你后续要读代码的人不熟悉这个技巧最好加一行注释说明。精英保留这里有一个很多人没注意到的边界条件如果合并后总个体数少于等于种群规模那直接全部保留不需要截断。但这种情况通常只会出现在算法初期因为后续父代子代加起来一定是 2 倍规模除非你的交叉变异实现有 bug 导致部分子代没生成。2.4 模拟二进制交叉与多项式变异算子参数到底怎么设才不玄学SBX 交叉和多项式变异是 NSGA-II 的默认算子也是课设里最容易“调参调成玄学”的地方。SBX 的分布指数 eta_c 控制子代离父代的远近eta_c 越大子代越可能贴近父代越小子代越可能远离父代。多项式变异的分布指数 eta_m 同理控制变异步长的大小分布。Deb 在原始论文里的建议是 eta_c20、eta_m20实际使用中我也建议你先从这两个值开始不要随便改。SBX 交叉的核心是一个随机数 u 和一个对应的分布因子 beta。当 u 小于等于 0.5 时按一个公式算否则按另一个公式算。关键是这个对称设计两个父代产生两个子代保证种群规模不变。我见过有些初学者只生成一个子代导致种群数量不对主循环跑两代就崩掉或收敛异常。def sbx_crossover(p1, p2, eta_c20, lb0, ub1): import random c1 [] c2 [] for x1, x2 in zip(p1, p2): if random.random() 0.5: u random.random() if u 0.5: beta (2 * u) ** (1 / (eta_c 1)) else: beta (1 / (2 * (1 - u))) ** (1 / (eta_c 1)) c1_x 0.5 * ((x1 x2) - beta * abs(x2 - x1)) c2_x 0.5 * ((x1 x2) beta * abs(x2 - x1)) c1_x min(max(c1_x, lb), ub) c2_x min(max(c2_x, lb), ub) else: c1_x x1 c2_x x2 c1.append(c1_x) c2.append(c2_x) return c1, c2 def polynomial_mutation(child, eta_m20, lb0, ub1): import random mutated [] for x in child: if random.random() 1.0 / len(child): u random.random() if u 0.5: delta (2 * u) ** (1 / (eta_m 1)) - 1 else: delta 1 - (2 * (1 - u)) ** (1 / (eta_m 1)) new_x x delta * (ub - lb) mutated.append(min(max(new_x, lb), ub)) else: mutated.append(x) return mutated参数说明交叉部分的lb和ub是决策变量的边界CEC-2021 每个测试函数的边界在问题定义里写得明明白白一般是对称区间比如 [-100, 100]。变异概率这里用了1.0 / len(child)也就是每个变量平均变一个这是常见的多项式变异配置。如果你想加快收敛可以把变异概率调低一点想增强探索就调高一点。但一定记住这两个算子的本质是概率性的单次运行的结果有随机性判断算法好坏要看多次运行的统计结果。一个值得注意的坑SBX 交叉后的子代可能越界。你在生成子代后必须做边界修正否则 CEC-2021 的函数在某些区域会返回异常值或无穷大。很多人算法结构完全正确就是因为没做边界修正导致种群中混入越界个体最终 IGD 指标爆炸。3. 把 CEC-2021 挂到算法上测试函数体系、Python 改写与最小跑通代码课设题目明确点出了 CEC-2021。CEC 系列竞赛是进化计算领域的经典基准测试集每年换一套新函数设计思路一般是打乱函数之间的相似性让“调参调出来的强算法”失去优势。CEC-2021 的多目标测试函数是从之前的 CEC-2019 多目标测试集延续演化过来的也融合了单目标 CEC-2021 的一些设计思路。函数定义、真实 Pareto 前沿、最优解位置等数据都是公开的竞赛材料通常以 PDF 或官网数据包形式发布。做课设的时候先不要急着写算法把每个函数的公式和变量边界、目标数量、是否旋转、是否移位看清楚这个步骤省下来后面排查问题会痛苦很多。3.1 CEC-2021 的函数结构为什么说它“专门治各种过拟合”CEC-2021 的多目标函数有一个显著特征每个函数都附带对应的真实 Pareto 前沿这个前沿是从已知最优解采样得到的不是随便算出来的。你在验证自己算法时要把最后得到的近似前沿跟真实前沿做对比计算 IGD 指标这个指标是课设评分的重要参考。如果你的算法连 IGD 都不会算或算错了实验报告的含金量会大打折扣。CEC-2021 的函数设计有很多细节非常狠。比如有些函数是旋转过的——把决策变量先做线性变换再计算导致变量之间不再独立可比。有些函数是欺骗性的——全局最优区域很小让算法很容易收敛到局部最优。还有些函数是多峰密集型的多个峰值之间差距很小算法会在这几个峰之间反复横跳。这么设计的目的是确保一个算法只有在真正理解了多目标优化的核心机制、而不是靠某些问题特性取巧才能在竞赛中拿到好成绩。从工程角度看做 CEC-2021 的课设不需要去重新实现这些函数的内部逻辑——题目通常会给 MATLAB 版本的函数包但我们是 Python 实现处理方式有两种要么用scipy.io.loadmat读取 MATLAB 数据文件要么自己用 Python 把函数重新写一遍。我建议你优先找有没有官方的 Python 接口如果没有就仔细读官方定义自己翻译。3.2 真实前沿数据怎么加载numpy 读取与指标计算的最小闭环拿到官方数据包以后通常会看到两类文件函数定义的源码或描述文件、真实 Pareto 前沿的数据文件。真实前沿文件一般是一个矩阵每一行是一个 Pareto 最优解的目标值向量行数至少几百。用 Python 加载这类数据时我习惯用 numpy 的np.loadtxt或np.genfromtxt注意文件头部可能有多行描述性的注释需要正确处理。import numpy as np def load_pf_file(file_path): # 跳过以 # 或 % 开头的注释行, 加载真实 Pareto 前沿 pf np.loadtxt(file_path, comments#) if pf.ndim 1: pf pf.reshape(1, -1) return pf这段代码虽然短但解决了一个真实问题很多 CEC 数据文件的第一行是文件格式说明用#开头直接丢给np.loadtxt会读取失败。如果你读出来的前沿形状不对比如只有一行或者行数特别少一定要先打印一下原始文件的前几行看看结构。另外如果你的数据文件是 MATLAB 的.mat格式用scipy.io.loadmat读取时需要先看清楚变量名——官方文件里不同年份的变量名并不统一盲猜容易报错。加载完真实前沿接下来就是 IGD 指标的计算。IGD 衡量算法求得的近似前沿与真实前沿之间的平均距离越小越好。它反映的是算法最终结果的综合表现——既看收敛性也看覆盖度。计算 IGD 的代码如下def compute_igd(approx_front, true_front): # approx_front: 算法得到的非支配解目标值矩阵, 每行一个解 # true_front: 真实 Pareto 前沿目标值矩阵 distances [] for true_point in true_front: diff approx_front - true_point dist np.sqrt(np.sum(diff ** 2, axis1)) distances.append(np.min(dist)) return np.mean(distances)IGD 的计算逻辑是对真实前沿上的每个点找到算法近似前沿中离它最近的那个点计算欧氏距离然后对所有真实前沿点取平均。IGD 值小说明算法得到的解不仅离真实前沿近还能覆盖到真实前沿的各个区域。如果算法只收敛到真实前沿的某一段IGD 依然会很高。这个指标很公平它同时惩罚了漏掉区域和偏离前沿两种情况。还有另一个常用指标是 HV超体积指标它计算算法得到的非支配解在目标空间中所覆盖的体积。HV 不需要知道真实前沿对目标空间中的参考点选择比较敏感但它在处理不规则前沿时比 IGD 更稳定。如果课设要求两个指标都算建议你先把 IGD 做对再补 HV。3.3 跑通一个函数的完整主循环种群初始化到下一代生成的代码骨架所有模块都具备后主循环是最后一步拼接。我把最常用的主循环结构贴在这里它包含初始化、评价、排序、选择、交叉、变异、合并、截断的完整流程。注意这里为了可读性去掉了收敛判断和统计收集的代码实际课设中你需要把这些信息记录下来画收敛曲线。import numpy as np import random POP_SIZE 100 MAX_GEN 300 DIM 10 LB, UB -100, 100 def objective_sum(x): # 以 CEC-2021 某个函数为例, 这里用 ZDT1 替代演示结构 obj1 x[0] g 1 9.0 / (DIM - 1) * np.sum(x[1:]) obj2 g * (1 - np.sqrt(obj1 / g)) return [obj1, obj2] population [] for _ in range(POP_SIZE): vector [random.uniform(LB, UB) for _ in range(DIM)] population.append({vector: vector}) fronts fast_non_dominated_sort(population, [objective_sum]) for front in fronts: distances crowding_distance(front, population, [objective_sum]) for idx in front: population[idx][crowd_dist] distances[idx] for gen in range(MAX_GEN): offspring [] while len(offspring) POP_SIZE: p1 tournament_selection(population) p2 tournament_selection(population) c1, c2 sbx_crossover(p1[vector], p2[vector], eta_c20, lbLB, ubUB) c1 polynomial_mutation(c1, eta_m20, lbLB, ubUB) c2 polynomial_mutation(c2, eta_m20, lbLB, ubUB) offspring.append({vector: c1}) if len(offspring) POP_SIZE: offspring.append({vector: c2}) fronts fast_non_dominated_sort(offspring, [objective_sum]) for front in fronts: distances crowding_distance(front, offspring, [objective_sum]) for idx in front: offspring[idx][crowd_dist] distances[idx] population elitist_merge(population, offspring, POP_SIZE)主循环说明这里首先生成父代并初始化排序然后每代循环里不停做锦标赛选父、交叉、变异、生成子代。子代生成后重新计算非支配排序和拥挤度最后合并父代子代截断到 POP_SIZE。细节上要注意两点一是objective_sum函数每次调用都要重新计算目标值如果在交叉变异后没有重新计算 fitness 就去做排序那排序用的全是旧数据算法会完全失效二是elitist_merge里直接对组合列表排序靠的是之前计算的 rank 和 crowd_dist所以合并前必须确认子代个体这些字段都正确生成。主循环写完后先别着急跑全参数先用小规模验证一下POP_SIZE30, MAX_GEN50, DIM2跑几次人工看看结果是否有基本合理的 Pareto 前沿形状。如果连这个简单情况都跑不对直接上大规模只会浪费时间。4. 课设代码要“看得懂”也要“跑得动”CUG 智能优化课设的验收视角与实验设计如果你交的课设只是代码能跑、结果能用那也就是及格水平。CUG 这类智能优化课设的验收通常会追问几个问题NSGA-II 为什么比随机搜索好你的参数为什么这么设置IGD 指标是怎么算的收敛曲线有什么特点这些问题你在做实验的时候顺手记录就能答上临时抱佛脚容易露馅。我的建议是实验部分按“固定随机种子 多次运行 平均值/标准差”来做。多目标进化算法本质是随机算法一次跑出来的结果不能说明任何问题。每次换一组随机种子跑 10 遍记录 IGD 的均值和标准差才能体现算法的稳定性。这也是课设报告里的核心数据来源。4.1 实验报告要放的五类数据从收敛曲线到非支配前沿的可视化第一个必放的是收敛曲线。横轴是迭代代数纵轴是 IGD 值。每代都算一次 IGD把曲线画出来可以看出算法是稳定下降还是反复震荡。震荡说明交叉变异算子太激进稳定但下降慢说明选择压力不够。第二个必放的是最终的非支配前沿图横纵轴是两个目标值真实前沿和算法前沿画在一张图上一眼就能看出收敛性和分布性好坏。第三个必放的是不同运行次数下的 IGD 统计表包括均值、标准差、最好值、最差值。这个表格比一张效果图更有说服力。第四个是参数敏感性分析比如固定其他参数只改种群规模看 IGD 怎么变化固定种群规模只改迭代代数看多少代开始收敛。第五个是把你的算法结果跟随机搜索或者单目标优化器的结果做一个对比哪怕只有一组对比数据也能凸显 NSGA-II 作为多目标优化算法的真正价值。真实前沿加载和可视化画图时有个小坑如果你的真实前沿文件是 CEC-2021 官方提供的那里的目标值分布范围往往很大画图前要不要归一化取决于你后续是否要跟算法结果画在同一个坐标系。我建议先直接画观察一下分布范围如果两者量级差太多再决定是否归一化。4.2 怎么判断代码是不是“真的写对了”从单目标退化到多目标验证的方法一个非常有效的验证方法是把一个多目标问题退化成单目标来测试。怎么退化给两个目标加权求和变成一个目标然后让 NSGA-II 去优化这个单目标理论上最终应该逼近或接近单目标最优解。如果连这个都做不到说明你的交叉变异或评价流程写错了。这不是绕路而是用已知结论来验证算法实现正确性。具体操作流程先用一个已知最优解的单目标函数比如 Sphere 函数把 NSGA-II 里面的目标函数改成两个 Sphere一个用 x 的奇偶位计算一个用偶位计算让它们既相关又有差异。跑完以后查看最终非支配前沿——真实的 Pareto 前沿你们自己都能推出来对照验证。另一种更直接的方式是拿 NSGA-II 跟一个成熟的多目标优化库做对比。虽然我不想依赖外部库但课设验证阶段用pymoo或platypus跑同一个 CEC 函数做参考看你的算法 IGD 跟库结果差多少比你闷头调自己的代码快得多。注意到差距大不一定是你算法差可能有几个原因参考库用了不同的算子参数、不同的种群淘汰策略或者你用的函数实现跟库用的不完全一致。4.3 参数不调好别甩锅给算法种群规模、迭代代数和算子参数的匹配原则我做的时候有一条经验先固定一个你认为合理的参数组合跑通逻辑再系统调参。千万不要同时改多个参数——比如把种群大小从 100 改成 200 的同时又把交叉概率从 1.0 改成 0.8那实验结果变了你根本不知道是哪个参数起了作用。参数设置有几个经验关系可以参考种群规模越大需要的迭代代数通常越多。如果你只打算跑 100 代种群规模设成 50 或 100 就够了如果你设 300 代种群规模 100 仍然合理但如果你设 1000 代且种群规模只有 50算法可能在后半段出现近亲繁殖导致多样性下降。CEC 函数在 10 维到 50 维之间我一般按“种群规模不低于维度数的 5 倍不超过维度数的 20 倍”来粗估。维度 10 时用 100维度 30 时用 300这是一个不会出大错的起点。变异概率和交叉概率的设定我一般保持交叉概率接近 1.0变异概率不按固定常数设而是跟决策变量维度挂钩——每维变异概率取 1/DIM这样平均一次变异会改动一个维度比较平衡。如果你发现早熟收敛可以把变异概率调高两倍如果你发现收敛慢则可以把变异概率降三分之一左右。这些调整幅度不要太大一次只能动一个参数效果要记录。5. 把 CEC-2021 课设从“能跑”做到“好用”避坑手记5.1 现象IGD 指标突然飙到无穷大或异常大原因一般是目标函数计算过程中出现了 NaN 或无穷大。CEC-2021 里有些函数包含对数、根号、除法等操作决策变量越界后这些操作会返回非数值。另一个常见原因是真实前沿文件中典型值很小而算法前沿中部分点目标值数量级差异很大导致距离计算出现巨大值。解决方法是两件事同时做一是在目标函数里对返回值做一次有效性检查如果出现非有限数值直接赋一个极大值比如 1e10防止污染排序二是在交叉变异函数里严格做边界修剪确保个体永远不会越界。这两个步骤做了以后绝大多数 IGD 异常问题都能解决。你可以在调试阶段打印每一代中非有限数值个体数量看看是哪里产生了问题。5.2 现象跑了很多代解全部挤到目标空间的一个角落原因通常是多样化机制失效了要么拥挤度没起作用要么选择时拥挤度排序写反了。我见过有人把排序条件写成了“拥挤度小的优先保留”这会导致算法极度偏向拥挤区域相当于主动放弃多样性。还有一种可能性是你的变异概率设得太低种群很快被选择压力“吸”到一个局部区域再也跳不出来。解决方法是先检查选择函数里的排序元组确认是rank正序、crowd_dist降序然后在多个随机种子下重复跑排除单次随机性的影响。如果问题依然存在尝试把变异概率从 1/DIM 提高到 2/DIM这通常能有效增加早期探索能力。解挤在一角的问题在 CEC-2021 的旋转函数上尤其常见因为变量之间的耦合会让常用交叉算子产生大量无效子代。5.3 现象算法结果跟参考库差距巨大但你的代码看起来没问题原因可能不在算法本身而在测试函数的 Python 实现上。CEC 系列函数的定义非常精细稍微漏掉一个旋转矩阵、移位向量或者换了一个计算顺序结果就会完全不同。如果你是自己照着 PDF 描述翻译成 Python 的这里就是最大风险区。解决办法是做一个交叉验证用几组已知输入和对应输出跟 MATLAB 参考实现或 pymoo 内置测试函数的结果做对比。如果目标值对不上说明函数实现有误。这个验证过程也许要花一两个小时但它能省掉后面怀疑算法结构的大量时间。如果目标值能对上再找算法差异——一般就是参数配置不同了。5.4 现象运行速度慢得难以接受改小规模后又怕结果不可信原因通常是你实现时重复计算了太多目标函数。比如在非支配排序里每比较一对个体都重新计算目标值或者每一代都对整个种群重新计算一遍真实前沿距离。CEC-2021 的函数复杂度不低维度一大重复计算会指数级拉慢。解决方法是在算法主循环开始前把种群个体的目标值先全部算好存入表格后续排序、拥挤度计算都只查表。另外IGD 计算不需要每代都做可以每 10 代或者 50 代记录一次最后画曲线时用记录值即可。用 Python 写这种计算密集型算法时学会把计算集中到最必要的位置速度提升往往非常明显。5.5 现象反复调参数课设报告里却写不出“为什么这么调”原因是你可能一直在黑盒式地调参缺少对参数影响机理的理解。比如你发现增大种群规模后 IGD 下降了如果你明白这是因为搜索空间的覆盖率提升了那你在报告里就能写清楚种群规模本质控制每代采样密度密度太低时交叉变异产生的子代容易集中在有限区域。如果你发现增大迭代代数后 IGD 还在下降说明算法还没完全收敛需要跑更多代才能逼近真实前沿。更好的办法是画一个参数敏感性热力图横轴是种群规模或者迭代代数纵轴是不同问题维度颜色是 IGD 值。这一张图比你用文字解释一百句都有说服力。参数不在多学做图就能讲明白。6. 从课设到竞赛复现交叉验证、CEC-2021 真实前沿对齐与性能验证课设交完不是终点。很多人做完了这套题对 NSGA-II 的认知还是停留在“能跑出图”的层面实际上离“能复现论文结果”还有一段距离。这段距离可以用三个步骤缩短第一步用手工构造的简单问题验证每个模块的正确性第二步用多个函数交叉验证算法的整体性能第三步用统计手段确认结果可信。这三个步骤做完你的代码才算真正有了可复现性——这不仅对课设有用对将来发表论文或做工程落地也有直接帮助。交叉验证的具体做法是把种群初始化为几个已知的极值点比如全 0、全 1、边界值然后看经过一代选择交叉变异后目标值是否符合预期方向。一个更直接的做法是把交叉概率设为 0、变异概率设为 0跑一代后种群应该原封不动——这个测试能排除很多隐藏 bug。我调试时经常用这三个测试全零初始化、单点收敛、固定随机种子。如果这三个都过了再上真实 CEC-2021 函数。CEC-2021 多目标测试集中的每个问题都有官方给出的真实前沿文件。在做最终比对前先确认你的目标值方向跟官方一致CEC 系列绝大多数是求最小化。如果你的优化方向反了前沿图会完全错位IGD 指标毫无意义。格式方面官方数据文件中有些是横着排的有些是竖着排的你的true_front矩阵务必跟算法输出的格式保持一致。性能验证的最后一块拼图是多次运行的统计报告。我常用的做法是固定 5 个不同的随机种子每个种子跑 20 次共 100 次运行结果然后计算 IGD 的均值、中位数和标准差。这种方法能同时验证算法的平均性能和稳定性。如果均值低但标准差很大说明算法不稳定——对某些幸运的初始化表现很好对另一些初始化则表现很差如果均值高但标准差小说明算法稳定但能力不足需要调整算子参数。真正的竞赛复现还涉及一个问题CEC-2021 的测试函数有时会提供不同维度设置10 维、30 维、50 维用到的函数特征可能不同。做课设时你可能只跑了 10 维或 30 维但如果你想升级这个项目可以尝试在多维度上跑同一组参数观察算法是否保持竞争力。这种“泛化能力”才是多目标进化算法研究者真正关心的东西。我自己做这类实验时习惯把所有运行结果保存为 CSV 文件便于后续统计分析。文件内容包含算法名、问题名、维度、随机种子、代数、IGD、HV 等字段。这套方法建议你直接照做它能让你的课设报告从“写了一堆字”变成“数据支撑的结论”。最后提一个习惯性的建议代码和实验记录一定要分开。把算法实现放一个文件把实验结果和图片放另一个目录把你的参数调整记录存在 Excel 或 Markdown 文件里。这样即使过了几个月再回来看这个项目也能立刻知道当时试了哪些参数、试出来的效果如何。我吃过不记录的亏翻车以后再也不敢偷懒了。希望这套流程能帮你的课设省下几个通宵也能让你真正把 NSGA-II 这件事做明白。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →