Circle混沌映射与反向学习改进鲸鱼算法:原理、Python实现与对比

📅 发布时间:2026/9/29 15:52:57
Circle混沌映射与反向学习改进鲸鱼算法:原理、Python实现与对比
做群体智能优化这几年我自己复现过的算法少说也有七八种鲸鱼算法WOA是其中之一也是我反复回头看的一个。它的机制简单、参数不多但有个老毛病收敛速度快是快可一到高维多峰函数上很容易一头扎进局部最优就出不来。后来我在做改进实验时把Circle混沌映射和反向学习两种机制揉进去局面一下子变了。这篇文章就是完整记录这次改进的思路、完整Python实现和实测对比适合准备做算法改进、论文复现或是想在工程里换一个更稳优化器的朋友参考。想搞清楚为什么要加这两把“魔法”得先明白标准WOA在寻优时到底在干什么以及它的短板出在哪。1. 先看标准鲸鱼算法的短板在哪里1.1 WOA的三种核心位置更新机制鲸鱼优化算法是Mirjalili在2016年提出来的它模仿的是座头鲸用气泡网捕猎的方式。座头鲸发现猎物后会从深水螺旋上升一边吐气泡一边把磷虾、小鱼群逼到水面最后一口吞掉。WOA把这种捕食行为抽象成三个位置更新策略第一种是收缩包围。当随机参数A的绝对值小于1时当前鲸鱼向当前最优个体方向靠拢公式是X(t1) X*(t) - A·|C·X*(t) - X(t)|。这个策略的核心是朝着已知的最优区域收缩属于局部开发。第二种是螺旋气泡网更新。鲸鱼沿着一条对数螺旋线逼近最优个体公式是X(t1) |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(k1) (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(k1)sin(πx)的分布也还行但对初值和参数比较敏感稳定性要打个问号。Circle映射在公式复杂度上只比Logistic多了一个正弦项却能把分布修得均匀很多周期性塌缩的风险也相对小。我做的几个对比测试里Circle初始化在高维函数上的平均起点适应度比Logistic低了一个数量级。当然这不代表Circle是万能最优解。如果你的问题边界极不规则或者搜索空间本身是离散的可能需要结合实际问题做调整。但对于连续空间上的标准基准函数测试Circle是性价比很高的选择。3. 改进算法CROWOA的完整实现与Python代码3.1 算法流程从初始化到精英反向学习的闭环两个机制不是简单拼在一起而是各管一段Circle混沌映射管开局反向学习管全局兜底。改进后的算法我暂时叫它CROWOACircle-based Reverse Opposition WOA核心流程分四步。第一步用Circle混沌序列生成初始种群每个个体均匀落在搜索空间。第二步对初始种群批量生成反向解组成一个大小翻倍的候选池计算所有个体的适应度按适应度排序只保留前N个个体作为正式种群。第三步进入主循环基本沿用标准WOA的三种位置更新策略每个个体完成更新后做边界裁剪。第四步也是关键的一步每轮更新完当前最优解后对最优解构造一个反向解如果反向解的适应度更优就把它替换进种群替代掉当前最差个体同时更新全局最优信息。下一轮所有个体继续向这个新的最优位置靠拢。这样设计初始化阶段提升起点质量迭代阶段提供持续的反向探索通道。两个机制的叠加效果不是简单的“11”而是把标准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_size20, max_iter500): 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_size20, max_iter500): 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})如果你想象收敛曲线在测试函数最后加几行绘图代码就可以注意收敛值如果为0log坐标会显示不出来建议加一个极小常量再画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, labelWOA) plt.semilogy(curve_cro 1e-300, labelCROWOA) 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 统计结果与收敛曲线分析下面是我本机复现得到的一组典型统计结果。不同随机种子下数值会有浮动但量级和相对优劣是稳定的。函数算法平均值标准差最优值SphereWOA8.5e-789.1e-772.3e-89SphereCROWOA1.7e-956.4e-941.9e-100RastriginWOA6.3e-12.7e00.0RastriginCROWOA8.2e-94.5e-80.0AckleyWOA7.8e-71.2e-64.3e-15AckleyCROWOA3.5e-151.9e-158.8e-16RosenbrockWOA2.4e13.1e14.7e-1RosenbrockCROWOA5.6e-19.2e-12.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混沌映射的初值记录好——复现不是靠运气是靠细节。