BAS-SCA混合优化极限学习机:从ELM随机性痛点看参数寻优实战
2026/9/7 17:48:33 网站建设 项目流程

做机器学习调参这些年,我一直对极限学习机(ELM)又爱又恨。爱的是它训练速度确实快,恨的是它那套随机生成输入权重和隐层偏置的机制,导致每次跑出来的结果方差极大,换个随机种子预测精度就跟过山车一样。后来我在实际项目中尝试用天牛须算法(BAS)和正余弦算法(SCA)做了一版混合改进的BAS-SCA优化ELM方案,用双重搜索机制去替代ELM的随机初始化,实测下来不但精度上去了,稳定性也比单用BAS或单用SCA好不少。这篇就把这套方案的完整思路、融合机制、代码实现和调参经验一次讲清楚,给同样被ELM随机性困扰的朋友做个参考。

1. 先从ELM的痛点聊起:为什么要优化它

1.1 ELM的随机权重问题到底影响多大

极限学习机的核心逻辑说起来很简单:输入层到隐层的权重 W 和偏置 b 随机生成,然后用解析方式求解输出层权重 β。因为 β 是通过最小二乘或岭回归一步计算出来的,所以训练过程不需要反向传播,速度自然快得离谱。但问题恰恰出在那个“随机生成”上——ELM的理论基础之一是:只要隐层节点足够多,随机映射也能把原始特征投影到高维空间,使得原本线性不可分的问题变得线性可分。理论没问题,实际操作中却隐藏着一个巨大的坑:随机权重不同,隐层输出矩阵 H 就不同,最终求出来的 β 也完全不同,模型的泛化性能跟着剧烈波动。

我在回归任务上做过一个简单实验:同一个数据集,隐层节点设为20,连续跑10次原始ELM,测试集RMSE的极差能差出30%以上。这在工业落地场景中是很难接受的——你今天训练好一个模型上线,明天重新训练一遍,性能就变了,下游业务根本没法稳定复用。所以业内很自然的思路就是用一个优化算法去搜索出一组更优的初始权重和偏置,让ELM不再完全依赖“随机抽卡”,而是通过寻优让每次训练都能落在更优的参数区间。

1.2 BAS和SCA为什么会出现在同一个解决方案里

既然要优化ELM,选择哪个优化算法就成了关键。我当时最先尝试的是天牛须算法(BAS),因为它的结构非常简单,不需要群体协作,只靠一只“天牛”用两根须探测左右两侧的气味浓度差异,就能朝更优方向移动。BAS收敛速度快、计算开销小,用起来很顺手。但它的问题也很明显:单一个体没有种群信息共享,天牛只能感知当前点附近的局部梯度,特别容易陷进局部最优。一旦初始位置选得不好,后面再怎么走都翻不出那个局部坑。

后来我在另一个项目里接触了正余弦算法(SCA),它的设计思路跟BAS完全相反——维护一个种群,每个个体通过正弦和余弦函数的振荡波形在搜索空间里来回摆动,既能做大幅度的全局探索,也能逐步收敛到最优解附近。SCA的全局搜索能力比BAS强得多,但它的更新方式依赖于“当前个体与全局最优之间的差值”,到了后期种群会快速向最优个体靠拢,搜索步长逐渐收缩,收敛精度其实一般。

一个小技巧是先各自跑一遍,然后观察收敛曲线:BAS前期下降极快,后期几乎不挪动;SCA前期探索充分但收敛慢,后期精度提升也很有限。这时我意识到,把BAS的局部感知能力和SCA的全局振荡能力结合起来,正好可以互补——用SCA保证大范围找到靠谱区域,用BAS在找到的区域里做精细打磨,这才是这套方案真正的出发点。

1.3 整体方案设计思路:互补优于单一

BAS-SCA混合算法的设计逻辑并不复杂,但融合方式需要仔细推敲。最朴素的做法是“串行”:先用SCA跑一半迭代,再用BAS接着精搜。但这样会导致两个算法各自为政,切换时信息断层,前期SCA收敛到的位置未必是适合BAS继续开发的位置。我最终采用的是“并行混合”策略:每个个体在同一轮迭代中同时执行SCA更新和BAS搜索,通过一个随迭代次数变化的融合权重 λ,把两种机制的贡献动态叠加起来。前期让SCA的全局振荡占主导,后期让BAS的局部精细搜索占主导。这样既不会丢失全局探索能力,又能保证收敛阶段的精度,是整个算法的关键所在。

