ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

BP、RBF与PSO-RBF神经网络回归预测对比:从调参到优化实战

BP、RBF与PSO-RBF神经网络回归预测对比:从调参到优化实战 简介本资源面向机器学习与深度学习入门及进阶学习者聚焦数据预测这一典型任务系统对比BP神经网络、RBF神经网络以及经粒子群优化算法PSO改进的RBF网络三种模型的实现与效果。压缩包共9个文件约87KB包含3个m脚本文件分别对应三种网络的核心实现1个mat数据文件与1个xlsx表格用于训练测试另有4张png图片展示预测结果与真实值的对比曲线便于直观评估各模型性能差异。目前已有1074人学习下载说明该案例在预测类问题中具有较高的参考价值。读者可借此掌握反向传播调参、径向基函数中心与带宽选择、PSO全局寻优等关键环节理解BP易陷局部极小、RBF训练快但参数敏感、PSO可提升泛化能力等要点并直接运行脚本复现实验为股票、天气等实际预测场景提供可迁移的代码框架与调参思路。1. 三种网络跑同一份数据为什么我最后把 BP 换成了 PSO-RBF手上有一份结构化数据字段不多量也不大要做回归预测。很多人第一反应是上 BP 神经网络毕竟结构图好画、Python 代码一搜一大把。但真跑起来你会发现两个问题一是 BP 对初始权重敏感同一份数据跑三次结果能差出一截二是当样本量小、特征维度低的时候BP 的隐层节点数怎么定全靠试试到后面就是玄学调参。RBF 神经网络在这类场景下往往更稳它用径向基函数做局部响应收敛快、参数少。但 RBF 也有自己的命门中心点和宽度怎么选。随机选中心预测精度看运气用 K-means 聚类选中心又容易陷进局部最优。这时候 PSO 优化就派上用场了——把 RBF 的中心、宽度甚至输出层权值交给粒子群去搜相当于给网络装了一个全局寻优的引擎。这篇笔记要讲的就是这三条路线怎么在同一份数据上跑通、怎么对比、参数怎么设、坑在哪。适合手里有结构化数据、想做回归预测、又不想把时间全耗在调参上的工程师。完整程序我会按模块拆开讲每一步都能直接抄。2. BP、RBF、PSO-RBF 三条路线的选型与数据准备2.1 为什么不是所有预测都该上 BPBP 神经网络本质是反向传播调权重的多层前馈网络。它的优势在于通用逼近能力理论上只要隐层节点够多能拟合任意连续函数。但落到小样本结构化数据上这个优势反而变成负担参数多、容易过拟合、对学习率和初始权重敏感。我一般会先看数据规模。样本量在几百到几千、特征在十个以内BP 的隐层节点数通常要试 5 到 20 之间的多个值学习率在 0.01 到 0.1 之间扫。这个过程如果没有自动化脚本手工试很痛苦。而且 BP 的损失曲面非凸梯度下降容易卡在局部极小表现出来就是训练集误差降不下去或者验证集误差忽高忽低。RBF 神经网络的结构不一样。它只有三层输入层、隐层、输出层。隐层用径向基函数常见是高斯函数对输入做非线性映射输出层是线性加权。因为输出层是线性的权值可以直接用最小二乘解出来不需要迭代。真正需要定的只有隐层中心、宽度和输出权值。参数少了训练就快也不容易过拟合。但 RBF 的精度高度依赖中心点的选择。中心点选得不好隐层神经元对输入的响应就覆盖不到关键区域预测自然不准。常见做法是用 K-means 聚类找中心但 K-means 本身对初始聚类中心敏感也容易局部最优。这就是 PSO 介入的理由。2.2 数据准备归一化和划分的固定动作不管走哪条路线数据预处理都是同一套。我习惯先把数据读进来做归一化到 [0,1] 或标准化然后按 7:3 或 8:2 划分训练集和测试集。注意归一化参数只能用训练集算再应用到测试集否则就是数据泄露。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split # 读取数据假设最后一列是目标值 data pd.read_csv(data.csv) X data.iloc[:, :-1].values y data.iloc[:, -1].values.reshape(-1, 1) # 归一化输入和输出都缩放到 [0,1] scaler_X MinMaxScaler() scaler_y MinMaxScaler() X_scaled scaler_X.fit_transform(X) y_scaled scaler_y.fit_transform(y) # 划分训练集和测试集固定随机种子保证可复现 X_train, X_test, y_train, y_test train_test_split( X_scaled, y_scaled, test_size0.3, random_state42 ) print(f训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]})这段代码里MinMaxScaler把每一维特征线性映射到 [0,1]避免量纲差异大的特征主导距离计算。random_state42是为了每次划分结果一致方便对比三种网络。注意y也要归一化因为 RBF 输出层是线性的如果目标值范围很大输出权值会很大数值不稳定。提示如果数据里有异常值先做箱线图或 3σ 检查再归一化否则异常值会把正常样本压缩到很窄的区间。2.3 三种网络的参数对照表在动手写代码之前先把三条路线的关键参数列清楚。这张表是我自己调参时的速查表参数范围是经验值具体数据要微调。网络类型关键参数常用范围调参优先级BP隐层节点数5~20高BP学习率0.01~0.1高BP训练轮数500~2000中RBF隐层中心数10~50高RBF高斯宽度 σ0.1~2.0高RBF输出权值求解最小二乘低PSO-RBF粒子数20~50中PSO-RBF迭代次数50~200中PSO-RBF惯性权重 w0.4~0.9高PSO-RBF学习因子 c1,c21.5~2.0中BP 的隐层节点数和学习率是最影响结果的RBF 的中心数和宽度决定拟合能力PSO 的惯性权重决定搜索的全局与局部平衡。后面每一节我会具体说怎么设。3. BP 神经网络预测从结构到可运行代码3.1 BP 网络结构与前向传播BP 网络的结构图热词搜的人多但真正写代码时不需要画图把矩阵维度对齐就行。假设输入维度是 n隐层节点数是 h输出维度是 m回归预测 m1。前向传播就是隐层输入 X · W1 b1隐层输出 sigmoid(隐层输入)输出层输入 隐层输出 · W2 b2预测值 输出层输入回归不加激活。反向传播用链式法则算梯度更新 W1、b1、W2、b2。下面是一个不依赖深度学习框架的纯 NumPy 实现方便看清每一步。import numpy as np class BPNetwork: def __init__(self, n_input, n_hidden, n_output, lr0.05): # 权重初始化小随机数避免对称性 self.W1 np.random.randn(n_input, n_hidden) * 0.5 self.b1 np.zeros((1, n_hidden)) self.W2 np.random.randn(n_hidden, n_output) * 0.5 self.b2 np.zeros((1, n_output)) self.lr lr def sigmoid(self, x): return 1 / (1 np.exp(-np.clip(x, -500, 500))) def sigmoid_deriv(self, x): s self.sigmoid(x) return s * (1 - s) def forward(self, X): self.z1 X self.W1 self.b1 self.a1 self.sigmoid(self.z1) self.z2 self.a1 self.W2 self.b2 return self.z2 def backward(self, X, y, output): m X.shape[0] # 输出层梯度回归用 MSE激活为线性 delta2 (output - y) / m dW2 self.a1.T delta2 db2 np.sum(delta2, axis0, keepdimsTrue) # 隐层梯度 delta1 (delta2 self.W2.T) * self.sigmoid_deriv(self.z1) dW1 X.T delta1 db1 np.sum(delta1, axis0, keepdimsTrue) # 更新 self.W2 - self.lr * dW2 self.b2 - self.lr * db2 self.W1 - self.lr * dW1 self.b1 - self.lr * db1 def train(self, X, y, epochs1000): losses [] for i in range(epochs): output self.forward(X) loss np.mean((output - y) ** 2) losses.append(loss) self.backward(X, y, output) return losses def predict(self, X): return self.forward(X)代码里np.clip是防止 sigmoid 溢出delta2除以m是取 batch 平均梯度。lr0.05是经验起点如果损失震荡就降到 0.01如果下降太慢就升到 0.1 但别超过 0.2。隐层节点数n_hidden先设 10后面用循环扫。3.2 训练 BP 并观察损失曲线训练时不要只看最终误差损失曲线能告诉你很多信息。下降平滑且收敛说明学习率合适震荡剧烈学习率偏大下降太慢学习率偏小或节点不够。# 假设 X_train, y_train 已经归一化 n_input X_train.shape[1] n_hidden 10 n_output 1 bp BPNetwork(n_input, n_hidden, n_output, lr0.05) losses bp.train(X_train, y_train, epochs1000) # 预测并反归一化 y_pred_bp bp.predict(X_test) y_pred_bp_inv scaler_y.inverse_transform(y_pred_bp) y_test_inv scaler_y.inverse_transform(y_test) # 计算 RMSE rmse_bp np.sqrt(np.mean((y_pred_bp_inv - y_test_inv) ** 2)) print(fBP 测试集 RMSE: {rmse_bp:.4f})epochs1000对小样本通常够用如果损失还在降可以加到 2000。scaler_y.inverse_transform把预测值还原到原始量纲RMSE 才有物理意义。我一般会同时看训练集和测试集的 RMSE如果训练集远小于测试集就是过拟合要减节点或加正则。3.3 BP 的隐层节点数怎么扫隐层节点数没有公式只能扫。写个循环每个值跑三次取平均避免随机初始化的偶然性。hidden_list [5, 8, 10, 15, 20] results {} for h in hidden_list: rmses [] for seed in range(3): np.random.seed(seed) net BPNetwork(n_input, h, n_output, lr0.05) net.train(X_train, y_train, epochs1000) pred scaler_y.inverse_transform(net.predict(X_test)) rmse np.sqrt(np.mean((pred - y_test_inv) ** 2)) rmses.append(rmse) results[h] np.mean(rmses) print(f隐层节点 {h}: 平均 RMSE {np.mean(rmses):.4f})这段代码里np.random.seed(seed)保证每个节点数下三次运行的初始权重不同但可复现。取平均是为了排除运气成分。通常你会看到 RMSE 先降后升最低点就是相对合适的节点数。但注意这个最低点在不同数据上不一样换数据要重新扫。4. RBF 神经网络预测中心点、宽度与最小二乘输出4.1 RBF 的数学形式和三个待定参数RBF 网络的输出是隐层神经元输出的线性组合y(x) Σ w_i · φ(||x - c_i||)其中 φ 是径向基函数常用高斯形式 φ(r) exp(-r² / (2σ²))。c_i 是第 i 个隐层神经元的中心σ 是宽度w_i 是输出权值。三个参数里输出权值 w 最好求固定中心和宽度后隐层输出矩阵 H 已知w (HᵀH)⁻¹Hᵀy就是最小二乘。中心和宽度才是难点。常见做法有三种随机从训练样本里选、K-means 聚类选、正交最小二乘选。我一般先用 K-means因为实现简单且比随机稳。4.2 用 K-means 确定中心点K-means 把训练样本聚成 k 类每类的质心作为 RBF 中心。k 就是隐层节点数通常取 10 到 50 之间。from sklearn.cluster import KMeans def train_rbf_kmeans(X, y, n_centers20, sigma1.0): # 用 K-means 找中心 kmeans KMeans(n_clustersn_centers, random_state42, n_init10) kmeans.fit(X) centers kmeans.cluster_centers_ # shape: (n_centers, n_features) # 计算隐层输出矩阵 H # 每个样本到每个中心的欧氏距离 dists np.linalg.norm(X[:, np.newaxis, :] - centers[np.newaxis, :, :], axis2) H np.exp(-dists ** 2 / (2 * sigma ** 2)) # shape: (n_samples, n_centers) # 最小二乘求输出权值加小正则防止奇异 lam 1e-6 w np.linalg.solve(H.T H lam * np.eye(n_centers), H.T y) return centers, sigma, w def predict_rbf(X, centers, sigma, w): dists np.linalg.norm(X[:, np.newaxis, :] - centers[np.newaxis, :, :], axis2) H np.exp(-dists ** 2 / (2 * sigma ** 2)) return H wn_init10让 K-means 跑 10 次不同初始化取最优减少局部最优。lam1e-6是岭正则防止 HᵀH 奇异。sigma先设 1.0后面要调。注意dists的计算用了广播样本多的时候内存占用大可以分批算。4.3 宽度 σ 和中心数怎么配合调σ 控制径向基函数的“胖瘦”。σ 太小每个神经元只对很小区域响应网络变成查表泛化差σ 太大所有神经元响应都差不多网络退化成线性模型。经验上 σ 取中心点之间平均距离的 0.5 到 2 倍。# 计算中心点之间的平均距离作为 sigma 的参考 from scipy.spatial.distance import pdist avg_dist np.mean(pdist(centers)) sigma_candidates [avg_dist * 0.5, avg_dist * 1.0, avg_dist * 1.5, avg_dist * 2.0] for sigma in sigma_candidates: centers, _, w train_rbf_kmeans(X_train, y_train, n_centers20, sigmasigma) pred scaler_y.inverse_transform(predict_rbf(X_test, centers, sigma, w)) rmse np.sqrt(np.mean((pred - y_test_inv) ** 2)) print(fsigma{sigma:.4f}, RMSE{rmse:.4f})pdist算的是中心点两两距离取平均后乘系数。这段代码会告诉你哪个 σ 在测试集上最好。注意不要用测试集调 σ应该从训练集里再分一个验证集否则测试集就泄露了。我为了演示方便直接用了测试集实际项目要改。4.4 RBF 的完整训练和预测流程把上面的片段串起来加上中心数扫描就是 RBF 的完整程序。best_rmse float(inf) best_config None for n_centers in [10, 20, 30, 40]: for sigma_factor in [0.5, 1.0, 1.5, 2.0]: centers, _, w train_rbf_kmeans(X_train, y_train, n_centers, sigma1.0) avg_dist np.mean(pdist(centers)) sigma avg_dist * sigma_factor centers, _, w train_rbf_kmeans(X_train, y_train, n_centers, sigma) pred scaler_y.inverse_transform(predict_rbf(X_test, centers, sigma, w)) rmse np.sqrt(np.mean((pred - y_test_inv) ** 2)) if rmse best_rmse: best_rmse rmse best_config (n_centers, sigma, rmse) print(f中心数{n_centers}, sigma{sigma:.4f}, RMSE{rmse:.4f}) print(f最优配置: 中心数{best_config[0]}, sigma{best_config[1]:.4f}, RMSE{best_config[2]:.4f})这段双重循环会跑 16 种组合每种组合重新聚类和求权值。注意sigma依赖centers所以先聚类再算avg_dist再重新训练。实际跑的时候如果数据大可以把 K-means 结果缓存只重算 H 和 w。5. PSO 优化 RBF把中心和宽度交给粒子群5.1 PSO 优化 RBF 的编码方式PSO 优化 RBF核心是把 RBF 的待定参数编码成粒子的位置向量。有两种常见编码一种是只优化中心和宽度输出权值仍用最小二乘另一种是把中心、宽度、输出权值全编码进去。我一般用第一种因为输出权值用最小二乘是解析最优没必要让 PSO 去搜搜了反而慢且不一定更好。假设 RBF 有 k 个中心每个中心是 d 维加上一个宽度 σ粒子维度就是 k×d 1。如果 d5、k20粒子就是 101 维。维度不低但 PSO 对 100 维左右的问题还能应付。适应度函数用训练集上的 RMSE 或验证集 RMSE。注意每次粒子更新位置后要重新算隐层输出矩阵 H再用最小二乘求 w然后算预测误差。5.2 PSO 的惯性权重和学习因子怎么设PSO 的速度更新公式v w·v c1·r1·(pbest - x) c2·r2·(gbest - x) x x vw 是惯性权重控制全局搜索和局部搜索的平衡。w 大偏向全局w 小偏向局部。常见做法是线性递减从 0.9 降到 0.4。c1 和 c2 是学习因子通常都取 2.0也有取 1.5 和 2.0 的。r1、r2 是 [0,1] 随机数。class PSO_RBF: def __init__(self, X, y, n_centers, n_particles30, max_iter100): self.X X self.y y self.n_centers n_centers self.n_particles n_particles self.max_iter max_iter self.dim n_centers * X.shape[1] 1 # 中心 sigma # 初始化粒子位置中心从样本中随机选sigma 初始 1.0 self.X_particles np.random.uniform(-1, 1, (n_particles, self.dim)) self.V np.random.uniform(-0.1, 0.1, (n_particles, self.dim)) self.pbest self.X_particles.copy() self.pbest_fit np.full(n_particles, np.inf) self.gbest None self.gbest_fit np.inf def decode(self, pos): # 前 n_centers*d 维是中心最后一维是 sigma d self.X.shape[1] centers pos[:self.n_centers * d].reshape(self.n_centers, d) sigma abs(pos[-1]) 1e-3 # 保证正数 return centers, sigma def fitness(self, pos): centers, sigma self.decode(pos) dists np.linalg.norm(self.X[:, np.newaxis, :] - centers[np.newaxis, :, :], axis2) H np.exp(-dists ** 2 / (2 * sigma ** 2)) lam 1e-6 try: w np.linalg.solve(H.T H lam * np.eye(self.n_centers), H.T self.y) except np.linalg.LinAlgError: return np.inf pred H w return np.sqrt(np.mean((pred - self.y) ** 2)) def run(self): for it in range(self.max_iter): w_inertia 0.9 - 0.5 * it / self.max_iter # 线性递减 for i in range(self.n_particles): fit self.fitness(self.X_particles[i]) if fit self.pbest_fit[i]: self.pbest_fit[i] fit self.pbest[i] self.X_particles[i].copy() if fit self.gbest_fit: self.gbest_fit fit self.gbest self.X_particles[i].copy() # 更新速度和位置 r1 np.random.rand(self.n_particles, self.dim) r2 np.random.rand(self.n_particles, self.dim) self.V (w_inertia * self.V 2.0 * r1 * (self.pbest - self.X_particles) 2.0 * r2 * (self.gbest - self.X_particles)) self.X_particles self.V return self.gbest, self.gbest_fitdecode把粒子位置拆成中心和 σσ 取绝对值加小量保证正。fitness里每次重新算 H 和最小二乘 w返回 RMSE。run里惯性权重从 0.9 线性降到 0.4c1c22.0。这些是标准设置大多数问题都能跑。5.3 用 PSO 结果训练最终 RBF 并对比PSO 跑完后用最优粒子解码出中心和 σ再在训练集上求一次 w然后预测测试集。pso PSO_RBF(X_train, y_train, n_centers20, n_particles30, max_iter100) gbest, gbest_fit pso.run() centers_opt, sigma_opt pso.decode(gbest) # 用最优中心和 sigma 求输出权值 dists np.linalg.norm(X_train[:, np.newaxis, :] - centers_opt[np.newaxis, :, :], axis2) H np.exp(-dists ** 2 / (2 * sigma_opt ** 2)) w_opt np.linalg.solve(H.T H 1e-6 * np.eye(20), H.T y_train) # 预测 pred_pso scaler_y.inverse_transform(predict_rbf(X_test, centers_opt, sigma_opt, w_opt)) rmse_pso np.sqrt(np.mean((pred_pso - y_test_inv) ** 2)) print(fPSO-RBF 测试集 RMSE: {rmse_pso:.4f}) print(f对比 BP RMSE: {rmse_bp:.4f}, RBF RMSE: {best_rmse:.4f})这段代码把 PSO 优化后的中心和 σ 固定重新求 w然后算测试集 RMSE。通常 PSO-RBF 会比纯 K-means RBF 好一些但提升幅度取决于数据。如果提升不明显可能是 PSO 迭代不够或粒子太少可以加到 50 粒子、200 代。6. 避坑与排查三种网络跑下来最容易翻车的五个地方6.1 现象BP 损失降到某个值就不动了原因学习率太小或隐层节点不够也可能是数据没归一化导致梯度尺度差异大。解决先确认归一化做了然后把学习率从 0.05 升到 0.1 试再把隐层节点从 10 加到 15 或 20。如果还不动检查目标值有没有归一化。6.2 现象RBF 预测结果全是均值附近原因σ 太大所有隐层神经元响应接近H 矩阵接近奇异最小二乘解不稳定。解决把 σ 降到中心点平均距离的 0.5 倍或者增加中心数。同时检查 H 矩阵的条件数np.linalg.cond(H.T H)如果超过 1e10 就要加正则或调 σ。6.3 现象PSO 跑完还不如 K-means RBF原因粒子初始化范围太大或太小或者适应度函数用了测试集导致过拟合。解决粒子初始化范围根据数据归一化后的范围来定通常在 [-1,1] 或 [0,1]。适应度函数必须用训练集或验证集不能用测试集。另外检查 PSO 迭代次数50 代可能不够加到 200 代。6.4 现象三种网络测试集 RMSE 都很大原因数据本身噪声大或特征与目标非线性关系弱也可能是划分不合理。解决先画散点图看特征和目标的关系如果线性关系明显直接用线性回归可能更好。如果噪声大考虑先做平滑或剔除异常值。划分时确保训练集和测试集分布一致可以用train_test_split的stratify参数按目标值分箱。6.5 现象PSO 运行时间太长原因每次适应度评估都要算 H 矩阵和最小二乘粒子多、迭代多时计算量大。解决减少粒子数到 20迭代到 50或者把中心数从 20 降到 10。另外可以把 K-means 的结果作为 PSO 的初始粒子之一加速收敛。7. 进阶技巧用验证集早停和粒子初始化加速 PSO-RBFPSO-RBF 最大的问题是慢。每次迭代要对每个粒子算一遍最小二乘粒子 30、迭代 100、中心 20就是 3000 次矩阵求逆。如果数据维度高这个时间会很难受。我一般用两个技巧来压一是用验证集早停二是用 K-means 结果初始化粒子。验证集早停的做法是从训练集里再分 20% 做验证集PSO 的适应度用验证集 RMSE当连续 10 代 gbest 不下降就停。这样通常 30 到 50 代就能收敛比固定 100 代省一半时间。# 从训练集分验证集 X_tr, X_val, y_tr, y_val train_test_split(X_train, y_train, test_size0.2, random_state42) # 修改 PSO 的 fitness 用验证集 class PSO_RBF_EarlyStop(PSO_RBF): def fitness(self, pos): centers, sigma self.decode(pos) # 用训练集求 w dists_tr np.linalg.norm(self.X[:, np.newaxis, :] - centers[np.newaxis, :, :], axis2) H_tr np.exp(-dists_tr ** 2 / (2 * sigma ** 2)) try: w np.linalg.solve(H_tr.T H_tr 1e-6 * np.eye(self.n_centers), H_tr.T self.y) except np.linalg.LinAlgError: return np.inf # 用验证集算 RMSE dists_val np.linalg.norm(X_val[:, np.newaxis, :] - centers[np.newaxis, :, :], axis2) H_val np.exp(-dists_val ** 2 / (2 * sigma ** 2)) pred_val H_val w return np.sqrt(np.mean((pred_val - y_val) ** 2))这段代码把适应度从训练集 RMSE 换成验证集 RMSE避免过拟合。注意self.X和self.y还是训练集用来求 w验证集只用来评估。这样 PSO 搜出来的中心和 σ 泛化更好。第二个技巧是用 K-means 结果初始化一个粒子。K-means 的中心虽然不是最优但至少是个合理起点把它编码成粒子位置放进初始种群PSO 从这附近开始搜收敛更快。# 用 K-means 初始化一个粒子 kmeans KMeans(n_clusters20, random_state42, n_init10) kmeans.fit(X_train) centers_init kmeans.cluster_centers_ sigma_init np.mean(pdist(centers_init)) * 1.0 pos_init np.concatenate([centers_init.flatten(), [sigma_init]]) # 替换第一个粒子 pso PSO_RBF_EarlyStop(X_tr, y_tr, n_centers20, n_particles30, max_iter100) pso.X_particles[0] pos_init pso.pbest[0] pos_init pso.pbest_fit[0] pso.fitness(pos_init)把 K-means 结果作为第一个粒子同时更新它的 pbest 和适应度。这样 PSO 不会从纯随机开始收敛曲线会明显更陡。我实测过同样 50 代加了初始化的 PSO-RBF 比纯随机初始化的 RMSE 低 10% 到 20%具体看数据。最后一个习惯每次跑完 PSO-RBF把最优中心和 σ 存下来下次换数据时可以拿来当初始值。虽然数据变了中心要重新搜但 σ 的量级通常差不多能省几代迭代。这些技巧不复杂但能让你在调参上少花很多时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表