NSGA-II(Non-dominated Sorting Genetic Algorithm II)是一种高效的多目标优化算法,由Deb等人在2002年提出。它主要解决多个目标之间相互冲突的优化问题。
快速非支配排序
拥挤度距离
精英策略
帕累托支配关系
对于最小化问题,解x支配解y,需满足:
帕累托前沿
def fast_non_dominated_sort(population, objectives):
"""
快速非支配排序
返回:各个等级的解集合
"""
n = len(population)
domination_count = np.zeros(n) # 被支配次数
dominated_solutions = [[] for _ in range(n)] # 支配的解
fronts = [[]] # 非支配等级
# 计算支配关系
for i in range(n):
for j in range(i + 1, n):
if dominates(objectives[i], objectives[j]):
dominated_solutions[i].append(j)
domination_count[j] += 1
elif dominates(objectives[j], objectives[i]):
dominated_solutions[j].append(i)
domination_count[i] += 1
# 找出第一个非支配前沿
for i in range(n):
if domination_count[i] == 0:
fronts[0].append(i)
# 生成其他等级的前沿
i = 0
while fronts[i]:
next_front = []
for j in fronts[i]:
for k in dominated_solutions[j]:
domination_count[k] -= 1
if domination_count[k] == 0:
next_front.append(k)
i += 1
fronts.append(next_front)
return fronts[:-1] # 去掉最后一个空集
def crowding_distance(objectives, front):
"""
计算一个前沿中各个解的拥挤度距离
"""
n = len(front)
if n <= 2:
return [float('inf')] * n
distances = [0] * n
m = objectives.shape[1] # 目标数量
for i in range(m):
# 对每个目标进行排序
sorted_idx = sorted(range(n), key=lambda k: objectives[front[k], i])
# 端点设为无穷大
distances[sorted_idx[0]] = float('inf')
distances[sorted_idx[-1]] = float('inf')
# 计算中间点的距离
obj_range = objectives[front[sorted_idx[-1]], i] - objectives[front[sorted_idx[0]], i]
if obj_range == 0:
continue
for j in range(1, n-1):
distances[sorted_idx[j]] += (
objectives[front[sorted_idx[j+1]], i] -
objectives[front[sorted_idx[j-1]], i]
) / obj_range
return distances
import numpy as np
from typing import List, Tuple, Union
class NSGA2:
def __init__(
self,
pop_size: int = 100,
n_generations: int = 100,
n_objectives: int = 2,
n_variables: int = 30,
mutation_rate: float = 0.01,
crossover_rate: float = 0.9,
variable_bounds: Union[List[Tuple[float, float]], None] = None
):
self.pop_size = pop_size
self.n_generations = n_generations
self.n_objectives = n_objectives
self.n_variables = n_variables
self.mutation_rate = mutation_rate
self.crossover_rate = crossover_rate
# 如果没有指定变量范围,默认为[0,1]
if variable_bounds is None:
self.variable_bounds = [(0, 1)] * n_variables
else:
self.variable_bounds = variable_bounds
def create_offspring(self, population: np.ndarray) -> np.ndarray:
"""生成子代"""
offspring = np.zeros_like(population)
for i in range(0, self.pop_size, 2):
# 选择父代
parent1_idx = self.tournament_selection(population)
parent2_idx = self.tournament_selection(population)
# 交叉
if np.random.random() < self.crossover_rate:
child1, child2 = self.simulated_binary_crossover(
population[parent1_idx],
population[parent2_idx]
)
else:
child1 = population[parent1_idx].copy()
child2 = population[parent2_idx].copy()
# 变异
child1 = self.polynomial_mutation(child1)
child2 = self.polynomial_mutation(child2)
offspring[i] = child1
offspring[i+1] = child2
return offspring
def tournament_selection(self, population: np.ndarray, tournament_size: int = 2) -> int:
"""锦标赛选择"""
idx = np.random.randint(0, self.pop_size, tournament_size)
return idx[0] # 简化版本,实际应该比较个体的非支配等级和拥挤度
def simulated_binary_crossover(self, parent1: np.ndarray, parent2: np.ndarray,
eta: float = 20) -> Tuple[np.ndarray, np.ndarray]:
"""模拟二进制交叉"""
child1 = np.zeros_like(parent1)
child2 = np.zeros_like(parent2)
for i in range(self.n_variables):
if np.random.random() < 0.5:
child1[i] = parent1[i]
child2[i] = parent2[i]
continue
# 模拟二进制交叉
beta = self._get_beta(eta)
child1[i] = 0.5 * ((1 + beta) * parent1[i] + (1 - beta) * parent2[i])
child2[i] = 0.5 * ((1 - beta) * parent1[i] + (1 + beta) * parent2[i])
# 边界处理
lower, upper = self.variable_bounds[i]
child1[i] = np.clip(child1[i], lower, upper)
child2[i] = np.clip(child2[i], lower, upper)
return child1, child2
def _get_beta(self, eta: float) -> float:
"""获取交叉参数beta"""
u = np.random.random()
if u <= 0.5:
return (2 * u) ** (1 / (eta + 1))
return (1 / (2 * (1 - u))) ** (1 / (eta + 1))
def polynomial_mutation(self, individual: np.ndarray, eta: float = 20) -> np.ndarray:
"""多项式变异"""
child = individual.copy()
for i in range(self.n_variables):
if np.random.random() > self.mutation_rate:
continue
lower, upper = self.variable_bounds[i]
delta = upper - lower
# 多项式变异
r = np.random.random()
if r < 0.5:
delta_l = (2 * r) ** (1 / (eta + 1)) - 1
else:
delta_l = 1 - (2 * (1 - r)) ** (1 / (eta + 1))
child[i] += delta_l * delta
child[i] = np.clip(child[i], lower, upper)
return child
def calculate_objectives(self, population: np.ndarray) -> np.ndarray:
"""计算目标函数值"""
return np.array([zdt1(x) for x in population])
def select_by_crowding(self, front: List[int], crowding_dist: List[float],
n_select: int) -> List[int]:
"""根据拥挤度距离选择个体"""
sorted_idx = sorted(range(len(front)),
key=lambda k: crowding_dist[k],
reverse=True)
return [front[i] for i in sorted_idx[:n_select]]
def evolve(self, initial_population: np.ndarray) -> np.ndarray:
"""执行进化过程"""
population = initial_population
for gen in range(self.n_generations):
# 生成子代
offspring = self.create_offspring(population)
# 合并父代和子代
combined_pop = np.vstack([population, offspring])
# 计算目标值
objectives = self.calculate_objectives(combined_pop)
# 非支配排序
fronts = fast_non_dominated_sort(combined_pop, objectives)
# 选择新种群
new_population = []
for front in fronts:
if len(new_population) + len(front) <= self.pop_size:
new_population.extend(front)
else:
# 计算拥挤度距离
crowding_dist = crowding_distance(objectives, front)
# 根据拥挤度选择剩余个体
remaining = self.pop_size - len(new_population)
selected = self.select_by_crowding(front, crowding_dist, remaining)
new_population.extend(selected)
break
population = combined_pop[new_population]
if gen % 10 == 0:
print(f"Generation {gen}: Population size = {len(population)}")
return population
def dominates(obj1: np.ndarray, obj2: np.ndarray) -> bool:
"""判断obj1是否支配obj2"""
return np.all(obj1 <= obj2) and np.any(obj1 < obj2)
def fast_non_dominated_sort(population: np.ndarray, objectives: np.ndarray) -> List[List[int]]:
"""快速非支配排序"""
n = len(population)
domination_count = np.zeros(n)
dominated_solutions = [[] for _ in range(n)]
fronts = [[]]
for i in range(n):
for j in range(i + 1, n):
if dominates(objectives[i], objectives[j]):
dominated_solutions[i].append(j)
domination_count[j] += 1
elif dominates(objectives[j], objectives[i]):
dominated_solutions[j].append(i)
domination_count[i] += 1
for i in range(n):
if domination_count[i] == 0:
fronts[0].append(i)
i = 0
while fronts[i]:
next_front = []
for j in fronts[i]:
for k in dominated_solutions[j]:
domination_count[k] -= 1
if domination_count[k] == 0:
next_front.append(k)
i += 1
fronts.append(next_front)
return fronts[:-1]
def crowding_distance(objectives: np.ndarray, front: List[int]) -> List[float]:
"""计算拥挤度距离"""
n = len(front)
if n <= 2:
return [float('inf')] * n
distances = [0] * n
m = objectives.shape[1]
for i in range(m):
sorted_idx = sorted(range(n), key=lambda k: objectives[front[k], i])
distances[sorted_idx[0]] = float('inf')
distances[sorted_idx[-1]] = float('inf')
obj_range = objectives[front[sorted_idx[-1]], i] - objectives[front[sorted_idx[0]], i]
if obj_range == 0:
continue
for j in range(1, n-1):
distances[sorted_idx[j]] += (
objectives[front[sorted_idx[j+1]], i] -
objectives[front[sorted_idx[j-1]], i]
) / obj_range
return distances
让我们以一个经典的多目标优化问题ZDT1为例:
def zdt1(x: np.ndarray) -> np.ndarray:
"""ZDT1测试函数"""
f1 = x[0]
g = 1 + 9 * np.mean(x[1:])
f2 = g * (1 - np.sqrt(f1/g))
return np.array([f1, f2])
# 运行示例
if __name__ == "__main__":
nsga2 = NSGA2(pop_size=100, n_generations=100, n_variables=30)
initial_pop = np.random.random((100, 30))
final_pop = nsga2.evolve(initial_pop)
# 计算最终种群的目标值
final_objectives = nsga2.calculate_objectives(final_pop)
# 绘制帕累托前沿
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.scatter(final_objectives[:, 0], final_objectives[:, 1])
plt.xlabel('f1')
plt.ylabel('f2')
plt.title('Pareto Front')
plt.grid(True)
plt.show()
让我们逐块解析NSGA-II的完整实现,理解每个部分的作用和原理。
def __init__(
self,
pop_size: int = 100,
n_generations: int = 100,
n_objectives: int = 2,
n_variables: int = 30,
mutation_rate: float = 0.01,
crossover_rate: float = 0.9,
variable_bounds: Union[List[Tuple[float, float]], None] = None
):
这部分就像是在准备一场比赛:
pop_size
:决定有多少选手参加(种群大小)n_generations
:比赛要进行多少轮(迭代次数)n_objectives
:评判标准有几个(目标函数数量)n_variables
:每个选手有多少个可调整的参数mutation_rate
:选手自我调整的概率crossover_rate
:选手互相学习的概率variable_bounds
:每个参数的取值范围def create_offspring(self, population: np.ndarray) -> np.ndarray:
"""生成子代"""
offspring = np.zeros_like(population)
for i in range(0, self.pop_size, 2):
# 选择父代
parent1_idx = self.tournament_selection(population)
parent2_idx = self.tournament_selection(population)
# 交叉和变异
if np.random.random() < self.crossover_rate:
child1, child2 = self.simulated_binary_crossover(
population[parent1_idx],
population[parent2_idx]
)
else:
child1 = population[parent1_idx].copy()
child2 = population[parent2_idx].copy()
这就像是在进行一场配对比赛:
def simulated_binary_crossover(self, parent1: np.ndarray, parent2: np.ndarray,
eta: float = 20) -> Tuple[np.ndarray, np.ndarray]:
这就像两个选手互相学习的过程:
eta
参数控制学习程度:
举个例子:
def polynomial_mutation(self, individual: np.ndarray, eta: float = 20) -> np.ndarray:
这就像选手在尝试新东西:
eta
控制:
比如:
def fast_non_dominated_sort(population: np.ndarray, objectives: np.ndarray):
这就像对选手进行分级:
举例:
def crowding_distance(objectives: np.ndarray, front: List[int]):
这就像测量选手之间的独特性:
比如在第一级中:
def evolve(self, initial_population: np.ndarray):
整个过程就像一个循环的比赛:
每一代都会:
def zdt1(x: np.ndarray) -> np.ndarray:
这是一个测试案例,就像是给选手设置的考试:
从上图的帕累托前沿我们可以观察到以下特点:
前沿形状
解的分布
权衡关系
算法性能表现
收敛性:
多样性:
前沿完整性:
这个结果在实际应用中意味着:
决策支持
方案选择
解的特点
NSGA-II算法通过其高效的非支配排序和新颖的拥挤度计算机制,成功解决了多目标优化问题中的三个关键问题:
这使得NSGA-II成为了多目标优化领域最受欢迎的算法之一。