十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用scikit-opt一网打尽七大启发式算法:从GA到ACA的Python实践

用scikit-opt一网打尽七大启发式算法:从GA到ACA的Python实践 简介一套Python代码库封装了差分进化、遗传、粒子群、模拟退火、蚁群、人工鱼群及免疫算法等7种启发式优化方法并附带TSP旅行商问题求解示例适合算法学习者、科研人员及需要快速集成优化模块的开发者参考使用。压缩包共83个文件其中45个.py脚本为核心算法实现与demo演示25个.md文档涵盖说明与使用指南另有少量配置、测试及可视化辅助文件整体仅96KB轻量易部署。已有207人学习下载。内容采用scikit-opt风格的项目结构提供GA、PSO、SA、ACA、AFSA、DE、IA等独立模块并配有TSP、VRP、函数寻优等多类场景代码读者可直接运行demo观察收敛过程也可提取算法类进行二次封装调用快速掌握启发式算法的实现思路与调参技巧。1. 启发式算法不是玄学scikit-opt 把七个算法收进同一套 API做调度、排产、路径规划的人对遗传算法、粒子群算法、模拟退火算法这些启发式算法的态度通常是又爱又怕原理看懂了代码到用的时候却要重新造轮子写出来的版本还未必能收敛。scikit-opt 这个第三方库干了一件事——把差分进化、遗传、粒子群、模拟退火、蚁群、人工鱼群、免疫算法这七类算法封装成风格统一的 Python 接口连续优化传一个目标函数就能跑TSP 传一个距离矩阵也能跑。它的价值在于你不需要成为每一种算法的专家就能先把结果跑出来再谈调参。这个资源适合两类人一类是算法岗想快速对比多种优化器的从业者另一类是业务侧需要交付优化结果、但没时间从零实现算法的工程师。源码包里的 examples 目录就是现成的入门素材跟着 demo 跑一遍比看十篇算法原理都管用。2. 用一个例子把七个算法跑通GA、PSO、SA、DE 的连续优化与参数含义2.1 安装与最小示例从 pip 到第一个遗传算法结果拿到源码包之后我建议你先别急着看源码先把环境跑通。这个库的安装和其他 Python 第三方库一样包名和导入名不一致是个容易绊脚的地方pip 安装用的名字是 scikit-opt而代码里 import 的是 sko。pip install scikit-opt安装完成后先跑一个遗传算法的最小例子确认整个链路是通的import numpy as np from sko.GA import GA def obj(p): x, y p return x**2 y**2 ga GA(funcobj, n_dim2, size_pop50, max_iter200, lb[-10, -10], ub[10, 10]) best_x, best_y ga.run() print(best_x:, best_x, best_y:, best_y)这里 obj 就是你要优化的目标函数GA 会在这个函数上做选择、交叉、变异。n_dim 是决策变量的维度size_pop 是种群规模max_iter 是迭代代数lb 和 ub 分别是每个维度的下界和上界。遗传算法的核心思想是模拟自然选择每一代里适应度高的个体有更高概率被保留下来再通过交叉和变异产生新的候选解。这个 demo 里的目标函数是 x 和 y 的平方和理论最优值是 0跑完你会发现 best_y 落在 1e-6 附近说明优化器确实在工作。如果你的 Python 环境还没配置好遇到 ModuleNotFoundError 之类的报错先检查 pip 是否安装成功再确认 import 的名字是 sko 而不是 scikit_opt。这一步花不了两分钟但能避开后面很多莫名其妙的错误。2.2 七个算法类的统一接口一张表看清选型scikit-opt 把这七个算法都放进了 sko 包里每个算法一个模块调用方式高度相似。我整理了一张表把这七个算法的类入口、常用参数和典型场景列出来做选型的时候对着看就够了。算法类入口需要传的核心参数典型场景遗传算法sko.GA.GAfunc, n_dim, lb, ub, size_pop, max_iter连续优化、组合优化、TSP差分进化算法sko.DE.DEfunc, n_dim, lb, ub, size_pop, max_iter连续优化收敛速度快粒子群算法sko.PSO.PSOfunc, n_dim, lb, ub, size_pop, max_iter, w, c1, c2连续优化适合中等维数模拟退火算法sko.SA.SAfunc, x0, T_max, T_min, L连续优化、TSP单点搜索蚁群算法sko.ACA.ACA_TSPnum_points, distance_matrix, size_pop, max_iter路径规划、TSP人工鱼群算法sko.AFSA.AFSAfunc, n_dim, lb, ub, size_pop, max_iter连续优化全局搜索能力强免疫算法sko.IA.IAfunc, n_dim, lb, ub, size_pop, max_iter连续优化多峰函数表里的 DE 和 GA 接口几乎一致PSO 增加了惯性权重 w 和加速常数 c1、c2SA 的结构差异最大——它不需要种群而是从一个初始解 x0 出发做邻域搜索。ACA 比较特殊它面向组合优化输入不是目标函数而是距离矩阵。AFSA 和 IA 的使用方式和 GA 差不多直接换类名和参数就行。这种统一接口的设计有一个实际好处你可以把目标函数和边界条件固定住只换优化器就能快速对比不同算法在你这个问题上的表现。比如先跑一轮 GA再把 GA 换成 PSO代码改动只有两行数据流完全不用动。2.3 目标函数、dim 和搜索边界跑优化前必须搞清的三件事我拆过的优化需求里七成问题不是算法本身的问题而是目标函数和参数没设置对。第一个容易出错的点是 dim 和目标函数的输入维度不一致。比如你定义了 3 个决策变量但目标函数里只取前两个维度PSO 和 DE 会照常运行只是搜索空间里有一个维度永远不参与计算结果自然不理想。第二个重点是搜索边界 lb 和 ub。启发式算法的初始种群或初始粒子位置是在边界内随机生成的如果边界范围设得太大比如把焊接工艺参数的温度范围设成 0 到 100000算法会在无效区间里浪费大量迭代。我一般会先看业务上允许的取值范围再把边界收紧到物理可行的空间内。这里有个技巧如果你不确定边界是否合理可以先跑一次随机搜索评估一下目标函数在这个范围内的量级再决定要不要缩小边界。第三个是目标函数的程序化写法。SKO 的目标函数要求是输入一个数组、输出一个标量。如果你的目标函数涉及外部资源比如读取日志、请求数据库那计算耗时会成为每次迭代的瓶颈。常见的做法是把所有数据预处理成内存里的数组目标函数只做纯计算。demo_func.py 里那些标准测试函数就是这样的纯计算函数用来验证算法实现是否正确非常方便。import numpy as np from sko.PSO import PSO def obj(p): x1, x2, x3, x4, x5 p return (x1 - 1)**2 (x2 - 2)**2 (x3 - 3)**2 (x4 - 4)**2 (x5 - 5)**2 pso PSO(funcobj, n_dim5, size_pop100, max_iter300, lb[-10] * 5, ub[10] * 5, w0.8, c10.5, c20.5) pso.run() print(PSO best_x:, pso.gbest_x, best_y:, pso.gbest_y)这个例子里 w 是惯性权重控制粒子保持当前速度的程度c1 是自我认知系数拉向粒子历史最优c2 是社会认知系数拉向群体全局最优。三个参数的经验值区间分别是 w 取 0.6 到 0.9c1 和 c2 取 0.5 到 2.5。w 太大粒子会飞过最优解w 太小又容易陷入局部最优。这里展示的就是粒子群算法的完整调用链路输入目标函数、边界运行后从 gbest_x 和 gbest_y 拿结果。你可以直观看到 PSO 和 GA 的差异GA 靠种群代际迭代PSO 靠粒子间信息共享两者在不同问题上的收敛速度差别很大。3. 用 ACA 和 GA 解 TSP从坐标到路径把旅行商问题真正跑出结果3.1 数据准备把坐标变成距离矩阵TSP 的第一步在运输调度场景里遗传算法最常见的解释方式就是路径规划给定若干个城市坐标要求找一条经过所有城市且总距离最短的路径这就是 TSP。scikit-opt 里 ACA 和 GA 都提供了 TSP 专用版本但它们的输入不是原始坐标而是距离矩阵。如果你手头的数据是经纬度或平面坐标需要先自己构造距离矩阵。import numpy as np # 城市坐标每行一个城市 (x, y) coords np.array([ [0, 0], [10, 20], [20, 30], [30, 10], [40, 40], [50, 20] ]) num_points len(coords) distance_matrix np.zeros((num_points, num_points)) for i in range(num_points): for j in range(num_points): distance_matrix[i, j] np.sqrt(np.sum((coords[i] - coords[j])**2))这段代码用欧氏距离构造了完整的距离矩阵。注意 distance_matrix 的最后一行和最后一列在 TSP 算法里会被特殊处理——scikit-opt 里特定算法会把最后一行和最后一列当作起终点约束。如果你直接用随机坐标造数据ACA_TSP 会以索引为城市编号生成的路径是一个排列代表访问城市的顺序。算距离矩阵这一步看起来不起眼但重复坐标会导致距离为 0后面 ACA 计算信息素更新时可能出现除零异常我在避坑章节会详细说。3.2 蚁群算法求解 TSP信息素、启发因子与参数选择蚁群算法求解 TSP 的基本思想是模拟蚂蚁觅食蚂蚁在路径上释放信息素信息素浓度越高的路径被后续蚂蚁选中的概率越大形成一个正反馈。scikit-opt 的 ACA_TSP 把这个过程封装好了需要传的是距离矩阵和几个关键参数。from sko.ACA import ACA_TSP aca ACA_TSP(funclambda x: distance_matrix[x, np.roll(x, 1)].sum(), n_dimnum_points, size_pop20, max_iter100, distance_matrixdistance_matrix, alpha1, beta2, rho0.1) best_x, best_y aca.run() print(ACA best path:, best_x) print(ACA path length:, best_y)这里的 func 定义为路径总长度x 是城市索引的排列np.roll 把路径头尾相接求和就是走完一圈的距离。参数 alpha 是信息素因子控制信息素的相对重要程度beta 是启发因子控制距离的倒数就是启发式信息的相对重要程度rho 是信息素挥发系数。alpha 太小蚂蚁会忽略已有经验beta 太大会让算法退化成贪心搜索。经验值上 alpha 取 1 到 2beta 取 2 到 5rho 取 0.1 到 0.5。蚁群算法最实用的调参思路是先固定 alpha 和 beta把 rho 从 0.5 往下调观察 best_y 的变化如果收敛结果波动大就把 size_pop 从 20 提到 50代价只是多跑几秒。examples 目录下的 demo_aca_tsp.py 里有完整的可运行版本配合 vrp.py 里的数据可以直接复现。3.3 遗传算法解 TSP排列编码下的算子问题与自定义算子GA 解 TSP 的坑比 ACA 多一点。原因是经典的遗传算法算子如单点交叉是面向实数向量设计的而 TSP 的解是城市索引的一个排列。直接拿实数编码的交叉算子去处理排列会产生非法解——比如城市重复出现或漏掉某个城市。scikit-opt 单独提供了 GA_TSP 类来解决这个问题from sko.GA import GA_TSP ga_tsp GA_TSP(funclambda x: distance_matrix[x, np.roll(x, 1)].sum(), n_dimnum_points, size_pop50, max_iter200, prob_mutation0.05) best_x, best_y ga_tsp.run() print(GA best path:, best_x) print(GA path length:, best_y)GA_TSP 在内部使用了适合排列编码的算子和变异策略生成的新个体始终保持合法的排列所以不需要你手动修正。如果你需要尝试自己的交叉策略可以仿照 examples 里的 demo_ga_udf_tsp.py自定义 crossover 和 mutate 函数传进去。这里有一个很实用的观察角度GA_TSP 在小规模 TSP比如 20 个城市以内上收敛速度快但城市数超过 50 后暴力交叉的效率会明显下降。而 ACA_TSP 在路径长度和收敛稳定性上通常更好。所以我的习惯是先用 GA 跑一次快速拿到一个可行解来估算路径长度的量级再切到 ACA 做精调。3.4 模拟退火算法做对照TSP 与邻域搜索的关系模拟退火算法求解 TSP 的思路和 GA、ACA 完全不同它不是维护一个种群而是从一个初始解出发通过邻域交换产生新解再以一定概率接受新解从而跳出局部最优。这种单点搜索机制的好处是实现简单、内存占用小而且 TSP 天然的邻域结构交换两个城市非常适合 SA。from sko.SA import SA def cal_total_distance(path): return distance_matrix[path, np.roll(path, 1)].sum() sa SA(funccal_total_distance, x0np.arange(num_points), T_max100, T_min1e-3, L200) best_x, best_y sa.run() print(SA best path:, best_x) print(SA path length:, best_y)x0 是初始路径这里用了默认的城市顺序。T_max 是初始温度T_min 是终止温度L 是每个温度下的迭代次数。SA 对 T_max 的选择比较敏感如果初始路径的总长度在 300 左右T_max 取 100 就足够让算法在早期接受较差的解实现全局搜索。拿 SA 的结果和 ACA、GA 对比时你往往会发现 SA 在 TSP 上的表现并不差尤其是城市规模不大时因为邻域交换的策略和 TSP 的结构高度匹配。examples 下的 demo_sa_tsp.py 就是这个场景的完整实现可以直接对照。4. 常见问题排查我把七个算法调废之后的五条踩坑记录4.1 现象import sko 直接报 ModuleNotFoundError原因源码包是从 GitHub 或网盘下载的 zip解压后直接把 sko 文件夹拖进了自定义工程目录但运行时的工作目录和 sko 所在目录不在同一个 sys.path。另一种情况是 pip 安装时名字敲错了——装的是 scikit-optimport 却写成 scikit_opt。解决优先走常规安装流程在终端执行 pip install scikit-opt然后确认 import sko 能正常导入。如果你确实想用源码包本地调试把解压后的 sko 目录放在工程根目录下并在入口脚本顶部手动加一行 sys.path.insert(0, os.path.dirname(file))保证解释器能找到这个包。我还遇到过装完两个版本相互覆盖的情况排查办法是看报错堆栈里 sko 的实际路径定位到具体是哪个目录下的文件。4.2 现象GA_TSP 跑出的 best_x 不是一个合法排列原因直接拿 sko.GA.GA 这个类去解 TSP而不是用 GA_TSP。普通 GA 的交叉算子假设解是实数向量两个排列做算术交叉会生成带小数的索引取整后又会出现重复城市导致路径长度计算完全失真。解决换用 GA_TSP 类。如果确实需要自定义算子参考 examples 里的 demo_ga_udf_tsp.py自己写 permutation 专用的交叉和变异逻辑。另外要注意GA_TSP 的输入 func 必须是路径总长度的计算函数不能直接把 sko.GA.GA 的目标函数套过来用。4.3 现象粒子群算法在同一个目标函数上反复收敛到同一个点原因这个现象不一定代表算法坏掉更常见的原因是惯性权重 w 设置过大粒子速度没有衰减全部飞向某一侧的边界附近。还有一种可能是随机种子没固定导致每次运行的初值分布差异极小加上优化目标是单峰函数收敛结果自然集中在同一个区域。解决把 w 从默认值往 0.4 到 0.6 方向调让粒子在后期趋向于局部精细搜索。同时在每次运行前固定 np.random.seed让结果可复现。如果固定种子后还是全部扎堆检查 lb 和 ub 是不是设得过于靠近——比如两个边界只差 0.1那搜索空间本身就没有多样性并不是算法的问题。4.4 现象蚁群算法跑出来的最优路径长度是 inf 或 NaN原因距离矩阵里存在零值或负值。零距离会让信息素更新公式里的分母变成 0负距离会产生 NaN 并持续在迭代中传播。这种情况多半是坐标数据里有重复点或者经纬度缺测值被直接填充成了 0。解决构造距离矩阵之前先对坐标数组做去重把重复城市移除然后在距离矩阵上加一个极小量比如 distance_matrix 1e-6避免零值出现在参与计算的元素中。这个手法不会影响路径排名的正确性因为所有距离都被等量抬升了。处理完之后重新跑 ACA正常情况下路径长度很快就收敛到有限值。4.5 现象模拟退火算法的结果比随机搜索还差原因初始温度 T_max 和目标函数的量级不匹配。目标函数值动辄上千T_max 却只给了 10算法在高温阶段的扰动幅度太小根本没法跳出局部最优或者 T_min 设得过高退火提前结束最后几步还在接受劣化解。解决先用随机搜索跑 500 次记录目标函数值的分布范围。把 T_max 设成这个范围的最大值附近T_min 设到 1e-3。L 是每个温度下的迭代次数一般是 100 到 300。如果目标函数是高度多峰的可以把 T_max 进一步放大让算法在早期充分探索。SA 的可调参数相对少问题一般就出在温度尺度和迭代次数上。5. 结果可信吗用种子矩阵和收敛曲线做一次效果体检算法跑通了结果也出来了但你能确定这个结果是算法在优化还是随机蒙出来的吗我见过太多直接把第一次运行结果当最终结果的情况后来我养成一个习惯任何优化任务交付之前都要做一次效果体检——固定五组随机种子跑五遍记录最优值分布再看收敛曲线。import numpy as np import matplotlib.pyplot as plt from sko.PSO import PSO def obj(p): return np.sum(p**2) seeds [0, 1, 2, 42, 2024] results [] for seed in seeds: np.random.seed(seed) pso PSO(funcobj, n_dim5, size_pop50, max_iter200, lb[-10] * 5, ub[10] * 5, w0.8, c10.5, c20.5) pso.run() results.append(pso.gbest_y) print(五次最优值:, results) print(均值:, np.mean(results), 标准差:, np.std(results)) plt.plot(pso.gbest_y_hist) plt.xlabel(iterations) plt.ylabel(best objective value) plt.show()五次最优值的均值和标准差能回答两个问题第一最优值是否稳定标准差比均值还大往往说明目标函数写错了或边界范围不合理第二算法是否真的收敛到了同一区域如果五次结果差了几个数量级就不是参数问题而是算法没有进入正确的搜索空间。收敛曲线的用途是判断迭代是否提前停止——如果曲线在 50 代之前就完全平了可以把 max_iter 缩到 100 省时间如果曲线还在明显下降但迭代已经用完说明 max_iter 要加。我把这个方法用在了每一个接触到的新目标函数上。从那以后每次换目标函数或换数据集我都会先固定种子跑一组对照确认结果不是随机游走的产物再继续往下调参。这个检查习惯帮我拦下了至少两次返工比任何调参技巧都管用。希望这些从源码包里拆出来的经验能帮到你让 scikit-opt 在你的项目里真正发挥出七种算法该有的价值。本文还有配套的精品资源点击获取
返回列表