遗传算法(Genetic Algorithm)详解与实现
1. 基本原理
遗传算法(Genetic Algorithm, GA)是一种模拟生物进化过程的优化算法。它的核心思想来源于达尔文进化论:
“物竞天择,适者生存”
1.1 核心概念
遗传算法包含以下关键要素:
- 染色体(Chromosome):问题解的编码表示
- 种群(Population):多个候选解的集合
- 适应度(Fitness):评价解的好坏程度
- 选择(Selection):模拟自然选择过程
- 交叉(Crossover):产生新的后代
- 变异(Mutation):维持种群多样性
1.2 数学基础
从优化的角度看,遗传算法在解空间 S S S中搜索最优解 x ∗ x^* x∗:
f ( x ∗ ) = m a x { f ( x ) ∣ x ∈ S } f(x^*) = max\{f(x) | x \in S\} f(x∗)=max{f(x)∣x∈S}
或 f ( x ∗ ) = m i n { f ( x ) ∣ x ∈ S } f(x^*) = min\{f(x) | x \in S\} f(x∗)=min{f(x)∣x∈S}
算法复杂度:
- 空间复杂度: O ( N L ) O(NL) O(NL)
- 时间复杂度: O ( G N L ) O(GNL) O(GNL)
其中:
- N N N为种群大小
- L L L为染色体长度
- G G G为迭代代数
2. 编码机制
2.1 二进制编码
将实数 x ∈ [ a , b ] x \in [a,b] x∈[a,b]编码为 n n n位二进制串:
x = a + ( b − a ) ⋅ ∑ i = 0 n − 1 b i ⋅ 2 i 2 n − 1 x = a + (b-a) \cdot \frac{\sum_{i=0}^{n-1}b_i \cdot 2^i}{2^n - 1} x=a+(b−a)⋅2n−1∑i=0n−1bi⋅2i
其中 b i b_i bi表示第 i i i位的二进制值(0或1)。
3. 遗传操作
3.1 选择操作
选择概率计算:
P ( i ) = f i ∑ j = 1 N f j P(i) = \frac{f_i}{\sum_{j=1}^N f_j} P(i)=∑j=1Nfjfi
3.2 交叉操作
交叉概率 p c p_c pc通常取0.6-0.9,表示进行交叉操作的概率。
3.3 变异操作
变异概率 p m p_m pm通常取0.001-0.1,表示基因发生变异的概率。
4. 完整实现
以下是一个完整的遗传算法Python实现,用于求解函数优化问题:
python">import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple, Callableclass GeneticAlgorithm:def __init__(self,pop_size: int = 100,chromosome_length: int = 16,max_gen: int = 200,crossover_rate: float = 0.8,mutation_rate: float = 0.01,x_bounds: Tuple[float, float] = (-1, 2)):"""遗传算法类参数:pop_size: 种群大小chromosome_length: 染色体长度(二进制编码位数)max_gen: 最大迭代代数crossover_rate: 交叉概率mutation_rate: 变异概率x_bounds: 自变量范围"""self.pop_size = pop_sizeself.chromosome_length = chromosome_lengthself.max_gen = max_genself.crossover_rate = crossover_rateself.mutation_rate = mutation_rateself.x_bounds = x_bounds# 记录每代的最佳适应度self.best_fitness_history = []self.avg_fitness_history = []def initialize_population(self) -> np.ndarray:"""初始化种群"""return np.random.randint(2, size=(self.pop_size, self.chromosome_length))def decode_chromosome(self, chromosome: np.ndarray) -> float:"""将二进制染色体解码为实数"""x_min, x_max = self.x_boundsdecimal = int(''.join(map(str, chromosome)), 2)x = x_min + decimal * (x_max - x_min) / (2**self.chromosome_length - 1)return xdef fitness_function(self, x: float) -> float:"""适应度函数: f(x) = x·sin(10πx) + 2.0"""return x * np.sin(10 * np.pi * x) + 2.0def calculate_fitness(self, population: np.ndarray) -> np.ndarray:"""计算种群中所有个体的适应度"""fitness = np.zeros(self.pop_size)for i, chromosome in enumerate(population):x = self.decode_chromosome(chromosome)fitness[i] = self.fitness_function(x)return fitnessdef roulette_selection(self, population: np.ndarray, fitness: np.ndarray) -> np.ndarray:"""轮盘赌选择"""fitness = fitness - np.min(fitness) + 1e-6probs = fitness / fitness.sum()selected_idx = np.random.choice(self.pop_size,size=self.pop_size,p=probs)return population[selected_idx]def crossover(self, parent1: np.ndarray, parent2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:"""单点交叉"""if np.random.random() < self.crossover_rate:point = np.random.randint(1, self.chromosome_length)child1 = np.concatenate([parent1[:point], parent2[point:]])child2 = np.concatenate([parent2[:point], parent1[point:]])return child1, child2return parent1.copy(), parent2.copy()def mutation(self, chromosome: np.ndarray) -> np.ndarray:"""位变异"""for i in range(self.chromosome_length):if np.random.random() < self.mutation_rate:chromosome[i] = 1 - chromosome[i]return chromosomedef evolve(self) -> Tuple[float, float]:"""执行遗传算法"""population = self.initialize_population()for generation in range(self.max_gen):fitness = self.calculate_fitness(population)self.best_fitness_history.append(np.max(fitness))self.avg_fitness_history.append(np.mean(fitness))selected = self.roulette_selection(population, fitness)new_population = []for i in range(0, self.pop_size, 2):parent1, parent2 = selected[i], selected[i+1]child1, child2 = self.crossover(parent1, parent2)child1 = self.mutation(child1)child2 = self.mutation(child2)new_population.extend([child1, child2])population = np.array(new_population)final_fitness = self.calculate_fitness(population)best_idx = np.argmax(final_fitness)best_x = self.decode_chromosome(population[best_idx])best_y = final_fitness[best_idx]return best_x, best_ydef plot_fitness_history(self):"""绘制适应度变化曲线"""plt.figure(figsize=(10, 6))plt.plot(self.best_fitness_history, label='Best Fitness')plt.plot(self.avg_fitness_history, label='Average Fitness')plt.xlabel('Generation')plt.ylabel('Fitness')plt.title('Fitness History')plt.legend()plt.grid(True)plt.show()
使用示例
python">if __name__ == "__main__":# 创建遗传算法实例ga = GeneticAlgorithm(pop_size=100,chromosome_length=16,max_gen=200,crossover_rate=0.8,mutation_rate=0.01,x_bounds=(-1, 2))# 执行算法best_x, best_y = ga.evolve()print(f"最优解: x = {best_x:.6f}")print(f"最优值: f(x) = {best_y:.6f}")# 绘制适应度变化曲线ga.plot_fitness_history()# 绘制目标函数x = np.linspace(-1, 2, 1000)y = x * np.sin(10 * np.pi * x) + 2.0plt.figure(figsize=(10, 6))plt.plot(x, y, label='f(x) = x·sin(10πx) + 2.0')plt.scatter([best_x], [best_y], color='red', marker='*', s=200, label=f'Best Solution: ({best_x:.3f}, {best_y:.3f})')plt.xlabel('x')plt.ylabel('f(x)')plt.title('Optimization Result')plt.legend()plt.grid(True)plt.show()
5. 代码设计解析
让我们深入理解这个遗传算法的实现,看看每个部分是如何工作的,以及为什么要这样设计。
5.1 整体框架设计
首先,我们把整个算法封装成一个类GeneticAlgorithm
。这样做有几个好处:
-
参数管理更清晰
- 所有重要参数都在初始化时设置
- 可以随时访问和修改这些参数
- 不同的优化问题可以创建不同的实例
-
代码结构更合理
- 相关的功能被组织在一起
- 方法之间可以方便地共享数据
- 便于扩展和修改功能
5.2 参数设计
我们在初始化时设置了几个关键参数:
python">def __init__(self,pop_size: int = 100, # 种群大小chromosome_length: int = 16, # 染色体长度max_gen: int = 200, # 最大代数crossover_rate: float = 0.8, # 交叉概率mutation_rate: float = 0.01, # 变异概率x_bounds: Tuple[float, float] = (-1, 2) # 取值范围
):
为什么要这样设置这些参数?
-
种群大小(pop_size)
- 设置为100是一个比较好的平衡点
- 太小:容易陷入局部最优
- 太大:计算量会显著增加
- 类比:就像找最好的餐馆,同时派100个人去不同的餐馆吃饭,然后互相分享体验
-
染色体长度(chromosome_length)
- 使用16位二进制编码
- 提供了足够的精度(可以表示2^16个不同的值)
- 计算量还不会太大
- 类比:就像用16位数字来表示一个价格,精确到分
-
最大代数(max_gen)
- 设置为200代
- 给算法足够的进化时间
- 防止运行时间过长
- 类比:就像限制找餐馆的时间,不能无限期地找下去
5.3 编码和解码机制
这部分可能是最有趣的设计之一:
python">def decode_chromosome(self, chromosome: np.ndarray) -> float:"""将二进制染色体解码为实数"""x_min, x_max = self.x_boundsdecimal = int(''.join(map(str, chromosome)), 2)x = x_min + decimal * (x_max - x_min) / (2**self.chromosome_length - 1)return x
为什么要用二进制编码?
-
直观性
- 二进制是最基本的编码方式
- 容易理解和实现
- 类比:就像把一个数字转换成二进制,然后再转回来
-
精确性
- 16位二进制可以表示65536个不同的值
- 在[-1, 2]区间内,精度约为0.00004
- 类比:就像用尺子测量长度,刻度越密,测量越精确
5.4 适应度计算
适应度函数决定了个体的优劣:
python">def fitness_function(self, x: float) -> float:"""适应度函数: f(x) = x·sin(10πx) + 2.0"""return x * np.sin(10 * np.pi * x) + 2.0
这个设计考虑了:
-
评价标准
- 直接用目标函数作为适应度
- 值越大表示解越好
- 类比:就像给餐馆打分,分数越高越好
-
计算效率
- 计算简单快速
- 能够清晰区分解的好坏
- 类比:评分标准要简单明确
5.5 选择机制
选择操作模拟"适者生存":
python">def roulette_selection(self, population: np.ndarray, fitness: np.ndarray):"""轮盘赌选择"""fitness = fitness - np.min(fitness) + 1e-6probs = fitness / fitness.sum()selected_idx = np.random.choice(self.pop_size,size=self.pop_size,p=probs)return population[selected_idx]
为什么用轮盘赌选择?
-
公平性
- 适应度越高,被选中的概率越大
- 但较差的个体仍有机会被选中
- 类比:就像转轮盘,好的解占的格子多,但其他解仍有机会
-
多样性
- 保留了一定的随机性
- 防止过早收敛到局部最优
- 类比:不能只选最好的餐馆,偶尔也要尝试新餐馆
5.6 交叉操作
交叉操作创造新的解:
python">def crossover(self, parent1: np.ndarray, parent2: np.ndarray):"""单点交叉"""if np.random.random() < self.crossover_rate:point = np.random.randint(1, self.chromosome_length)child1 = np.concatenate([parent1[:point], parent2[point:]])child2 = np.concatenate([parent2[:point], parent1[point:]])return child1, child2return parent1.copy(), parent2.copy()
这个设计的巧妙之处:
-
信息交换
- 两个父代解交换部分信息
- 产生新的可能更好的解
- 类比:就像把两家餐馆的优点结合起来
-
概率控制
- 不是每次都交叉
- 保留一部分原有的好的解
- 类比:保留一些已经很好的餐馆,不要都改变
5.7 变异操作
变异带来新的可能:
python">def mutation(self, chromosome: np.ndarray):"""位变异"""for i in range(self.chromosome_length):if np.random.random() < self.mutation_rate:chromosome[i] = 1 - chromosome[i]return chromosome
变异的重要性:
-
探索新空间
- 随机改变一些基因
- 可能发现更好的解
- 类比:偶尔去一个完全没去过的餐馆
-
防止早熟
- 维持种群多样性
- 防止陷入局部最优
- 类比:不能总是去同一家餐馆
5.8 进化过程
整个进化过程是这样工作的:
-
初始化
- 随机生成一组解
- 类比:随机选择一些餐馆开始尝试
-
迭代进化
- 评价当前解的好坏
- 选择好的解
- 通过交叉产生新解
- 偶尔发生变异
- 类比:不断尝试新餐馆,记住好餐馆,组合好餐馆的特点
-
终止条件
- 达到最大代数
- 找到足够好的解
- 类比:在规定时间内找到满意的餐馆
5.9 结果分析
最后,我们通过可视化来分析结果:
-
适应度曲线
- 显示进化过程
- 反映算法效果
- 类比:看看找到的餐馆质量是否在不断提升
-
最优解展示
- 直观显示找到的最好的解
- 便于理解和评价结果
- 类比:最终推荐的最佳餐馆
6. 结果展示与分析
6.1 运行结果
算法找到的最优解:
- 最优解: x = 1.850629
- 最优值: f(x) = 3.850268
6.2 适应度进化曲线分析
从适应度历史曲线可以观察到以下特点:
-
快速收敛阶段(0-25代)
- 平均适应度从2.0快速上升到3.3左右
- 最优适应度在前20代就达到了接近3.85的水平
- 表明算法在初期具有很强的探索能力
-
稳定优化阶段(25-200代)
- 最优适应度保持在3.85左右稳定
- 平均适应度在3.5-3.7之间波动
- 说明种群维持了良好的多样性
-
收敛特征
- 最优解在早期就被找到
- 后期主要是种群整体的优化
- 没有出现明显的早熟收敛现象
6.3 优化结果可视化分析
目标函数f(x) = x·sin(10πx) + 2.0在区间[-1, 2]上:
-
函数特征
- 高度非线性
- 存在多个局部最优解
- 在x接近2处有全局最优解
-
算法表现
- 成功找到了靠近全局最优解的位置(x ≈ 1.85)
- 避开了多个局部最优解
- 红星标记处确实是函数的最高点之一
-
解的质量
- 理论最优解应该在x = 1.85附近
- 算法找到的解与理论值非常接近
- 优化结果令人满意
6.4 算法性能评估
-
收敛速度
- 在前25代内就基本收敛
- 收敛速度较快
- 计算效率较高
-
解的稳定性
- 最优解在进化后期保持稳定
- 平均适应度波动在合理范围内
- 说明算法具有良好的稳定性
-
全局搜索能力
- 成功跳出局部最优
- 找到了全局最优解区域
- 展现了良好的全局搜索能力