ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python遗传算法实战:DEAP库从入门到多目标优化与避坑指南

Python遗传算法实战:DEAP库从入门到多目标优化与避坑指南 简介《Hands-On Genetic Algorithms with Python》第二版由资深数据科学家Eyal Wirsansky撰写面向具备一定Python基础、希望将遗传算法落地于AI与机器学习场景的开发者与研究者。全书围绕编码、选择、交叉、变异等核心环节展开结合优化问题、神经网络训练、路径规划与强化学习等实战案例讲解遗传算法与传统优化方法的差异及适用边界。资源包为1个PDF文件大小约5.99MB内容完整涵盖2024年6月新版正文便于在电脑或平板上直接阅读与检索。目前已有54人学习下载适合作为系统入门与项目参考。读者可从中掌握用Python实现遗传算法各组件的方法理解其与神经网络、强化学习结合提升性能的思路并借助书中案例建立从原理到代码的完整认知为复杂搜索与优化任务提供可复用的解决框架。1. 从《Hands-On Genetic Algorithms with Python》第2版说起为什么2024年还值得把遗传算法跑一遍如果你手上正好有这本 Wirsansky Eyal 写的《Hands-On Genetic Algorithms with Python》第2版或者只是搜到了这个书名想确认它值不值得花时间那这篇笔记就是写给你的。它讲的不是遗传算法是什么这种教科书问题而是怎么用 Python 把一套能跑、能调、能出结果的遗传算法流程搭起来。2024 年这个时间点Python 生态里的 DEAP、PyGAD、LEAP 这些库已经相当成熟配合 numpy、pandas、matplotlib 做可视化和数据处理一台普通笔记本就能把大部分经典问题跑通。适合谁适合已经会写 Python 基础代码、想找一个能直接抄作业的优化框架的工程师也适合做量化、调度、参数寻优、特征选择这类实际任务的人。它解决的核心问题是当你的搜索空间大到穷举不现实、梯度信息又拿不到的时候怎么用一套可解释、可调参的进化流程把近似最优解逼出来。下面我按自己复现这套东西的顺序把选型、代码、参数和踩过的坑一条条讲清楚。2. 遗传算法在 Python 里到底怎么落地从 DEAP 到 PyGAD 的选型与最小可跑框架2.1 为什么先选 DEAP 而不是自己从零写很多人第一次接触遗传算法会想自己写选择、交叉、变异觉得这样可控。我一开始也这么干过结果卡在种群多样性维护和早熟收敛上调了两周还不如换个成熟库。DEAP 的优势在于它把个体Individual、种群Population、适应度Fitness抽象得很干净交叉变异算子可以直接调库里的也可以自己写。PyGAD 更偏开箱即用适合快速验证LEAP 性能好但文档相对少。我的建议是先用 DEAP 把流程跑通理解每一层在干什么再根据场景决定要不要换。安装这一步没什么玄学但要注意 Python 版本。DEAP 在 3.9 到 3.12 上都能跑numpy 建议 1.24 以上。# 建议用虚拟环境避免和系统里的包打架 python -m venv ga_env source ga_env/bin/activate # Windows 用 ga_env\Scripts\activate pip install deap numpy matplotlib pandas装完之后先别急着上复杂问题用一个一维函数求最大值把链路走通。下面这段代码是我常用的最小骨架目标是找f(x) x * sin(10 * pi * x) 2在 [-1, 2] 上的最大值。import random import numpy as np from deap import base, creator, tools, algorithms # 1. 定义适应度最大化和个体类型 creator.create(FitnessMax, base.Fitness, weights(1.0,)) creator.create(Individual, list, fitnesscreator.FitnessMax) toolbox base.Toolbox() # 2. 个体编码一个浮点数范围 [-1, 2] BOUND_LOW, BOUND_UP -1.0, 2.0 toolbox.register(attr_float, random.uniform, BOUND_LOW, BOUND_UP) toolbox.register(individual, tools.initRepeat, creator.Individual, toolbox.attr_float, n1) toolbox.register(population, tools.initRepeat, list, toolbox.individual) # 3. 适应度函数 def eval_func(individual): x individual[0] return (x * np.sin(10 * np.pi * x) 2.0,) toolbox.register(evaluate, eval_func) toolbox.register(mate, tools.cxBlend, alpha0.5) toolbox.register(mutate, tools.mutGaussian, mu0, sigma0.2, indpb0.2) toolbox.register(select, tools.selTournament, tournsize3) def main(): random.seed(42) pop toolbox.population(n100) hof tools.HallOfFame(1) stats tools.Statistics(lambda ind: ind.fitness.values) stats.register(max, np.max) stats.register(avg, np.mean) pop, log algorithms.eaSimple( pop, toolbox, cxpb0.6, mutpb0.3, ngen50, statsstats, halloffamehof, verboseTrue ) print(最优个体:, hof[0], 适应度:, hof[0].fitness.values[0]) if __name__ __main__: main()这段代码的逻辑分三层编码层决定了解的空间长什么样attr_float生成 [-1, 2] 内的随机数算子层里cxBlend是混合交叉mutGaussian是高斯变异selTournament是锦标赛选择进化层用eaSimple把选择、交叉、变异串起来跑 50 代。参数上cxpb0.6表示 60% 的配对会交叉mutpb0.3表示 30% 的个体会变异这两个值不是拍脑袋定的——交叉率太高会破坏已有好解太低则探索不足变异率太高退化成随机搜索太低容易早熟。tournsize3是锦标赛规模越大选择压力越大收敛快但多样性掉得也快。2.2 编码方式选错后面全白搭遗传算法最容易翻车的地方不是算子是编码。上面用的实数编码适合连续优化但如果你做的是特征选择、路径规划、调度排程就得换二进制编码或排列编码。我见过有人拿实数编码去做背包问题结果交叉出来的个体根本不满足约束只能靠惩罚函数硬压收敛慢得让人想砸键盘。二进制编码用tools.cxTwoPoint和tools.mutFlipBit适合 0/1 决策类问题。排列编码用tools.cxOrdered和tools.mutShuffleIndexes适合旅行商、作业车间调度这类顺序敏感的问题。选编码的原则就一条让交叉和变异产生的后代天然合法而不是靠事后修补。# 二进制编码示例n 位 0/1 个体 toolbox.register(attr_bool, random.randint, 0, 1) toolbox.register(individual, tools.initRepeat, creator.Individual, toolbox.attr_bool, n20) toolbox.register(mate, tools.cxTwoPoint) toolbox.register(mutate, tools.mutFlipBit, indpb0.05)indpb0.05是每一位翻转的概率20 位的话平均每代翻一位这个强度在大多数 0/1 问题上比较稳。如果问题维度到几百位这个值还要往下压否则等于每代都在大洗牌。2.3 适应度函数里的惩罚项怎么写才不拖垮收敛约束优化是绕不开的。常见做法是罚函数违反约束就扣分。但罚系数设大了可行解和不可行解的适应度差距悬殊种群会迅速挤到可行域边缘设小了不可行解照样繁殖。我一般用自适应罚系数前期松、后期紧。def eval_with_penalty(individual, gen, max_gen): x individual[0] obj x * np.sin(10 * np.pi * x) 2.0 # 假设约束是 x 1.5 violation max(0.0, x - 1.5) penalty (gen / max_gen) * 10.0 * violation return (obj - penalty,)这里gen / max_gen让惩罚随代数线性增强前期允许探索后期逼向可行域。注意 DEAP 的eaSimple不直接传代数给适应度函数需要自己包一层或者改用eaMuPlusLambda手动控制循环。这个细节书里不一定展开但实际写的时候一定会遇到。3. 把书里的案例跑成自己的项目参数调优、并行化和结果验证3.1 种群规模和代数怎么定一个可复用的起步配置书里给的建议是种群 50 到 200、代数 50 到 500这个范围没错但太宽。我的经验是按维度定变量维度 d 在 10 以内种群 50 到 80 够用d 在 10 到 50种群 100 到 200d 超过 50种群至少 300否则多样性撑不住。代数看收敛曲线如果 30 代内 max 和 avg 就贴在一起不动了加代数没用得加变异率或换编码。下面这个配置是我做特征选择d30时调出来的可以直接当起点POP_SIZE 150 NGEN 200 CXPB 0.7 MUTPB 0.2 TOURNSIZE 4跑的时候把stats里的 max 和 avg 打到日志里用 matplotlib 画出来。如果 avg 一直贴着 max说明多样性没了如果 avg 震荡很大说明变异太猛。这两种情况我都遇到过前者加mutpb或引入随机 immigrants后者降mutpb或减小sigma。3.2 用 multiprocessing 把适应度评估并行起来适应度评估往往是整个流程里最慢的一环尤其是调用外部仿真或训练模型的时候。DEAP 支持用toolbox.register(map, ...)替换默认的 map直接接 multiprocessing 的 Pool。import multiprocessing def main_parallel(): pool multiprocessing.Pool(processes8) toolbox.register(map, pool.map) pop toolbox.population(nPOP_SIZE) hof tools.HallOfFame(1) stats tools.Statistics(lambda ind: ind.fitness.values) stats.register(max, np.max) stats.register(avg, np.mean) pop, log algorithms.eaSimple( pop, toolbox, cxpbCXPB, mutpbMUTPB, ngenNGEN, statsstats, halloffamehof, verboseTrue ) pool.close() pool.join() return hof[0]注意两点一是适应度函数必须是可 pickle 的定义在模块顶层别用 lambda 或闭包二是进程数别超过物理核数超了反而因为上下文切换变慢。我一般设成cpu_count() - 1留一个核给系统。3.3 结果怎么验证不是运气好遗传算法是随机算法跑一次得到好结果不代表稳定。我的做法是固定三组不同随机种子各跑 10 次看最优值的分布。如果 10 次里有 8 次能到同一个值附近说明收敛稳定如果忽高忽低说明参数或编码有问题。results [] for seed in [1, 42, 2024]: random.seed(seed) np.random.seed(seed) best main_parallel() results.append(best.fitness.values[0]) print(三次最优值:, results) print(均值:, np.mean(results), 标准差:, np.std(results))标准差大就回去查变异率和种群规模别急着下结论说算法不行。大多数时候是参数没配好不是算法本身的问题。4. 避坑与排查遗传算法在 Python 里最常见的五个翻车现场4.1 现象跑了几代之后种群全一样适应度不再提升原因早熟收敛。选择压力过大锦标赛规模太大或用了精英保留但没控制比例加上变异率太低种群多样性迅速丢失。解决把tournsize从 5 降到 3mutpb从 0.1 提到 0.2 到 0.3或者引入随机 immigrants——每代替换掉最差的 5% 个体为随机新个体。DEAP 里可以手动做这一步在eaSimple外面包循环。4.2 现象适应度函数报错说个体长度不对原因编码方式和算子不匹配。比如用cxTwoPoint去交叉实数编码的个体或者initRepeat的 n 和后面算子里假设的维度不一致。解决检查toolbox.individual的 n 和适应度函数里对 individual 的索引方式。二进制编码的个体是 0/1 列表实数编码是浮点列表别混用。4.3 现象并行跑的时候卡死或者报 pickle 错误原因适应度函数定义在if __name__ __main__里面或者用了 lambda、局部函数multiprocessing 没法序列化。解决把适应度函数提到模块顶层所有依赖的全局变量也提上去。Windows 上还要确保if __name__ __main__保护到位否则子进程会重复导入主模块。4.4 现象结果每次都不一样没法复现原因没有固定随机种子或者固定了 Python 的 random 但没固定 numpy 的。解决random.seed()和np.random.seed()都要设而且要在创建种群之前设。如果用了 multiprocessing每个子进程的随机状态是独立的需要在 worker 初始化时重新播种。4.5 现象适应度评估特别慢跑一代要几分钟原因适应度函数里有重复计算或者每次都在重新加载数据/模型。解决把不变的数据和模型加载提到全局用缓存functools.lru_cache存已经算过的个体适应度。如果个体有重复离散问题里常见缓存能省掉大量重复评估。5. 进阶技巧用 NSGA-II 做多目标优化以及一个我常用的收敛判断习惯单目标跑顺之后下一步多半是多目标。DEAP 内置了 NSGA-II 的选择算子tools.selNSGA2配合creator.create(FitnessMulti, base.Fitness, weights(1.0, -1.0))就能同时最大化一个目标、最小化另一个。下面是一个双目标的最小示例目标是最大化 x 同时最小化 x 的平方。creator.create(FitnessMulti, base.Fitness, weights(1.0, -1.0)) creator.create(Individual, list, fitnesscreator.FitnessMulti) def eval_multi(individual): x individual[0] return (x, x ** 2) toolbox.register(evaluate, eval_multi) toolbox.register(select, tools.selNSGA2) def main_nsga2(): pop toolbox.population(n100) # 先评估初始种群 for ind in pop: ind.fitness.values toolbox.evaluate(ind) pop toolbox.select(pop, len(pop)) for gen in range(50): offspring tools.selTournamentDCD(pop, len(pop)) offspring [toolbox.clone(ind) for ind in offspring] for c1, c2 in zip(offspring[::2], offspring[1::2]): if random.random() 0.7: toolbox.mate(c1, c2) del c1.fitness.values, c2.fitness.values if random.random() 0.2: toolbox.mutate(c1) del c1.fitness.values for ind in offspring: if not ind.fitness.valid: ind.fitness.values toolbox.evaluate(ind) pop toolbox.select(pop offspring, 100) return tools.sortNondominated(pop, len(pop), first_front_onlyTrue)[0]这里的关键是selTournamentDCD和selNSGA2的配合前者负责配对选择后者负责环境选择把父代和子代合并后按非支配排序和拥挤度筛选。weights(1.0, -1.0)里的正负号决定最大化和最小化方向写反了结果会完全不对。跑完之后sortNondominated返回第一前沿也就是帕累托最优解集。关于收敛判断我有一个用了很久的习惯不看最终最优值看最后 20 代的 avg 变化率。如果连续 20 代 avg 的波动小于 1%我就认为收敛了直接停不再浪费算力。这个阈值对大多数连续优化问题够用离散问题可以放宽到 2%。另外每次调完参数我都会把 log 存成 CSV方便后面对比不同配置。这个习惯帮我省了很多上次那个参数是多少来着的后悔药时间。最后说一句实在的遗传算法不是万能钥匙它适合搜索空间大、梯度不可用、约束复杂的场景。如果你的问题维度低、目标函数光滑先用梯度方法或网格搜索别上来就套 GA。我踩过这个坑花了三天调参最后发现 scipy.optimize 两行就搞定了。希望帮到你。本文还有配套的精品资源点击获取
返回列表