简介:这份资源面向具备一定编程基础与金融数据分析经验的量化分析师、金融工程学生及科研人员,提供一套在MATLAB R2025b环境下实现的BMA-LSTM股票价格预测完整项目。它针对金融时间序列非线性、非平稳与高噪声的痛点,用多个结构不同的LSTM子模型分别训练,再基于验证集误差以近似贝叶斯方法计算权重,实现多模型集成,并构建预测区间以量化不确定性,从而提升预测精度与鲁棒性。压缩包共1个docx文件,约120KB,内容涵盖数据预处理、子模型定义与训练、贝叶斯权重计算、测试集集成预测、评估可视化及GUI交互界面设计,并附模拟数据生成、模型保存与部署预测的全流程代码。已有158人学习。读者可借此掌握LSTM与贝叶斯模型平均融合的工程实现思路,理解模型集成、不确定性量化在量化投资与风险管理中的应用,并获取可扩展至注意力机制、多资产联合建模的代码框架与调参参考。
1. 金融预测里的 BMA-LSTM:为什么单模型总是差一口气
做股票价格预测的人大多经历过这个阶段:先用 ARIMA 试水,发现它对非线性拐点几乎无感;换成 LSTM,回测曲线漂亮得不像话,一上模拟盘就原形毕露。问题往往不在网络结构本身,而在于单模型对市场状态的假设太单一——趋势段表现好的模型,在震荡段可能就是个灾难。
BMA-LSTM 要解决的就是这件事。BMA 是贝叶斯模型平均,它不赌某一个模型是对的,而是给多个候选模型分配后验权重,再按权重加权融合预测结果。把它和 LSTM 结合,本质上是让若干个结构或超参不同的 LSTM 子模型各自出预测,再用贝叶斯框架决定「该信谁多一点」。这套思路适合有一定机器学习基础、想从「调一个 LSTM」进阶到「管理一组 LSTM」的量化从业者和研究生。下面从原理到 MATLAB 落地,一步步拆开讲。
2. BMA 与 LSTM 的融合逻辑:权重从哪来,预测怎么合
2.1 贝叶斯模型平均的核心公式与直觉
BMA 的基本思想可以用一个式子概括。假设我们有 K 个候选模型 M₁ 到 M_K,对于给定的历史数据 D,目标变量 y 的后验预测分布是:
p(y | D) = Σₖ p(y | Mₖ, D) · p(Mₖ | D)
其中 p(Mₖ | D) 是模型 Mₖ 的后验概率,也就是它的权重。这个权重正比于「先验概率 × 边际似然」:
p(Mₖ | D) ∝ p(Mₖ) · p(D | Mₖ)
直觉上,一个模型如果在历史数据上预测得又准又稳,它的边际似然就大,后验权重自然高。和简单平均或固定权重集成相比,BMA 的优势在于权重是从数据里推出来的,而不是拍脑袋定的。在 MATLAB 里,我们通常用 BIC 或 AIC 近似边际似然,避免直接算高维积分。
2.2 为什么选 LSTM 作为子模型基座
LSTM 处理时序数据的优势已经不需要多解释,门控机制让它能记住长距离依赖,适合股价这种带记忆效应的序列。但单个 LSTM 的问题也很明显:初始化不同、层数不同、学习率不同,结果可能差很多。这恰恰是 BMA 的用武之地——与其纠结用哪个配置,不如训练一组配置不同的 LSTM,让 BMA 来分配权重。
常见做法是固定 LSTM 的核心结构,在三个维度上做扰动:隐藏层单元数(比如 32、64、128)、输入窗口长度(比如 10、20、30 个交易日)、Dropout 比例(0.1、0.2、0.3)。这样能生成一组有差异但都合理的子模型。
2.3 权重计算:用 BIC 近似边际似然
在 MATLAB 里落地时,我一般用 BIC 来近似每个子模型的边际似然,因为直接算积分不现实。BIC 的公式是:
BIC = n · ln(SSE / n) + k · ln(n)
其中 n 是样本数,SSE 是残差平方和,k 是模型有效参数个数。BIC 越小,模型越好。把 BIC 转成权重:
wₖ = exp(-0.5 · ΔBICₖ) / Σⱼ exp(-0.5 · ΔBICⱼ)
其中 ΔBICₖ = BICₖ - min(BIC)。这个转换保证了权重在 0 到 1 之间且总和为 1。下面是对应的 MATLAB 代码:
% 假设 bicVals 是一个 K×1 的向量,存了每个子模型的 BIC 值 function w = bmaWeights(bicVals) % 减去最小值,防止 exp 溢出 deltaBIC = bicVals - min(bicVals); % 计算未归一化权重 rawW = exp(-0.5 * deltaBIC); % 归一化 w = rawW / sum(rawW); end这段代码的关键在deltaBIC那一步。如果直接对原始 BIC 取 exp,当 BIC 值很大时(比如几千),exp 会直接下溢到 0,所有权重都变成 NaN。减去最小值之后,最好的模型 ΔBIC=0,权重最大,其余按差距指数衰减,数值上稳定得多。
2.4 融合预测:加权求和与方差合成
拿到权重后,最终预测是各子模型预测的加权和:
ŷ = Σₖ wₖ · ŷₖ
但 BMA 不止给点预测,还能给预测方差,这对风险管理很有用:
Var(y) = Σₖ wₖ · (σₖ² + ŷₖ²) - ŷ²
其中 σₖ² 是子模型 k 的预测方差(可以用残差方差估计)。这个方差反映了模型间的不一致性——如果子模型们预测分歧大,方差就大,说明当前市场状态让模型们「没共识」,这时候对预测结果就该更谨慎。
% preds 是 K×T 矩阵,每行是一个子模型对 T 个测试点的预测 % weights 是 K×1 权重向量 % residVar 是 K×1 各子模型残差方差 function [yHat, yVar] = bmaPredict(preds, weights, residVar) yHat = weights' * preds; % 1×T 加权预测 % 方差合成 K = length(weights); T = size(preds, 2); yVar = zeros(1, T); for t = 1:T term1 = sum(weights .* (residVar + preds(:,t).^2)); yVar(t) = term1 - yHat(t)^2; end end参数说明:preds的每一行对应一个子模型,列是时间点;weights来自上一步的bmaWeights;residVar用训练集残差的方差估计。注意方差合成那一步不能简化成加权平均方差,必须带上均值平方项,否则会低估不确定性。
3. MATLAB 工程落地:从数据到 GUI 的完整链路
3.1 数据准备与特征工程
股票数据我一般用 MATLAB 的readtable从 CSV 读入,字段至少要有日期、开盘、最高、最低、收盘、成交量。核心预测目标是收盘价,但直接拿原始价格喂 LSTM 效果往往不好,因为价格是非平稳的。常见做法是先做一阶差分或者对数收益率:
% 读取数据 data = readtable('stock_data.csv'); closePrice = data.Close; % 计算对数收益率 logRet = [NaN; diff(log(closePrice))]; logRet(isnan(logRet)) = 0; % 构造滑动窗口特征 windowSize = 20; numSamples = length(logRet) - windowSize; X = zeros(numSamples, windowSize); Y = zeros(numSamples, 1); for i = 1:numSamples X(i, :) = logRet(i : i + windowSize - 1)'; Y(i) = logRet(i + windowSize); end这里windowSize是输入窗口长度,我一般会在 10 到 30 之间试。注意logRet的第一个值是 NaN,用 0 填充只是权宜之计,更稳妥的做法是直接丢弃第一个样本。特征方面,除了收益率本身,还可以加入成交量变化率、高低价差等,但要注意归一化——LSTM 对输入尺度敏感,用mapminmax把每列缩放到 [-1, 1]。
3.2 训练一组差异化 LSTM 子模型
子模型的差异化靠超参扰动实现。下面这段代码生成一组配置并逐个训练:
% 定义超参网格 hiddenSizes = [32, 64, 128]; dropouts = [0.1, 0.2, 0.3]; configs = []; for h = hiddenSizes for d = dropouts configs = [configs; h, d]; end end numModels = size(configs, 1); models = cell(numModels, 1); bicVals = zeros(numModels, 1); for m = 1:numModels hiddenSize = configs(m, 1); dropoutRate = configs(m, 2); layers = [ sequenceInputLayer(1) lstmLayer(hiddenSize, 'OutputMode', 'last') dropoutLayer(dropoutRate) fullyConnectedLayer(1) regressionLayer]; options = trainingOptions('adam', ... 'MaxEpochs', 100, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.005, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 30, ... 'LearnRateDropFactor', 0.5, ... 'Verbose', false); % 注意:LSTM 输入需要是 cell 数组,每个 cell 是一个序列 XTrain = arrayfun(@(i) X(i,:)', 1:size(X,1), 'UniformOutput', false)'; YTrain = Y; net = trainNetwork(XTrain, YTrain, layers, options); models{m} = net; % 计算训练集残差和 BIC YPred = predict(net, XTrain); residuals = YTrain - YPred; SSE = sum(residuals.^2); n = length(YTrain); k = hiddenSize * 4 + 1; % LSTM 参数量粗略估计 bicVals(m) = n * log(SSE / n) + k * log(n); end参数说明:hiddenSize控制 LSTM 记忆容量,太小欠拟合,太大容易过拟合;dropoutRate是正则化强度;InitialLearnRate设 0.005 是我在收益率序列上比较稳的值,太大容易震荡,太小收敛慢。k的估计用了 LSTM 参数量约等于 4 倍隐藏单元数加输出层,这是粗略值,精确计算可以用net.Layers逐层累加。
3.3 权重计算与融合预测
训练完所有子模型后,用第 2 章的bmaWeights算权重,再用bmaPredict做融合:
% 计算权重 weights = bmaWeights(bicVals); % 在测试集上预测 XTest = arrayfun(@(i) XTestRaw(i,:)', 1:size(XTestRaw,1), 'UniformOutput', false)'; preds = zeros(numModels, length(XTest)); residVar = zeros(numModels, 1); for m = 1:numModels preds(m, :) = predict(models{m}, XTest)'; % 用训练集残差方差作为该子模型的方差估计 YPredTrain = predict(models{m}, XTrain); residVar(m) = var(YTrain - YPredTrain); end [yHat, yVar] = bmaPredict(preds, weights, residVar); % 反归一化(如果之前做了归一化) % yHat 和 yVar 需要按同样方式还原这里有个容易翻车的点:predict对 LSTM 的输入格式要求是 cell 数组,每个 cell 是一个特征维度 × 时间步的矩阵。如果输入维度是 1,就是 1×windowSize。很多人在这里报维度错误,多半是忘了转置或者忘了包 cell。
3.4 GUI 设计:用 App Designer 搭一个预测面板
MATLAB 的 App Designer 适合做这种演示型 GUI。核心控件就几个:一个按钮加载数据,一个下拉框选股票或数据集,一个坐标轴显示预测曲线和置信区间,一个表格显示各子模型权重。
% 在 App Designer 的回调函数里 function PredictButtonPushed(app, event) % 加载并预处理数据 data = readtable(app.DataFileEditField.Value); [X, Y] = prepareData(data.Close, app.WindowSizeSpinner.Value); % 训练子模型(实际项目里应该预训练好,这里演示流程) [models, bicVals] = trainSubModels(X, Y); weights = bmaWeights(bicVals); % 预测 [yHat, yVar] = bmaPredict(...); % 绘图 plot(app.UIAxes, yHat, 'b-', 'LineWidth', 1.5); hold(app.UIAxes, 'on'); % 画置信区间 upper = yHat + 1.96 * sqrt(yVar); lower = yHat - 1.96 * sqrt(yVar); fill(app.UIAxes, [1:length(yHat), fliplr(1:length(yHat))], ... [upper, fliplr(lower)], 'b', 'FaceAlpha', 0.1, 'EdgeColor', 'none'); % 更新权重表 app.UITable.Data = table((1:length(weights))', weights, ... 'VariableNames', {'Model', 'Weight'}); endGUI 里最容易出问题的是数据传递——App Designer 的属性访问和传统 GUIDE 不一样,建议把训练好的模型和权重存成 app 的属性,避免每次点按钮都重新训练。另外绘图时fill的 X 坐标要首尾闭合,否则置信区间会画歪。
4. 避坑与排查:那些让回测和实盘脱节的细节
4.1 数据泄漏:归一化用了全量数据
现象:回测曲线完美,实盘一塌糊涂。原因:归一化时用了整个数据集的均值和标准差,测试集的信息泄漏到了训练阶段。解决:归一化参数只能用训练集算,然后应用到测试集。我一般把mapminmax的ps结构存下来,测试时用mapminmax('apply', XTest, ps)。
4.2 权重极端化:某个子模型独占 0.99
现象:BMA 权重几乎全给了一个模型,融合退化成单模型。原因:BIC 差距过大,或者子模型之间差异太小。解决:检查子模型配置是否有足够差异;如果 BIC 差距确实大,可以给权重加一个温度系数exp(-0.5 * deltaBIC / T),T 取 2 到 5 能平滑权重。但要注意,平滑会牺牲理论上的最优性,属于工程妥协。
4.3 LSTM 输入格式报错
现象:trainNetwork报「Invalid training data」或维度不匹配。原因:LSTM 要求输入是 cell 数组,每个 cell 是特征数 × 时间步。很多人直接把矩阵传进去。解决:用arrayfun把每行转成列向量再包 cell,如 3.2 节代码所示。另外注意OutputMode设为'last'时,输出是最后一个时间步,对应单点预测。
4.4 预测方差为负
现象:bmaPredict算出的yVar出现负值。原因:方差合成公式里term1 - yHat^2在数值上可能因为浮点误差变成负数。解决:加一个max(yVar, 0)兜底,或者检查residVar是否估计正确。如果residVar本身很小而子模型预测分歧大,也可能出现这种情况,这时候说明方差估计不可靠,建议用子模型预测的经验方差替代。
4.5 过拟合:训练集 R² 0.95,测试集 0.1
现象:子模型在训练集上拟合极好,测试集崩盘。原因:LSTM 参数量大,收益率序列噪声高,容易记住噪声。解决:加大 Dropout、减少隐藏单元、早停('ValidationData'配合'ValidationPatience')。我一般会把ValidationPatience设成 10,验证损失连续 10 轮不降就停。另外,BMA 本身也有一定抗过拟合能力,因为过拟合的模型 BIC 会差,权重自然低。
5. 进阶技巧:让 BMA-LSTM 从能跑到好用
5.1 滚动窗口重训练与权重更新
股票市场是非平稳的,一个在 2020 年有效的模型配置,到 2023 年可能完全失效。我一般用滚动窗口:每 60 个交易日重新训练一次子模型并更新 BMA 权重,窗口长度取 500 到 750 个交易日。这样权重能跟着市场状态走。代价是计算量大,K 个子模型 × 每次重训练,MATLAB 里可以用parfor并行加速。
% 滚动窗口重训练框架 windowLen = 600; retrainStep = 60; T = length(logRet); for startIdx = 1:retrainStep:(T - windowLen - 1) trainIdx = startIdx : (startIdx + windowLen - 1); testIdx = (startIdx + windowLen) : min(startIdx + windowLen + retrainStep - 1, T); % 用 trainIdx 训练子模型,算权重 % 用 testIdx 做样本外预测 % 记录预测结果 end参数说明:windowLen太短会导致训练不充分,太长会包含过时信息,600 是我在日频数据上比较常用的值;retrainStep控制重训练频率,60 约等于一个季度,再短计算成本吃不消。
5.2 用预测方差做仓位管理
BMA 给的预测方差不只是副产品,可以直接用来做仓位控制。一个简单规则:仓位 = 基础仓位 × (1 - 归一化方差)。方差高说明模型分歧大,降低仓位;方差低说明模型有共识,可以加仓。这个规则在震荡市里能明显降低回撤。
| 方差分位 | 建议仓位系数 | 适用场景 |
|---|---|---|
| 0-25% | 1.0 | 模型共识强,趋势明确 |
| 25-50% | 0.8 | 正常状态 |
| 50-75% | 0.5 | 分歧加大,减仓 |
| 75-100% | 0.2 | 高度不确定,防守 |
5.3 子模型多样性:不止是超参扰动
超参扰动只是最基础的多样性来源。进阶做法包括:用不同的输入特征组合(有的子模型只看价格,有的加入成交量)、不同的网络结构(LSTM、GRU、TCN 混搭)、不同的训练数据时间段(有的用近 1 年,有的用近 3 年)。多样性越丰富,BMA 的融合效果越好。但要注意,子模型不能太差,否则 BIC 会把它们权重压到接近 0,等于白训练。
5.4 验证方法:别只看 RMSE
评估 BMA-LSTM 不能只看 RMSE 或 MAE,还要看方向准确率(涨跌判断对了多少)和预测区间的覆盖率(95% 置信区间是否真的覆盖了 95% 的实际值)。我一般会画三张图:预测值 vs 实际值、预测误差分布、置信区间覆盖率随时间的变化。如果覆盖率远低于 95%,说明方差估计偏小,需要调整residVar的估计方式。
最后说个我自己的教训:早期做这套东西时,我花了很多时间调 LSTM 结构,却忽略了 BMA 权重的时间稳定性。后来发现,权重在滚动窗口里跳变很厉害,说明子模型之间的相对优劣在快速切换。这其实是个信号——市场状态在变。现在我更关注权重的变化趋势,而不是单次权重的绝对值。希望帮到你。
本文还有配套的精品资源,点击获取