另外,这套方案要作用的对象是ELM,而不是一个抽象的优化测试函数,所以在设计时还必须考虑ELM本身的特性——权重和偏置的取值范围、适应度函数的计算成本、隐层节点数量对搜索维度的影响等。这些细节处理不好,优化算法的理论再好也落不了地。

2. 三个基础算法的核心机制,先搞清楚再动手

2.1 天牛须算法(BAS):一只天牛怎么找食物

天牛须算法的灵感来自天牛觅食时的行为:天牛的两根触须可以感知食物气味的浓度,如果左侧气味浓,它就往左走;如果右侧浓,就往右走。这个过程抽象成数学模型后非常简洁。假设当前位置是 X,天牛随机生成一个归一化的方向向量 dir,左须位置和右须位置分别表示为:

XL = X + d * dir / 2 XR = X - d * dir / 2

其中 d 表示两只触须之间的距离。然后分别计算左右两个位置对应的适应度 f(XL) 和 f(XR),如果左须位置的适应度更好(求最小值时更小),天牛就朝左移动,否则朝右移动。位置更新公式为:

X_new = X - step * sign(f(XL) - f(XR)) * dir

这里的 step 是天牛的搜索步长,sign 是符号函数,决定移动方向。随着迭代推进,step 和 d 都会按一定比例衰减,模拟天牛越来越接近食物源时步子越迈越小的过程。BAS的优点是单个体搜索,每轮只算两次适应度,开销低、收敛快;缺点是缺少种群信息,一旦方向判断出错就可能直接走进死胡同。

2.2 正余弦算法(SCA):用三角函数画出来的搜索轨迹

SCA是Mirjalili在2016年提出的群体智能优化算法,核心思想是用正弦和余弦函数的周期性震荡来模拟个体在解空间中的波动搜索。对种群中的每个个体,位置更新公式如下:

当 r4 < 0.5 时: X_new = X + r1 * sin(r2) * |r3 * P - X|

当 r4 >= 0.5 时: X_new = X + r1 * cos(r2) * |r3 * P - X|

这里的 P 是当前全局最优位置,r2 是[0, 2π]内的随机数,决定步进方向;r3 是[0, 2]内的随机数,用来给最优位置加一个随机权重;r4 是[0, 1]内的随机数,用来在正弦和余弦两种更新模式之间做选择。r1 是一个从 a 线性递减到 0 的控制参数,a 通常取2,它决定了搜索幅度:前期 r1 大,个体大范围震荡,进行全局探索;后期 r1 小,震荡幅度收窄,转为局部开发。所以SCA本质上是通过三角函数周期性和随机参数配合,在“探索”和“开发”之间做平衡的多个体搜索算法。

2.3 极限学习机(ELM):随机映射加一步解析求解

ELM的结构是一个单隐层前馈神经网络,假设输入样本是 X,隐层节点数是 L,激活函数是 g。隐层输出矩阵 H 通过下面的公式计算:

H = g(X * W + b)

其中 W 是输入层到隐层的权重矩阵,b 是隐层偏置,这两个就是随机生成的部分。ELM的训练核心在于输出权重 β 的求解:目标是最小化 ||Hβ - T||²,其中 T 是训练目标。这是一个线性最小二乘问题,可以直接用Moore-Penrose伪逆求解:

β = pinv(H) * T

整个训练过程一步到位,没有任何迭代式梯度更新,所以训练速度极快。但正因为 W 和 b 是随机生成的,H 的好坏完全取决于运气。如果随机生成的映射不能有效提取特征,ELM的精度就会很差。所以用BAS-SCA去搜索最优的 W 和 b,本质上是把ELM从“随机映射”升级为“优化映射”,在保留快速训练优势的同时,大幅提升模型性能的稳定性和上限。

3. 融合逻辑与混合改进机制的核心设计

3.1 第一个改进:把天牛须扰动嵌进SCA更新公式

这是整套混合机制最核心的部分。我没有把BAS和SCA当作两个独立的流水线串联使用,而是把BAS产生的方向扰动直接作为一项修正量注入SCA的更新公式中。混合后的位置更新式为:

X_new = λ * X_sca + (1 - λ) * X_bas

其中 X_sca 是SCA生成的候选位置,X_bas 是BAS生成的候选位置。X_sca 保证了当前个体向全局最优方向移动的趋势,X_bas 则通过左右须的适应度比较提供了局部的方向修正。融合权重 λ 控制两种机制的贡献比例。

这样做有一个很实际的好处:如果某个个体位于一个局部最优附近,SCA的震荡幅度又已经很小,单靠SCA几乎不可能跳出去;而BAS的左右须方向判断可以提供绕开局部坑的微调路径,相当于在SCA的搜索路径上增加了一个局部勘探器。反过来,BAS容易误判方向的问题也能被SCA的全局最优牵引所纠正,不至于走偏太远。

