MATLAB手写DBN实现股票时间序列预测:从RBM原理到微调全流程
发布时间:2026/10/3 14:52:38 作者:尧图编辑部 阅读量:1,286

最近研究时间序列预测时我重新把目光投向了深度置信网络DBN这个“老牌选手”。原本只是抱着复习经典算法的想法没想到在股票日线数据上折腾了一轮MATLAB实操后发现DBN的实际表现比我预想中有意思得多。今天这篇就来完整过一遍从DBN原理拆解、MATLAB手写RBM组件、逐层预训练到有监督微调、再到预测结果评价和避坑经验。全程边写代码边聊门道面向想真正把DBN用在时间序列任务上的读者尤其是那些已经用过LSTM、但在小样本或高频噪声场景下想换一种思路的朋友。1. 为什么都202x年了还在折腾DBN做预测先说结论DBN绝对不是最潮的模型但它身上有几个特性在时间序列预测这件事上依然能打尤其适合股票这类信噪比低、非平稳、样本量还不太大的数据。1.1 先拆一下DBN的骨架RBM堆栈DBN的全称是Deep Belief Network核心结构就是堆叠多个受限玻尔兹曼机Restricted Boltzmann MachineRBM。每个RBM有两层——可视层和隐层同层之间没有连接层间全连接。这种“二分图”结构让它在无监督学习时有一个非常好的性质给定一层节点的状态另一层的条件概率可以直接算出来于是就能用对比散度Contrastive DivergenceCD快速逼近极大似然。多个RBM逐层堆叠之后上一层RBM的隐层输出会当作下一层RBM的输入。预训练完成后再在最顶上接一个回归层或分类层整条网络通过反向传播做有监督微调。换句话说DBN是“无监督预训练 有监督微调”这个范式的鼻祖级实现。现在大家熟悉的很多深度学习套路其实都继承了它的思想。我在MATLAB里实现时最直观的感觉是RBM就像一个个“特征提炼器”第一层RBM学会的是相邻价格波动之间的短期相关性第二层RBM学到的可能就是更抽象的趋势形态。整个过程不需要标签全部是自监督式地从数据中摸规律这在股票数据标签噪声极大的背景下反而比直接端到端训练更稳。1.2 DBN和LSTM在时序任务上的位置差异现在一提时间序列预测大家习惯性就上LSTM。我在之前的项目里也确实大量用过LSTM它的循环结构对时间依赖的建模是天然的。但LSTM也有几个让我头疼的点训练对样本量要求高超参数敏感小数据集上非常容易过拟合。尤其是股票日线数据几十个特征、几百条样本硬上LSTM经常是验证集上看似收敛、实盘预测一塌糊涂。DBN的路线完全不同。它不依赖循环连接而是通过“逐层预训练 展开微调”把原始输入映射到高维抽象特征空间再用顶部的浅层模型完成预测。这种结构在样本量中等、特征维度不高但噪声强的场景下有很强的正则化效果。打个不严谨但形象的比方LSTM像一个死记硬背大量语料的学生数据给够了效果惊艳给少了就开始胡编DBN更像一个先自学了很多基础概念再去做题的学生即使题目难至少不容易懵。当然这不是说DBN要取代LSTM。我的实际经验是如果数据量在几千条以上时序依赖明显且长优先LSTM或Transformer但如果数据只有几百到一千条左右又希望在MATLAB里快速验证一个可解释的深度特征提取流程DBN是一个非常值得加入对比列表的选手。2. 数据准备与特征构造股票预测的坑先从数据开始很多人会把精力全部放在模型代码上结果数据预处理一塌糊涂最后模型效果差还怪算法。股票数据的时间序列预测数据准备这块至少有三大坑未来数据泄漏、非平稳性、特征尺度失控。2.1 数据源与预处理方案这次实操我用的是日线行情数据CSV里有两列必选字段日期和收盘价。为了简化DEMO我没加成交量和技术指标先把纯价格序列的预测能力测出来。如果你希望效果更好后续可以按同样的思路加入开盘、最高、最低、成交量等字段甚至自己算一批RSI、MACD、布林带之类的衍生特征。读取数据用MATLAB的readtable:% 读取CSV假设列名为Date, Close data readtable(stock_daily.csv); prices data.Close; % 肉眼检查缺失值必要时空值用前值填充 prices fillmissing(prices, previous);预处理时最重要的一件事是归一化。我习惯用mapminmax但有一个极其关键的细节归一化参数必须在训练集上计算然后应用到验证集和测试集上。如果你拿全序列统一计算最大最小值再划分训练测试这就是典型的数据泄漏预测结果会虚高实盘必然翻车。我实际写的是这样一段:% 切记先划分再归一化不要在划分前算全量统计量 trainLen round(length(prices) * 0.8); trainData prices(1:trainLen); testData prices(trainLen1:end); % 在训练集上计算归一化参数 [trNorm, ps] mapminmax(trainData, 0, 1); % 用同一组参数归一化测试集 teNorm mapminmax(apply, testData, ps);2.2 滑动窗口构造样本对时间序列预测不是拿全部历史丢进模型而是要用滑动窗口构造输入输出对。比如窗口长度是10预测目标是下一日收盘价那么第1到10天的价格就是一组输入特征第11天的价格就是对应的标签。窗口长度的选择直接影响预测效果。我这次对比了5、10、20三个窗口长度最终10在多数股票数据上综合表现更稳。窗口太短模型看不到短期趋势窗口太长容易把很久以前的噪声也学进去。另外输入特征维度与RBM可视层节点数是对应的窗口长度就决定了第一个RBM可视层大小所以这个参数也要兼顾网络规模。构造样本对的代码function [X, Y] makeSamples(dataNorm, windowLen) n length(dataNorm); X zeros(n - windowLen, windowLen); Y zeros(n - windowLen, 1); for i 1:n - windowLen X(i, :) dataNorm(i:i windowLen - 1); Y(i, :) dataNorm(i windowLen); end end这里有个容易被忽略的细节样本之间是有重叠的。相邻两个样本窗口重叠了windowLen-1个数据点所以训练集内部并不是完全独立同分布。用DBN做无监督预训练问题不大但后续评估时要注意不要对重叠样本做过强的时间交叉验证假设否则指标会显得过于乐观。3. MATLAB手写DBNRBM基础组件与预训练MATLAB里有深度学习工具箱但DBN没有像LSTM那样官方封装好的现成层所以要自己实现。好处是手写一遍RBM之后你对DBN的理解会比调API深得多。3.1 RBM的正向、重构与对比散度更新一个RBM的完整训练循环就三步正向采样、反向重构、用CD准则更新参数。我写代码时没有用那些复杂的加速技巧先把逻辑理清楚最重要function [W, a, b] trainRBM(X, hiddenSize, opts) % X输入数据每一行是一个样本 % hiddenSize隐层节点数 % opts.lr学习率opts.epochs训练轮数opts.kCD步数 [nSamples, visibleSize] size(X); W 0.01 * randn(visibleSize, hiddenSize); a zeros(1, visibleSize); % 可视层偏置 b zeros(1, hiddenSize); % 隐层偏置 batchSize 64; nBatches ceil(nSamples / batchSize); for epoch 1:opts.epochs for bi 1:nBatches idx (bi-1)*batchSize1 : min(bi*batchSize, nSamples); v0 X(idx, :); % 正向根据可视层计算隐层概率并采样隐层状态 hProb sigmoid(v0 * W b); hState hProb rand(size(hProb)); % 重构根据隐层状态重构可视层 vReconProb sigmoid(hState * W a); vReconState vReconProb rand(size(vReconProb)); % 再计算一次重构后隐层概率 hReconProb sigmoid(vReconState * W b); % CD近似梯度 dW (v0 * hProb - vReconState * hReconProb) / size(v0,1); da mean(v0 - vReconState, 1); db mean(hProb - hReconProb, 1); W W opts.lr * dW; a a opts.lr * da; b b opts.lr * db; end % 每轮结束后计算重构误差用于观察训练是否健康 reconErr mean(sum((v0 - vReconState).^2, 2)); if mod(epoch, 20) 0 fprintf(Epoch %d, recon error: %.6f\n, epoch, reconErr); end end end function y sigmoid(x) y 1 ./ (1 exp(-x)); end代码里有几个点值得展开说。第一opts.k我直接设成1这就是经典的CD-1。理论上一阶近似是有偏的但实践下来CD-1又快又稳继续增大k收益非常有限反而训练时间成倍增加。第二权重初始化我用了0.01乘以标准正态随机数。很多人一开始喜欢用大一点的随机初始化但RBM训练对初值很敏感初值太大会让隐层概率一开始就饱和在0或1附近梯度消失基本训不动。第三采样用的是“概率大于随机数”的方式不是直接把概率当输出。这符合RBM的吉布斯采样本质。如果你拿概率直接当状态模型就变成了一个确定性自编码器丢失了玻尔兹曼机的随机采样特性重构质量会下降。3.2 逐层贪婪预训练把第一层输出喂给第二层DBN的预训练是逐层进行的。第一层RBM直接在原始样本上训练训练完之后把原始样本映射成第一层隐层的概率输出这个输出再作为第二层RBM的输入以此类推。我把训练过程封装成这样:function [rbmLayers, H] pretrainDBN(X, layerSizes, opts) % layerSizes 形如 [20, 10]表示两个隐层分别20、10个节点 numLayers length(layerSizes); rbmLayers cell(1, numLayers); currentInput X; for layerIdx 1:numLayers fprintf(Pretraining layer %d...\n, layerIdx); [W, a, b] trainRBM(currentInput, layerSizes(layerIdx), opts); rbmLayers{layerIdx}.W W; rbmLayers{layerIdx}.a a; rbmLayers{layerIdx}.b b; % 将当前层的隐层概率作为下一层的输入 currentInput sigmoid(currentInput * W b); end % 预训练结束后把最后一层隐层输出也返回方便后续接回归层 H currentInput; end这里有一个经验值隐层节点数不是越大越好。我试过把两个隐层都设成50训练时间暴涨重构误差是降下去了但最终有监督微调后的预测误差反而变差。后来我总结了一个规律第一层隐层节点数可以略多于输入维度第二层再递减类似一个“升维再降维”的瓶颈结构。这次用的结构是输入10维隐层[20, 10]效果明显优于[10,10]和[30,30]。预训练过程里要重点留意重构误差的曲线。如果重构误差在头几十轮内快速下降、之后缓慢收敛说明训练正常。如果重构误差震荡剧烈甚至上升多半是学习率太大。我这次用学习率0.01训练轮数100整体比较平稳。4. 微调与预测把DBN从特征提取器变成预测器预训练完成之后DBN目前的角色只是一个特征提取器。要拿它做预测还需要在顶部加一个输出层然后把整个网络展开做有监督微调。4.1 有两种微调路线我各说下适用场景第一种是“特征 浅层模型”路线。把预训练得到的最后一层隐层输出当作特征直接喂给线性回归、岭回归或者SVR。这种做法最省事而且能充分发挥DBN提取非线性特征的能力。我在小样本场景下测过效果甚至比完整微调还稳因为浅层模型不容易在末端引入过多过拟合。第二种是完整反向传播微调。把预训练好的RBM权重当作深度网络的初始权重再加一个带线性激活的输出层然后用梯度下降对整个网络做端到端训练。这种路线更“深度学习”理论上能进一步优化特征表示但对超参数更敏感调不好容易破坏预训练得到的良好初值。实际项目里我一般是先跑第一种快速验证可行性再上第二种追求精度。这次博文给出的是完整微调的实现因为更贴合DBN的原始设定。4.2 完整微调与预测流程展开后的网络结构是这样的输入层10个节点隐层1为20个节点隐层2为10个节点输出层1个节点。隐层激活函数用sigmoid输出层用线性激活。MATLAB里我手写了一个最简的批量梯度下降核心代码% 初始化网络权重 W1 rbmLayers{1}.W; b1 rbmLayers{1}.b; W2 rbmLayers{2}.W; b2 rbmLayers{2}.b; rng(42); W3 0.01 * randn(10, 1); b3 0; % 把预训练输出拼接上标签训练集 % trainData 是训练输入矩阵trainLabel是对应标签 fineTuneEpochs 200; lrFine 0.001; % 微调阶段学习率要小 for epoch 1:fineTuneEpochs % 前向传播 a1 sigmoid(trainData * W1 b1); a2 sigmoid(a1 * W2 b2); pred a2 * W3 b3; % 损失均方误差 loss mean((pred - trainLabel).^2); % 反向传播 delta3 (pred - trainLabel) / length(trainLabel); gradW3 a2 * delta3; gradb3 sum(delta3, 1); delta2 (delta3 * W3) .* (a2 .* (1 - a2)); gradW2 a1 * delta2; gradb2 sum(delta2, 1); delta1 (delta2 * W2) .* (a1 .* (1 - a1)); gradW1 trainData * delta1; gradb1 sum(delta1, 1); % 参数更新 W3 W3 - lrFine * gradW3; b3 b3 - lrFine * gradb3; W2 W2 - lrFine * gradW2; b2 b2 - lrFine * gradb2; W1 W1 - lrFine * gradW1; b1 b1 - lrFine * gradb1; if mod(epoch, 50) 0 fprintf(Fine-tune epoch %d, loss: %.6f\n, epoch, loss); end end这版反向传播代码没有加正则化和动量属于最朴素的框架。但有一个细节我花过不少时间微调阶段的学习率一定要比预训练阶段小。预训练学习率可以用0.01甚至0.05因为无监督目标相对平滑到了端到端微调阶段0.01经常会让损失震荡0.001左右更稳。如果震荡严重还可以考虑在delta计算里加入L2正则项这次为了聚焦主要流程我对纯价格序列没有做太多正则化。预测时测试集经过与训练集相同的归一化、滑动窗口处理后直接走一遍前向传播predNorm sigmoid(sigmoid(testX * W1 b1) * W2 b2) * W3 b3; % 反归一化回原始价格区间 predPrice mapminmax(reverse, predNorm, ps);4.3 结果评估不只盯RMSE还要看方向纯价格预测任务我习惯同时算三个指标均方根误差RMSE、平均绝对百分比误差MAPE和方向准确率DA。方向准确率在股票场景里特别关键因为即使数值误差不小只要涨跌方向判断对策略上就有参考价值。我这次在测试集上的结果大致是RMSE在0.02左右MAPE约1.8%方向准确率57%。坦白说方向准确率谈不上惊艳但相比直接用原始价格训练LSTM的对照组DBN在小样本下的稳定性确实更好尤其是不会出现那种“训练集完美拟合、测试集直线崩坏”的严重过拟合现象。微调之后我还做了一件事把最后一层隐层输出单独拿出来可视化。肉眼能看到隐层节点对不同的价格形态有明显的选择性激活这说明预训练阶段确实学到了有区分度的特征而不是在盲目拟合噪声。5. 常见问题与排错实录这部分是我这次实操中真正踩过坑、花过时间排查的地方整理成速查表希望能帮你少走弯路。问题现象排查思路与解决方案重构误差不降训练多轮后recon error依旧平缓学习率过大或过小建议从0.01开始检查输入归一化是否有效原始价格直接进RBM基本训不动预训练正常但微调崩坏微调损失先降后暴涨微调学习率过大试着降到0.001或更低检查输出层梯度是否过大必要时对W3单独用小学习率测试集指标虚高回测很完美实盘很差疑似数据泄漏重点检查归一化参数是否用了全序列统计量、滑动窗口构造时是否跨越了训练测试边界隐层节点数怎么定尝试多个方案效果都一般建议用“升维再降维”的瓶颈结构首层隐层略大于输入维度后续逐层递减不要盲目堆大隐层过拟合明显训练集RMSE远低于测试集增加L2正则项、微调时引入早停、把预训练层数减少一层股票数据信号弱网络容量要克制度高MATLAB报编译错trainRBM函数运行时报编译器相关错误检查MATLAB是否安装了可用的MEX编译器运行mex -setup选择已安装的MinGW-w64编译器即可License报错安装后启动报MathWorks licensing error确认license文件中的HostID与当前机器一致配置环境变量或删除旧的license缓存文件后重启除表格里的内容我再单独提示一个容易闷头踩的坑RBM输入数据的尺度问题。我第一次跑的时候直接拿原始价格序列进了RBM结果重构损失大得像天文数字隐层概率几乎全是0或1整个预训练形同虚设。加了mapminmax归一化之后才算正常。所以凡是看到重构误差不下降第一反应不是调网络结构而是回头看数据是不是归到0-1区间了。还有一点是关于随机种子的。RBM的参数初始化和采样过程都涉及随机性同一份代码在不同随机种子下结果可能差异明显。复现实验或者对比两个方案时务必先用rng固定种子。我这次在预训练、微调和最终预测前都固定了随机种子才保证前后几轮结果可比。至于MATLAB环境层面的问题热词里很多人搜安装、License、编译器相关的内容。我的建议是在开始手写DBN之前先花十分钟把运行环境整干净。装好合适的MinGW-w64编译器确认license没问题再把工作路径设置到项目目录。环境问题虽然和算法无关但排查起来极其消耗耐心特别是训练跑到一半报编译器错真的让人血压升高。我自己的习惯是每台新机器上跑任何深度学习代码前先运行一段最简单的for i1:10, fprintf(%d\n,i); end把基本环境先验证一遍再开始写项目代码。6. 如果让我再做一次我会这样调整这次用MATLAB完整跑通DBN股票时间序列预测总体的感受是DBN不是一个“看起来很美”的模型它是真的能在大噪声小样本的时序数据里稳定提取特征。但它的优势也并不是精度碾压而是“不容易翻车”和“训练过程可控”。如果你正在做的项目也用到了股票、流量、能耗这类实际场景数据我建议不要跳过DBN这个经典方案至少加入对比基线里。如果我再做一次大概率会在两个地方做调整。第一在微调阶段加入L2正则化和早停机制把模型对噪声的敏感度再压一压。第二把输入特征从纯价格扩展成“价格 成交量 技术指标”的多维组合观察DBN在特征丰富场景下的表现是否会有明显提升。手上还有一批小时级别的数据后面我会继续在这个框架上试一试。到时候有新发现再回来和大家分享。