简介:这份PDF文档面向遗传算法与组合优化方向的研究者、研究生及算法工程师,系统探讨双种群遗传算法的改进思路。全文围绕加入竞争机制的双种群遗传算法(CDPGA)展开,分析一般双种群遗传算法的基本结构与特征,讨论竞争种群规模与生命点两个关键参数,并选取Camel、Banana、Mexican Hat's、Himmelblau等经典函数进行收敛性测试,验证改进算法在最优解精确度与搜索成功率上的优势。针对0/1背包问题,文中进一步引入贪婪修补算子,在三种不同规模实例上完成实验对比,为类似组合优化问题提供新方法。资源包共1个PDF文件,约2.68MB,内容涵盖摘要、插图清单、算法流程图与实验收敛图,结构完整,适合作为算法改进与实验复现的参考材料。目前已有209人学习下载。
1. 双种群遗传算法:当单一种群陷入局部最优时,换一条路走
如果你用标准遗传算法跑过函数优化或组合调度问题,大概率见过这种场面:前几十代适应度掉得很快,然后整条曲线像被焊死一样横在那里,种群个体越来越像,交叉操作几乎产生不了新基因型,变异率调大就退化、调小就停滞。这不是参数没调好,而是单一种群结构本身的天花板——它把全部搜索压力压在一个基因池里,一旦这个池子被某个局部最优区域吸住,逃逸能力就急剧衰减。
双种群遗传算法(Dual Population Genetic Algorithm,常缩写为 DPGA)就是针对这个瓶颈提出的改进路线:把种群拆成两个独立演化的子种群,各自承担不同的搜索角色,再通过周期性的信息交换把两边的优势拼起来。它适合已经会写基础遗传算法、但在多峰函数、约束优化或离散调度问题上反复卡住的从业者。这篇笔记不讲空泛的“算法综述”,而是把双种群的结构设计、迁移策略、参数设置和代码实现拆开,让你能自己跑通一个可复现的版本,并且知道每一步为什么这么做。
2. 双种群结构怎么设计:分工逻辑与三种主流方案
2.1 为什么拆成两个种群比单纯调大种群规模更有效
很多人第一反应是:既然单种群容易早熟,那把种群规模从 100 调到 500 不就行了?实际跑过就知道,规模翻五倍,计算开销翻五倍,但逃逸局部最优的能力并没有线性提升。原因是标准遗传算法的选择压力会把整个种群往当前最优个体附近拉,规模越大,这种“向心收缩”越明显,多样性衰减反而更快。
双种群的核心思路不是“更多个体”,而是“不同搜索行为”。两个子种群可以设置不同的选择压力、不同的交叉变异策略,甚至不同的适应度评价侧重。一个负责快速收敛到有希望的区域,另一个负责维持探索能力、在解空间里游走。两者定期交换少量个体,相当于给收敛种群注入新鲜基因,同时让探索种群获得当前最优解附近的信息。这种结构上的异质性,是单一种群靠调参很难模拟出来的。
常见做法是把两个子种群分别称为“开发种群”和“探索种群”,但名称不重要,关键是它们在选择强度、变异率、交叉方式上要有明确差异。如果两个种群参数完全一样,只是各跑各的,那本质上还是两个独立的标准遗传算法,迁移带来的收益会非常有限。
2.2 三种双种群拓扑:岛模型、捕食者-猎物模型、主从协同模型
落地时绕不开拓扑选择。目前工程上最常见的有三类:
岛模型(Island Model)是最容易实现的。两个子种群各自独立演化若干代,每隔固定代数交换一定比例的个体。交换方式可以是单向的(探索种群向开发种群输送个体),也可以是双向的。它的优点是逻辑简单、并行友好,缺点是迁移时机和迁移量需要调,调不好会出现“迁移即同化”的问题。
捕食者-猎物模型(Predator-Prey)把两个种群定义为捕食关系和被捕食关系,适应度评价中引入相互依赖项。这种模型在动态优化问题里表现更好,因为两个种群会形成此消彼长的动态平衡,不容易同时陷入同一个局部最优。但它的适应度函数设计更复杂,需要额外定义交互项,调参空间也更大。
主从协同模型(Master-Slave)是一个主种群负责全局探索,一个从种群负责局部精修。从种群在主种群给出的候选解附近做精细搜索,主种群根据从种群的反馈调整搜索方向。这种结构在连续函数优化里比较常见,实现上需要设计主从之间的信息传递接口。
我一般会先从岛模型入手,因为它的代码改动量最小,在标准遗传算法基础上加一个迁移函数就能跑起来。如果岛模型效果不理想,再考虑捕食者-猎物或主从结构。下面给出的实现也以岛模型为骨架。
2.3 迁移策略:什么时候换、换多少、换谁
迁移策略是双种群算法里最需要动手调的部分。三个核心参数:迁移间隔、迁移比例、迁移选择方式。
迁移间隔太短,两个种群还没来得及形成各自的搜索特征就被同化了,等于白拆;间隔太长,探索种群积累的有用信息无法及时传递,收敛速度会明显下降。经验上,迁移间隔取总代数的 5% 到 15% 比较稳妥。比如总代数 200,每 10 到 30 代迁移一次。
迁移比例通常取种群规模的 5% 到 20%。比例太高会破坏种群结构,太低则起不到注入新鲜基因的作用。我一般从 10% 开始试,观察适应度曲线是否出现“迁移后明显下降”的台阶,如果有,说明迁移带来了有效扰动。
迁移选择方式有两种常见做法:一种是选各自种群里的最优个体进行交换,这叫精英迁移;另一种是随机选个体迁移,保持随机性。精英迁移收敛快但容易同化,随机迁移多样性好但收敛慢。折中方案是:开发种群向探索种群输送精英个体,探索种群向开发种群输送随机个体。这样开发种群获得扰动,探索种群获得方向。
注意:迁移操作要在两个种群完成各自的选择、交叉、变异之后执行,不要在进化中途插入,否则会打乱各自的演化节奏。
3. 用 Python 跑通一个双种群遗传算法的最小实现
3.1 问题定义与编码:以多峰函数优化为例
为了能直观看到双种群的效果,我选一个经典的多峰函数作为测试对象:Rastrigin 函数。它在二维空间里有大量局部最优,标准遗传算法很容易卡在某个局部峰上。目标是最小化函数值,理论最优解在原点,值为 0。
编码方式用实数编码,每个个体是一个二维向量,取值范围 [-5.12, 5.12]。实数编码比二进制编码更适合连续函数优化,不需要解码步骤,交叉变异操作也更直接。
import numpy as np # Rastrigin 函数,输入 shape=(n,2),输出 shape=(n,) def rastrigin(pop): x = pop[:, 0] y = pop[:, 1] return 20 + (x**2 - 10 * np.cos(2 * np.pi * x)) + (y**2 - 10 * np.cos(2 * np.pi * y)) # 种群初始化 def init_population(size, bounds): return np.random.uniform(bounds[0], bounds[1], size=(size, 2)) BOUNDS = (-5.12, 5.12) POP_SIZE = 60 # 每个子种群的规模 GENERATIONS = 200 # 总演化代数 MIGRATION_INTERVAL = 20 # 每 20 代迁移一次 MIGRATION_RATE = 0.1 # 迁移 10% 的个体这段代码定义了问题和初始化逻辑。rastrigin函数直接接受种群矩阵,返回每个个体的适应度值。init_population在给定边界内均匀采样。参数方面,POP_SIZE设 60 是经过几次试跑后比较稳的值,太小容易早熟,太大计算开销上升但收益不明显。MIGRATION_INTERVAL和MIGRATION_RATE是双种群特有的参数,后面会结合运行结果说明怎么调。
3.2 两个子种群的差异化参数配置
双种群的关键在于两个子种群要有不同的行为特征。我给开发种群设置较高的选择压力和较低的变异率,让它快速向当前最优区域收敛;给探索种群设置较低的选择压力和较高的变异率,让它保持在大范围游走。
# 开发种群参数:选择压力大,变异率低 DEV_PARAMS = { "tournament_size": 5, # 锦标赛选择规模大,选择压力强 "crossover_rate": 0.85, "mutation_rate": 0.05, # 变异率低,偏向开发 "mutation_scale": 0.3 # 变异步长小 } # 探索种群参数:选择压力小,变异率高 EXP_PARAMS = { "tournament_size": 2, # 锦标赛规模小,选择压力弱 "crossover_rate": 0.6, "mutation_rate": 0.25, # 变异率高,偏向探索 "mutation_scale": 1.0 # 变异步长大 }tournament_size控制选择压力:规模越大,适应度高的个体越容易被选中,种群收敛越快。开发种群用 5,探索种群用 2,形成明显差异。mutation_rate和mutation_scale共同决定扰动强度:开发种群用小步长低频率做精细调整,探索种群用大步长高频率做全局跳跃。这两组参数不是唯一解,但作为起点比较可靠。如果你跑的问题维度更高,探索种群的mutation_scale可以适当加大。
3.3 选择、交叉、变异与迁移的完整代码
下面把进化操作和迁移逻辑串起来。代码结构是:每个子种群独立执行一轮选择-交叉-变异,然后每隔MIGRATION_INTERVAL代执行一次迁移。
def tournament_select(pop, fitness, k): # 随机选 k 个个体,返回适应度最好的那个 idx = np.random.choice(len(pop), k, replace=False) best = idx[np.argmin(fitness[idx])] return pop[best].copy() def crossover(p1, p2, rate): if np.random.rand() < rate: alpha = np.random.rand() return alpha * p1 + (1 - alpha) * p2 return p1.copy() def mutate(ind, rate, scale, bounds): if np.random.rand() < rate: ind = ind + np.random.randn(2) * scale ind = np.clip(ind, bounds[0], bounds[1]) return ind def evolve(pop, params, bounds): fitness = rastrigin(pop) new_pop = [] for _ in range(len(pop)): p1 = tournament_select(pop, fitness, params["tournament_size"]) p2 = tournament_select(pop, fitness, params["tournament_size"]) child = crossover(p1, p2, params["crossover_rate"]) child = mutate(child, params["mutation_rate"], params["mutation_scale"], bounds) new_pop.append(child) return np.array(new_pop) def migrate(dev_pop, exp_pop, rate): # 开发种群向探索种群输送精英,探索种群向开发种群输送随机个体 n = int(len(dev_pop) * rate) dev_fitness = rastrigin(dev_pop) exp_fitness = rastrigin(exp_pop) dev_elite_idx = np.argsort(dev_fitness)[:n] exp_random_idx = np.random.choice(len(exp_pop), n, replace=False) # 交换 dev_pop[exp_random_idx], exp_pop[dev_elite_idx] = \ exp_pop[exp_random_idx].copy(), dev_pop[dev_elite_idx].copy() return dev_pop, exp_pop # 主循环 dev_pop = init_population(POP_SIZE, BOUNDS) exp_pop = init_population(POP_SIZE, BOUNDS) best_history = [] for gen in range(GENERATIONS): dev_pop = evolve(dev_pop, DEV_PARAMS, BOUNDS) exp_pop = evolve(exp_pop, EXP_PARAMS, BOUNDS) if (gen + 1) % MIGRATION_INTERVAL == 0: dev_pop, exp_pop = migrate(dev_pop, exp_pop, MIGRATION_RATE) all_fitness = np.concatenate([rastrigin(dev_pop), rastrigin(exp_pop)]) best_history.append(all_fitness.min()) if (gen + 1) % 50 == 0: print(f"Gen {gen+1}, best = {all_fitness.min():.4f}")evolve函数封装了一轮完整的选择-交叉-变异流程,两个子种群用各自的参数调用它。migrate函数实现了非对称迁移:开发种群的精英个体替换探索种群中的随机个体,探索种群的随机个体替换开发种群中的随机个体。这种设计让开发种群获得扰动,探索种群获得方向信息。
主循环里每代分别演化两个种群,到迁移间隔时执行迁移,然后记录全局最优。运行这段代码,你会看到适应度曲线在迁移点附近出现小幅跳变,这正是双种群在起作用。
3.4 运行结果怎么看:收敛曲线与迁移点的对应关系
跑完 200 代,把best_history画出来,重点观察三件事:
第一,前 20 代下降速度。如果开发种群参数合理,前 20 代应该快速下降到 10 以下。如果下降很慢,说明选择压力不够或者变异率太高。
第二,迁移点是否带来明显改善。在MIGRATION_INTERVAL的整数倍代数附近,曲线应该出现小幅下降或至少不上升。如果迁移后曲线反而变差,说明迁移比例太高,破坏了开发种群的结构。
第三,最终收敛值。Rastrigin 函数在二维下,好的双种群实现通常能收敛到 1 以下,接近 0。如果停在 5 以上,说明探索种群的变异强度不够,或者迁移策略需要调整。
我一般会同时跑一个标准遗传算法作为对照,用相同的总评价次数。双种群的优势在 50 代以后会逐渐显现:标准遗传算法曲线趋于平缓时,双种群还能继续下降。
4. 避坑与排查:双种群遗传算法最容易翻车的五个地方
4.1 两个种群参数完全一样,等于没拆
现象:跑出来的结果和标准遗传算法几乎一样,迁移操作看不出效果。
原因:两个子种群用了相同的选择压力、交叉率和变异率,各自演化出来的种群结构高度相似,迁移时交换的个体没有本质差异,相当于把左口袋的东西放到右口袋。
解决:强制让两个种群在至少两个参数上有明显差异。我一般固定选择压力和变异率这两个维度:开发种群锦标赛规模 4 到 6,变异率 0.02 到 0.08;探索种群锦标赛规模 2 到 3,变异率 0.15 到 0.3。先拉开差距,再微调。
4.2 迁移间隔设得太短,种群被同化
现象:迁移后适应度短暂改善,但很快又停滞,两个种群的个体越来越像。
原因:迁移太频繁,探索种群还没来得及积累多样性就被开发种群的精英个体覆盖,两个种群实际上合并成了一个。
解决:把迁移间隔调到总代数的 10% 左右。如果总代数 200,就从 20 代开始试。观察迁移后两个种群的平均适应度差异,如果差异小于 5%,说明同化严重,需要加大间隔或降低迁移比例。
4.3 迁移比例过高,开发种群被拖慢
现象:每次迁移后,全局最优值出现明显回升,需要很多代才能恢复。
原因:迁移比例太高,大量随机个体涌入开发种群,稀释了已经积累的优良基因。
解决:迁移比例从 5% 起步,最多不超过 15%。同时采用非对称迁移:开发种群只向探索种群输送精英,探索种群向开发种群输送随机个体时控制数量。如果问题维度高,迁移比例还要再降。
4.4 探索种群变异步长过大,变成随机搜索
现象:探索种群的适应度长期在很差的水平波动,从不收敛。
原因:变异步长mutation_scale设得太大,子代偏离父代太远,探索种群退化成随机采样,无法积累任何有用信息。
解决:变异步长要和搜索空间尺度匹配。搜索空间半径 5 左右时,步长 0.5 到 1.5 比较合适。如果发现探索种群最优值长期不下降,先把步长减半试一轮。
4.5 只记录全局最优,看不到种群多样性变化
现象:算法跑完了,但不知道两个种群各自在干什么,调参全靠猜。
原因:只监控了全局最优适应度,没有记录两个种群的平均适应度和方差。
解决:每代分别记录开发种群和探索种群的平均适应度、最优适应度、种群方差。如果开发种群方差快速降到接近 0,说明它早熟了;如果探索种群方差一直很高但最优值不降,说明它在无效游走。根据这些指标调参数,比只看一条全局曲线有效得多。
5. 进阶技巧:自适应迁移与多种群扩展
5.1 让迁移间隔和迁移比例随演化阶段自动调整
固定迁移参数在简单问题上够用,但遇到复杂多峰函数,前期需要频繁迁移来交换信息,后期需要减少迁移来保护已收敛的优良结构。一个实用的自适应策略是:迁移间隔随代数线性增大,迁移比例随代数线性减小。
def adaptive_migration_params(gen, total_gen): # 迁移间隔从 10 代逐步增加到 40 代 interval = int(10 + (40 - 10) * gen / total_gen) # 迁移比例从 0.15 逐步降到 0.05 rate = 0.15 - (0.15 - 0.05) * gen / total_gen return interval, rate这个策略的逻辑是:前期两个种群都需要快速获取对方的信息,所以迁移频繁、比例高;后期开发种群已经接近最优区域,频繁迁移会破坏精细搜索,所以间隔拉大、比例降低。实际跑的时候,把主循环里的固定参数替换成这个函数返回的值即可。注意interval要取整,并且至少为 1。
5.2 从双种群扩展到多种群:什么时候值得做
双种群解决了单一种群的早熟问题,但如果问题有多个截然不同的搜索区域,两个种群可能不够。比如多目标优化中,不同 Pareto 前沿区域可能需要不同的搜索策略;再比如约束优化中,可行域被分割成多个不相连的区域。
扩展到多种群时,不要简单地把两个种群复制成四个。有效的做法是:每个种群仍然保持差异化参数,但增加一个“种群角色”维度。比如两个开发种群用不同的交叉策略,两个探索种群用不同的变异分布。迁移拓扑也要从全连接改成环形或随机连接,避免所有种群被同一个最优解吸引。
不过要提醒一句:种群数量增加带来的计算开销是线性的,但收益往往递减。我一般先跑双种群,如果收敛曲线在多个独立运行中仍然频繁陷入不同的局部最优,再考虑扩展到三到四个种群。超过四个种群后,调参复杂度上升很快,需要更系统的实验设计。
5.3 用多次独立运行验证改进是否真实有效
最后说一个容易被忽略但很重要的习惯:不要只跑一次就下结论。遗传算法本身有随机性,单次运行的结果可能好也可能坏。验证双种群是否真的比单种群好,至少跑 20 次独立实验,比较平均最优值、标准差和达到指定精度所需的平均代数。
我自己的做法是写一个批量运行脚本,固定随机种子范围,分别跑标准遗传算法和双种群遗传算法,输出对比表格。如果双种群在 20 次运行中的平均最优值明显更低,且标准差更小,才能说明改进是稳定的。如果只是某一次跑得好,那大概率是运气。
这个习惯帮我避免过好几次“看起来有效但实际是随机波动”的误判。算法改进这件事,最怕的就是拿一次结果当结论。希望帮到你。
本文还有配套的精品资源,点击获取