ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多目标优化实战:NSGA-II Python代码包解析与调参指南

多目标优化实战:NSGA-II Python代码包解析与调参指南 简介多目标优化处理的是多个目标相互冲突的决策问题例如汽车选购中的动力与油耗、算法调参中的精度与延迟其核心输出不是唯一最优解而是一组互不支配的Pareto前沿解集。实现多目标优化的经典算法NSGA-II通过非支配排序将种群划分为不同层级并结合拥挤度距离保证解的均匀分布从而在无需人工指定权重的前提下逼近真实前沿。Python作为一种高效易用的编程语言常被用来实现NSGA-II等进化算法配合ZDT1等测试函数可以快速验证算法的正确性。本文围绕一套附带讲解视频的Python代码包梳理从算法原理、代码结构、参数调整到实际应用的关键要点涵盖机器学习超参寻优、量化策略搜索等典型场景为读者提供从理解到落地的完整参考。1. 多目标优化与这套 Python 代码包先看它解决什么问题从网上下载的《多目标优化 (python代码讲解视频).zip》并不新鲜新鲜的是它同时给了能跑的代码和一段把代码讲明白的视频。多目标优化处理的是两个目标互相打架的场景——买车时动力与油耗、调度时耗时与成本、模型调参时精度与延迟任何一个单值都说不清“最好”长什么样。它输出的是互相不占优的一整条解集而不是某一个大盘股式的唯一答案。这个包适合正想把单目标程序改成多目标版、或者需要一份能复现 NSGA-II 的 python 代码的人。拿到包后先别急着解压运行我建议先弄清楚三件事里面跑的是 NSGA-II 还是 MOEA/D目标函数用的哪个测试问题以及视频与代码是否逐段对得上。2. 非支配排序与拥挤度判别多目标代码质量的第一个分水岭多目标优化与单目标最大的区别是“比大小”的规则变了。单目标里 3 比 5 好直接排序就行多目标里一个解在一个目标上更优在另一个目标上却更差这时候不能说谁绝对好只能说谁支配谁。对最小化问题而言解 A 支配解 B当且仅当 A 在全部目标上都不比 B 差且至少在一个目标上严格优于 B。所有不被任何解支配的解构成了这个问题的非支配前沿也就是常说的 Pareto 前沿。这份 zip 里的核心代码可能只有两三百行但要判断它值不值得看先看它有没有处理这种“部分序关系”。如果代码连支配判断都没有那它大概率只是用了加权求和改装出来的伪多目标后文会讲它为什么不好用。反之如果它实现了非支配排序和拥挤度距离哪怕画图丑一点算法骨架也是对的值得继续读。2.1 为什么多目标不能直接当单目标做加权求和的三个坑加权求和是新手最容易想到的转化方式把两个目标乘上权重加一起然后交给单目标优化器。这个思路有实际问题不是数学上严格不行而是工程上难用。第一权重本身就是一种偏好。业务方说“延迟和成本一样重要”时他并不知道这句话在数值上意味着 0.5 和 0.5还是 0.1 和 0.9。第二不同目标的量纲差太多时权重会被淹没。比如延迟是毫秒级、成本是万元级加权后延迟那个目标几乎不起作用。第三加权求和只能得到凸前沿上的点遇到凹的 Pareto 前沿段无论怎么调权重都扫不到那一块。也就是说你花了很多力气调权重最后得到的解集可能是残缺的。NSGA-II 一类方法绕开了这个问题。它不做加权而是用非支配排序把种群分成多层再在每层内用拥挤度保证解不扎堆。这样目标函数只需要各自返回数值不需要你决定“哪个更重要”偏好可以在算完之后再选。这也是鉴别的第一道分水岭代码里有没有一部分逻辑在做层级划分。2.2 支配关系判断看懂 8 行代码就抓住了这份 zip 的核心不管 zip 里的代码封装得怎么样只要它声称实现了 NSGA-II就一定有一个函数在做支配判断。通常只有几行但整个排序都建立在它上面。import numpy as np def dominates(a, b): a 是否支配 b。默认两个目标均为最小化。 返回 True 说明 a 在所有目标上不劣于 b且至少一个目标严格更优。 a、b 可以是长度为 M 的目标值列表或 ndarray。 a np.asarray(a, dtypefloat) b np.asarray(b, dtypefloat) return np.all(a b) and np.any(a b)这段的逻辑很好对照先做一次逐目标比较np.all(a b)保证没有哪一项比 b 差np.any(a b)保证确实有一项比 b 好。两个条件同时满足才叫支配否则就是不相关解两者都留在前沿上。我用这个函数时有个习惯目标函数在返回前先做归一化而不是在dominates里做。因为归一化本身也是一种偏好处理放在支配判断里会让代码使用者搞不清实际优化的是什么。视频里如果花了五分钟讲这一小段那后面大概率是认真讲原理的如果只是把代码念一遍建议你自己拿测试问题跑一跑。2.3 拥挤度距离解在前沿上铺开的关键非支配排序把解分成了好几层但同一层里的解也可能挤在一起。比如种群有 100 个解Pareto 前沿上可能只有 3 个不同的位置反而不如均匀分布的 50 个解有用。拥挤度距离就是为了解决这个分布性问题。计算方式是对某个非支配层的解先按第一个目标排序边界上的两个解拥挤度直接设为无穷大保证它们一定被保留中间的每个解用相邻两个解在该目标上的差除以这一层的目标值跨度得到一个归一化距离再对每个目标累加。公式写出来是这样的crowd_i (f_{i1,1} - f_{i-1,1}) / (f_1_max - f_1_min) (f_{i1,2} - f_{i-1,2}) / (f_2_max - f_2_min)这个值越大说明这个解周围越空旷。在选择下一代时NSGA-II 的策略是“先比层级层级相同比拥挤度”。层级越小越好因为更接近真实前沿拥挤度越大越好因为它能撑开前沿的两端和中间空隙。如果一份代码把拥挤度算成了越小越好或者忘了给边界解赋无穷大跑出来的前沿大概率会是一长条中间密、两头空的形状。这个问题放到第 5 章再展开排查。3. 把 NSGA-II 跑起来从文件入口到种群主循环这一章解决“拿到 zip 之后怎么让它先跑起来”的问题。先别纠结算法细节第一步是让脚本在本地输出一张能看的前沿散点图。有了这张图再谈改目标和调参数。3.1 解压后先找两样东西主函数入口与依赖环境解压之后先列一下目录看看哪些是.py哪些是视频文件。常见打包习惯是一个主脚本加几个工具模块比如nsga2.py、test_function.py、plot_result.py。不要从第一个文件开始读而是先用编辑器打开每个文件搜索if __name__ __main__那是程序入口。入口里通常写着测试函数名、种群大小、迭代代数以及最后的画图调用。环境方面这个包大概率只需要numpy和matplotlib。如果机器上还没有 python 环境从 python 官网装一个 3.8 以上的版本然后执行pip install numpy matplotlib装完先跑一次主脚本确认能出图再继续。视频的建议观看方式和这个顺序一致先跳到主循环那段看作者怎么把排序、交叉、变异串起来不用从头看“什么是 Pareto”这类铺垫。很多讲解视频本质上是把示例代码逐段讲一遍你代码已经跑通了再看视频吸收率会高很多。3.2 一个最小可跑的 NSGA-II 骨架排序、交叉、变异的完整实现下面给一份我常用的 NSGA-II 最小骨架拿 ZDT1 测试函数做演示。如果你手里的 zip 里代码结构比较复杂对照这个骨架找对应函数就行。先看非支配排序和拥挤度import random import numpy as np def dominates(a, b): a np.asarray(a, dtypefloat) b np.asarray(b, dtypefloat) return np.all(a b) and np.any(a b) def non_dominated_sort(pop): 快速非支配排序返回分层后的解集合并给每个解写入 rank 属性。 n len(pop) S [[] for _ in range(n)] # 被 i 支配的解下标 nd [0] * n # 支配 i 的解个数 fronts [[]] for i in range(n): for j in range(n): if i j: continue if dominates(pop[i][f], pop[j][f]): S[i].append(j) elif dominates(pop[j][f], pop[i][f]): nd[i] 1 if nd[i] 0: fronts[0].append(i) k 0 while fronts[k]: cur [] for i in fronts[k]: for j in S[i]: nd[j] - 1 if nd[j] 0: cur.append(j) k 1 fronts.append(cur) fronts.pop() # 末尾的空列表删掉 result [] for idx, f in enumerate(fronts): for i in f: pop[i][rank] idx result.append([pop[i] for i in f]) return result def crowding_distance(front): 为单个非支配层计算拥挤度边界解赋无穷大。 if len(front) 0: return m len(front[0][f]) for ind in front: ind[crowding] 0.0 for obj in range(m): front.sort(keylambda ind: ind[f][obj]) front[0][crowding] float(inf) front[-1][crowding] float(inf) f_min front[0][f][obj] f_max front[-1][f][obj] if f_max f_min: continue for i in range(1, len(front) - 1): front[i][crowding] ( front[i 1][f][obj] - front[i - 1][f][obj] ) / (f_max - f_min)这段里有几个实现细节值得对着自己的代码检查。S[i]存的是被 i 支配的解nd[i]存的是支配 i 的解数量两者方向相反写反了排序会直接崩溃。拥挤度里分母用了这一层当前目标的极差所以每个目标的贡献天然被缩放到 0 到 1 之间不同量纲的目标可以直接相加。边界解赋无穷大不是可选项不赋的话两端会被误删最后折中解会把前端“拉”成一小团。接下来是选择、交叉和变异然后拼成主循环def tournament_select(pop, k2): 锦标赛选择层级小优先层级相同拥挤度大优先。 cand random.sample(pop, k) cand.sort(keylambda ind: (ind[rank], -ind[crowding])) return cand[0] def sbx_crossover(x1, x2, pc0.9, eta_c15): 模拟二进制交叉 SBX交叉后子代强制夹在 [0,1] 范围内。 x1 np.asarray(x1, dtypefloat).copy() x2 np.asarray(x2, dtypefloat).copy() if random.random() pc: return x1, x2 c1, c2 x1.copy(), x2.copy() for i in range(len(x1)): if random.random() 0.5: if abs(x1[i] - x2[i]) 1e-10: u random.random() if u 0.5: beta (2.0 * u) ** (1.0 / (eta_c 1)) else: beta (1.0 / (2.0 * (1.0 - u))) ** (1.0 / (eta_c 1)) c1[i] 0.5 * ((1 beta) * x1[i] (1 - beta) * x2[i]) c2[i] 0.5 * ((1 - beta) * x1[i] (1 beta) * x2[i]) c1[i] min(1.0, max(0.0, c1[i])) c2[i] min(1.0, max(0.0, c2[i])) return c1, c2 def polynomial_mutation(x, pm0.1, eta_m20): 多项式变异pm 是单个基因位的变异概率。 x np.asarray(x, dtypefloat).copy() for i in range(len(x)): if random.random() pm: u random.random() if u 0.5: delta (2.0 * u) ** (1.0 / (eta_m 1)) - 1.0 else: delta 1.0 - (2.0 * (1.0 - u)) ** (1.0 / (eta_m 1)) x[i] delta x[i] min(1.0, max(0.0, x[i])) return x def zdt1(x): ZDT1 测试函数30 维决策变量两个目标最小化。 x np.asarray(x, dtypefloat) f1 x[0] g 1 9 * np.sum(x[1:]) / (len(x) - 1) f2 g * (1 - np.sqrt(f1 / g)) return np.array([f1, f2]) def nsga2(pop_size100, n_var30, max_gen250, pc0.9, pm0.1, eta_c15, eta_m20): pop [] for _ in range(pop_size): x np.random.rand(n_var) pop.append({x: x, f: zdt1(x)}) for _ in range(max_gen): fronts non_dominated_sort(pop) for f in fronts: crowding_distance(f) offspring [] while len(offspring) pop_size: p1 tournament_select(pop) p2 tournament_select(pop) c1, c2 sbx_crossover(p1[x], p2[x], pc, eta_c) c1 polynomial_mutation(c1, pm, eta_m) c2 polynomial_mutation(c2, pm, eta_m) offspring.append({x: c1, f: zdt1(c1)}) offspring.append({x: c2, f: zdt1(c2)}) offspring offspring[:pop_size] combined pop offspring fronts non_dominated_sort(combined) next_pop [] for f in fronts: if len(next_pop) len(f) pop_size: next_pop.extend(f) else: crowding_distance(f) f.sort(keylambda ind: ind[crowding], reverseTrue) next_pop.extend(f[:pop_size - len(next_pop)]) break pop next_pop return pop这段代码的主流程是“排序选亲本 → 生成子代 → 合并 → 环境选择”每代做一次。环境选择是 NSGA-II 的关键父代和子代合并成 2 倍大小的种群按层级从低到高往里收收不下时对当前层按拥挤度排序取拥挤度大的填满。这就是所谓的精英保留保证好解不丢。三个参数在跑之前就要想清楚。pop_size决定前沿上点的密度ZDT1 这种两目标问题 100 个点够用提到 200 会更平滑但每代耗时接近翻倍。max_gen决定收敛程度ZDT1 一般 200 代左右前沿就基本成型不要一上来设 1000。pm是变异概率0.05 到 0.2 之间比较稳太小容易丢多样性太大后期前沿会一直抖动这是个玄学参数后面避坑章有具体排查方向。3.3 七个必调参数与它们的方向骨架代码里一共有七个参数直接影响结果列表整理如下方便对着调。参数含义常见取值调高会怎样调低会怎样pop_size种群规模100~300前沿更密每代变慢前沿稀疏容易丢两端max_gen最大代数200~500更接近收敛耗时更长可能没跑够就停pc交叉概率0.8~1.0后代更接近父本组合种群更像克隆体pm变异概率0.05~0.2多样性好后期易抖收敛快易早熟eta_c交叉分布指数10~20子代更接近父本子代离父本更远eta_m变异分布指数15~25变异步幅更小变异步幅更大n_var决策变量维度问题决定搜索空间变大搜索空间变小调参方向记住一句话跑出来的前沿点分布有问题先动 pop_size 和 pm收敛不够先动 max_gen结果对初始化特别敏感先动 pm 而不是 pc。交叉分布指数 eta_c 调到 20 以上时子代几乎就是父本的轻微扰动适合后期精细搜索调小到 5 左右交叉产生的子代会明显跳出父本区域适合前期探索。视频里如果对这几个参数的演示只有一句话带过多数情况下可以在自己的问题上跑一个小网格来验证不必迷信默认值。4. 把测试问题换成自己的优化目标函数改写与验证跑通 ZDT1 只是第一步真正要解决自己的问题需要把目标函数、决策变量边界和约束换成实际的。这一章给一个可以直接替换的模板并讲清楚替换后怎么确认算法没有写错。4.1 三个需要改的位置目标函数、变量边界、约束NSGA-II 不需要目标函数可导也不需要知道目标函数的内部结构它只关心“输入一组决策变量输出一组目标值”。这就意味着目标函数是一个黑匣子都可以只要输入输出接口稳定。改的时候只需要动三个位置。第一个位置是决策变量映射。骨架里x是 0 到 1 之间的实数值但实际问题里参数往往有自己的范围和类型比如批量大小必须是整数、学习率应该在对数尺度上搜索。建议在目标函数内部做反归一化而不是直接改主循环里的采样逻辑这样主循环保持通用。import numpy as np def my_problem(x): 把 NSGA-II 的决策变量 x 映射到真实参数空间返回两个目标。 x np.asarray(x, dtypefloat) # 参数1batch size16 到 128 之间的整数 batch_size int(16 x[0] * (128 - 16)) # 参数2学习率0.001 到 1.0 的对数尺度 learning_rate 0.001 * (10 ** (x[1] * 3)) # 参数3dropout0.1 到 0.6 dropout 0.1 x[2] * 0.5 # 这里替换成你的真实计算逻辑 val_loss 0.3 0.02 * dropout - 0.01 * x[2] * (x[1] * 2 - 0.5) train_time 240.0 / (batch_size / 32.0) return np.array([val_loss, train_time])第二个位置是决策变量维度。如果问题只有三个参数就把n_var改成 3。第三个位置是约束。骨架里没有约束处理如果某个参数组合不合法最简单的做法是在目标函数里返回一个很大的目标值比如[1e9, 1e9]让优化器自动避开。这种做法叫罚函数注意罚值要远远大于正常目标值的量级否则约束形同虚设。第 5 章会有专门排查。这里有个常见误解遗传算法看起来“不需要梯度”所以有人把离散组合优化也硬塞进来。如果决策变量是排列型问题比如路径顺序用这种固定维度的向量编码并不合适需要改成排列编码并设计对应的交叉算子。这份 zip 里的代码如果只支持向量编码就只适合连续参数调优别硬用它做 TSP。4.2 用 ZDT1 的已知解析前沿验证算法是否实现正确换了目标函数之后代码万一跑坏了你很难判断是目标函数写错还是算法写错。所以验证多目标代码有一个常用做法先在测试函数上跑一遍确认前沿形状与解析解对齐再换自己的问题。ZDT1 的好处是它的真实 Pareto 前沿有解析表达式。当 g 等于 1 时f2 1 - sqrt(f1)f1 的取值范围是 0 到 1。把上一次运行的种群拿出来把 rank 为 0 的解画出来和这条曲线叠一起看import matplotlib.pyplot as plt import numpy as np # 运行主循环取最终种群 pop nsga2(pop_size200, max_gen300) front [ind for ind in pop if ind[rank] 0] front.sort(keylambda ind: ind[f][0]) # 算法得到的近似前沿 xs [ind[f][0] for ind in front] ys [ind[f][1] for ind in front] plt.scatter(xs, ys, s8, labelNSGA-II) # ZDT1 解析前沿 xx np.linspace(0, 1, 200) plt.plot(xx, 1 - np.sqrt(xx), r--, labelTrue PF) plt.xlabel(f1) plt.ylabel(f2) plt.legend() plt.show()验证标准有三条。第一散点是否贴着红色虚线而不是明显偏向一侧。第二散点是否从 f10 一直延伸到 f11两端有没有明显缺口。第三层内是否有大量解堆积在中间段。如果三条都过得去说明排序、选择、交叉、变异这段逻辑基本正确可以放心替换目标函数。如果你的 zip 里用的是 DTLZ 系列测试函数验证方式要换成理论前沿的解析式。比如 DTLZ2 的真实前沿是单位球面在第一象限的曲面画出来是一个四分之一圆弧检查方式相同算法得到的点应该均匀分布在弧面上且没有点落在弧面内侧。这类函数的好处是目标数量可以扩展到三个以上但可视化会麻烦一些通常用三维散点图或者只投影两维来看。4.3 算法包之外多目标在超参寻优与量化策略中的应用把测试函数换成实际问题之后这套代码能做的应用场景比视频里展示的更广。我最常见的两个方向一个是机器学习超参寻优另一个是量化策略参数搜索。超参寻优的例子在上面已经写过了两个目标可以设成验证集损失和训练耗时也可以设成精度和模型大小。这类问题的特点是参数变量少则 3 个多则十几个NSGA-II 比网格搜索明显省时间因为每一代都在上一代的基础上做局部探索和全局探索的平衡。量化策略搜索用多目标也很自然。收益和最大回撤天然冲突单目标做收益最大化容易拿高风险换收益做回撤最小化又学不到趋势。把两个目标同时优化得到的不是一个“最优点”而是一条收益-回撤的可选带。实际生产中再根据资金规模、风险预算从前沿上挑一个解做参数落地。如果你原本在找 python 量化交易策略代码多目标优化包正好可以配合回测脚本。不过要注意NSGA-II 在前沿形状复杂的参数空间里效果好但如果参数只有两三个、且你只需要一个可用的解直接跑贝叶斯优化更省事。5. 多目标优化常见问题排查5 个翻车现场与解法骨架能跑换了自己的问题就会开始出各种状况。以下五条是我跑多目标代码包里最常见的翻车现场按“现象 → 原因 → 解决”的格式来写每一条都能对上具体的代码位置。5.1 前沿只集中在中间段两端全是稀疏点现象散点图看起来是一条肥短的曲线f1 小于 0.2 和大于 0.8 的区域几乎没有解。原因两种可能。一是变异概率pm太低种群收敛太快初始种群里落在两端的个体被淘汰后靠变异很难再补回来。二是拥挤度计算里把边界解的无穷大值漏了导致每次环境选择都把两端比较“孤独”的解当成拥挤度小的解优先淘汰越淘汰越缩向中间。解决先把pm从 0.05 提到 0.15 再跑一次。如果两端还是不出现检查crowding_distance里front[0][crowding] float(inf)和front[-1][crowding] float(inf)这两行是否存在。边界解保不住是分布性问题里排名第一的坑十有八九都是这里写漏了。5.2 加上约束之后一半解落在不可行域现象换了自己的目标函数并在里面加了约束判断结果大量不满足约束的解仍然出现在最终前沿上。原因罚函数罚得太轻。假设正常目标值在 0.1 到 1.0 之间你给不可行解返回的目标值是[1.0, 1.0]这个惩罚力度和正常解没有拉开差距非支配排序会把这些非可行解和可行解混在同一层。解决把罚值改成[1e5, 1e5]这种量级确保不可行解在第一轮非支配排序里就被排到最后一层。更稳妥的做法是采用带约束处理的非支配排序先比较约束违反程度违反程度小的优先都可行时再走支配关系。这个改法在代码里要多写一个约束函数但效果比罚函数稳定得多。5.3 每次跑出的前沿都不一样复现不了自己的结果现象同样一份代码、同样一组参数连续跑三次得到三张不同的前沿图论文里的效果图在自己机器上始终对不上。原因NSGA-II 是随机算法初始化种群、交叉点、变异位全部依赖随机数。没有固定随机种子每次运行都是不同的搜索轨迹。解决在 import 之后第一行加上random.seed(42) np.random.seed(42)固定住全局随机数。注意这两行必须在任何随机操作之前执行否则还是会漂。固定种子不能提升算法效果但能让调试变得可复现这是所有后续调参的前提。5.4 代码包在别的机器上跑出 IndentationError现象从 zip 里解压出的.py文件在本地打开没问题换到另一台机器或者另一个编辑器里运行直接报IndentationError: unexpected indent。原因多数是文件里混用了 Tab 和空格或者作者用了一个你当前环境不认识的非标准缩进。这个问题在开源 zip 包里出现频率很高尤其是经过网盘转存、Windows 与 Linux 之间多次拷贝后容易触发。解决先用命令检查缩进问题python -m tabnanny nsga2.pytabnanny会明确报告哪一行缩进有问题。修复方法是用 VS Code 这类编辑器选中全部代码执行“重新缩进”并打开“转换缩进为空格”的选项。检查代码规范这件事做完再跑省得被玄学报错浪费一下午。5.5 两个目标量级差 1000 倍支配关系直接失真现象两个目标分别叫 f1 和 f2结果前沿图上 f1 已经分辨出很多梯度f2 看起来像一条几乎水平的噪声带。原因目标函数的量级不一致。假设 f1 在 0.1 到 0.2 范围内波动f2 在 10000 到 20000 之间波动非支配排序里np.all(a b)比较的是原始值f1 那 0.1 的差别相对 10000 的量级完全不敏感真正决定排序关系的只有 f2。小目标就变得形同虚设。解决在目标函数返回前做 min-max 归一化或者除以一个典型量级值。归一化之后两个目标都落在相近范围内支配判断才有意义。raw np.array([val_loss, train_time]) scale np.array([1.0, 240.0]) # 各自目标的典型量级 return raw / scale注意归一化不是把两个目标强行变成“同等重要”它只是让数值比较不因量纲失真。业务上哪个目标更重要是在前沿出来之后用偏好去选解不要在归一化之前就偷偷加权。6. 用 IGD 与超体积验证算法改进一个能直接跑的评价脚本6.1 IGD 与超体积分别在什么场合用前沿图画出来只能肉眼判断不能作为改进算法的依据。评价多目标算法常用两个指标IGD反向世代距离和超体积。IGD 衡量近似前沿与真实前沿的接近程度需要知道真实前沿的参考点适合在测试函数上有解析前沿的场合超体积衡量算法得到的前沿在目标空间覆盖的体积不需要真实前沿只需要一个参考点适合评估自己改过的问题。IGD 越小越好超体积越大越好。6.2 IGD 计算脚本与收敛判断下面这个脚本可以直接接在 NSGA-II 主循环后面用来算 ZDT1 的 IGDdef igd(approx, true_front): IGD对真实前沿上每个参考点找近似前沿最近点的距离取平均。 approx np.asarray(approx, dtypefloat) true_front np.asarray(true_front, dtypefloat) if approx.ndim 1: approx approx.reshape(1, -1) return float(np.mean([ np.min(np.linalg.norm(approx - p, axis1)) for p in true_front ])) # ZDT1 解析前沿作为参考点 t np.linspace(0, 1, 500) true_front np.column_stack([t, 1 - np.sqrt(t)]) # 用第 3 章的 nsga2 跑一组结果 pop nsga2(pop_size100, max_gen250) approx np.array([ind[f] for ind in pop if ind[rank] 0]) print(IGD , igd(approx, true_front))使用 IGD 的正确姿势是在每一代结束后都算一次画出 IGD 随迭代次数的下降曲线。曲线从高到低快速下降然后趋于平缓平缓的开始点就是收敛点。如果你发现 max_gen500 的曲线在第 150 代就平了后面 350 代都在原地抖动说明代数设多了调到 200 就够。IGD 下降过程中若出现明显反弹通常说明变异概率太大已经收敛的种群被暴力扰动破坏。这时候把pm从 0.15 降到 0.08 再看曲线。6.3 我的调参习惯先看 IGD 曲线再谈散点图我现在的习惯是任何多目标代码拿到手先不急着看前沿散点先改出 IGD 曲线。这个办法帮我省过不少机时。之前跑一个三层目标的调度问题一直以为 500 代才收敛后来把 IGD 和超体积算出来才发现 200 代就已经稳定剩下的时间都在浪费。多目标优化的调参很多时候像个黑匣子散点图给不了你收敛信息IGD 曲线才是那个能定位“到底跑没跑够”的工具。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进