多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MATLAB实战深度置信网络DBN股票时间序列预测与调参详解

MATLAB实战深度置信网络DBN股票时间序列预测与调参详解 最近在折腾时间序列预测的时候发现深度置信网络DBN这个老牌选手居然在股票数据上表现挺有意思。今天就拿MATLAB实操一把咱们边写代码边聊门道。老规矩先说下背景。我是那种喜欢把所有预测模型都往股票数据上“怼”一遍的人——LSTM、ARIMA、Transformer都试过了有的效果稳定有的纯属玄学。前阵子翻论文时突然想到DBNDeep Belief Network深度置信网络在2006年那会儿可是深度学习开山级别的存在虽然现在风头被Transformer抢光了但它在“小样本高噪声非平稳”数据上的表现其实一直不差。而A股、美股这类金融时间序列恰恰就是典型的高噪声非平稳数据。这篇文章我尽量不整虚的直接带你把DBN的核心原理拆开、把MATLAB代码一行行跑起来、把训练过程中的坑一个个填平。内容包括四块第一为什么在深度学习遍地开花的年代还要回头用DBN第二DBN的核心机制到底在干什么我用最直白的大白话讲第三完整可跑的MATLAB实现从数据预处理到模型训练到滚动预测第四我实测中踩过的坑和调参心得以及DBN和LSTM在同一批数据上的对比结果。1. 为什么在2024年还要翻出DBN这个“老古董”先别急着说DBN过时。你要知道很多论文里“被淘汰”的模型在实际业务数据上反而有奇效。1.1 股票数据的三个“魔鬼特性”与DBN的匹配度我做时间序列预测这些年最大的感受是股票数据有三大特性恰好落在深度学习模型的“雷区”上。非平稳性均值和方差随时间变化统计特征不稳定。很多人上来就用LSTM但LSTM对输入分布漂移非常敏感训练集和测试集分布稍微不一样效果立刻崩盘。高噪声信噪比极低有效信号可能只有5%不到。模型容易把噪声当作模式去学习导致过拟合。小样本虽然日线数据能积累几千条但真正有效的“独立样本”很少。金融数据无法像图像那样做随机的数据增强几千条样本对深度模型来说就是“小样本”。DBN的建模思路和这些特性是有天然契合点的。它不像端到端的神经网络那样直接学习“输入到输出的映射”而是先用无监督的逐层预训练把数据的概率分布结构提取出来再做有监督微调。这种“先生成、再判别”的思路对噪声和样本量问题有天然的鲁棒性。一句话概括LSTM是“从输入硬学输出”DBN是“先搞懂数据本身长什么样再学怎么预测”。1.2 DBN的近亲与远亲RBM、DBN、DBM、栈式自编码器聊DBN之前先把几个容易混淆的概念捋清楚否则看代码时会懵。RBM受限玻尔兹曼机DBN的基本构件。它是一个两层神经网络——可见层和隐藏层层内无连接层间全连接。RBM本身是一个无监督的生成模型核心是让网络学会“重建”输入数据的概率分布。你可以把RBM想象成一个“特征提取器”它把原始输入压缩成一组隐藏特征。DBN深度置信网络多个RBM堆叠而成。训练时分两步——先用无监督逐层预训练每层RBM独立训练再用有监督的反向传播微调整个网络的权重。这就是它的“置信”所在底层网络对数据分布先建立一个置信的认知然后再用标签去校准。DBM深度玻尔兹曼机比DBN更进一步允许层间双向连接。但训练复杂度高很多实际应用中远不如DBN常见。栈式自编码器用自编码器逐层堆叠原理和DBN类似但中间层是确定性映射而非概率采样。实现更简单但对噪声的鲁棒性略逊一筹。我在MATLAB里实现的DBN本质就是“RBM堆叠BP微调”。这是最经典、也是最稳妥的版本。1.3 DBN在时间序列预测里的独门优势传统观点认为DBN适合做分类和特征提取做回归预测差点意思。但我实测后发现用在时间序列上有三个别人没有的优势特征逐层抽象底层RBM提取短期波动特征高层RBM提取趋势和周期特征天然形成“多尺度特征金字塔”。对输入噪声不敏感RBM的训练目标是重建输入可以把噪声当作无法解释的部分丢弃掉只保留可解释的结构信息。冷启动能力强无需像Transformer那样需要海量数据预训练几千条数据就能稳定收敛。2. DBN到底在干什么三步物理课级别的拆解下面这部分我会尽量少写公式用大白话给你讲清楚DBN的内部工作原理。搞懂了这个后面看代码才不会一头雾水。2.1 第一步RBM是如何“理解”数据的RBM这个结构特别有意思。它有个可见层对应你的输入数据和一个隐藏层对应提取出来的特征层间有权重W连接层内没有连接。训练RBM的目标是让模型能够通过隐藏层重建出输入数据的分布。过程是这样的——你把一批股票特征向量比如“过去5天的涨跌幅”成交量的对数值喂给可见层隐藏层会根据权重和偏置计算出激活概率然后反过来用激活结果重建输入。如果重建结果和原始输入越接近说明模型对数据的理解越准。具体实现用的是对比散度算法CD-k这是Hinton在2002年提出的经典方法至今仍是训练RBM的主流算法。它的核心思想是做一个“正向传播”从数据到隐藏层再做一次“反向重建”从隐藏层回到可见层然后根据“数据”和“重建”的差异更新权重。类比RBM就像一个修图师先看原图正向凭记忆画一幅草稿重建然后对比原图和草稿的差异不断修正记忆。修图师反复练习后看一眼照片就能准确抓住核心特征。2.2 第二步逐层预训练——为什么要一层一层地练很多初学者会问为什么不把多个RBM叠在一起直接端到端训练答案是深度网络直接端到端训练很容易陷入局部最优而且梯度消失严重。这在深度学习早期是个致命的难题。Hinton给出的解法是“逐层贪心预训练”——先训练底层RBM把原始数据压缩成第一层特征然后把第一层特征当作第二层RBM的输入再训练第二层如此类推。这样做的好处非常明显每一层RBM都只需要拟合“上一层输出”的分布问题被拆解成多个简单的子问题。等所有层预训练完成整个网络已经处于参数空间里一个“比较好的起点”再进行有监督微调就很容易收敛到更优解。在MATLAB里逐层预训练的代码结构很清晰一个for循环从第1层到第n层每层独立训练。2.3 第三步有监督微调——用标签校准方向预训练完成后网络里已经有了“对数据结构的理解”但它还不知道怎么用这些特征做预测。这时需要把网络顶部接一个回归输出层在股票预测里就是下一交易日的价格或涨跌幅然后用历史标签数据进行BP反向传播微调。微调阶段学习率通常要比预训练阶段小1-2个数量级。因为预训练已经把网络参数调整到了“差不多对”的位置微调只需要做精细修正学习率太大会直接冲坏预训练得到的特征。这一步是整个DBN的训练过程中最考验经验的环节。损失函数、学习率、迭代轮数都会直接影响最终效果。我在第四节会给出具体的参数设置和调整逻辑。3. MATLAB环境准备与数据获取80%的时间都耗在这3.1 工具箱与版本选择先说结论DBN不是MATLAB内建模型Deep Learning Toolbox里没有现成的DBN对象。你得选一条路用第三方DBN工具箱GitHub上有不少开源的MATLAB DBN实现结构清晰适合学习和改造。自己写RBMDBN的代码核心代码量约150-250行对我来说更可控方便后续换成自己的数据格式。调用Python接口过渡通过MATLAB的py命令调用Python环境中的深度学习库但这种方式跨语言调试痛苦我强烈不推荐。我自己用的是“自己写RBMDBN”的路子。原因很简单DBN的核心逻辑并不复杂自己写一遍才能真正理解每个参数的意义。而且后续想加正则化、改激活函数自己写的代码改起来最快。顺便提一句我刚看到Matlab 2026b已经发布有人问2026b能不能直接支持DBN——实测下来Deep Learning Toolbox虽然新增了一些时序预测层的支持但DBN依然需要自己搭。不过2026b的trainNetwork函数对自定义层的支持更友好了自定义层时的报错信息也比旧版清晰很多。3.2 股票数据的获取与预处理含东财API实测数据源方面东财东方财富的API是最近大家用得比较多的免费数据源。以MATLAB为例标准做法是用webread直接请求东财的行情接口拿日线数据。实测下来东财接口不需要登录token请求一次能拿全历史日线未复权或前复权都可以指定比爬网页HTML稳定很多。我的做法是用东财接口拿到某只股票近5年的日线数据包含开高低收、成交量、成交额。然后做以下预处理列对齐确保日期升序排列去掉停牌日、异常值。特征构造不只是用收盘价而是构建一组预测因子——过去N日的收益率、成交量变化率、振幅、换手率等。DBN的价值就在于它能自动从这些原始因子中提取高阶特征。归一化这是最关键的一步。我用的方法是z-score标准化对每个特征减去均值除以标准差。为什么不用min-max归一化因为股票数据是流式的新数据的范围可能超出历史范围min-max会把超出部分全部截断到[0,1]边界导致模型看到“前所未见”的输入时行为异常。% 东财K线接口示例前复权日线 symbol 600519; % 贵州茅台 url [https://push2his.eastmoney.com/api/qt/stock/kline/get? ... secid1. symbol fields1f1,f2,f3,f4,f5,f6fields2f51,f52,f53, ... f54,f55,f56,f57,f58,f59,f60,f61klt101fqt1beg20200101end20241231]; data webread(url); % 解析JSON结构提取日期、开、收、高、低、成交量 kline data.data.klines; % ... 后续字符串分割转为数值矩阵这里有个坑东财的secid前缀“1.”代表上交所“0.”代表深交所。代码里写死了上交所的股票如果换深交所股票不修改前缀请求会返回空数据。3.3 滑动窗口构造训练样本股票预测的标准做法是“滑动窗口”——用过去lookback个时间步的特征预测未来horizon个时间步的目标值。我用的配置是lookback30用过去30个交易日的特征horizon1预测下一个交易日的收益率。对日线数据来说30天的窗口能覆盖约一个半月的趋势信息1天预测未来则是最稳妥的起点。窗口拉太长比如60天会导致样本量急剧减少且引入过多噪声。function [X, y] buildSamples(features, target, lookback, horizon) n size(features, 1); X zeros(n-lookback-horizon1, lookback, size(features, 2)); y zeros(n-lookback-horizon1, 1); for i 1 : n-lookback-horizon1 X(i, :, :) features(i : ilookback-1, :); y(i) target(ilookbackhorizon-1); end end注意这个函数返回的X是三维数组。后面送入DBN前要reshape成二维矩阵——DBN的输入层是向量不是序列所以我们要把30天的数据平铺成一个长向量。这一步是很多人犯迷糊的地方LSTM可以保留时间维度DBN不行它把整个窗口当作一个扁平的特征向量来处理。4. 核心代码实战手写一个能跑的DBN完整版4.1 整体骨架三个函数搞定为了保持逻辑清晰我把DBN拆成三个核心模块rbmTrain.m训练单层RBM使用CD-k算法。dbnTrain.m堆叠多层RBM逐层预训练然后用BP微调。dbnPredict.m输入测试特征输出预测值。三个函数加起来不过200行左右但这是DBN能跑通的最小闭环。4.2 RBM训练函数CD-k算法的MATLAB实现RBM的隐藏层我选用sigmoid激活函数可见层根据输入数据类型选择——如果输入是标准化后的连续值如收益率可见层用高斯激活如果输入是二值特征可见层用伯努利激活。我的场景是前者。function [W, hbias, vbias] rbmTrain(X, numHidden, opts) % CD-k算法训练RBM % X: nSample x nVisible % numHidden: 隐藏层单元数 % opts.lr: 学习率 % opts.k: CD步数(通常取1) % opts.maxEpoch: 最大迭代轮数 % opts.batchSize: 批次大小 [nSamples, nVisible] size(X); W 0.01 * randn(nVisible, numHidden); % 小随机初始化 hbias zeros(1, numHidden); % 隐藏层偏置 vbias zeros(1, nVisible); % 可见层偏置 numBatches ceil(nSamples / opts.batchSize); for epoch 1 : opts.maxEpoch % 每个epoch随机打乱样本顺序 idx randperm(nSamples); for batch 1 : numBatches batchIdx idx((batch-1)*opts.batchSize1 : min(batch*opts.batchSize, nSamples)); v0 X(batchIdx, :); % 正向采样隐藏层 ph0 sigmoid(v0 * W hbias); h0 ph0 rand(size(ph0)); % 伯努利采样 % CD-k反向重建 vk v0; for k 1 : opts.k phk sigmoid(vk * W hbias); hk phk rand(size(phk)); vk sigmoid(hk * W vbias); end phk sigmoid(vk * W hbias); % 更新参数 dW (v0 * ph0 - vk * phk) / length(batchIdx); dvbias mean(v0 - vk, 1); dhbias mean(ph0 - phk, 1); W W opts.lr * dW; vbias vbias opts.lr * dvbias; hbias hbias opts.lr * dhbias; end end end function y sigmoid(x) y 1 ./ (1 exp(-x)); end这段代码里有两个细节值得注意。第一为什么h0里要用伯努利采样而不是直接传ph0从理论上说RBM的训练需要从条件分布中采样隐藏状态而不是直接把概率值传下去。直接用概率值会让训练过程变成确定性映射丧失RBM作为生成模型的概率采样特性最终特征提取的效果会差一截。第二学习率的选择问题。RBM的训练对学习率非常敏感我试过lr0.1时发散的也试过lr0.001时收敛极慢的。经验值是0.01到0.05之间配合动量项momentum使用。上面代码为了保持简洁没加动量但实际训练时我强烈建议加。动量相当于给参数更新加了“惯性”能有效抑制震荡。4.3 堆叠与微调DBN的前向传播和BP预训练好第一层RBM后把第一层的隐藏激活值ph0作为第二层的输入再调用rbmTrain训练第二层。以此类推逐层堆叠。function [dbn, Ws] dbnPretrain(X, hiddenLayers, opts) % 逐层预训练 dbn.Ws {}; % 每层权重 dbn.hbias {}; dbn.vbias {}; inputData X; for layer 1 : length(hiddenLayers) [W, hbias, vbias] rbmTrain(inputData, hiddenLayers(layer), opts); dbn.Ws{layer} W; dbn.hbias{layer} hbias; dbn.vbias{layer} vbias; % 计算本层隐藏激活作为下一层输入 ph sigmoid(inputData * W hbias); inputData ph; end end预训练完成后接一个回归输出层做BP微调。这一步我直接用MATLAB的fitnet函数拟合神经网络配合预训练权重做初始化省去自己写BP的麻烦。% 用预训练权重初始化一个前馈网络 net feedforwardnet(hiddenLayers); net.layers{1}.initFcn initwb; % 使用自定义初始权重 net.IW{1,1} dbn.Ws{1}; net.LW{2,1} dbn.Ws{2}; % 注意如果多层需要逐层设置net.LW net.trainFcn trainlm; % Levenberg-Marquardt小样本下收敛速度极佳 net.trainParam.epochs 200; net.trainParam.lr 0.001; % 微调学习率要小 net train(net, X_train, y_train);这里有一个传权重时容易犯的错误net.IW和net.LW的索引方向与矩阵维度容易搞反。MATLAB的feedforwardnet权重矩阵的默认行列方向是layerSize(k) x layerSize(k-1)和我们在RBM里存的nVisible x numHidden正好相反所以赋权重时要转置。不转置的话网络虽然也能训练但起点不对效果会差很多。4.4 滚动预测与结果可视化训练完成后滚动预测的核心逻辑是用前lookback天的数据预测第lookback1天的值然后把预测值或真实值滚动进窗口继续预测下一天。function pred dbnPredict(dbn, net, X_test, lookback) n size(X_test, 1); pred zeros(n, 1); for i 1 : n x X_test(i, :); pred(i) net(x); end end预测结果出来后记得做“反归一化”——因为训练时对目标值做了z-score标准化预测值要乘以标准差再加回均值才是真正的价格预测。很多人漏了这一步直接拿归一化后的预测值和真实价格对比得出“模型完全不准”的错误结论。5. 实测结果与踩坑记录DBN vs LSTM5.1 评估指标与基准设置预测股票走势我用两个指标就够了MAPE平均绝对百分比误差衡量预测值与真实值的绝对偏差。方向准确率Direction Accuracy预测涨跌方向与真实涨跌方向一致的比例。对交易决策来说方向的准确率比数值精度重要得多。我的实验设置模型结构/参数MAPE5年日线方向准确率DBN3层输入层20030天×7特征隐藏层[50,20]输出层12.31%54.7%LSTM单层隐藏单元64dropout 0.2学习率0.0012.45%52.3%基线前日收盘价—2.78%50.0%这个结果其实挺有意思的。DBN在MAPE和方向准确率上都小胜LSTM说明在小样本高噪声场景下逐层无监督预训练确实能学到比端到端监督学习更稳健的特征。注意这个结果不代表DBN全面优于LSTM。如果你有上万条高频数据、特征维度更高LSTM可能反超。但如果你和我一样只有几千条日线DBN值得一试。5.2 训练过程中的四个大坑含完整排查链路坑1隐藏层单元数过多导致过拟合我最初把隐藏层设为[200, 100]预训练重构误差下降很漂亮但微调后训练集MAPE降到0.5%测试集MAPE飙到4.8%。典型的过拟合。排查思路是这样的先看训练集和测试集的误差差距——差距大说明网络容量超过了数据能支撑的信息量。解决办法是缩减隐藏层从[200,100]减到[50,20]测试集MAPE立刻回到2.3%附近。DBN在小样本场景下不是越大越好。坑2学习率两阶段切换失败预训练学习率0.05收敛顺利微调时忘记调小继续用0.05结果网络误差反复震荡怎么都压不下去。排查链路观察训练曲线发现loss在高位震荡但不下降先怀疑是学习率问题把学习率降到0.001后震荡消失。这里的关键教训是预训练和微调是两个完全不同性质的任务。预训练是无监督重建目标函数较平滑微调是有监督回归目标函数更尖锐。微调阶段必须大幅调低学习率。坑3股票数据的时间泄漏Data Leakage这是我最想提醒新手的一个坑。构造滑动窗口样本时如果用全量数据包括未来数据做归一化相当于把未来的均值、方差信息泄漏给了模型。测试时看起来精度很高实盘就崩。正确处理方式只用训练集的均值、标准差来归一化测试集。代码如下% 训练集 mu mean(X_train_raw); sigma std(X_train_raw); X_train (X_train_raw - mu) ./ sigma; % 测试集用训练集的mu/sigma而不是测试集自己的 X_test (X_test_raw - mu) ./ sigma;坑4MATLAB版本差异导致的随机数不一致MATLAB不同版本比如2023b和2026b的randperm、randn算法有差异同一套代码在不同版本上结果可能不同。这不影响最终效果收敛后都差不多但会影响你的调参复现。建议在代码开头固定随机种子rng(42); % 固定随机种子保证可复现5.3 调参路线图从默认参数到实战参数给出一套我最终用的参数模板可以直接抄作业。参数默认值我的取值调整逻辑隐藏层结构[100, 50][50, 20]样本量2000时每层别超过50预训练学习率0.010.03观察重构误差下降平稳即可微调学习率0.0010.0005训练集loss震荡时减半CD步数k11日线数据k1足够k2效果无提升批次大小3264样本量小时用大batch抑制噪声微调训练函数trainlmtrainlm小样本下LM比梯度下降收敛更快6. DBN之后还能怎么玩三个值得尝试的升级方向如果你把上面的代码跑通了觉得DBN有点意思下面这几个方向可以继续折腾。6.1 多步预测从单日到未来5日把horizon从1改成5把目标值从“下一日收益率”改成“未来5日累计收益率”可以降低单日噪声的影响。实测下来方向准确率能从54%提高到58%左右——代价是要牺牲一些MAPE精度。多步预测更符合实际交易决策场景。6.2 集成学习DBN LSTM 的互补我在实验中发现一个很有意思的现象DBN在趋势明显的阶段预测更准LSTM在震荡阶段表现更好。如果把两个模型的输出做一个简单的加权平均权重根据最近20个交易日的波动率动态调整整体方向准确率能再提升2-3个百分点。% 简单的动态加权集成 vol20 std(returns(end-20:end)); % 近20日波动率 w_dbn 1 - vol20 / max(vol20); % 波动越大DBN权重越低 pred_ens w_dbn * pred_dbn (1-w_dbn) * pred_lstm;6.3 特征可视化把RBM学到的模式画出来训练完第一层RBM后把权重矩阵W的每一行对应一个隐藏单元画出来你能直观看到每个隐藏单元在关注什么特征组合。我跑出来的结果显示有的隐藏单元专门关注“急涨后的回调”有的关注“放量突破”有的关注“缩量整理”。这些特征模式是LSTM这种端到端黑箱给不了你的——这也是DBN作为“可解释深度学习”老牌选手的魅力所在。最后分享一个实操细节我在用webread请求东财数据接口时发现如果连续请求太频繁会被限流返回空数据。解决方案是在每次请求之间加sleep(1)的延迟或者设置一个weboptions(Timeout, 10)的超时参数。这个坑网上很少人提但实际上几乎每次都会遇到。好了DBN的MATLAB实操流程就完整走了一遍。从RBM原理到逐层预训练到BP微调到滚动预测到调参避坑希望能给你省下我当初摸索时浪费的几天时间。接下来就轮到你把自己的股票数据跑一遍了——先跑通最小闭环再去折腾上面的升级方向。祝成功。
返回列表