N皇后遗传算法实战:Python模块化实现与调参避坑指南
1. 这不是教科书,而是一次真实的GA项目复盘:从Matlab到Python的N皇后实战手记
你点开这篇文章,大概率不是为了背诵“遗传算法是模拟生物进化过程的优化方法”这种定义。你真正想搞清楚的是:当一个真实项目摆在面前——比如用遗传算法解100个皇后的棋盘布局——代码到底怎么写?参数为什么这么设?为什么跑着跑着突然卡在600分不动了?为什么改一行fitness函数,整个收敛曲线就全乱套?这些在论文里不会写、在教程里被跳过的“现场感”,才是我今天要掏心窝子分享的。
我叫Hossein Chegini,过去十年里,我用遗传算法做过芯片布线优化、做过物流路径规划、也做过工业传感器数据异常检测。但最让我反复调试、拍过桌子、也笑出声的,还是这个看似简单的N皇后问题。它像一面镜子,照出GA所有核心机制的真实表现:编码是否合理,适应度函数是否真正反映问题本质,选择压力是否足够又不过头,变异强度是否恰到好处。这篇文章,就是我把那个放在GitHub上、被上百人star、也收到过二十多条issue的Python仓库,掰开了、揉碎了,把每一行关键代码背后踩过的坑、算过的账、调过的参,原原本本告诉你。它不讲抽象理论,只讲你明天就能打开终端、复制粘贴、亲眼看到100个皇后如何在棋盘上“进化”出来的全过程。如果你正打算用GA解决一个实际工程问题,或者刚学完概念却对“怎么落地”毫无头绪,那这篇就是为你写的——它不承诺让你成为理论专家,但能确保你下次写GA代码时,心里有底,手上不慌。
2. 项目整体设计与思路拆解:为什么选这个结构,而不是别的?
2.1 从Matlab到Python:一次彻底的“工程化”重构
上一篇介绍GA基础原理的文章发布后,我立刻意识到:光讲概念远远不够。读者需要一个能立刻运行、能修改、能debug的完整项目。当时我的Matlab原型代码,是典型的科研快糙猛风格:函数堆在一个文件里,参数硬编码,绘图和逻辑混在一起,连注释都带着“这里应该优化”的TODO。把它直接扔给读者,无异于递过去一把没开刃的刀。
所以,这次重构的核心目标只有一个:让GA的每一个环节,都变成一个可独立理解、可单独测试、可清晰替换的模块。这不是为了炫技,而是因为GA的本质决定了它必须如此。你看,编码(encoding)决定了问题如何被“翻译”成染色体;适应度(fitness)是整个进化的“指南针”;选择(selection)、交叉(crossover)、变异(mutation)是驱动进化的“引擎”。任何一个环节出问题,整个系统就会失灵。如果它们都挤在main函数里,你根本无法判断是编码错了,还是适应度函数把好解判成了坏解,抑或是变异太猛把好不容易积累的优良基因全搅乱了。
因此,我彻底放弃了Matlab那种“脚本式”写法,采用Python标准的模块化结构。n_queen_solver.py只是个指挥官,它只做三件事:解析用户输入的参数、调用各个模块、汇总结果。真正的“活儿”,全交给独立的函数:init_population()负责造人(初始化种群),fitness()负责打分(评估个体优劣),mutation()负责微调(引入新变化)。这种设计,意味着你可以轻松地把fitness()换成另一个更复杂的冲突检测逻辑,或者把mutation()换成高斯扰动,而完全不用碰主流程。这正是工程实践和学术演示的根本区别:前者追求鲁棒和可维护,后者追求简洁和可解释。
2.2 为什么是“100-Queen”?规模跃迁带来的真实挑战
很多教程用4皇后或8皇后演示,这没问题,它能快速验证逻辑。但当你把规模拉到100,一切就变了。这不是简单的数字变大,而是量变引发的质变。
首先,搜索空间爆炸式增长。8皇后的合法解只有92个,而100皇后的理论解数量是天文数字(远超10^100)。这意味着,随机初始化的种群,几乎不可能包含一个接近最优的个体。你的算法必须具备强大的“爬坡”能力,能从一片混沌中,一点点筛选、组合、变异,最终找到那条狭窄的可行路径。
其次,适应度函数的“分辨率”变得至关重要。在8皇后中,一个解有3个冲突和有0个冲突,差距巨大,算法很容易区分好坏。但在100皇后中,一个“好”解可能有5个冲突,一个“差”解可能有200个冲突。如果适应度函数不能精细地区分5和10的差别,算法就会陷入停滞——它觉得“5个冲突”已经很好了,没必要再折腾。这就是原文中提到的“卡在600分”的真实原因:那个600分,对应着一个有1.67个冲突的解(因为1/(1.67+0.001)≈0.6),算法误以为这就是山顶。
最后,计算效率成为瓶颈。每一代都要对整个种群计算适应度,而每个适应度计算本身就是一个O(n²)的双重循环(检查每一对皇后)。对于100皇后、种群大小为200的配置,单次适应度计算就要进行约10,000次比较。如果一代要算200次,那就是200万次比较。没有合理的向量化和缓存,程序会慢得让人绝望。所以,我在代码里大量使用NumPy数组操作,避免Python原生for循环,这是100皇后能跑起来的底层保障,而不是什么高级技巧。
2.3 核心架构选择:为什么没有交叉(Crossover)?
这是读者最容易提出的问题,也是我重构时纠结最久的决定。标准GA教材里,“选择-交叉-变异”是铁三角。但在这个N皇后项目里,我完全移除了交叉操作。这不是偷懒,而是基于对问题本质的深刻理解后,做出的主动取舍。
原因很简单:N皇后问题的解,其“优良性”具有高度的局部耦合性,而非全局可分割性。什么意思?想象两个都还不错的解:解A的前50列排布极佳,几乎没有冲突;解B的后50列排布极佳。如果我把它们“交叉”,比如前50列用A,后50列用B,得到的新解,极大概率会在第50列和第51列的交界处产生大量新的冲突。因为A的第50列皇后位置,是和A的后50列共同“协商”出来的平衡点;B同理。强行拼接,就像把两台不同型号发动机的零件硬拧在一起,只会导致系统崩溃。
我做过对比实验。加入单点交叉后,算法收敛速度反而变慢,且更容易陷入局部最优。因为交叉产生的大量“畸形”后代,其适应度往往比父代还差,它们不仅没带来新信息,还稀释了种群中宝贵的优良基因。相比之下,聚焦于高质量父代的“精英变异”(Elitist Mutation)策略,效果更稳定、更可预测。我们只挑出最好的2个父代(num_best_parents = 2),然后对它们分别施加变异,再把变异后的结果放回种群顶端。这相当于让最优秀的“种子选手”去尝试各种微创新,而不是让两个半吊子去“联姻”。这是一种更符合N皇后问题特性的、务实的工程选择。
3. 核心细节解析与实操要点:代码里的每一个字都有它的道理
3.1 编码方案(Encoding):一维数组为何是N皇后的最优解?
在GA里,“编码”是第一步,也是最关键的一步。它决定了你的算法能否理解问题。N皇后问题,直观上是一个二维棋盘,但用二维数组(100x100的矩阵)来编码,是灾难性的。
为什么?因为一个100x100的矩阵,有10,000个元素,每个元素是0或1(表示有无皇后)。这意味着一个染色体有10,000位基因!而其中合法的解,必须满足“每行每列恰好一个1”,这构成了极其严苛的约束。GA的随机变异操作,几乎必然破坏这个约束,产生“某行没有皇后”或“某列有两个皇后”的非法个体。处理这些非法个体,要么丢弃(浪费计算资源),要么惩罚(扭曲适应度函数),都非常麻烦。
我们采用的是一维数组编码,[3, 1, 4, 2]代表4皇后的一个解:第0行皇后在第3列,第1行在第1列,以此类推。这个方案的精妙之处在于,它天然满足了“每行一个皇后”的约束。因为我们数组的索引(index)就代表了行号,而数组的值(value)就代表了列号。只要我们保证数组是一个1到n的排列(permutation),那么“每列一个皇后”的约束也就自动满足了。
所以,init_population()函数的核心任务,就是生成一堆随机排列。我用的是np.random.permutation(chromosome_size),它高效、简洁、且100%保证合法性。你可能会问:“那变异操作会不会破坏排列?”答案是:会。所以mutation()函数在变异后,必须进行修复。我的做法是:随机交换数组中两个位置的值。这本质上是一种“交换变异”(Swap Mutation),它作用于排列上,结果永远是一个新的、合法的排列。例如[1,2,3,4]交换位置0和2,得到[3,2,1,4],依然是一个完美的1-4排列。这个小小的细节,省去了90%的约束处理工作,是整个项目能顺利运转的基石。
3.2 适应度函数(Fitness Function):为什么是1/(q+0.001),而不是其他形式?
这是全文最核心、也最容易被误解的一段代码。让我们逐行拆解:
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): q = q + (tmp == (i2 - chrom[i2])) # 如果另一皇后在同一主对角线,q加1 # 检查副对角线冲突 (i + j 为常数) for i1 in range(chromosome_size): tmp = i1 + chrom[i1] # 计算第i1行皇后的副对角线索引 for i2 in range(i1+1, chromosome_size): q = q + (tmp == (i2 + chrom[i2])) # 如果另一皇后在同一副对角线,q加1 return 1/(q+0.001)第一眼,你可能觉得q就是冲突总数。没错,但它只计算了对角线冲突,而完全忽略了同行、同列冲突。这合理吗?非常合理。因为我们的编码方案(一维排列)已经100%保证了“每行一个、每列一个”,所以同行同列冲突根本不可能发生。q,就是这个解的全部缺陷所在。
那么,为什么返回1/(q+0.001)?这里有两个深意。
第一,方向性:GA的“选择”操作,总是倾向于选择适应度值更高的个体。而我们希望冲突越少的解,适应度越高。q越小,1/q越大,完美匹配这个需求。如果直接用q,那算法就会疯狂追逐冲突最多的解,彻底南辕北辙。
第二,数值稳定性与尺度:q的理论最小值是0(完美解),最大值呢?对于100皇后,最差的情况是所有皇后都在同一对角线上,q可以达到约5000(C(100,2)的一半)。如果我们用1/q,那么完美解的适应度是无穷大(1/0),这在计算机里是inf,会导致后续计算崩溃。而最差解的适应度是1/5000=0.0002,数值太小,在浮点运算中容易被当作0而丢失精度。
+0.001这个微小的偏移量,就是为了解决这两个问题。它把完美解的适应度从inf拉回到一个巨大的、但有限的数:1/0.001 = 1000。同时,它把最差解的适应度从0.0002提升到1/5000.001 ≈ 0.0002,虽然还是小,但至少不再是零。更重要的是,它创造了一个清晰的、可感知的“满分”刻度:1000分。当你的程序输出“Woowww, the model could find the solution!!”,并显示适应度为1000时,你知道,它真的找到了一个q=0的解。这个1000,不是随便定的,它是1/0.001的数学结果,是整个适应度标尺的锚点。
提示:如果你把0.001改成0.1,满分就变成了10分,收敛曲线看起来会“更快”,但那只是幻觉,因为算法对“好”和“更好”的区分度急剧下降。我建议你永远保留这个0.001,它代表了对问题本质的尊重。
3.3 种群初始化与精英选择:为什么只保留2个最佳父代?
init_population()的实现很直接,就是生成population_size个随机排列。但train_population()里的选择逻辑,却大有讲究。
# ... 在每一代循环内 ... fitness_score = [] for i2 in range(population_size): fitness_score.append(fitness(population[i2], chromosome_size)) # 将适应度分数附加到种群数组末尾 pop = np.concatenate((population, np.expand_dims(fitness_score, axis=1)), axis=1) # 按最后一列(适应度)升序排序 sorted_indices = np.argsort(pop[:, -1]) pop_sorted = pop[sorted_indices] # 去掉适应度列,只留下染色体 pop = pop_sorted[:, :-1] # 取出最后两个(适应度最高)的父代 best_parents = pop[-num_best_parents:]这段代码展示了标准的“轮盘赌”选择的简化版——精英选择(Elitism)。它不按概率抽样,而是直接把适应度最高的几个个体揪出来。num_best_parents = 2,这个数字是我经过大量实验后确定的“甜点”。
为什么不是1?因为如果只选1个,变异后的后代多样性会严重不足。一个父代无论怎么变异,其“基因池”都是有限的,容易导致种群早熟(premature convergence),即所有个体都长得差不多,再也进化不出新东西。
为什么不是5或10?因为太多精英会“挤占”种群空间。假设种群大小是200,你拿走10个最好的去变异,再放回去,那剩下的190个个体就全是“陪跑”的。算法失去了探索(exploration)能力,变成了一群人在原地打转。
2,是一个经验平衡点。它保证了:
- 足够的探索:198个普通个体,依然在进行随机变异,维持种群多样性。
- 足够的开发(exploitation):2个最优秀的个体,被重点“培养”,确保优良基因不会在随机过程中丢失。
- 计算开销可控:只对2个个体做变异,计算量最小。
我在调试时发现,当num_best_parents从2增加到3时,100皇后问题的平均收敛代数从70代降到了65代,但方差(波动性)却增大了20%。这意味着,虽然有时运气好能更快找到解,但更多时候会因为过度开发而卡在某个局部最优里。2,是稳定性和速度的最佳折中。
4. 实操过程与核心环节实现:从命令行到学习曲线的完整旅程
4.1 启动与参数配置:如何用一行命令跑起100皇后?
项目的入口是n_queen_solver.py,它使用argparse来接收三个必需参数。这不是为了装模作样,而是因为这三个参数,恰恰是控制GA行为的三根杠杆。
python n_queen_solver.py 100 200 500这条命令的意思是:求解100皇后问题,初始种群大小为200个个体,最多运行500代。
chromosome_size=100:这是问题的规模,不可更改。它决定了编码长度、适应度函数的计算范围。population_size=200:这是你的“人才库”大小。它直接影响算法的探索能力。太小(如50),种群多样性不足,容易早熟;太大(如1000),计算开销剧增,但收益递减。200是我为100皇后找到的黄金值,它能在内存占用(约16MB)和收敛速度之间取得最佳平衡。epoches=500:这是你的“耐心上限”。它不是一个保证能找到解的代数,而是一个安全阀。因为GA是概率算法,理论上它可能永远找不到解(虽然概率极低)。设置一个上限,可以防止程序无限运行下去。在实践中,100皇后问题,95%的情况下,70-120代就能找到解,500代是留足了余量。
注意:不要试图用
python n_queen_solver.py 1000 1000 1000去挑战极限。1000皇后问题,其搜索空间比宇宙原子数还多,当前的算法和硬件,无法在合理时间内解决。GA不是万能的,它擅长的是在“巨大但结构化”的空间里找路,而不是在“无限混沌”的空间里碰运气。
4.2 训练循环详解:每一代发生了什么?
train_population()函数是整个算法的心脏。让我们把它拆解成一个清晰的、每一代都重复执行的流水线:
- 评估(Evaluation):对当前种群中的每一个个体(染色体),调用
fitness()函数,计算其适应度分数。这是一个纯计算步骤,不改变种群。 - 记录(Logging):将这一代所有个体的适应度分数求平均,存入
ft列表。这个ft列表,就是最终学习曲线(Learning Curve)的数据源。 - 排序与选择(Sorting & Selection):将种群和其对应的适应度分数“粘”在一起,然后按适应度从小到大排序。排序后,种群底部的个体,就是适应度最高的“精英”。
- 精英变异(Elitist Mutation):取出排序后最底部的2个精英个体,对它们分别调用
mutation()函数。mutation()的实现非常简单:随机选择染色体中的两个位置,然后交换它们的值。这个操作保证了变异后的个体依然是一个合法的排列。 - 更新种群(Population Update):将变异后的2个精英个体,放回种群的最底部(即,替换掉原来那2个精英)。其余198个个体保持不变。
- 终止检查(Termination Check):检查最新一代的平均适应度
ft[-1]是否等于1000。如果是,说明至少有一个个体达到了q=0的完美状态,算法成功,立即break退出循环。
这个流程的关键在于,它没有引入任何新的、未经评估的随机个体。每一代,种群的99%都是上一代的“老人”,只有2个是经过严格筛选后“升级”的“新人”。这保证了进化的稳健性,避免了因随机性过大而导致的剧烈震荡。
4.3 可视化与结果解读:如何读懂那张“学习曲线”?
训练结束后,程序会自动调用fitness_curve_plot()和n_queen_plot()两个函数。
fitness_curve_plot()生成的学习曲线,横轴是代数(Epoch),纵轴是平均适应度(Average Fitness)。这张图的价值,远不止于“看它是不是涨上去了”。它是一个诊断工具。
- 平直的“高原期”:如原文所述,曲线在前28代一直停留在0附近。这很正常。初始种群是完全随机的,绝大多数个体的冲突数
q都极大(可能上千),导致适应度1/(q+0.001)趋近于0。这个阶段,算法在“黑暗中摸索”,靠随机变异慢慢减少冲突。 - 陡峭的“起飞期”:当曲线突然从0跳到100,意味着种群中开始出现一些
q值较小(比如几十)的“好苗子”。算法的精英选择机制开始发挥作用,这些好苗子被选中、变异、繁衍,迅速拉高了整个种群的平均水平。 - 反复的“震荡期”:在接近1000分的过程中,曲线常常会上下波动。这是因为,当
q已经很小(比如1或2)时,一次微小的变异,可能让q从1变成0(适应度从500跳到1000),也可能让q从1变成3(适应度从500跌到333)。这种震荡,恰恰说明算法正在精细地“打磨”最优解,而不是粗暴地“寻找”一个解。
n_queen_plot()则负责将最终找到的解,以一张直观的棋盘图展示出来。它用matplotlib绘制一个100x100的网格,并在正确的位置画上皇后标记。这张图的意义在于终极验证。无论你的适应度函数算出多少分,只有当你亲眼看到100个皇后在棋盘上互不攻击时,你才能100%确认:算法真的成功了。我建议你在每次修改代码后,都手动检查这张图,这是防止逻辑错误的最后一道防线。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪史”
5.1 问题速查表:遇到这些症状,你应该怎么做?
| 症状(Symptom) | 可能原因(Root Cause) | 排查与解决(Diagnosis & Fix) |
|---|---|---|
程序运行几秒就结束,但没找到解,ft[-1]始终是0 | 种群大小population_size过小,或代数epoches过少 | 增加population_size(如从100到200),或增加epoches(如从100到500)。小规模问题(如8皇后)可以快速验证。 |
| 学习曲线在某个值(如600)长时间停滞,无法突破 | 适应度函数的“分辨率”不足,或变异强度太弱 | 检查fitness()函数,确认q的计算逻辑无误。尝试略微增加变异概率(在mutation()中增加交换次数),或检查精英数量num_best_parents是否过多导致多样性丧失。 |
程序报错ValueError: operands could not be broadcast together | NumPy数组维度不匹配,通常发生在np.concatenate或np.argsort时 | 检查population数组的形状。确保init_population()返回的是(population_size, chromosome_size)的二维数组,而不是一维的。打印population.shape进行调试。 |
| 生成的棋盘图上,皇后数量不对(多于或少于100个) | 编码或绘图逻辑错误,最可能是n_queen_plot()中索引越界 | 回到n_queen_plot()函数,仔细检查循环变量i和j的范围。确保for i in range(len(solution)):,且solution[i]的值在0到len(solution)-1之间。 |
| 程序运行极慢,CPU占用100%,但进度条几乎不动 | 适应度计算未向量化,仍在使用纯Python for循环 | 确认fitness()函数内部没有嵌套的、未被NumPy优化的Python循环。对于大规模问题,考虑用Numba进行JIT编译加速。 |
5.2 我踩过的三个大坑:关于“为什么我的GA不工作”的深度反思
坑一:把“适应度高”等同于“解好”,而忽略了“解的分布”
我最初认为,只要适应度曲线在上升,算法就在进步。直到有一次,我看到曲线稳定在800分(对应q=1.25),欣喜若狂地以为找到了“准优解”。但当我用n_queen_plot()画出棋盘时,发现100个皇后里,有99个完美排布,只有1个皇后“孤零零”地被挤在角落,造成了1.25个冲突。这个解在数学上是“好”的,但在工程上毫无价值——它离完美只差一步,但这一步,算法却花了整整200代都没迈过去。这让我明白,GA的适应度函数,不仅要能区分“好”和“坏”,更要能敏感地捕捉到“差一点就完美”的微妙状态。后来,我尝试过给q=0的解赋予一个巨大的奖励分(如10000),但效果不佳,因为它破坏了适应度的连续性。最终,我选择接受这个现实:GA擅长的是“找到”,而不是“精确逼近”。对于N皇后,我们追求的是q=0,而不是q=0.001。
坑二:迷信“标准流程”,而忽视了问题的特殊性
在加入交叉操作失败后,我一度怀疑是自己的交叉实现有bug。于是我查阅了所有经典文献,尝试了单点交叉、两点交叉、均匀交叉……结果无一例外,都让性能变得更差。直到我静下心来,画了一张图:把两个“好”解的皇后位置在棋盘上标出来,然后手动模拟交叉。我才恍然大悟——N皇后问题的解,其结构是高度全局关联的。一个皇后的安全位置,取决于其他99个皇后的位置。它不像旅行商问题(TSP),一段好的路径片段可以被独立地“继承”。在这里,没有“好的片段”,只有“好的整体”。这个认知上的转变,让我彻底放弃了对“标准GA”的执念,转而拥抱了更务实的“精英变异”策略。最好的算法,永远是那个最懂你的问题的算法,而不是教科书里最漂亮的那个。
坑三:低估了随机性的力量,而过度依赖“确定性”
GA的核心是随机。但作为工程师,我本能地讨厌随机。我曾试图在init_population()里固定随机种子(np.random.seed(42)),以确保每次运行结果一致。这确实让调试变得容易,但也让我错过了一个关键现象:算法的收敛时间,其方差非常大。有时70代就成功,有时要150代。当我取消了固定种子,跑了100次实验,画出收敛代数的分布图时,我才真正理解了GA的“概率”本质。它不是一个确定性的函数,而是一个在概率云中穿行的探索者。接受这种不确定性,学会用统计的眼光(如平均代数、成功率)去评估算法,而不是执着于某一次的“完美表现”,这是我从这个项目里学到的最重要一课。它教会我,真正的工程能力,不在于写出永不失败的代码,而在于写出能优雅地与失败共处的代码。
6. 项目扩展与思考:从N皇后到更广阔的世界
6.1 一个更“真实”的问题:带约束的车间调度
N皇后是一个优雅的玩具问题,它帮助我们理解GA的骨架。但真实世界的问题,往往包裹着层层约束。比如,一个典型的车间调度问题(Job Shop Scheduling):有10台机器,要加工20个工件,每个工件有特定的加工顺序和在每台机器上的加工时间。目标是最小化所有工件的完工时间(makespan)。
这个问题和N皇后一样,可以用一维排列编码(例如,一个长度为200的序列,表示所有工件的所有工序的执行顺序)。但它的适应度函数,就远比计算q复杂得多。你需要模拟整个加工过程,跟踪每一台机器的状态,计算每个工件的开始和结束时间,最后得出makespan。而且,它还有硬约束(如工序顺序不能颠倒)和软约束(如尽量减少机器空闲时间)。
如果你打算用GA解决这类问题,N皇后项目就是你最好的起点。你可以复用它的种群管理、精英选择、变异操作框架,只需要把fitness()函数,替换成一个完整的、能模拟车间运行的仿真器。这正是GA的魅力所在:它提供了一个通用的“引擎”,而你只需要为它安装上适合你问题的“燃料”和“方向盘”。
6.2 关于编码的再思考:为什么“排列”不是万能的?
在N皇后中,一维排列编码是天作之合。但请记住,编码是GA成功与否的先决条件,而不是一个可以随意套用的模板。我曾经用同样的排列编码,去尝试解决一个图像分割问题,结果惨败。因为图像像素之间的关系,是空间上的邻域关系,而不是全局的排列关系。在那里,一个更好的编码,可能是用一组浮点数来表示分割边界的参数,或者用一个二进制掩码来表示每个像素的归属。
所以,当你面对一个新问题时,永远要先问自己:这个问题的“合法解”有什么数学结构?这个结构,能否被一种简单、紧凑、且易于进行遗传操作(变异、交叉)的编码方式所表达?如果答案是否定的,那么,与其强行用GA,不如去寻找更适合的算法。GA不是银弹,它只是一个强大的工具箱里,最锋利的那把扳手。而选择哪把工具,永远是工程师的第一要务。
6.3 最后一个小技巧:如何让你的GA“说话”?
在调试一个复杂的GA项目时,最痛苦的不是它不工作,而是它不告诉你为什么不工作。我养成的一个习惯是,在train_population()循环里,加入一个简单的日志打印:
if i1 % 10 == 0: # 每10代打印一次 best_fitness = max(fitness_score) avg_fitness = sum(fitness_score) / len(fitness_score) print(f"Epoch {i1}: Best={best_fitness:.3f}, Avg={avg_fitness:.3f}")这短短几行代码,能在你喝一杯咖啡的时间里,给你提供最直观的反馈:算法是在稳步前进,还是在原地踏步,抑或是在倒退。它比盯着一个静态的学习曲线图,要生动得多。一个好的工程师,不是靠猜,而是靠观察。而观察的第一步,就是让程序学会向你报告它的状态。这个习惯,我已经坚持了十年,它帮我节省了无数个通宵调试的时间。