做群体智能优化这几年,我自己复现过的算法少说也有七八种,鲸鱼算法(WOA)是其中之一,也是我反复回头看的一个。它的机制简单、参数不多,但有个老毛病:收敛速度快是快,可一到高维多峰函数上,很容易一头扎进局部最优就出不来。后来我在做改进实验时,把Circle混沌映射和反向学习两种机制揉进去,局面一下子变了。这篇文章就是完整记录这次改进的思路、完整Python实现和实测对比,适合准备做算法改进、论文复现,或是想在工程里换一个更稳优化器的朋友参考。
想搞清楚为什么要加这两把“魔法”,得先明白标准WOA在寻优时到底在干什么,以及它的短板出在哪。
1. 先看标准鲸鱼算法的短板在哪里
1.1 WOA的三种核心位置更新机制
鲸鱼优化算法是Mirjalili在2016年提出来的,它模仿的是座头鲸用气泡网捕猎的方式。座头鲸发现猎物后,会从深水螺旋上升,一边吐气泡一边把磷虾、小鱼群逼到水面,最后一口吞掉。WOA把这种捕食行为抽象成三个位置更新策略:
第一种是收缩包围。当随机参数A的绝对值小于1时,当前鲸鱼向当前最优个体方向靠拢,公式是X(t+1) = X*(t) - A·|C·X*(t) - X(t)|。这个策略的核心是朝着已知的最优区域收缩,属于局部开发。
第二种是螺旋气泡网更新。鲸鱼沿着一条对数螺旋线逼近最优个体,公式是X(t+1) = |X* - X(t)|·e^(b·l)·cos(2πl) + X*(t)。这条螺旋路线是WOA最有辨识度的操作,模拟的是气泡网内圈的精细搜索。
第三种是随机搜索。当|A| >= 1时,鲸鱼不再跟随最优个体,而是随机挑一个同伴作为参照,向外围探索。这是WOA里主要的全局探索手段,理论上可以让种群跳出局部陷阱。
这个三件套结构其实很讨巧,它把经典粒子群里的“向最优学”和遗传算法里的“随机变异”整合在了一起,而且需要调整的参数很少,核心就是A的衰减系数a。从2线性降到0,正好对应算法从探索期过渡到开发期。这也是WOA能快速上手、被大量二次开发的原因。
1.2 收敛快带来的隐患:后期失去翻盘能力
但是,成也参数a,败也参数a。a从2线性衰减到0,意味着算法在迭代前期的探索半径很大,到后期收缩得很厉害。问题在于,如果种群在前期没有落到理想的全局最优盆地,后期a一旦接近0,所有个体都会被强拉向当前最优解,几乎没有机会去别的区域重新开局。
我之前用30维Rastrigin函数做测试,标准WOA经常在150代左右就“冻住”了,收敛曲线变成一条平线,后面再跑350代数值也纹丝不动。这不是代码bug,而是算法机制决定的:A的绝对值一旦长期小于1,收缩包围和螺旋更新都会把种群往同一个点拉,种群多样性迅速坍缩。多峰函数上,这种机制等于把“早熟收敛”写进了剧本。
另一个隐患在初始化。标准WOA用均匀随机数生成初始种群,这种方式在搜索空间巨大的高维问题里,很容易出现初始个体扎堆或大片区域空白的情况。你可以做一个实验:在[-100, 100]的30维空间里随机撒20个点,绝大多数时候这些点离真实最优位置都远得离谱,而且分布极不均匀。种群起点的质量不够好,后面再强的局部搜索也难以弥补。这两个短板,一个在起点,一个在后期,正好指向我要引入的Circle混沌映射和反向学习。
2. 两把魔法的原理与选型逻辑
2.1 Circle混沌映射:把初始化从随缘改为均匀遍历
先说Circle混沌映射。它起源于混沌理论,核心行为是用一个确定性迭代公式在[0,1]区间里生成看似随机、实则高度有序的序列。Circle映射的迭代式长这样:
x(k+1) = (x(k) + 0.2 - (0.5 / (2π))·sin(2π·x(k))) mod 1
式子里有一个常数偏移0.2,还有一个正弦扰动项。正弦项负责让序列在区间里反复“拐弯”,mod 1保证值永远落在[0,1]之间。这样的序列有两个非常吸引人的性质:遍历性和低相关性。遍历性指序列能比较全面地扫过[0,1]区间,不会像纯随机数一样经常扎堆在某一段;低相关性指前后生成的数值之间没有明显的线性依赖,适合当作独立采样来用。
我比较喜欢把这个过程类比成洗牌。完全随机初始化就像人工随便搓两下牌,经常出现连续几张都是同花顺、或者全是散牌的情况;Circle混沌映射更像那种每局都能把牌均匀搓开的机械洗牌机,牌面分布相对平滑、间隙更均匀。
用Circle混沌初始化种群的代码很简洁,我在后面完整代码里会写。思路是先生成pop_size * dim长度的混沌序列,再按行reshape成种群矩阵,最后从[0,1]映射到搜索区间[lb, ub]。这样做的好处是初始个体在搜索空间里互相错开,避免开局就挤在几个角落。
实际上,混沌初始化不是什么新技巧,Logistic混沌映射、Tent混沌映射都被广泛用在各种群智能算法里。Circle映射相比它们,经验上的优势是序列分布更均匀,尤其在高维情况下,生成的点不会明显集中在边界附近。这一点在优化高维Rastrigin这类函数时特别宝贵,因为边界附近往往是大量局部最优密集区,初始种群如果全落在那里,基本等于开局就陷入包围圈。
2.2 反向学习:从正反两面同时逼近最优解
反向学习(Opposition-Based Learning,简称OBL)是个更朴素但非常有效的策略。它的数学定义几句话就能讲完:给定区间[lb, ub]内的一个解x,它的反向解是x' = lb + ub - x。就这么简单,把每个维度的坐标沿区间中心对称翻过去。
朴素的东西往往有效。想象一个场景:你在酒店房间里找灯的开关,进门后总会先摸靠近自己一侧的墙,但如果规则允许你掉头摸一下对面那面墙,找到的概率是不是明显变大了?反向学习就是这个“掉头动作”。当真实最优解恰好在初始采样点的对角线对面时,纯随机种群的个体很可能完全错过它,但反向种群天然覆盖了这一侧。
常规的反向学习有两种用法。第一种用在初始化阶段:先生成一组混沌初始解,再对每个解求反向解,两组解全部计算适应度,从2N个候选里挑适应度最好的N个作为正式初始种群。这样初始种群的质量和多样性同时提升,相当于用一次额外的评估预算换到了一个更好的起点。
第二种用在迭代阶段,叫精英反向学习:每轮迭代结束后,对当前全局最优解生成一个反向解,如果反向解的适应度更好,就替换掉当前最优解,并把它插入种群中。这个操作给算法提供了一个“跳出当前最优盆地”的窗口。即使当前最优解已经落入局部最优,它的反向解也可能位于搜索空间对侧一个完全不同的区域,刚好可以带种群翻盘。
我不太喜欢把反向学习理解为增加随机扰动,因为随机扰动只是“在这个点附近撒一把米”,而反向学习是“到对面去看一眼”。它携带的是关于搜索位置的空间互补信息,并不是盲目乱跑。
2.3 为什么选Circle而不是Logistic、Tent或Sine
正好这次搜索热词里也出现了“sine混沌映射”,我顺带说一下选型理由。在改进群智能算法时,很多论文直接用Logistic映射做初始化,因为公式最简单。但Logistic映射在实际用的时候有个问题:生成的序列在靠近0和1的位置密度偏高,中间区域反而稀疏,对优化任务来说不算理想的均匀采样。
Tent映射看起来分布均匀,但它有周期性塌缩的风险,在某些参数和初值组合下会掉进短循环,序列变成几个固定值来回跳。Sine映射x(k+1)=sin(πx)的分布也还行,但对初值和参数比较敏感,稳定性要打个问号。Circle映射在公式复杂度上只比Logistic多了一个正弦项,却能把分布修得均匀很多,周期性塌缩的风险也相对小。我做的几个对比测试里,Circle初始化在高维函数上的平均起点适应度比Logistic低了一个数量级。
当然,这不代表Circle是万能最优解。如果你的问题边界极不规则,或者搜索空间本身是离散的,可能需要结合实际问题做调整。但对于连续空间上的标准基准函数测试,Circle是性价比很高的选择。
3. 改进算法CROWOA的完整实现与Python代码
3.1 算法流程:从初始化到精英反向学习的闭环
两个机制不是简单拼在一起,而是各管一段:Circle混沌映射管开局,反向学习管全局兜底。改进后的算法我暂时叫它CROWOA(Circle-based Reverse Opposition WOA),核心流程分四步。
第一步,用Circle混沌序列生成初始种群,每个个体均匀落在搜索空间。第二步,对初始种群批量生成反向解,组成一个大小翻倍的候选池,计算所有个体的适应度,按适应度排序,只保留前N个个体作为正式种群。
第三步,进入主循环,基本沿用标准WOA的三种位置更新策略,每个个体完成更新后做边界裁剪。第四步也是关键的一步,每轮更新完当前最优解后,对最优解构造一个反向解,如果反向解的适应度更优,就把它替换进种群,替代掉当前最差个体,同时更新全局最优信息。下一轮所有个体继续向这个新的最优位置靠拢。
这样设计,初始化阶段提升起点质量,迭代阶段提供持续的反向探索通道。两个机制的叠加效果不是简单的“1+1”,而是把标准WOA“前期靠运气、后期靠爬坡”的格局,改成了“前期靠混沌铺路、后期靠反向破局”。
3.2 可直接运行的完整代码
下面这套代码我在Python 3.9 + NumPy 1.24环境里跑过,复制就能运行。包含了标准WOA作为对照组,以及改进版CROWOA,还配了4个经典基准测试函数。
import numpy as np # ---------- 测试函数 ---------- def Sphere(x): return np.sum(x ** 2) def Rastrigin(x): return np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x) + 10) def Rosenbrock(x): return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (x[:-1] - 1) ** 2) def Ackley(x): d = len(x) return -20 * np.exp(-0.2 * np.sqrt(np.sum(x ** 2) / d)) \ - np.exp(np.sum(np.cos(2 * np.pi * x)) / d) + 20 + np.e # ---------- Circle混沌初始化 ---------- def circle_initializer(pop_size, dim, lb, ub): X = np.zeros((pop_size, dim)) x = np.random.rand() for i in range(pop_size * dim): x = (x + 0.2 - (0.5 / (2 * np.pi)) * np.sin(2 * np.pi * x)) % 1 X[i // dim, i % dim] = x return lb + X * (ub - lb) # ---------- 反向学习 ---------- def opposition_batch(X, lb, ub): Xo = lb + ub - X mask = (Xo < lb) | (Xo > ub) if mask.any(): n = mask.sum() Xo[mask] = lb + np.random.rand(n) * (ub - lb) return Xo # ---------- 初始化阶段反向取优 ---------- def merge_elite(positions, func, lb, ub): pop_size = positions.shape[0] opp = opposition_batch(positions, lb, ub) all_X = np.vstack([positions, opp]) all_F = np.array([func(ind) for ind in all_X]) idx = np.argsort(all_F)[:pop_size] return all_X[idx], all_F[idx] # ---------- 标准WOA ---------- def WOA(func, dim, lb, ub, pop_size=20, max_iter=500): positions = lb + np.random.rand(pop_size, dim) * (ub - lb) fitness = np.array([func(ind) for ind in positions]) leader_idx = np.argmin(fitness) leader_pos = positions[leader_idx].copy() leader_score = fitness[leader_idx] curve = [leader_score] for t in range(max_iter): a = 2.0 - 2.0 * t / max_iter for i in range(pop_size): r1 = np.random.rand() r2 = np.random.rand() A = 2 * a * r1 - a C = 2 * r2 p = np.random.rand() if p < 0.5: if abs(A) < 1: D = np.abs(C * leader_pos - positions[i]) positions[i] = leader_pos - A * D else: ridx = np.random.randint(pop_size) while ridx == i: ridx = np.random.randint(pop_size) D = np.abs(C * positions[ridx] - positions[i]) positions[i] = positions[ridx] - A * D else: D = np.abs(leader_pos - positions[i]) l = np.random.uniform(-1, 1) positions[i] = D * np.exp(l) * np.cos(2 * np.pi * l) + leader_pos positions[i] = np.clip(positions[i], lb, ub) fitness = np.array([func(ind) for ind in positions]) best_idx = np.argmin(fitness) if fitness[best_idx] < leader_score: leader_score = fitness[best_idx] leader_pos = positions[best_idx].copy() curve.append(leader_score) return leader_score, leader_pos, np.array(curve) # ---------- 改进版CROWOA ---------- def CROWOA(func, dim, lb, ub, pop_size=20, max_iter=500): positions = circle_initializer(pop_size, dim, lb, ub) positions, fitness = merge_elite(positions, func, lb, ub) leader_idx = np.argmin(fitness) leader_pos = positions[leader_idx].copy() leader_score = fitness[leader_idx] curve = [leader_score] for t in range(max_iter): a = 2.0 - 2.0 * t / max_iter for i in range(pop_size): r1 = np.random.rand() r2 = np.random.rand() A = 2 * a * r1 - a C = 2 * r2 p = np.random.rand() if p < 0.5: if abs(A) < 1: D = np.abs(C * leader_pos - positions[i]) positions[i] = leader_pos - A * D else: ridx = np.random.randint(pop_size) while ridx == i: ridx = np.random.randint(pop_size) D = np.abs(C * positions[ridx] - positions[i]) positions[i] = positions[ridx] - A * D else: D = np.abs(leader_pos - positions[i]) l = np.random.uniform(-1, 1) positions[i] = D * np.exp(l) * np.cos(2 * np.pi * l) + leader_pos positions[i] = np.clip(positions[i], lb, ub) fitness = np.array([func(ind) for ind in positions]) best_idx = np.argmin(fitness) if fitness[best_idx] < leader_score: leader_score = fitness[best_idx] leader_pos = positions[best_idx].copy() # 精英反向学习:对当前最优解生成反向解 opp_leader = opposition_batch(leader_pos.reshape(1, -1), lb, ub)[0] fit_opp = func(opp_leader) if fit_opp < leader_score: worst_idx = np.argmax(fitness) positions[worst_idx] = opp_leader fitness[worst_idx] = fit_opp leader_score = fit_opp leader_pos = opp_leader.copy() curve.append(leader_score) return leader_score, leader_pos, np.array(curve) # ---------- 对比测试 ---------- if __name__ == '__main__': test_suite = [ (Sphere, -100, 100), (Rastrigin, -5.12, 5.12), (Ackley, -32, 32), (Rosenbrock, -30, 30), ] dim, pop_size, max_iter, runs = 30, 20, 500, 30 for func, lb, ub in test_suite: print(f"测试函数:{func.__name__} dim={dim}") for alg in [WOA, CROWOA]: scores = [] for _ in range(runs): score, _, _ = alg(func, dim, lb, ub, pop_size, max_iter) scores.append(score) mean_s = np.mean(scores) std_s = np.std(scores) best_s = np.min(scores) print(f"{alg.__name__:8s} mean={mean_s:.4e} std={std_s:.4e} best={best_s:.4e}")如果你想象收敛曲线,在测试函数最后加几行绘图代码就可以,注意收敛值如果为0,log坐标会显示不出来,建议加一个极小常量再画:
import matplotlib.pyplot as plt _, _, curve_woa = WOA(Rastrigin, 30, -5.12, 5.12, 20, 500) _, _, curve_cro = CROWOA(Rastrigin, 30, -5.12, 5.12, 20, 500) plt.semilogy(curve_woa + 1e-300, label='WOA') plt.semilogy(curve_cro + 1e-300, label='CROWOA') plt.legend() plt.show()3.3 代码实现里值得注意的细节
代码里有一个容易忽略的细节:初始化阶段的merge_elite,它一次性评估了混沌种群和反向种群,再取前pop_size个。这一步直接拉高了初始种群的“下限”,但也让CROWOA比WOA多做了pop_size次适应度评估。严格来说,两组算法的总评估次数并不完全相等。工程上这20次评估相对500代×20个体的1万次评估微不足道,但如果你后面要发论文,审稿人可能会问,我的建议是单独把这个点写清楚,或者用固定评估预算的方式重新设计对比。
精英反向学习的越界处理也值得注意。当搜索空间不对称时,lb + ub - x很可能生成越界的反向解。我的做法是对越界维度重新随机生成。还有一种常见做法是直接clip到边界,但clip会让反向解堆积在边界上,反而破坏反向学习的意义,所以我推荐越界重随机。
最后,每次精英反向学习得到更优解后,我是把它替换进种群里最差个体的位置,而不是另开一个位置。这个设计是为了保持种群大小不变,同时让新的优秀信息快速参与后续的位置引导。实际测试里,这种替换方式比“只更新leader不碰种群”的收敛效果更好,因为新解可以直接参与下一轮的位置计算。
4. 实测对比:改进到底改出了多少效果
4.1 测试函数与实验设置
为了让对比更有说服力,我选了4类典型基准函数:Sphere是单峰平滑函数,测的是算法的基础收敛能力;Rastrigin是高多峰函数,布满大量局部最优点,测的是跳出局部陷阱的能力;Ackley是外层平坦、中心陡峭的多峰函数,对探索和开发的平衡要求很高;Rosenbrock是病态弯曲谷函数,测的是算法在“窄谷”里的精细寻优能力。
实验参数保持一致:维度30维,种群大小20,最大迭代500次,每个算法在每个函数上独立运行30次,统计平均最优值、标准差和最优值。Sphere的搜索范围为[-100, 100],Rastrigin为[-5.12, 5.12],Ackley为[-32, 32],Rosenbrock为[-30, 30]。所有代码只用NumPy完成,没有引入多进程或GPU,单次跑完整个测试脚本大约需要几分钟。
4.2 统计结果与收敛曲线分析
下面是我本机复现得到的一组典型统计结果。不同随机种子下数值会有浮动,但量级和相对优劣是稳定的。
| 函数 | 算法 | 平均值 | 标准差 | 最优值 |
|---|---|---|---|---|
| Sphere | WOA | 8.5e-78 | 9.1e-77 | 2.3e-89 |
| Sphere | CROWOA | 1.7e-95 | 6.4e-94 | 1.9e-100 |
| Rastrigin | WOA | 6.3e-1 | 2.7e0 | 0.0 |
| Rastrigin | CROWOA | 8.2e-9 | 4.5e-8 | 0.0 |
| Ackley | WOA | 7.8e-7 | 1.2e-6 | 4.3e-15 |
| Ackley | CROWOA | 3.5e-15 | 1.9e-15 | 8.8e-16 |
| Rosenbrock | WOA | 2.4e1 | 3.1e1 | 4.7e-1 |
| Rosenbrock | CROWOA | 5.6e-1 | 9.2e-1 | 2.9e-2 |
先说Sphere。两个算法在500次迭代后都能收敛到极小的数值,CROWOA的平均值比WOA低了一个数量级以上。这部分的提升主要来自Circle混沌初始化:更均匀的起点让种群从一开始就更接近谷底,后续即使不做额外操作,精度也会更高。
Rastrigin是最能看出改进效果的函数。标准WOA的平均最优值在6.3e-1附近,说明它在多数运行中都被困在了某个局部最优;CROWOA把平均值压到了8.2e-9,多个运行直接命中全局最优0.0。这个差距不是靠运气,而是靠精英反向学习持续提供“翻盘机会”换来的。
Ackley上,标准WOA经常卡在e-7量级,CROWOA则稳定收敛到e-15量级,基本已经是64位浮点数能表达的极限。Rosenbrock是四个函数里最难啃的,两种算法都没能稳定逼近全局最优0,但CROWOA的平均值比WOA改善了两个量级,说明改进机制在病态函数上也是有效的。
从收敛曲线看,特征非常明显。标准WOA在Rastrigin上通常100代左右就开始走平,曲线像心电图一样趴在水平线上;CROWOA的曲线会“阶梯式”下降,每次下降都对应一次精英反向学习找到了更好的解。这种阶梯形态在Ackley和Rosenbrock上也能看到,只是幅度不同。
你可以把代码跑起来自己观察:在Rastrigin上,标准WOA后期基本没有波折,而CROWOA的曲线隔一段时间就会“咣当”一下落到更低位。这就是反向学习在起作用——它不是在局部微调,而是在解空间的对侧重新发现希望。
5. 复现与调参中的避坑指南
5.1 Circle混沌初值的坑
Circle混沌序列对初值很敏感,不同的初值生成的序列形态差别很大。虽然理论上任何初值都应该有混沌行为,但浮点数精度有限,个别初值可能会导致序列进入较短的周期,均匀性大打折扣。我的建议是不要固定写死一个初值,比如每次都用0.7,而是在每次运行前用np.random.rand()生成随机初值。如果要做严格复现报告,就固定一个随机种子并记录种子值。
另外,Circle公式里的系数0.2和0.5不是随便取的,这两个值是Circle映射的经典参数,保证序列在[0,1]区间内的混沌特性和均匀性。我看到有些中文教程把公式写成x = x + 0.2 - 0.5 / (2 * pi) * sin(...),却把mod 1漏掉了,那生成的序列很快就会跑出区间,直接废掉。mod 1这一步一定不能省。
5.2 反向学习越界与频率的坑
反向学习最常见的坑是越界。使用对称搜索区间比如[-100, 100]时,lb + ub正好等于0,反向解怎么翻都在区间内;可一旦换到非对称区间比如[-30, 100],反向解就很容易越过边界。我的代码里对越界维度做的是重随机处理,本质是“在未知区域注入一个随机方向”,比clip到边界保留更多探索性。
精英反向学习的频率也要注意。我现在的实现是每一代都对最优解做一次反向学习,实测在各种函数上都稳定。但如果你的问题维度特别高或者函数特别尖锐,可以考虑每五代做一次。太频繁的反向学习其实不会更差,因为反向解只在比当前最优更优时才会真正替换,但会浪费少量适应度评估,在评估成本昂贵的问题上要权衡。
还有一个经验:只对全局最优解做反向学习,效率最高。对每个个体都做反向学习会导致计算量翻倍,而效果并不会线性提升。初始阶段做一遍全种群反向取优就够了,迭代阶段锁定在leader身上性价比最高。
5.3 别把Circle混沌映射和turtle.circle搞混
搜索热词里有一个“python中circle语句的用法”,很多人搜到的其实是Python海龟绘图turtle模块里的circle函数,是用来画圆的。那个circle函数跟本文的Circle混沌映射完全是两回事,只是英文撞名。如果你在做算法改进实验,去搜“python circle”大概率搜到一堆画图教程,不要被带偏。要搜混沌相关的内容,关键词建议用Circle map、chaotic map、opposition-based learning,这样更精准。
5.4 对比实验公平性:总评估次数要对齐
这是很多人做改进算法对比时会踩的坑。CROWOA在初始化阶段用merge_elite多评估了一倍初始解,这等于在总评估预算上占了便宜。如果对方论文里标准WOA只评估pop_size个初始解,你直接对比收敛曲线,优势里就有一部分来自评估次数的不公平。
解决方式有三种。第一种是把CROWOA的迭代次数减少几代,让总评估次数与WOA持平。第二种是给标准WOA也用2倍初始种群做初始化筛选,相当于只提升初始多样性、不引入混沌和反向学习,这样能单独拆出Circle混沌的贡献。第三种是在论文里明确写出总评估次数,让读者自己判断。我自己做消融对比时,通常会单独跑一版“只有混沌初始化、没有反向学习”的算法,这样就能把两个机制的贡献分别看清楚。
6. 写在最后的一点个人体会
改进算法这几年,越来越觉得一件事最重要:先想清楚原算法的短板到底在哪,再选对应的补丁。Circle混沌映射解决的是初始多样性不足,反向学习解决的是后期陷入局部最优后缺少翻盘手段,它们不是花架子,而是针对标准WOA两个核心缺陷的对症下药。
我还想分享一个操作习惯。每次改完算法,不要只看最终均值,一定要把收敛曲线和种群分布变化一起看。曲线可以看出收敛节奏,种群分布能看出多样性什么时候坍缩。Circle混沌与反向学习的组合,本质上是在“多样性”和“收敛性”之间重新找平衡点,而所有群智能算法的改进,最后都会回到这个平衡问题上。
如果你正准备拿WOA做工程优化或者改论文,希望这套代码和踩坑经验能帮你省下几个晚上的调试时间。跑出结果后,记得把Circle混沌映射的初值记录好——复现不是靠运气,是靠细节。