遗传算法工业级实践:编码选择、算子设计与边界处理全解析
1. 项目概述为什么“遗传算法第二讲”比第一讲更值得你花时间啃透“遗传算法”这四个字对很多刚接触优化问题的朋友来说像一本封皮烫金但内页全是古文的书——知道它很厉害常被用来解调度、调参数、搞设计可翻开第一页就卡在“适应度函数怎么写”“交叉概率设多少才不瞎折腾”上。我带过不少实习生和转行学员发现一个普遍现象他们能复现课本里的“二进制编码轮盘赌选择单点交叉”标准流程但一碰到真实业务场景——比如用GA优化一个有12个连续变量的供应链成本模型或者给嵌入式设备找一组低功耗运行参数——立刻手足无措。问题不出在“会不会”而在于“为什么这么设计”。Part Two正是为解决这个断层而生的。它不是Part One的简单延续而是从“照着代码跑通”跃迁到“动手调得稳、改得准、扩得开”的分水岭。这里没有抽象的数学推导堆砌所有内容都锚定在三个硬核问题上第一当你的解空间不是0/1字符串而是浮点数、整数序列、甚至树结构时编码与解码怎么不丢精度、不越边界第二标准选择算子轮盘赌、锦标赛在目标函数存在噪声或局部极值密集时为什么容易早熟收敛有没有更鲁棒的替代方案第三交叉与变异这两个“基因操作”到底在改变解的什么本质属性参数调高调低影响的是搜索的广度、深度还是跳出陷阱的能力这些问题的答案直接决定你写的GA是能落地跑出结果还是只在测试集上“看起来很美”。我见过太多人把GA当成黑箱调参工具反复试了几十组交叉率、变异率结果不如手动调两轮也见过有人为一个5维参数优化硬生生把GA跑成“随机采样暴力筛选”耗时3小时精度还不如网格搜索。Part Two要做的就是帮你把这层黑布掀开看清每个齿轮怎么咬合、哪根弹簧该调多紧。适合已经写过一次完整GA流程、但总在真实项目里“差一口气”的工程师、算法初学者、运筹优化实践者以及所有不想再靠玄学调参的务实派。2. 核心思路拆解从“模拟进化”到“可控搜索”的范式升级2.1 为什么必须抛弃“教科书式GA”的思维惯性Part One通常用一个经典例子开场求函数f(x)x·sin(10πx)2.0在区间[-1,2]上的最大值编码用8位二进制解码公式是x -1 decimal(编码) × 3 / (2⁸-1)。这个例子干净漂亮但它埋下了三个危险的思维陷阱提示这三个陷阱是90%的GA初学者在真实项目中踩坑的根源。第一个陷阱叫“编码万能论”。它让你误以为“只要把变量转成二进制GA就能自动工作”。但现实里你的变量可能是温度0~100℃需0.1℃精度、订单数量正整数上限10000、或者一个由5个布尔开关组成的控制策略如[开,关,开,开,关]。用固定长度二进制去编码温度要么精度不够8位只能分256档0.1℃精度需要3000档得12位要么浪费空间12位编码10000个订单实际只用到10000/4096≈2.4个比特。更麻烦的是当变量间存在强约束如“电机功率不能超过散热能力”二进制编码后做交叉大概率生成违反约束的非法解后续还得花大力气修复或惩罚——这已经不是“模拟进化”而是“制造混乱”。第二个陷阱是“算子神圣化”。轮盘赌选择被奉为圭臬因为它“符合自然选择”。但自然界的“适者生存”在GA里被简化成了“按适应度值比例抽签”。问题来了如果当前种群中最优个体适应度是100其余99个个体都是99.9轮盘赌会近乎100%选中那个最优个体导致种群多样性一夜归零搜索彻底僵死。这在真实优化中极其常见——比如训练一个神经网络权重初始几代大部分权重组合效果都差不多差只有个别微小调整带来一点提升。轮盘赌此时就是加速器而不是探索器。第三个陷阱最隐蔽“交叉信息交换变异引入扰动”。这是对生物遗传的浪漫化误读。在算法层面单点交叉Single-point Crossover对二进制串本质是把两个父本的高位段和低位段粗暴拼接而对浮点数向量常用的模拟二进制交叉SBX则通过一个分布指数η来控制子代与父代的接近程度。η越大子代越靠近父代中点探索性弱η越小子代越可能落在父代之外探索性强。这根本不是“交换”而是“按概率生成新点”。变异同理高斯变异Gaussian Mutation不是随机翻转某个比特而是给变量加一个服从N(0, σ²)的噪声σ的大小直接决定了搜索步长——它既是“扰动”也是“步长控制器”。Part Two的整个设计逻辑就是系统性地拆除这三个陷阱。我们不追求“像不像生物”而追求“好不好用”。核心思路就一句话把GA重新定义为一个“自适应搜索框架”其中编码、选择、交叉、变异全都是可配置、可替换、可量化评估的模块它们的唯一KPI是在给定计算预算下找到尽可能优的可行解。这意味着面对不同问题我们要像搭积木一样为每个模块选最合适的“零件”而不是套用一套万能模板。2.2 模块化设计四个核心组件的选型逻辑与实战权衡GA的骨架由四个核心组件构成编码/解码Encoding/Decoding、选择Selection、交叉Crossover、变异Mutation。Part Two的突破在于为每个组件提供了不止一种选项并明确了每种选项的适用场景、性能代价和实操门槛。这不是罗列工具而是给出一张“决策地图”。编码/解码精度、约束、效率的三角平衡实数编码Real-value Encoding直接用浮点数数组表示个体如[23.5, 1500, 0.78, 1, 0]。优势是零转换损耗、天然支持连续变量、解码无误差。劣势是交叉和变异操作需专门设计不能直接用二进制的“位操作”。适用场景变量多为连续值如工程参数、金融权重且约束可通过罚函数或修复机制处理。我在优化一个光伏逆变器MPPT算法参数时5个核心参数全是浮点数用实数编码后交叉直接用SBX变异用高斯扰动收敛速度比二进制快3倍且最终解精度高出2个数量级。整数编码Integer Encoding用整数数组表示如[3, 1, 5, 2, 4]。专为排列问题Permutation Problems设计如旅行商问题TSP、作业车间调度JSP。关键在于交叉算子必须保证子代仍是合法排列不能出现重复城市或缺失城市。常用算子顺序交叉OX、部分映射交叉PMX、循环交叉CX。OX保留父本A的某一段顺序再将父本B中未出现的城市按顺序填入剩余位置。适用场景解是元素的某种特定顺序且顺序本身携带语义如加工工序。注意不要用在“选几个物品”这类组合问题上那是0-1编码的领域。混合编码Hybrid Encoding一个个体里同时包含不同类型变量。例如一个物流路径优化问题既要选车辆类型离散卡车/厢货/摩托又要定每辆车的出发时间连续8:15, 9:30、载重整数吨。这时个体结构可能是[truck, 8.25, 3, van, 9.5, 5, ...]。挑战在于交叉必须分类型进行对离散部分用均匀交叉对连续部分用SBX变异也要按类型施加离散部分用交换变异连续部分用高斯变异。这是工业级GA的标配但实现复杂度陡增。我的建议是先用单一编码搞定核心变量再逐步加入次要变量每次只加一类充分测试其影响。选择从“抽签”到“精英护航”的策略演进锦标赛选择Tournament Selection随机挑k个个体k通常为2或3选其中适应度最好的一个。优势计算快、易并行、对适应度尺度不敏感不怕目标函数值很大或很小且k值可调——k越大偏向精英的程度越高多样性越低。这是我目前所有项目的默认选择。k2时它比轮盘赌更公平k3时它能在保持一定多样性的同时加速收敛。实测在优化一个10维天线阵列参数时k2的锦标赛比轮盘赌早收敛15%且最终解质量稳定。精英保留Elitism强制把当前代最优的1~2个个体原封不动复制到下一代。这不是独立的选择算子而是必须叠加在任何选择策略之上的“安全阀”。它确保最优解不会因随机选择而丢失。关键参数精英数量。太少如1个起不到保护作用太多如5个会严重抑制探索。我的经验是种群规模N100时保留1个N在100~500时保留2个N500时保留3个。注意精英保留后下一代种群中精英个体与其他个体是“共存”关系不是“替代”关系。常见错误是把精英直接塞进新种群却不减少其他选择的数量导致种群规模膨胀——这会拖慢计算还可能让精英“稀释”掉。线性排名选择Linear Rank Selection不看绝对适应度值而是先按适应度给所有个体排个名次1,2,3,...,N再按线性函数分配选择概率。例如第i名的概率为P(i) (2-μ)/N 2μ(i-1)/(N(N-1))其中μ是选择压selection pressure范围0~1。μ0时所有个体概率相等纯随机μ1时第一名概率最高最后一名概率为0。优势完全规避了适应度值尺度问题且压力量化可控。缺点是计算稍慢需排序。我在处理一个目标函数值波动剧烈的在线学习模型超参优化时用线性排名μ0.8替代轮盘赌种群崩溃率从35%降到7%。交叉与变异从“固定操作”到“搜索步长控制器”的认知升维这部分是Part Two的重中之重。我们必须扔掉“交叉就是换基因变异就是随机变”的旧观念建立一个新模型交叉控制“搜索方向”变异控制“搜索步长”。交叉的本质是“在父代构建的子空间内采样”。单点交叉是在两个父本连线的延长线上采样SBX是在两个父本周围的一个“类高斯”分布区域采样而离散问题的OX则是在父本A的某段顺序所定义的“局部邻域”内采样。因此交叉算子的选择决定了你的搜索是倾向于“精细微调”SBXη大还是“大胆跳跃”SBXη小或是“保持结构”OX。变异的本质是“在当前解周围添加可控噪声”。高斯变异的σ就是这个噪声的标准差。σ太大变异变成“重采样”失去“围绕当前解改进”的意义σ太小变异变成“无效抖动”无法跳出局部陷阱。关键洞察σ不应是全局固定值而应随进化代数衰减或随个体适应度自适应调整。我采用的策略是σ(t) σ₀ × (1 - t/T)ᵖ其中t是当前代T是总代数p是衰减指数通常取1~2。这样前期σ大鼓励探索后期σ小专注开发。实测在优化一个机械臂关节角度时这种自适应变异比固定σ快2倍收敛且避免了后期在最优解附近无谓震荡。这四个组件不是孤立的它们相互耦合。例如你选了实数编码就必须配SBX交叉和高斯变异你选了整数编码排列就必须配OX交叉和交换变异。Part Two的设计就是把这种耦合关系显性化、可配置化让你能根据问题特性像调音师一样拧紧每一个旋钮。3. 核心细节解析与实操要点手把手拆解五个致命细节3.1 细节一实数编码下的边界处理——为什么“截断法”是新手最大的坑当你用实数编码优化一个变量x∈[a,b]时最直觉的做法是变异后如果x a就设x a如果x b就设x b。这叫“截断法Clamping”。听起来很稳妥对吧错。它在实践中会制造一个隐形的“悬崖效应”。想象一下你的最优解就在边界b附近比如x* b - εε极小。用截断法变异当一个接近b的个体x₁ b - δδ ε发生变异加了一个正向噪声x₁ x₁ noise b立刻被拉回b。于是所有在[b-δ, b]这个窄区间内的个体变异后都挤在b点上。种群在b点形成一个虚假的“高密度峰”算法误以为这就是全局最优迅速收敛于此再也无法向右探索哪怕一微米。这比找不到最优解更糟——它让你自信地停在一个次优解上。正确做法是“反射法Reflection”或“循环法Wrapping”。反射法当x a令x a (a - x)当x b令x b - (x - b)。这相当于把越界的部分像光线一样“反射”回区间内。循环法当x a令x b - (a - x) % (b-a)当x b令x a (x - b) % (b-a)。这相当于把区间首尾相连越界就绕回来。实测对比在优化一个化工反应釜温度设定x∈[150, 250]℃时用截断法90%的运行结果卡在250℃用反射法100%运行都能找到真正的最优解242.3℃且收敛代数稳定在120代左右。反射法更常用因为它保持了变量的“距离感”——两个靠近边界的点反射后依然靠近不会被强行拉开。注意反射法要求变异噪声的分布是对称的如高斯分布。如果你用的是非对称分布如指数分布反射法会扭曲分布形态此时应改用“重采样法Resampling”一旦越界就重新生成一个噪声直到x落在[a,b]内。虽然计算稍慢但数学上最干净。3.2 细节二锦标赛选择中的“k值陷阱”——为什么k2不是万能解k2的锦标赛选择常被教材推荐为“平衡探索与开发”的黄金值。但这是在理想种群分布下成立的。在真实项目中种群的适应度分布往往极度偏斜。比如一个100个体的种群95个个体适应度在[0.1, 0.3]4个在[0.8, 0.9]1个在[0.95]。此时k2的锦标赛选出精英0.95的概率是P 1 - (99/100)² ≈ 0.02即2%。这意味着平均每50代才能让精英个体被选中一次。而k3时P 1 - (99/100)³ ≈ 0.03仅提高1个百分点。这显然不够。破解之道是“动态k值”或“分层锦标赛”。动态k值初期前30%代用k2保证多样性中期30%~70%代用k3加速收敛后期70%~100%代用k4或k5强力聚焦最优区域。但更优雅的方案是分层锦标赛先将种群按适应度分成三组Top前10%、Mid中间80%、Bottom后10%。然后以高概率如70%从Top组内进行k2锦标赛以中等概率25%从Mid组内进行k2锦标赛以低概率5%从Bottom组内进行k2锦标赛。这样精英始终有高曝光但底层个体也有“咸鱼翻身”的机会避免了种群过早同质化。我在一个电商推荐模型的特征权重优化中用分层锦标赛相比固定k2最终AUC提升了0.008且收敛曲线平滑没有剧烈波动。3.3 细节三SBX交叉的η参数——它不只是一个数字而是“搜索粒度”的刻度尺模拟二进制交叉SBX是实数编码下最主流的交叉算子。它的核心公式是对于父本x₁, x₂子代y₁, y₂的生成方式为 y₁ 0.5 × [(1β) × x₁ (1-β) × x₂] y₂ 0.5 × [(1-β) × x₁ (1β) × x₂] 其中β是[0,1]之间的随机数其概率密度函数为 p(β) 0.5 × (η1) × β^η这里的ηeta就是那个神秘参数。η越大p(β)越集中在β1附近意味着y₁, y₂越靠近x₁, x₂的中点η越小p(β)越分散y₁, y₂越可能落在x₁, x₂之外甚至远超它们的范围。η的物理意义就是“搜索粒度”。η2时算法倾向于在父代之间“微调”η15时它几乎只在父代中点附近“精修”。所以η不是越大越好也不是越小越好而要匹配你的问题特性。一个经验法则是η应与变量的“有效变化范围”成反比。例如优化一个电压值V∈[0, 5]V若你期望的改进精度是0.01V那么有效变化范围是5/0.01500档此时η宜取较小值如2~5允许较大跨度的探索。反之若优化一个比例系数α∈[0.9, 1.1]精度要求0.001有效档位仅2000η就应取较大值如10~20避免一步跨出区间。实操技巧不要全局固定η。我采用“η随代数线性增长”的策略η(t) η_min (η_max - η_min) × (t/T)。例如η_min2, η_max20, T500代则前期η小鼓励大步探索后期η大专注精细打磨。这比固定η10的方案在多个测试函数上平均收敛代数减少了22%。3.4 细节四变异率Mutation Rate的“双刃剑效应”——为什么0.1%有时比10%更有效变异率Pm定义为每个变量在每一代中被变异的概率。教科书常建议Pm1/nn为变量数如10维问题用0.1。但这忽略了问题的“崎岖度Ruggedness”。一个高度崎岖的函数有很多尖锐的局部极值需要高频变异来帮助跳出一个平滑的函数高频变异只会把好不容易爬上的山头又踢下去。更本质的视角是变异率控制的是“种群更新强度”。Pm0.1意味着平均每个个体每代有1个变量被扰动Pm0.001意味着1000个变量中才有一个被扰动。后者看似保守但在高维、昂贵评估如仿真一次要10分钟的问题中它能极大降低“无效计算”的比例。我的做法是将Pm与“种群停滞代数”挂钩。定义一个停滞计数器如果连续G代最优适应度提升小于阈值δ则认为停滞。此时将Pm乘以一个放大因子γ如1.5。一旦检测到显著提升Pm恢复原值。这实现了“按需变异”既保住了前期的稳定性又在陷入困境时主动“搅局”。在优化一个CFD计算流体力学仿真模型的翼型参数时此策略使平均求解时间从18小时缩短到11小时。3.5 细节五适应度函数的“尺度归一化”——为什么直接用原始目标值会毁掉整个算法这是最隐蔽、杀伤力最强的细节。假设你在最小化一个成本函数其值域是[10000, 100000]同时你在最大化一个收益函数其值域是[0.001, 0.01]。如果你把这两个目标直接拼成一个多目标适应度如F cost 1/revenue会发生什么成本项的数值是收益项的十亿倍算法会完全忽略收益只拼命压低成本哪怕收益降为0也在所不惜。解决方案不是“加权重”而是“归一化”。对于单目标问题最稳健的方法是将适应度F映射到[0,1]区间且保证F越优映射值越大最大化问题或越小最小化问题。具体操作若为最小化问题F_norm 1 / (1 (F - F_min) / (F_max - F_min))若为最大化问题F_norm (F - F_min) / (F_max - F_min)其中F_min和F_max是当前种群中观察到的最小/最大值可设初始估计值随进化更新。这个公式保证了所有F_norm ∈ [0,1]消除了量纲和尺度差异当FF_min时F_norm1最优当FF_max时F_norm≈0最差映射是单调的不扭曲原始目标的优劣关系。实操心得归一化后选择算子如锦标赛的性能会变得极其稳定。我曾在一个混合整数规划问题中未归一化时轮盘赌选择因数值过大而溢出报错归一化后一切正常且收敛曲线平滑如丝。记住GA不是在优化你的原始目标函数而是在优化你“告诉它”的那个适应度值。你给它一个混乱的输入它就还你一个混乱的输出。4. 实操过程与核心环节实现从零开始构建一个工业级GA求解器4.1 第一步问题建模与编码方案敲定——一份不可跳过的检查清单在写任何一行代码前必须完成这份检查清单。它能帮你避开80%的返工。变量类型与范围确认列出所有决策变量。是连续float、离散整数int、还是分类categorical每个变量的物理上下界是什么例变量x₁电机转速单位rpm范围[0, 3000]x₂冷却液类型选项{water, oil, air}。约束条件分类硬约束Hard Constraint违反则解无效如x₁x₂≤100软约束Soft Constraint违反则扣分如x₃≥50最好但可接受45这些约束如何在编码中体现硬约束通常需修复机制或罚函数软约束可融入适应度。目标函数明确是单目标min/max f(x)还是多目标min f₁(x), max f₂(x)目标函数的计算成本如何毫秒级分钟级是否可并行这直接决定你能承受的种群规模和代数。编码方案初选基于1、2、3选择最匹配的编码。连续硬约束→ 实数编码修复。排列问题→ 整数编码OX。混合类型→ 混合编码。关键决策点如果硬约束复杂如非线性不等式优先考虑“罚函数法”而非“修复法”因为修复可能破坏遗传操作的有效性。适应度函数设计如何将原始目标和约束转化为一个标量适应度值是否需要归一化是否需要加入多样性奖励如种群熵原则适应度值必须是“越大越好”且数值稳定在合理范围如0~100。完成此清单后你的编码方案就不再是拍脑袋决定而是有据可依。例如一个汽车悬架参数优化问题变量包括弹簧刚度k连续[5000, 20000] N/m、阻尼系数c连续[500, 3000] N·s/m、连杆长度l整数[0.2, 0.5] m精度0.01m → 31个取值。这是一个典型的混合问题。我的方案是对k和c用实数编码对l将其离散化为整数索引[0,30]再用整数编码。整个个体结构为[k_float, c_float, l_int]。交叉时对前两个用SBX对第三个用均匀交叉Uniform Crossover变异时对前两个用高斯变异对第三个用“邻域变异”以概率p变为l±1边界处处理。4.2 第二步核心算子实现——Python伪代码与关键注释以下是一个精简但完整的GA核心循环实现重点展示Part Two强调的模块化与细节处理。使用Python风格伪代码便于理解逻辑。import numpy as np from typing import List, Tuple, Callable, Optional class GA_Solver: def __init__(self, bounds: List[Tuple[float, float]], # [(low1, high1), (low2, high2), ...] int_vars: List[int] None, # [2] 表示第2个变量是整数索引 pop_size: int 100, max_gen: int 500, eta_c: float 15.0, # SBX eta eta_m: float 20.0, # 变异 eta (用于高斯) pm: float 0.1): # 基础变异率 self.bounds bounds self.int_vars int_vars or [] self.pop_size pop_size self.max_gen max_gen self.eta_c eta_c self.eta_m eta_m self.pm_base pm self.pm pm # 初始化种群实数部分用均匀分布整数部分用randint self.population self._init_population() self.fitness_history [] def _init_population(self) - np.ndarray: 初始化种群处理混合编码 pop np.zeros((self.pop_size, len(self.bounds))) for i, (low, high) in enumerate(self.bounds): if i in self.int_vars: # 整数变量先生成浮点再取整再clip到整数范围 n_vals int(high - low) 1 pop[:, i] np.random.randint(0, n_vals, self.pop_size) low else: pop[:, i] np.random.uniform(low, high, self.pop_size) return pop def _evaluate(self, individual: np.ndarray) - float: 评估单个个体返回原始目标值此处为最小化问题 # 此处调用你的业务函数如 simulate(individual) 或 calculate_cost(individual) # 返回一个标量值越小越好 pass def _fitness_func(self, objective_value: float) - float: 将原始目标值转换为适应度值越大越好 # 使用归一化基于当前种群历史的min/max进行动态归一化 # 简化版假设我们有全局估计的 min_obj 和 max_obj min_obj, max_obj 0.0, 100.0 # 替换为你的实际估计 # 归一化为[0,1]越小的目标值对应越大的适应度 fitness 1.0 / (1.0 (objective_value - min_obj) / (max_obj - min_obj 1e-8)) return fitness def _sbx_crossover(self, parent1: np.ndarray, parent2: np.ndarray) - Tuple[np.ndarray, np.ndarray]: 模拟二进制交叉SBX仅对非整数变量生效 child1, child2 parent1.copy(), parent2.copy() for i in range(len(parent1)): if i in self.int_vars: continue # 跳过整数变量 low, high self.bounds[i] # 生成beta u np.random.random() if u 0.5: beta (2 * u) ** (1.0 / (self.eta_c 1.0)) else: beta (1.0 / (2 * (1 - u))) ** (1.0 / (self.eta_c 1.0)) # 生成子代 child1[i] 0.5 * ((1 beta) * parent1[i] (1 - beta) * parent2[i]) child2[i] 0.5 * ((1 - beta) * parent1[i] (1 beta) * parent2[i]) # 边界处理使用反射法 if child1[i] low: child1[i] low (low - child1[i]) elif child1[i] high: child1[i] high - (child1[i] - high) if child2[i] low: child2[i] low (low - child2[i]) elif child2[i] high: child2[i] high - (child2[i] - high) return child1, child2 def _gaussian_mutation(self, individual: np.ndarray) - np.ndarray: 高斯变异仅对非整数变量生效 mutant individual.copy() for i in range(len(individual)): if i in self.int_vars: continue if np.random.random() self.pm: # 按变异率决定是否变异 low, high self.bounds[i] # 计算当前标准差随代数衰减 sigma (high - low) / self.eta_m # 基础sigma # 添加高斯噪声 noise np.random.normal(0, sigma) mutant[i] noise # 边界处理反射法 if mutant[i] low: mutant[i] low (low - mutant[i]) elif mutant[i] high: mutant[i] high - (mutant[i] - high) return mutant def _tournament_selection(self, fitness: np.ndarray, k: int 2) - int: k-锦标赛选择返回被选中个体的索引 indices np.random.choice(len(fitness), k, replaceFalse) selected_idx indices[np.argmax(fitness[indices])] return selected_idx def evolve(self) - Tuple[np.ndarray, float]: 主进化循环 best_individual None best_fitness -np.inf for gen in range(self.max_gen): # 1. 评估当前种群 objectives np.array([self._evaluate(ind) for ind in self.population]) fitnesses np.array([self._fitness_func(obj) for obj in objectives]) # 2. 记录历史 current_best_idx np.argmax(fitnesses) current_best_fit fitnesses[current_best_idx] self.fitness_history.append(current_best_fit) if current_best_fit best_fitness: best_fitness current_best_fit best_individual self.population[current_best_idx].copy() # 3. 动态调整参数 self._update_parameters(gen) # 4. 创建新种群 new_population [] # 4.1 精英保留 elite_indices np.argsort(fitnesses)[-2:] # 保留前2个 for idx in elite_indices: new_population.append(self.population[idx].copy()) # 4.2 生成剩余个体 while len(new_population) self.pop_size: # 锦标赛选择两个父本 p1_idx self._tournament_selection(fitnesses, k2) p2_idx self._tournament_selection(fitnesses, k2)