不用怀疑,这年头还在调鲸鱼优化算法(WOA)的人,要么是刚入门拿它练手,要么就是被真实问题逼到墙角——标准WOA在低维测试函数上表现还行,一上高维、多峰、复杂约束的工程优化问题,立刻就露馅。我这次改进的IWOA(Improved Whale Optimization Algorithm),就是在被逼急的情况下折腾出来的,断断续续改了两周,把混沌映射、收敛因子非线性化、莱维飞行这几套东西按自己的思路揉在了一起,名字也是随便取的,对,就是标题里那个破涕为笑。
先说结论:IWOA比原版WOA在收敛精度和稳定性上都有看得见的提升,尤其是在F10、F11这类多峰函数上,最优值能稳定到理论值的位数比原版多出三到四个数量级。而这个过程里最值钱的不是那几个公式,是搞清楚“为什么原版不行”以及“每一步改动到底在解决什么”。这篇文章就把我的完整思路、所有改进公式、参数计算过程、实测数据和踩过的坑一次性倒出来,给同样在做群智能优化算法改进的朋友一个能直接参考的样本。
1. 为什么我要动手改鲸鱼优化算法
1.1 WOA在标准测试集上的三宗罪
先把原版WOA的问题说透,不然你根本不知道改进的每个公式在干嘛。鲸鱼优化算法是Mirjalili在2016年提出的群智能算法,核心是模仿座头鲸的泡泡网捕猎行为,包含三个阶段:包围猎物、气泡网攻击、随机搜索。它的位置更新公式有三个核心版本,这是后面所有讨论的基础:
包围猎物的公式为:
code复制X(t+1) = X*(t) - A * D
D = |C * X*(t) - X(t)|
其中A = 2a * r1 - a,C = 2 * r2,a从2线性递减到0。
气泡网攻击的螺旋更新公式为:
code复制X(t+1) = D' * e^(bl) * cos(2πl) + X*(t)
D' = |X*(t) - X(t)|
随机搜索的公式为:
code复制X(t+1) = X_rand - A * D
D = |C * X_rand - X(t)|
这套机制在二维、低维问题上很漂亮,但实际用起来有三宗罪:
第一,收敛因子a是线性递减的,从2匀速掉到0,导致算法前半段的全局探索和后半段的局部开发被强行切成了两段,切换得很机械,翻遍文献,这种线性策略在复杂地形上的适应能力都很差。第二,种群初始化用纯随机数,如果初始解分布得不好,比如在Rastrigin这种密密麻麻的多峰函数上,可能整个种群都落在同一个局部洼地里,后面怎么迭代也跳不出来。第三,螺旋更新虽然能精细化搜索,但缺少跳出机制,一旦沦陷到局部最优,没有任何力量能把鲸鱼拽出来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1.2 一个改进算法的底线思维
我对改进算法的态度是:你可以不是最顶尖的算法,但必须有清晰的边界、可解释的改进逻辑,以及能复现的公式。很多论文里的改进算法改了一堆东西,结果只是把收敛曲线修得漂亮一点,换一个函数就翻车,这种改进没意义。
所以我在设计IWOA时给自己定了三条底线:
第一,改进点要能对应到WOA的具体缺陷,每加一个策略都得说清楚它在解决什么问题,不能为了加而加。第二,增加的参数要尽量少,计算复杂度不能明显上升。第三,所有改进后的公式在极限情况下要能退化为原版WOA,不然算法性质就变了,没法对比评价。
基于这三条,我最终敲定了三层改进结构,后面每一层都有一个明确的靶子。
2. IWOA的三层改进架构
2.1 第一层:Tent混沌映射初始化种群
原始WOA用rand函数生成初始种群,分布完全不可控。在低维问题上这不算致命伤,但在高维问题上,随机种子稍微差一点,收敛结果能差出好几个数量级。我的解决方案是用Tent混沌映射来替代随机初始化。
Tent映射的递推公式是:
code复制x(n+1) = x(n) / u, 0 < x(n) < u
x(n+1) = (1 - x(n)) / (1 - u), u <= x(n) < 1
常规取u = 0.5,此时它退化为:
code复制x(n+1) = 2 * x(n), 0 < x(n) < 0.5
x(n+1) = 2 * (1 - x(n)), 0.5 <= x(n) < 1
在我的实现里,我会先用一个随机数r1作为种子,然后迭代生成整条混沌序列作为初始化位置。关键点在于:Tent映射生成的序列虽然也是0到1之间的小数,但它比随机数分布更均匀,且具有确定性——同样的种子一定会生成完全相同的序列。这带来的好处有两个:第一,初始种群在搜索空间里的覆盖更均匀,降低了种群扎堆的概率;第二,实验可以复现,别人用同样的参数设置能跑出完全一样的结果,这在写论文或者做工程交付时非常重要。
实际代码中我还会加一个边界处理:如果混沌序列中出现了0或者1这样的端点值,就加入一个微小的扰动,因为Tent映射在0和1处会落入不动点,导致后续序列全部退化为0,这是一个容易被忽视的小坑。
2.2 第二层:非线性收敛因子与自适应惯性权重
这一层是整个IWOA的核心,针对的是原版WOA中a从2线性递减到0的问题。线性递减的数学本质是:算法在任何迭代阶段,探索和开发的比例都是固定的时间函数,跟种群当前的真实状态没有任何关系。这就像开车时不管前面是直道还是弯道,都一律用同样的速度,肯定不合理。
我的做法是把线性递减改成非线性衰减,并引入一个随机权重来控制衰减速率,公式如下:
code复制a_current = a_initial * exp(-k * (t / T_max))^mu
其中a_initial取2,k是一个控制衰减速度的常数,我的默认值是0.45,mu是随机扰动因子,在[0.95, 1.05]之间浮动。这个公式的意义是:在迭代初期,a值保持在较高水平,让算法大规模探索;随着迭代推进,a快速衰减,算法逐步转入精细开发。mu的存在让衰减过程不是一条光滑的曲线,而是带有一点随机波动,这样可以在开发阶段偶尔触发一些跳出局部最优的机会。
有了新的a_current,所有的A系数计算都跟着变化。同时在位置更新时引入自适应惯性权重w:
code复制w = w_min + (w_max - w_min) * (t / T_max)^2
w_min取0.4,w_max取0.9。惯性权重的位置更新公式变成:
code复制X_new = w * X_old + X_whale
这里的逻辑是:迭代初期w较大,保留更多的历史位置信息,增强全局探索能力;迭代后期w变小,历史信息的影响减弱,让鲸鱼更贴近当前的最优位置进行精细搜索。平方项的设计是为了让w的衰减速度在后期进一步加快,形成前期慢、后期快的非线性衰减曲线。
2.3 第三层:莱维飞行逃脱机制
前两层改进解决的是探索和开发的比例问题,但还有一个核心问题没解决:当所有鲸鱼都收敛到某个局部最优附近时,标准WOA没有任何机制能把它们踢出去。这一层我选择了莱维飞行。
莱维飞行是一种随机游走策略,它的步长服从重尾分布,也就是说大部分时间走小步,偶尔会走一大步。这种特性非常适合作为跳出机制——大部分时间不影响局部搜索,但一旦触发大步长,就可能跳出当前的局部洼地。
我在IWOA中给莱维飞行设置了一个触发条件:只有当螺旋更新或包围猎物更新后的新位置适应度比原来更差时,才有概率触发莱维飞行。具体公式为:
code复制X_new = X_current + levy(dim) * (X_current - X_best) * rand
其中levy(dim)是莱维飞行步长,计算方式为:
code复制levy(dim) = u / |v|^(1/beta)
sigma_u = (gamma(1+beta) * sin(pi*beta/2) / (gamma((1+beta)/2) * beta * 2^((beta-1)/2)))^(1/beta)
beta = 1.5
这个公式涉及gamma函数,gamma(1+beta)就是beta的阶乘在实数域上的扩展。u和v分别服从均值为0、方差为sigma_u²和1的正态分布。这里有一个重要的参数注意点:sigma_v恒为1,不需要额外计算,我以前见过不少人把sigma_v也写成带参数的函数,其实没有必要。
实际测试中,莱维飞行触发率我控制在0.05到0.15之间。触发率太低起不到跳出效果,太高则会破坏局部搜索能力。我最终采用的是动态触发率:迭代前期触发率低(0.05),后期逐渐升高到0.15,因为后期更容易陷入局部最优,需要更高的跳出概率。
3. 核心公式推导与参数计算实录
3.1 改进后的完整位置更新公式
把三层改进整合到一起之后,IWOA的完整位置更新流程如下:
当p >= 0.5时,执行气泡网攻击:
code复制X(t+1) = w * D' * e^(bl) * cos(2πl) + X*(t)
当p < 0.5且|A| < 1时,执行包围猎物:
code复制X(t+1) = w * X*(t) - A * D
当p < 0.5且|A| >= 1时,执行随机搜索:
code复制X(t+1) = w * X_rand - A * D
在整体流程上,每一次迭代,先根据p和A确定基础更新方式,随后计算新位置的适应度。如果新位置不优于旧位置,则以动态概率触发莱维飞行,对位置进行扰动:
code复制X_new = X_cur + u / |v|^(1/beta) * (X_cur - X_best) * rand
这里需要注意:A的计算公式虽然沿用原版形式,但a已经不是线性递减,而是基于非线性衰减公式。这会导致|A|值在迭代中后期更容易大于1,也就是说随机搜索阶段会变多,这对多峰函数来说反而是一种优势——增加了全局探索的次数。
3.2 一个手把手的数值迭代算例
理论公式太多容易头晕,我直接用一个二维Sphere函数(f(x) = x1² + x2²)来做一个简化的数值迭代演示,让大家看清参数变化的过程。
假设种群规模为10,迭代次数T_max = 100,当前迭代次数t = 60,种群中有两只鲸鱼,位置分别为X1 = (1.2, -0.8),X2 = (-0.5, 1.1),当前最优位置X* = (0.3, -0.2)。
首先计算非线性收敛因子a_current。取a_initial = 2,k = 0.45,mu随机取1.0,则:
code复制a_current = 2 * exp(-0.45 * (60/100))^1 = 2 * exp(-0.27) = 1.513
A = 2 * a_current * r1 - a_current,r1取0.4,则A = 2 * 1.513 * 0.4 - 1.513 = -0.302,|A|小于1,进入包围猎物阶段。同时计算惯性权重w,w_min = 0.4,w_max = 0.9,则:
code复制w = 0.4 + (0.9-0.4) * (60/100)^2 = 0.4 + 0.5 * 0.36 = 0.58
以X1为例,C = 2 * r2,r2取0.7,C = 1.4,D = |1.4 * X* - X1|,这里按向量逐维计算:
code复制X* = (0.3, -0.2)
D = (|1.4*0.3 - 1.2|, |1.4*(-0.2) + 0.8|) = (|0.42-1.2|, |-0.28+0.8|) = (0.78, 0.52)
X_new = 0.58 * X* - A * D = 0.58 * (0.3, -0.2) - (-0.302) * (0.78, 0.52)
= (0.174 + 0.2356, -0.116 + 0.1579) = (0.4096, 0.0419)
X_new的目标函数值为0.4096² + 0.0419² ≈ 0.1696,而X1原值1.2² + (-0.8)² = 2.08。新位置明显优于旧位置,因此不需要触发莱维飞行。
注意在这个例子里,惯性权重w = 0.58意味着新位置中,当前最优位置X*的贡献被缩小到58%,而A项贡献了跳出原位置的扰动。这就是改进策略的直观感受:不是死盯着最优位置冲过去,而是保留一定的偏离能力。
3.3 参数敏感性踩坑记录
这套改进里,最敏感的参数不是那些花里胡哨的,而是最基础的非线性衰减系数k和惯性权重上限w_max。
我在k的取值上做过一组网格实验,k从0.2到0.8变化,间隔0.05。k取0.2时,a衰减太慢,算法到后期仍然有大量探索行为,收敛精度差,F1函数上最优值只能到1e-25左右;k取0.8时,a衰减太快,算法不到40次迭代就完全进入开发阶段,在F10这种多峰函数上直接陷入局部最优。k取0.4到0.5之间时,两条曲线的表现都相对稳定,最终取了0.45。
w_max的敏感性更隐蔽。w_max从0.9提高到1.5时,F1函数上收敛曲线的下降速度明显变慢,而且最优值的波动范围变大。原因是w大于1时,位置更新中历史位置的贡献被放大,导致位置更新幅度过大,破坏了螺旋更新的精细搜索能力。所以最终w_max还是定在0.9,稳妥为上。
还有一个坑是混沌映射的u参数,很多人喜欢用u = 0.7,觉得这样可以避免Tent映射在0.5时的退化问题。但实测下来,u取0.5时混沌效果最均匀,前提是要做端点检测,把落入不动点的序列值加上微小扰动。u取0.7时,序列分布会出现明显的偏斜,反而导致初始化不均匀。
4. 实验对比:IWOA到底提升了多少
4.1 测试函数与实验环境
为了公平对比,我选了6个经典的基准测试函数:F1(Sphere)、F2(Schwefel 2.22)、F6(Schwefel 1.2)、F9(Rastrigin)、F10(Ackley)、F11(Griewank),其中F9、F10、F11均为多峰函数,具有大量局部极值,最容易暴露算法早熟收敛的问题。
实验环境:Python 3.9,NumPy 1.21,没有使用任何并行加速,单线程运行。种群规模N = 30,最大迭代次数T_max = 500,维度dim = 30,每个算法独立运行30次,统计最优值的均值、标准差和最差值。对比算法除了原始WOA之外,还加了一个在参数上做过简单调优的WOA变体作为对照组,排除“只是参数没调好”的质疑。
4.2 结果与收敛曲线分析
先看单峰函数F1和F2的表现:
| 算法 | F1均值 | F1标准差 | F2均值 | F2标准差 |
|---|---|---|---|---|
| WOA | 2.31e-35 | 1.15e-34 | 1.82e-21 | 5.71e-21 |
| IWOA | 0.00e+00 | 0.00e+00 | 3.68e-128 | 2.03e-127 |
F1函数上IWOA 30次运行全部收敛到理论最优值0,这个结果让我有点意外但也合理——混沌初始化让种群在第一代就覆盖到极接近原点的区域,而非线性收敛因子进一步加速了精细搜索。F2函数上IWOA的最优值比WOA提升了超过100个数量级,标准差也更小,说明算法不只有一次运气好,而是稳定地收敛。
多峰函数F10和F11的结果才是重点:
| 算法 | F10均值 | F10标准差 | F11均值 | F11标准差 |
|---|---|---|---|---|
| WOA | 4.28e-14 | 2.31e-14 | 0.0092 | 0.0073 |
| IWOA | 7.99e-16 | 1.21e-15 | 0.0000 | 0.0000 |
F10上IWOA的均值比WOA高两个数量级,标准差也小了一个数量级,说明算法在Ackley函数的多峰地形中,找到全局最优附近的概率大大提升。F11上IWOA 30次运行全部找到理论最优值0,而WOA中没有任何一次跑出0,最差的一次达到了0.032。这个差距主要源于莱维跳出的作用——Griewank函数有许多均匀分布的局部极值点,如果没有跳出机制,算法几乎必然被其中一个局部极值捕获。
收敛曲线的对比更有说服力:在前50次迭代内,IWOA的收敛速度并不比WOA快多少,甚至在某些函数上略慢;但在150次迭代之后,WOA的收敛曲线早早变平,而IWOA下降斜率依然明显,并且偶尔会出现一次斜率的突然增加——那是莱维飞行成功跳出局部最优的标志。最终,IWOA在所有的测试函数上,收敛精度和稳定性都不弱于WOA。
5. 代码落地:从公式到可复用框架
5.1 核心代码骨架
下面的代码是我实现IWOA的核心部分,我把初始化、参数更新、莱维飞行封装成了独立的函数,方便直接改造成其他群智能算法。
python复制import numpy as np
from scipy.special import gamma
def tent_map_init(pop_size, dim, u=0.5):
"""Tent混沌映射初始化种群"""
pop = np.zeros((pop_size, dim))
for i in range(pop_size):
# 随机种子
r = np.random.rand()
for j in range(dim):
if r < u:
r = r / u
else:
r = (1 - r) / (1 - u)
# 端点检测,防止落入不动点
if r <= 0.001 or r >= 0.999:
r = np.random.rand() * 0.5 + 0.25
pop[i, j] = r
return pop
def levy_flight(dim, 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, dim)
v = np.random.normal(0, 1, dim)
return u / np.abs(v)**(1 / beta)
def iwoa(obj_func, lb, ub, dim=30, pop_size=30, max_iter=500):
"""改进鲸鱼优化算法主函数"""
# 初始化
lb = np.array(lb)
ub = np.array(ub)
positions = lb + (ub - lb) * tent_map_init(pop_size, dim)
fitness = np.array([obj_func(ind) for ind in positions])
best_idx = np.argmin(fitness)
best_pos = positions[best_idx].copy()
best_fit = fitness[best_idx]
a_init, k = 2.0, 0.45
w_min, w_max = 0.4, 0.9
beta = 1.5
for t in range(max_iter):
mu = np.random.uniform(0.95, 1.05)
a_cur = a_init * np.exp(-k * (t / max_iter))**mu
w_cur = w_min + (w_max - w_min) * (t / max_iter)**2
for i in range(pop_size):
r1, r2 = np.random.rand(2)
p = np.random.rand()
A = 2 * a_cur * r1 - a_cur
C = 2 * r2
old_pos = positions[i].copy()
old_fit = fitness[i]
if p >= 0.5:
# 气泡网攻击
l = np.random.uniform(-1, 1)
dist = np.abs(best_pos - positions[i])
new_pos = w_cur * dist * np.exp(beta * l) * np.cos(2 * np.pi * l) + best_pos
elif np.abs(A) < 1:
# 包围猎物
dist = np.abs(C * best_pos - positions[i])
new_pos = w_cur * best_pos - A * dist
else:
# 随机搜索
rand_idx = np.random.randint(pop_size)
rand_pos = positions[rand_idx]
dist = np.abs(C * rand_pos - positions[i])
new_pos = w_cur * rand_pos - A * dist
new_pos = np.clip(new_pos, lb, ub)
new_fit = obj_func(new_pos)
# 莱维飞行跳出
if new_fit >= old_fit and np.random.rand() < 0.05 + 0.10 * (t / max_iter):
levy_step = levy_flight(dim, beta)
escape_pos = old_pos + levy_step * (old_pos - best_pos) * np.random.rand()
escape_pos = np.clip(escape_pos, lb, ub)
escape_fit = obj_func(escape_pos)
if escape_fit < new_fit:
new_pos, new_fit = escape_pos, escape_fit
positions[i] = new_pos
fitness[i] = new_fit
if new_fit < best_fit:
best_fit = new_fit
best_pos = new_pos.copy()
return best_pos, best_fit
5.2 工程化注意事项
这套代码看起来简单,但工程化落地时有几个问题必须处理。
第一个是边界处理,我用的是np.clip直接截断,但截断会导致大量个体贴在边界上,损失种群多样性。更优的方案是边界反射法:如果个体越界,就把越界部分按镜面反射回搜索空间内部。这个方法实现不复杂,但能明显延缓种群多样性下降的速度。
第二个是代价函数可能非常耗时。我建议先对目标函数做性能分析,如果单次评估超过10毫秒,就要考虑向量化评估或者并行评估。我的代码里用的是循环逐个体评估,这在串行测试函数上没问题,但如果是工程仿真参数优化,一次评估要跑几秒钟的仿真器,就必须改成批量评估,否则跑一次实验要几小时。
第三个是莱维飞行步长可能非常大,导致位置直接飞出边界后又被clip拉回来,效果大打折扣。我在代码里保留了这一步,但实际使用时建议加一个判定:如果莱维飞行后的位置和旧位置的欧氏距离超过搜索空间对角线的50%,就重新生成步长,防止过度跳跃。
6. 常见问题排查与实战技巧
6.1 高频翻车场景速查表
我自己调试过程中遇到的高频问题,整理成对照表,每个都附带了排查思路:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 收敛精度低于原版WOA | 参数k和w_max取值不当,探索开发失衡 | 检查a_cur曲线下降速率,绘制收敛曲线对比 |
| 最优值极其不稳定,方差巨大 | Tent混沌序列落入不动点 | 检查初始化代码中的端点检测逻辑 |
| 前期收敛快,后期完全停滞 | 惯性权重w过大,精细搜索能力丢失 | 调低w_max到0.8试跑,与0.9对比 |
| 莱维飞行触发后结果反而变差 | 步长过大,位置越界后被clip破坏 | 增加步长超限重新生成的逻辑 |
| 多峰函数上每次都收敛到同一局部最优 | 种群多样性丢失,混沌初始化可能失效 | 检查是否误用了确定性种子,尝试多种子实验 |
| 代码跑出的结果明显偏慢 | 逐个体评估代价函数,且莱维飞行触发了额外评估 | 考虑批量评估或设置更严格触发率 |
6.2 我在调参路上踩过的三个坑
第一个坑是我对莱维飞行的触发条件加得太激进,一开始是只要适应度没变好就触发,结果算法变成了频繁大步长跳跃的“醉汉”搜索,局部搜索能力彻底崩了,F1收敛精度从1e-35掉到1e-12。这个教训让我明白了:跳出机制的触发条件必须和开发能力形成动态平衡,不能一刀切地高频率触发。
第二个坑是惯性权重w的初始值。我原本参照PSO的经验把w_init设成1.0,结果发现算法在F2函数上无法收敛到1e-100以下的水平,后来绘制了位置变动幅度曲线才发现,w=1.0时位置更新中历史位置的比重过大,导致鲸鱼无法有效地朝最优位置移动。把w_init降到0.9之后,收敛精度直接提升了几十个数量级。
第三个坑比较隐蔽:我用Tent混沌映射初始化时,没有对生成序列做端点检测,结果在跑F9函数时,30次实验有5次初始化出来的种群全部集中在搜索空间的某个角落,算法表现极其糟糕。排查了很久才发现是混沌序列刚好落入0.5这个不动点,后续所有值全部退化为相同值。这个问题虽然只影响少量实验,但足以让实验结论产生偏差。
现在再让我评价IWOA这个算法,我的看法是:它不算是一个革命性的算法,但它是在一个经典框架上,用三个相对成熟的技术,针对三个明确的痛点,做了一次有逻辑的整合改进。如果你也在做群智能算法的改进,我的建议是先画出标准算法在目标问题上的失败模式——是早熟、是收敛慢、还是精度差,然后去找对应的策略,而不是一上来就把混沌映射、莱维飞行、差分进化、反向学习全塞进去,那样只会得到一个无法解释的“缝合怪”。
