简介:一份基于深度置信网络(DBN)的时间序列预测实例,主要面向需要利用 DBN 模型处理序列数据的科研人员、研究生或机器学习开发者,通过完整示例演示从数据准备、模型训练到预测输出的流程。包内共183个文件,压缩包整体约31.11MB,核心是123个.m的Matlab源码文件,涵盖网络构建、训练与预测函数;另有7个.mat数据文件用于加载测试数据,7个.fig结果图可直观查看预测效果,4个.md说明文档帮助理解工程结构,并包含少量C语言辅助文件。目前已有257人浏览学习,适合快速复现实验流程。读者可通过该实例掌握DBN在时间序列预测中的建模思路,理解参数设置与数据预处理细节,并借助携带的完整数据与结果图表进行对照验证,便于替换自己的序列数据做进一步拓展实验。
1. DBN时间序列预测:为什么还要用回深度信念网络
即便在lstm时间序列预测python、transformer预测python代码这类教程铺天盖地的今天,深度信念网络(Deep Belief Network, DBN)依然值得专门写一篇。原因不复杂:LSTM 和 Transformer 擅长捕捉长程依赖,但对小样本、强噪声、周期性明显的结构化序列,它们的训练成本和过拟合风险反而更高。DBN 通过逐层无监督预训练,先把序列数据转成更稳定的高层特征,再做回归或分类,这种两阶段训练方式在金融时序预测、银行客户认购产品预测、用户消费预测等场景里往往比端到端深度学习更稳。标题里的test_example_DBN.m更像是一个 MATLAB 版本的示例脚本,里面包含DBN.m、序列数据、DBN预测这几个关键元素。这篇文章就把这条路径说透:先讲清楚 DBN 在时序任务中的位置,再给一套能直接复现的最小代码,最后落到工程上的验证技巧和坑。
2. 深度信念网络处理序列数据前,先把这三件事想清楚
2.1 从RBM到DBN:无监督预训练给时序特征提取留下了什么
DBN 的结构基础是受限玻尔兹曼机(Restricted Boltzmann Machine, RBM)。单个 RBM 可以看成是一个两层网络:可见层接收原始输入,隐藏层学习该输入的分布特征。训练时通过对比散度(Contrastive Divergence, CD)算法逼近对数似然梯度,更新权重。DBN 的做法是把多个 RBM 逐层堆叠:前一个 RBM 的隐藏层输出作为后一个 RBM 的可见层输入。这个过程不需要标签,属于无监督预训练。
对时间序列预测来说,这个过程的价值在于:原始的序列数据往往带有大量局部波动和噪声,直接喂给监督模型容易让模型去拟合这些噪声。DBN 的预训练像是先做了一次“软特征提取”,把相邻时间步之间的相关性通过 RBM 权重固化下来。预训练结束后,再在最后一层接一个回归层或分类层进行有监督微调,模型面对的就是一组比原始输入更干净的抽象特征。
这里有一个关键差异:LSTM 通过门控单元显式建模时间顺序,DBN 则不做这件事。DBN 对时序建模的本质是把序列转成特征,再用前馈网络完成映射。所以用 DBN 做时序预测,输入形态的设计比模型本身更影响效果。这一点很多人第一次跑 DBN 时容易忽略,总以为把序列拉平喂进去就行,结果预测曲线严重滞后。
2.2 把时间序列改造成DBN的输入:滑动窗口与归一化
DBN 的前馈结构要求输入是固定维度的向量。把一串不等长的历史序列变成固定维度,通用做法是滑动窗口(sliding window)。假设原始序列是s[1], s[2], ..., s[T],设定窗口大小window_size = w,预测步长horizon = h,那么每一条训练样本就是X = [s[t-w+1], ..., s[t]],标签是y = s[t+h]。
这里最容易被忽视的是归一化。RBM 的可见层通常假设输入取值在[0, 1]之间,尤其是使用二进制单元时更是如此。对于股票价格、气温、用电量这类数值型序列,必须先做 min-max 归一化:
x_scaled = (x - min) / (max - min)在sklearn里直接用MinMaxScaler即可。注意:MinMaxScaler必须用训练集的min和max去变换验证集和测试集,不能用整个数据集的统计量,否则会造成信息泄漏。下面的代码演示了这一流程。
import numpy as np from sklearn.preprocessing import MinMaxScaler def create_dataset(series, window_size=12, horizon=1): X, y = [], [] for i in range(len(series) - window_size - horizon + 1): X.append(series[i:i + window_size]) y.append(series[i + window_size + horizon - 1]) return np.array(X), np.array(y) # 原始序列 raw = np.array([...]) # 你的原始序列 scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(raw.reshape(-1, 1)).ravel() # 划分训练/测试再滑窗 train_len = int(len(scaled) * 0.8) train_scaled = scaled[:train_len] test_scaled = scaled[train_len - 12:] # 留出12个时间步做窗口 X_train, y_train = create_dataset(train_scaled, window_size=12, horizon=1)这段代码把训练集的前12个时间步作为第一个窗口,标签是第13个时间步的值。test_scaled取train_len - 12开始,是为了保证测试集第一个样本的窗口来自训练集末端,符合真实预测场景。参数horizon=1表示单步预测;想预测未来三步,就把horizon改成3,同时要在创建数据集时调整索引范围。
2.3 DBN与LSTM在时间依赖上的本质差别
很多文章把 DBN 和 LSTM 放在一起对比,但两者的建模哲学完全不同。LSTM 在循环结构中通过遗忘门、输入门和输出门逐时间步传播状态,天然适合捕捉序列内部的先后依赖;DBN 则是一个静态映射:给定一个窗口内的向量,映射到一个目标值。窗口里各时间步之间的相对位置关系,DBN 只能靠输入特征的排列顺序来隐式学习,没有任何跨时间步的参数共享。
这就导致一个常见现象:用 DBN 做单步预测时,结果往往比 LSTM 更平滑,但不太“跟手”,峰值的幅值经常被低估。原因在于预训练阶段 RBM 学到的特征更偏全局分布,对局部的剧烈变化不敏感。如果数据里存在明显的季节项或趋势项,建议在进入 DBN 之前先做差分或季节性拆分。后面第五章会给出具体的差分处理代码。简单地说,DBN 适合那些整体规律性强、允许一定滞后、但特征维度高的序列,不适合频谱成分极复杂的高频交易数据。
3. 从test_example_DBN.m出发:DBN预测的最小可运行实例
3.1 用Python还原一个可跑的DBN时序预测流程
虽然标题里给的是.m文件,但今天在 IT 环境里复现 DBN,更通用的方式是 Python。核心思路不变:先用sklearn的BernoulliRBM做逐层特征提取,再在特征基础上训练一个回归层。严格意义上,多 RBM 堆叠才算 DBN,但为了演示预训练思想,这里用一个 RBM 加回归层的最小版本,跑通之后再堆多层。
from sklearn.neural_network import BernoulliRBM from sklearn.linear_model import LinearRegression from sklearn.pipeline import Pipeline from sklearn.metrics import mean_squared_error def train_dbn_predictor(X_train, y_train, X_test, hidden_dim=64, learning_rate=0.01, n_iter=20, batch_size=32): # 1. 无监督预训练:RBM学习原始特征分布 rbm = BernoulliRBM( n_components=hidden_dim, learning_rate=learning_rate, n_iter=n_iter, batch_size=batch_size, random_state=42 ) # 2. 监督微调:回归器直接作用在RBM的隐藏特征上 regressor = LinearRegression() pipeline = Pipeline([("rbm", rbm), ("regression", regressor)]) pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) return y_pred X_train, y_train = create_dataset(train_scaled, window_size=12, horizon=1) X_test, y_test = create_dataset(test_scaled, window_size=12, horizon=1) y_pred = train_dbn_predictor( X_train, y_train, X_test, hidden_dim=64, learning_rate=0.01, n_iter=20, batch_size=32 ) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False))这段代码里,BernoulliRBM先把输入窗口映射成64维的隐藏特征,这一层是无监督训练,目标是最小化重建误差;接着LinearRegression在隐藏特征上拟合目标值。Pipeline保证 RBM 的输出自动传给回归器,避免手工写中间转换。
hidden_dim决定特征表达的容量,太小会丢失信息,太大会让预训练变得不稳定;learning_rate控制 CD 算法每次更新的步长;n_iter是 CD 迭代轮数;batch_size影响梯度估计的噪声水平。这几个参数是这类模型最重要的调节对象,下一节给出具体的调整思路。
3.2 DBN预测模型的四个必调参数速查
| 参数 | 设置范围 | 对预测结果的影响 | 调参建议 |
|---|---|---|---|
hidden_dim | 16~128 | 隐藏层节点数越多,特征表达越丰富,但过大会导致预训练收敛慢 | 先从小开始,观察验证集 RMSE 变化,如果欠拟合再加 |
learning_rate | 0.001~0.1 | 学习率过大,RBM 权重震荡;过小,预训练迭代速度慢 | 用对数刻度搜索,优先试 0.01 |
n_iter | 10~50 | 迭代次数不足,特征未充分收敛;过多则可能过拟合 | 固定其他参数,绘制损失曲线,在曲线变平处停止 |
batch_size | 16~64 | 小批量能让训练更稳定,但太小会使梯度噪声大 | 样本少时用 16,样本多时用 64 |
这四个参数之间不是完全独立的。hidden_dim 变大时,通常需要适当增大n_iter,让更多特征被充分训练;learning_rate也应该随之略微下调。实际操作中,我习惯先把hidden_dim固定为64,其余三个参数用三次随机搜索,每次只跑 20 轮以节省时间。如果测试集 RMSE 明显下降,再扩大hidden_dim重试。
3.3 读懂test_example_DBN.m里常见的MATLAB写法
在test_example_DBN.m这类示例脚本里,MATLAB 的常见组织方式是先加载序列数据,再调用一个DBN类或函数完成预训练与微调。典型代码结构如下:
% 读取序列数据 data = load('series_data.mat'); series = data.series; % 归一化 series_min = min(series); series_max = max(series); series_norm = (series - series_min) / (series_max - series_min); % 调用DBN训练函数 dbn = DBN([10, 50, 20]); % 输入层10,隐藏层50,输出层20 dbn = dbn.train(series_norm, opts); % opts含学习率、迭代次数 pred = dbn.predict(series_norm);这里的DBN([10, 50, 20])表示网络结构为输入层 10 个节点、隐藏层 50 个节点、输出层 20 个节点。结合前面的滑窗概念,10对应窗口大小,20是回归层维度。如果你只拿到这样一个 MATLAB 文件,迁移到 Python 时只需要抓住三点:网络结构数组、训练参数(学习率、迭代次数)、数据归一化方式。其他诸如绘图、误差计算等代码都是外围辅助,不影响模型主体。
4. 用DBN做序列预测的三个大坑:数据泄漏、滞后与不稳定
4.1 因果滑窗是数据泄漏的重灾区
用train_test_split随机划分时间序列是初学者最常犯的错误。DBN 的每个样本包含一个窗口和它对应的未来值,如果训练集和测试集来自重叠的时间区间,模型相当于提前见过测试窗口的信息。正确做法是把连续的时间范围切开,训练集取前 80% 时间步,测试集取后 20% 时间步。
# 错误做法:随机打乱 # from sklearn.model_selection import train_test_split # X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) # 正确做法:按时间顺序切分 split_idx = int(len(X) * 0.8) X_train_seq, X_test_seq = X[:split_idx], X[split_idx:] y_train_seq, y_test_seq = y[:split_idx], y[split_idx:]注意,创建X和y时用的是整个序列,所以split_idx必须同时作用于X和y,先切分再训练。如果先滑窗生成样本再随机切分,会造成测试样本的时间戳早于训练样本,预测没有实际意义。
4.2 预测曲线滞后一个节拍:根因与缓解
几乎所有序列预测模型都会遇到滞后问题,DBN 更明显。原因是模型在训练时倾向于输出靠近窗口末端的均值,因为这样可以最小化均方误差。给定窗口[v(t-11), ..., v(t)],模型学到的最优预测往往是v(t)附近的值,而不是真正的v(t+h)。这导致预测曲线看起来像是把真值整体右移了一步。
缓解滞后有两个有效手段。第一是增加预测步长,不要预测t+1,而是预测t+3或t+5,虽然误差会变大,但滞后感会减轻。第二是对目标值做差分,让模型预测增量而不是绝对值:
diff_series = np.diff(scaled_series) X_diff, y_diff = create_dataset(diff_series, window_size=12, horizon=1)这里的np.diff把原始序列转成相邻时间步之差。模型预测的是下一步的变化量,预测完成后用pred = last_value + diff_pred恢复原始尺度。由于差分了原始序列,模型的输出不再是窗口末端的近似值,滞后现象会显著缓解。
4.3 多次运行验证稳定性:DBN的随机性比你想象的大
BernoulliRBM有随机初始化,即使随机种子固定,不同批次数据的顺序和采样也会带来结果差异。我在实际项目中观察到,同一套参数跑五次,测试集 RMSE 的变异系数可能达到 5%~10%。如果没有多次运行,很难判断一次结果好坏是模型能力还是运气。
建议至少跑 5 次,并在每次训练前分别设置不同的random_state,然后记录 RMSE 的均值和标准差。
results = [] for seed in range(5): pred_seed, _ = train_dbn_predictor( X_train, y_train, X_test, hidden_dim=64, learning_rate=0.01, n_iter=20, batch_size=32 ) rmse = mean_squared_error(y_test, pred_seed, squared=False) results.append(rmse) print("RMSE mean: %.4f, std: %.4f" % (np.mean(results), np.std(results)))如果标准差大于均值的一半,说明当前参数下模型极不稳定,优先调低learning_rate或增大n_iter。如果依然发散,请检查数据是否包含异常尖峰,RBM 对极端值非常敏感,预训练阶段可能被个别样本拉偏。
5. 滚动预测与误差指标:DBN预测模型上线前的验证组合拳
5.1 用滚动预测模拟真实的上线状态
一次性预测整个测试集的做法,相当于假设未来数据可以同时获得所有历史窗口,实际上线时做不到。更严谨的办法是滚动预测:每次只用当前时间点之前的数据预测下一个点,然后把真实观测值并入历史窗口,继续预测再下一个点。这样能真实检验模型的在线表现,也更容易暴露滞后和累积误差。
def rolling_forecast(model_pipeline, X_initial, y_test, history_len): history = list(X_initial) predictions = [] for t in range(len(y_test)): current_window = np.array(history[-history_len:]).reshape(1, -1) pred = model_pipeline.predict(current_window)[0] predictions.append(pred) # 把真实值加入历史,保持窗口长度 history.append(np.append(current_window[0, 1:], y_test[t])) return np.array(predictions)这里X_initial是测试期开始前的最后一个窗口。每预测完一个时间步,就把该时间步的真实值拼接进历史,形成新的窗口,从而让模型始终基于截止到当前时刻的信息做预测。对比一次性预测和滚动预测的结果差异,如果滚动误差远大于一次性误差,说明模型对远距离依赖较弱,更适合短窗口。
5.2 三个误差指标别只盯RMSE
RMSE 对极端值敏感,在金融时序和消费预测中会被几个异常样本拉高。我至少会再算两个指标:
| 指标 | 公式 | 适用场景 |
|---|---|---|
| SMAPE | mean(2*abs(y-ŷ)/(abs(y)+abs(ŷ))) * 100 | 数值跨多个量级时的稳健比较 |
| MASE | mean(abs(y-ŷ)) / mean(abs(y[t]-y[t-1])) | 与朴素预测(上一期值)比较改善程度 |
| MDA | mean(sign(y[t]-y[t-1]) == sign(ŷ[t]-y[t-1])) | 分类正确率更高,适合方向性预测 |
def mase(y_true, y_pred, y_history): n = len(y_true) naive_errors = np.mean(np.abs(np.diff(y_history[-n-1:]))) return np.mean(np.abs(y_true - y_pred)) / naive_errorsMASE 小于 1 表示模型优于“用上一期值预测当前值”的朴素方法,这是 DBN 预测能否上线的关键门槛。MDA 则在股票预测、银行客户认购这类方向比幅度更重要的任务里有参考价值。
5.3 非平稳序列的快速预处理技巧
如果数据有明显趋势或季节周期,直接做归一化后进入 DBN 会让预训练阶段学到的特征以趋势为主,忽略局部模式。建议先用numpy做差分和一阶季节性提取:
def seasonal_adjust(series, period=24): # 计算每个季节位置的均值,做季节性分解 season_avg = np.array([ series[i::period].mean() for i in range(period) ]) season_pattern = np.tile(season_avg, int(np.ceil(len(series) / period)))[:len(series)] adjusted = series - season_pattern return adjusted, season_patternseasonal_adjust返回去除季节性的序列和季节模式。模型只对adjusted部分训练,预测结果再加上对应的season_pattern分量回归真实值。对于周期为 24 的小时级数据、12 的月度数据,这个处理能让 DBN 的预训练集中在更微观的序列变化上,实际效果往往比直接建模原始序列稳定得多。
本文还有配套的精品资源,点击获取