最近在折腾时间序列预测的时候,发现深度置信网络(DBN)这个老牌选手居然在股票数据上表现挺有意思。今天就拿MATLAB实操一把,咱们边写代码边聊门道。
先说清楚我为什么回头看DBN。现在一提深度学习做预测,大家条件反射就是LSTM、Transformer这些时序模型,再不然就是TCN。但DBN这种2006年前后的思路,放在股票这类噪声极大、非平稳性极强的数据上,反而有它独特的优势:它先用逐层无监督预训练把数据的分布结构摸了一遍,再来做回归拟合,这种“先理解再预测”的思路,在处理金融数据这种信噪比极低(大概就0.01到0.001这个量级)的场景时,比直接从零开始端到端训练要稳得多。
这篇文章不是什么学术综述,就是一个能直接跑的MATLAB实操记录。我会把DBN拆成RBM堆叠和反向微调两段来讲,给出完整的代码骨架、参数设置思路,以及我在实验里踩过的那些坑。适合谁看?适合已经会用MATLAB跑机器学习、但对DBN不太熟,想在股票或其他金融时序数据上试试水的人。如果你是纯小白,RBM的能量函数、对比散度这些概念我也会用大白话解释。这里面没有“稳定盈利”的圣杯,但有可以复现的实验框架和评估方法,这一点我会在最后特别强调。
1. 深度置信网络的核心思路:为什么它能处理股票数据
在动手写代码之前,得先把DBN的原理搞清楚,不然参数调起来就是盲人摸象。
1.1 受限玻尔兹曼机到底在学什么
DBN堆叠的基本单元叫受限玻尔兹曼机(RBM)。RBM是一个概率图模型,它有两层:可见层和隐藏层。同一层内部的节点没有连接,只有层与层之间有全连接。这个结构意味着,在给定可见层状态的情况下,隐藏层各个节点的激活状态是相互独立的;反过来也一样。这个独立性是RBM能高效训练的关键。
那RBM在学什么?它在学一个概率分布。具体点说,它在学习输入数据的分布。举个例子,你把股票过去20天的收益率序列当成一个样本喂进去,RBM会尝试用一个由隐藏层节点表示的模式来“解释”这个样本。每一组可见层和隐藏层的状态组合都有一个能量值,能量越低,这个组合出现的概率就越高。训练RBM的过程,就是调整权重和偏置,让模型给真实数据样本分配高概率,给随机噪声分配低概率。
不过你不需要真的去计算那个概率分布,因为归一化因子(配分函数)几乎不可能精确计算。实际操作中用的是对比散度算法:拿真实数据算一遍隐藏层状态,然后从这个状态反推重构出可见层,再用重构数据和原始数据的差异来更新权重。你看,生活化的类比就是一个“编码再解码”的过程:先压缩成隐藏特征,再从隐藏特征还原原始输入,还原的误差越小,说明隐藏特征提取得越好。这种无监督的逐层预训练方式,正是DBN处理高噪声数据时的底气来源。
1.2 为什么“预训练+微调”这条路对时序预测有效
传统神经网络训练时,最头疼的问题是深度结构带来的梯度消失和局部最优。你从随机初始化开始反向传播,前面几层的权重往往更新得很慢,最后整个网络就没有真正“深”起来。DBN的思路不一样:它在做反向传播之前,先用无监督的方式把每一层都单独初始化一遍——这相当于给网络找了一个好的起点。
放到股票数据上,这意味着什么?股票数据有几个让模型头疼的特质:非平稳、噪声强、信噪比低、特征之间有复杂的非线性关系。如果你直接拿随机初始化的深度网络去拟合,大概率会被噪声带着跑,在训练集上看似学到了规律,实则是记住了噪声。DBN的逐层预训练相当于在做“特征筛选”:RBM每一层只保留能够稳定重构输入的那部分信息,噪声在重构过程中会被过滤掉一部分。等预训练完成后,再用回归目标做整体微调,这时候网络已经站在一个相对合理的参数空间里,寻优过程会更加平滑。
另一个好处是数据效率更高。在样本量有限的情况下,特别是A股个股数据往往只有几千个交易日,没办法支撑一个从零开始训练的深度网络。DBN的先验结构让它在样本量不充裕时也有一战之力。所以从设计意图上看,DBN并不是最潮的模型,但它用“预训练”这个思路,精准命中了金融时序预测的痛点:数据少、噪声大、特征复杂。
2. MATLAB环境下的DBN实现:从数据到网络搭建
MATLAB本身没有内置DBN工具箱,但它的Deep Learning Toolbox提供了一些底层构件,我们可以用这些来拼出完整训练流程。
2.1 训练数据的构造方法
股票预测第一步永远是构造样本。我习惯的做法是:用过去L天的收益率序列作为输入特征,预测未来H天的累计收益率。这里的L和H是自己定的,先固定一个L=20、H=5,后面再讨论这两个参数的影响。
特征的构造逻辑是,不要直接用原始价格,一定要做变换。常用的几个变换包括:日收益率(收盘价的一阶差分再除以前一日收盘价)、对数收益(log(今日收盘/昨日收盘))、以及与指数的相对强弱(个股收益减去大盘收益)。我实测下来,对数收益率的统计性质比简单收益率略好一些,因为它能让收益分布更接近正态,对RBM这种基于能量模型的网络更友好。
还有一个关键的操作:样本切分时不能用随机打乱,必须按时间顺序。随机打乱会让模型“偷看”到未来数据,在时序预测里属于信息泄露,会导致你对模型的评估严重失真。假设你的数据共2000个交易日,前1600个做训练,中间200个做验证,最后200个做测试,严格按这个顺序来。用训练集的均值和标准差做标准化,验证集和测试集也沿用训练集的统计量,这一点容易写错,一旦用全量数据的统计量,又属于信息泄露。建议在MATLAB里写成变量保存,比如trainMu和trainSigma,处理验证和测试数据时直接复用。
构造样本时,每个样本是L=20天的连续窗口。为了加快计算,可以一次性生成一个大的特征矩阵:
% 输入数据:priceLog返回对数价格序列,长度为N nSample = N - L - H + 1; X = zeros(nSample, L); Y = zeros(nSample, 1); for i = 1:nSample X(i, :) = logRet(i:i+L-1)'; % 预测目标:未来H天的累计对数收益 Y(i) = sum(logRet(i+L : i+L+H-1)); end注意,如果全部用循环,数据量大了会很慢。建议预先分配好X和Y矩阵的空间(上面代码已做),MATLAB里预先分配能提效好几倍。之后再按时间顺序切分训练集和测试集,用trainMu和trainSigma做标准化。
2.2 基于MATLAB手写RBM训练过程
MATLAB Deep Learning Toolbox自带了RBM节点,但说实话用起来不够灵活,尤其是你后面想修改学习率衰减策略或权重初始化方式时。我建议自己写一个轻量的RBM训练函数,代码不复杂,但灵活性大幅提升。
一个标准的RBM训练循环包含:权重初始化、Gibbs采样(在实际代码里是对比散度)、重构误差计算。核心代码可以这样组织:
function [W, vb, hb] = trainRBM(X, nHidden, opts) % X为输入数据,每行一个样本,列数为可见层节点数 nVisible = size(X, 2); nSample = size(X, 1); lr = opts.lr; % 学习率 epoch = opts.epoch; % 迭代轮数 batch = opts.batch; % 批大小 % 权重和偏置初始化为小随机数 W = 0.01 * randn(nVisible, nHidden); vb = zeros(1, nVisible); hb = zeros(1, nHidden); nBatch = ceil(nSample / batch); for e = 1:epoch % 每个epoch打乱数据顺序 idx = randperm(nSample); for b = 1:nBatch % 取一个小批量样本 batchIdx = idx((b-1)*batch+1 : min(b*batch, nSample)); v0 = X(batchIdx, :); % 正向计算隐藏层条件概率 ph = sigmoid(v0 * W + hb); h0 = double(rand(size(ph)) < ph); % 采样隐藏状态 % 反向重构 pv = sigmoid(h0 * W' + vb); v1 = double(rand(size(pv)) < pv); % 采样重构可见层 ph1 = sigmoid(v1 * W + hb); % 对比散度更新权值 dW = (v0' * h0 - v1' * ph1) / size(v0, 1); dvb = mean(v0 - v1); dhb = mean(h0 - ph1); % 加上权重衰减项,抑制过拟合 W = W + lr * (dW - 0.0002 * W); vb = vb + lr * dvb; hb = hb + lr * dhb; end % 记录重构误差 reconErr(e) = mean(sum((X - sigmoid(sigmoid(X * W + hb) * W' + vb)).^2, 2)); end end这里的原理在做什么?v0到h0是“编码”,h0到v1是“解码”,然后v1到ph1是再一次“编码”。对比散度就是让模型将原始输入产生的隐藏状态与重构输入产生的隐藏状态作比较。更新量等于两者的差异,这个差异越小,说明重构越接近原始输入,能量函数越低。权重的衰减项,就是给权重加一个惩罚系数保留在较小范围,防止过拟合,数值不值得去神化,一般取值0.0001到0.001之间即可。
这里有一个经验之谈:训练RBM时,输入数据的范围最好在0到1之间,或者均值在0附近。如果你的特征是收益率,均值接近于0,标准化后可以直接用。但如果你用价格类特征,范围内动不动就是几十几百,那sigmoid非常容易饱和,梯度直接消失。所以我坚持用标准化后的对数收益率。
2.3 构造完整DBN结构及顶层回归层
单个RBM学到的是数据的低维表示。把多个RBM串起来,每一层RBM的隐藏层输出作为下一层RBM的可见层输入,就得到了DBN的逐层预训练过程。这样每一层学到的是上一层特征的更抽象表示。数据流有点像流水线:第一层RBM提取原始特征的低阶模式,第二层RBM在这个模式的基础上提取更复杂的组合特征。
在MATLAB里,就是重复调用trainRBM函数数次。这里要注意一个细节:第一层RBM训练完成后,计算它的隐藏层激活值,作为下一层的输入。而隐藏层激活值可以有两种选择,一是采样后的0/1值,二是sigmoid输出的连续概率值。两种我都试过,实践下来用连续概率值作为下一层的输入,信息损失更少,更平滑,后面的回归结果通常更好。0/1采样会引入额外的随机性,对金融这种本来就带噪声的数据并不友好。
预训练完成后,在DBN最顶层加一个回归输出层,进行有监督的微调。回归任务要用线性输出层,不能再用sigmoid,因为我们要预测的累计收益是连续变量,范围可能在-0.2到0.2之间,你不可能把它压到0到1的区间里再去缩放。微调时可以用MATLAB标准函数,也可以手动写一个反向传播,或者干脆把预训练得到的权重当作初始值,然后直接用内置的trainNetwork进行训练。只是需要说明一个坑:trainNetwork如果你不加以控制,它会重新从头训练;你需要把预训练的权重作为初始值注入网络层,但MATLAB的API接口不一定给你方便地设置每层权重的入口。所以更稳妥的方案是自己实现微调的反向传播过程。如果只是为了验证DBN的效果,自己实现一个包含两三个隐藏层的微调过程并不复杂。
3. 实操验证:DBN在股票数据上的预测效果
理论讲完了,这部分直接演示用真实股票数据跑一遍。我会用一段月度级别的行业指数数据来做演示样本(你也可以换成自己的交易品种),展示从特征构造、预训练、微调到效果评估的完整流程。
3.1 数据准备与参数配置
以下几点参数可以直接作为基线,是我反复实验后获得的一个比较平衡的配置:L=20天输入窗口,H=5天预测周期,DBN结构为20-32-16-1。20是输入维度(20天的收益序列),32是第一层隐藏层,16是第二层隐藏层,1是输出节点。预训练时每个RBM迭代200轮,批大小32,学习率0.01;微调阶段迭代300轮,学习率0.001。
别看结构简单,这个配置在大多数股票数据上都能跑出一个相对稳定的结果。隐藏层节点不需要太多,因为股票数据的有效信息量本身有限,你堆太多节点,预训练时模型会把噪声的细节也背下来,结果微调时过拟合严重。另外一个原因是,DBN的预训练过程本身需要足够多的样本来学习分布,32个节点在这个样本量级别上已经足够编码主要模式。
这个配置里的20-32-16结构是有讲究的。第一层32个节点负责做特征表达,第二层16个节点是度数压缩。从20维扩展到32维,相当于把特征映射到高维空间里,让线性不可分的模式有更好的表达空间;再降到16维是为了避免维度爆炸,同时迫使模型保留关键信息。如果你数据量大,节点可以加一些;如果数据量小,就要减一些。作为一种参考,我的经验值是数据量每增加一倍,隐秘节点数增加20%左右。
3.2 训练过程及预测结果评估
在训练时,我会同时记录预训练阶段的重构误差和微调阶段的训练/验证损失。重构误差应该随着迭代稳步下降,但如果出现剧烈的跳变,多半是学习率太大。而微调阶段要盯着验证集的损失曲线,如果在验证集误差上升但训练集误差还在降,果断停止训练,这就是所谓的早停。
下面这段代码是对训练集做预训练的过程:
% 假设X_train 是标准化后的训练特征矩阵 rbm1 = trainRBM(X_train, 32, opts1); % 计算第一层输出 H1 = sigmoid(X_train * rbm1.W + rbm1.hb); rbm2 = trainRBM(H1, 16, opts2); % 整个DBN的输入到隐藏层2的表示 H2 = sigmoid(H1 * rbm2.W + rbm2.hb);然后我们把H2和原始训练标签Y_train拼起来,做微调:
% 初始化微调网络权重 W1 = rbm1.W; b1 = rbm1.hb; W2 = rbm2.W; b2 = rbm2.hb; % 顶层回归层随机初始化 W3 = 0.1 * randn(16, 1); b3 = 0; % 前向传播(基于全量数据梯度下降) for iter = 1:300 % 前向 a1 = X_train * W1 + b1; h1 = sigmoid(a1); a2 = h1 * W2 + b2; h2 = sigmoid(a2); yPred = h2 * W3 + b3; % 损失对输出层的梯度 dy = (yPred - Y_train) / length(Y_train); % 反向传播 dW3 = h2' * dy; db3 = sum(dy); dh2 = dy * W3' .* h2 .* (1 - h2); dW2 = h1' * dh2; db2 = sum(dh2); dh1 = dh2 * W2' .* h1 .* (1 - h1); dW1 = X_train' * dh1; db1 = sum(dh1); % 梯度下降更新 W3 = W3 - lr * dW3; b3 = b3 - lr * db3; W2 = W2 - lr * dW2; b2 = b2 - lr * db2; W1 = W1 - lr * dW1; b1 = b1 - lr * db1; end然后就可以在测试集上做预测了。评估指标我用的是方向准确率(预测的涨跌方向是否与真实一致)和IC(预测值与真实值的秩相关系数或皮尔逊相关系数)。为什么不用均方误差?因为股票预测的均方误差天然就很大,你预测得再准,也远小于正常波动范围。你要真正关心的是方向:明天是涨还是跌,涨多少反而不需要太精准。我实验里得到的方向准确率大概在53%-56%之间,IC在0.05左右。这点可能让你失望,但这才是金融数据的真实情况。
预测结果可以用一段MATLAB代码可视化:
figure; plot(dates_test, realCumRet, 'b-', 'LineWidth', 1.5); hold on; plot(dates_test, predCumRet, 'r--', 'LineWidth', 1.5); legend('真实累计收益', 'DBN预测累计收益'); title('DBN股票预测效果(测试集)');看图的时候别被整体趋势迷惑,真正要关注的是转折位置的预测。如果模型在转折点附近经常给出错误方向,那这个模型的价值就很有限。
4. 参数调优、常见问题与避坑经验
这部分是实打实的经验分享。参数怎么调、哪些地方容易出错,都是我在实验过程中踩出来的,希望能让你们少走弯路。
4.1 学习率、隐藏层与批大小的调整策略
先说学习率。RBM预训练阶段的学习率和微调阶段完全不是一回事。预训练阶段的学习率可以相对大一点,比如0.01到0.05,因为对比散度本身就是个近似算法,你不需要它收敛到极致精确,只需要权重接近一个合理的分布即可。微调阶段是真正在优化预测目标,学习率要调低一个量级,0.001左右比较稳妥。如果微调时学习率太大,很容易震荡,损失曲线呈锯齿状,根本停不下来。
批大小的选择也直接影响训练的稳定性。我试过batch=1(随机梯度下降)和batch=128,发现对于DBN来说,批大小32-64最稳定。过小时,权重的更新方向太过随机,重构误差波动很大;过大时,每次迭代耗时太长,而且容易陷入局部极小值。批大小可以理解为每次决策前听“多少个人的意见”:人少了决策噪声大,人多了决策太保守。32-64是一个在金融数据上比较均衡的范围。
隐藏层层数和节点数不要贪多。有一个很容易被人忽略的原则:时间序列的数据信息量是有限的,尤其是单只股票,核心的规律可能用几个模式就能概括。你堆叠太多层的DBN,预训练出来的低层特征可能是有意义的,但高层特征在微调时并没有足够的样本去进一步优化。我的建议是总层数(包括输入输出)最多5层,超过这个数在中小数据集上基本都是过拟合。你可以在验证集上观察:如果从第2层到第3层隐藏层的加入,验证损失并没有明显下降,那就果断删掉那一层。
4.2 训练不收敛或结果震荡怎么排查
我在调DBN时,训练不收敛的情况大致有以下几种:数据没有标准化、学习率过大、权重初始化不当、没有早停。
数据标准化的影响超过你的想象。还是那句话,RBM内部用的是sigmoid激活函数,对输入范围极其敏感,你把范围相差很大的特征塞进去,模型很难训练。正常做法是对每个特征维度分别做z-score标准化,或者统一缩放到0到1区间。股票收益率这种数据本身均值接近0,做z-score就够了。测试集的数据标准化参数必须来自训练集,否则就引入了未来信息。
学习率过大的表现是损失曲线猛降一下然后变成NaN,或者持续震荡。此时第一时间把学习率调到当前的十分之一重试。观察到震荡也不一定要改结构,通常先用学习率衰减策略试试。在预训练阶段做线性衰减,比如从0.02降到0.002,能有效缓解后期的震荡。
权重初始化我推荐用0.01倍的标准正态分布随机数,不要太大也不要全0。全0初始化会导致所有节点学到同样的特征(对称性问题),随机数太大会让激活值落入sigmoid饱和区,梯度基本为零。0.01这个量级,又在饱和区周围,梯度能流动起来,又不会对概率分布产生过大的先验影响。
还有最后一个微妙的问题:RBM的训练结果对随机种子很敏感。同样的参数,不同的随机种子,出来的重构误差可能会差不少。我的做法是固定随机种子验证一次,然后跑多次取平均来评估真实效果。别把单次运行的结果当成模型的真实水平,尤其是金融数据本身信噪比就低,一次好的随机种子带来的提升甚至可能超过调参的收益。
4.3 评估方法论:方向准确率、IC与信息泄露
评估DBN在股票预测上的表现,要特别注意方法论,否则得出的结论会非常虚。评测时,最关键的是防止信息泄露。
信息泄露的高危场景有三个。一是使用全量数据做标准化,导致训练集中的统计量隐含了测试集的信息。二是特征构造时使用了未来数据,比如本应该用t日及之前的数据做特征,却混入了t+1日的数据。三是样本切分时随机打乱了时间顺序,让模型在训练时“见”到了未来的样本。这三条每一行都是我在实际中踩过的,遵循严格的时间顺序切分、在训练集上计算统计量、反复检查特征窗口边界,才能得到可信的结论。
评估指标上,方向准确率是最直观的指标,也就是预测方向与实际方向一致的样本比例。在股票数据上,能稳定达到55%就已经说明模型具备一定的预测能力。IC则是相关系数,用来衡量模型预测值与实际值的相关性,一般0.05以上说明有一点预测能力,0.1以上就算相当不错了。IC的意义在于,它衡量的是排序能力而非数值精度。举个例子,如果某一天模型预测的涨幅排序与真实涨幅排序高度一致,哪怕具体数值差很多,作为一个信号也是可用的。
最后我特别想强调一点:不要把模型的预测结果直接当成交易信号来用。DBN或者任何机器学习模型给出的预测,都只是整个交易系统的一环。你还需要考虑交易成本、滑点、流动性等因素,这些因素可能把模型5%的准确率优势全部吃掉。机器学习的预测能力评估和真实的交易策略执行之间,隔着一条巨大的鸿沟。如果你的目的是研究模型能力,那本文的方法已经足够;如果你是为了实盘交易,请务必再加一道“信号转化为仓位”的流程,并且先在模拟环境里跑足够长的时间。
DBN这个模型虽然老,但它的“预训练+微调”思想在今天依然有价值。尤其在数据量不足、噪声强烈的场景下,它比从头训练的深度模型要稳不少。你在自己的数据上跑的时候,我建议第一轮跑通代码,第二轮再认真调参。第一轮的目标是确认整个流程没有bug、评测指标的计算是正确的;第二轮才去追求预测效果的提升。这个顺序很重要,我见过太多人第一轮就急着调参,最后连自己模型好不好都说不清,浪费了更多时间。