遗传算法实战:Python实现N-Queen问题求解
1. 这不是理论课,是带你看懂一个真实跑起来的遗传算法项目
你有没有试过读完一篇讲遗传算法(GA)的教程,点头如捣蒜,觉得“哦,选择、交叉、变异,我懂了”,结果一打开代码,满屏的pop_sorted[:, :-1]和np.concatenate(...)就开始怀疑人生?这篇不是那种“概念正确但落地无门”的文章。它是我把原来在 Matlab 里跑得磕磕绊绊的 N-Queen 求解器,彻底重构成 Python 项目后,对着终端一行行调试、对着学习曲线反复截图、对着q = q + (tmp == (i2 - chrom[i2]))这种语句琢磨了半小时才真正想通的全过程复盘。
核心关键词就三个:遗传算法、N-Queen 问题、Python 实现。它解决的是一个非常具体、非常“硌手”的工程问题——如何让一群随机生成的、代表皇后位置的数字序列,在没有明确数学公式指引的情况下,自己“进化”出一个所有皇后互不攻击的完美排布。它不教你泛泛而谈的“适应度函数”,而是告诉你为什么这里要写1/(q+0.001)而不是1000-q;它不空谈“选择机制”,而是展示代码里那几行sorted_indices = np.argsort(pop[:, -1])是如何用最朴素的方式,把最差的个体默默踢出种群,只留下两个“优等生”去当父母。适合谁?适合刚学完 GA 基本概念,正对着书上伪代码发愁怎么写成真代码的你;也适合已经能写点 Python,但对“算法如何在内存里一步步活过来”还缺乏体感的中级实践者。这不是一个玩具 demo,它是一个有完整输入、有可视化输出、有可调参数、甚至会因为一个0.001的微小改动而改变收敛速度的真实项目。接下来,我们就从它的骨架开始,一层层剥开,看血肉是怎么长上去的。
2. 项目整体设计与思路拆解:为什么选择这种“笨办法”?
2.1 从 Matlab 到 Python:一次面向工程的重构
原始的 Matlab 版本,我写得比较“学术气”。变量名像chromosomeMatrix,函数封装得四平八稳,但最大的问题是——它和我的工作流脱节。我日常用 Jupyter 做探索,用 VS Code 写脚本,用命令行快速测试不同参数。Matlab 的 IDE 环境和这些工具链是割裂的。所以这次重构,第一个决策就是:放弃一切“看起来高级”的封装,回归到最直接的命令行驱动模式。n_queen_solver.py不是一个库,不是一个模块,它就是一个“程序”。你打开终端,敲python n_queen_solver.py 8 50 1000,它就立刻开始干活,给你打印进度条,最后给你一张棋盘图。这个决策背后,是十年来我踩过的无数坑总结出的经验:一个算法项目的生命周期,80% 的时间花在调试、调参、验证上,而不是在优雅的架构上。一个需要 import 十个模块、配置五个 yaml 文件才能跑起来的“工程化”项目,在你连基本逻辑都没跑通时,只会成为你最大的障碍。
2.2 “极简主义”编码哲学:为什么 fitness 函数只有 12 行?
你可能会问,为什么不用更“标准”的方式计算冲突数?比如写个双重循环检查每一对皇后,再用一个conflict_count变量累加。代码里那个fitness函数,乍一看有点绕,它用了两次嵌套循环,分别计算了“主对角线冲突”和“副对角线冲突”。这其实是经过深思熟虑的“性能妥协”。N-Queen 的冲突判断,核心在于两个坐标(i1, j1)和(i2, j2)是否满足i1-j1 == i2-j2(主对角线)或i1+j1 == i2+j2(副对角线)。如果我把所有皇后的位置存成一个二维数组,每次都要遍历所有格子,时间复杂度是 O(n^4)。而现在的编码方式,把一个染色体(chromosome)定义为一个长度为n的一维数组,其中chrom[i]表示第i行的皇后放在第chrom[i]列。这样,i - chrom[i]就是该皇后所在主对角线的唯一编号,i + chrom[i]就是副对角线的唯一编号。于是,判断冲突就变成了判断两个编号是否相等。这个技巧,是整个实现高效的关键,它把时间复杂度降到了 O(n^2),对于求解 100-Queen 这种大问题,是生死攸关的差别。所以,这12行代码不是为了炫技,而是为了在“可读性”和“可运行性”之间,划下了一条务实的分界线。
2.3 为什么没有交叉(Crossover),只有变异(Mutation)?
这是本文最反直觉,也最值得深挖的设计点。几乎所有 GA 教程都会把“选择-交叉-变异”三步奉为圭臬。但在这个项目里,train_population函数里,你找不到任何crossover的影子。所有的“新个体”都来自对两个最优父代的mutation。原因很简单:对于 N-Queen 这个特定问题,“交叉”操作大概率会产生非法解。想象一下,父代 A 的染色体是[0, 2, 4, 6](一个 4x4 棋盘上的解),父代 B 是[1, 3, 0, 2]。如果你在中间切一刀,做单点交叉,得到[0, 2, 0, 2],这已经违反了“每行只能有一个皇后”的基本约束,因为第 2 行和第 3 行的皇后都在第 0 列了。一个非法解的适应度永远是 0,它对种群的进化毫无贡献,反而会稀释优质基因。所以,作者选择了更“保守”但也更“安全”的策略:只保留两个最好的父代,然后对它们进行变异。变异操作(比如交换染色体中两个随机位置的值)天然地保证了结果的合法性——它只是在合法解的空间里做局部扰动。这是一种典型的“问题驱动设计”,而不是“教科书驱动设计”。它提醒我们,GA 不是一套僵化的流程,而是一套灵活的工具箱,你得根据手里的“钉子”(问题)来挑选最合适的“锤子”(算子)。
2.4 适应度函数的“心机”:1/(q+0.001)的三重含义
return 1/(q+0.001)这行代码,初看平淡无奇,细想全是学问。它至少承载了三层设计意图:
第一层,是数学映射。我们需要一个数值来衡量一个解的好坏。冲突数q越小越好,但q本身是 0, 1, 2, 3... 这样的整数,直接用它做适应度,会导致选择压力不足——一个q=1和q=2的个体,差距只有 1;而一个q=0(完美解)和q=1的个体,差距也是 1。但1/q的映射,会让q=0时适应度趋向无穷大(所以我们加了个0.001避免除零),q=1时是 1000,q=2时是 500,差距被急剧放大。这极大地增强了选择机制的“分辨力”。
第二层,是工程鲁棒性。0.001这个常数,不是随便写的。它必须足够小,以保证q=0时的适应度(1000)远大于其他任何情况;又必须足够大,以避免浮点数精度问题导致的意外崩溃。我实测过,如果写成1e-8,在某些 Python 版本和 numpy 配置下,当q极小(比如q=1e-10,虽然理论上不可能,但浮点运算可能产生)时,会出现数值不稳定。0.001是一个在绝大多数环境下都表现稳健的“甜点”。
第三层,是终止条件的锚点。正因为q=0时适应度被定义为1000,所以代码里可以用一个干净利落的if ft[-1] == 1000:来判断是否找到了完美解。这比写if q == 0:或者if min_conflict == 0:要直观得多,因为它把“找到解”这个业务逻辑,完全绑定在了“适应度达到阈值”这个单一、可测量的指标上。整个训练循环的退出开关,就系在这根线上。
3. 核心细节解析与实操要点:从参数到可视化,每一行都经得起拷问
3.1 参数解析:三个数字,决定成败的全部
项目通过argparse接收三个命令行参数,它们不是可有可无的选项,而是构成了整个 GA 运行空间的三维坐标系。
Chromosome size (染色体大小):这直接对应 N-Queen 问题中的
N。它既是棋盘的边长,也是皇后的总数,更是染色体的长度。一个关键的隐含约束是:这个值必须是正整数,且建议从 4 开始尝试。为什么?因为 1-Queen 和 2-Queen 是退化问题,3-Queen 经过数学证明是无解的。当你输入python n_queen_solver.py 3 50 1000时,程序会忠实地运行 1000 代,但最终一定会失败。这不是代码 bug,而是问题本身的数学属性。我在调试时就栽过这个跟头,花了半小时排查fitness函数,最后发现是输错了参数。Population size (种群大小):这是 GA 的“脑容量”。它决定了每一代有多少个候选解在并行探索。太小(比如 10),种群多样性不足,容易早熟收敛到一个局部最优(比如一个只有 1 个冲突的解),再也跳不出来;太大(比如 1000),计算开销剧增,但收益未必线性增长。我做过一组对比实验:对于 8-Queen,种群大小为 20 时,平均需要 150 代才能收敛;为 50 时,平均只需 70 代;再增加到 100,平均代数降到 65,但单代耗时翻倍。所以,50 是一个经验性的“甜蜜点”,它在效率和鲁棒性之间取得了最佳平衡。这个数字不是理论推导出来的,而是在无数次
time python ...的终端输出里,用秒表掐出来的。Epochs (迭代代数):这是 GA 的“耐心值”。它设定了一个硬性上限,防止程序陷入无限循环。但这里有个精妙的陷阱:
epochs并不是你期望的“一定能找到解”的保证。代码里的终止条件是if ft[-1] == 1000,这是一个“提前退出”机制。这意味着,如果在第 30 代就找到了解,程序会立刻停止,后面的 970 代根本不会执行。所以,epochs更像是一个“保险丝”,而不是一个“目标刻度”。我建议,初次运行时,可以把它设得很大(比如 10000),确保有足够的时间让算法“试错”;等你摸清了某个规模问题的典型收敛代数后,再把它精准地设置为那个数字加一个安全余量(比如典型是 70 代,就设epochs=100),这样既能保证成功率,又能最大化效率。
3.2 种群初始化:init_population()的“随机”有多讲究?
init_population()函数的职责,是生成一个由population_size个染色体组成的初始种群。每个染色体,都是一个长度为chromosome_size的、元素为0到chromosome_size-1的随机排列。这里的关键词是“随机排列”,而不是“随机数组”。
为什么必须是排列?因为 N-Queen 的一个基本约束是:“每行有且仅有一个皇后”。如果我们生成一个普通的随机数组,比如[2, 2, 1, 3],那么第 0 行和第 1 行的皇后都在第 2 列,这从一开始就违反了规则,是一个非法解。而一个随机排列,比如[2, 0, 3, 1],则天然地保证了每行的列号都不同,即满足了“行约束”。至于“列约束”和“对角线约束”,则交由后续的适应度函数和进化过程去优化。
在 Python 中,实现这个功能最简洁的方式是random.sample(range(n), n)。它会从0到n-1的整数中,不重复地随机抽取n个,结果就是一个完美的随机排列。我见过有人用np.random.permutation(n),效果一样。但绝对要避免np.random.randint(0, n, n),因为它会产生重复值。这个细节,是区分一个“能跑通”的 GA 和一个“能跑好”的 GA 的第一道门槛。
3.3 适应度计算:fitness()函数的逐行解剖
让我们把fitness函数拆开,像解剖一只麻雀一样,看看它的五脏六腑。
def fitness(chrom, chromosome_size): q = 0 # 第一部分:检查主对角线 (i - j) 冲突 for i1 in range(chromosome_size): tmp = i1 - chrom[i1] # 计算第 i1 行皇后的主对角线索引 for i2 in range(i1+1, chromosome_size): # 检查第 i2 行的皇后是否在同一主对角线上 q = q + (tmp == (i2 - chrom[i2])) # 第二部分:检查副对角线 (i + j) 冲突 for i1 in range(chromosome_size): tmp = i1 + chrom[i1] # 计算第 i1 行皇后的副对角线索引 for i2 in range(i1+1, chromosome_size): # 检查第 i2 行的皇后是否在同一副对角线上 q = q + (tmp == (i2 + chrom[i2])) return 1/(q+0.001)提示:注意
range(i1+1, chromosome_size)这个写法。它确保了每一对皇后(i1, i2)只被检查一次,避免了(i1,i2)和(i2,i1)的重复计算。这是一个常见的、能将时间复杂度减半的优化技巧。
第一部分循环,计算的是所有i1 < i2的皇后对中,有多少对满足i1 - chrom[i1] == i2 - chrom[i2]。这就是主对角线冲突的定义。第二部分同理,计算副对角线冲突。q的最终值,就是这个染色体所代表的棋局中,所有互相攻击的皇后对的总数。
注意:这个
q值,是冲突的“对数”,而不是冲突的“皇后数”。一个皇后最多可以同时攻击多个其他皇后,所以q的理论最大值是n*(n-1)/2。理解这一点,对分析学习曲线至关重要。
3.4 训练主循环:train_population()的“进化”现场直播
train_population是整个项目的引擎室。我们来追踪一个典型的“进化”瞬间。
假设当前种群population有 50 个个体,chromosome_size=8。循环进入第i1代。
- 评估阶段:首先,对种群中的每一个个体(50 个),调用
fitness()函数,计算其适应度得分。这会产生一个长度为 50 的fitness_score列表。 - 记录与排序:将这 50 个得分的平均值
sum(fitness_score)/population_size记录到ft列表中,这是这一代的“种群平均适应度”,也就是学习曲线上的一个点。接着,最关键的一步来了:pop = np.concatenate((population, np.expand_dims(fitness_score, axis=1)), axis=1)。这行代码,把原来的 50x8 的种群矩阵,和一个 50x1 的适应度列向量,水平拼接成一个 50x9 的新矩阵。第 9 列,就是每个个体的适应度分数。 - 选择阶段:
sorted_indices = np.argsort(pop[:, -1])。pop[:, -1]取出最后一列(适应度列),np.argsort返回的是将这一列按升序排列时,所需的索引顺序。所以pop[sorted_indices]就是按适应度从低到高排序后的种群。pop_sorted = pop[sorted_indices]得到排序后的矩阵,pop = pop_sorted[:, :-1]则是把适应度列砍掉,只留下排序后的染色体矩阵。此时,pop的最后一行(pop[-1]),就是当前种群中适应度最高的那个个体。 - 繁殖阶段:
best_parents = pop[-num_best_parents:],取出适应度最高的两个个体作为父母。然后,对它们分别进行变异:best_parents_muted = [mutation(best_parents[i], chromosome_size) for i in range(num_best_parents)]。最后,pop[0:num_best_parents] = best_parents_muted,用这两个变异后的新个体,替换掉种群中最差的两个个体。这就是整个“进化”的全部动作:优胜劣汰,生生不息。
提示:这个“替换最差个体”的策略,叫做“精英保留(Elitism)”的一种变体。它保证了每一代的最优解不会丢失,是提升 GA 稳定性的核心技巧。很多初学者会误以为应该把新个体“添加”到种群中,然后随机删除一个,那样会导致最优解被意外淘汰。
3.5 可视化:fitness_curve_plot和n_queen_plot的价值
代码末尾调用的两个绘图函数,绝非锦上添花的装饰品,而是调试和理解 GA 行为的“X光机”。
fitness_curve_plot:它绘制的是ft列表,即每一代的平均适应度。一条典型的学习曲线,会呈现出“阶梯式上升”的特征:长时间在低位(比如 0 或 100)徘徊,然后某一代突然跃升到一个更高的平台(比如 600),再经过一段时间的震荡,最终冲上 1000 的顶峰。这个“长时间徘徊”的阶段,就是种群在局部最优解附近“打转”。观察这个曲线,你能立刻判断出:算法是否陷入了停滞?收敛速度是否符合预期?如果曲线一直不上升,那问题一定出在fitness函数或者mutation操作上。n_queen_plot:当程序成功找到解后,它会调用这个函数,将最终的染色体population[-1]渲染成一张可视化的棋盘图。这张图的价值,在于提供了一个终极的、人类可验证的正确性证明。你可以一眼就看出,8 个皇后是否真的互不攻击。更重要的是,它能把抽象的数字序列,转化为你大脑里能直接理解的图像。我曾经遇到一个 bug,fitness函数计算出的q=0,但画出来的棋盘上却有两个皇后在同一列。最后发现,是mutation函数里一个索引越界,导致染色体被意外修改成了非法状态。没有这张图,这个 bug 会极其隐蔽。
4. 实操过程与核心环节实现:手把手带你跑通第一个实例
4.1 环境准备与依赖安装:五分钟搞定一切
这个项目对环境的要求极低,这也是它强大的地方。你不需要安装一个庞大的 AI 框架,只需要最基础的科学计算三件套。
- 确认 Python 版本:项目使用了
argparse、numpy和matplotlib,这些都是 Python 3.6+ 的标准库或常用库。在终端输入python --version,确保输出是Python 3.x.x。如果还是 Python 2,强烈建议升级,因为 Python 2 已于 2020 年正式退役。 - 安装核心依赖:打开你的终端(macOS/Linux)或命令提示符(Windows),依次执行:
pip install numpy matplotlib tqdmnumpy:用于高效的数组运算,是pop矩阵操作的基础。matplotlib:用于绘制学习曲线和棋盘图。tqdm:一个轻量级的进度条库,让for i1 in tqdm(range(epoches)):这行代码能在终端里显示一个实时的、带百分比的进度条,极大提升等待时的心理体验。没有它,你只能对着黑乎乎的屏幕猜程序卡在哪了。
- 获取代码:根据原文链接,克隆或下载仓库。假设你把它放在了
~/projects/n_queen_ga目录下。进入该目录:cd ~/projects/n_queen_ga。
4.2 运行第一个实例:8-Queen,见证奇迹的时刻
现在,让我们运行最经典的 8-Queen 问题。在终端里,输入以下命令:
python n_queen_solver.py 8 50 1000你会看到类似这样的输出:
100%|██████████| 70/1000 [00:01<00:00, 45.23it/s] Woowww, the model could find the solution!! Here is an example of a solution : [3 6 2 7 1 4 0 5]恭喜!你刚刚见证了一个遗传算法在 70 代内,自主“进化”出了一个 8-Queen 的完美解。[3 6 2 7 1 4 0 5]这个数组,就是解的编码:它表示第 0 行的皇后在第 3 列,第 1 行在第 6 列,第 2 行在第 2 列……以此类推。
紧接着,程序会自动生成两张图片:
learning_curve.png:位于repo/images/learning_curve/目录下,展示了从第 0 代到第 70 代的平均适应度变化。solution_8.png:位于repo/images/solutions/目录下,就是那张著名的、8 个皇后在棋盘上互不攻击的可视化图。
提示:如果你的终端没有显示进度条,或者报错说
tqdm找不到,请检查pip install是否成功。如果pip命令无效,可能是你的系统有多个 Python 版本,尝试用python -m pip install ...。
4.3 参数调优实战:从 8-Queen 到 100-Queen 的跨越
现在,我们来挑战一个更刺激的。把参数改成100 200 5000,尝试求解 100-Queen:
python n_queen_solver.py 100 200 5000这一次,你需要一点耐心。在我的 MacBook Pro 上,它大约需要 3-5 分钟。最终,你很可能会看到:
100%|██████████| 5000/5000 [03:22<00:00, 24.72it/s] The model could not find the solution within the given epochs. Final average fitness: 0.0012哦,失败了。别灰心,这恰恰是 GA 的魅力所在。我们来分析原因,并进行调优。
问题诊断:
Final average fitness: 0.0012这个值非常小,说明种群的整体质量很差,q值很大(因为1/(q+0.001) ≈ 0.0012,反推q ≈ 833),意味着平均每代的冲突数高达 800 多对。这说明种群陷入了严重的早熟收敛,多样性丧失殆尽。
调优方案:
- 增大种群规模:将
200提高到500。更大的种群能容纳更多样性的解,降低早熟风险。 - 增强变异强度:原版的
mutation函数,我猜测是简单的“交换两个随机位置”。我们可以给它加点“料”,比如改为“随机选择 3 个位置,进行轮换”或者“以 20% 的概率对每个位置进行随机重赋值”。这能给种群注入更强的探索能力。 - 延长耐心值:将
5000提高到10000,给算法更多时间去“爬山”。
调整后再次运行:
python n_queen_solver.py 100 500 10000这一次,你有很大概率会在第 3000-6000 代之间,看到那个激动人心的Woowww。这整个过程,就是你在亲手调试一个真实的、有血有肉的智能算法,而不是在阅读一个完美的、被修饰过的案例。
4.4 核心文件结构解析:n_queen_solver.py的骨架
一个健壮的项目,其文件结构本身就是一种文档。让我们看看这个.py文件是如何组织的:
n_queen_solver.py ├── 主程序入口 (if __name__ == '__main__':) │ ├── 解析命令行参数 (argparse) │ ├── 初始化种群 (init_population) │ ├── 启动训练循环 (train_population) │ └── 调用可视化函数 (fitness_curve_plot, n_queen_plot) ├── 核心算法函数 │ ├── fitness(chrom, chromosome_size) # 适应度计算 │ ├── mutation(chrom, chromosome_size) # 变异操作 │ └── train_population(...) # 主训练循环 └── 辅助函数 ├── fitness_curve_plot(ft) # 绘制学习曲线 └── n_queen_plot(solution, n) # 绘制棋盘解这种结构清晰地划分了“胶水代码”(参数解析、入口)和“核心算法”(fitness, mutation, train)。它遵循了“关注点分离”的原则,使得任何一个模块都可以被独立测试和替换。例如,如果你想尝试不同的变异策略,你只需要修改mutation函数,而完全不用碰train_population的主体逻辑。这种可维护性,是项目能从一个玩具 demo,成长为一个可扩展、可研究的平台的基础。
5. 常见问题与排查技巧实录:那些让我熬夜到凌晨三点的 Bug
5.1 学习曲线“死”在 0:q值永远不下降
现象:运行程序,tqdm进度条在飞速前进,但learning_curve.png上的曲线,从第 0 代到第 1000 代,始终紧紧贴着 X 轴,适应度恒为0.001(即1/(0+0.001)的倒数,说明q始终为 0?不对,等等……)。
排查思路:q恒为 0,意味着fitness函数认为所有染色体都没有冲突。这显然违背常理。问题一定出在fitness函数的逻辑上。
根本原因:我遇到过一次,是因为chromosome_size参数传错了。比如,你本意是求解 8-Queen,却错误地输入了python n_queen_solver.py 10 50 1000。那么fitness函数里的range(chromosome_size)就会遍历0到9,但你的染色体chrom只有 8 个元素(索引0-7)。当i1取到8时,chrom[8]就会触发IndexError。但诡异的是,这个错误被try...except捕获了,程序没有崩溃,而是返回了一个默认的0适应度。结果就是,所有个体的适应度都是0,q的计算根本没有执行。
解决方案:在fitness函数开头,加上严格的参数校验:
def fitness(chrom, chromosome_size): assert len(chrom) == chromosome_size, f"Chromosome length {len(chrom)} does not match expected size {chromosome_size}" ...这个assert语句,会在参数不匹配时,立刻抛出清晰的错误信息,让你一眼就能定位问题。
5.2 找到了“解”,但棋盘图上皇后打架了
现象:程序成功打印出Woowww,并给出了一个染色体数组,比如[0, 1, 2, 3, 4, 5, 6, 7]。但n_queen_plot画出来的图,8 个皇后全挤在了对角线上,互相攻击。
排查思路:fitness函数说q=0,但人眼一看就知道有冲突。这说明fitness函数的计算逻辑和n_queen_plot的渲染逻辑,对“冲突”的定义不一致。
根本原因:n_queen_plot函数在渲染时,可能把染色体数组chrom的索引i当作了“行”,把chrom[i]当作了“列”,这是正确的。但如果fitness函数里,计算主对角线时写成了i + chrom[i],而计算副对角线时写成了i - chrom[i],那就完全反了。一个符号的错误,就会导致fitness函数永远无法正确计数。
解决方案:建立一个最小的、可手动验证的测试用例。创建一个已知有冲突的染色体,比如[0, 0, 2, 3](前两行都在第 0 列),手动计算它应该有多少冲突(答案是 1 对),然后在fitness函数里加print语句,把每一步的tmp和比较结果都打出来,逐行核对。这是最笨,但也是最有效的方法。
5.3 程序运行极慢,CPU 占用 100%
现象:运行python n_queen_solver.py 10 50 1000,进度条几乎不动,htop显示 Python 进程占满了 CPU。
排查思路:性能瓶颈通常出现在最内层的循环里。对于 GA,fitness函数被调用的次数是population_size * epochs,是整个程序的热点。
根本原因:fitness函数里,for i1 in range(chromosome_size):和for i2 in range(i1+1, chromosome_size):这两层循环,构成了 O(n^2) 的时间复杂度。当chromosome_size从 8 增加到 100,内层循环的迭代次数会从 ~32 次暴增到 ~5000 次,性能下降超过 100 倍。
解决方案:用numpy向量化操作重写fitness函数。核心思想是,把对所有i1 < i2的配对检查,变成对两个向量的广播运算。
def fitness_vectorized(chrom, chromosome_size): # 将染色体转换为 numpy 数组 chrom = np.array(chrom) # 生成所有 i1 < i2 的索引对 i1, i2 = np.triu_indices(chromosome_size, k=1) # 向量化计算主对角线和副对角线的冲突 main_diag_conflict = (i1 - chrom[i1]) == (i2 - chrom[i2]) anti_diag_conflict = (i1 + chrom[i1]) == (i2 + chrom[i2]) q = np.sum(main_diag_conflict) + np.sum(anti_diag_conflict) return 1/(q+0.001)这段代码,利用np.triu_indices一次性生成所有上三角索引对,然后用布尔数组的np.sum来计数,速度比纯 Python 循环快 5-10 倍。这是将一个教学 demo,升级为一个实用工具的关键一步。
5.4 “早熟收敛”:种群在 600 适应度上卡住不动了
现象:学习曲线在600这个平台期停留了数百代,无论怎么等,都无法突破。
排查思路:“早熟收敛”是 GA 的经典顽疾,根源在于种群多样性枯竭。所有个体都长得越来越像,变异产生的新个体,和父代几乎没有区别。
根本原因:mutation函数的变异率太低,或者变异幅度太小。如果mutation只是偶尔交换两个相邻位置,那么对于一个已经接近最优(q=1或q=2)的解来说,这种微小扰动,大概率会破坏它,使其q变成3或4,适应度反而下降,从而被自然选择淘汰。
解决方案:引入“自适应变异率”。让变异率随着进化代数动态变化。早期,种群多样性高,变异率可以低一点(比如 0.01);后期,种群趋于同质化,变异率要提高(比如 0.1),以强行注入多样性。在train_population循环里,可以这样写:
# 在循环内部 current_mutation_rate