遗传算法(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实现,用于求解函数优化问题:
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple, Callable
class 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_size
self.chromosome_length = chromosome_length
self.max_gen = max_gen
self.crossover_rate = crossover_rate
self.mutation_rate = mutation_rate
self.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_bounds
decimal = int(''.join(map(str, chromosome)), 2)
x = x_min + decimal * (x_max - x_min) / (2**self.chromosome_length - 1)
return x
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
def 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 fitness
def roulette_selection(self, population: np.ndarray, fitness: np.ndarray) -> np.ndarray:
"""轮盘赌选择"""
fitness = fitness - np.min(fitness) + 1e-6
probs = 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, child2
return 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 chromosome
def 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_y
def 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()
使用示例
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.0
plt.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 参数设计
我们在初始化时设置了几个关键参数:
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 编码和解码机制
这部分可能是最有趣的设计之一:
def decode_chromosome(self, chromosome: np.ndarray) -> float:
"""将二进制染色体解码为实数"""
x_min, x_max = self.x_bounds
decimal = 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 适应度计算
适应度函数决定了个体的优劣:
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 选择机制
选择操作模拟"适者生存":
def roulette_selection(self, population: np.ndarray, fitness: np.ndarray):
"""轮盘赌选择"""
fitness = fitness - np.min(fitness) + 1e-6
probs = fitness / fitness.sum()
selected_idx = np.random.choice(
self.pop_size,
size=self.pop_size,
p=probs
)
return population[selected_idx]
为什么用轮盘赌选择?
-
公平性
- 适应度越高,被选中的概率越大
- 但较差的个体仍有机会被选中
- 类比:就像转轮盘,好的解占的格子多,但其他解仍有机会
-
多样性
- 保留了一定的随机性
- 防止过早收敛到局部最优
- 类比:不能只选最好的餐馆,偶尔也要尝试新餐馆
5.6 交叉操作
交叉操作创造新的解:
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, child2
return parent1.copy(), parent2.copy()
这个设计的巧妙之处:
-
信息交换
- 两个父代解交换部分信息
- 产生新的可能更好的解
- 类比:就像把两家餐馆的优点结合起来
-
概率控制
- 不是每次都交叉
- 保留一部分原有的好的解
- 类比:保留一些已经很好的餐馆,不要都改变
5.7 变异操作
变异带来新的可能:
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代内就基本收敛
- 收敛速度较快
- 计算效率较高
-
解的稳定性
- 最优解在进化后期保持稳定
- 平均适应度波动在合理范围内
- 说明算法具有良好的稳定性
-
全局搜索能力
- 成功跳出局部最优
- 找到了全局最优解区域
- 展现了良好的全局搜索能力