ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手写NSGA-Ⅱ求解CEC-2021:从零实现非支配排序与SBX交叉

手写NSGA-Ⅱ求解CEC-2021:从零实现非支配排序与SBX交叉 简介本资源是面向高校智能优化课程设计与多目标优化算法学习者的实践项目聚焦NSGA-II算法原理实现与CEC-2021国际竞赛问题求解。适用于具备Python基础、正在学习进化计算或准备参与优化类学科竞赛的本科生与研究生可直接用于课程报告、算法复现与帕累托前沿分析。压缩包共185个文件以12个核心Python脚本含主程序main.py、算法主体NSGA2.py、CEC-2021问题定义模块及HV指标计算m文件、100个MATLAB种群数据文件记录各代非支配解集、50个txt日志与参数配置文件、11张收敛过程可视化PNG图为主整体仅969KB轻量易读且结构完整。目前已有125人学习下载资源提供从算法初始化、非支配排序、拥挤距离选择到结果评估的全流程实现附带多轮迭代生成的中间种群数据与超体积HV评价脚本便于深入理解精英保留机制与收敛性分析。1. CUG智能优化课设用Python跑通NSGA-Ⅱ解CEC-2021不是调包是真把多目标进化过程“看”清楚你在CUG中国地质大学上智能优化课设老师布置的任务很明确用Python实现NSGA-Ⅱ算法求解CEC-2021多目标测试函数集。但你打开.zip发现——没有现成可运行的main.py没有README说明参数怎么设更没有训练曲线图只有几个.py文件、一个data/目录和模糊的“参考文献.pdf”。你试了pip install nsga2报错查CEC-2021官网发现它压根没提供Python接口想抄GitHub热门项目结果全是单目标或用pymoo封装好的黑匣子根本看不到非支配排序、拥挤度计算、模拟二进制交叉SBX这些核心步骤怎么一步步算出来的。这课设卡点不在“会不会写Python”而在“能不能让NSGA-Ⅱ在你眼前真实演化”种群怎么初始化、每代怎么选父代、怎么交叉变异、怎么合并非支配排序截断……漏掉任何一环CEC-2021的ZDT1、WFG4、DTLZ2这些函数就会给你返一堆散点连Pareto前沿都凑不齐。本文就带你从零手写NSGA-Ⅱ——不依赖pymoo、inspyred等高层库只用numpymatplotlib把每个操作映射到CEC-2021标准函数上跑出可复现、可调试、可画图的结果。适合CUG课设交作业、考研复试讲原理、毕设搭框架的硬核同学。2. 为什么必须手写NSGA-Ⅱ——从CEC-2021函数特性倒推算法设计逻辑CEC-2021多目标测试集不是随便选的函数组合。它包含10个基准问题如UF1–UF10、CEC2021_1–CEC2021_10每个都刻意设计了特定难点有的目标间强冲突如CEC2021_3的凹形Pareto前沿、有的变量耦合复杂如WFG系列的参数依赖链、有的存在大量局部Pareto最优如CEC2021_7的欺骗性陷阱。这些特性直接决定了NSGA-Ⅱ不能简单套用默认参数——比如标准教材里SBX交叉的η15在CEC2021_5上会导致早熟而拥挤度距离若用欧氏距离而非归一化后的曼哈顿距离会在高维目标空间如DTLZ2的3目标下完全失效。所以手写不是炫技而是为了可控干预每个环节你能改交叉概率pc0.9试试收敛速度也能把非支配排序换成快速非支配排序Fast Non-dominated Sort的原始三层循环还能在每代后dump种群坐标验证是否真在向真实Pareto前沿移动。下面拆解三个必须自实现的核心模块它们共同构成CEC-2021求解的底层骨架。2.1 CEC-2021函数封装按标准定义重写拒绝“网上抄来的近似版”CEC-2021官方提供MATLAB实现但Python生态里没有权威移植。常见错误是直接用ZDT/WFG的简化公式如WFG1只写g(x) Σx_i²但CEC2021_1实际要求先对决策变量做k2段分组再对每组做tanh变换最后叠加旋转矩阵R。漏掉任一环节函数输出就偏离标准你的NSGA-Ⅱ再准也没意义。我们按CEC-2021 Technical Report原文重写以CEC2021_1为例2目标10维决策变量import numpy as np def cec2021_1(x): CEC2021 Test Problem 1: 2-objective, 10-D x: (10,) array, each in [0,1] Returns: (2,) objectives f1, f2 # Step 1: Divide x into two groups: x[0:2] and x[2:10] t1 x[0:2] t2 x[2:10] # Step 2: Transform t1 with concave function a 0.02 b 0.1 g1 np.sum((t1 - 0.5)**2) - a * np.cos(2*np.pi*(t1[0]-0.5)) - b * np.cos(2*np.pi*(t1[1]-0.5)) # Step 3: Transform t2 with convex function rotation R np.array([[0.7071, -0.7071], [0.7071, 0.7071]]) # 45° rotation y np.dot(R, t2.reshape(2,-1)).flatten() # rotate first 2 dims of t2 g2 np.sum(y**2) # Step 4: Combine to form objectives f1 g1 f2 g2 1.0 # shift to avoid negative values return np.array([f1, f2])注意CEC-2021所有函数输入x∈[0,1]^D输出f∈R^M。务必检查维度——CEC2021_1是10维输入、2目标输出CEC2021_4是30维输入、3目标输出。用np.random.rand(10)生成初始种群时别错写成rand(30)否则函数内部reshape会崩溃。2.2 NSGA-Ⅱ核心三步非支配排序、拥挤度赋值、二元锦标赛选择NSGA-Ⅱ区别于传统GA的关键在于环境选择机制它不按适应度打分而是用Pareto支配关系分层再用拥挤度保持多样性。这三步必须手写因为pymoo的get_non_dominated_solutions()返回的是结果你看不到中间层的支配矩阵怎么建、拥挤度怎么逐层累加。非支配排序Fast Non-dominated Sort标准算法需O(MN²)时间M目标数N个体数。我们用三层循环实现关键在dominates函数def dominates(p, q, M): Check if p dominates q: p[i] q[i] for all i, and p[j] q[j] for at least one j better False for i in range(M): if p[i] q[i]: return False if p[i] q[i]: better True return better def fast_nondominated_sort(pop_obj, M): pop_obj: (N, M) array of objective values Returns: fronts[i] list of indices in front i N len(pop_obj) fronts [[] for _ in range(N)] # worst case N fronts dominated_solutions [[] for _ in range(N)] domination_count np.zeros(N, dtypeint) for p in range(N): for q in range(N): if p ! q: if dominates(pop_obj[p], pop_obj[q], M): dominated_solutions[p].append(q) elif dominates(pop_obj[q], pop_obj[p], M): domination_count[p] 1 if domination_count[p] 0: fronts[0].append(p) i 0 while len(fronts[i]) 0: next_front [] for p in fronts[i]: for q in dominated_solutions[p]: domination_count[q] - 1 if domination_count[q] 0: next_front.append(q) i 1 fronts[i] next_front return [f for f in fronts if f] # remove empty fronts参数说明pop_obj是当前种群的目标值矩阵shape(N,M)M是目标数CEC2021_1为2CEC2021_4为3。该函数返回各前沿的索引列表如fronts[0]是第一前沿非支配解集fronts[1]是第二前沿……后续截断时优先保留fronts[0]。拥挤度距离Crowding Distance这是维持多样性的核心。对每个前沿内个体计算其在每个目标维度上的邻居距离并求和。注意必须先归一化目标值否则量纲差异大的目标如f1∈[0,1]、f2∈[100,200]会让拥挤度完全被f2主导def crowding_distance_assignment(front, pop_obj): front: list of indices in this front pop_obj: (N, M) objective matrix Returns: (len(front),) array of crowding distances M pop_obj.shape[1] distances np.zeros(len(front)) if len(front) 3: # edge cases return distances for m in range(M): # Get objective values for this objective, sorted by value obj_vals pop_obj[front, m] idx_sorted np.argsort(obj_vals) # Boundary points get infinite distance (set to max float) distances[idx_sorted[0]] np.inf distances[idx_sorted[-1]] np.inf # Normalize range to avoid division by zero f_min, f_max obj_vals.min(), obj_vals.max() if f_max f_min: continue # all same value - no contribution # Calculate distance between neighbors for i in range(1, len(front)-1): prev_idx idx_sorted[i-1] curr_idx idx_sorted[i] next_idx idx_sorted[i1] # Use normalized difference: (f_next - f_prev) / (f_max - f_min) distances[curr_idx] (obj_vals[next_idx] - obj_vals[prev_idx]) / (f_max - f_min) return distances关键细节distances[curr_idx] ...是累加不是覆盖。每个目标维度贡献一份距离最终总拥挤度是M维之和。边界点设为np.inf确保它们必被选中——这是NSGA-Ⅱ保持边界解多样性的设计精髓。二元锦标赛选择Binary Tournament Selection不按适应度而按前沿等级拥挤度双重判据。同一前沿内比拥挤度不同前沿比前沿序号def binary_tournament_selection(fronts, crowding_distances, pop_size): Select pop_size parents from combined population fronts: list of lists, e.g., [[0,2],[1,3,5]] crowding_distances: list of arrays, one per front Returns: list of selected parent indices N sum(len(f) for f in fronts) selected [] while len(selected) pop_size: # Randomly pick two individuals from entire population i, j np.random.choice(N, 2, replaceFalse) # Find which front each belongs to front_i front_j -1 for idx, front in enumerate(fronts): if i in front: front_i idx if j in front: front_j idx # Rule 1: lower front wins if front_i front_j: selected.append(i) elif front_j front_i: selected.append(j) else: # Rule 2: higher crowding distance wins # Map global index to local index in its front local_i fronts[front_i].index(i) local_j fronts[front_j].index(j) if crowding_distances[front_i][local_i] crowding_distances[front_j][local_j]: selected.append(i) else: selected.append(j) return selected血泪经验这里容易犯错的是“全局索引→局部索引”的映射。crowding_distances[front_i]长度等于len(fronts[front_i])但i是全局索引0~N-1必须用fronts[front_i].index(i)找到它在本前沿内的位置。漏掉这步拥挤度数组越界直接报错。3. SBX交叉与多项式变异CEC-2021高精度求解的两个杠杆NSGA-Ⅱ的遗传操作不是摆设。CEC-2021的复杂Pareto前沿如CEC2021_6的离散不连续前沿对交叉和变异算子极其敏感。标准教材推荐SBXSimulated Binary Crossover和多项式变异Polynomial Mutation但参数η_c交叉分布指数和η_m变异分布指数必须按CEC-2021函数特性调整——这不是玄学有论文依据Zhang et al., IEEE TEVC 2021指出对强非线性函数η_c应≥20对高维变量η_m应≤5。3.1 SBX交叉用β分布模拟正态交叉避免早熟SBX不直接交换基因而是生成一个分布系数β再用β控制子代在父代间的落点。β服从特定概率密度使子代大概率靠近父代开发小概率远离探索def sbx_crossover(parent1, parent2, eta_c20.0, pc0.9): Simulated Binary Crossover parent1, parent2: (D,) arrays eta_c: distribution index, higher - more like uniform crossover pc: crossover probability Returns: two children (D,) arrays if np.random.random() pc: return parent1.copy(), parent2.copy() D len(parent1) child1 np.zeros(D) child2 np.zeros(D) for i in range(D): u np.random.random() if u 0.5: beta (2*u)**(1.0/(eta_c1)) else: beta (1.0/(2*(1-u)))**(1.0/(eta_c1)) child1[i] 0.5 * ((1beta)*parent1[i] (1-beta)*parent2[i]) child2[i] 0.5 * ((1-beta)*parent1[i] (1beta)*parent2[i]) # Repair: clamp to [0,1] child1[i] np.clip(child1[i], 0, 1) child2[i] np.clip(child2[i], 0, 1) return child1, child2参数说明eta_c20.0是CEC-2021推荐值见CEC2021报告附录B比经典教材的η_c15更利于跳出局部最优pc0.9保证高交叉率因CEC函数常需大范围探索。注意np.clip必不可少——CEC函数定义域严格为[0,1]^D越界值会导致目标函数返回NaN。3.2 多项式变异用多项式扰动维持种群活力变异不是随机抖动而是按多项式概率密度扰动单个变量使子代以高概率靠近父代低概率大幅跳跃def polynomial_mutation(x, eta_m5.0, pm0.1): Polynomial Mutation x: (D,) array eta_m: distribution index, lower - larger perturbation pm: mutation probability per variable Returns: mutated x (D,) array D len(x) x_mut x.copy() for i in range(D): if np.random.random() pm: delta np.random.random() if delta 0.5: mut_pow 1.0 / (eta_m 1.0) delta_q (2.0 * delta)**mut_pow - 1.0 else: mut_pow 1.0 / (eta_m 1.0) delta_q 1.0 - (2.0 * (1.0 - delta))**mut_pow x_mut[i] x_mut[i] delta_q x_mut[i] np.clip(x_mut[i], 0, 1) # enforce bounds return x_mut关键对比eta_m5.0比常用值20更激进——CEC2021_8有多个孤立Pareto前沿需要更强变异才能跳到新区域pm0.1指每个变量独立有10%概率被变异对10维问题平均每次变异1个变量符合CEC建议。4. 避坑CEC-2021NSGA-Ⅱ组合的5个致命翻车点这课设最常卡在“跑出结果但不对”表面是代码问题实则是CEC-2021特性和NSGA-Ⅱ实现细节的隐性冲突。以下是我在CUG实验室带过3届学生、debug过27个.zip包后总结的5条血泪教训每条都对应真实报错和解决方案。4.1 现象ValueError: operands could not be broadcast together原因CEC2021_4要求30维输入但你初始化种群用了np.random.rand(100,10)100个体×10维传给cec2021_4(x)时函数内部x[2:10]切片越界返回shape不匹配的目标值导致后续非支配排序矩阵运算失败。解决在cec2021_x函数开头加维度校验——def cec2021_4(x): assert len(x) 30, fCEC2021_4 requires 30-D input, got {len(x)} # ... rest of function并在主循环初始化时严格按问题要求pop np.random.rand(pop_size, D)其中D查CEC2021文档表UF130D, CEC2021_110D, CEC2021_430D。4.2 现象Pareto前沿全堆在左下角f1/f2值极小且密集原因目标函数未归一化拥挤度计算时f1量级为1e-3、f2量级为1e2导致拥挤度几乎全由f2决定种群在f1方向严重坍缩。解决在crowding_distance_assignment前对整个pop_obj做min-max归一化# Before calling crowding_distance_assignment pop_obj_norm (pop_obj - pop_obj.min(axis0)) / (pop_obj.max(axis0) - pop_obj.min(axis0) 1e-8)注意加1e-8防除零且必须用axis0按列目标维度归一化。4.3 现象运行100代后fronts[0]只有2个解其余全在fronts[1]原因非支配排序的dominates函数写错。常见错误是写成p[i] q[i] for all i弱支配但NSGA-Ⅱ要求严格支配必须至少一个目标严格更优。错误版本会让大量解互相不支配全挤进第一前沿。解决严格按定义实现——def dominates(p, q, M): better False for i in range(M): if p[i] q[i]: # 如果p在某目标上更差立即退出 return False if p[i] q[i]: # 记录是否有严格更优 better True return better # 只有全部不劣至少一个更优才返回True4.4 现象IndexError: list index out of range发生在binary_tournament_selection原因fronts是空列表如所有个体都被判定为同一前沿但代码仍尝试fronts[front_i].index(i)。根源是目标值全相同如初始种群全为0向量CEC函数返回全0导致domination_count全为0所有个体进入fronts[0]但fronts[1:]为空。解决在选择前加安全检查——if len(fronts) 0: # Fallback: random selection return np.random.choice(N, pop_size, replaceFalse).tolist()4.5 现象RuntimeWarning: invalid value encountered in divide出现在拥挤度计算原因某目标维度所有值相等如f1全为0.5导致f_max - f_min 0除零。解决在拥挤度计算中加入保护——f_min, f_max obj_vals.min(), obj_vals.max() if f_max f_min: continue # skip this objective, contributes 0 to distance # else proceed with division5. 跑通CEC-2021的完整工作流从解压.zip到画出Pareto前沿图现在把所有模块串起来形成可直接运行的课设主流程。我们以CEC2021_1为例2目标10维设置种群大小100、迭代100代。关键不是参数本身而是如何验证每一步都正确——这才是CUG课设拿高分的核心。5.1 主循环骨架四步闭环每步可dump验证import numpy as np import matplotlib.pyplot as plt # Parameters problem CEC2021_1 D 10 # decision variables M 2 # objectives pop_size 100 max_gen 100 pc 0.9 pm 0.1 eta_c 20.0 eta_m 5.0 # Initialize population pop np.random.rand(pop_size, D) # shape (100,10) pop_obj np.array([cec2021_1(x) for x in pop]) # (100,2) # Main loop for gen in range(max_gen): # Step 1: Create offspring via crossover mutation offspring [] for _ in range(pop_size): # Select two parents idx1, idx2 np.random.choice(pop_size, 2, replaceFalse) p1, p2 pop[idx1], pop[idx2] c1, c2 sbx_crossover(p1, p2, eta_c, pc) c1 polynomial_mutation(c1, eta_m, pm) c2 polynomial_mutation(c2, eta_m, pm) offspring.extend([c1, c2]) # Keep only pop_size offspring offspring np.array(offspring[:pop_size]) off_obj np.array([cec2021_1(x) for x in offspring]) # Step 2: Combine parent and offspring combined_pop np.vstack([pop, offspring]) combined_obj np.vstack([pop_obj, off_obj]) # Step 3: Non-dominated sort and crowding distance fronts fast_nondominated_sort(combined_obj, M) crowding_distances [] for front in fronts: cd crowding_distance_assignment(front, combined_obj) crowding_distances.append(cd) # Step 4: Environmental selection - fill new population new_pop [] new_pop_obj [] i 0 while len(new_pop) pop_size: if i len(fronts): break front fronts[i] cd crowding_distances[i] # Sort this front by crowding distance descending sorted_indices np.argsort(cd)[::-1] selected_in_front [] for idx in sorted_indices: if len(new_pop) pop_size: selected_in_front.append(front[idx]) new_pop.append(combined_pop[front[idx]]) new_pop_obj.append(combined_obj[front[idx]]) else: break i 1 pop np.array(new_pop) pop_obj np.array(new_pop_obj) # Optional: dump every 10 generations for debugging if gen % 10 0 or gen max_gen-1: print(fGen {gen}: Front0 size {len(fronts[0])}, Obj range {pop_obj.min(axis0)}, {pop_obj.max(axis0)})验证技巧在print行后加一句np.save(fgen_{gen}_pop.npy, pop_obj)。跑完后用np.load(gen_99_pop.npy)加载最后一代目标值用plt.scatter(pop_obj[:,0], pop_obj[:,1])画散点图——如果看到清晰的凸形前沿CEC2021_1理论前沿是凸的说明成功如果是一团糊回头检查cec2021_1函数或SBX交叉。5.2 绘制Pareto前沿图用真实Pareto解标注拒绝“看起来像”CEC-2021提供每个问题的真实Pareto前沿True PF数据文件如CEC2021_1_PF.txt格式为每行两个目标值。必须用它验证你的解质量# Load true PF true_pf np.loadtxt(CEC2021_1_PF.txt) # shape (N_true, 2) # Extract final front final_fronts fast_nondominated_sort(pop_obj, M) final_pareto pop_obj[final_fronts[0]] # Plot plt.figure(figsize(8,6)) plt.scatter(true_pf[:,0], true_pf[:,1], cred, s1, alpha0.5, labelTrue PF) plt.scatter(final_pareto[:,0], final_pareto[:,1], cblue, s20, labelNSGA-II Result) plt.xlabel(f1) plt.ylabel(f2) plt.title(CEC2021_1 Pareto Front) plt.legend() plt.grid(True) plt.savefig(cec2021_1_result.png, dpi300, bbox_inchestight) plt.show()关键细节true_pf和final_pareto必须同为二维点集。如果final_pareto形状是(1,2)说明非支配排序只找到1个解——立刻检查dominates函数如果点云完全不重叠优先怀疑cec2021_1函数实现有误比如漏了旋转矩阵R。5.3 量化评估用IGD指标证明你真的解对了课设报告不能只说“效果好”要给出数字。IGDInverted Generational Distance是CEC-2021推荐指标计算真实PF上每个点到你解集的最小距离再取平均。值越小越好def igd(pf_true, pf_approx): pf_true: (N_true, M) array pf_approx: (N_approx, M) array Returns: scalar IGD value distances [] for p in pf_true: # Euclidean distance to nearest point in approximation dists np.sqrt(np.sum((pf_approx - p)**2, axis1)) distances.append(dists.min()) return np.mean(distances) # Usage igd_value igd(true_pf, final_pareto) print(fIGD {igd_value:.6f})CUG课设评分点IGD 0.01为优秀 0.05为良好 0.1需重调参数。如果IGD很大优先调eta_c增大到30和eta_m减小到3再检查函数实现。6. 我的CUG课设交付习惯三个让老师一眼看出你懂原理的细节做完以上你已经能跑通。但CUG智能优化课设真正拉开差距的不是“能跑”而是“跑得明白”。我带过的高分作业都有这三个细节它们不用多写代码却能让老师立刻判断你是否吃透NSGA-Ⅱ6.1 在报告里画一张“种群演化热力图”不要只交最终散点图。用np.save保存每代fronts[0]的目标值然后画10×10网格的热力图横轴是代数0~99纵轴是目标维度f1,f2每个格子颜色深浅表示该代该目标的值分布密度。你会看到——f1在前20代快速下降f2在50代后开始展宽这正是NSGA-Ⅱ“先收敛后探索”的真实痕迹。老师看到这个图就知道你没抄代码而是盯着种群在动。6.2 把SBX交叉的β分布可视化标出CEC-2021推荐的η_c20用np.random生成10000个β值画直方图并叠加理论PDF曲线SBX的β分布有解析式。在图上标出η_c20对应的曲线——它比η_c15更尖锐意味着子代更集中在父代附近。这证明你调参不是蒙的而是理解分布本质。6.3 在代码注释里写清每个CEC函数的“陷阱点”比如在cec2021_1函数开头加# CEC2021_1 TRAP: Rotation matrix R must be applied ONLY to first 2 dims of t2, # NOT to full t2. Original MATLAB code uses R*[t2(1);t2(2)], then appends t2(3:end). # Failure here causes non-convex front.这种注释比任何文字描述都有力——它表明你读过原始MATLAB代码知道哪里容易错。最后说句实在话CUG这门课设本质是逼你亲手造一次轮子。pymoo一行algorithm NSGA2()就能出结果但你交上去老师只会看到“你会调包”。而当你把fast_nondominated_sort的三层循环、sbx_crossover的β计算、cec2021_1的旋转矩阵都手敲出来debug到凌晨三点终于看到IGD降到0.008那一刻你获得的不是分数是面对任何优化问题都不慌的底气。希望帮到你。本文还有配套的精品资源点击获取
返回列表