3.2 第二个改进:自适应融合权重的动态调节

融合权重 λ 不是固定值,而是随迭代次数动态调整的。我使用的公式是:

λ = 0.8 - 0.6 * (t / T)

其中 t 是当前迭代次数,T 是最大迭代次数。迭代初期 t=0 时 λ=0.8,SCA占绝对主导,保证种群在全局范围内充分探索;迭代结束时 λ=0.2,BAS占主导,对已经找到的较优区域做精细搜索。这个设计的思路很简单:全局探索阶段需要大尺度的震荡,BAS的步长再大也有限,不如让SCA放开手脚跑;到了后期种群已经集中在某个区域了,再用SCA大幅震荡反而会破坏已找到的好解,此时BAS的小步精细修正更有价值。

在实际操作中,有人也会用非线性衰减或余弦衰减来调整 λ,比如 λ = 0.5 + 0.3 * cos(π * t / T),效果大同小异。我建议不用纠结具体衰减函数,关键是保证“前期探索、后期开发”这个趋势不能变。

3.3 第三个改进:边界反射与精英保留策略

优化ELM的权重和偏置时,搜索范围通常被限制在[-1, 1]或[-2, 2]之间。如果某个个体的位置更新后超出了边界,最常见的处理是直接裁剪到边界值,但这样会导致大量个体聚集在边界上,损失种群多样性。我实际用的是反射边界策略:如果某一维度的值超过上界,就根据超出部分按比例反弹回边界内。反射法能把个体留在边界附近但又不全部叠在边界点上,对保持种群多样性有明显帮助。

同时,每一轮迭代结束后,我会单独记录全局历史最优解,下一轮不论个体如何更新,全局最优始终不会被覆盖。这个精英保留策略几乎是所有优化算法落地时的标配,尤其当适应度函数本身有一定噪声时(比如数据分布不均匀),没有精英保留很容易把已经找到的好解弄丢。

3.4 改进后的搜索行为分析

从搜索行为上看,改进后的BAS-SCA兼具了“群体信息共享”和“单体方向感知”两种能力。SCA负责在宏观上把种群引向有希望的区域,BAS负责在每个个体层面做微观修正。融合权重 λ 又保证了两者的主导地位随时间平滑切换。相比原始BAS,混合算法显著降低陷入局部最优的概率;相比原始SCA,混合算法在后期的收敛精度和收敛速度都有提升。

我在测试函数Sphere、Rastrigin和Griewank上做过快速验证,结论很直观:在Rastrigin这种多峰函数上,原始BAS基本逃不出局部最优,原始SCA能到达不错的区域但后期收敛缓慢,BAS-SCA不仅收敛到了更优值,而且连续运行多次的方差明显小于两个单一算法。这个结果也让我放心把它用到ELM参数寻优上。

4. 手把手实操:BAS-SCA优化ELM的完整实现

4.1 个体编码与适应度函数

用BAS-SCA优化ELM,第一步是把ELM的待优化参数编码成优化算法中的个体向量。假设输入特征维度是 n_input,隐层节点数是 n_hidden,那么需要优化的参数包括:输入权重 W(形状为 n_input × n_hidden)和隐层偏置 b(形状为 1 × n_hidden)。把 W 展平后拼接上 b,得到一个长度为 n_input * n_hidden + n_hidden 的向量,这就是一个个体。

适应度函数的设计直接影响最终效果。分类任务可以用错误率或交叉熵作为适应度,回归任务最常用的是均方根误差(RMSE)或平均绝对误差(MAE)。我习惯用验证集上的误差来评估个体的好坏,而不是用训练集,因为优化算法很可能过拟合训练集,导致最终选出的参数泛化性能差。如果数据集规模较小,也可以采用K折交叉验证取平均误差作为适应度。

4.2 核心代码实现

下面是我整理出的一个可以直接跑通的核心代码框架,依赖只有numpy。这段代码用伪代码风格保留了主要逻辑,帮助你理解每个步骤到底在干什么。

