资讯动态

BMA-LSTM金融时序预测:多模型融合降低方差实战

发布时间:2026/10/21 18:19:50 来源:尧图企业网站定制
简介这份资源面向具备一定编程基础与金融数据分析经验的量化分析师、金融工程学生及科研人员提供一套在MATLAB R2025b中实现的BMA-LSTM股票价格预测完整项目。它针对金融时间序列非线性、非平稳与高噪声的痛点用多个结构不同的LSTM子模型分别训练再基于验证集误差以近似贝叶斯方法计算权重实现多模型集成并构建预测区间以量化不确定性从而提升预测精度与鲁棒性。压缩包共1个docx文件约120KB内容涵盖数据预处理、子模型定义与训练、贝叶斯权重计算、测试集成、评估可视化及GUI交互界面设计并附模拟数据生成、模型保存与部署预测的全流程代码。已有158人学习。读者可据此掌握LSTM与贝叶斯模型平均融合的工程实现路径理解权重调优、复杂度控制与数据质量管理要点为量化投资、风险管理及后续引入注意力机制、多资产联合建模等改进提供可扩展的实践参考。1. 金融预测里BMA-LSTM 到底在解决什么麻烦做股票价格预测的人大多经历过同一个尴尬单模型跑出来的曲线回测漂亮得像艺术品一上模拟盘就原形毕露。LSTM 尤其典型它记性好、能吃长序列但初始化权重、超参数、训练轮数稍微动一动预测结果就飘得厉害。你调了三天的模型换台机器重跑一遍误差可能差出两个百分点。这种不确定性才是金融时序预测真正的敌人。BMA-LSTM 这个组合思路其实很朴素既然单个 LSTM 不稳那就训练一批结构不同、初始化不同的 LSTM再用贝叶斯模型平均BMA给它们分配权重把多个模型的预测按后验概率加权融合。它解决的不是“让某个模型更准”而是“让整体预测更稳、更可解释”。适合谁适合已经会用 MATLAB 搭 LSTM、但被单模型方差折磨过的量化从业者也适合想把概率预测引入风控的研究者。下面这套流程我会按能复现的标准拆开讲。2. 从单 LSTM 到 BMA为什么加权比调参更值2.1 单模型预测的方差从哪来LSTM 在股票价格这种低信噪比数据上方差来源至少有四层。第一层是权重初始化随机种子不同收敛到的局部最优就不同第二层是超参数窗口长度、隐藏层单元数、学习率、dropout 率任意一个变动都会改变输出第三层是数据切分训练集和验证集的边界挪一挪模型学到的分布就变了第四层是训练随机性mini-batch 的洗牌顺序、早停的触发点都会让最终权重产生差异。这四层叠加起来导致一个很现实的问题你没法判断某次预测变好是因为模型真的学到了规律还是因为运气好抽到了有利的初始化。金融数据本身噪声大这种“运气成分”占比往往高得吓人。我见过不少项目调参调到凌晨最后发现提升的那点精度换一个随机种子就没了。BMA 的价值就在这里。它不追求消灭方差而是承认方差存在然后用概率的方式把多个模型的不确定性量化出来。每个 LSTM 是一个“专家”BMA 根据它们在验证集上的表现计算后验权重表现好的权重大表现差的权重小但不会直接扔掉。最终预测是加权和方差自然被平滑掉一部分。2.2 BMA 的权重是怎么算出来的BMA 的核心公式不复杂。假设有 K 个模型第 k 个模型对下一时刻价格的预测是 y_k它的后验权重是 w_k那么 BMA 的融合预测就是y_BMA Σ w_k · y_k关键在于 w_k 怎么定。标准做法是用贝叶斯定理w_k P(M_k | D) ∝ P(D | M_k) · P(M_k)其中 P(M_k) 是先验通常取均匀分布P(D | M_k) 是模型 k 在观测数据 D 上的边际似然。边际似然直接算很难实操里一般用验证集上的预测误差来近似。常见近似方式有两种一种是用高斯似然把验证集残差的方差代进去另一种是用信息准则BIC 或 AIC做指数化再归一化。我一般用高斯似然近似因为它对残差分布的要求没那么强而且计算量小。具体做法是对每个 LSTM在验证集上算预测残差求残差方差 σ_k²然后令log P(D | M_k) ≈ -0.5 · N · log(σ_k²)N 是验证集样本数。这个近似假设残差服从零均值高斯分布虽然金融数据常有厚尾但作为权重分配的依据够用了。算完所有模型的 log 边际似然后做 softmax 归一化就得到 w_k。注意如果某个模型的验证集残差方差特别大它的权重会趋近于零但不会完全为零。这是 BMA 和简单平均的区别——它保留了“万一这个模型在测试集上翻身”的可能性只是概率很低。2.3 在 MATLAB 里搭 BMA-LSTM 的最小骨架下面这段代码是核心骨架跑通它你就能看到 BMA 融合的效果。我假设你已经有了一个预处理好的价格序列存在priceData里是一个列向量。% 参数设置 numModels 8; % LSTM 模型数量 windowSize 30; % 输入窗口长度 hiddenUnits 64; % 隐藏层单元数 trainRatio 0.7; % 训练集比例 valRatio 0.15; % 验证集比例 testRatio 0.15; % 测试集比例 % 构造监督学习样本用前 windowSize 个点预测下一个点 numSamples length(priceData) - windowSize; X zeros(windowSize, numSamples); Y zeros(1, numSamples); for i 1:numSamples X(:, i) priceData(i : i windowSize - 1); Y(i) priceData(i windowSize); end % 划分训练、验证、测试 numTrain floor(numSamples * trainRatio); numVal floor(numSamples * valRatio); idxTrain 1:numTrain; idxVal numTrain 1 : numTrain numVal; idxTest numTrain numVal 1 : numSamples; XTrain X(:, idxTrain); YTrain Y(idxTrain); XVal X(:, idxVal); YVal Y(idxVal); XTest X(:, idxTest); YTest Y(idxTest); % 训练多个 LSTM保存验证集残差 models cell(numModels, 1); valResiduals zeros(numModels, length(YVal)); for k 1:numModels % 每次用不同的随机种子和略微不同的超参数 rng(k * 100); layers [ sequenceInputLayer(windowSize) lstmLayer(hiddenUnits randi([-16, 16]), OutputMode, last) dropoutLayer(0.1 0.1 * rand()) fullyConnectedLayer(1) regressionLayer ]; options trainingOptions(adam, ... MaxEpochs, 80, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005 * (0.8 0.4 * rand()), ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 20, ... Verbose, false, ... Plots, none); % 转成 cell 格式以适配 sequenceInputLayer XTrainCell num2cell(XTrain, 1); XValCell num2cell(XVal, 1); XTestCell num2cell(XTest, 1); net trainNetwork(XTrainCell, YTrain, layers, options); models{k} net; % 在验证集上预测记录残差 YValPred predict(net, XValCell); valResiduals(k, :) YVal - YValPred; end % 计算 BMA 权重高斯似然近似 logLik zeros(numModels, 1); for k 1:numModels sigma2 var(valResiduals(k, :)); logLik(k) -0.5 * length(YVal) * log(sigma2 eps); end % softmax 归一化 w exp(logLik - max(logLik)); w w / sum(w); % 测试集上做 BMA 融合预测 YTestPredAll zeros(numModels, length(YTest)); for k 1:numModels YTestPredAll(k, :) predict(models{k}, XTestCell); end YBMA w * YTestPredAll; % 评估 rmseSingle sqrt(mean((YTestPredAll(1, :) - YTest).^2)); rmseBMA sqrt(mean((YBMA - YTest).^2)); fprintf(单模型 RMSE: %.4f\n, rmseSingle); fprintf(BMA 融合 RMSE: %.4f\n, rmseBMA);这段代码有几个关键点需要说明。numModels设成 8 是经验值太少融合效果不明显太多训练时间线性增长8 到 12 之间比较平衡。windowSize取 30 是日频数据的常见选择对应大约一个半月的交易日如果你做的是分钟级数据这个值要相应缩小。hiddenUnits在 64 附近波动每个模型加一个 ±16 的随机偏移是为了制造模型多样性。valResiduals记录的是每个模型在验证集上的逐点残差不是单一误差指标。用逐点残差算方差比用 RMSE 更细因为它保留了残差的分布信息。logLik的计算里加了eps防止 log(0)这是数值稳定性的常规操作。权重w算出来后你会发现表现最好的模型权重可能在 0.3 到 0.4 之间其余模型分摊剩下的权重。这就是 BMA 的特点不搞赢家通吃而是让每个模型都有发言权只是音量不同。3. 数据预处理与模型多样性BMA 效果的两个命门3.1 价格序列的平稳化处理股票价格是非平稳序列直接喂给 LSTM模型很容易学到“价格一直在涨”这种伪规律。常见做法是转成对数收益率% 假设原始价格在 rawPrice 里 logRet diff(log(rawPrice)); % 如果要做价格预测再累积回去 % 但训练时用收益率预测时再还原对数收益率的好处是量纲统一、近似平稳而且有明确的金融含义。但这里有个坑BMA 融合的是价格预测还是收益率预测如果你在收益率上训练BMA 融合后的也是收益率最后要累积求和还原成价格。还原这一步会放大误差因为累积求和把每一步的小误差都加起来了。我的做法是训练和融合都在收益率上做但评估时同时看收益率 RMSE 和还原后的价格 RMSE。如果价格 RMSE 明显比收益率 RMSE 恶化说明累积误差在作祟这时候要么缩短预测步长要么改用差分后的价格做目标。注意不要用归一化后的价格直接训练。归一化会引入未来信息因为均值和方差是从全序列算的。正确做法是用训练集的均值和方差去归一化验证集和测试集或者干脆用收益率。3.2 怎么让多个 LSTM 真正“不同”BMA 的前提是模型之间有差异。如果所有 LSTM 结构一样、初始化一样那融合等于没融合。制造多样性的手段有几种第一种是随机初始化种子不同。这是最省事的但多样性有限因为结构相同收敛到的解可能很接近。第二种是超参数扰动。隐藏层单元数、dropout 率、学习率、batch size每个都加一点随机扰动。上面代码里就是这么做的。扰动幅度要控制好太大导致个别模型完全跑偏太小又起不到多样化效果。我一般让隐藏层单元数在 ±25% 内波动学习率在 ±20% 内波动。第三种是结构差异。比如有的用单层 LSTM有的用双层有的加注意力机制有的不加。这种多样性最强但实现成本高适合对精度要求苛刻的场景。第四种是数据子采样。每个模型用不同的训练子集比如 bootstrap 采样。这在统计上最符合 BMA 的精神但金融数据样本量本来就少子采样会进一步减少有效样本要谨慎。我一般用第二种为主、第一种为辅结构差异留到进阶阶段再上。实测下来超参数扰动带来的多样性已经能让 BMA 比单模型在测试集上稳定提升 5% 到 15% 的 RMSE。3.3 验证集残差的高斯假设检验BMA 权重计算依赖残差服从高斯分布这个近似。如果残差严重偏离高斯权重分配就会失真。跑完模型后我习惯做一步检验% 对每个模型的验证集残差做正态性检验 for k 1:numModels [h, p] lillietest(valResiduals(k, :)); if h 1 fprintf(模型 %d 的残差拒绝正态假设p %.4f\n, k, p); end end如果超过一半的模型残差都拒绝正态说明高斯似然近似不可靠这时候要么改用 t 分布似然要么改用信息准则算权重。信息准则的做法是% 用 BIC 近似边际似然 for k 1:numModels rss sum(valResiduals(k, :).^2); n length(YVal); bic n * log(rss / n) 2 * log(n); % 简化版 BIC logLik(k) -0.5 * bic; endBIC 对残差分布的假设更弱但它是渐近近似小样本下可能不如高斯似然准。我的经验是验证集样本超过 200 个用高斯似然少于 200 个用 BIC 更稳。4. GUI 设计把 BMA-LSTM 包装成能用的工具4.1 界面布局与回调逻辑MATLAB 的 App Designer 或者传统的 GUIDE 都能做 GUI。我倾向用 App Designer因为布局更灵活回调函数写起来清晰。核心控件包括一个文件选择按钮用来加载 CSV 格式的价格数据一个下拉框选择预测目标收盘价、开盘价、最高价两个数值输入框设置训练集比例和 LSTM 模型数量一个“开始训练”按钮一个坐标轴显示原始价格和 BMA 预测曲线一个文本区域输出 RMSE、权重分布等指标回调逻辑的关键是把训练过程放在后台避免界面卡死。MATLAB 里可以用parfeval或者batch做异步训练但最简单的做法是用drawnow在训练循环里刷新进度条。% 在训练循环里更新进度 for k 1:numModels % ... 训练代码 ... progress k / numModels; app.ProgressBar.Value progress; app.StatusLabel.Text sprintf(正在训练第 %d / %d 个模型, k, numModels); drawnow; enddrawnow会强制刷新界面代价是训练速度略微下降但用户体验好很多。如果模型数量多、训练时间长建议用parfeval并行训练但要注意并行工具箱的许可和内存占用。4.2 预测结果的可视化要点BMA 的输出不只是点预测还有权重分布和预测区间。可视化时我一般画三样东西第一原始价格曲线和 BMA 预测曲线叠加让用户直观看到拟合效果。第二每个 LSTM 的权重柱状图展示哪些模型被 BMA 看重。第三预测区间用验证集残差的加权方差算出来% 计算 BMA 预测的方差 varBMA 0; for k 1:numModels varBMA varBMA w(k) * (valResiduals(k, :).^2 (YTestPredAll(k, :) - YBMA).^2); end stdBMA sqrt(varBMA); upperBound YBMA 1.96 * stdBMA; lowerBound YBMA - 1.96 * stdBMA;这个方差公式是 BMA 的经典分解一部分来自模型内部的残差方差一部分来自模型之间的预测差异。画出来就是一个 95% 置信区间比单模型的点预测信息量大得多。注意预测区间在金融场景里要谨慎解读。它假设残差分布稳定但市场波动率会突变区间可能在某些时段明显偏窄。我一般会在图上标注波动率突变点提醒用户区间可能不可靠。4.3 参数持久化与批量回测GUI 里调好的参数应该能保存下来下次直接加载。MATLAB 里用save和load存成.mat文件就行% 保存参数 params.numModels app.NumModelsEditField.Value; params.windowSize app.WindowSizeEditField.Value; params.hiddenUnits app.HiddenUnitsEditField.Value; save(bma_lstm_params.mat, params); % 加载参数 loaded load(bma_lstm_params.mat); app.NumModelsEditField.Value loaded.params.numModels;批量回测是另一个实用功能。用户可能想测试不同时间段、不同股票这时候需要一个循环自动跑完所有组合输出汇总表格。我一般用uitable展示结果每行是一个回测配置列包括 RMSE、方向准确率、最大回撤。方向准确率在金融预测里比 RMSE 更受关注因为它直接关系到交易信号。算法很简单预测涨跌方向和实际涨跌方向一致的比例。directionPred sign(YBMA(2:end) - YBMA(1:end-1)); directionTrue sign(YTest(2:end) - YTest(1:end-1)); dirAcc mean(directionPred directionTrue);这个指标能帮你判断 BMA 融合是否真的保留了方向信息。有时候 RMSE 降了但方向准确率没提升说明模型只是把幅度拟合得更准但拐点还是抓不住。5. 避坑与排查BMA-LSTM 落地时最容易翻车的五件事5.1 权重全压在一个模型上现象算出来的 BMA 权重里某一个模型的权重超过 0.9其余模型几乎为零。原因通常是某个模型的验证集残差方差远小于其他模型导致它的 log 似然远高于别人。这往往不是因为它真的更好而是因为它过拟合了验证集或者验证集恰好落在它擅长的区间。解决给权重加一个下限比如每个模型至少 0.02 的权重防止完全塌缩。或者改用温度参数调节 softmax 的平滑度temperature 2.0; % 大于 1 会让权重更平滑 w exp((logLik - max(logLik)) / temperature); w w / sum(w);温度设成 2 到 5 之间权重分布会明显更均匀。代价是融合预测可能略微偏向表现差的模型但稳定性更好。5.2 训练集和测试集的时间泄漏现象测试集 RMSE 低得离谱但实盘模拟一塌糊涂。原因预处理时用了全序列的统计量比如用整个价格序列算均值和方差做归一化或者用未来数据填充缺失值。LSTM 的窗口滑动如果跨过了训练集和测试集的边界也会泄漏。解决严格按时间顺序切分所有预处理参数只从训练集算。窗口滑动时测试集的第一个窗口必须完全来自测试集内部不能借用训练集的尾巴。我一般会在切分后检查一遍assert(max(idxTrain) min(idxVal), 训练集和验证集有重叠); assert(max(idxVal) min(idxTest), 验证集和测试集有重叠);5.3 残差方差为零导致 log 爆炸现象某个模型的验证集残差全是零或者接近零算 log 时出现 Inf 或 NaN。原因模型在验证集上完美预测这通常发生在验证集样本极少或者模型严重过拟合。金融数据里完美预测基本不可能出现这种情况多半是数据泄漏。解决在方差计算里加一个下限sigma2 max(var(valResiduals(k, :)), 1e-6);同时检查验证集样本数如果少于 30 个BMA 权重不可靠建议扩大验证集或改用交叉验证。5.4 LSTM 训练不收敛现象训练损失震荡或者一直不下降最终模型预测一条直线。原因学习率太大、序列太长、梯度爆炸。金融收益率数值很小通常在 0.01 量级如果直接喂给 LSTM梯度会非常小学习几乎停滞。解决把收益率放大 100 倍再训练预测完再缩回去。或者用sequenceInputLayer的Normalization参数做在线归一化。学习率从 0.001 开始试配合piecewise学习率调度。options trainingOptions(adam, ... InitialLearnRate, 0.001, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.5, ... LearnRateDropPeriod, 30, ... ...);5.5 GUI 卡死或无响应现象点击“开始训练”后界面冻结进度条不动Windows 提示“程序未响应”。原因训练循环在主线程里跑MATLAB 的单线程界面无法同时处理回调。drawnow如果调用太频繁反而会拖慢训练。解决用parfeval把训练放到后台f parfeval(trainBMAModels, 1, XTrain, YTrain, XVal, YVal, numModels); % 然后轮询 f.State更新界面 while strcmp(f.State, running) pause(0.5); app.StatusLabel.Text 训练中...; drawnow; end [models, w] fetchOutputs(f);如果没装并行工具箱退而求其次把drawnow换成pause(0.01)减少界面刷新频率。6. 进阶技巧用滚动窗口 BMA 应对市场状态切换BMA 的权重是在验证集上一次性算出来的但市场状态会变。牛市里表现好的模型熊市里可能一塌糊涂。固定权重的 BMA本质上假设模型相对表现是稳定的这个假设在金融数据里经常不成立。我的做法是滚动窗口 BMA。具体来说不只用最近一段验证集算权重而是每隔一段时间重新算一次。比如每 20 个交易日用过去 60 个交易日的残差重新估计权重。这样权重会随着市场状态缓慢漂移比固定权重更适应。实现上把权重计算从训练阶段剥离出来做成一个独立函数function w computeBMAWeights(valResiduals, temperature) % valResiduals: numModels x numValSamples numModels size(valResiduals, 1); logLik zeros(numModels, 1); for k 1:numModels sigma2 max(var(valResiduals(k, :)), 1e-6); logLik(k) -0.5 * size(valResiduals, 2) * log(sigma2); end w exp((logLik - max(logLik)) / temperature); w w / sum(w); end然后在测试集上滚动调用rollingWindow 60; stepSize 20; YBMA_rolling zeros(size(YTest)); for t 1:stepSize:length(YTest) idxEnd min(t rollingWindow - 1, length(YTest)); idxStart max(1, idxEnd - rollingWindow 1); % 用这段窗口的残差重新算权重 wRoll computeBMAWeights(valResiduals(:, idxStart:idxEnd), 2.0); % 对当前时刻做融合预测 YBMA_rolling(t:idxEnd) wRoll * YTestPredAll(:, t:idxEnd); end滚动窗口的长度是个权衡太短权重估计噪声大太长跟不上市场变化。60 个交易日大约三个月是我试下来比较平衡的值。步长 20 天对应一个月调一次权重频率不算高但足够捕捉中期状态切换。还有一个技巧是给不同模型加“遗忘因子”。越近的残差权重越大。这相当于指数加权让 BMA 对近期表现更敏感lambda 0.95; % 遗忘因子 weights_time lambda .^ (size(valResiduals, 2) - (1:size(valResiduals, 2))); for k 1:numModels weightedResidual valResiduals(k, :) .* sqrt(weights_time); sigma2 max(var(weightedResidual), 1e-6); logLik(k) -0.5 * length(weightedResidual) * log(sigma2); endlambda取 0.95 意味着 20 天前的残差权重降到 0.3660 天前降到 0.05。这个衰减速度适合日频数据如果是周频或月频lambda要相应调大。最后说一个我踩过的坑滚动 BMA 的权重更新频率不要和预测频率一样。如果你每天预测一次每天更新权重权重会跟着噪声剧烈波动反而比固定权重更差。权重更新要慢于预测让它在多个预测周期上平滑。我一般让权重更新频率是预测频率的 1/5 到 1/10。这套 BMA-LSTM 的方案我从单模型调到多模型融合前后折腾了大半年。最大的教训是不要指望 BMA 能化腐朽为神奇它只能在你已经有一批还不错的模型时帮你把方差压下去。如果单个 LSTM 的 RMSE 比朴素预测还差先回去检查数据和训练流程别急着上 BMA。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