简介:这份资源面向时间序列预测的初学者与进阶学习者,提供基于RBF径向基神经网络与BP反向传播神经网络两套完整实现方案,可用于股票、销售、气象等趋势预测场景的对比实验。压缩包共5个文件,约349KB,包含2个m脚本文件作为主程序入口与BP预测模块,1个mat数据文件存放预处理后的训练测试矩阵,2个xlsx表格提供真实时间序列原始数据,便于直接运行与替换数据。已有74人学习下载。资源覆盖数据预处理、网络结构设定、超参数调整到均方误差与平均绝对误差对比的完整流程,读者可借此理解RBF隐含层权重固定、仅优化输出层的高效训练机制,以及BP多层前馈网络迭代更新全部权重的差异,并通过实际数据验证两种模型表现,快速搭建可复用的预测框架。
1. 两条路线摆在面前:RBF 和 BP 做时间序列预测,到底该选哪个
手上有一份带时间戳的单变量序列,可能是设备振动幅值、日活曲线、GNSS 站点坐标残差,也可能是某条产线的温度采样。任务很朴素:用过去 N 个点预测下一个点。真到动手时,第一个岔路口就出现了——用 RBF 神经网络还是 BP 神经网络?这两个名字在搜索框里经常一起出现,但它们的脾气完全不同。BP 靠误差反向传播一层层调权重,逼近能力强但训练慢、对初始权重敏感;RBF 用径向基函数做局部响应,训练可以拆成「选中心 + 解线性层」两步,收敛快、对非线性突变响应灵敏,可一旦中心选偏,整个模型就废了。这篇笔记把两条路线都跑一遍,从数据构造、网络搭建、参数设置到踩坑排查,给出能直接复现的代码和判断依据,适合手里有序列数据、想快速拿到一个可用基线的人。
2. 先把时间序列喂成监督样本:滑窗、归一化与训练集切分
2.1 滑窗构造:把一维序列变成 (X, y) 对
时间序列预测的第一步不是建网络,而是把序列改造成监督学习能吃的形状。常见做法是滑动窗口:用前look_back个点作为输入特征,紧邻的下一个点作为标签。假设原始序列是[x0, x1, x2, x3, x4],look_back=3,那么样本就是([x0,x1,x2] -> x3)和([x1,x2,x3] -> x4)。这一步决定了模型能看到多长的历史,是后面所有调参里最该先定下来的量。
import numpy as np def create_dataset(series, look_back=5): """把一维序列转成监督学习样本 series: 一维 np.array look_back: 用过去多少个点预测下一个点 返回 X shape=(样本数, look_back), y shape=(样本数,) """ X, y = [], [] for i in range(len(series) - look_back): X.append(series[i:i + look_back]) y.append(series[i + look_back]) return np.array(X), np.array(y) # 示例:造一段带趋势和噪声的序列 t = np.linspace(0, 20, 400) series = np.sin(t) + 0.1 * t + 0.05 * np.random.randn(len(t)) X, y = create_dataset(series, look_back=5) print(X.shape, y.shape) # (395, 5) (395,)逻辑说明:循环从i=0走到len(series)-look_back-1,每次切出连续look_back个点做输入,紧跟其后的点做标签。参数look_back是核心,太小模型看不到周期,太大样本数骤减且引入冗余。经验上先取序列主周期的 1 到 2 倍,比如日周期数据取 7 或 14,再靠验证集微调。
2.2 归一化:别让量纲差异毁掉训练
神经网络对输入尺度极其敏感。如果序列数值在几千量级,而学习率还是 0.01,梯度会直接炸掉;反过来数值都在 0.001 量级,权重更新又慢得像蜗牛。所以训练前必须归一化,预测后再反归一化还原到原始量纲。最稳妥的是 MinMax 缩放到 [0,1],或者用训练集的均值和标准差做标准化。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) series_scaled = scaler.fit_transform(series.reshape(-1, 1)).flatten() X, y = create_dataset(series_scaled, look_back=5) # 按时间顺序切分,不能打乱 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:]逻辑说明:fit_transform只在训练数据上拟合,测试集用同一个 scaler 做transform,否则就是数据泄漏。时间序列绝不能像普通分类那样随机 shuffle 切分,必须按时间先后切,否则未来信息会漏进训练集,验证指标好看得离谱,上线就翻车。参数split一般取 0.7 到 0.9,序列越短越要留足测试段。
2.3 输入维度与 batch 组织
BP 网络吃的是扁平向量,look_back=5就是 5 维输入;RBF 网络同样接受这个形状。如果后面想接 LSTM 这类循环结构,才需要 reshape 成(样本, 时间步, 特征)。这里两条路线都用二维输入即可。batch size 常见取 16、32、64,样本量小于 500 时用全批量或 16 就够,太大反而让梯度更新次数不足。
3. BP 神经网络路线:结构、训练循环与早停
3.1 网络结构怎么定:层数、神经元与激活函数
BP 神经网络结构图里最常见的是「输入层—若干隐藏层—输出层」全连接堆叠。对时间序列这种中小规模任务,一到两层隐藏层基本够用,每层 16 到 64 个神经元。激活函数隐藏层用 ReLU 或 tanh,输出层做回归必须用线性(不加激活),否则预测值会被压进激活函数的取值范围里出不来。
import torch import torch.nn as nn class BPNet(nn.Module): def __init__(self, input_dim, hidden=32): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) # 回归输出,线性 ) def forward(self, x): return self.net(x).squeeze(-1) model = BPNet(input_dim=5, hidden=32)逻辑说明:nn.Sequential按顺序堆叠两层隐藏层,每层后接 ReLU 引入非线性,最后映射到 1 维输出。参数hidden控制容量,序列规律简单就调小到 16,欠拟合再往上加。注意输出层没有激活函数,这是回归任务和分类任务最容易搞混的地方。
3.2 训练循环:损失、优化器与早停
BP 的训练靠反向传播,损失函数回归用 MSE,优化器 Adam 是默认首选,学习率从 1e-3 起步。关键是要有早停:验证损失连续若干轮不下降就停,避免过拟合,也省时间。
import numpy as np X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) X_test_t = torch.tensor(X_test, dtype=torch.float32) y_test_t = torch.tensor(y_test, dtype=torch.float32) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) best_val = float('inf') patience, wait = 20, 0 best_state = None for epoch in range(500): model.train() optimizer.zero_grad() pred = model(X_train_t) loss = criterion(pred, y_train_t) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_loss = criterion(model(X_test_t), y_test_t).item() if val_loss < best_val: best_val = val_loss best_state = {k: v.clone() for k, v in model.state_dict().items()} wait = 0 else: wait += 1 if wait >= patience: print(f"早停于 epoch {epoch}") break model.load_state_dict(best_state)逻辑说明:每轮先train()前向、算损失、反向、更新;再切eval()在测试集上算验证损失。patience=20表示连续 20 轮没进步就停,best_state保存历史最优权重,最后回滚。参数lr太大震荡、太小收敛慢,1e-3 是稳妥起点;patience太小会误停,太大浪费时间,20 到 50 之间取。
3.3 预测与反归一化
训练完拿到的是 [0,1] 区间的预测值,必须用同一个 scaler 反变换回原始量纲,才能和真实值比较。
model.eval() with torch.no_grad(): pred_scaled = model(X_test_t).numpy().reshape(-1, 1) pred_inv = scaler.inverse_transform(pred_scaled).flatten() y_test_inv = scaler.inverse_transform(y_test.reshape(-1, 1)).flatten() rmse = np.sqrt(np.mean((pred_inv - y_test_inv) ** 2)) print(f"BP 测试集 RMSE: {rmse:.4f}")逻辑说明:inverse_transform把预测和真实标签都还原,再算 RMSE。注意预测值要先 reshape 成(-1,1)才能喂给 scaler。这一步不做,指标会小得离谱,看着漂亮其实毫无意义。
4. RBF 神经网络路线:中心选取与线性层求解
4.1 RBF 和 BP 的本质差别
RBF 神经网络只有三层:输入层、一个径向基隐藏层、线性输出层。隐藏层每个神经元是一个高斯核,对输入空间中某个中心附近的区域产生响应,离中心越远输出越小。它和 BP 最大的区别在于:BP 是全局逼近,每个权重都参与所有样本的调整;RBF 是局部逼近,每个核只管自己附近那块。这带来两个后果——RBF 收敛快、对局部突变敏感,但中心数量和位置选不好,泛化就崩。常见做法是用 KMeans 在训练输入上聚类,把聚类中心直接当 RBF 中心。
4.2 用 KMeans 选中心,再解线性输出层
RBF 的训练可以拆成两步:先用无监督方法定中心和宽度,再用最小二乘或梯度法解输出层权重。这样避开了 BP 那种全局非线性优化,稳定得多。
from sklearn.cluster import KMeans class RBFNet: def __init__(self, n_centers=20, gamma=1.0): self.n_centers = n_centers self.gamma = gamma # 高斯核宽度参数 def _rbf(self, X, centers): # 计算每个样本到每个中心的欧氏距离平方 d2 = np.sum((X[:, None, :] - centers[None, :, :]) ** 2, axis=2) return np.exp(-self.gamma * d2) def fit(self, X, y): km = KMeans(n_clusters=self.n_centers, n_init=10, random_state=42) km.fit(X) self.centers = km.cluster_centers_ H = self._rbf(X, self.centers) # (样本, 中心数) # 最小二乘解输出权重,加偏置 H_aug = np.hstack([H, np.ones((H.shape[0], 1))]) self.W = np.linalg.lstsq(H_aug, y, rcond=None)[0] def predict(self, X): H = self._rbf(X, self.centers) H_aug = np.hstack([H, np.ones((H.shape[0], 1))]) return H_aug @ self.W rbf = RBFNet(n_centers=20, gamma=1.0) rbf.fit(X_train, y_train) pred_rbf = rbf.predict(X_test)逻辑说明:_rbf用广播算出每个样本到每个中心的距离平方,再套高斯核exp(-gamma*d2)。fit里 KMeans 定中心,构造隐层响应矩阵H,加一列常数做偏置,用lstsq一步解出输出权重。参数n_centers是容量旋钮,太小欠拟合,太大过拟合且 KMeans 变慢,一般取 10 到 50;gamma控制核宽度,越大响应越尖锐,通常按中心间平均距离的倒数来估。
4.3 gamma 怎么定:一个能落地的经验公式
gamma是 RBF 最玄学的参数。太大会让每个核只认自己那一个点,测试样本全落在核外,输出趋近偏置;太小则所有核响应都差不多,退化成线性模型。一个可靠起点是用中心之间的平均距离反推。
from scipy.spatial.distance import pdist avg_dist = pdist(rbf.centers).mean() gamma_est = 1.0 / (2 * avg_dist ** 2) print(f"建议 gamma: {gamma_est:.4f}") rbf = RBFNet(n_centers=20, gamma=gamma_est) rbf.fit(X_train, y_train) pred_rbf = rbf.predict(X_test) rmse_rbf = np.sqrt(np.mean((scaler.inverse_transform(pred_rbf.reshape(-1,1)).flatten() - y_test_inv) ** 2)) print(f"RBF 测试集 RMSE: {rmse_rbf:.4f}")逻辑说明:pdist算中心两两距离,取均值后按高斯核标准形式1/(2σ²)估gamma。这个值不是最优,但能避开量级错误,再在它附近做网格搜索即可。RBF 的预测同样要反归一化再算指标,和 BP 保持同一套评估口径。
5. 避坑与排查:这两条路线最容易翻车的地方
5.1 现象:测试集指标好得离谱,上线全错
原因:切分时随机 shuffle 了,未来数据漏进训练集。时间序列的样本之间有强时序依赖,随机切分等于让模型偷看答案。解决:永远按时间顺序切,训练段在前、测试段在后,归一化的 scaler 只在训练段拟合。
5.2 现象:BP 损失不下降,一直卡在高位
原因:学习率过大导致震荡,或输入没归一化导致梯度爆炸。解决:先把数据缩放到 [0,1],学习率降到 1e-3 甚至 1e-4,观察前几十轮损失是否稳定下降。还不行就检查输出层是不是误加了激活函数。
5.3 现象:RBF 预测出来几乎是一条直线
原因:gamma太小,所有核响应趋同,隐层输出没有区分度,模型退化成线性回归。解决:按 4.3 的经验公式重估gamma,或直接对gamma做对数网格搜索,看验证损失在哪一档最低。
5.4 现象:RBF 训练极慢或内存爆掉
原因:n_centers设得太大,隐层响应矩阵变成(样本数, 中心数)的超宽矩阵,KMeans 和最小二乘都吃不消。解决:中心数控制在样本数的十分之一以内,样本上万时改用 mini-batch KMeans,或分批计算响应矩阵。
5.5 现象:两条路线 RMSE 差很多,不知道信谁
原因:评估口径不一致,比如一个反归一化了另一个没有,或者测试段长度不同。解决:固定同一份切分、同一个 scaler、同一个指标函数,把两条路线的预测画在同一张图上对比。通常 RBF 在平滑周期序列上更快更准,BP 在复杂非线性、样本充足时上限更高。
6. 进阶技巧:用残差诊断决定该留哪条路线
跑完两条路线,别急着看 RMSE 谁小就选谁。真正有用的做法是看残差结构。把预测误差按时间画出来,如果 BP 的残差在序列突变处明显放大,说明它没抓住局部动态,这时候 RBF 的局部响应反而更合适;反过来如果 RBF 的残差呈现系统性偏移,说明中心覆盖不足,得加中心或换 BP 全局逼近。我一般会做一张对比表,把两条路线的关键指标和调参代价摆在一起:
| 维度 | BP 神经网络 | RBF 神经网络 |
|---|---|---|
| 训练方式 | 反向传播全局优化 | KMeans 定中心 + 最小二乘 |
| 收敛速度 | 慢,需多轮迭代 | 快,线性层一步解 |
| 关键参数 | 学习率、隐藏层宽度、patience | 中心数、gamma |
| 局部突变响应 | 一般 | 强 |
| 样本需求 | 较多 | 中等 |
| 过拟合风险 | 中,靠早停控制 | 高,中心过多即过拟合 |
一个具体技巧:把 RBF 的预测当作 BP 的输入特征之一,做一次简单堆叠。因为 RBF 捕捉的是局部模式,BP 擅长全局映射,两者互补。实现上就是把rbf.predict(X)的结果拼到原始X后面,再训一个 BP,往往能把 RMSE 再压一截。
# 堆叠:RBF 输出作为额外特征喂给 BP rbf_train_feat = rbf.predict(X_train).reshape(-1, 1) rbf_test_feat = rbf.predict(X_test).reshape(-1, 1) X_train_stack = np.hstack([X_train, rbf_train_feat]) X_test_stack = np.hstack([X_test, rbf_test_feat]) stack_model = BPNet(input_dim=X_train_stack.shape[1], hidden=32) # 后续训练循环与第 3 章一致,输入换成 X_train_stack逻辑说明:把 RBF 的标量预测拼成新的一列特征,输入维度从look_back变成look_back+1。这样 BP 既能看到原始历史窗口,又能看到 RBF 给出的局部估计,相当于让两个模型各出一份意见再融合。参数上 BP 部分沿用原来的设置即可,不需要额外调。
验证这套方案是否真的有效,别只看单次 RMSE。把序列按时间切成三段,做滚动预测:用前两段训练,第三段逐点预测并滚动更新输入窗口,统计多步预测的误差累积曲线。如果堆叠模型在 5 步以上的预测里误差增长明显慢于单模型,才说明它真的学到了东西,而不是在测试集上碰巧。这个验证过程比任何单点指标都更能说明问题。
我自己踩过最深的一个坑,是早期做 RBF 时图省事把gamma设成固定值 1.0,结果序列量纲一变,模型直接失效,排查了大半天才反应过来是核宽度和输入尺度不匹配。从那以后我养成了一个习惯:任何 RBF 方案,先把gamma按中心距离估一遍,再动手调别的。希望帮到你。
本文还有配套的精品资源,点击获取