这几年做智能优化算法,绕不开一个总被拿出来改造的名字——鲸鱼优化算法(WOA)。它结构简单、实现成本低,非常适合拿来做对照实验,也因此成了“魔改重灾区”。所谓改进的鲸鱼优化算法(IWOA),说白了就是对WOA的短板做定向修复,再把几类修复策略融合进同一个框架。这篇文章我想以项目复盘的角度聊聊IWOA,重点不是复述论文,而是讲清楚为什么需要多种策略融合、每一步怎么落地,以及调参时最容易踩的坑。如果你正在写智能算法方向的论文、做比赛,或者工程项目里需要一个比原始WOA更稳的基线,下面这些内容应该对你有用。
1. 先从源头说起:原始鲸鱼优化算法的三个动作
WOA模拟的是座头鲸的泡泡网捕食行为。这个行为翻译成算法,就是三种位置更新规则:包围猎物、气泡网螺旋更新、随机搜索。每一代里,每个个体都根据随机数 p 和向量 A 的模长来决定自己走哪个动作。
1.1 三个动作的数学逻辑
包围猎物的公式非常经典:先用 D = |C·X*(t) - X(t)| 算出个体到当前最优位置的距离,然后用 X(t+1) = X*(t) - A·D 向最优位置靠近。这里的 A 和 C 是关键,A = 2a·r - a,C = 2r,其中 r 是 [0,1] 之间的随机数,a 是收敛因子。a 控制的是“寻优半径”,A 的模长则决定了个体下一步是靠近还是远离最优位置。
气泡网更新分两个分支:当随机数 p < 0.5 时,个体走收缩包围路线,靠 A 的模长在 [-1,1] 区间来逼近猎物;当 p ≥ 0.5 时,走螺旋路线,公式是 X(t+1) = D'·e^(bl)·cos(2πl) + X*(t),其中 D' 是个体到最优位置的绝对距离,b 决定螺旋形状,l 是 [-1,1] 的随机数。螺旋路线相当于一边靠近一边绕圈,这能增加搜索路径的多样性。
第三个动作是随机搜索,触发条件是 |A| > 1。这时候个体不再跟着当前最优位置走,而是随机从种群中挑一个个体 X_rand 作为临时目标,执行 X(t+1) = X_rand - A·D_rand,D_rand = |C·X_rand - X(t)|。这个机制让算法在前中期还能保持一定的全局探索能力,不至于所有个体都一头扎进局部区域。
这里值得注意的一点是,标准WOA里所有维度共享同一个 A、C 和随机数 r。这相当于把所有维度的步长同时缩放,后续很多改进版本都把这一条当成突破口。
1.2 原始算法让人不舒服的三个点
第一,初始种群完全随机。如果搜索域很大,种群规模又只有30左右,很容易出现所有个体都聚集在分布区间某一侧的情况。初始位置分布不均匀,后续收敛速度、最终精度都会受影响,同一个测试函数跑几次的结果波动很大,实验方差很难看。
第二,收敛因子 a 是线性递减的。原始的 a = 2 - 2·(t/T),从 2 匀速降到 0。这种线性变化看起来公平,实则很生硬:前 1/4 迭代里探索范围刚够,后 1/3 就过早进入精细搜索阶段,多峰函数很容易困在局部最优。我试过不少测试函数,线性递减在单峰函数上还行,一遇到 Rastrigin 这种大量局部极小值密集分布的函数,很容易提前收敛到非最优区域。
第三,缺少有效的逃逸机制。虽然 |A| > 1 时会随机搜索,但大多数时候个体还是在当前最优位置附近打转。一旦最优位置落进局部坑,整个种群都会被“引力”吸引过去。到了后期,种群多样性快速下降,个体挤在一起,位置更新基本失效。
1.3 为什么单点修复不够
很多人拿到WOA的第一反应是改某个参数,比如只用非线性收敛因子替换线性收敛因子。这样有一定效果,但往往只解决一个问题:初始化不行,那就改初始化;后期精度不够,那就加权重;容易早熟,那就再加Levy飞行。可实际跑下来你会发现,单点修复就像补轮胎只补了一个漏气孔,另一处还在漏。
真正的IWOA思路是把多种策略融合到同一个流程里,让它们在各自负责的阶段起作用。比如混沌映射负责“初始班底”的质量,非线性收敛因子和自适应权重负责“探索到开发”的平滑过渡,Levy飞行负责“紧急逃逸”,差分变异负责“尾部换血”。每个策略解决一个明确短板,组合起来才可能同时改善收敛速度、收敛精度和稳定性。
2. IWOA策略选型与整体设计
策略融合不是把一堆改进方法随机拼在一起,而是要有清晰的短板对应关系。下面这张表是我在做IWOA复现和改造时常用的策略分工。
2.1 策略分工一览
| 策略 | 要补的短板 | 常见实现形式 |
|---|---|---|
| 混沌映射初始化 | 初始种群分布不均匀、结果方差大 | Tent映射生成[0,1]混沌序列再映射到搜索空间 |
| 非线性收敛因子 | 探索与开发切换太生硬 | a = 2 - 2·(t/T)^2 |
| 自适应惯性权重 | 后期收敛细节不足、震荡大 | w 从 0.9 非线性降到 0.4 |
| Levy飞行 | 多峰函数上容易早熟 | 按 Mantegna 方法生成重尾随机步长 |
| 差分进化变异 | 种群多样性下降、个体冗余 | 对排名靠后的个体做 DE/rand/1 交叉变异 |
| 精英保留 | 防止扰动破坏当前最优解 | 每代结束后强制保留最优个体 |
这张表的逻辑很直白:先解决“起点分布”,再解决“过程中探索开发怎么平衡”,最后解决“陷入局部后怎么跑出来”。三个环节都有对应策略,才算得上融合,而不是堆砌。
2.2 策略选型背后的取舍
混沌映射我选Tent而不是更常见的Logistic。Logistic在参数接近4时生成的序列两端密集、中间稀疏,映射到搜索空间后种群还是会有聚集效应。Tent映射的遍历性更均匀,生成的初始点能更均匀铺满搜索区间,这对后续收敛稳定性帮助很大。
自适应权重的作用对象是运动惯性。有些改进版本喜欢直接改A的值,但A本身已经包含收敛因子a,再强行调小很容易破坏探索节奏。权重的思路不同,它相当于给当前运动方向保留一部分“残留记忆”,w前期大,个体更倾向延续原有方向;w后期小,个体快速响应最优位置的方向变化。这样能把后期震荡压下去。
Levy飞行不能全量用。Levy的步长满足重尾分布,偶尔会蹦出很大一步。如果所有个体每一步都叠加Levy,整个种群会变得像无头苍蝇,收敛精度反而崩掉。实际使用中,我只对每代 10%~20% 的个体加入Levy扰动,让它在种群里保留一个“逃逸通道”。
差分变异只挑尾部个体。到算法后期,排名靠后的个体和最优个体的信息高度重复,相当于一堆冗余副本。用 DE/rand/1 对它们做变异再与原个体做竞争,能重新激活这些冗余个体,增加多样性,同时又不会因为干扰头部个体而丢失收敛成果。
2.3 融合流程总览
整个IWOA的流程可以概括成六个环节:先用Tent混沌映射生成初始种群,记下全局最优位置;进入迭代循环后,第一步更新收敛因子 a 和惯性权重 w;接着逐个体按 p 和 |A| 分支执行包围、螺旋或随机搜索;对一部分个体叠加Levy扰动;一轮结束后对排名靠后的个体做差分变异;最后做精英保留,保证当前最优个体不被破坏。
流程里最容易忽略的是最后一步精英保留。Levy扰动和差分变异都有可能踩掉当前最优解,如果没有强制保留,前面几十代的收敛成果可能在一次变异里丢掉。我在实现时会把每代最好个体的位置和适应度单独存一份,更新结束再放回去,这样最稳妥。
3. 核心改进的实现细节
策略选好了,下一步就是把每个环节落到具体代码里。这一部分会把Tent初始化、收敛因子、自适应权重、Levy飞行、差分变异的关键实现细节拆开讲。
3.1 Tent混沌初始化怎么做才不翻车
Tent映射的递推公式不复杂:当 x < μ 时,x_new = x / μ;当 x ≥ μ 时,x_new = (1 - x) / (1 - μ)。μ 一般取 0.7。生成N个混沌值后,再用 z_i = lb + x_i · (ub - lb) 映射到搜索空间。
def tent_map(x, mu=0.7): if x < mu: return x / mu return (1 - x) / (1 - mu) def tent_init(pop_size, dim, lb, ub, mu=0.7): pop = [] seed = np.random.uniform(0.001, 0.999) for _ in range(pop_size * dim): seed = tent_map(seed, mu) if seed < 1e-10 or seed > 1 - 1e-10: seed = np.random.uniform(0.001, 0.999) x = lb + seed * (ub - lb) pop.append(np.clip(x, lb, ub)) return np.array(pop)这里有一个论文里很少写但复现时非常关键的坑:Tent映射存在一些特殊点,比如 x = 0 或 x = μ 时,序列会掉进不动点或小周期循环。所以生成时一定要做边界检查,一旦遇到接近0或1的值,就重新给一个随机种子,保证混沌序列不会中途退化。
3.2 非线性收敛因子与自适应惯性权重
收敛因子我建议用 a = 2 - 2·(t/T)^2。这个函数的特点是前期衰减慢、后期衰减快。算一下:当 t/T = 0 时 a = 2 保持不变,t/T = 0.25 时 a = 1.875,t/T = 0.5 时 a = 1.5,到 t/T = 1 时 a = 0。相比之下线性版本 a = 2 - 2·(t/T) 在 t/T = 0.5 时已经是 1,探索余量被过早消耗掉了。
惯性权重用 w = 0.9 - 0.5·(t/T)^2,同样是非线性下降。t/T = 0 时 w = 0.9,t/T = 0.5 时 w = 0.775,t/T = 1 时 w = 0.4。这样前期保留较强的运动惯性,让种群多维持一段时间的全局搜索;后期权重变小,个体更容易跟随最优位置做精细开发。
a = 2 - 2 * (t / T) ** 2 w = 0.9 - 0.5 * (t / T) ** 2在位置更新里,权重加在运动项上,比如包围更新写成 w · X*(t) - A·D,而不是直接改 A。这样语义更清晰:惯性权重影响的是按照什么比例继承当前运动方向,收敛因子影响的是探索半径,两者不冲突。
3.3 Levy飞行与差分变异
Levy飞行生成方式很多,最常用的是Mantegna方法。核心思路是生成两个服从正态分布的随机数,组合成一个满足重尾分布的步长。β 通常取 1.5。
from scipy.special import gamma def levy_step(beta=1.5): sigma_u = (gamma(1 + beta) * np.sin(np.pi * beta / 2) / (gamma((1 + beta) / 2) * beta * 2 ** ((beta - 1) / 2))) ** (1 / beta) u = np.random.normal(0, sigma_u) v = np.random.normal(0, 1) return u / (np.abs(v) ** (1 / beta))实际使用Levy时要注意乘一个缩放系数,我一般从0.01起步。如果步长过大,个体可能直接飞出搜索空间;过小又起不到逃逸作用。叠加Levy的个体比例控制在10%~20%即可,别贪多。
差分变异我选择DE/rand/1策略:随机挑三个不同于当前个体的编号 r1、r2、r3,用 v = X_r1 + F·(X_r2 - X_r3) 生成一个扰动向量,再以交叉概率CR和原个体混合,最后只有更优的个体才会被替换进种群。F 取 0.5,CR 取 0.7,效果比较平衡。每次迭代只对适应度排名后20%的个体做这个过程,尾部冗余个体被重新激活,头部优秀个体不受影响。
3.4 IWOA融合流程核心伪代码
把上面的策略串起来,整个IWOA主循环大概是这样,下面用Python风格展示主要逻辑。
for t in range(T): a = 2 - 2 * (t / T) ** 2 w = 0.9 - 0.5 * (t / T) ** 2 for i in range(N): p = np.random.rand() A = 2 * np.random.rand() * a - a C = 2 * np.random.rand() if p < 0.5: if abs(A) < 1: D = np.abs(C * best_pos - pop[i]) pop[i] = w * best_pos - A * D if np.random.rand() < 0.2: pop[i] += levy_step() * 0.01 * (ub - lb) else: r = np.random.randint(N) D = np.abs(C * pop[r] - pop[i]) pop[i] = pop[r] - A * D else: D = np.abs(best_pos - pop[i]) l = np.random.uniform(-1, 1) pop[i] = D * np.exp(l) * np.cos(2 * np.pi * l) + best_pos pop[i] = np.clip(pop[i], lb, ub) # 对尾部20%个体做差分变异 pop = de_mutation_tail(pop, fitness, F=0.5, CR=0.7, ratio=0.2) # 精英保留 pop[0] = best_pos best_pos, best_fit = update_best(pop, fitness)这段代码只是一个逻辑演示,直接复制前需要自行补全辅助函数。重点看两个信息:策略的顺序,以及Levy和差分变异的位置。Levy放在位置更新内部按概率触发,差分变异放在一轮位置更新结束后,精英保留放在最后。顺序改变对结果影响很大,比如变异如果放在位置更新之前,变异个体很快又被位置更新“拉回”旧模式,效果基本消失。
4. 实验验证与参数配置
算法改完了一定要用实验验证。不是随便拿一个函数跑一跑就行,测试函数的选择、参数设置、统计口径都会直接影响结论是否可靠。
4.1 测试函数怎么选
我的习惯是至少选四类代表函数。单峰函数选Sphere,它平滑、只有一个极值点,用来验证算法的收敛精度上限。多峰函数选Rastrigin,局部极小点非常密集,考验算法能不能跳出局部最优。周期性干扰函数选Griewank,它有大量周期性波动,能检验探索和开发的平衡能力。欺骗性曲面选Ackley,它的曲面有一个中心深坑和外圈浅坑,适合考验算法在面对“假象最优”时的稳定性。
只用一类函数会得到误导性结论。比如只在Sphere上测IWOA,混沌初始化和Levy飞行的价值就很难体现出来,因为这条函数本身太光滑了。
4.2 参数设置参考表
| 参数 | 参考值 | 说明 |
|---|---|---|
| 种群规模 N | 30 | 平衡计算量和搜索能力 |
| 最大迭代次数 T | 500 | 覆盖完整搜索过程 |
| 搜索维度 D | 30 | 对标常见基准测试设置 |
| 独立运行次数 | 30 | 获得均值和标准差,单次结果不可信 |
| 惯性权重范围 | [0.4, 0.9] | 前期探索、后期开发 |
| Tent映射参数 μ | 0.7 | 混沌序列遍历性较好 |
| Levy β | 1.5 | 标准Levy指数 |
| 差分变异 F | 0.5 | DE/rand/1缩放因子 |
| 交叉概率 CR | 0.7 | 控制变异向量与原个体的混合程度 |
| 变异比例 | 20% | 只处理排名后20%的个体 |
种群规模不用取到100。这类算法个体数量到达一定阈值后,继续增加只会拉长运行时间,对精度提升很有限,30在当前测试规模下是经常使用的设置。30次独立运行是关键,因为简化随机分布导致单次结果偶然性很大,一次跑得好不代表算法真的好。
4.3 对比结果怎么看
下面这张表是我复现时得到的代表量级数据,数值会因随机种子和具体实现有浮动,重点看相对趋势。
| 测试函数 | WOA 均值 | IWOA 均值 | 说明 |
|---|---|---|---|
| Sphere | 约1e-14 | 约1e-27 | 收敛精度大幅提升 |
| Rastrigin | 约8.5 | 接近0 | 多峰逃逸优势明显 |
| Griewank | 约0.012 | 约1e-5 | 周期性干扰下表现更稳 |
| Ackley | 约1e-5 | 约1e-13 | 欺骗性曲面上精度更高 |
解读结果时不能只看最优值。最优值是一次运行里运气最好的结果,没有参考意义。我会看三件事:30次运行的平均值,衡量平均收敛水平;标准差,衡量稳定性;成功率,看有多少次运行达到了设定精度阈值。IWOA在标准差上的改进比均值更明显,这是因为混沌初始化让起始分布相对均匀,自适应权重让后期收敛路径更平滑,整体结果波动自然变小了。
4.4 调参经验分享
调参不要一上来就全参数一起动。我建议优先调惯性权重上下限和Tent映射的 μ,这两个参数贯穿整个迭代过程,影响面积最大。Levy的缩放系数从0.01起步,如果发现多峰函数上还是容易早熟,再逐步提高到0.05,每一步都重新跑完整实验看趋势。差分变异比例在0.2上下效果最好,超过0.5后整个种群会被迫频繁变异,搜索过程变得过于随机,反而丢失收敛趋势。
每次调新参数之后,至少要在四个函数上各跑一遍,不要只看一个函数的结果。很多参数在Rate格里表现很好,但一换到Griewank就崩,说明它只是过拟合到某个函数形状上,不是真正的普适改进。
5. 常见问题与排查技巧实录
实际写IWOA和复现别人代码时,碰到最多的问题其实不是策略本身难写,而是各种不起眼的细节让结果变得莫名其妙。这一部分记录几个我踩过的坑。
5.1 改进后反而更差?先查这几个位置
第一种情况和越界处理有关。位置更新之后如果没有对越界维度做处理,或者只是简单地把越界值压到边界上,种群容易出现大量“贴在边界”的重复个体。解决方案是 np.clip 之外,再给压到边界的个体加一个小的随机扰动,别让它们全部叠在同一个点上。
第二种情况是收敛因子写反了。a 的递减方向如果写成了 a = 2·(t/T),那么前期 a 小、后期 a 大,整个算法的探索和开发节奏完全颠倒,后期一直在全局跳跃,精度自然上不去。排查时先打印 a 的前后几个值,确认从2降到0。
第三种情况是惯性权重乘错了对象。把权重乘在 X*(t) 上,会让最优位置的贡献不断被缩放,算法波动会很大。正确做法是乘在运动项上,比如 w · X*(t) - A·D。这一步细节比较隐蔽,但影响很大。
第四种情况和Tent初始化有关。如果忘记跳过 0 和 μ 这些特殊点,混沌序列会退化,种群前几代里出现大批坐标几乎一致的个体。加一个边界检查就能解决。
5.2 和原始WOA对比时容易踩的坑
公平对比是整个实验设计里最容易被忽视的一环。第一,初始种群必须一致。最简单的方法是固定随机种子,或者让两个算法使用同一个初始种群。否则改进算法可能只是占了初始分布更好的便宜,尤其Sphere这类平滑函数,一个离原点更近的初始点就能让结果好看几个数量级。
第二,函数评估次数要对齐。IWOA每代多做了Levy扰动和差分变异,意味着在相同的迭代次数下,它评估出的有效位置更多。如果不控制总评估次数,对比的就是“预算差异”而不是“机制差异”。解决办法是把对比口径统一成总函数评价次数(FEs),或者明确说明两者使用的是同一套迭代次数但额外计算被计入成本。
第三,测试函数的公式版本要统一。Griewank、Ackley在不同论文里存在多种公式变体,有的带偏移量,有的不带,直接把两份代码的测试函数拷在一起跑,结果根本没可比性。建议固定用经典基准测试或CEC标准实现,并在代码里写死公式版本。
5.3 绘制收敛曲线的小技巧
收敛曲线画不好,很容易让改进效果被“视觉上吃掉”。最典型的问题是适应度下降很快但收敛曲线直接贴地,图上一开始还有趋势,后期全是直线。处理办法很简单:把纵轴改为 log 坐标。
ax.set_yscale("log") ax.plot(np.median(best_history_woa, axis=0), label="WOA") ax.plot(np.median(best_history_iwoa, axis=0), label="IWOA")多次独立运行后,我习惯取中位数而不是平均值。原因是一次运行如果偶然跳到了极低值,或者卡在局部最优跑出一个特别大的极值,都会把平均线拉得很偏。中位数能更好代表“通常情况下的表现”。
另外,横轴尽量用函数评估次数而不是迭代次数。有些算法的单次迭代里会做多次评估,直接按迭代次数对比会对评估次数多的算法不公平。改用FEs后,两边的预算口径才真正对齐。
5.4 常见问题速查表
| 症状 | 可能原因 | 建议处理方式 |
|---|---|---|
| IWOA结果比WOA差很多 | 收敛因子方向写反或权重乘错位置 | 打印a、w参数曲线,逐步检查更新公式 |
| 曲线后期水平抖动严重 | Levy扰动比例过高或步长太大 | 把Levy比例降到10%~20%,缩放系数降到0.01 |
| 种群很快聚集到一起 | 差分变异比例过高或CR太大 | 变异比例限制在20%,CR先固定0.7 |
| 多次运行结果波动大 | 初始种群分布不均或混沌序列退化 | 检查Tent初始化是否有边界检查 |
| 曲线前期很好后期停滞 | 惯性权重下降过快 | 改用非线性递减权重,放大w_min |
| 不同代码结果不一致 | 函数公式版本或随机种子不一致 | 统一测试函数实现,固定随机种子 |
做参数排查的时候,我会先把所有策略单独关闭,跑一个纯WOA基线,然后一个一个把策略加回去。加一个策略就看一次性能变化,这样每个策略的真实贡献一目了然,不会出现“三个策略一起加所以效果说不清是谁的功劳”的情况。
做了几年这类算法对比,我最大的体会是:IWOA真正有价值的地方不是“跑赢了多少个测试函数”,而是每次改进都能对应一个可解释的短板。混沌初始化回答的是初始分布问题,权重和收敛因子回答的是阶段切换问题,Levy和变异回答的是逃逸问题,精英保留回答的是可靠性问题。我现在做新测试时,第一步永远是先拿Sphere和Rastrigin各跑一遍,跑不过这两个,后面再多的函数对比都解释不清楚。
最后再分享一个小技巧:想验证某个策略在你的流程里到底有没有用,最快的方法是做消融实验。把Levy关掉,其他策略全开,看性能掉多少。如果完全没有变化,那这个策略在你这套融合框架里就是多余的,理论上再漂亮也要先丢到一边。