简介:面向优化算法学习者的灰狼优化算法(GWO)Python实现,以经典Rastrigin函数为测试基准,完整演示了模拟灰狼社会等级(α、β、δ)与狩猎行为的优化流程。压缩包共2个文件,包含Python主程序与Markdown说明文档,整体仅2KB,小巧精炼,适合快速阅读理解与二次修改。目前已有42人学习下载。运行主程序后,可直接显示收敛曲线图,并输出最优解位置与适应度值,便于对照算法原理验证其在Rastrigin函数上的收敛速度与精度。代码结构简洁、注释清晰,既能帮助初学者掌握元启发式优化算法的基本框架,也可作为进一步改进或迁移到其他连续函数优化问题的实用起点。对于需要解决多维非线性优化任务的研究者而言,这份实现同样提供了直观的参考样例。
1. 为什么用 GWO 来优化函数:一个启发式搜索的典型场景
连续函数求最小值这件事,看起来简单,可一旦目标函数没有导数、存在大量局部极值、或者计算一次就要跑好几秒的仿真,传统的梯度下降就变得不可靠。把搜索空间想象成一片高低起伏的地形,你站在某个山坡上,梯度的方向只告诉你眼前最陡的下降方向,却没法告诉你翻过这座山之后还有没有更深的谷。灰狼优化算法(Grey Wolf Optimizer,GWO)的思路是用一群候选解去覆盖这片地形,通过模拟狼群的社会等级和捕食行为,让这些候选解一边保持探索新区域的能力、一边逐步收敛到当前找到的最优区域。这种思路不依赖目标函数的梯度,也不需要目标函数有解析形式,只要能把候选解输入进去、得到适应度值,就可以跑优化。
GWO 的实现门槛很低,核心数学公式不超过十个,几十行 Python 代码就能写出一个够用的版本,配合 matplotlib 可以把收敛过程和狼群位置变化直接画出来,这也是很多人接触群体智能算法的第一个可视化入门项目。对于数据分析、工程设计这类需要反复调参的场景,GWO 的价值在于提供了一套可解释、易调试的搜索框架:你可以画二维等高线看狼群怎么移动,画收敛曲线看算法有没有早熟,画不同参数对比看种群规模对结果的影响。这篇文章从算法原理讲起,给出一份不带依赖、能直接跑通的最小实现,然后把 matplotlib 可视化拆成散点图、等高线图、收敛曲线和子图编排四个部分,最后补充几个实际操作中最容易踩的坑。
2. GWO 的数学建模与包围机制:从灰狼捕猎到参数方程
GWO 的灵感来源是灰狼群体的社会等级。狼群被划分为 alpha、beta、delta、omega 四个层级,alpha 是头狼,负责决定捕猎方向;beta 和 delta 协助决策;omega 是底层个体,服从前三者的指挥。在算法里,alpha 对应当前适应度最好的解,beta 次之,delta 再次之,剩下的所有狼都是 omega。每次迭代时,omega 层的狼会根据 alpha、beta、delta 三只头狼的位置来更新自己的位置,这样既保证了向着最优区域靠拢,又因为三只头狼的位置存在差异,避免了所有个体挤到同一个点上。
2.1 包围猎物的数学表达:从位置更新公式说起
在 t 次迭代时,第 i 匹狼的位置是一个 D 维向量,记为 X_i(t)。它到某一头狼 X_p(t) 的距离和下一步位置由下面两个公式决定:
D_p = | C ∘ X_p(t) - X_i(t) | X_i(t+1) = X_p(t) - A ∘ D_p
其中 ∘ 表示逐元素相乘,A 和 C 是系数向量,A 的计算方式是 A = 2a ∘ r1 - a,C = 2 ∘ r2。注意这里有三个不同的随机源:r1 和 r2 是 [0,1) 区间均匀分布的随机向量,每次迭代重新生成;a 是一个从 2 线性递减到 0 的标量。A 的模长决定了狼的移动策略:当 |A| > 1 时,狼会远离当前的猎物,这是全局探索行为;当 |A| < 1 时,狼会靠近猎物,这是局部开发行为。C 的作用是给猎物坐标加上随机权重,防止狼群过于快速地冲向头狼位置,从而丢失搜索的多样性。
实际更新时,每一匹 omega 狼会分别对 alpha、beta、delta 三头狼做上述计算,得到三个候选新位置,然后取平均值作为最终位置。这个平均操作用数学语言描述就是:
X_i(t+1) = (X_alpha(t) - A1 ∘ D_alpha + X_beta(t) - A2 ∘ D_beta + X_delta(t) - A3 ∘ D_delta) / 3
每匹狼其实是在一个由三头狼围成的三角形区域内移动。alpha 的位置代表当前最优方向,如果 alpha 判断失误,beta 和 delta 的贡献能把狼群拉回到正确的搜索区域,这种冗余设计是 GWO 在简单粒子群算法基础上做的主要改进。
2.2 算法流程的三个阶段:探索、开发与收敛条件
GWO 的完整流程可以拆成三个阶段。第一阶段是初始化:确定种群规模 N、最大迭代次数 T、搜索空间的维度 D 以及上下界,然后生成 N 个 D 维的随机位置向量,计算每个位置的适应度,选出前三名记录为 alpha、beta、delta。第二阶段是迭代更新:每轮先更新 a 的值(通常按 a = 2 - 2t/T 线性衰减),再对每匹狼计算三头狼的包围参数、更新位置,把超出边界的坐标裁剪回可行域,重新计算适应度并刷新前三名。第三阶段是终止判断:达到最大迭代次数时输出 alpha 的位置和适应度值。
三个系数有一个常用的参数配置表,新手照着设置基本不会出大问题:
| 参数 | 含义 | 常用配置 |
|---|---|---|
| N | 种群规模 | 30 左右,维度高时加大到 50 |
| T | 最大迭代次数 | 500 到 1000,看收敛曲线决定 |
| a | 控制探索与开发的平衡 | 2 线性递减至 0 |
| r1, r2 | 随机扰动 | [0, 1) 均匀分布,每轮重新生成 |
与遗传算法相比,GWO 没有交叉和变异操作,代码天然短;与粒子群算法相比,GWO 不用维护每个粒子的个体历史最优和全局最优两组变量,速度向量也省了,更新公式更少。代价是 GWO 的局部开发能力偏弱,在某些多峰函数上收敛精度可能不如加了惯性权重的 PSO,这正好引出一个优化思路:可以把 GWO 的结果当作初始种群,再切换到一个局部搜索算法做精细化,这种混合策略在工程落地中比单独调 GWO 参数有效得多。
3. 用 Python 实现 GWO 的最小可运行版本:代码与参数逐行拆解
先明确目标函数。为了测试优化算法,通常会选用一组有解析形式的基准函数,比如 Sphere 函数 f(x) = Σ x_i²,它只有一个全局最小值点 0,适合验证算法能不能收敛;Rastrigin 函数 f(x) = Σ (x_i² - 10cos(2πx_i)) + 10D,它有大量局部极值,适合测试算法会不会陷入局部最优。这一章的示例代码以 Rastrigin 为例,因为它的地形更接近真实优化问题的复杂度。
3.1 核心代码:一个只有六十行的 GWO 优化器
下面给出一个完整的可运行版本,不依赖任何第三方库以外的工具,只用 numpy 和 matplotlib:
import numpy as np def rastrigin(X): """Rastrigin 函数:大量局部极值,全局最小值在 x = 0 处取得""" D = X.shape[1] return np.sum(X**2 - 10 * np.cos(2 * np.pi * X) + 10, axis=1) def gwo(obj_func, dim, lb, ub, n_pop=30, max_iter=500, seed=42): """灰狼优化器 参数: obj_func: 目标函数,输入 (N, dim) 输出 (N,) 形式的适应度 dim: 维度,整数 lb, ub: 搜索空间的上下界 n_pop: 种群规模 max_iter: 最大迭代次数 返回: alpha_pos: 最优位置 alpha_score: 最优适应度 history: 每次迭代的最优适应度列表 """ rng = np.random.default_rng(seed) lb = np.array(lb) ub = np.array(ub) # 在 [lb, ub] 范围内随机初始化种群 positions = rng.uniform(lb, ub, size=(n_pop, dim)) fitness = obj_func(positions) # 按适应度排序,取前三个作为 alpha, beta, delta sort_idx = np.argsort(fitness) alpha_pos = positions[sort_idx[0]].copy() beta_pos = positions[sort_idx[1]].copy() delta_pos = positions[sort_idx[2]].copy() alpha_score = fitness[sort_idx[0]] beta_score = fitness[sort_idx[1]] delta_score = fitness[sort_idx[2]] history = [alpha_score] for t in range(max_iter): # a 从 2 线性递减到 0,控制探索与开发的比重 a = 2.0 * (1.0 - t / max_iter) for i in range(n_pop): # 对当前狼计算到三头狼的距离 dist_alpha = np.abs(2.0 * rng.random(dim) * alpha_pos - positions[i]) dist_beta = np.abs(2.0 * rng.random(dim) * beta_pos - positions[i]) dist_delta = np.abs(2.0 * rng.random(dim) * delta_pos - positions[i]) # 计算向三头狼移动的候选位置 x1 = alpha_pos - (2.0 * a * rng.random(dim) - a) * dist_alpha x2 = beta_pos - (2.0 * a * rng.random(dim) - a) * dist_beta x3 = delta_pos - (2.0 * a * rng.random(dim) - a) * dist_delta # 取三个候选位置的均值作为新位置 positions[i] = (x1 + x2 + x3) / 3.0 # 越界裁剪 positions = np.clip(positions, lb, ub) # 重新计算适应度并更新前三名 fitness = obj_func(positions) for i in range(n_pop): if fitness[i] < alpha_score: alpha_score = fitness[i] alpha_pos = positions[i].copy() elif fitness[i] < beta_score: beta_score = fitness[i] beta_pos = positions[i].copy() elif fitness[i] < delta_score: delta_score = fitness[i] delta_pos = positions[i].copy() history.append(alpha_score) return alpha_pos, alpha_score, history if __name__ == "__main__": best_pos, best_score, history = gwo(rastrigin, dim=30, lb=-100.0, ub=100.0) print(f"最优位置: {best_pos}") print(f"最优适应度: {best_score}")整个脚本的核心逻辑在迭代循环内部:先算每匹狼到三头狼的距离,注意这里 A 和 C 的随机分量直接在计算中生成,没有单独声明变量,好处是代码短,坏处是如果你要可视化每一轮的具体移动轨迹,需要改写成返回中间状态的形式。a采用线性衰减公式,在迭代初期数值接近 2,此时2*a*rng.random(dim) - a的区间大致是 [-2, 2],狼的步长可以超过与猎物的距离,表现为向外探索;迭代后期a趋近于 0,步长缩小,狼群围拢到小范围精确搜索。
排序取前三名的写法只在初始化时用了一次,之后每轮用三个if判断逐个比较,这种选择方式比每轮重新排序效率高,且能保证一旦 alpha 被更优解替代,原 alpha 顺延为 beta、原 beta 顺延为 delta,维持等级结构的连续性。注意赋值时使用了.copy(),否则会把引用而不是副本存储起来,后续positions[i]的修改会意外改变 alpha_pos 的值——这是新手最容易踩的 alias 陷阱。
history数组单独记录每轮的 alpha_score,方便后续画收敛曲线。运行上面的代码,在 30 维 Rastrigin 函数上,500 次迭代后最优适应度一般能降到 1 以下,如果多次运行结果波动很大,可以把n_pop增大到 50 试试。
3.2 参数怎么调:种群规模、迭代次数与边界裁剪的关系
参数调优的几个原则值得单独说明。种群规模影响的是搜索的覆盖面:N 太小,狼群容易被 Rastrigin 这类多峰函数的局部极值捕获;N 太大,每轮的计算量线性增长,收益递减,实际中 30 是一个覆盖了效率和效果平衡点的经验值。维度是另一个决定性因素,30 维函数的搜索空间体积远大于 2 维,同样的种群规模在低维够用,高维就可能稀疏,所以维度升高时优先增加种群规模而不是迭代次数。
迭代次数的设置取决于你观察到的收敛曲线形态。常见做法是先跑一次 500 次迭代的试验,看history列表最后几十轮的下降幅度:如果最后 50 轮的改进小于 1e-6,说明已经收敛,可以提前终止;如果曲线还在明显下降,说明还没到极限,需要加大迭代次数。不要盲目把迭代次数设到 5000,因为后期a已经接近 0,狼群步长很小,单轮带来的改进极为有限,多跑几千轮往往只是浪费算力。
边界裁剪是一个容易被忽略的细节。上面的代码在每轮更新后统一做了一次np.clip,把越界坐标拉回到边界上。有些实现会随机重置越界个体的位置,那样会破坏已经积累的搜索方向信息,导致收敛变慢。折中的做法是把越界的坐标设置为靠近边界的随机值,保留一部分多样性,但在大多数基准函数上,简单裁剪的效果已经足够好,而且代码更容易维护。
4. matplotlib 可视化:从收敛曲线到多子图对比
可视化之于优化算法,不只是把结果画出来,更是调试工具。没有可视化的情况下,你只能看到最终输出的一堆浮点数;有了图,你一眼就能看出狼群是否在迭代后期还保持着探索能力、是否过早聚集到了某个次优区域、收敛速度是否如预期那样呈指数下降。matplotlib 在这里承担的核心任务有三个:展示目标函数的形状、展示狼群的运动轨迹、展示收敛过程的数值变化。
4.1 二维适应度地形与散点图的叠加:直观看到狼群如何移动
对二维目标函数,最直观的可视化是等高线图加上狼群位置散点图。等高线用np.meshgrid生成网格坐标,然后计算每个点的函数值,再用plt.contour或plt.contourf绘制。散点图则在每次迭代后将positions数组画到同一张图上。下面这段代码展示了具体做法:
import numpy as np import matplotlib.pyplot as plt def plot_2d_gwo(obj_func, lb, ub, positions_list, best_idx_list): """绘制 GWO 运行过程:等高线 + 狼群位置散点 positions_list: 每次迭代后的种群位置列表 best_idx_list: 每次迭代的最优个体索引列表 """ x = np.linspace(lb, ub, 200) y = np.linspace(lb, ub, 200) X, Y = np.meshgrid(x, y) Z = obj_func(np.column_stack([X.ravel(), Y.ravel()])).reshape(X.shape) fig, ax = plt.subplots(figsize=(8, 6)) cf = ax.contourf(X, Y, Z, levels=20, cmap="viridis", alpha=0.8) plt.colorbar(cf, ax=ax, label="fitness") for i, positions in enumerate(positions_list[::20]): # 每 20 轮画一次 ax.scatter(positions[:, 0], positions[:, 1], s=8, alpha=0.5, label=f"iter {i*20}") ax.set_xlabel("x1") ax.set_ylabel("x2") ax.set_title("GWO 狼群位置演变") ax.legend()Z的计算直接把二维坐标展平成一维向量,批量送进目标函数再还原形状,这样能利用 numpy 的向量化能力,避免用 Python 循环逐点计算。positions_list每 20 轮采样一次,是因为 500 轮全画上去会糊成一片,看不出移动趋势。画图时有一个常见的坑:如果你用plt.scatter连续画多帧而不是保存列表一次性绘制,那需要配合plt.pause做动画,但动画的录制和交互调试成本更高,先画一张静态叠图往往能更快发现问题。
4.2 收敛曲线的三种画法:线性坐标、对数坐标与多函数对比
收敛曲线是评估算法性能最直接的证据。最简单的画法是把history列表直接plt.plot(history),但这样画有一个问题:如果最优适应度从 1000 降到 0.001,跨越了六个数量级,线性坐标下前半段曲线会被压低成一条直线,后半段的细节完全看不见。解决方案是把纵轴改成对数刻度,plt.yscale("log"),这样下降过程在图上呈现为近似直线,能清楚看到每个阶段的收敛速度。
更实用的画法是把同一个 GWO 在不同参数下的收敛曲线放在同一张图里对比。下面的代码是一个完整的对比脚本:
import matplotlib.pyplot as plt import numpy as np def compare_convergence(): """对比不同种群规模下 GWO 的收敛行为""" configs = [{"n_pop": 10}, {"n_pop": 30}, {"n_pop": 50}] results = [] for cfg in configs: _, _, history = gwo(rastrigin, dim=30, lb=-100.0, ub=100.0, **cfg) results.append(history) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) for i, history in enumerate(results): axes[0].plot(history, label=f"N={configs[i]['n_pop']}") axes[0].set_xlabel("迭代次数") axes[0].set_ylabel("最优适应度") axes[0].set_yscale("log") axes[0].legend() axes[0].set_title("线性线性对比") for i, history in enumerate(results): axes[1].plot(np.clip(history, 1e-10, None), label=f"N={configs[i]['n_pop']}") axes[1].set_xscale("log") axes[1].set_xlabel("迭代次数(对数)") axes[1].set_ylabel("最优适应度(对数)") axes[1].legend() axes[1].set_title("双对数坐标下的收敛段对比") plt.tight_layout()左边子图用普通横轴加对数纵轴,适合看整体趋势;右边子图把横轴也改成对数坐标,能透视出迭代早期每轮改进的幅度差异。写这段代码时我通常用np.clip(history, 1e-10, None)处理历史中可能出现的 0 值,因为log(0)会报错。如果你对比的是多个版本的算法,不只是参数不同,这个脚本框架可以直接套用,把gwo替换成其他实现即可。
4.3 标题、坐标轴名称和图例的设置规范:让图表具有可解释性
可视化最容易犯的错误是图表信息不完整。一张图交给同事看,对方不知道横轴是什么、纵轴是什么、每个颜色代表什么,那张图的信息传递效率就接近于零。matplotlib 里对应的原子操作就是set_xlabel、set_ylabel、set_title、legend。字体设置也有讲究:matplotlib 默认字体是英文,中文标注会出现方框,常见的解决方案是按下述代码全局设置字体:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False # 修正负号显示第一行把无衬线字体替换为支持中文的字体,第二行关闭 Unicode 负号,否则坐标轴上的负号会显示成方块。上面提到需要标注的图例名,注意不要在label里混入不必要的字符,图例数量超过 5 个时颜色区分度会下降,考虑改用线型或标记形状来区分。
5. 种群规模、维度与绘图的三处细节经验
这一章节聚焦三个具体技巧:怎么处理高维 GWO 的可视化限制、怎么让收敛曲线和位置动画配合分析、以及 matplotlib 在批量实验时的编排技巧。
5.1 高维问题如何可视化的两种变通方案
当维度超过 3 时,无法直接画出狼群位置分布图。变通方案有两个。第一个是选两个最有代表性的维度画投影图,代表维度的选取依据是 alpha 位置中绝对值最大的两个分量,因为它们对适应度贡献最大。实现方式是在每次迭代结束时记录alpha_pos,然后只取这两个维度画散点图,代码如下:
def alpha_trajectory_2d(alpha_history, dim_a, dim_b): """alpha 前两维投影轨迹:把每次迭代的最优点连成线""" alpha_history = np.array(alpha_history) plt.plot(alpha_history[:, dim_a], alpha_history[:, dim_b], marker="o", markersize=2, linestyle="-", alpha=0.6)第二个方案是转做面向目标的聚合可视化,比如画每轮狼群在某一维度上的分布直方图,或者画种群位置到 alpha 距离的箱线图,观察离散程度随迭代的变化。聚合可视化牺牲了空间直觉,但能在二维图上表达出高维搜索过程的多样性演进趋势。
5.2 收敛曲线和位置动画配合分析的共同调参技巧
只看收敛曲线很容易被误导:曲线平滑下降不代表搜索过程健康,可能只是所有狼都挤在同一个局部极值附近,适应度不再改善而已。所以分析时要结合位置分布来看。先画收敛曲线,如果曲线前期快速下降、后期变成平台,再看位置分布图:分布仍然分散,说明算法还在探索;分布高度聚集但不在全局最优点附近,说明发生了早熟收敛——这时应增大种群规模或调大a的初值来增加探索强度。
制作位置动画时,可以用matplotlib.animation.FuncAnimation,但第一次跑通建议先保存成 MP4 或 GIF 而不是实时播放,因为交互界面卡顿会影响观察节奏。注意动画的帧间隔与迭代次数的换算,不要每帧绘制一轮迭代,取每 5 或 10 轮一帧,帧间隔 200 毫秒左右,同时在图上叠加显示当前帧的适应度,这样动画本身就成了一页可回放的收敛报告。
5.3 Python 环境配置与 matplotlib 安装的一个提示
如果本机还没装 matplotlib,在终端执行pip install matplotlib即可。装完后在交互式环境中运行import matplotlib.pyplot as plt,如果报错提示后端问题,通常是缺少 GUI 依赖,一个稳妥的做法是改用export MPLBACKEND=Agg先保存图片验证代码逻辑,确认无误后再切回交互式后端。这条建议在服务器上用 Jupyter 跑实验时尤其实用,能避免长期挂着一个不可交互的绘图进程浪费内存。
GWO 的真正价值不在于它比别的算法好多少,而在于它提供了一个结构足够简单、行为足够可解释的搜索框架——你总能通过一两张图说清楚当前的搜索状态,这一点在调试新问题的时候,比盲目的参数搜索有意义得多。
本文还有配套的精品资源,点击获取