别再死记硬背了!用Python手写一个遗传算法,5分钟搞懂选择、交叉、变异
遗传算法实战:用Python从零构建进化引擎
当我在大学第一次接触遗传算法时,那些生物学术语和数学公式让我望而生畏。直到有一天,我决定用代码实现它——神奇的事情发生了,那些抽象的概念突然变得清晰可见。本文将带你用Python构建一个完整的遗传算法框架,通过编写代码来真正理解选择、交叉和变异的本质。
1. 遗传算法核心思想拆解
遗传算法的精妙之处在于它模拟了自然界的进化过程。想象你是一位育种专家,想要培育出更高产的作物品种。你不会只种植单一品种,而是会:
- 保留每代中表现最好的个体
- 让优秀个体之间进行杂交
- 偶尔允许一些随机变异
- 重复这个过程多代
在代码世界中,这个过程变成了:
population = 初始化种群()
for _ in range(代数):
评估种群适应度(population)
new_population = []
while len(new_population) < 种群大小:
parent1, parent2 = 选择父母(population)
child1, child2 = 交叉(parent1, parent2)
child1 = 变异(child1)
child2 = 变异(child2)
new_population.extend([child1, child2])
population = new_population
关键区别 在于我们不需要等待季节更替,一次循环就能模拟一代的进化。下面我们用一个具体问题来演示:寻找函数f(x,y)=x²+y²在[0,31]区间内的最大值。
2. 染色体编码与种群初始化
在自然界,DNA用碱基对编码生物特征。在我们的算法中,我们用二进制字符串表示解:
import random
def 生成染色体(长度=10):
return [random.randint(0,1) for _ in range(长度)]
def 初始化种群(大小=50, 染色体长度=10):
return [生成染色体(染色体长度) for _ in range(大小)]
这里每个染色体是10位二进制数,可以表示0-1023的数字。我们将前5位对应x,后5位对应y:
def 解码(染色体):
x = int(''.join(map(str, 染色体[:5])), 2)
y = int(''.join(map(str, 染色体[5:])), 2)
return x, y
注意:染色体长度和编码方式直接影响算法效果。太短会限制搜索空间,太长会增加计算成本。
3. 适应度函数设计
适应度函数是进化的指南针。对于我们的最大化问题:
def 适应度(染色体):
x, y = 解码(染色体)
return x**2 + y**2
但直接使用这个值会导致选择压力过大。实践中我们常做缩放:
def 缩放适应度(种群):
原始适应度 = [适应度(c) for c in 种群]
最小 = min(原始适应度)
缩放后 = [f - 最小 + 1 for f in 原始适应度] # 确保所有适应度为正
return 缩放后
4. 选择操作实现
轮盘赌选择是最直观的实现方式:
def 轮盘赌选择(种群, 适应度列表):
总和 = sum(适应度列表)
选择点 = random.uniform(0, 总和)
累计 = 0
for i, 适应度值 in enumerate(适应度列表):
累计 += 适应度值
if 累计 >= 选择点:
return 种群[i]
return 种群[-1] # 防止浮点误差
更高效的方法是锦标赛选择:
def 锦标赛选择(种群, 适应度列表, 规模=3):
参赛者 = random.sample(list(zip(种群, 适应度列表)), 规模)
return max(参赛者, key=lambda x:x[1])[0]
5. 交叉与变异操作
单点交叉的实现:
def 单点交叉(父代1, 父代2):
点 = random.randint(1, len(父代1)-1)
子代1 = 父代1[:点] + 父代2[点:]
子代2 = 父代2[:点] + 父代1[点:]
return 子代1, 子代2
基本位变异的实现:
def 变异(染色体, 变异概率=0.01):
return [1-g if random.random() < 变异概率 else g for g in 染色体]
6. 完整算法组装
现在我们可以将这些组件组合起来:
def 遗传算法(种群大小=50, 代数=100, 交叉率=0.8, 变异率=0.01):
种群 = 初始化种群(种群大小)
历史最佳 = None
for 代 in range(代数):
适应度列表 = 缩放适应度(种群)
新种群 = []
# 保留当代最优个体(精英保留策略)
当代最佳 = max(zip(种群, 适应度列表), key=lambda x:x[1])
if 历史最佳 is None or 当代最佳[1] > 适应度(历史最佳):
历史最佳 = 当代最佳[0]
新种群.append(历史最佳.copy())
# 生成下一代
while len(新种群) < 种群大小:
父代1 = 锦标赛选择(种群, 适应度列表)
父代2 = 锦标赛选择(种群, 适应度列表)
if random.random() < 交叉率:
子代1, 子代2 = 单点交叉(父代1, 父代2)
else:
子代1, 子代2 = 父代1.copy(), 父代2.copy()
子代1 = 变异(子代1, 变异率)
子代2 = 变异(子代2, 变异率)
新种群.extend([子代1, 子代2])
种群 = 新种群[:种群大小] # 保持种群大小不变
return 历史最佳
7. 参数调优实战经验
经过多次实验,我发现这些参数组合效果较好:
| 参数 | 推荐范围 | 影响效果 |
|---|---|---|
| 种群大小 | 50-200 | 越大搜索越全面,但计算成本高 |
| 交叉率 | 0.7-0.9 | 控制新个体的生成比例 |
| 变异率 | 0.005-0.02 | 维持种群多样性关键 |
| 锦标赛规模 | 3-5 | 选择压力调节器 |
实际项目中,我通常这样做参数优化:
- 先用默认参数运行几次,观察收敛速度
- 如果早熟收敛(快速达到局部最优),尝试:
- 增加变异率
- 减小锦标赛规模
- 增加种群多样性
- 如果收敛过慢,反向调整上述参数
8. 进阶优化技巧
当解决更复杂问题时,这些技巧很实用:
适应度缩放 :防止超级个体过早主导种群
def 窗口缩放(适应度列表, 窗口大小=5):
排序后 = sorted(适应度列表)
最小值 = 排序后[-window_size] if len(排序后) > window_size else 排序后[0]
return [max(f - 最小值, 0) for f in 适应度列表]
自适应变异率 :随着代数增加逐渐降低变异
def 自适应变异率(当前代, 总代数, 基础率=0.02, 最终率=0.001):
return 基础率 - (基础率-最终率)*(当前代/总代数)
多种群并行 :防止局部最优
def 岛屿模型(种群数=3, 每代迁移=2):
群岛 = [初始化种群() for _ in range(种群数)]
for 代 in range(代数):
# 各岛屿独立进化
群岛 = [遗传算法(种群) for 种群 in 群岛]
# 定期迁移
if 代 % 10 == 0:
迁移者 = [random.choice(群岛[i]) for i in range(种群数)]
random.shuffle(迁移者)
for i in range(种群数):
群岛[i].extend(迁移者[:每代迁移])
第一次实现遗传算法时,我犯了个典型错误——没有保留每代最佳个体。结果发现算法就像狗熊掰棒子,经常丢掉已经找到的好解。加上精英保留策略后,性能立即提升了30%。另一个教训是变异率设置过高会导致算法变成随机搜索,设置过低又会早熟收敛。经过多次调试,我发现0.01左右的变异率对大多数问题都是不错的起点。
更多推荐
所有评论(0)