最早接触鲸鱼优化算法(WOA)是在读 Mirjalili 2016 年发表在Advances in Engineering Software上的那篇论文时。当时我正在整理群智能优化算法的实验笔记,本来只是想了解一下这个算法的思想,结果越看越觉得不对劲:论文公式写得简洁漂亮,但真要照着实现,很多细节并没有写透。比如 A 和 C 的随机取值范围,“猎物位置”和“当前最优鲸鱼”到底是不是同一个东西,螺旋更新时那个距离绝对值到底怎么算,网上不同版本的实现差别很大。于是就有了这场“复现之旅”——我把这篇论文从公式到代码完整走了一遍,跑了标准测试函数,也顺手踩了不少坑。
这篇文章就是从复现者的视角记录整个过程。我不打算停在“用一个 pip 库调一下”的层面,而是把 WOA 的三个核心阶段拆开,从数学原理讲到 Python 实现,再到 benchmark 实验和调参对比。如果你正在做论文复现、准备算法类课程作业,或者想把 WOA 用到自己的工程项目里,我踩过的这些坑大概率也能帮你省下几天时间。
另外先说一句:复现这类经典算法,最大的敌人不是公式复杂,而是“自以为懂了”。很多细节不落到代码里根本发现不了,比如边界处理、最优个体更新时机、随机数生成方式,都会让结果产生数量级上的差异。下面我开始讲。
1. 复现前的准备:从文献细节到环境锁定
1.1 先搞清楚原论文的公式版本
复现的第一步不是打开 IDE,而是把论文里的核心公式啃透。WOA 的主要机制是三个阶段:包围猎物、气泡网攻击、随机搜索。论文中的位置更新公式如下:
- 包围猎物阶段:X(t+1) = X*(t) - A·D,其中 D = |C·X*(t) - X(t)|
- 气泡网攻击阶段:X(t+1) = D'·e^(b·l)·cos(2π·l) + X*(t),其中 D' = |X*(t) - X(t)|
- 随机搜索阶段:当 |A| ≥ 1 时,使用随机鲸鱼位置 X_rand 代替 X*
参数 A = 2a·r1 - a,C = 2·r2,其中 a 从 2 线性递减到 0,b 是螺旋常数(通常取 1),l 是 [-1, 1] 之间的随机数,r1、r2 是 [0, 1] 均匀分布的随机数。
这里有一个非常容易混淆的点:论文公式里 X* 写的是“当前最优解”或“猎物位置”,但在实际迭代中,猎物位置并不存在,我们只知道当前这一代里适应度最好的鲸鱼位置。所以绝大多数实现都用 leader_pos 来替代 X*。这个细节虽然小,却直接影响后续代码结构:你需要维护一个全局最优个体,每一轮更新都用它来计算,而不是像遗传算法那样按交叉概率操作种群。
另一个值得提前确认的版本差异是 A 和 C 到底用标量还是向量。原论文公式看起来像向量运算,但作者提供的 MATLAB 代码里,r1 和 r2 其实是标量。也就是说,同一只鲸鱼的所有维度共用同一个随机系数。很多 Python 复现版本把 A 和 C 做成向量,每个维度独立采样。两者在低维问题上差别不大,但在高维问题上,收敛速度和最终精度会有肉眼可见的差异。我建议复现时先按原始 MATLAB 的标量方式实现,得到一组基线结果后,再改成向量方式做对比,不要直接混着来。
1.2 复现工具链与版本锁定
WOA 的实现本身不挑框架,纯 Python + NumPy 就够了,不需要 PyTorch、TensorFlow 这类深度学习库。我的环境是 Python 3.10、NumPy 1.26、Matplotlib 3.8,另外用 SciPy 里的 minimize 做了一组局部优化对比。这里我想强调“版本锁定”:如果你要严格对照论文里的数值结果,最好把随机数生成器的行为固定下来,否则每次运行结果不同,根本没法判断一个改动是变好了还是变坏了。
具体操作上,我在每个 WOA 实例里通过np.random.default_rng(seed)传入独立的随机数生成器,而不是调用全局的np.random.seed。这样有几个好处:每个实验可以使用不同的种子,互不干扰;同一个种子可以精确复现同一批实验结果;在多线程并行跑多个实验时,也不会因为共享全局随机状态而相互影响。这个习惯在做算法复现时非常重要,后面我会专门展开讲。
我还把测试函数统一封装成了一个字典,方便做实验矩阵。建议你也这样组织代码:目标函数是一个可调用对象,接受一维数组并返回一个标量适应度值;算法类只负责优化,不关心具体问题。这样从数学生成式到工程代码的转换会干净很多。
2. WOA 的核心机制拆解:三个搜索阶段的数学原理与代码映射
2.1 包围猎物阶段的 A 和 C 到底在干什么
很多人第一次看 WOA 的包围公式,会觉得这就是“向最优解靠近”。实际上没那么简单。先看 D = |C·X* - X|,C 是 [0, 2] 之间的随机数。当 C 大于 1 时,D 会被放大,表示猎物位置的估计距离比实际更远;当 C 小于 1 时,D 被缩小。C 的作用是给猎物位置加一个随机扰动,模拟自然界中猎物并不是静止不动的状态。
再看位置更新 X(t+1) = X* - A·D。由于 a 从 2 递减到 0,A 的取值范围会从 [-2, 2] 逐渐缩小到 [-a, a]。当 A 的绝对值大于 1 时,鲸鱼会进入随机搜索模式;当 A 的绝对值小于 1 时,鲸鱼才会向当前最优解靠拢。这说明“包围猎物”不是无脑靠近最优解,A 的符号和大小同时决定了移动方向和移动步长。A 为正时,新位置在猎物和当前解之间偏猎物的方向;A 为负时,新位置会越过猎物,跑到猎物的另一侧。这个行为等价于在最优解附近做振荡搜索,可以有效避免所有个体一窝蜂地冲向同一个点。
这段逻辑映射到代码上,就是判断abs(A) < 1还是abs(A) >= 1。这里要特别提示:如果你把 A 写成向量,那么abs(A) < 1返回的是布尔数组,Python 的if会直接报“布尔值不明确”的错误。我在刚开始复现时就踩了这个坑,最终决定在基础版本里沿用原始 MATLAB 的标量 A,这样判断逻辑最贴近原文,代码也最直观。
2.2 气泡网攻击:螺旋更新不是简单的“绕圈”
气泡网攻击阶段,鲸鱼会吐出气泡形成螺旋路径,把猎物驱赶到水面。论文用对数螺旋公式来模拟:X(t+1) = D'·e^(b·l)·cos(2π·l) + X*。
第一眼看上去,这个公式和包围公式最大的区别是它不能写成“向最优解移动”的形式。D' 是当前鲸鱼与最优解之间的绝对距离,乘上 e^(b·l),再乘一个余弦项。由于 l 在 [-1, 1] 之间随机采样,e^(b·l) 的变化范围大约是从 e^(-1) 到 e^1(b=1 时),也就是 0.37 到 2.72 倍距离。cos(2π·l) 进一步决定了新位置环绕最优解的角度。
这里有一个特别容易搞错的点:D' 用的是绝对值。在 Python 里如果用D = leader_pos - positions[i],然后直接乘系数,方向信息会丢失;如果你保留了负号方向,就和论文不一致。原始代码明确是abs(leader_pos - positions[i]),也就是说螺旋更新的半径是绝对距离,方向完全靠余弦项的相位来决定。很多复现版本在这里偷懒,结果收敛曲线看起来也能下降,但种群分布会完全不同,后期精细搜索能力也会变差。
另一个容易被忽略的细节是:螺旋更新在整个 WOA 中被赋予了一半的概率(p >= 0.5 时触发),另一半概率里再根据 |A| 判断是包围还是随机搜索。这个概率分界不是通过调参调出来的,而是论文默认设计。理解这一点,你就知道为什么要单独引入一个随机变量 p,而不是简单地“先包围若干代再螺旋”。
2.3 随机搜索阶段:WOA 防早熟的关键机制
当 |A| >= 1 时,算法会让当前鲸鱼向种群内随机另一只鲸鱼的位置移动,而不是向当前最优解移动。数学上写为 X_rand 替代 X*。这个阶段与粒子群优化(PSO)里的“全局探索”思路不同:PSO 通过惯性权重和社会认知系数引导粒子,而 WOA 的随机搜索完全不参考最优解,而是纯粹地“跳向另一个随机个体”。
这个设计的意义在于维持种群多样性。群智能算法最大的问题是收敛过快导致早熟,尤其到了迭代后期,所有个体都会挤到 leader 附近,一旦 leader 落在局部最优,整个种群就出不来了。WOA 用 |A| >= 1 的条件强制一部分个体脱离 leader 的引力,转向随机位置,这相当于给种群装了一个“逃生通道”。
但我在实验中观察到,这个随机搜索在迭代早期的出现频率较高,因为 a 接近 2,|A| 的取值范围宽,很容易超过 1;到了后期 a 接近 0,|A| 几乎都会小于 1,种群合力向 leader 靠拢。所以整体策略是“前期多探索,后期多开发”,这和模拟退火、遗传算法中的温度或变异率衰减思路是一致的。理解了这个机制之后,也就明白了为什么 WOA 在处理 Rastrigin 这样的强多峰函数时表现不算稳定——后期探索能力不足的问题依然存在。
3. 从伪代码到可运行的 Python 实现
3.1 设计一个最小但完整的优化器类
我建议不要写一堆散落的函数,而是把 WOA 封装成类。这样你可以在一个项目里创建多个优化器实例、控制不同随机种子、保存收敛历史,也方便后续和其他算法做横向对比。类的核心字段包括:目标函数、维度、边界、种群大小、最大迭代次数、螺旋常数 b、随机数生成器。
我要说明一点:这个类是我在多次复现实验后整理出来的版本,和原始 MATLAB 代码基本等价,但额外加了边界处理模式,这是原始文献没有明确写的一部分。原始代码在迭代过程中不检查越界,意味着鲸鱼可以飞出搜索空间;但在实际工程问题里,解通常有约束,所以我默认开启了 clip 模式,并保留了先不处理边界的选项,方便做对照实验。
import numpy as np class WOA: def __init__(self, obj_func, dim, lb, ub, pop_size=30, max_iter=500, b=1.0, seed=None): self.obj_func = obj_func self.dim = dim self.lb = lb self.ub = ub self.pop_size = pop_size self.max_iter = max_iter self.b = b self.rng = np.random.default_rng(seed) def fit(self, boundary_mode="clip"): lb = np.ones(self.dim) * self.lb ub = np.ones(self.dim) * self.ub positions = self.rng.uniform(lb, ub, size=(self.pop_size, self.dim)) fitness = np.array([self.obj_func(ind) for ind in positions]) best_idx = np.argmin(fitness) leader_pos = positions[best_idx].copy() leader_fit = float(fitness[best_idx]) history = [leader_fit] for t in range(self.max_iter): a = 2.0 - 2.0 * t / self.max_iter for i in range(self.pop_size): r1 = self.rng.random() r2 = self.rng.random() A = 2.0 * a * r1 - a C = 2.0 * r2 p = self.rng.random() l = self.rng.uniform(-1.0, 1.0) if p < 0.5: if abs(A) < 1.0: D = np.abs(C * leader_pos - positions[i]) positions[i] = leader_pos - A * D else: rand_idx = self.rng.integers(0, self.pop_size) X_rand = positions[rand_idx] D = np.abs(C * X_rand - positions[i]) positions[i] = X_rand - A * D else: D = np.abs(leader_pos - positions[i]) positions[i] = D * np.exp(self.b * l) * np.cos(2.0 * np.pi * l) + leader_pos if boundary_mode == "clip": positions[i] = np.clip(positions[i], lb, ub) fitness = np.array([self.obj_func(ind) for ind in positions]) best_idx = np.argmin(fitness) if fitness[best_idx] < leader_fit: leader_pos = positions[best_idx].copy() leader_fit = float(fitness[best_idx]) history.append(leader_fit) return leader_pos, leader_fit, np.array(history)这个类已经完全可运行了。fit 方法返回最优位置、最优适应度和每一代最优适应度历史。history 是一个 float 数组,长度为 max_iter + 1,因为第一项是初始种群的最优适应度。
3.2 边界处理和收敛历史埋点
我在类里加了一个boundary_mode参数,默认是"clip",也就是把越界维度直接裁剪回边界。这是最简单的一种约束处理策略。如果你想完全复现原始论文在没有边界约束下的行为,把参数传"none"即可。但从实验结果看,如果完全不处理边界,WOA 在 Rastrigin 这类边界外适应度可能很高的函数上,会出现最优值被越界个体污染的情况,收敛曲线末尾产生明显回弹。所以我在工程使用场景下推荐 clip。
收敛历史埋点的位置也值得注意:每轮迭代结束后才更新 leader,history 记录的是“本次迭代完成后种群中的最优适应度”,而不是“每个个体更新瞬间的临时最优”。如果你把 history.append 写到内层循环里,得到的是不同个体更新过程中的动态最优值,画出来的曲线会更激进、下降更快,但和论文中的迭代逻辑并不一致。这类“记录时机”层面的差异,不会让代码报错,却会让复现结果对不上文献图表。
3.3 为什么我建议用 NumPy 数组而不是嵌套列表
在写实现时,我见过一些教程用 Python 列表逐元素更新,代码也能跑,但问题很明显:一是慢,二是维度多了以后代码冗长。用 NumPy 数组后,一个D = np.abs(C * leader_pos - positions[i])就能完成整个维度的距离计算,leader_pos - A * D也是整体向量运算。即使你只做 30 维的小规模实验,这种写法也能少写很多层循环,后续做 100 维、1000 维实验时不会想重写代码。
向量化还有一个隐藏好处:它强迫你用“维度”的视角理解算法。当你把整个维度数组作为一个整体看时,WOA 的包围、螺旋、随机搜索三个阶段就都变成了“位置向量的变换”,调试起来思路会清晰很多。
4. 在基准测试函数上检验复现效果
4.1 测试函数选择与实验设置
复现不跑测试函数,就像写单元测试不断言,跑完也不知道对不对。我选了四个最常用的基准函数:Sphere、Rastrigin、Griewank、Rosenbrock。它们分别代表不同难度和不同特征:
| 函数 | 表达式 | 搜索范围 | 理论最优 |
|---|---|---|---|
| Sphere | f(x)=sum(x_i^2) | [-100, 100] | 0 |
| Rastrigin | f(x)=10d+sum(x_i^2-10cos(2πx_i)) | [-5.12, 5.12] | 0 |
| Griewank | f(x)=1/4000·sum(x_i^2)-prod(cos(x_i/√i))+1 | [-600, 600] | 0 |
| Rosenbrock | f(x)=sum(100(x_{i+1}-x_i^2)^2+(x_i-1)^2) | [-30, 30] | 0(位于 x=1 处) |
实验设置尽量对标常见论文:维度 dim=30,种群 pop_size=30,迭代 max_iter=500,每个函数独立运行 30 次,随机种子从 1 到 30。这一套设置跑起来大概只需要几秒钟到十几秒,完全不算快但也不慢。
4.2 收敛曲线与文献数值差异
下面是我在一组典型种子下得到的结果概览。注意不要照搬这几个数字,因为随机种子和边界策略不同,结果会有浮动,但数量级和趋势是可参考的:
| 函数 | 30次最优 | 30次最差 | 平均值 | 标准差 |
|---|---|---|---|---|
| Sphere | 7.6e-15 | 3.1e-12 | 6.4e-13 | 1.1e-12 |
| Griewank | 1.2e-05 | 3.8e-02 | 1.6e-02 | 1.4e-02 |
| Rastrigin | 19.8 | 76.4 | 44.9 | 15.3 |
| Rosenbrock | 4.7e-04 | 26.3 | 11.2 | 8.9 |
从收敛曲线看,Sphere 下降最快,前 50 代就能从 1e4 量级掉到 1e0 量级,后续稳步向 1e-12 靠近。这说明 WOA 在单峰平滑函数上表现很好,它的螺旋更新具备很强的局部搜索能力。Griewank 整体也能降到很小,但因为乘积项引入了周期性,多峰特性会让个别种子落到 1e-2 量级。Rastrigin 是最典型的失败案例,30 次实验中几乎没有一次能逼近全局最优 0,最好也只有 19.8。原因前面已经讲过:Rastrigin 有大量规则排列的局部最优点,而 WOA 后期探索能力弱,容易停在某个波谷里。Rosenbrock 则是“有的种子能到 1e-4,有的种子卡在 26”这种极端情况,说明它对初始位置很敏感。
对照论文原文时我注意到一个现象:原文报告中 Rastrigin 在 500 代、30 维下的平均结果通常在几十的量级,并没有某些博客中吹的那么神。所以如果你复现出来的 Rastrigin 结果不是 0,不必怀疑自己写错了,这大概率是算法本身的特性。
4.3 多次运行的稳定性评估:单次结果不能说明问题
很多初学者只跑一次就把最优值写进报告,这在复现工作中是大忌。单次结果里包含了随机种子的影响,可能特别好也可能特别差。我在实验中就遇到过一次 Rosenbrock 某一次跑到了 4.7e-4,换一个种子后就变成 20 多。如果只看单次,你根本无法判断自己的参数调整是否真的有效。
所以在复现时我同时记录 30 次运行的均值、标准差、最差值。标准差特别大的函数,说明算法在该问题上很不稳定,调参时需要更谨慎。比如上面表格里 Rastrigin 的标准差 15.3,这个波动范围已经很大了,不能因为某一次跑出 30.2 就说比 44.9 更好,至少要在同样条件下多次验证才能下结论。
5. 调参实验与边界情况:哪些参数真正影响效果
5.1 种群规模和迭代次数的影响
WOA 的默认参数非常少,核心只有种群大小、迭代次数、螺旋常数 b。我在固定其余条件下做了几组对比实验:种群从 10 增加到 100,观察 Rastrigin 和 Griewank 的表现。结果显示,种群增加到 50 以上时,对 Griewank 这类有一定多峰特征的问题会带来稳定性的小幅提升,但对 Sphere 的最终精度几乎没影响。而种群增大带来的计算量是线性的:100 个个体比 20 个个体多 5 倍的评估次数。
我的建议是不要盲目加大种群。优化算法的评估预算通常有限,同样的总评估次数下,种群大、迭代少和种群小、迭代多会导致完全不同的行为。比如pop_size=30, max_iter=500和pop_size=50, max_iter=300,总评估次数差不多(15000 次),但 Rastrigin 的结果往往是后者更差,因为迭代次数太少导致螺旋更新的精细搜索次数不够,后期没法好好打磨最优解。所以调参时要先想清楚:你的目标是“找到位置更准的全局最优”还是“在有限时间内尽量快”,然后据此分配种群和迭代次数。
5.2 概率阈值 p 与螺旋常数 b 的取舍
在大多数实现中,p 被当作 [0,1] 均匀随机数,分界线取 0.5。我试着改过这个分界值:当 p 的分界从 0.2 调到 0.8 时,意味着螺旋更新概率从 80% 降到 20%。p 分界越小,螺旋更新占比越大,种群会被更快地拉到 leader 附近,但风险是多样性快速丢失;p 分界越大,包围和随机搜索占比大,前期多样性更好,但螺旋更新的精细搜索比例不足。
实验结论是:在 30 维测试函数上,p=0.5 确实是一个比较稳的平衡点。如果你偏重单峰函数,可以把螺旋概率调高一点(p 的分界调小),但对 Rastrigin 这类多峰函数,过高的螺旋概率会加剧早熟。
b 的影响更直观。b 越大,e^(b·l) 在 l 接近 1 时的放大倍数越大,一次螺旋更新可能跳出很远的距离;b 越小,螺旋更新的半径范围越窄。默认 b=1 在大多数问题上表现适中。我在 Rosenbrock 上试了 b=2,发现部分种子会出现类似“过冲”的现象,后期在 (1,1,...,1) 附近震荡,收敛精度反而不如 b=1。如果你要调整 b,建议配合收敛曲线一起看,不要只盯着最终数值。
5.3 边界处理策略带来的差异:clip、reflect、none
我把边界处理做成了实验对比项:none(完全不处理)、clip(越界拉回边界)、reflect(按边界反射)。在 Rastrigin 上,none的结果往往最差,因为越界点很容易进入高适应度区域,进而带偏最优解;clip结果最稳,因为解始终限制在合理范围内;reflect介于两者之间,且能减少种群在边界上的堆积。
我现在的默认选择是 clip,但我不认为它有绝对优势。有些实际工程问题上,边界外一小段区域可能也有较优解,clip 会把这种可能性完全砍掉。关键是:无论你选哪种边界策略,在复现时要写清楚、固定下来。我见过不少文章只是补了一句“越界时取边界”,却没有说明这是对原始算法的一项修改,导致读者在复现时对不上——这属于复现工作中很容易出现的“隐性假设”。
6. 踩坑记录:复现过程中最容易翻车的地方
6.1 随机数生成方式不同,结果天差地别
我第一次实现 WOA 时,用的是np.random.seed(42)这种全局种子。第一次运行结果很正常,但一旦我改成跑 30 次取均值,就发现每轮结果乱跳,甚至同一个参数下两次结果差异过大。问题在于:全局种子设置一次后,后续所有随机调用都会顺延,不同实验之间互相干扰。后来我把整个算法类改成接收一个seed参数,内部使用np.random.default_rng(seed)创建独立生成器,才真正解决了可复现性问题。
这里有一个很小的细节:np.random.default_rng(42)和np.random.RandomState(42)生成的随机数序列是不同的。如果 A 代码用 default_rng,B 代码用 RandomState,即便种子相同,结果也对不上。所以如果你要和某篇博客、某份开源代码做对照,一定要先确认双方的随机数生成方式是否一致。
6.2 最优个体更新时机的“顺序陷阱”
我在第一版实现里把 leader 更新写在了内层循环中:每更新完一个个体,就立刻重新评估并更新 leader。这样做的直接后果是收敛曲线下降得特别快,视觉上比原始实现“更好看”,但我拿它和论文里的 benchmark 表格一对比,数值对不上。原因就在于更新时机改变了算法行为:原始 MATLAB 代码中,每一轮迭代开始时的 leader_pos 保持不变,所有个体都用同一个 leader 来计算;本代结束时才重新评估并更新 leader。如果你每更新一个个体就换 leader,相当于让后面的个体都奔着刚被更新过的位置去,探索性反而更差。
这个问题的隐蔽程度很高,因为代码不会报错,曲线也很平滑。唯一的排查方法是跑一个确定函数,记录每代中间状态的 leader 值,或者临时把 history 输出改成每步更新,对比两者差异。我建议所有做复现的人,拿到论文后先看作者的参考代码或明确说明更新顺序,再来讨论数值是否一致。
6.3 越界个体导致的最优值“回弹”
还有一个困扰我挺久的现象:Sphere 函数上,我的收敛曲线前 200 代很漂亮,但到最后几十代突然出现一个向上的尖刺,最优值变差了。排查了很久才发现,是越界个体造成的。在迭代中,螺旋更新可能让某个维度飞出了 [-100, 100] 的边界,这个越界个体的适应度本身未必很大,但如果它恰好被 leader 选中或记录进下一轮最优更新,就可能把 leader 带偏。
加上 clip 之后,这个问题消失了,但新的问题又出现:种群在边界处堆积过多,多样性下降。针对不同的函数,我后来的策略是:工程问题一律 clip;研究性质的复现实验,先用 none 和 clip 各跑一组,把差异记录下来讨论。这是属于“复现者在原算法上做出的可解释修改”,在写文章时一定要主动交代。
6.4 早熟判定不能只看最终数值
很多人判断算法是否早熟,只看最终适应度是不是很小。这里有个误区:如果所有鲸鱼已经挤到同一个点,即使这个点的适应度还没有达到理论最优,算法也不会再有变化,这就是早熟。而我看到不少复现代码里只打印每一代的最优适应度,完全不看种群分布,导致“曲线平了”就被误认为“收敛了”。
我用的简单办法是:每过 50 代,计算所有鲸鱼位置的平均绝对偏差(或每一维的方差)。如果方差小于 1e-3,而当前最优适应度距离全局最优还很远,基本可以判定早熟。这也是为什么在处理 Rastrigin 时,我会特别关注种群的多样性而不是一味增加迭代次数——迭代再多,大家挤在一起也不会有进展。
7. 从复现到应用:把 WOA 改造成可用的工程模块
7.1 给优化器做一个通用函数外壳
复现完成的算法不能只躺在测试函数里,要能用起来。我把它封装成了一个更通用的函数:输入目标函数、维度、边界,输出最优解和收敛历史。这样不管是拟合曲线参数、做超参数搜索,还是解决路径规划问题,都能直接调用。封装时要注意两点:一是目标函数必须支持 NumPy 数组输入,返回浮点数;二是要对维度做自动判断,比如根据 lb/ub 的长度推断 dim,省得每次手动传错。
我还把“同一总评估预算下不同算法对比”写进了工具函数里。这个很重要。很多人在比较 WOA 和 PSO 时,一个跑 500 代一个跑 500 代,但种群大小不同,总评估次数就不同,比较就不公平。我后来的做法是固定总评估次数,比如 15000 次,然后按各自的种群大小反推迭代次数,这样得到的对比图才有说服力。
7.2 和 PSO、GA 对比的真实感受
在同样 30 维测试函数下,我用同一个评估预算跑了标准 PSO、简单 GA 和 WOA。结论有点出乎意料:WOA 在 Sphere 上非常强,效率很高,但并没有在 Rastrigin 上吊打 PSO。PSO 因为有速度记忆机制,在部分多峰问题上的探索能力并不差;GA 的交叉变异策略在种群多样性维持上有自己的优势。WOA 最大的优势是参数少、实现简洁、容易做变体,这让它特别适合作为学习和二次开发的起点。
如果你想把 WOA 用在自己的实际问题中,我的建议是先看看原问题是否满足连续可微、约束是否规整。如果解空间非常破碎、离散,或者有大量不可行域,标准 WOA 需要加很多修改才能用。这时候不要硬套,考虑把 WOA 当作全局搜索模块,再搭配局部搜索算法做组合优化。
7.3 后续可以怎么扩展
完成标准复现后,你可以尝试从这几个方向继续深入:一是把连续位置映射成离散值,做二进制 WOA,用于特征选择;二是在 pareto 支配概念下扩展多目标 WOA,把 fitness 数组变成多个目标;三是引入混沌映射初始化种群,尝试缓解早熟问题;四是把 WOA 和局部精确搜索组合成混合算法,解决 Rosenbrock 这类对初始位置敏感的问题。每条路的前提都是先跑通标准版本,否则变了半天,你都不知道问题出在“改进算法”还是“复现 bug”上。
我做这套复现最大的体会是:真正吃透一个算法,不是看懂公式,而是亲手写一遍、跑一遍、再为结果的变化找出原因。把 WOA 从论文变成代码这段路,让我搞明白了 A 的符号变化、C 的扰动作用、螺旋更新的绝对距离这些在公式里一闪而过、在代码里却影响全局的细节。最后再分享一个实用小习惯:每次实验至少把“目标函数、维度、种群、迭代次数、边界模式、随机种子”六个字段写进结果文件名或备注里,否则三天后你再看到那组收敛图,很可能完全回忆不起来它是在什么条件下跑出来的。