遗传算法工程化实战:选择、交叉与变异的可调试设计
1. 项目概述为什么“遗传算法第二讲”比第一讲更值得你花时间重读“遗传算法”这四个字听上去像生物课的延伸又像计算机系高年级的选修课代号。但如果你正在做参数调优、路径规划、排班调度、结构优化甚至只是想让一个神经网络训练得更快一点那它就不是课本里的概念而是你手边那把没开刃但潜力巨大的刀——Part One 讲的是怎么把刀从鞘里拔出来Part Two 才真正教你如何发力、如何借势、如何在真实阻力下劈开问题。我带过三届算法实训营每届都有学员卡在“能跑通示例却改不动自己的问题”这个节点原因几乎都出在对选择机制、交叉策略和变异强度这三块“软性逻辑”的误判上。这不是代码写错的问题而是对进化过程本身的理解偏差。比如有人把种群规模设为200以为“越多越好”结果收敛慢得像冬天的Wi-Fi也有人把变异率调到0.05结果早早就陷进局部最优连山脚都没爬出去。Part Two 的核心就是把“模拟自然进化”这件事从口号变成可计算、可调试、可复现的操作系统。它不讲抽象定义只拆解你在Jupyter里敲下ga.run()之前必须亲手调过的7个关键旋钮它不谈哲学隐喻只告诉你为什么轮盘赌选择在多峰函数上容易失效而锦标赛选择为什么在实时调度中更抗抖动。适合谁适合已经跑过De Jong函数、看过种群迭代图、但一换到自己业务数据就崩的工程师适合被导师催着用GA优化模型超参、却连适应度函数该不该归一化都拿不准的研究生也适合想绕过数学证明、直接抄作业式落地的算法应用者。它解决的不是“什么是遗传算法”而是“为什么我的遗传算法不遗传、也不进化”。2. 核心设计逻辑与方案选型深度拆解2.1 为什么必须放弃“教科书式”标准流程真实问题的三个反直觉特征教科书里遗传算法的流程图永远是初始化→评估→选择→交叉→变异→循环。干净、对称、充满确定性。但我在给一家物流公司的车辆路径问题VRP做算法支持时发现这套流程在真实场景里会连续踩三个坑而这三个坑正是Part Two要重点破除的认知惯性。第一个反直觉“适应度越高越好”在多目标场景下是毒药。VRP同时要最小化总行驶距离、最小化最长单程耗时、最大化客户满意度比如准时率。如果强行把三者加权成一个标量适应度权重设定就成了玄学——调高距离权重司机跑得近了但某条线路延误两小时调高准时率权重系统拼命塞短途单总里程暴涨30%。后来我们改用NSGA-II的非支配排序把“帕累托前沿”作为选择依据。这时“适应度”不再是数字而是一组不可互相替代的向量。选择操作不再是从池子里捞“最高分”而是从前沿面上挑“分布最均匀”的个体。这直接导致种群多样性管理策略必须重构不能靠简单增大种群规模而要用拥挤度距离crowding distance来量化每个解在目标空间里的“稀有程度”。实测下来在100个解的种群中拥挤度距离前20名的解其后续10代内的探索广度比随机选20个高适应度解高出2.3倍。第二个反直觉“交叉必优于复制”在离散编码中常是陷阱。教科书常用二进制编码演示单点交叉逻辑清晰。但VRP的解是城市ID的排列比如[1,5,3,2,4]代表访问顺序。如果对两个父代[1,5,3,2,4]和[2,3,4,1,5]做普通单点交叉切点在第2位后得到子代[1,5,4,1,5]——城市5重复了城市3丢了解非法。这时候标准交叉不是加速进化而是制造垃圾。我们最终采用顺序交叉OX先复制父代A切片[5,3,2]再按父代B顺序填入未出现的城市[1,4]得到合法子代[1,5,3,2,4]。这个选择背后有硬逻辑OX保留了父代A的相对顺序片段这对路径类问题至关重要——相邻城市间的地理邻近性往往比绝对位置更重要。我们做过对比实验在相同参数下OX比随机修复的普通交叉使可行解生成率从38%提升到92%且平均收敛代数减少41%。第三个反直觉“变异率恒定”在动态环境中等于自废武功。原系统每天要处理2000新订单订单分布随促销活动剧烈波动。如果变异率固定为0.01系统在订单平稳期过度扰动优质解被炸散在订单突增期又扰动不足无法跳出旧模式。我们引入自适应变异率基础值0.005每代根据种群适应度方差动态调整。方差0.001说明种群快同质化了变异率×1.5方差0.05说明探索太野变异率×0.7。这个简单规则让系统在促销峰值期的路径优化成功率从63%稳定到89%以上。它揭示了一个本质遗传算法不是在模拟静态的“物竞天择”而是在模拟一个持续应对环境变化的“活系统”。Part Two 的所有设计都锚定在这个认知上——不是让算法去适配教科书而是让教科书逻辑去适配你手上的脏数据、烂约束、急需求。2.2 选择机制轮盘赌、锦标赛、精英保留到底在选什么选择操作常被简化为“按适应度概率抽样”但抽样方式直接决定算法是“精耕细作”还是“广撒网”。我用一个具体案例说明三者的实战差异优化一个7层神经网络的剪枝比例搜索空间是7维向量每维取值[0.1,0.9]目标是最小化模型体积与精度损失的加权和。轮盘赌选择Roulette Wheel Selection适应度1/(1加权损失)所以损失越小适应度越高被选概率越大。表面公平但有个致命缺陷当种群中出现一个“超级个体”比如损失0.02适应度≈49.5而其他个体损失都在0.1~0.3之间适应度≈9~3.3这个超级个体的被选概率会高达65%。结果就是下一代种群中65%的基因都来自它多样性断崖下跌。我们在测试中观察到轮盘赌在第12代就陷入停滞后续50代适应度无改善。它的适用场景其实很窄仅当种群适应度分布相对均匀标准差均值的30%时才稳健。一旦出现“尖峰”它就变成“富者愈富”的放大器。锦标赛选择Tournament Selection每次随机选k个个体k3取其中适应度最高者胜出。k值是关键杠杆。k2时选择压力温和多样性保持好但收敛慢k5时选择压力大易早熟。我们通过网格搜索发现k3是VRP问题的甜点既能有效淘汰劣质解又不会过度压制中等解携带的潜在优良基因片段。更重要的是锦标赛天然免疫“超级个体”问题——无论那个体多强它每次最多只赢一场而其他个体仍有概率在别的场次胜出。实测中k3的锦标赛让种群多样性以汉明距离均值衡量比轮盘赌高2.1倍且收敛代数仅多7%。精英保留Elitism这不是独立的选择机制而是必须叠加在任何选择之上的安全阀。我们规定每代强制将当前最优的1个个体无修改地复制到下一代。这看似微小却解决了GA最经典的“退化”问题——由于交叉和变异的随机性某代产生的最优解可能在下一代因不幸的交叉/变异而彻底丢失。在神经网络剪枝任务中启用精英保留后历史最优解的“存活率”从78%提升至100%且最终收敛精度的标准差降低44%。注意精英数量不宜多1~2个足矣。超过3个就会形成“基因孤岛”阻碍新基因组合的产生。提示选择机制的本质是在“利用exploitation”与“探索exploration”之间动态配比。轮盘赌偏向利用锦标赛提供可调的平衡点精英保留则是利用的底线保障。没有最优只有最适合你问题特性的组合。2.3 交叉与变异不是随机扰动而是结构化信息重组交叉和变异常被初学者当作“加点随机性”的黑箱操作。但Part Two的核心洞见是它们是两种不同粒度的信息重组工具。交叉在宏观层面交换成熟模块变异在微观层面注入新可能性。理解这个分工才能避免“调参玄学”。交叉策略的工程化选型逻辑交叉不是为了“混合”而是为了“继承有用结构”。对于连续参数优化如超参搜索我们首选模拟二进制交叉SBX。它不像单点交叉那样粗暴切割而是基于父代值生成子代公式为child1 0.5 * [(1β) * p1 (1-β) * p2]child2 0.5 * [(1-β) * p1 (1β) * p2]其中β由分布指数η控制η越大子代越靠近父代开发η越小子代越分散探索。我们固定η2因为实测发现η2时子代在父代区间内的分布最接近正态既保证了继承性又提供了足够扰动。对于离散序列如VRP如前所述OX是首选但要注意OX对切片长度敏感。切片太短如只取2个城市继承的结构信息太少切片太长如取一半子代与父代A过于相似。我们通过分析城市间路网密度将切片长度动态设为max(2, floor(0.3 * n_cities))效果最佳。变异的三种角色与参数真相变异常被简化为“以概率p翻转某位”。但在工程中它承担三重角色多样性守门员防止种群完全同质化。此时需要低概率p0.001~0.01、高幅度如高斯变异σ0.1局部搜索器在优质解附近精细搜索。此时需要高概率p0.1~0.3、低幅度如均匀变异范围±0.05创新触发器当连续10代无进展强制注入全新基因。此时需要一次性高概率p0.5、全维度变异。我们在物流系统中实现了三级变异常规变异p0.005高斯停滞检测变异连续5代方差0.0001时触发p0.1均匀以及每日重置变异凌晨自动执行p0.3全维度。这种分层设计让系统在99.2%的日常请求中稳定运行仅在极端异常日如暴雨导致全城配送延迟才需人工介入。3. 实操全流程与核心环节实现细节3.1 从零构建可调试GA框架一个极简但完整的Python实现下面是一个去掉所有包装、直击核心的GA实现。它只有127行但覆盖了Part Two强调的所有关键可调点。我把它称为“透明GA”因为每一行代码都对应一个明确的决策没有任何魔法方法。import numpy as np from typing import List, Tuple, Callable, Optional class TransparentGA: def __init__(self, bounds: List[Tuple[float, float]], # 每维的上下界如[(-5,5), (0,10)] fitness_func: Callable[[np.ndarray], float], # 适应度函数输入解向量输出标量 pop_size: int 100, elite_size: int 1, tournament_k: int 3, sbx_eta: float 2.0, mutation_rate: float 0.005, mutation_sigma: float 0.1): self.bounds bounds self.fitness_func fitness_func self.pop_size pop_size self.elite_size elite_size self.tournament_k tournament_k self.sbx_eta sbx_eta self.mutation_rate mutation_rate self.mutation_sigma mutation_sigma self.dim len(bounds) # 初始化种群在每维边界内均匀采样 self.population np.random.uniform( low[b[0] for b in bounds], high[b[1] for b in bounds], size(pop_size, self.dim) ) self.fitness_history [] def _evaluate_population(self): 批量评估种群缓存结果 self.fitness np.array([self.fitness_func(ind) for ind in self.population]) self.fitness_history.append(np.max(self.fitness)) # 记录每代最优适应度 def _tournament_select(self) - np.ndarray: 锦标赛选择返回一个父代个体 indices np.random.choice(len(self.population), self.tournament_k, replaceFalse) winner_idx indices[np.argmax(self.fitness[indices])] return self.population[winner_idx].copy() def _sbx_crossover(self, parent1: np.ndarray, parent2: np.ndarray) - Tuple[np.ndarray, np.ndarray]: 模拟二进制交叉 u np.random.random(self.dim) beta np.where(u 0.5, (2 * u) ** (1.0 / (self.sbx_eta 1)), (2 * (1 - u)) ** (1.0 / (self.sbx_eta 1))) child1 0.5 * ((1 beta) * parent1 (1 - beta) * parent2) child2 0.5 * ((1 - beta) * parent1 (1 beta) * parent2) # 边界处理超出则拉回边界 for i, (low, high) in enumerate(self.bounds): child1[i] np.clip(child1[i], low, high) child2[i] np.clip(child2[i], low, high) return child1, child2 def _gaussian_mutation(self, individual: np.ndarray) - np.ndarray: 高斯变异 mutated individual.copy() mask np.random.random(self.dim) self.mutation_rate mutated[mask] np.random.normal(0, self.mutation_sigma, sizemask.sum()) # 边界处理 for i, (low, high) in enumerate(self.bounds): mutated[i] np.clip(mutated[i], low, high) return mutated def _generate_offspring(self) - np.ndarray: 生成新一代种群 offspring [] # 1. 精英保留 elite_indices np.argsort(self.fitness)[-self.elite_size:] for idx in elite_indices: offspring.append(self.population[idx].copy()) # 2. 交叉产生新个体 while len(offspring) self.pop_size: p1 self._tournament_select() p2 self._tournament_select() c1, c2 self._sbx_crossover(p1, p2) offspring.append(c1) if len(offspring) self.pop_size: offspring.append(c2) # 3. 变异 for i in range(self.elite_size, len(offspring)): offspring[i] self._gaussian_mutation(offspring[i]) return np.array(offspring) def run(self, n_generations: int 100) - Tuple[np.ndarray, float]: 运行GA返回最优解和最优适应度 for gen in range(n_generations): self._evaluate_population() self.population self._generate_offspring() best_idx np.argmax(self.fitness) return self.population[best_idx], self.fitness[best_idx]这个框架的“透明性”体现在哪里参数即文档sbx_eta、mutation_sigma这些变量名直接告诉你它控制什么而不是藏在config.yaml里。无隐藏状态所有中间变量如fitness_history都显式暴露方便你用plt.plot(ga.fitness_history)立刻画出收敛曲线。边界处理内聚np.clip在交叉和变异后立即执行避免非法解污染种群这是很多开源库忽略的细节。精英保留前置在生成后代前就锁定精英杜绝“最优解在交叉中被意外破坏”的风险。注意这个实现刻意回避了deap或pymoo等高级库。不是它们不好而是当你第一次调试时库的抽象层会掩盖问题根源。比如deap的tools.cxSimulatedBinaryBounded在边界处理上默认是“反射”而我们的np.clip是“截断”效果完全不同。先用透明代码摸清原理再用库提效这才是正道。3.2 关键参数调优实战一份可直接抄作业的配置表参数调优不是试错而是基于问题特征的推理。下面这张表总结了我在12个不同项目中验证过的参数配置逻辑。它不承诺“放之四海而皆准”但保证“在同类问题上按此配置起步80%概率能跑通”。参数推荐初始值调整逻辑当遇到问题时物理意义与影响种群规模 (pop_size)min(200, 10 × dim)若收敛慢且多样性低 → ↑ 至20 × dim若内存溢出或单代耗时5s → ↓ 至5 × dim规模过小搜索空间覆盖不足过大计算冗余且选择压力下降导致收敛变慢。dim10时200是黄金点。锦标赛大小 (tournament_k)3若早熟前20代停滞→ ↓ 至2若收敛慢且最优解波动大 → ↑ 至4k2时选择压力弱利于探索k4时压力强利于开发。k3是经验平衡点。SBX分布指数 (sbx_eta)2.0若子代过于接近父代探索不足→ ↓ 至1.0若子代过于发散优质结构被破坏→ ↑ 至5.0η越小子代越分散探索η越大子代越集中开发。η2.0时子代在父代区间内呈近似正态分布最符合“渐进式改进”直觉。变异率 (mutation_rate)0.005若种群快速同质化连续5代方差0.001→ ↑ 至0.01若最优解频繁被破坏 → ↓ 至0.001控制“扰动频率”。过高算法退化为随机搜索过低无法跳出局部最优。0.005是连续空间的稳健起点。变异步长 (mutation_sigma)0.1 × (bound_high - bound_low)若变异后解质量骤降 → ↓ 步长若变异后无明显变化 → ↑ 步长控制“扰动幅度”。应与搜索空间尺度匹配。对[-10,10]的维度σ2.0对[0,1]的维度σ0.1。实操心得不要同时调多个参数。我的标准流程是固定pop_size100,tournament_k3,sbx_eta2.0,mutation_rate0.005,mutation_sigma0.1跑20代看收敛曲线是否平滑上升若曲线抖动剧烈说明探索过猛优先调小mutation_sigma若曲线前期飙升后长期平缓说明早熟优先调大tournament_k或调小sbx_eta若20代后仍无进展再考虑增大pop_size。这个顺序把“高频小扰动”变异放在最后调是因为变异是“最后一道防线”应先确保选择和交叉的主干逻辑健康。3.3 适应度函数设计避开三大死亡陷阱适应度函数是GA的“方向盘”方向错了再好的引擎也到不了目的地。我在审查37个失败的GA项目时发现82%的根源在于适应度函数设计失误。以下是三个最致命的陷阱以及如何绕过它们。陷阱一惩罚项滥用——把约束变成“慢性毒药”常见做法对违反约束的解施加巨大惩罚如fitness original_fitness - 1e6 * violation_count。这看似合理但后果严重算法会把绝大部分精力花在“避免被罚”而非“追求最优”。在VRP中一个解若超载1kg就被罚1e6那么算法宁可让所有车都空跑20%也要确保不超载。结果是它永远学不会“如何聪明地装车”。破解方案可行性驱动的两阶段评估。第一阶段只评估可行性。对每个解计算其违反硬约束的数量如超载次数、时间窗违反数。可行解得分为1不可行解得分为0。第二阶段仅对可行解计算原始目标函数如总里程。最终适应度 feasibility_score * (1 1/(1 objective_value))目标是最小化故用倒数。这样算法先学会“如何成为可行解”再学会“如何成为好可行解”。在VRP测试中可行性达成时间从平均47代缩短至12代。陷阱二尺度失衡——让一个指标绑架全局在神经网络剪枝中体积减小10MB和精度损失0.5%数值上前者远大于后者若直接相加算法会疯狂剪枝直到模型报废。破解方案Z-score归一化 权重解耦。先对历史数据或预估范围计算各目标的均值μ和标准差σnormalized_volume (volume - μ_volume) / σ_volumenormalized_acc_loss (acc_loss - μ_acc_loss) / σ_acc_loss然后加权fitness w1 * normalized_volume w2 * normalized_acc_loss。关键是w1和w2不再是经验权重而是通过目标重要性问卷确定让3位业务方打分1-5分取平均。体积重要性4精度5则w1:w24:5。这把主观判断转化为客观参数避免工程师拍脑袋。陷阱三平滑性缺失——在“悬崖边”进化有些问题的适应度函数存在陡峭断崖。例如一个调度解若晚于客户要求时间1分钟适应度0早于1分钟适应度100。算法在断崖两侧来回震荡永远学不会“如何稳定地早到”。破解方案引入软约束与Sigmoid平滑。定义一个容忍窗口Δt如5分钟用Sigmoid函数平滑过渡penalty 100 / (1 exp(-k * (actual_time - required_time Δt)))其中k控制平滑度k2时过渡区约±2分钟。这样算法能感知到“早到3分钟比早到1分钟好”从而学习渐进式优化而非在断崖上赌博。4. 常见问题与排查技巧实录4.1 收敛诊断树5分钟定位你的GA卡在哪一步当GA跑起来后毫无进展别急着改代码。先用这张诊断树5分钟内定位瓶颈。它基于我处理过的156个“不收敛”案例的共性提炼。graph TD A[GA收敛缓慢或停滞] -- B{检查第1代种群} B --|适应度方差 0.001| C[问题初始化失败br对策检查bounds是否过窄或随机种子是否固定] B --|适应度方差 0.1| D[正常进入下一步] D -- E{运行至第10代} E --|最优适应度提升 5%| F[检查选择机制br对策若用轮盘赌换锦标赛k3若已用锦标赛检查k是否过大] E --|最优适应度提升 5%| G[正常继续] G -- H{运行至第50代} H --|种群适应度方差 0.0001| I[问题早熟br对策↑ mutation_rate 至0.01或 ↓ sbx_eta 至1.0] H --|种群适应度方差 0.01| J[问题探索过度br对策↓ mutation_rate 至0.001或 ↑ sbx_eta 至5.0] H --|最优适应度停滞| K[检查适应度函数br对策用陷阱三的Sigmoid平滑法或检查是否存在未声明的硬约束]现场记录上周帮一位做光伏板倾角优化的工程师排查。他的GA在第3代就停滞最优倾角始终在28°。按诊断树第一步检查第1代种群发现所有个体的倾角都在27.5°~28.5°之间方差仅0.002。追问后得知他把bounds设成了(27.5, 28.5)理由是“文献说最优在28°附近”。这等于把搜索空间锁死在一个针尖上。我们立刻放开bounds至(0, 90)并加入物理约束如避免阴影遮挡作为软惩罚第7代就找到了22.3°的全局最优发电量提升4.7%。教训初始化的宽度决定了算法的天花板。4.2 “最优解消失”事件复盘一次血泪教训这是GA最令人抓狂的问题第15代出现一个适应度99.2的解第16代它不见了而且再也找不回来。我称之为“幽灵解事件”。在2022年为一家芯片设计公司做功耗优化时我们连续3天被这个问题折磨。现象还原种群规模200精英保留1个。第15代个体#177适应度99.2是当时历史最优。第16代#177未被选为精英因为精英保留只留1个而#177在第15代末的适应度被另一个个体以99.3超越它参与了交叉。交叉中#177与一个适应度仅10.5的个体配对SBX交叉后其关键基因控制某模块电压的参数被大幅扰动子代适应度跌至32.1。同时#177未被变异变异率0.005它运气好躲过了但它在交叉中已被“杀死”。根因分析精英保留只保护“当前最优”不保护“历史最优”。而GA的交叉操作对优质个体是双刃剑——它可能诞生更好解也可能摧毁它。问题不在交叉本身而在缺乏对优质基因的主动保护机制。解决方案双轨精英策略主精英Primary Elitism保留当前代最优1个不变。副精英Secondary Elitism额外保留历史最优的1个即使它不是当前最优并确保它每代都参与交叉但不参与变异。在_generate_offspring方法中我们增加# 在精英保留后添加副精英 if not hasattr(self, historical_best) or self.fitness[best_idx] self.historical_best[1]: self.historical_best (self.population[best_idx].copy(), self.fitness[best_idx]) offspring.append(self.historical_best[0].copy()) # 副精英不变异上线后“幽灵解”事件归零。更重要的是副精英作为“稳定锚点”让种群在面对突发噪声如某次评估因硬件抖动误差±5%时鲁棒性提升了3倍。这个技巧教科书不会写但它是工业级GA的标配。4.3 性能瓶颈排查当GA慢得像在思考人生GA慢90%的原因不在算法复杂度而在I/O和评估。下面是我整理的性能排查清单按优先级排序瓶颈层级典型症状快速检测命令解决方案评估函数最常见单次fitness_func调用100msrun()总耗时中_evaluate_population占95%import time; starttime.time(); fitness_func(test_ind); print(time.time()-start)1. 缓存对相同输入返回缓存结果lru_cache2. 近似用代理模型如GP替代耗时仿真3. 并行用joblib.Parallel批量评估。种群规模次常见pop_size500时单代耗时非线性增长内存占用8GBpsutil.virtual_memory().percent1. 减小pop_size但↑tournament_k维持选择压力2. 用numpy.memmap将种群存外存流式加载。交叉/变异少见sbx_crossover或_gaussian_mutation单次耗时1mscProfile.run(ga._sbx_crossover(p1,p2))1. 向量化用np.where替代for循环2. 预分配提前生成beta数组避免每调用一次都np.random。Python解释器罕见所有函数都快但run()总耗时高time.sleep(0)占比异常import sys; print(sys.version)升级到Python 3.11其更快的解释器对纯Python GA提升显著或用Cython编译核心循环。真实案例一个材料模拟项目GA单代耗时12分钟。cProfile显示98%时间在fitness_func它调用一个Fortran编译的模拟器。我们没动算法只加了一行缓存from functools import lru_cache lru_cache(maxsize1000) def fitness_func_cached(params_tuple): return fitness_func(np.array(params_tuple))参数是浮点数tuple(np.round(params, 3))作为key。结果单代耗时降至47秒提速15倍。记住在GA里让评估变快永远比让算法变快更有效。5. 工程化落地 checklist从实验室到生产环境的七道关GA从能跑通到能上线中间隔着七道关。跨不过它就是个玩具跨过去它就是印钞机。这是我给团队制定的上线checklist每一条都来自血泪教训。【必过】可重现性验证设置固定随机种子np.random.seed(42)运行3次确认最优解、收敛代数、最终适应度的差异0.1%。若不满足检查所有随机源random,numpy.random,torch.manual_seed是否都被控制。曾有一个项目因torch种子未设导致A/B测试结果不可比返工两周。【必过】边界鲁棒性测试对种群中每个个体手动将其某一维设为bounds[i][0]-1下界外和bounds[i][1]1上界外传入fitness_func。确认函数能优雅处理如返回极低适应度或抛出明确异常而非崩溃或返回NaN。边界外的非法输入是线上环境的常态。【必过】中断-恢复能力在第50代时手动CtrlC中断。检查能否从checkpoint.pkl保存种群、代数、历史最优中准确恢复并继续运行至100代且最终结果与未中断版本一致。生产环境没有“永远在线”必须拥抱中断。【建议】热启动支持允许用户上传一个初始解如initial_solution.npy作为种群的第一部分。这对有历史经验的场景如“昨天最优解今天在此基础上微调”至关重要。我们为此增加了__init__的initial_population参数。【建议】资源用量监控在run()中嵌入