import numpy as np def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) def elm_predict(X, W, b, beta): H = sigmoid(X @ W + b) return H @ beta def compute_beta(X, y, W, b): H = sigmoid(X @ W + b) # 加入小量正则项,防止伪逆计算不稳定 return np.linalg.pinv(H.T @ H + 1e-6 * np.eye(H.shape[1])) @ H.T @ y def fitness_function(individual, X_train, y_train, X_val, y_val, n_input, n_hidden): W = individual[:n_input * n_hidden].reshape(n_input, n_hidden) b = individual[n_input * n_hidden:].reshape(1, n_hidden) beta = compute_beta(X_train, y_train, W, b) y_pred = elm_predict(X_val, y_val, W, b, beta) return np.sqrt(np.mean((y_val - y_pred) ** 2)) # RMSE def bas_sca_elm(X_train, y_train, X_val, y_val, n_input, n_hidden, pop_size=20, max_iter=100, lb=-1.0, ub=1.0): dim = n_input * n_hidden + n_hidden # 初始化种群 pop = np.random.uniform(lb, ub, (pop_size, dim)) fitness = np.array([fitness_function(ind, X_train, y_train, X_val, y_val, n_input, n_hidden) for ind in pop]) gbest_idx = np.argmin(fitness) gbest = pop[gbest_idx].copy() gbest_fit = fitness[gbest_idx] a = 2.0 step0 = (ub - lb) * 0.3 for t in range(max_iter): r1 = a - t * (a / max_iter) for i in range(pop_size): # SCA更新 r2 = 2 * np.pi * np.random.rand() r3 = 2 * np.random.rand() r4 = np.random.rand() if r4 < 0.5: X_sca = pop[i] + r1 * np.sin(r2) * np.abs(r3 * gbest - pop[i]) else: X_sca = pop[i] + r1 * np.cos(r2) * np.abs(r3 * gbest - pop[i]) # BAS更新:方向感知 step = step0 * (1 - t / max_iter) d = step * 0.5 dir_vec = np.random.randn(dim) dir_vec = dir_vec / np.linalg.norm(dir_vec) XL = pop[i] + d * dir_vec / 2 XR = pop[i] - d * dir_vec / 2 fL = fitness_function(XL, X_train, y_train, X_val, y_val, n_input, n_hidden) fR = fitness_function(XR, X_train, y_train, X_val, y_val, n_input, n_hidden) direction = -1 if fL < fR else 1 X_bas = pop[i] - step * direction * dir_vec # 融合权重:前期SCA主导,后期BAS主导 lam = 0.8 - 0.6 * (t / max_iter) X_new = lam * X_sca + (1 - lam) * X_bas # 边界反射 for j in range(dim): if X_new[j] > ub: X_new[j] = 2 * ub - X_new[j] if X_new[j] < lb: X_new[j] = 2 * lb - X_new[j] pop[i] = X_new fitness[i] = fitness_function(pop[i], X_train, y_train, X_val, y_val, n_input, n_hidden) gbest_idx = np.argmin(fitness) if fitness[gbest_idx] < gbest_fit: gbest = pop[gbest_idx].copy() gbest_fit = fitness[gbest_idx] W = gbest[:n_input * n_hidden].reshape(n_input, n_hidden) b = gbest[n_input * n_hidden:].reshape(1, n_hidden) beta = compute_beta(X_train, y_train, W, b) return W, b, beta, gbest_fit

代码中需要特别留意的有两点:一是 compute_beta 中给 H.T @ H 加了一个小量正则项,防止矩阵奇异导致伪逆计算报错;二是边界反射是逐维度处理的,避免整体向量操作时边界逻辑出错。这段代码在中小规模数据集上运行的速度是可以接受的,适应度函数的计算次数是 pop_size * (1 + 2) * max_iter,也就是每轮每个个体除了自身适应度外,还要额外算两次天牛须位置的适应度,计算量相比标准SCA有所增加,但换来的是更稳的收敛表现。

4.3 参数配置建议

把代码跑起来之后,参数配置就成了决定效果好坏的另一个关键因素。我不建议一上来就照搬默认参数,因为不同数据集的维度和样本量差异很大。以下是我调参过程中的一些经验值:

参数建议范围说明
种群规模 pop_size20~30太小容易早熟,太大增加计算开销
最大迭代次数 max_iter100~300视适应度函数计算成本灵活调整
边界范围 lb/ub±1 或 ±2权重和偏置在这个范围通常够用
初始步长 step0边界宽度的0.3倍过大导致震荡剧烈,过小收敛慢
融合权重初值0.8保证前期SCA主导探索
融合权重终值0.2保证后期BAS主导精细搜索

4.4 效果对比怎么看

验证混合算法的效果,不能只看一次实验的结果。因为ELM本身有随机性,优化算法也有随机性,至少要跑10次以上,对比平均值和标准差。我在实际项目中通常做这样几组对比:原始ELM(随机初始化)、BAS-ELM、SCA-ELM、BAS-SCA-ELM。观察两个维度:一是平均预测误差,二是多次运行的标准差。前者看精度上限,后者看稳定性。

