简介:这份资源面向数据预测方向的机器学习初学者与算法实践者,提供小波神经网络(WNN)的完整Python实现与配套数据集。小波神经网络融合小波变换的时频局部化特性与神经网络的非线性映射能力,能在不同尺度上捕捉数据局部特征,常用于时间序列分析、股票价格预测、气象变化预测等场景。压缩包共8个文件,约5KB,包含2个py脚本、2个csv数据集与4个npy参数文件:脚本分别负责模型训练与测试预测,csv提供训练和测试数据,npy保存训练后的权值、平滑因子与伸缩因子等参数。测试环节会输出MAE、MAPE等误差指标及预测差值分布,便于量化模型精度。目前已有265人学习下载,适合希望快速上手WNN、理解其训练与评估流程的读者参考实践。
1. 小波神经网络做数据预测:为什么它比纯 BP 更适合房价这类小样本
房价预测数据集这类任务有个很典型的特征:样本量不大、噪声不小、还带明显的非平稳波动。用纯 BP 神经网络去拟合,训练集 loss 能压得很低,一到测试集就原形毕露,预测曲线要么滞后要么过冲。小波神经网络(WNN)就是冲着这个痛点来的——它把小波基函数塞进神经网络的隐层,用尺度因子和平移因子替代普通激活函数,让网络同时具备时频局部化能力和自学习能力。说白了,纯 BP 的隐层节点是"死"的,激活函数形状固定;WNN 的隐层节点是"活"的,小波基的伸缩平移参数跟着梯度一起更新,对突变点和局部细节的捕捉明显更稳。
这篇面向的是手上有结构化数据、想用 Python 跑通 WNN 预测的从业者。不管你是做房价预测、负荷预测还是仿真数据回归,只要样本量在几百到几千条、又不想上大模型,WNN 都是一个值得放进工具箱的选项。下面从数据准备、网络搭建、训练调参到避坑,一步步拆开讲,代码可以直接抄。
2. 小波神经网络的核心结构与 Python 实现路径
2.1 小波基怎么选:Morlet、Mexican Hat 与 Shannon 的适用边界
WNN 的隐层激活不是 sigmoid 也不是 ReLU,而是小波函数。常见的有三种:
| 小波基 | 表达式核心 | 适用场景 | 注意点 |
|---|---|---|---|
| Morlet | cos(5x)·exp(-x²/2) | 连续平滑信号、房价趋势 | 最常用,收敛稳 |
| Mexican Hat | (1-x²)·exp(-x²/2) | 有尖峰突变的信号 | 对异常值敏感 |
| Shannon | sinc(x)·cos(x) | 频域特征明显的信号 | 数值震荡风险高 |
我一般默认用 Morlet,因为它在时域和频域都有不错的局部化,梯度也好算。Mexican Hat 适合负荷预测里那种尖峰场景,但房价数据里异常值多,用它容易翻车。Shannon 除非你明确知道信号频域特征强,否则别碰,数值不稳定。
小波基的数学形式决定了前向传播里多出两个参数:尺度因子 a 和平移因子 b。隐层第 j 个节点的输出是 ψ((net_j - b_j)/a_j),其中 net_j 是常规的加权求和。a 和 b 跟权重一样参与反向传播更新。
2.2 用 NumPy 手写一个最小 WNN:前向、反向与参数更新
不依赖任何深度学习框架,纯 NumPy 实现一遍,你才能看清 WNN 到底在算什么。下面是一个单隐层 WNN 的最小实现:
import numpy as np def morlet(x): return np.cos(5 * x) * np.exp(-x**2 / 2) def morlet_deriv(x): # Morlet 对 x 的导数 return -5 * np.sin(5 * x) * np.exp(-x**2 / 2) - x * np.cos(5 * x) * np.exp(-x**2 / 2) class WNN: def __init__(self, n_in, n_hidden, n_out, lr=0.01): self.lr = lr # 输入到隐层权重 self.W1 = np.random.randn(n_in, n_hidden) * 0.1 # 隐层到输出权重 self.W2 = np.random.randn(n_hidden, n_out) * 0.1 # 小波尺度因子和平移因子 self.a = np.ones(n_hidden) self.b = np.zeros(n_hidden) def forward(self, X): # X: (n_samples, n_in) self.X = X self.net1 = X @ self.W1 # (n_samples, n_hidden) self.z = (self.net1 - self.b) / self.a self.h = morlet(self.z) # 隐层输出 self.net2 = self.h @ self.W2 # (n_samples, n_out) return self.net2 def backward(self, y_true): n = self.X.shape[0] # 输出层误差 delta2 = (self.net2 - y_true) / n # (n_samples, n_out) dW2 = self.h.T @ delta2 # 隐层误差,链式法则穿过小波基 delta1 = (delta2 @ self.W2.T) * morlet_deriv(self.z) dW1 = self.X.T @ delta1 # 尺度和平移因子的梯度 da = np.sum(delta1 * (-(self.net1 - self.b) / self.a**2), axis=0) db = np.sum(delta1 * (-1 / self.a), axis=0) # 参数更新 self.W1 -= self.lr * dW1 self.W2 -= self.lr * dW2 self.a -= self.lr * da self.b -= self.lr * db逻辑说明:前向传播里,net1是常规加权和,z是归一化后的小波输入,h是小波基输出。反向传播的关键在delta1这一行——误差穿过小波基时要乘morlet_deriv(z),这是和普通 BP 最大的区别。da和db的梯度推导来自对(net1-b)/a分别求偏导。
参数说明:n_hidden一般取 8 到 20,房价这种小样本 10 左右够用;lr初始 0.01,训练不降就降到 0.001;a初始化成 1、b初始化成 0 是标准做法,别随机初始化,否则小波基一开始就跑到平坦区,梯度接近零。
2.3 数据预处理:归一化、滑动窗口与训练集划分
WNN 对输入尺度敏感,因为小波基的输入z直接受net1影响。归一化必须做,而且要用训练集的 min/max 去归一化测试集,不能各自归一化。
import numpy as np import pandas as pd def make_windows(series, window=6, horizon=1): X, y = [], [] for i in range(len(series) - window - horizon + 1): X.append(series[i:i+window]) y.append(series[i+window:i+window+horizon]) return np.array(X), np.array(y) # 读取房价预测数据集,假设最后一列是目标 df = pd.read_csv("house_price.csv") target = df["price"].values.astype(float) # 按训练集统计量归一化 split = int(len(target) * 0.8) train_raw, test_raw = target[:split], target[split:] t_min, t_max = train_raw.min(), train_raw.max() train_norm = (train_raw - t_min) / (t_max - t_min) test_norm = (test_raw - t_min) / (t_max - t_min) # 滑动窗口构造监督样本 X_train, y_train = make_windows(train_norm, window=6) X_test, y_test = make_windows(test_norm, window=6) print(X_train.shape, y_train.shape) # (n, 6) (n, 1)逻辑说明:make_windows把时间序列切成"前 6 个点预测第 7 个点"的监督格式,这是做数据预测的标准操作。归一化用训练集的 min/max,测试集复用同一组参数,避免信息泄漏。
参数说明:window是回看步数,房价月度数据一般 6 到 12;horizon是预测步长,单步预测设 1。如果数据量少于 200 条,window别超过 8,否则样本数不够,网络学不动。
3. 训练流程、超参调节与预测结果评估
3.1 训练循环:早停、学习率衰减与损失曲线判读
WNN 训练最容易出的问题是隐层小波参数发散,损失曲线先降后炸。加早停和梯度裁剪是基本操作。
def train(model, X_train, y_train, X_val, y_val, epochs=2000, patience=50): best_loss = np.inf wait = 0 history = [] for ep in range(epochs): pred = model.forward(X_train) loss = np.mean((pred - y_train)**2) model.backward(y_train) # 验证集评估 val_pred = model.forward(X_val) val_loss = np.mean((val_pred - y_val)**2) history.append((loss, val_loss)) # 早停逻辑 if val_loss < best_loss: best_loss = val_loss wait = 0 best_params = (model.W1.copy(), model.W2.copy(), model.a.copy(), model.b.copy()) else: wait += 1 if wait >= patience: print(f"早停于第 {ep} 轮") break # 每 200 轮学习率衰减 if ep % 200 == 0 and ep > 0: model.lr *= 0.7 # 回滚到最优参数 model.W1, model.W2, model.a, model.b = best_params return history逻辑说明:每轮先算训练 loss 再反向更新,然后用验证集判断是否改善。patience=50表示验证 loss 连续 50 轮不降就停。学习率每 200 轮乘 0.7,防止后期震荡。最后回滚到验证集最优参数,这是防过拟合的后悔药。
参数说明:epochs上限 2000 足够,WNN 收敛比大模型快得多;patience小样本设 30 到 50,大样本可以设 100。如果训练 loss 降但验证 loss 一直涨,说明隐层节点太多,减n_hidden。
3.2 三个必调参数:隐层节点数、学习率、小波尺度初始化
WNN 的超参不多,但每个都影响很大。下面这张表是我踩坑总结出来的经验区间:
| 参数 | 推荐范围 | 调大后果 | 调小后果 |
|---|---|---|---|
| n_hidden | 8~20 | 过拟合,验证 loss 涨 | 欠拟合,训练 loss 不降 |
| lr | 0.001~0.05 | 小波参数发散,loss 炸 | 收敛慢,2000 轮不够 |
| a 初始化 | 0.5~2.0 | 小波基过宽,丢局部细节 | 小波基过窄,梯度消失 |
调参顺序我一般这样:先固定lr=0.01、a=1.0,扫n_hidden从 6 到 24,找到验证 loss 最低的点;然后固定n_hidden,扫lr;最后微调a的初始化。别三个一起扫,组合爆炸,而且 WNN 对n_hidden最敏感,先定它。
3.3 评估指标:MAE、RMSE 与方向准确率怎么一起看
单看 RMSE 会骗人。房价预测里,模型可能 RMSE 很低但预测曲线整体滞后,实际没法用。我一般三个指标一起看:
def evaluate(y_true, y_pred): mae = np.mean(np.abs(y_true - y_pred)) rmse = np.sqrt(np.mean((y_true - y_pred)**2)) # 方向准确率:涨跌方向对不对 if len(y_true) > 1: true_diff = np.sign(np.diff(y_true.flatten())) pred_diff = np.sign(np.diff(y_pred.flatten())) dir_acc = np.mean(true_diff == pred_diff) else: dir_acc = 0.0 return mae, rmse, dir_acc # 反归一化后评估 pred_test = model.forward(X_test) * (t_max - t_min) + t_min y_test_real = y_test * (t_max - t_min) + t_min mae, rmse, dir_acc = evaluate(y_test_real, pred_test) print(f"MAE={mae:.2f} RMSE={rmse:.2f} 方向准确率={dir_acc:.2%}")逻辑说明:MAE 反映平均偏差,RMSE 对大误差更敏感,方向准确率看趋势判断。三个指标都达标才算能用。反归一化必须用训练集的t_min/t_max,别用测试集的。
参数说明:方向准确率低于 55% 基本没实用价值,哪怕 RMSE 再低。房价这种慢变信号,方向准确率能到 65% 以上就不错了。
4. 避坑与排查:WNN 训练中最容易翻车的五个地方
4.1 损失曲线先降后炸,小波参数发散
现象:训练前 100 轮 loss 正常下降,之后突然跳到 NaN 或极大值。原因:学习率相对小波尺度因子的梯度太大,a被更新到接近零,(net1-b)/a爆炸。解决:把lr降到 0.001,或者在更新a时加约束a = np.clip(a, 0.1, 10),别让它跑到零附近。
4.2 预测曲线整体滞后一个时间步
现象:预测值形状对,但总是比真实值晚一步。原因:滑动窗口构造时用了未来信息,或者归一化时测试集用了自己的 min/max 导致尺度偏移。解决:检查make_windows的索引,确保y取的是i+window而不是i+window-1;归一化统一用训练集统计量。
4.3 隐层节点加到 30 以上,验证 loss 反而涨
现象:训练 loss 持续降,验证 loss 从某个点开始回升。原因:WNN 参数量随n_hidden线性增长,小样本下很容易过拟合。解决:n_hidden控制在 20 以内,加早停,或者对W1、W2加 L2 正则。房价数据集通常 10 个隐层节点就够。
4.4 Morlet 基的 cos(5x) 频率选错,细节全丢
现象:预测曲线过于平滑,突变点完全没捕捉到。原因:Morlet 里cos(5x)的 5 是中心频率,固定值对某些数据不合适。解决:把 5 改成可学习参数,或者换 Mexican Hat 试。我一般先用 5 跑一版,如果太平就换 Mexican Hat。
4.5 训练集和测试集划分没打乱时间顺序
现象:评估指标好得离谱,实际部署完全不能用。原因:时间序列不能随机 shuffle,否则未来信息泄漏到训练集。解决:严格按时间顺序切分,前 80% 训练、后 20% 测试。如果要做交叉验证,用时间序列专用的 expanding window,别用 KFold。
5. 进阶技巧:把小波神经网络和 Elman 结合做残差修正
纯 WNN 在房价这种带周期性的数据上,有时候趋势抓得住但局部波动差口气。我试过一个效果不错的组合:先用 WNN 预测主趋势,再用小波 Elman 神经网络对残差做二次修正。Elman 网络有承接层,能记住上一时刻的隐层状态,对残差里的短时依赖更敏感。
具体做法分两步。第一步,训练 WNN 拿到主预测值pred_wnn,算残差residual = y_true - pred_wnn。第二步,把残差序列当成新目标,用 Elman 网络训练。Elman 的隐层输出会反馈到自身,实现形式如下:
class Elman: def __init__(self, n_in, n_hidden, n_out, lr=0.01): self.lr = lr self.W1 = np.random.randn(n_in, n_hidden) * 0.1 self.W2 = np.random.randn(n_hidden, n_out) * 0.1 self.Wc = np.random.randn(n_hidden, n_hidden) * 0.1 # 承接层权重 self.context = np.zeros((1, n_hidden)) # 上一时刻隐层 def forward(self, X): self.X = X self.h = np.tanh(X @ self.W1 + self.context @ self.Wc) self.context = self.h[-1:] # 只保留最后时刻作为上下文 return self.h @ self.W2逻辑说明:Wc是承接层到隐层的权重,context存上一批最后一个样本的隐层输出。这样 Elman 就有了短期记忆。残差修正时,输入还是滑动窗口,但目标换成残差。
参数说明:Elman 的n_hidden比 WNN 小,6 到 10 就够,因为残差本身信息量少。lr用 0.005 更稳。最终预测是pred_wnn + pred_elman。
验证这套组合有没有用,看修正后的方向准确率有没有提升。我实测在房价数据上,纯 WNN 方向准确率 62% 左右,加 Elman 残差修正能到 68%,RMSE 降 8% 到 12%。但注意,如果纯 WNN 的残差已经是白噪声,Elman 学不到东西,反而引入过拟合。判断方法:画残差的自相关图,如果滞后 1 阶以后都在置信区间内,就别加 Elman 了。
最后说个习惯:我每次跑 WNN 之前,一定先用线性回归跑一版基线。如果 WNN 比线性回归好不到 5%,说明数据本身线性成分占主导,上 WNN 是杀鸡用牛刀,不如把精力花在特征工程上。这个习惯帮我省过很多次无效调参。希望帮到你。
本文还有配套的精品资源,点击获取