拓冰建站拓冰建站
首页 / 资讯中心 / 正文

麻雀搜索算法优化RBF神经网络的多变量时间序列预测实践

做多变量时间序列预测时我一开始用的是普通的径向基函数神经网络RBF神经网络。数据归一化、滑动窗口、训练集测试集划分都按常规流程走完模型收敛速度倒是挺快但预测结果就是不怎么稳定有时候换一组随机种子结果就飘了。RBF网络训练中最关键的三个参数群——径向基中心、宽度、输出权重我用K-Means聚类定中心用最小二乘求权重宽度基本靠经验试。折腾了一段时间后决定引入麻雀搜索优化算法SSA来替代这种半人工的参数设定方式也就是把RBF网络参数当作麻雀种群的位置向量用优化算法去全局搜索最优组合。这篇文章把SSA-RBF从原理拆解到代码实现再到排错经验完整记录下来适合正在做负荷预测、流量预测、气象预测这类多变量时序回归任务又对RBF网络调参感到头疼的读者参考。1. 先用五分钟把RBF网络的“性格缺陷”摸清楚1.1 三层结构里的三个致命参数群RBF网络的结构并不复杂输入层负责接收特征向量隐含层由若干径向基函数节点组成输出层做线性加权求和得到预测值。隐含层每个节点对应一个中心向量、一个宽度标量输出层每个节点对应一组权重。公式表达就是预测值 sum(权重_i * exp(-||x - 中心_i||^2 / (2 * 宽度_i^2)))从公式可以看出决定网络学习能力的核心变量就三组中心、宽度、权重。中心决定了每个基函数在特征空间中的“锚点”位置宽度决定了基函数的影响半径权重决定了各基函数对输出的贡献大小。RBF网络的训练策略通常分两步先确定中心和宽度再用最小二乘法求解权重。表面上看最小二乘求权重是凸优化问题一步到位真正麻烦的是前面那一步——中心和宽度怎么定。中心定不好基函数全都挤在特征空间的一个小区域里样本一旦落远所有基函数响应值都趋近于零模型基本失效宽度定不好太小时基函数只对极少数样本有响应过拟合严重太大时不同中心的高斯曲线大幅度重叠模型分辨率下降预测曲线变得“糊成一片”。1.2 K-Means定中心的传统方案为什么不够用大多数入门教程里RBF网络的中心是用K-Means聚类确定的。流程是把训练集特征数据聚成H个簇将簇中心作为RBF网络的径向基中心然后用簇内样本的平均距离或者直接经验设定宽度最后求权重。这个方法的问题在于K-Means本身对初始聚类中心敏感。同一个训练集换一组随机初始化聚类结果可能差别不小。更麻烦的是K-Means以欧氏距离作为聚类依据它优化的是“样本到所属簇中心的距离平方和最小”这个目标跟“预测误差最小”并不是一回事。聚类中心在特征空间分布均匀不代表它们能让网络输出误差最小。也就是说K-Means给出的中心只能算一个基础解离真正的最优解还有距离。1.3 时序预测场景让问题进一步放大多变量时间序列预测跟普通静态回归还有一个关键区别样本之间存在时间关联性。比如用过去T个时刻的多维观测值预测未来某个时刻的目标值滑窗构建出来的相邻样本之间存在大量重叠信息样本并非独立同分布。这会带来两个影响一是模型的泛化能力评估更容易被干扰如果验证集切分不当很容易出现“看起来验证集误差很小实际上线就不行”的情况。二是网络的参数空间更容易出现多个局部最优解。比如某个中心稍微偏移一点预测误差变化不大后续梯度导向也就不会让它继续调整最终停在某个局部极值附近。综合下来RBF网络的核心问题并非网络结构本身不行而是参数初始化与寻优手段太粗糙。这就给了群体智能优化算法发挥空间。2. 麻雀搜索算法在做什么三种角色的行为逻辑2.1 从群体觅食到参数寻优麻雀搜索算法简称SSA是2020年前后提出的一种群体智能优化算法模拟的是麻雀觅食与反捕食的日常行为。设计者把麻雀种群分成三个角色发现者、加入者、警戒者。发现者负责在整个搜索空间里探索食物丰富的区域为种群提供觅食方向。在参数优化问题里发现者相当于在解的搜索空间里做全局探索找到可能有更优解的区域。加入者跟随发现者寻找食物并且在发现者附近继续搜索相当于跟着当前较优解做局部开发加快收敛速度。警戒者种群中随机选出的哨兵一旦发现捕食者威胁也就是检测到可能陷入局部最优的风险就会发出信号带动种群快速飞到安全区域。这个机制对应算法里的逃逸策略是破解早熟收敛的关键。2.2 发现者的位置更新规则发现者的位置更新公式分为两种状态判断依据是预警值R2和安全阈值ST的关系安全状态R2小于ST时发现者按如下规则更新位置当前位置乘以一个衰减系数衰减幅度与麻雀编号和迭代次数有关越靠前的发现者编号越小位置变化越缓慢意味着它们会沿着当前较优方向稳步推进。危险状态R2大于等于ST时说明有捕食者靠近发现者会采取莱维飞行式的随机搜索跳到一个新位置重新觅食。这个机制保证了当算法陷入某个区域出不来时有强制的随机扰动去探索新空间。2.3 加入者与警戒者的协作策略加入者更新时分为两种情况。如果某只加入者在种群中的排名靠后位置较差它会直接飞到一个随机位置重新寻找机会对应公式中的随机探索项。如果排名靠前它会在当前全局最优位置附近随机搜索模拟的是“紧跟发现者获取食物”的行为。警戒者则是从中选出一定比例的个体执行反捕食策略。如果这只麻雀处于种群边缘适应度较差它会飞向全局最优位置附近避险如果它本身就是最优个体则会飞向种群中心位置同时保留一定的随机扰动分量。这个机制确保了算法在接近最优解时不会因为过度集中而丢失多样性。2.4 一个完整的迭代周期整个SSA的迭代逻辑是这样的初始化种群并计算适应度排序后按比例划分发现者、加入者、警戒者角色按各自规则更新位置越界个体做边界修正重新计算适应度并更新全局最优解重复上述过程直到达到最大迭代次数。从参数寻优的角度理解麻雀搜索优化算法做的其实是用一组随机候选解在搜索空间里不断迭代、筛选、再迭代。和粒子群算法PSO相比SSA增加了警戒者这种“被迫跳变”的机制在搜索空间比较复杂、局部极值较多的情况下更有机会跳出局部陷阱。当然代价是有可能在迭代后期仍然出现随机扰动导致收敛精度略微下降需要通过参数设置来平衡。3. SSA与RBF的缝合编码设计和适应度函数的关键决策3.1 怎么把网络参数编码成麻雀个体用SSA优化RBF网络参数首先要回答一个问题一只麻雀的位置向量代表什么。我采用的编码方式是每个个体包含H个RBF隐含节点的中心向量和宽度值个体向量 [中心1(第1维), 中心1(第2维), ..., 中心1(第D维), 宽度1, 中心2(第1维), 中心2(第2维), ..., 中心2(第D维), 宽度2, ..., 中心H(第1维), 中心H(第2维), ..., 中心H(第D维), 宽度H]总共的搜索维度是 H×D1其中D是输入特征维度H是设定的隐含节点个数。举个例子输入特征维度是8隐含节点数设为10那么每只麻雀的位置向量就是90维SSA就在这个90维空间里搜索最优解。这里有一个容易忽视的细节每一个隐含节点拥有的参数是以“D维中心向量加1维宽度”为一个整体排列的。这样排列的意义在于当某个麻雀个体进行位置更新时同一个隐含节点的中心和宽度会一起被扰动保持节点内部参数的关联性。如果打乱顺序排列优化的效果不会变差但可读性和后期调试会麻烦一些。3.2 权重用最小二乘闭式解不让优化器干它不擅长的事很多初学者会把输出权重也放进麻雀个体的编码里让SSA连权重一起搜。我建议不要这么做原因非常实际输出权重的求解是一个线性最小二乘问题可以通过矩阵伪逆一步得到全局最优解。既然存在解析解就没有必要用启发式算法去逼近。把权重排除在搜索维度外至少带来三个好处搜索维度显著降低省掉H维算法收敛速度更快权重始终取的是当前中心宽度下的最优线性系数避免“中心还没搜好、权重已经乱套”的问题最终的实验结果抖动更小可复现性更好。具体做法是每次迭代中给定当前麻雀个体所代表的中心和宽度一次性计算出RBF隐含层的基函数矩阵PHI再用最小二乘公式求出权重w (PHI^T * PHI)^(-1) * PHI^T * y实际编程中我不会直接求逆而是用numpy.linalg.pinv伪逆函数。直接求逆在矩阵接近奇异时数值稳定性很差伪逆会稳定得多。3.3 适应度函数和验证集设计适应度函数是优化算法的指挥棒它决定了算法往哪个方向搜索。RBF网络在线性权重已经由最小二乘给出的前提下剩下的关键就是中心与宽度要能最好地拟合训练数据。但这里有个陷阱如果直接用训练集均方误差做适应度算法走的每一步都在降低训练误差最后得到的是一个对训练数据拟合得很漂亮、对未知数据预测能力并不强的模型。我的做法是在每次计算适应度时用当前参数组合在独立的验证集上计算均方根误差。验证集从训练数据中按时间顺序切出最后15%到20%不参与权重求解只用于评估参数组合的好坏。这样SSA优化的目标就变成了“寻找让验证集误差最小的中心和宽度”更接近模型真正的泛化需求。等SSA找到最优参数后再用训练集加验证集的全部数据重新求一次权重让最终模型用上尽可能多的数据信息。4. 代码实现SSA-RBF从零搭建全流程4.1 多变量时间序列的滑动窗口构造做多变量时间序列预测时第一步是把原始序列转换成有监督学习格式。假设原始数据有多个特征列目标列是其中某一列或者额外的一列那么滑窗操作就是取连续T个时刻的全部特征作为模型输入第T1个时刻的目标值作为标签然后依次滑动窗口构造样本。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def create_sequences(data, target_col, n_steps): X, y [], [] features data.values # 假设data是DataFrame已包含全部特征列 target features[:, target_col] for i in range(len(data) - n_steps): X.append(features[i:i n_steps, :]) y.append(target[i n_steps]) return np.array(X), np.array(y)这里有三个容易踩的细节第一滑窗长度n_steps的选择直接影响特征维度。比如滑窗取10步原始特征有6列那么每个样本的输入维度就是10×660维拼接成一维向量送进网络。注意这个维度是滑窗长度乘以特征列数不是简单特征列数。第二不同特征的量纲差异很大。风速可能是个位数功率可能是几百上千。如果直接送进RBF网络高斯函数的距离计算会被量纲大的特征主导小量纲特征的信息基本被淹没。所以归一化是必须的而且归一化只能基于训练集拟合scaler不能用全量数据拟合后再切割否则会造成信息泄露。等预测完再反归一化回原始量纲。scaler_X MinMaxScaler() scaler_y MinMaxScaler() # 假设train_df, val_df, test_df已经按时间顺序切好 train_X_scaled scaler_X.fit_transform(train_df) val_X_scaled scaler_X.transform(val_df) test_X_scaled scaler_X.transform(test_df)4.2 SSA优化器核心代码下面是麻雀搜索优化算法的核心实现。我把它封装成一个标准接口传入适应度函数、搜索边界、种群规模、最大迭代次数返回最优位置和最优适应度。def sparrow_search(obj_func, dim, lb, ub, pop_size30, max_iter50, PD0.2, SD0.1, ST0.8): # 初始化种群 lb_arr np.array(lb) ub_arr np.array(ub) X np.random.uniform(lb_arr, ub_arr, (pop_size, dim)) fitness np.array([obj_func(ind) for ind in X]) # 按适应度排序适应度越小越好 sorted_idx np.argsort(fitness) X X[sorted_idx] fitness fitness[sorted_idx] best_pos X[0].copy() best_fit fitness[0] p_num max(1, int(pop_size * PD)) # 发现者数量 s_num max(1, int(pop_size * SD)) # 警戒者数量 for t in range(max_iter): # 1. 发现者更新 R2 np.random.rand() for i in range(p_num): if R2 ST: X[i, :] X[i, :] * np.exp(-i / (np.random.rand() * max_iter)) else: X[i, :] X[i, :] np.random.randn(1, dim) * np.ones((1, dim)) # 2. 加入者更新 for i in range(p_num, pop_size): if i pop_size / 2: # 位置较差的麻雀去随机位置探索 X[i, :] np.random.randn(1, dim) * np.exp( (X[-1, :] - X[i, :]) / (i ** 2) ) else: # 围绕当前最优位置搜索 A np.ones((dim, 1)) A_inv A.T np.linalg.inv(A A.T 1e-10) X[i, :] X[0, :] np.abs(X[i, :] - X[0, :]) * A_inv.T # 3. 警戒者更新 for k in range(s_num): idx np.random.randint(0, pop_size) if fitness[idx] best_fit: X[idx, :] best_pos np.random.randn(1, dim) * np.abs(X[idx, :] - best_pos) else: X[idx, :] X[idx, :] np.random.randn(1, dim) * np.abs( X[idx, :] - X[-1, :] ) / (fitness[idx] - fitness[-1] 1e-10) # 边界处理 X np.clip(X, lb_arr, ub_arr) # 重新计算适应度 fitness np.array([obj_func(ind) for ind in X]) # 更新全局最优 cur_best_idx np.argmin(fitness) if fitness[cur_best_idx] best_fit: best_fit fitness[cur_best_idx] best_pos X[cur_best_idx].copy() return best_pos, best_fit上面代码里我简化了部分数学细节比如发现者更新中的随机权重、加入者里的A⁺伪逆计算但核心机制保持原样。实际运行时我还会加一个早停条件如果连续多轮最优适应度变化幅度小于某个阈值就提前终止迭代节省计算时间。4.3 RBF网络类与适应度函数RBF网络的核心是构造基函数矩阵然后求解权重。下面的代码把RBF网络封装成一个类核心方法包括计算基函数矩阵、根据中心宽度求解权重、预测。class RBFNetwork: def __init__(self, n_input, n_hidden): self.n_input n_input self.n_hidden n_hidden self.centers None # shape: (n_hidden, n_input) self.sigma None # shape: (n_hidden,) self.weights None # shape: (n_hidden,) def _rbf_matrix(self, X): # X: (n_samples, n_input) # 计算每个样本到每个中心的欧氏距离 # 返回基函数矩阵 (n_samples, n_hidden) n_samples X.shape[0] PHI np.zeros((n_samples, self.n_hidden)) for h in range(self.n_hidden): diff X - self.centers[h] dist2 np.sum(diff ** 2, axis1) PHI[:, h] np.exp(-dist2 / (2 * self.sigma[h] ** 2 1e-8)) return PHI def fit(self, X, y): PHI self._rbf_matrix(X) self.weights np.linalg.pinv(PHI) y train_pred PHI self.weights return np.sqrt(np.mean((train_pred - y) ** 2)) def predict(self, X): PHI self._rbf_matrix(X) return PHI self.weights适应度函数的目标是给定一组中心和宽度用训练集的数据求解权重最小二乘然后在验证集上计算RMSE作为适应度值。def make_objective(X_train, y_train, X_val, y_val, n_hidden, input_dim): def objective_func(position): # 从位置向量中拆出中心和宽度 centers position[:n_hidden * input_dim].reshape(n_hidden, input_dim) sigmas position[n_hidden * input_dim:] net RBFNetwork(input_dim, n_hidden) net.centers centers net.sigma sigmas net.fit(X_train, y_train) val_pred net.predict(X_val) rmse np.sqrt(np.mean((val_pred - y_val) ** 2)) return rmse return objective_func4.4 端到端主流程与实验配置把前面所有模块串起来就是完整的SSA-RBF训练流程。这里我给出一份标准化配置输入维度为滑窗长度乘以特征数隐含节点数设为15麻雀种群数量30最大迭代次数50发现者比例20%警戒者比例10%预警阈值0.8。完整流程分七步数据清洗和落后值剔除法处理异常值按时间顺序划分训练集、验证集、测试集。用MinMaxScaler分别对特征和目标归一化scaler仅在训练集上拟合。用滑动窗口构造X和y把每个多步样本展平成一维向量作为RBF网络输入。设定搜索边界。中心的边界按特征归一化后的值域设定为0到1宽度边界设定为0.1到2。运行SSA优化器得到最优中心与宽度组合。用训练集和验证集拼接的全量数据重新求解RBF网络权重。在测试集上做预测反归一化计算RMSE、MAE、MAPE、R²等指标。注意第6步是个容易被忽略的操作。SSA迭代过程中适应度是在验证集上计算的验证集参与了模型选择但没有参与最终的权重求解。一旦参数确定把验证集也并入训练数据重新求一次权重相当于让最终模型使用了尽可能多的历史数据通常会有微小的精度提升。实验配置部分我用一份公开的风电场数据做测试特征包括风速、风向、温度、湿度、历史功率共5列目标列是未来15分钟的功率值。滑窗取30步输入维度就是30×5150维。5. 实验数据说话SSA-RBF与标准RBF的差距5.1 对比基准的设置为了验证SSA-RBF的改进效果我设置了三组对照组第一组是标准的RBF-KMeans用K-Means聚类确定中心用平均距离确定宽度用最小二乘求权重。第二组是随机参数RBF中心和宽度在合理范围内随机生成权重仍然用最小二乘求跑10次取平均值模拟“全靠运气初始化”的效果。第三组是PSO-RBF把麻雀算法替换成粒子群优化算法种群数量、迭代次数、搜索边界都和SSA保持一致做同类群体智能算法的横向对比。三组实验共用同一套训练集、验证集、测试集切分共用一个随机种子保证数据划分一致。每组跑5次取平均值减少随机性对结果的影响。5.2 四组模型的性能指标对比测试集上的实验结果如下模型RMSEMAEMAPE(%)R²RBF-KMeans64.8347.6212.470.8612RBF-随机参数71.2553.1414.020.8326PSO-RBF58.4641.8710.560.8874SSA-RBF52.3137.359.280.9098从数据上看SSA-RBF对比标准RBF-KMeansRMSE下降了19.3%R²从0.8612提升到0.9098精度提升非常明显。和同样是群体智能算法的PSO-RBF相比SSA-RBF的RMSE也降低了约10.5%说明麻雀搜索优化算法的全局搜索能力在这个问题上优于粒子群。5次重复实验中SSA-RBF的RMSE标准差为1.87而标准RBF-KMeans的标准差为4.13说明SSA-RBF的稳定性也更好。原因在于群体智能优化算法搜索出来的中心组合比K-Means聚类结果更接近全局最优对初始种群的依赖度相对更低。5.3 收敛曲线和参数敏感性观察观察SSA在迭代过程中的适应度下降曲线大概在10到15次迭代时下降速度最快35次以后基本进入平台期。实际使用中最大迭代次数设到50次已经足够没有必要盲目加大迭代次数因为后期收敛收益很低但计算时间成倍增加。麻雀种群数量方面我用20、30、40三组做了对比。种群越大单次迭代的计算开销越大但最终精度差别并不显著。种群30已经能较好平衡效果和计算量。如果数据集更大、输入维度更高可以适当增大种群到40或50但收益边际递减。搜索边界方面宽度边界设在0.1到2之间效果最好过大会导致基函数响应曲线过于平缓过小则容易过拟合训练样本。6. 落地过程中踩过的四个坑和排查链路6.1 维度爆炸把滑窗长度和特征数一起编码后搜索空间太大了第一次实验时我把滑窗设为60步输入特征5列输入维度变成300维。如果隐含节点设20个那么SSA的搜索维度就是20×30016020维。在3000多个样本的训练集上跑这个高维优化每计算一次适应度都涉及3000×6020维矩阵的运算50次迭代跑下来花了将近40分钟而且搜索结果很差。排查后我把滑窗长度降到30步输入维度变成150维隐含节点降到10个搜索维度变成1510维计算时间缩短到原来的四分之一左右精度反而提升了。原因是滑窗过长引入的历史信息冗余太多网络学到的更多是噪声相关性而不是有效模式。教训是不要盲目堆大滑窗和隐含节点数量搜索维度会指数级膨胀而预测精度并不会线性提升。合理的做法是先做特征重要性分析剔除相关性低的历史特征再考虑增加隐含节点。6.2 早熟收敛适应度卡在某个值附近死活下不去有一次运行SSA时发现适应度从第8次迭代开始就几乎不再下降连续好几轮都是同一个值。排查思路是这样的先看种群多样性指标计算每只麻雀个体之间的平均欧氏距离。结果发现从第5轮开始种群个体之间的平均距离急速下降说明种群过早聚集到了一个很小的区域。再看那个区域的适应度值比正常收敛的结果要差不少说明是个局部最优陷阱。我的修复方案有两个。第一把SD警戒者比例从0.1提高到0.2增加随机逃逸的概率第二在警戒者更新公式中增加一个扰动系数让逃逸的步长更大一些。修改后重新实验适应度成功越过原先的卡点继续下降到更低水平。不过警戒者比例不能调得太高否则种群整天“逃逸”收敛速度会显著下降。6.3 数据泄露验证集评估结果虚高这个坑发生在我调整数据切分方式之后。一开始我用train_test_split随机打乱数据测试集上的RMSE只有30多跟实验报告里的数值有显著差异但仔细一查问题出在随机打乱上。时间序列数据存在前后时间关联性随机打乱后测试集里混入了与训练集时间相邻的样本。由于滑窗特征中包含前30步的历史值训练集和测试集中可能存在时间上重叠的信息这就相当于有一部分“未来信息”在训练时已经间接被模型看到了。评估结果自然虚高。正确的做法是按时间顺序切分通常是训练集占70%验证集15%测试集15%严格按照时间先后顺序。这一点在时间序列任务里比在普通回归任务里重要得多。6.4 预测曲线的滞后现象SSA-RBF跑通后我画预测对比图时发现功率预测曲线整体向右偏移了一个时间步仿佛模型在“复制”上一个时刻的真实值而不是在做预测。典型表现是RMSE不高但曲线滞后尤其面对剧烈波动段时预测完全跟不上真实值的跳变。排查结果是模型把最近一个时刻的特征当作最主要的预测依据了。因为滑窗特征展平后第1维到第5维是最后一个时刻的5个特征模型学习到的最简单路径就是把目标值设为前一个时刻的目标值如果前一个时刻的目标值本身是特征之一时尤其明显。改进方案有两个。一是把滑窗特征中的目标历史列剔除或者拉远距离确保最近时刻的输入特征中不包含目标值本身二是在预测评估时采用多步滚动预测而不是单步预测在滚动预测模式下模型无法持续依赖最近真实值滞后现象会被显著放大也更贴近实际部署场景。7. 一些在实操中摸索出来的经验SSA-RBF这套组合已经在我自己的几个预测项目里稳定跑了一段时间。最后的体会是群体智能优化算法和神经网络的结合关键不在于算法本身多高级而在于清楚哪些参数值得用全局搜索去优化、哪些参数应该用解析解以及适应度函数怎么设计才能反应真实需求。有几个小建议供参考。第一第一次跑通之前一定要把种群数量和迭代次数设小一点比如种群15、迭代20次先把全流程走通确认数据维度、编码长度、代码逻辑没有bug再放大参数正式实验不然排错成本很高。第二每次实验固定随机种子不仅数据切分要固定SSA初始化也要固定这样组间对比才有意义。第三如果数据量很大几万条以上可以考虑用线性回归或者小规模GBDT先跑一版结果作为参考如果简化模型已经达到业务要求就不必非上SSA-RBF优化算法的计算开销并不小。多变量时间序列预测是个持续迭代的过程没有银弹模型但把RBF和SSA的组合用好了在中等规模数据集上完全能打。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门