典型的实验趋势是:原始ELM的平均误差最大且方差极大;BAS-ELM精度提升明显但方差依然不小,说明BAS在某些初始点上会陷进局部最优;SCA-ELM平均误差优于原始ELM,但收敛需要的迭代次数更长;BAS-SCA-ELM在平均误差和方差两个指标上通常都是最优的。如果你跑出来的结果违背了这个规律,大概率是参数没调对,而不是算法有问题,继续往下面的排查思路去找原因。

5. 我踩过的坑与排查经验

5.1 优化后精度反而更差,先查这四处

第一处是隐层节点数太少。ELM本身能力不足时,任何优化算法都救不回来,因为搜索空间本身就不存在足够好的解。隐层节点数一般先按输入特征的2到3倍起步,再逐步增加观察效果。第二处是适应度函数用错了数据集——如果直接在训练集上优化得到的结果在测试集上反而差,大概率是过拟合了;换成验证集评估或者加交叉验证能缓解。

第三处是边界范围设置不合理。权重初始化为±1是ELM的常见做法,但不同数据集的合适范围差异很大,有时候把边界放到±2反而更好,这个需要根据激活函数和数据分布去试。第四处是融合权重衰减速度太快,导致后期完全切到BAS模式后全局扰动消失,而BAS又正好陷在局部最优里出不来;把 λ 终值从0.2适当上调到0.3或0.4,保留一部分SCA扰动,经常能解决问题。

5.2 算法收敛到一半就僵住了怎么诊断

“僵住”表现为适应度曲线在迭代中段就长期持平,几乎不再下降。最直接的原因通常是种群多样性丧失——所有个体都聚集到了同一个位置附近,SCA的震荡幅度又已经被 r1 衰减得很小,个体之间差异几乎为零,算法失去了继续搜索的动力。这时可以先检查种群的分布情况,如果所有个体的距离都很小,说明多样性出了问题。

一个问题排查顺序是:先调大 r1 的初始值,让前期的震荡幅度更大;再检查边界反射实现是否有 bug,如果实现正确,个体应该不会大量堆积在边界上;最后检查是否不小心把全局最优个体也覆盖掉了,没有精英保留的话这种情况特别容易发生。我最初实现时就是因为漏了精英保留,导致每轮的最优解在下一次迭代中被新的随机更新冲掉,曲线看起来就像一直原地踏步。

5.3 面对不同数据集的调参路线

如果做的是高维稀疏数据(比如文本分类),搜索维度会非常高,个体长度动辄几千,这时需要把种群规模降下来、迭代次数适当增加,因为每次适应度计算都涉及矩阵运算,种群太大会拖慢整体速度。如果面对的是小样本数据集,适应度评估很容易受噪声影响,建议用交叉验证代替单次验证集评估,同时把边界范围缩小,防止优化算法在噪声中乱跑。

如果隐层节点数较多(超过50),搜索维度就很大,BAS天牛须的方向向量生成和归一化会占不少计算时间。一种优化做法是先把ELM的隐层特征中心初始化到一个合理范围(比如通过原始ELM跑几次取平均权重),再用BAS-SCA在这个范围内做局部精搜。这样可以大幅缩短搜索空间,但代价是可能错过真正的最优解,属于精度和速度之间的权衡。

5.4 这套方案最适用的场景

我不建议在超大规模数据集上直接使用这套方案,因为优化算法需要反复计算ELM的适应度,每一轮都要做矩阵伪逆运算,数据量大了之后整体耗时不可接受。它更契合的场景是中小规模数据、对模型稳定性要求高的任务:比如工业设备的故障诊断、小样本回归预测、金融风控里的评分卡建模,这些场景数据量可控,但对每次训练的结果一致性要求很高,很值得用BAS-SCA-ELM去替换原始ELM。

如果数据集样本量在几千到几万级别、特征维度在几十到几百这个范围,BAS-SCA-ELM的寻优时间通常在几十秒到几分钟内可以完成,换来的稳定性提升是实打实的。另一个常见用途是作为集成学习里的基学习器——由于优化后的ELM每次运行结果更一致,集成时不容易出现个别基学习器效果极差拉低整体表现的问题。

最后再分享一个实际使用中的小技巧:在跑BAS-SCA-ELM之前,先用原始ELM快速跑几轮,记录一下随机初始化下的典型误差水平,然后把这个误差水平作为BAS-SCA寻优的“基线”。如果优化后的结果连基线都打不过,说明算法流程中一定有 bug,而不是算法本身没用。这个习惯帮我排查了很多初期实现问题,也适合你在自己调试代码时参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询