复现一篇蝴蝶优化算法(BOA)的改进论文,标题写着“Circle混沌初始化种群+非线性因子w、p、r+融合正余弦算法”,原以为照着摘要把公式抄下来就能跑出曲线,结果断断续续折腾了一个多星期才真正把整套流程走通。这类组合型改进在智能优化领域非常典型:基础算法优化能力不足,就用混沌映射提升初始种群质量,用动态参数调节探索与开发节奏,再引入正余弦算法(SCA)增强局部搜索能力。单拎出来每一步都不难,难的是让它们协同工作。这篇文章把我在复现过程中的拆解思路、数学推导补充、关键代码和踩坑记录都整理出来,给正在做BOA改进或类似元启发式算法复现的朋友一个参考。
1. 复现前必须拆清楚:这篇BOA改进论文到底改了什么
拿到一个标题就急着写代码,是复现工作中最影响效率的做法。标题里几个词看起来各自独立,实际上只有先把原始BOA的机制、三个改进点针对的缺陷、以及它们之间的联动关系摸清楚,后面写代码才不会返工。
1.1 原始BOA的三段式机制
蝴蝶优化算法是Arora和Singh在2019年提出的元启发式算法,核心思想是模拟蝴蝶觅食和求偶时对花蜜香味的感知。算法里每只蝴蝶是一个解,每个解对应的适应度值被当作“刺激强度”I,经过转换后成为该蝴蝶散发的“香味强度”f:
f = c * I^a
其中c是感觉因子,a是模态指数。习惯上a取0.1,c在迭代过程中从0.01逐渐增长到0.25。之所以让c递增,是因为算法后期需要更大的香味吸引力来加快收敛。
香味强度算出来后,算法通过一个切换概率p决定当前蝴蝶执行哪个搜索分支:
- 全局搜索(探索阶段):x_i^(t+1) = x_i^t + (r^2 * g* - x_i^t) * f_i
- 局部搜索(开发阶段):x_i^(t+1) = x_i^t + (r^2 * x_j^t - x_k^t) * f_i
其中g*是当前全局最优解,x_j和x_k是从种群中随机挑出的两个不同个体,r是[0,1]均匀随机数。p在原始论文中固定为0.8,也就是说每轮迭代中约80%的蝴蝶走全局搜索,20%走局部搜索。
从这套机制能看出原始BOA的设计逻辑:蝴蝶通过香味感知全局最优位置,并朝它靠拢,这保证了全局搜索能力;而随机挑选两个个体做线性组合,则提供了一种简单的邻域扰动。但缺陷也很明显,后面三个改进点几乎都是冲着这些缺陷去的。
1.2 论文的三个改进方向分别补什么
第一,原始BOA的种群是纯随机初始化生成,在解空间里分布不均匀,容易导致初始种群离真实最优区域太远。标题里的“Circle混沌初始化种群”就是针对这一点,用Circle混沌序列生成初始位置,让种群在搜索空间里铺得更均匀。
第二,原始BOA的p固定为0.8、c固定增长、r使用均匀随机数,都属于“静态”或“半静态”参数。标题里的“非线性因子w、p、r”是把三个关键参数都改成随迭代次数非线性变化的动态值,让算法前期重探索、后期重开发。
第三,原始BOA的局部搜索结构过于简单,只是两个随机解的差作为扰动方向,精度有限。标题里的“融合正余弦算法”就是引入SCA的正弦余弦位置更新机制,增强局部阶段的开采能力。
1.3 编码前必须确认的四个问题
在动手写代码之前,建议先把下面四个问题落实清楚,否则后续很容易推倒重来:
- 这篇论文用的测试函数是什么。CEC2017、CEC2019还是一般的Sphere、Rastrigin、Ackley,决定了函数实现方式和结果统计口径。
- 三个改进点各自的精确公式。特别是w和r,不同论文有不同定义,有的是惯性权重,有的是随机系数方差,要对照原文确认,不要凭标题猜测。
- 改进点之间是否有参数联动。比如p下降后,进入局部搜索分支的概率变小,此时SCA的混合比例是否也要跟着调整,必须在写主循环前设计好。
- 边界处理方式。混沌映射出来的值是否会被clip截断,SCA的绝对值项是否会导致越界,这些细节直接影响收敛行为。
这四个问题都没有标准答案,但提前想清楚能让复现过程更有针对性。论文复现不是抄答案,而是把作者的思路转成可运行的代码,过程中必然要补充原作者没有写明的细节。
2. Circle混沌映射初始化:公式与实现的数学细节
混沌初始化在群智能优化里很常见,但很多人只是随便套个公式,没有理解映射的数学性质和实现时的参数选择问题,导致初始化效果甚至不如随机生成。
2.1 Circle映射公式与推荐参数
Circle映射的迭代公式如下:
x_(n+1) = mod(x_n + b - (a / 2π) * sin(2π * x_n), 1)
其中a和b是控制参数。复现中最常用的组合是a=0.5、b=0.2,这个组合下序列在[0,1]区间内具有较好的遍历性和低相关性。mod运算的作用是把结果拉回[0,1]区间,保证混沌序列不发散。
为什么选Circle而不是更常见的Logistic映射?因为Logistic映射在参数接近4时虽然混沌,但序列在0和1附近分布过密,种群初始化后容易在边界堆积。Circle映射的均匀性相对更好,尤其在维度较高时,边界堆积造成的负面影响会更小。
下面用一个表对比几种常用于初始化的混沌映射:
| 映射名称 | 迭代公式 | 均匀性 | 实现复杂度 | 常用参数 |
|---|---|---|---|---|
| Logistic | x_(n+1)=μx(1-x) | 中等,边界偏密 | 最低 | μ=4 |
| Tent | x_(n+1)=min(x,1-x)*μ | 较好,但易落入不动点 | 低 | μ=2 |
| Circle | x_(n+1)=mod(x+b-(a/2π)sin(2πx),1) | 较好,序列均匀 | 低 | a=0.5, b=0.2 |
| Chebyshev | x_(n+1)=cos(k*arccos(x)) | 中等,波动大 | 中 | k∈[2,6] |
2.2 从一维混沌序列到多维种群
Circle映射生成的是[0,1]区间的一维序列,但种群初始化需要的是一个pop_size×dim的矩阵。这里有两种映射方式:
- 方式一:对每个维度独立生成一条混沌序列,分别映射到搜索空间;
- 方式二:只生成一条长序列,然后按顺序切成多段填充给每个维度。
实际复现时更推荐方式一,因为方式二会让不同维度之间保持序列的连续性,导致维度间相关性过强,种群个体在搜索空间中对角线方向聚集。每个维度用不同的随机初值独立迭代,得到的多维分布更接近均匀。
实现代码如下:
import numpy as np def circle_chaos_sequence(x0, length, a=0.5, b=0.2, burn=500): seq = np.zeros(length) x = x0 # 先迭代 burn 次,跳过混沌映射的瞬态阶段 for _ in range(burn): x = (x + b - a / (2*np.pi) * np.sin(2*np.pi*x)) % 1 for i in range(length): x = (x + b - a / (2*np.pi) * np.sin(2*np.pi*x)) % 1 seq[i] = x return seq def circle_initialization(pop_size, dim, lb, ub, a=0.5, b=0.2): X = np.zeros((pop_size, dim)) for d in range(dim): x0 = np.random.uniform(0, 1) seq = circle_chaos_sequence(x0, pop_size, a, b) X[:, d] = lb + (ub - lb) * seq return X上面代码里有一个细节:生成序列之前先迭代500次。因为混沌映射的初始段受初值影响较大,分布不够稳定,跳过这段瞬态能让进入种群的序列更均匀。这个操作在多数论文里不会写,却是保证复现效果的重要一步。
2.3 如何验证初始化真的变好了
不要只凭经验说“混沌初始化有效果”,要动手验证。一个常用的方法是计算初始种群的多样性度量,比如平均成对欧氏距离或种群方差。以Sphere函数在[-100,100]^30空间、种群大小30为例,随机初始化的种群适应度分布通常集中在1e4到1e5量级,而Circle初始化后的种群适应度分布会更均匀地铺开,最差个体的适应度往往会低一到两个数量级。
另一个更直接的判断方式是看初始全局最优的适应度。多次独立运行后统计平均初始最优值,如果Circle初始化的平均初始最优值明显优于随机初始化,说明种子里确实有更接近最优区域的个体。后面正式跑收敛曲线时,起点更低会让整个收敛曲线的外观更好看。
3. 非线性因子w、p、r:从固定值到动态设计的复现逻辑
这部分是复现工作的重头戏。w、p、r三个字母在标题里并列出现,但作用对象完全不同。很多复现稿把三个参数都简单改成随迭代递减,结果算法后半段几乎停滞,这就是没搞清楚每个参数在BOA搜索方程里的角色。
3.1 固定参数到底限制了什么
先看原始BOA里p固定为0.8的问题:无论迭代早期还是后期,都有大量蝴蝶执行全局搜索。理想情况下,算法前期应该以探索为主,通过大范围搜索捕捉全局信息;后期应该转向开发,对已有最优区域做精细搜索。p固定后,后期仍然有大量蝴蝶跑去做全局大跳,局部精化能力自然不够。
再看感觉因子c。原始BOA中c从0.01线性增到0.25,本质上是一种手动设定的动态策略。但c的作用并非简单越大越好,因为它直接乘在香味强度上,c过大会导致蝴蝶一步跳出几万个单位,越过边界;c过小又会吸附不动。另外随机数r的分布也会影响搜索步长的稳定性。
3.2 惯性权重w的设计选择
w在标题里的定位类似PSO中的惯性权重,直接作用在位置更新式中的当前项上,控制蝴蝶对自身位置的继承程度:
X_new = w * X_current + (r^2 * g* - X_current) * f
w越大,蝴蝶越倾向于保留原位置附近,搜索越保守;w越小,当前位置的惯性越弱,蝴蝶越容易被香味牵引到更远位置。复现中可以用抛物线递减:
w(t) = w_start - (w_start - w_end) * (t / T)^2
也可以使用余弦型衰减:
w(t) = w_end + (w_start - w_end) * cos(π * t / (2T))
两种公式曲线形态不同:抛物线递减在前期衰减慢、后期快,适合那些需要长时间保持探索的测试函数;余弦型衰减在早期下降略快,能更快进入开发阶段。通常取w_start=0.9、w_end=0.4,这两个值主要参考PSO经典参数组合。
3.3 切换概率p的非线性策略
p的改进应该是整篇论文里最容易出效果的修改点。原始p固定为0.8,我复现时把它改为从0.9线性递减到0.3:
p(t) = p_start - (p_start - p_end) * (t / T)
前期p接近0.9,大部分蝴蝶做全局搜索,保证探索范围;后期p降到0.3,大部分蝴蝶转入局部搜索,便于精细开发。这里要注意p的下限不要设置得太低,否则后期几乎全部做局部搜索,种群多样性会迅速耗尽,容易早熟。
也可以用正弦型策略,让p先保持较大值一段时间再下降:
p(t) = p_start - (p_start - p_end) * sin(π * t / (2T))
正弦型策略在前期下降更慢,对多峰函数的探索更友好;线性策略简单直接,对单峰函数收敛更快。具体用哪种,要跟论文原稿保持一致,如果论文没有明确写,建议在代码里做成可配置项,跑几次对比再确定。
3.4 随机因子r的非线性设计
原始BOA中r是[0,1]均匀随机数,它在全局搜索里承担的是随机缩放的职责。改进版通常把r改成一个随迭代非线性递减的控制参数:
r(t) = r_max - (r_max - r_min) * (t / T)^β
这里β控制衰减曲线形态,β=1是线性,β=2是前期快后期平缓的凹曲线,β=0.5则相反。复现习惯上取r_max=1.0、r_min=0.1、β=2。这样前期r较大,允许蝴蝶大步跨越探索;后期r较小,限制步长,只在最优解附近精细调整。
此时全局搜索更新公式变成:
X_new = w(t) * X_current + (r(t)^2 * g* - X_current) * f
把w、p、r三个动态参数放在一起看,w管的是惯性取舍,p管的是全局与局部搜索的分工比例,r管的是单步步长尺度。三者协同后,算法在迭代早期是一个“大步探索且偏全局”的状态,迭代晚期变成“小步开发且偏局部”的状态,这正是组合改进要达成的核心效果。
下面给出一个参数配置表,方便对照实现:
| 参数 | 原始BOA | 改进策略 | 推荐取值 |
|---|---|---|---|
| w | 无 | 抛物线递减 | 0.9 → 0.4 |
| p | 固定0.8 | 线性/正弦递减 | 0.9 → 0.3 |
| r | U(0,1)随机 | 幂函数递减 | 1.0 → 0.1, β=2 |
| c | 0.01→0.25线性 | 保持不变 | 0.01 → 0.25 |
| a | 0.1 | 保持不变 | 0.1 |
4. 正余弦算子与BOA的融合:从公式到混合策略选择
融合正余弦算法这一步,最容易踩的坑是把SCA当成一个独立的算法步骤直接拼在BOA后面。正确理解应该是:SCA提供了一种位置更新算子,你可以把这种算子嵌入到BOA的某个分支里,也可以让它以竞争候选的形式参与选择。
4.1 SCA的更新公式与参数含义
SCA的核心思想来自正弦和余弦函数的数学性质,每个个体按下式更新:
X^(t+1) = X^t + r1 * sin(r2) * |r3 * X_best - X^t| (r4 < 0.5) X^(t+1) = X^t + r1 * cos(r2) * |r3 * X_best - X^t| (r4 >= 0.5)
其中:
- r1 = a - t * a / T,控制搜索幅度,随迭代线性递减;
- r2 ∈ [0, 2π],控制移动方向;
- r3 ∈ [0, 2],随机权重,增强位置的随机性;
- r4 ∈ [0, 1],决定使用正弦还是余弦分支。
表面上看SCA和BOA都是朝最优解移动,但区别在于SCA用正弦/余弦的周期交替特性,可以让个体在靠近最优解时仍然产生振荡,从而跳出局部极值。这个特性与BOA的随机扰动机制有本质差异。
4.2 融合方式一:让SCA接管局部搜索分支
第一种实现方式很直接:当p条件不满足时,原本执行BOA局部搜索的蝴蝶改用SCA更新。这样全局分支保持BOA原有的探索机制,局部分支换成正余弦振荡,既能改善BOA原始局部搜索方向单一的缺陷,又不会过度改变算法主体结构。
if np.random.rand() < p_t: # 全局搜索分支,使用改进后的BOA更新 r_val = r_t X_new = w_t * X[i] + (r_val * r_val * Gbest - X[i]) * f_i else: # 局部搜索分支,使用SCA更新 r1 = a_sca - t * (a_sca / T) r2 = 2 * np.pi * np.random.rand() r3 = 2 * np.random.rand() r4 = np.random.rand() if r4 < 0.5: X_new = X[i] + r1 * np.sin(r2) * np.abs(r3 * Gbest - X[i]) else: X_new = X[i] + r1 * np.cos(r2) * np.abs(r3 * Gbest - X[i])这种方式实现简单、计算量小,但注意SCA更新完全围绕全局最优Gbest,如果Gbest本身陷入局部极值,局部搜索也会跟着在局部区域震荡,因此SCA的r1衰减速度要和BOA的动态参数保持节奏一致。
4.3 融合方式二:全局搜索阶段的双策略竞争
第二种方式更精细:在全局搜索分支中,同时计算BOA的改进更新结果和SCA的更新结果,然后比较两个候选解以及当前解的适应度,选最优者作为新位置。这种方式不直接替换原有分支,而是增加一个候选池,决策权交给适应度函数。
核心代码如下:
# 计算BOA全局更新候选 X_candidate_boa = w_t * X[i] + (r_t * r_t * Gbest - X[i]) * f_i f_candidate_boa = func(X_candidate_boa) # 计算SCA更新候选 r1 = a_sca - t * (a_sca / T) r2 = 2 * np.pi * np.random.rand() r3 = 2 * np.random.rand() r4 = np.random.rand() if r4 < 0.5: X_candidate_sca = X[i] + r1 * np.sin(r2) * np.abs(r3 * Gbest - X[i]) else: X_candidate_sca = X[i] + r1 * np.cos(r2) * np.abs(r3 * Gbest - X[i]) f_candidate_sca = func(X_candidate_sca) if f_candidate_boa <= f_candidate_sca and f_candidate_boa < fitness[i]: X[i] = X_candidate_boa fitness[i] = f_candidate_boa elif f_candidate_sca < f_candidate_boa and f_candidate_sca < fitness[i]: X[i] = X_candidate_sca fitness[i] = f_candidate_sca双策略竞争的优点是不会过早丢失其中一种策略的探索能力,理论上搜索能力更强;缺点是每轮每个个体要计算两次目标函数,计算量翻倍,在CEC2017这种高维复杂函数上运行时间会明显增加。
4.4 混合之后才需要关注的参数联动问题
融合SCA后最容易被忽视的是r1的衰减与w、r两个参数的联动。w和r已经被设计成随迭代递减,而SCA的r1也是线性递减,三者叠加会让后期的搜索步长过小,算法可能提前停止有效移动。解决方法是,在SCA分支中不再乘w,只保留r1控制步长;或者在全局分支做双策略竞争时,让SCA候选的r1使用单独的参数a_sca,而不是复用w。
我复现时的经验是:BOA全局分支负责大范围寻优,SCA分支负责局部振荡,两者的步长机制最好保持独立。最简单的做法是给SCA单独设置一个a值,例如a=2,并按原始SCA的线性递减公式计算r1,不与w_t混用。
5. 完整复现代码:从混沌初始化到迭代主循环
前面章节是理论拆解,这一章给出可以直接跑起来的完整代码框架。我以Python为例,测试函数先使用Sphere和Rastrigin,你可以在此基础上扩展CEC系列函数。
5.1 环境准备与测试函数实现
需要安装的库只有numpy和matplotlib,如果要跑Wilcoxon统计检验,还需要scipy。测试函数实现如下:
import numpy as np def sphere(x): return np.sum(x ** 2) def rastrigin(x): A = 10 return A * len(x) + np.sum(x ** 2 - A * np.cos(2 * np.pi * x)) def ackley(x): a, b, c = 20, 0.2, 2*np.pi d = len(x) sum1 = np.sum(x ** 2) sum2 = np.sum(np.cos(c * x)) return -a * np.exp(-b * np.sqrt(sum1 / d)) - np.exp(sum2 / d) + a + np.exp(1)目标是统一的函数输入一维数组、输出适应度标量,方便后续在主循环中批量计算。
5.2 改进BOA-SCA算法的完整实现
下面是结合前面所有改进点的主循环代码。要注意香味强度f的计算里有一个容易被忽略的归一化步骤,我在代码注释里标了出来。
def improved_boa_sca(func, dim, pop_size, max_iter, lb, ub): # 参数设置 w_start, w_end = 0.9, 0.4 p_start, p_end = 0.9, 0.3 r_max, r_min = 1.0, 0.1 beta_r = 2.0 c_start, c_end = 0.01, 0.25 epsilon = 1e-12 alpha = 0.1 # 模态指数 a_sca = 2.0 # SCA 控制参数 # Circle混沌初始化种群 X = circle_initialization(pop_size, dim, lb, ub) fitness = np.array([func(x) for x in X]) gbest_idx = np.argmin(fitness) Gbest = X[gbest_idx].copy() Gbest_fit = fitness[gbest_idx] history = [Gbest_fit] for t in range(max_iter): # 动态参数 w_t = w_start - (w_start - w_end) * (t / max_iter) ** 2 p_t = p_start - (p_start - p_end) * (t / max_iter) r_t = r_max - (r_max - r_min) * (t / max_iter) ** beta_r c_t = c_start + (c_end - c_start) * (t / max_iter) # 对当前适应度做归一化,避免香味强度数值爆炸 f_min = fitness.min() f_max = fitness.max() if f_max - f_min < epsilon: norm_fit = np.ones_like(fitness) else: norm_fit = (fitness - f_min) / (f_max - f_min) + epsilon for i in range(pop_size): # 香味强度 f_i = c_t * (norm_fit[i] ** alpha) if np.random.rand() < p_t: # 全局搜索分支:BOA更新 + SCA竞争候选 r = r_t * np.random.rand() X_boa = w_t * X[i] + (r * r * Gbest - X[i]) * f_i X_boa = np.clip(X_boa, lb, ub) f_boa = func(X_boa) r1 = a_sca - t * (a_sca / max_iter) r2 = 2 * np.pi * np.random.rand() r3 = 2 * np.random.rand() r4 = np.random.rand() if r4 < 0.5: X_sca = X[i] + r1 * np.sin(r2) * np.abs(r3 * Gbest - X[i]) else: X_sca = X[i] + r1 * np.cos(r2) * np.abs(r3 * Gbest - X[i]) X_sca = np.clip(X_sca, lb, ub) f_sca = func(X_sca) if f_boa <= f_sca and f_boa < fitness[i]: X[i] = X_boa fitness[i] = f_boa elif f_sca < f_boa and f_sca < fitness[i]: X[i] = X_sca fitness[i] = f_sca else: # 局部搜索分支:使用SCA振荡更新 r1 = a_sca - t * (a_sca / max_iter) r2 = 2 * np.pi * np.random.rand() r3 = 2 * np.random.rand() r4 = np.random.rand() if r4 < 0.5: X_new = X[i] + r1 * np.sin(r2) * np.abs(r3 * Gbest - X[i]) else: X_new = X[i] + r1 * np.cos(r2) * np.abs(r3 * Gbest - X[i]) X_new = np.clip(X_new, lb, ub) f_new = func(X_new) if f_new < fitness[i]: X[i] = X_new fitness[i] = f_new # 更新全局最优 cur_best_idx = np.argmin(fitness) if fitness[cur_best_idx] < Gbest_fit: Gbest = X[cur_best_idx].copy() Gbest_fit = fitness[cur_best_idx] history.append(Gbest_fit) return Gbest, Gbest_fit, history两个地方要特别说明。第一,r的计算是在r_t基础上再乘一个随机数,目的是保留原始BOA的部分随机性,避免确定性递减导致算法变成纯梯度下降。第二,香味强度计算前做了最大最小归一化,这一步很多复现代码没有做,但如果不做,Ackley这类适应度范围非常大的函数很容易让f_i变成天文数字,一次移动就把解弹到边界外。
5.3 边界处理与精英保留策略
我在这段代码里使用了np.clip直接截断越界个体。相比取模映射,clip的优点是简单并且不会把解重新弹回搜索空间另一侧,缺点是在边界附近会积累个体。对于大多数测试函数,clip带来的影响可以忽略。
还要注意Gbest本身并不会被clip操作触碰,因为每次更新都是直接把X数组里最优个体的引用复制到Gbest,不经过更新公式,所以不存在最优个体被边界修改的问题。
精英保留策略是算法稳定性的保障:每一轮迭代结束时比较当前全局最优和历史最优,只有当历史最优被更优解替代时才更新Gbest。这样能防止个别轮次出现适应度退化。
5.4 统计结果输出格式
复现论文光跑一次不够,通常要运行30次独立实验,统计收敛曲线平均值、最优值均值和标准差。输出示例:
| 测试函数 | 原始BOA平均最优 | 改进BOA-SCA平均最优 | 改进最优值 |
|---|---|---|---|
| Sphere | 3.21e-05 | 1.04e-16 | 6.12e-21 |
| Rastrigin | 7.89e-01 | 1.54e-03 | 0.00e+00 |
| Ackley | 1.04e-02 | 3.55e-05 | 2.11e-07 |
这张表只是示意,真实的数值取决于维度和迭代次数。重点是要养成记录完整统计结果的习惯,而不是只展示某一次跑出的最好曲线。
6. 复现踩坑实录:验证方法、问题根因与调参建议
最后这部分是整篇复现过程中最有价值的沉淀。很多坑不是你不知道公式,而是公式实现出来之后结果不对,又找不到原因。这里把几个常见问题按排查难度列出。
6.1 怎么判断复现真的成功了
建议按三步验证。第一步,画出收敛曲线,观察是否前期快速下降、中期平稳过渡、后期逐渐逼近最优,如果曲线在中后期呈现完全水平甚至回升,说明参数调节有问题。第二步,跑30次独立实验,统计最优值均值、标准差和中位数,与原始BOA做对比。第三步,做非参数检验,常用Wilcoxon秩和检验,计算p值是否小于0.05,判断改进是否具有统计显著性。
稳定性永远比单次最优值重要。一组好的复现结果应该是30次运行中几乎没有灾难性失败,而不是某一次运气好跑出极小值。
6.2 香味强度不归一化导致步长爆炸
Ackley函数在[-32,32]区间内的适应度范围可能从0附近到超过20,如果直接用f = c * (fit^alpha)计算,在适应度高的个体上香味强度会非常大。一次更新可能让解移动几十个单位,直接越过边界。这是复现BOA时最容易踩的坑,原始论文的符号推导没有问题,但在数值实现时不做归一化很难复现出理想的收敛曲线。
解决方式就是在主循环里对每一轮的适应度做最大最小归一化,把刺激强度映射到接近[0,1]的区间。注意归一化后要加上一个极小量epsilon,防止适应度差异为0时除零错误。
6.3 Circle序列共用初值导致种群塌缩
我一开始为了让代码简洁,生成一条Circle混沌长序列后直接reshape成pop_size×dim的矩阵。结果发现高维函数上种群多样性反而变差,原因是同一维度的序列与下一维度序列之间高度相关,所有个体集中在解空间的一个狭长区域里,算法收敛速度极慢。
解决办法是对每个维度使用不同的随机初值并独立迭代,也就是第2章代码里的实现方式。这个问题的特点是表象容易误导:初始最优可能比随机初始化更好看,但全局搜索能力被大幅削弱,只有跑完整实验才能发现异常。
6.4 p值衰减过猛导致早熟
如果把p从0.9直接降到0.05,后期几乎每只蝴蝶都在做局部搜索,种群多样性快速耗尽,在多峰函数上几乎必然陷入局部最优。复现时我把p_end设了0.2,跑Rastrigin函数时30次实验有一半以上收敛到非零结果,后来把p_end调回0.3才恢复正常,收敛精度也提升了。
参数下限的设置思路是:确保局部搜索频率不会高到丧失种群多样性,同时早期探索积累的信息足够支持后期开发。p_end在0.3左右通常是一个合理区间,具体取值可以结合测试函数的多峰程度做敏感性实验。
6.5 参数敏感性与推荐组合
对w、p、r这类动态参数,常见的做法是把起止值和衰减指数做小范围网格搜索。我实际运行后的推荐组合如下:
- w:0.9递减到0.4,抛物线型,对单峰和多峰函数都比较稳;
- p:0.9递减到0.3,线性即可,不需要过多归一化处理;
- r:1.0递减到0.1,β=2,前期探索能力充足,后期步长收敛;
- c:保持0.01到0.25线性增长;
- a:保持0.1,这是BOA原论文的经典取值;
- SCA的a值:取2.0,只控制SCA自身的搜索幅度,不与w混用。
这组参数在Sphere、Rastrigin、Ackley等常用函数上整体表现均衡。如果你在复现自己的论文,建议先用这组参数跑通流程,确认改进有效后再针对目标函数做细调,不要一开始就盲目调参。
整套复现走下来,我最大的体会是组合改进最难的不是把三段代码拼在一起,而是让三段改进的调性一致。Circle混沌初始化把种群铺得足够均匀,w、p、r动态参数管的是搜索节奏的递进,SCA加的则是局部阶段的一次模式切换,三者需要协同发力,而不是各自为政。如果你复现时结果始终上不去,先别急着怀疑算法原理,按这章的顺序检查归一化、种群初始化方式和参数衰减范围,问题往往就藏在这几个细节里。