简介:这份Python代码库集中封装了差分进化、遗传算法、粒子群、模拟退火、蚁群、免疫及人工鱼群共7种启发式算法,内部结构参照scikit-opt组织,适合需要求解连续优化、组合优化及TSP路径规划等问题的Python开发者。算法实现与测试示例分离,每个优化器独立成模块,便于对照学习或直接嵌入自己的项目中。资源包共83个文件,其中45个py脚本承载核心算法逻辑、目标函数定义及运行入口;25个md文档提供使用说明、方法原理与目录导读;另有yml环境配置、txt依赖说明、sh和bat脚本等辅助文件,整体压缩包约96KB,轻量紧凑,尤其适合用于算法对比实验、智能优化课程作业或本科毕设参考。已有207人浏览学习。配套examples目录覆盖GA、PSO、SA、DE、AFSA、ACA的独立demo,并额外提供GA求解TSP、PSO动画展示、针对VRP的扩展示例,docs文档同时包含中英文页面,拿到后可按示例快速换用目标函数与边界约束,减少从零搭建启发式框架的时间成本。
1. 启发式算法代码库:一个入口切换六种寻优策略,少写一半调参代码
同一个优化问题,昨天用粒子群算法调参调到怀疑人生,换成差分进化算法三分钟就收敛了——这种场景在工程里几乎每周都会遇到。启发式算法没有银弹,但如果手头有一个把差分进化、遗传算法、粒子群、模拟退火、蚁群、鱼群这些经典方法统一封装好的 Python 代码库,换算法就只是改一个字符串的事。这类库解决的核心问题,不是替代 scipy 里的确定性优化器,而是给目标函数不光滑、变量强耦合、甚至像黑匣子一样的仿真模型提供一个统一的寻优入口。适合三类人:刚入门启发式算法、论文里需要做多算法对比、以及工业场景里想快速验证不同求解策略的工程师。这篇笔记按“理解算法脾气 → 跑通最小案例 → 调参与避坑 → 进阶验证”的顺序展开,每步都可复现。
2. 七种算法放进一个库:先搞懂它们的脾气再选型
标题说是七种,列出来只有六种——第七个坑位通常留给人工蜂群算法(ABC)或者差分进化的变体。这其实暴露了这类代码库的架构习惯:不追求算法数量,而是按搜索方式和问题类型把常用算法归类,留出扩展位。用之前得先理解它们各自的脾气,否则只会收获一堆“跑得动但解不对”的诡异结果。
2.1 单点搜索与群体智能:这个库的两条技术主线
六种算法里,模拟退火算法(SA)是唯一的单点搜索:从一个初始解出发,以一定概率接受更差的解,靠温度下降控制“冒险程度”。其他五种都是种群迭代——一次维护一批候选解,通过合作或竞争机制逐代逼近最优。这两条技术主线的差异直接决定了参数调节方式:SA 的成败在温度调度,而种群类算法的成败在种群规模、变异强度和收敛速度的平衡。
蚁群算法(ACO)在技术主线上比较特殊。它面向的是离散路径决策,靠信息素浓度引导后续蚁群的搜索方向,经典应用场景是 TSP、排班、路由这类组合优化问题。把它塞进一个以连续优化为主的代码库里,必须自己做编码映射——把连续变量离散化成路径节点。这也是很多人在这个库上翻车的第一个地方:拿蚁群去解连续函数,要么精度差,要么慢得离谱。
2.2 六种算法的适用边界与参数敏感度对比
| 算法 | 搜索方式 | 核心参数 | 最擅长场景 | 主要风险 |
|---|---|---|---|---|
| 差分进化(DE) | 种群迭代 + 差分变异 | F(缩放因子)、CR(交叉率) | 连续光滑但多峰的目标函数 | F 设置不当收敛慢或早熟 |
| 遗传算法(GA) | 种群迭代 + 交叉变异 | 交叉概率、变异概率、选择压力 | 离散编码、混合整数问题 | 连续问题精度不如 DE |
| 粒子群(PSO) | 种群迭代 + 速度位置更新 | w(惯性权重)、c1/c2(加速度系数) | 连续变量、计算代价低的场景 | 易早熟收敛,后期收敛乏力 |
| 模拟退火(SA) | 单点搜索 + 概率接受 | T0(初始温度)、alpha(降温系数) | 对初值不敏感,适合粗搜索 | 单点搜索,全局性依赖温度调度 |
| 蚁群(ACO) | 信息素 + 路径选择 | 信息素挥发率、启发式权重 | 路径规划、排序、组合优化 | 连续优化需要重新编码,效率低 |
| 鱼群(AFSA) | 聚群 + 追尾 + 觅食 | 视野距离、步长、拥挤度因子 | 多峰、非线性约束问题 | 参数耦合复杂,结果不稳定 |
这六条选型逻辑,才是这个代码库真正值钱的地方。实际项目中不存在“哪个算法最好”,只有“哪个算法在你这类的目标函数形状和约束条件下最省事”。我一般按两步走:先看变量是连续还是离散,连续优先试 DE 和 PSO,离散优先试 GA 和 ACO;再看目标函数是否多峰、是否昂贵,多峰用 DE 或鱼群,单次评估要好几秒的用 PSO 或 SA,因为它们对评估次数的消耗相对可控。
2.3 统一接口设计:为什么“目标函数 + 边界 + 迭代次数”就够
这类代码库最核心的抽象,是把所有算法收敛到同一个入口——minimize(func, bounds, algo, pop_size, max_iter)。你可能会问,各算法明明差别那么大,为什么只抽这几个参数就够了?因为启发式算法的本质都是在“给定空间里用一个评价函数引导搜索”,差异只在引导策略,不在接口。
# 示意接口:一个入口分发六种算法,kwargs 透传各算法专属参数 def minimize( func, # 目标函数,入参是一个 list,返回 float bounds, # 变量边界,如 [(0, 10), (-5, 5)] algo="de", # 算法名: de/ga/pso/sa/aco/afsa/abc pop_size=30, max_iter=200, seed=42, **kwargs, # 各算法的专属参数,如 F、CR、T0、w 等 ): # 内部按 algo 分发到对应实现,并统一维护边界与迭代逻辑 ...逻辑说明:func只接收 list 并返回一个 float,这是整个库的契约——算法内部不管怎么更新候选解,最终都要用这个函数去评价。bounds定义了搜索空间,所有算法都会在迭代时检查越界。algo是分发开关,**kwargs让每个算法保留自己的专属参数而不破坏统一入口。
参数说明:pop_size是种群规模,太小容易早熟,太大浪费算力,二维问题 20~50 足够,三十维以上建议 100 起步。max_iter是最大迭代次数,对 SA 这类单点算法意义不大,但对种群算法是收敛预算。seed是随机种子,固定它才能复现实验结果——后面避坑章节会专门讲这一点。
3. 五分钟跑通最小案例:用差分进化解 Rastrigin 函数
光看接口设计不落地没有意义。这一章用 Rastrigin 函数做测试题,把最小案例完整跑一遍。Rastrigin 是启发式算法评测里的标准函数,多峰、大量局部极值、全局最优在原点,拿来验证代码库的收敛能力正合适。
3.1 安装与导入:先确认环境再动手
代码库对环境要求不高,Python 3.8+,依赖基本只有 NumPy。建议先用虚拟环境隔离,避免污染其他项目的依赖。
# 创建并激活虚拟环境(按项目实际情况选择路径) python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\\Scripts\\activate pip install numpy逻辑说明:虚拟环境是 Python 项目的基本隔离手段,避免“在 A 项目装了老版本,结果 B 项目跑不起来”的问题。pip install numpy是因为绝大多数启发式算法库的底层向量运算都依赖 NumPy。如果你拿到的库还额外依赖 matplotlib(用于画收敛曲线),一起装上即可。
导入代码库时,不同封装暴露的入口不一样,以下按最常见的minimize入口示意,替换成你实际安装的包名即可:
# 假设代码库入口模块叫 heuristic_lib,按实际安装的包名替换 from heuristic_lib import minimize import numpy as np提示:如果导入报 ModuleNotFoundError,先确认当前 Python 解释器是不是虚拟环境里的那个,再检查包是否安装成功。这一步排掉了大概一半的环境类报错。
3.2 定义目标函数和边界:Rastrigin 为什么适合当“试金石”
Rastrigin 函数的公式不复杂,但特性极其刁钻:它在搜索空间里均匀分布着大量局部极小值,全局最小值在原点处。很多算法能轻松拿下凸函数,一到 Rastrigin 就暴露收敛能力。
def rastrigin(x): """Rastrigin 函数:多峰,全局最小在 x=0 处,f(0)=0""" A = 10.0 return A * len(x) + sum(xi**2 - A * np.cos(2 * np.pi * xi) for xi in x) # 二维情况下,变量范围取 [-5.12, 5.12],这是标准评测设置 bounds = [(-5.12, 5.12)] * 2逻辑说明:bounds是一个 list,每个元素是一个元组,对应一个变量的上下界。二维问题就是两个变量,所以这个 list 长度为 2。Rastrigin 的全局最优点在x=[0, 0],目标值f(0, 0) = 0,所以判断算法好坏很简单——看最终输出的目标值离 0 有多远。
参数说明:边界的选择影响搜索难度。把边界放大到[-100, 100],同样的算法和迭代次数下收敛精度会肉眼可见地下降,因为搜索空间膨胀了约 400 倍。这也是为什么“先看边界再选参数”是调参的第一步。
3.3 调用差分进化求解:第一次运行的参数设置
# 第一次运行:DE 算法,30 个个体,500 次迭代,固定随机种子保证可复现 result = minimize( func=rastrigin, bounds=bounds, algo="de", pop_size=30, max_iter=500, seed=42, ) print("最优解:", result.x) print("目标值:", result.fun)逻辑说明:result.x是算法找到的最优解坐标,result.fun是它对应的目标函数值。差分进化算法内部维护一个种群,每轮通过“变异 → 交叉 → 选择”三步更新。这里seed=42保证任何人在同样环境下跑出的结果完全一致。
参数说明:pop_size=30是 DE 常用的起点,解决二维问题绰绰有余。max_iter=500意味着种群最多进化 500 代,实际可能在 200 代左右就已经收敛。如果跑完发现result.fun明显偏离 0,不是算法不行,而是 F 和 CR 这两个专属参数没传——DE 默认的F=0.5, CR=0.7在多数连续问题上够用,但遇到更尖锐的多峰函数时需要调大F到 0.8~0.9 增强探索能力。
3.4 换成粒子群、遗传或模拟退火:只改一个字符串
这个代码库的爽点就在这里——同一份目标函数、同一组边界,换算法只是把algo参数改掉。
for algo_name in ["de", "pso", "ga", "sa"]: res = minimize( func=rastrigin, bounds=bounds, algo=algo_name, pop_size=30, max_iter=500, seed=42, ) print(f"{algo_name.upper():>4} 目标值: {res.fun:.6f}, 最优解: {res.x}")逻辑说明:循环里四次调用minimize,内部各自走不同的求解流程。你可能注意到 SA 也传了pop_size=30——这在统一接口里是无害的,SA 内部会忽略它,因为它是单点搜索算法。这个设计让上层调用代码完全不用关心算法内部细节,实验代码可以保持干净。
参数说明:这里没有给 SA 传初始温度和降温系数,也没给 PSO 传惯性权重w,代码库会使用各算法内置的默认值。默认值通常保守但不离谱,适合“先跑一遍看趋势”的阶段;到了需要压榨精度的阶段,再针对每个算法传专属参数——这正是第 4 章要做的事。
4. 参数调节的通用方法:让七种算法在同一问题上公平对比
一旦能跑通单算法案例,接下来最常见的需求是做算法对比实验。这个需求最容易踩的坑是“不公平对比”——不同算法用了不同评估次数,或者专属参数一个调过、另一个用默认。公平对比的前提是一套统一的调参方法论。
4.1 先定三个基础参数,再动算法核心参数
任何算法的实验,第一步先固定三个全局参数:评估预算(迭代次数 × 种群规模)、变量维度、随机种子。这三个参数不统一,后面所有结论都站不住。
| 算法 | 全局参数建议 | 核心专属参数 | 常见取值范围 |
|---|---|---|---|
| DE | pop_size=50, max_iter=500 | F(缩放因子) | 0.3~0.9,多峰问题取高值 |
| DE | 同上 | CR(交叉率) | 0.5~0.95,变量强耦合时取高值 |
| PSO | pop_size=50, max_iter=500 | w(惯性权重) | 0.4~0.9,常用线性递减 |
| PSO | 同上 | c1/c2(加速度系数) | 1.5~2.0,常见取 1.8 |
| GA | pop_size=50, max_iter=500 | 交叉概率 | 0.7~0.9 |
| GA | 同上 | 变异概率 | 0.01~0.2,高维时调小 |
| SA | 单点,max_iter=10000 | T0(初始温度) | 100~1000,视目标函数量级 |
| SA | 同上 | alpha(降温系数) | 0.85~0.99,越接近 1 搜索越慢越稳 |
调参顺序有讲究。我一般先调种群规模和迭代次数,到“跑一次稳定不早熟”的程度;再动算法专属参数,每次只动一个,记录结果;最后把所有参数组合做 5 次重复实验,取中位数而不是单次结果。这套流程下来,基本能在半天内把一个新的优化问题摸到 80 分。
4.2 收敛曲线对比:一组代码画出全部算法轨迹
调参没有可视化反馈就像闭眼开车。代码库如果记录每代的种群最优值,就可以直接画收敛曲线。标准做法是让返回结果带一个history_fitness数组,里面存了每一代的最优目标值。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) for algo_name in ["de", "pso", "ga", "sa"]: res = minimize( func=rastrigin, bounds=bounds, algo=algo_name, pop_size=50, max_iter=300, seed=1, ) # 画每一代的最优目标值,纵轴用对数坐标方便看收敛差异 plt.plot(res.history_fitness, label=algo_name.upper()) plt.yscale("log") plt.xlabel("Iteration") plt.ylabel("Best Fitness (log)") plt.legend() plt.show()逻辑说明:每一代种群都会产生一个“当前最优目标值”,按迭代顺序连成曲线就是收敛轨迹。用yscale("log")是因为 Rastrigin 这类函数的目标值跨越多个数量级,线性坐标下前期下降会把后期细节全部压扁。曲线的读法有三条:下降快且稳是最好的;下降快但中途平台期长,说明早熟;持续下降但斜率很缓,说明收敛慢,需要加大迭代次数或调整变异强度。
参数说明:max_iter=300对二维 Rastrigin 已经足够,但对更高维的问题要相应提高。history_fitness是这类封装库常见的返回字段,如果拿到手的结果对象没有这个属性,可以在最小化循环里自己记录每代最优值——有些库提供回调函数接口,作用一样。
4.3 无免费午餐定理:什么情况下该换算法
收敛曲线对比做得多了,你会发现一个规律:没有哪个算法在所有测试函数上都拿第一。这就是无免费午餐定理——对全部优化问题取平均,所有算法的性能等价。实际意义很简单:不存在万能算法,只有特定问题下的“称手工具”。
什么时候换算法?我给自己定了四条经验法则。目标函数不光滑、有大量平台区域时,DE 和 PSO 往往比 GA 好,因为差分变异对梯度信息不敏感;变量之间强相关、存在明显耦合时,DE 的交叉率要调高,或者直接换 PSO 配合旋转不变的变体策略;约束条件多且复杂,鱼群算法的聚群和追尾行为有时能绕过约束边界找到可行域,但代价是参数难调,新手慎用;单次目标函数评估要好几秒甚至几分钟时,尽量避免 DE 这种“每代大量评估”的算法,改用 SA 或降种群规模的 PSO。
5. 避坑:启发式算法代码库最容易翻车的五个细节
下面这几条不是理论推演,是真实项目里反复踩过的坑。每一条都按“现象 → 原因 → 解决”写,对照检查自己的代码能省下大半天排错时间。
5.1 目标函数返回 list 而不是 float,让适应度排序全部失效
现象:程序不报错,但结果忽好忽坏,或者种群朝着完全错误的方向进化。检查目标函数,发现返回的是[value]这样的列表。
原因:这个库的契约是目标函数必须返回单个 float。如果返回 list,算法内部做比较时用的是 Python 列表的字典序比较,而不是数值比较——[0.1]会被看成比[0.9]大,排序逻辑全乱。
# 错误写法:返回了 list def bad_func(x): return [sum(xi**2 for xi in x)] # 正确写法:强制转成 float def good_func(x): return float(sum(xi**2 for xi in x))解决:在目标函数末尾加一层float()包装,顺手处理 NaN 和 Inf——很多启发式算法在越界或计算异常时会产生非有限值,最好在函数内部过滤掉。
5.2 越界粒子只做 clip,解被压在边界上
现象:跑出来的最优解坐标恰好等于边界值,比如x=[-5.12, -5.12],目标值离全局最优很远。把边界放宽后,解又恰好落在新的边界上。
原因:粒子群算法更新位置时很容易越界,常见的简单处理是把越界值 clip 到边界。问题是 clip 会把边界变成一个“吸引点”,大量粒子堆积在边界附近,搜索彻底失去多样性。
# 错误做法:越界直接裁剪到边界 x_new = np.clip(x + velocity, lb, ub) # 正确做法:越界个体在边界内随机重生 for j in range(len(x)): if x[j] < lb[j] or x[j] > ub[j]: x[j] = np.random.uniform(lb[j], ub[j])解决:边界处理策略决定算法在高维问题上的表现。随机重生比 clip 好,基于速度反弹的策略也不差。如果代码库默认用 clip,可以在目标函数之外加一层边界约束逻辑覆盖掉。
5.3 随机种子不固定,两次实验无法复现
现象:同一个参数配置跑两次,结果分别是 0.03 和 0.47,差距大到不敢信。换了电脑之后结果又变一个样,实验记录形同虚设。
原因:没有设置随机种子。启发式算法本质是随机算法,每次运行都会使用系统当前熵源生成不同的初始种群和随机变异方向。这不是玄学,是随机性的必然结果。
解决:在minimize调用里固定seed,并且做实验时把“随机种子 + 算法 + 参数 + 边界 + 目标函数版本”一起写进实验记录。发论文或做项目汇报时,这个习惯是底线要求。
提示:有些库的随机种子是全局的,在调用前执行
numpy.random.seed(42)也能锁住,但要小心库里可能用自己的随机模块,最好的方式是通过接口参数传 seed。
5.4 并行评估时全局变量串扰
现象:单进程跑没问题,一改成多进程并行评估,结果出现无法解释的抖动,甚至随机报错,错误信息还各不相同。
原因:目标函数里引用了外部全局变量或共享数据结构,多进程下每个子进程复制了一份内存,但某些库用多线程,线程之间共享全局变量,一改全改。还有一个常见变体是目标函数内部用了random模块且没有局部种子,并行时每个进程的随机序列不一致。
解决:把目标函数写成纯函数——只依赖x和显式传入的常量,不读写任何全局状态。并行评估时用进程池,保证目标函数对象可以被 pickle 序列化:
from multiprocessing import Pool def eval_one(x): return objective(x) # objective 内不能依赖可变全局状态 with Pool(4) as pool: fitness_list = pool.map(eval_one, population)5.5 高维问题里默认参数几乎全失效
现象:同样是 Rastrigin 函数,把维度从 2 提到 30,默认pop_size=30, max_iter=500跑出来的目标值差了几个数量级,看着像算法完全没收敛。
原因:维度增加带来搜索空间指数膨胀。30 维下的边界立方体体积是二维的 10 的几十次方倍,种群规模和迭代次数不变时,算法找到全局最优的概率急剧下降。这是启发式算法的通病,不是代码库有 bug。
解决:先判断是“没收敛”还是“收敛了但精度差”。没收敛就提种群规模和迭代次数,比如从 30/500 提到 100/2000;精度差就调算法专属参数,DE 的F调高到 0.8 以上增强探索,PSO 把w从 0.9 线性降到 0.4 加强后期开发。如果问题本身就是超高维稀疏优化,任何启发式算法都吃力,应当先做特征筛选或降维,而不是硬刷迭代次数。
6. 进阶:给代码库加约束处理,再做可信对比
真实工程问题几乎没有“无约束优化”这回事,变量上限下限之外的约束条件才是常态。这个代码库如果只支持边界约束,最简单的补法是罚函数法——把约束违背量乘一个罚系数加进目标函数,借现有的算法流程继续搜。
def constrained_func(x): # 原始目标 f = rastrigin(x) # 示例约束:x[0] + x[1] >= 1,违背时惩罚 g = max(0, 1 - (x[0] + x[1])) return f + 1e4 * g罚系数太小约束会形同虚设,太大会让目标函数数值范围失衡、算法收敛变差。我一般从 1e3 试到 1e6,看约束满足率和目标值两个指标权衡。另一个更稳的验证方法是多次重复实验取统计量:单次最优值没有任何统计意义,固定 5~10 个不同种子各跑一遍,统计最优值的均值、中位数和满足约束的成功率,再做算法横向对比。这是论文和工程项目里最常被审稿人问到的部分,也是这个代码库能帮你一站式解决的问题。
我自己的习惯是每接到一个优化需求,第一件事先用这个库跑通一个已知最优解的测试函数,确认代码环境没改坏,再换真实工业数据。这个习惯救过我很多次,至少避免了一上来就在复杂问题上瞎调参、最后不知道是算法问题还是代码问题的尴尬局面。希望帮到你。
本文还有配套的精品资源,点击获取