ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手撕NSGA-Ⅱ:Python从零实现非支配排序与拥挤距离

2026/9/13 1:58:24 拓冰建站 浏览量
手撕NSGA-Ⅱ:Python从零实现非支配排序与拥挤距离 简介本资源是面向高校智能优化课程设计与多目标优化算法学习者的实践项目包聚焦NSGA-II算法原理实现与CEC-2021国际竞赛问题求解。资源完整复现了非支配排序、拥挤距离计算、精英保留策略等核心机制适用于算法课设、进化计算课程实验及竞赛备赛场景。压缩包共185个文件含12个Python源码涵盖主程序、算法核心、CEC-2021问题定义与适应度计算、100个MATLAB结果数据文件记录各代种群演化过程、50个TXT日志与参数配置、11个PNG可视化图谱Pareto前沿收敛曲线等整体仅969KB轻量但结构完备。已有125人下载学习读者可直接运行调试、对比不同代际的Pareto解集、分析HV指标变化趋势并基于预置的mat与txt结果快速开展性能复现与算法改进实验。1. CUG智能优化课设不是调包跑个结果而是用Python把NSGA-Ⅱ的每一步“手撕”进CEC-2021测试函数里中国地质大学CUG智能优化课程设计中“Python实现NSGA-Ⅱ算法解决CEC-2021竞赛问题”这个标题背后藏着一个常被低估的硬核事实它不是用pymoo一行algorithm.run(problem)就交差的作业而是要求学生从零构建非支配排序、拥挤距离计算、模拟二进制交叉SBX与多项式变异PM三大核心模块并在CEC-2021提供的8个真实感极强的多目标测试函数如F1旋转偏移的ZDT变体、F5带复杂约束的多峰函数上完成收敛性与分布性双重验证。这类课设面向的是已掌握Python基础语法、NumPy向量化操作和基本优化概念的大三以上学生——你得能看懂crowding_distance.py里那个嵌套三层for循环为什么必须用argsort重写才能不超时也得明白CEC-2021的get_true_pareto_front()返回的参考前沿为何不能直接用于IGD计算而需先做归一化。它考的不是“会不会用框架”而是“能不能把教科书公式翻译成可调试、可断点、可替换算子的生产级Python代码”。2. 从零构建NSGA-Ⅱ非支配排序与拥挤距离的Python实现细节NSGA-Ⅱ的骨架由非支配排序Non-dominated Sorting和拥挤距离Crowding Distance共同支撑。很多初学者直接调用pymoo的NonDominatedSorting类但在CUG课设中这恰恰是扣分点——你需要亲手写出fast_non_dominated_sort函数并理解其O(MN²)时间复杂度在CEC-2021高维问题如F7的100维决策变量下的实际开销。2.1 非支配排序避免嵌套循环的向量化重构原始伪代码中常见的双层for循环对每个个体i遍历所有j判断支配关系在Python中极易成为性能瓶颈。CUG课设推荐采用NumPy广播机制重构import numpy as np def fast_non_dominated_sort(pop_obj): pop_obj: (N, M) array, N individuals, M objectives Returns: list of fronts, each front is array of indices N, M pop_obj.shape fronts [] dominated_counts np.zeros(N, dtypeint) # i被多少个解支配 dominated_solutions [[] for _ in range(N)] # i支配哪些解 # 向量化支配关系计算避免显式双重循环 for i in range(N): # 对每个i计算pop_obj[i]是否支配pop_obj[j]j≠i # 支配条件对所有目标kf_i[k] f_j[k]且存在至少一个k使f_i[k] f_j[k] less_equal pop_obj[i] pop_obj # (N, M), True if f_i[k] f_j[k] strictly_less pop_obj[i] pop_obj # (N, M) # 每行j满足所有目标都不大于f_i且至少一个目标严格小于 is_dominated np.all(less_equal, axis1) np.any(strictly_less, axis1) is_dominated[i] False # 自身不支配自身 dominated_counts[i] np.sum(is_dominated) # i被多少个解支配 dominated_solutions[i] np.where(is_dominated)[0].tolist() # 开始分层第一层是不被任何解支配的个体 current_front np.where(dominated_counts 0)[0] front_idx 0 while len(current_front) 0: fronts.append(current_front.copy()) next_dominated [] for i in current_front: for j in dominated_solutions[i]: dominated_counts[j] - 1 if dominated_counts[j] 0: next_dominated.append(j) current_front np.array(next_dominated, dtypeint) front_idx 1 return fronts注意上述实现虽用向量化减少外层循环但内层仍需遍历dominated_solutions[i]。对于CEC-2021的F310目标或F83目标但100维当种群规模N100时dominated_counts更新部分仍为O(N²)这是NSGA-Ⅱ理论复杂度决定的。课设评分时会检查你是否理解这一限制并在报告中说明——例如指出“当N200时建议改用pymoo的Cython加速版作为对比基线”。2.2 拥挤距离坐标归一化与边界处理的关键参数拥挤距离计算直接影响解集在Pareto前沿上的分布均匀性。CEC-2021的测试函数如F1目标值范围差异极大f1∈[0,1],f2∈[0,1000]若不做归一化f2的微小变化就会主导距离计算导致f1方向严重稀疏。def calculate_crowding_distance(pop_obj, front_indices): pop_obj: (N, M) objective matrix front_indices: indices of individuals in current front Returns: (len(front_indices),) array of crowding distances M pop_obj.shape[1] distances np.zeros(len(front_indices)) # 对每个目标维度单独计算 for m in range(M): # 提取当前前沿在第m个目标上的值并获取排序索引 obj_vals pop_obj[front_indices, m] sorted_idx np.argsort(obj_vals) sorted_indices front_indices[sorted_idx] # 原始种群索引 # 边界个体距离设为无穷大实际用最大值替代 distances[sorted_idx[0]] np.inf distances[sorted_idx[-1]] np.inf # 中间个体距离 (右邻值 - 左邻值) / (max - min) if len(sorted_idx) 2: # 归一化分母避免除零加1e-8防浮点误差 range_m np.max(obj_vals) - np.min(obj_vals) 1e-8 for k in range(1, len(sorted_idx)-1): left_val obj_vals[sorted_idx[k-1]] right_val obj_vals[sorted_idx[k1]] distances[sorted_idx[k]] (right_val - left_val) / range_m # 将inf替换为实际大数避免后续比较出错 distances[np.isinf(distances)] 1e10 return distances提示CEC-2021官方文档明确要求对F4-F8使用“目标值归一化到[0,1]区间后再计算拥挤距离”。你在calculate_crowding_distance调用前必须先对整个种群的目标矩阵执行# 假设已知各目标理论上下界CEC-2021提供 bounds np.array([[0, 1], [0, 1000], [-100, 100]]) # 示例3目标 norm_obj (pop_obj - bounds[:, 0]) / (bounds[:, 1] - bounds[:, 0] 1e-8)若未做此步在F6带旋转的DTLZ2变体上运行时你的解集会在f3方向严重坍缩——这是CUG课设答辩中最常被追问的错误点。3. CEC-2021测试函数接入从标准接口到边界约束的硬编码适配CEC-2021提供了一套标准化的多目标测试函数集但其Python实现并非开箱即用。CUG课设要求你手动下载cec2021.py官方MATLAB转译版并针对Python生态进行三处关键改造否则无法与NSGA-Ⅱ主循环兼容。3.1 函数签名统一强制输入为一维数组输出为一维目标向量官方CEC-2021的cec2021.py中F1(x)接受x为(D,)数组但部分函数如F5内部会调用np.reshape(x, (-1, D))导致传入x为(1, D)时出错。必须重写包装器# cec2021_wrapper.py from cec2021 import F1, F2, ..., F8 # 假设已正确导入 def get_cec2021_problem(func_id, n_dim10): func_id: int from 1 to 8 n_dim: decision variable dimension (CEC-2021 specifies: F1-F410, F5-F8100) Returns: callable function that takes (n_dim,) array - (M,) array # 映射func_id到具体函数和目标数M funcs {1: (F1, 2), 2: (F2, 2), 3: (F3, 2), 4: (F4, 3), 5: (F5, 2), 6: (F6, 3), 7: (F7, 2), 8: (F8, 3)} if func_id not in funcs: raise ValueError(fCEC-2021 only supports func_id 1-8, got {func_id}) cec_func, M funcs[func_id] def problem_func(x): # 强制x为一维长度为n_dim assert x.ndim 1 and len(x) n_dim, \ fx must be 1D array of length {n_dim}, got shape {x.shape} # CEC-2021函数要求x为列向量不官方Python版要求行向量 # 但某些函数内部有reshape故显式保证 x_reshaped x.reshape(1, -1) # (1, n_dim) try: # 调用CEC函数返回(M,) array obj_vals cec_func(x_reshaped).flatten() except Exception as e: # 捕获常见错误F5的约束检查失败 if func_id 5: # F5有显式约束sum(x_i^2) 100 if np.sum(x**2) 100: # 违反约束返回极大惩罚值 obj_vals np.full(M, 1e6) else: obj_vals cec_func(x_reshaped).flatten() else: raise e return obj_vals return problem_func, M # 使用示例 problem, n_obj get_cec2021_problem(func_id1, n_dim10) x_sample np.random.uniform(-100, 100, size10) f_vals problem(x_sample) # 返回(2,) array3.2 约束处理F5的显式约束必须在评估阶段硬编码CEC-2021的F5CEC2021_F5是唯一带显式等式/不等式约束的函数“∑xᵢ² ≤ 100”。NSGA-Ⅱ本身不处理约束因此必须在目标函数评估时注入惩罚项def cec2021_f5_with_penalty(x): F5 with constraint handling: sum(x_i^2) 100 Penalty: add 1e6 to all objectives if violated # 先计算原始目标 from cec2021 import F5 x_reshaped x.reshape(1, -1) f_raw F5(x_reshaped).flatten() # (2,) # 检查约束 constraint_violation np.sum(x**2) - 100 if constraint_violation 1e-6: # 容忍浮点误差 f_raw 1e6 # 重罚确保不可行解被快速淘汰 return f_raw关键参数表CEC-2021各函数核心特征CUG课设必记func_id名称决策变量维数目标数是否含约束主要难点推荐初始种群范围1Rotated ZDT1102否旋转导致Pareto前沿弯曲[-100, 100]5Constrained DTLZ11002是∑xᵢ²≤100约束区域狭窄易早熟[-10, 10]7Multi-modal DTLZ2102否多峰Pareto前沿需强探索[-1, 1]8Hybrid F81003否混合旋转偏移IGD计算敏感[-5, 5]4. SBX交叉与PM变异控制参数对CEC-2021收敛速度的实测影响NSGA-Ⅱ的进化算子质量直接决定其在CEC-2021上的表现。CUG课设要求你对比不同eta_cSBX分布指数和eta_mPM分布指数组合并用Hypervolume指标量化差异——这不是调参游戏而是理解算子行为的实验。4.1 SBX交叉eta_c越小搜索越激进但F5约束下易失效SBX模拟单点交叉的“类高斯”扰动eta_c控制扰动强度。eta_c2时子代集中在父代附近eta_c0.5时子代可能远离父代达2倍距离。在CEC-2021的F1平滑凸前沿上小eta_c加速收敛但在F5约束紧上eta_c1会导致大量子代违反∑xᵢ²≤100触发惩罚机制后种群退化。def sbx_crossover(parent1, parent2, eta_c2.0, prob0.9): Simulated Binary Crossover parent1, parent2: (D,) arrays Returns: two (D,) arrays if np.random.random() prob: return parent1.copy(), parent2.copy() child1, child2 np.copy(parent1), np.copy(parent2) for i in range(len(parent1)): if np.random.random() 0.5: if abs(parent1[i] - parent2[i]) 1e-14: x1, x2 min(parent1[i], parent2[i]), max(parent1[i], parent2[i]) # 计算beta_q u np.random.random() if u 0.5: beta_q (2*u)**(1.0/(eta_c1)) else: beta_q (1.0/(2*(1-u)))**(1.0/(eta_c1)) child1[i] 0.5 * ((x1x2) - beta_q*(x2-x1)) child2[i] 0.5 * ((x1x2) beta_q*(x2-x1)) # else: same value, no change return child1, child2实测结论CUG实验室数据在F1上eta_c0.5比eta_c2.0早12代达到HV0.95但在F5上eta_c0.5运行50代后可行解比例仅32%而eta_c2.0为89%。课设报告中必须呈现此对比曲线——横轴为代数纵轴为可行解率两条线交叉点即为参数失效阈值。4.2 多项式变异eta_m决定局部搜索粒度F7多峰问题需动态调整PM变异在单个变量上添加扰动eta_m越大扰动越集中于原值附近。CEC-2021的F7Multi-modal DTLZ2具有多个局部Pareto前沿固定eta_m易陷入局部最优。CUG推荐采用线性衰减策略def polynomial_mutation(x, eta_m_init20.0, eta_m_final5.0, prob1.0/len(x), gen0, max_gen500): Polynomial Mutation with linearly decreasing eta_m eta_m eta_m_init - (eta_m_init - eta_m_final) * (gen / max_gen) y np.copy(x) for i in range(len(x)): if np.random.random() prob: delta np.random.random() if delta 0.5: delta_q (2*delta)**(1.0/(eta_m1)) - 1 else: delta_q 1 - (2*(1-delta))**(1.0/(eta_m1)) y[i] delta_q * (x[i] - x[i]) # 此处应为范围需补充上下界 return y # 实际应用中需传入变量边界 def pm_with_bounds(x, xl, xu, eta_m20.0, prob1.0/len(x)): y np.copy(x) for i in range(len(x)): if np.random.random() prob: delta np.random.random() if delta 0.5: delta_q (2*delta)**(1.0/(eta_m1)) - 1 else: delta_q 1 - (2*(1-delta))**(1.0/(eta_m1)) y[i] delta_q * (xu[i] - xl[i]) y[i] np.clip(y[i], xl[i], xu[i]) # 强制在边界内 return y5. 性能验证用HV、IGD、SP指标量化NSGA-Ⅱ在CEC-2021上的真实表现CUG课设最终验收不看“算法跑通”而看“指标达标”。你必须用pymoo的get_performance_indicator模块计算三个核心指标并与CEC-2021官方提供的真实Pareto前沿True Pareto Front, TPF对比。这里没有捷径——TPF文件需从CEC官网下载且必须按函数ID匹配。5.1 HypervolumeHV唯一需要参考点的绝对指标HV衡量解集覆盖的“体积”但其值高度依赖参考点reference point选择。CEC-2021规定对F1-F4参考点为[1.1, 1.1]2目标或[1.1, 1.1, 1.1]3目标对F5-F8参考点为[1.1*max_f1, 1.1*max_f2, ...]其中max_f取自TPF。from pymoo.indicators.hv import HV from pymoo.problems import get_problem # 加载CEC-2021 F1的TPF假设已下载为f1_tpf.npy tpf_f1 np.load(cec2021_tpf/F1_tpf.npy) # shape (N_ref, 2) # 参考点取TPF中各目标最大值的1.1倍 ref_point np.max(tpf_f1, axis0) * 1.1 # 计算你的解集HV your_pareto np.array([...]) # 从NSGA-Ⅱ最后一层front提取 hv_indicator HV(ref_pointref_point) hv_value hv_indicator(your_pareto) print(fF1 HV {hv_value:.6f} (CEC-2021 baseline: 0.9821))注意若你的your_pareto包含重复解或非Pareto解HV计算会出错。务必先用fast_non_dominated_sort提取纯Pareto前沿fronts fast_non_dominated_sort(your_objectives) pareto_mask np.zeros(len(your_objectives), dtypebool) pareto_mask[fronts[0]] True # 第一层即Pareto前沿 your_pareto your_objectives[pareto_mask]5.2 IGD与SP分布性与收敛性的双刃剑IGDInverted Generational Distance反映解集对TPF的逼近程度SPSpacing衡量解间均匀性。二者需联合解读IGD低但SP高说明解集收敛但分布不均如全挤在前沿一端SP低但IGD高说明分布均匀但整体偏移。指标计算公式CUG课设合格线F1常见陷阱IGD$\frac{1}{TPF}\sum_{x\in TPF}\min_{y\in S}|x-y|_2$SP$\frac{1}{S-1}\sum_{i1}^{from scipy.spatial.distance import cdist def calculate_igd(pareto_set, tpf, normTrue): pareto_set: (N, M) array tpf: (N_ref, M) array if norm: # 归一化用TPF的min/max缩放到[0,1] tpf_min np.min(tpf, axis0) tpf_max np.max(tpf, axis0) range_tp tpf_max - tpf_min 1e-8 pareto_norm (pareto_set - tpf_min) / range_tp tpf_norm (tpf - tpf_min) / range_tp else: pareto_norm, tpf_norm pareto_set, tpf # 计算TPF中每个点到pareto_set的最近距离 dist_matrix cdist(tpf_norm, pareto_norm, metriceuclidean) min_distances np.min(dist_matrix, axis1) return np.mean(min_distances) # 使用示例 igd_f1 calculate_igd(your_pareto, tpf_f1) # 输出如 0.0123CUG课设硬性要求在F1、F5、F7三个函数上HV≥0.95、IGD≤0.015、SP≤0.08且运行时间Intel i7-10870H, 32GB RAM不超过180秒种群规模100代数500。若任一指标不达标需在报告中分析原因——例如指出“F5的约束处理过于粗暴建议改用ε约束法替代惩罚项”。本文还有配套的精品资源点击获取