用Python手把手复现NSGA-II:从理论到代码,搞定多目标优化(附完整源码)
从零实现NSGA-IIPython实战多目标优化算法在工程优化和科学研究中我们常常面临需要同时优化多个相互冲突目标的场景。传统单目标优化算法难以应对这类挑战而进化算法家族中的NSGA-II非支配排序遗传算法II因其出色的表现成为解决多目标优化问题的利器。本文将带你从理论到代码完整实现这一经典算法。1. 多目标优化与NSGA-II基础多目标优化问题(MOP)可以形式化表示为 min F(x) (f₁(x), f₂(x), ..., fₘ(x)) s.t. x ∈ Ω其中m≥2Ω是决策空间。与单目标优化不同多目标问题通常没有唯一最优解而是存在一组Pareto最优解——这些解在目标空间中形成了Pareto前沿。NSGA-II的核心创新在于快速非支配排序高效分层解集拥挤度距离保持种群多样性精英保留策略加速收敛下面是一个简单的个体类实现用于存储解的信息class Individual: def __init__(self): self.solution None # 决策变量 self.objective {} # 目标函数值 self.rank None # 非支配排序等级 self.distance 0 # 拥挤度距离 def __lt__(self, other): # 支配关系判断 v1 list(self.objective.values()) v2 list(other.objective.values()) for i in range(len(v1)): if v1[i] v2[i]: return False return True2. 快速非支配排序实现非支配排序是NSGA-II的核心操作它将种群分成多个前沿层计算每个解p的两个量nₚ支配p的解数量Sₚ被p支配的解集合第一前沿F₁包含所有nₚ0的解对于F₁中的每个解p对每个q∈Sₚn_q n_q -1若n_q0则将q放入下一前沿from collections import defaultdict def fast_non_dominated_sort(P): F defaultdict(list) # 初始化支配关系 for p in P: p.S [] p.n 0 for q in P: if p q: p.S.append(q) elif q p: p.n 1 # 构建前沿层级 i 1 F[i] [p for p in P if p.n 0] while F[i]: Q [] for p in F[i]: for q in p.S: q.n - 1 if q.n 0: q.rank i 1 Q.append(q) i 1 F[i] Q return F3. 拥挤度距离计算拥挤度距离衡量解在其前沿层中的分布密度计算步骤对每个目标方向分别处理对解集按目标值排序边界解距离设为∞中间解距离为相邻解归一化差值之和def crowding_distance_assignment(L): l len(L) for i in range(l): L[i].distance 0 for m in L[0].objective.keys(): # 按目标值排序 L.sort(keylambda x: x.objective[m]) L[0].distance float(inf) L[-1].distance float(inf) f_min L[0].objective[m] f_max L[-1].objective[m] if f_max f_min: continue for i in range(1, l-1): L[i].distance (L[i1].objective[m] - L[i-1].objective[m]) / (f_max - f_min)4. 选择与繁殖操作NSGA-II采用二元锦标赛选择机制def binary_tournament(ind1, ind2): if ind1.rank ! ind2.rank: return ind1 if ind1.rank ind2.rank else ind2 return ind1 if ind1.distance ind2.distance else ind2交叉操作使用模拟二进制交叉(SBX)def sbx_crossover(parent1, parent2, eta): child1 parent1.copy() child2 parent2.copy() for i in range(len(parent1)): u random.random() if u 0.5: beta (2*u)**(1/(eta1)) else: beta (1/(2*(1-u)))**(1/(eta1)) child1[i] 0.5*((1beta)*parent1[i] (1-beta)*parent2[i]) child2[i] 0.5*((1-beta)*parent1[i] (1beta)*parent2[i]) return child1, child2变异操作采用多项式变异def polynomial_mutation(individual, eta, lower, upper): mutated individual.copy() for i in range(len(individual)): if random.random() 1/len(individual): delta calculate_delta(eta, upper[i]-lower[i]) mutated[i] delta mutated[i] np.clip(mutated[i], lower[i], upper[i]) return mutated def calculate_delta(eta, range_val): u random.random() if u 0.5: return (2*u)**(1/(eta1)) - 1 else: return 1 - (2*(1-u))**(1/(eta1))5. 完整算法流程实现将上述组件组合成完整NSGA-IIdef nsga2(pop_size, generations, problem): # 初始化种群 population initialize_population(pop_size, problem) for gen in range(generations): # 生成子代 offspring generate_offspring(population, problem) # 合并种群 combined population offspring # 非支配排序 fronts fast_non_dominated_sort(combined) # 构建新种群 new_pop [] i 0 while len(new_pop) len(fronts[i]) pop_size: crowding_distance_assignment(fronts[i]) new_pop.extend(fronts[i]) i 1 # 按拥挤度距离补充剩余个体 if len(new_pop) pop_size: fronts[i].sort(keylambda x: -x.distance) new_pop.extend(fronts[i][:pop_size-len(new_pop)]) population new_pop return population6. 测试与可视化使用ZDT1测试函数评估算法表现def zdt1(x): f1 x[0] g 1 9 * np.mean(x[1:]) h 1 - np.sqrt(f1 / g) f2 g * h return {f1: f1, f2: f2} # 运行算法 result nsga2(100, 250, zdt1) # 可视化 plt.figure(figsize(10,6)) f1 [ind.objective[f1] for ind in result] f2 [ind.objective[f2] for ind in result] plt.scatter(f1, f2, cblue, alpha0.5) plt.xlabel(f1) plt.ylabel(f2) plt.title(NSGA-II on ZDT1) plt.grid(True) plt.show()7. 工程实践中的关键点在实际应用中有几个需要特别注意的方面参数调优种群大小通常100-500交叉概率0.7-0.9变异概率1/nn为变量维度分布指数η通常1-20约束处理def handle_constraints(individual, constraints): violation 0 for cons in constraints: if cons(individual) 0: violation abs(cons(individual)) return violation性能优化技巧使用numpy向量化计算并行化评估操作采用自适应参数调整常见问题排查种群过早收敛增加变异强度分布不均匀调整拥挤度计算方式收敛速度慢检查选择压力设置在真实项目中应用NSGA-II时建议先在小规模问题上验证算法实现再逐步扩展到复杂场景。算法性能可以通过超体积(HV)和反向世代距离(IGD)等指标量化评估。