简介:本资源是一份面向深度学习初学者与金融量化实践者的MATLAB实战项目,聚焦利用长短期记忆网络(LSTM)解决股票价格时间序列预测这一典型非线性建模问题。压缩包共5个文件(3个核心M脚本、1个Excel原始数据表、1个CSV结果文件),总大小仅114KB,轻量易部署,其中main2.m为主程序,MSE_RMSE_MBE_MAE.m与R_2.m分别提供多维度预测误差评估与拟合优度计算,便于模型效果量化分析。已有1063人学习下载,体现了该主题在算法落地与实证教学中的持续热度。用户可直接运行代码复现完整预测流程:从历史股价数据读取、LSTM网络结构定义、序列归一化与滑动窗口构造,到训练、验证及未来走势预测,并获得可视化结果与评估指标输出,是理解LSTM时序建模原理与MATLAB深度学习工具箱应用的优质入门范例。
1. 用 MATLAB 写 LSTM 预测股票价格:不是调个函数就完事,而是得把时间序列的“记忆断点”和“过拟合黑匣子”全揪出来
你手头有一份标着“基于 MATLAB 编程的 LSTM 股票价格预测”的压缩包,解压后看到main2.m、MSE_RMSE_MBE_MAE.m、R_2.m、结果.csv和股票价格走势预测.xlsx——第一反应可能是:“终于有现成代码了,改个路径就能跑”。但实操过三次以上的人会立刻停住:LSTM 在 MATLAB 里跑股票预测,90% 的翻车不是因为模型没收敛,而是因为数据切片方式错了、归一化没回滚、滑动窗口步长和预测长度不匹配这三处“静默陷阱”。这份资源真正价值不在代码本身,而在于它用最朴素的trainNetwork+sequenceInputLayer搭建了一条可追溯、可打断、可逐层验证的完整链路:从 Excel 原始 OHLC 数据读入 → 时间步对齐 → Min-Max 归一化 → 滑动窗口构造(含 label 对齐)→ LSTM 层堆叠(含 dropout 控制)→ 回归输出 → 反归一化 → 多指标评估(MAE/RMSE/R²/MBE)。它适合两类人:一是刚学完《深度学习导论》想落地第一个时序项目的 MATLAB 新手,二是被 Python PyTorch 的DataLoader和torch.nn.LSTM绕晕、想先用 MATLAB 看清 LSTM 底层时序逻辑的转岗工程师。别急着 run,先看清它怎么把“股价涨跌”这个混沌系统,硬生生掰成一个可训练、可验证、可解释的序列回归问题。
2. 从 Excel 到 sequenceInputLayer:数据预处理的四步不可跳过链
2.1 原始数据结构解析:为什么股票价格走势预测.xlsx必须是单列时间序列?
打开股票价格走势预测.xlsx,你会发现它只有A 列为日期(如 2020/1/2)、B 列为收盘价(如 3.256),没有成交量、MACD 或其他技术指标。这不是疏漏,而是刻意为之——本项目聚焦于纯价格驱动的单变量时间序列预测,回避多变量对齐、缺失值插补、不同量纲耦合等干扰项。MATLAB 的readmatrix默认按列读取,所以data = readmatrix('股票价格走势预测.xlsx'); price = data(:,2);得到的是一个 N×1 向量。注意:日期列未参与建模,仅用于后续绘图横轴标注。若你手头是带多列的 CSV(如含开盘、最高、最低),必须先用price = data(:,4);显式提取收盘价,否则sequenceInputLayer会报错维度不匹配。
% main2.m 中关键读取段(已加注释说明逻辑) data = readmatrix('股票价格走势预测.xlsx'); price = data(:,2); % 强制取第2列,即收盘价 T = length(price); % 总长度,用于后续划分训练/测试集提示:不要用
xlsread(R2020a 已弃用),readmatrix是当前推荐方式,自动跳过表头并返回 double 数组。若 Excel 有空行或非数字字符,readmatrix会报错,此时需先用 Excel 手动清理,或改用readtable+table2array组合。
2.2 滑动窗口构造:createSequenceData函数里的三个生死参数
核心预处理函数藏在main2.m里,名为createSequenceData(虽未单独 .m 文件,但逻辑内聚)。它接收原始 price 向量,输出XTrain,YTrain,XTest,YTest四个 cell 数组。关键参数有三:
inputSize: 输入序列长度(即用过去多少天预测下一天),默认设为 60(对应两个月交易日);numPredictSteps: 单次预测步数,本项目固定为 1(预测下一个交易日收盘价);trainRatio: 训练集占比,代码中写死为 0.8。
% main2.m 中滑动窗口核心逻辑(已展开注释) inputSize = 60; % 过去60天作为输入特征 numPredictSteps = 1; % 预测未来1天 trainRatio = 0.8; trainEndIdx = floor(trainRatio * T); % 构造训练集:从第 inputSize+1 行开始,每行取前 inputSize 个点为 X,后1个点为 Y for i = inputSize+1:trainEndIdx XTrain{i-inputSize} = price(i-inputSize:i-1)'; % 转置成 1×60 行向量 YTrain{i-inputSize} = price(i); % 标量 end % 测试集同理,但起始位置为 trainEndIdx+1 for i = trainEndIdx+inputSize+1:T XTest{i-trainEndIdx-inputSize} = price(i-inputSize:i-1)'; YTest{i-trainEndIdx-inputSize} = price(i); end注意:
XTrain是 cell 数组,每个元素为 1×60 向量,而非矩阵。这是因为sequenceInputLayer要求输入为C×S×N格式(C=特征维数,S=时间步,N=样本数),而单变量序列 C=1,故每个样本是 1×S 向量。若强行拼成 60×N 矩阵,trainNetwork会报错 “Expected input size mismatch”。
2.3 归一化与反归一化:minmax不是万能钥匙,必须保存极值
MATLAB 的mapminmax是最常用归一化工具,但它返回的settings结构体必须全程保留,否则测试集反归一化将失效。main2.m中关键操作如下:
% 归一化训练集输入 [XTrainNorm, PS_X] = mapminmax(XTrain); % PS_X 包含 min/max 值 % 归一化训练集标签(注意:YTrain 是标量,需转 cell 再归一化) YTrainCell = num2cell(YTrain); [YTrainNorm, PS_Y] = mapminmax(YTrainCell); % 对测试集应用相同变换(不能重新计算 PS!) XTestNorm = mapminmax('apply', XTest, PS_X); YTestCell = num2cell(YTest); YTestNorm = mapminmax('apply', YTestCell, PS_Y);关键逻辑:
PS_X和PS_Y是两个独立的归一化参数结构体。mapminmax('apply', ..., PS)表示用训练集统计量去变换测试集,而非重新计算测试集自身的 min/max。若此处误用mapminmax(XTest),会导致测试集被错误缩放,最终预测结果整体偏移。
2.4 构建 LSTM 网络架构:为什么sequenceInputLayer后必须接dropoutLayer?
网络定义部分在main2.m中以layers = ...开头。标准结构为:
layers = [ sequenceInputLayer(1,'Normalization','none') % 输入维度为1(单变量) lstmLayer(128,'OutputMode','last') % 隐藏单元数128,只取最后时刻输出 dropoutLayer(0.2) % 20% 神经元随机失活,防过拟合 fullyConnectedLayer(1) % 输出1维(预测值) regressionLayer];解析:
'OutputMode','last'是关键——它表示只取 LSTM 最后一个时间步的隐藏状态作为特征,而非全部时间步('sequence'模式)。因本项目是单步预测,'last'更高效;若要做多步滚动预测,则需改为'sequence'并搭配sequenceFoldingLayer。dropoutLayer(0.2)不可省略:股票价格噪声极大,无 dropout 的 LSTM 在训练集上 R² 可达 0.95,测试集直接掉到 0.3,这就是典型的过拟合黑匣子。
3. 训练配置与超参调试:trainingOptions里的五个决定性开关
3.1 优化器选择:adam是唯一合理选项,sgdm会震荡发散
股票价格序列梯度极不稳定,sgdm(带动量的随机梯度下降)极易在局部极小值反复震荡,loss 曲线呈锯齿状且不收敛。adam自适应学习率,对稀疏梯度和噪声鲁棒性强。main2.m中配置如下:
options = trainingOptions('adam', ... 'MaxEpochs',200, ... % 最大训练轮数 'InitialLearnRate',0.005, ... % 初始学习率(0.005 是经验值,0.01 易发散) 'GradientThreshold',1, ... % 梯度裁剪阈值,防梯度爆炸 'Verbose',true, ... % 实时打印 loss 'Plots','training-progress', ... % 绘制训练曲线 'ValidationData',{XVal,YVal}, ... % 验证集(若无则删此项) 'ValidationFrequency',30, ... % 每30轮验证一次 'ExecutionEnvironment','cpu'); % 强制 CPU(避免 GPU 显存不足报错)注意:
'ExecutionEnvironment','cpu'是血泪经验。MATLAB 的trainNetwork在 GPU 上运行 LSTM 时,若 batch size > 128 或序列长度 > 100,常触发显存溢出(Out of memory on GPU),错误码CUDA_ERROR_OUT_OF_MEMORY。设为'cpu'虽慢,但稳定;若坚持用 GPU,必须同步降低inputSize(如从 60 改为 30)和MiniBatchSize(如从 128 改为 64)。
3.2 Mini-Batch Size:128 不是玄学,而是内存与梯度稳定性的平衡点
MiniBatchSize直接影响两个核心指标:
- 内存占用:batch size 每翻倍,GPU 显存需求约翻 1.8 倍(因 LSTM 需缓存所有时间步的中间状态);
- 梯度方差:batch size 过小(如 16),梯度噪声大,loss 曲线抖动剧烈;过大(如 256),单步更新信息量饱和,收敛变慢。
本项目默认MiniBatchSize=128,适配大多数笔记本(16GB 内存 + GTX 1050 Ti)。若你机器内存 ≤ 8GB,必须降至 64,并相应增加MaxEpochs至 300 以补偿收敛速度。
3.3 Early Stopping:不用ValidationPatience就是赌运气
main2.m未启用早停机制,这是重大隐患。股票数据存在明显结构性变化(如 2020 年疫情冲击),模型可能在第 150 轮过拟合,第 180 轮 loss 反弹却仍在训练。正确做法是添加:
options = trainingOptions('adam', ... ... 'ValidationPatience',15, ... % 连续15轮验证 loss 不下降则停止 'ValidationFrequency',10, ... % 每10轮验证一次(比默认30更敏感) 'StopTrainingCriteria','validation-loss'); % 以验证 loss 为准验证 loss 不是可选指标:它反映模型泛化能力。若只看训练 loss,你会得到一个在历史数据上完美拟合、对未来完全失效的“幻觉模型”。
3.4 学习率衰减:'LearnRateSchedule','piecewise'是防过拟合后悔药
固定学习率易导致后期震荡。加入分段衰减:
options = trainingOptions('adam', ... ... 'LearnRateSchedule','piecewise', ... 'LearnRateDropFactor',0.5, ... % 学习率乘以 0.5 'LearnRateDropPeriod',50); % 每50轮衰减一次这样,第 1–50 轮用 0.005,51–100 轮用 0.0025,101–150 轮用 0.00125……让模型在后期更精细地调整权重,避免在最优解附近反复横跳。
4. 预测结果验证与误差分析:R_2.m和MSE_RMSE_MBE_MAE.m的真实用途
4.1 R² 不是越高越好:警惕R_2.m的“虚假繁荣”
R_2.m计算决定系数,公式为R² = 1 - SS_res / SS_tot。但股票价格预测中,R² > 0.8 常是过拟合信号。原因在于:
SS_tot(总离差平方和)由真实价格波动幅度决定,若股价长期横盘(如 2021 年某蓝筹股),SS_tot极小,即使预测值轻微偏移,SS_res相对变大,R² 反而暴跌;- 反之,若股价剧烈波动(如 2020 年初),
SS_tot极大,模型只要抓住趋势方向,R² 就轻易破 0.9,但 MAE 可能高达 2 元。
因此,必须同时看MSE_RMSE_MBE_MAE.m输出的四指标:
| 指标 | 公式 | 物理意义 | 健康阈值(以 A 股为例) |
|---|---|---|---|
| MAE | mean( | y_pred - y_true | ) |
| RMSE | sqrt(mean((y_pred - y_true)²)) | 均方根误差(放大异常值惩罚) | < MAE × 1.3 |
| MBE | mean(y_pred - y_true) | 平均偏差(判断系统性高估/低估) | |
| R² | 1 - SS_res/SS_tot | 解释方差比例 | 0.4~0.7(过高必查过拟合) |
% R_2.m 核心代码(已加注释) function r2 = R_2(y_true, y_pred) SS_res = sum((y_true - y_pred).^2); SS_tot = sum((y_true - mean(y_true)).^2); r2 = 1 - SS_res/SS_tot; end % 注意:y_true 和 y_pred 必须是同长度向量,且已反归一化4.2 反归一化必须用原始 PS:结果.csv里的数值是“裸输出”,不是最终结果
main2.m运行后生成结果.csv,但其中数值是归一化后的预测值,直接绘图会是一条扁平直线。必须用PS_Y反变换:
% 加载结果.csv(假设为 N×1 矩阵) pred_norm = csvread('结果.csv'); % 反归一化:注意 mapminmax('reverse', ...) 用法 pred_actual = mapminmax('reverse', pred_norm, PS_Y); % 保存为新文件 writematrix(pred_actual, '结果_实际价格.csv');错误示范:
pred_actual = (pred_norm - PS_Y.ymin) / (PS_Y.ymax - PS_Y.ymin) * (max_price - min_price) + min_price;—— 这是手动实现,但PS_Y中ymin/ymax是 cell 数组,需用PS_Y.ymin{1}提取,极易出错。坚持用mapminmax('reverse', ...)。
4.3 可视化验证:用plot对齐时间轴,而非简单plot(1:N, pred)
main2.m中绘图部分常简化为plot(pred),但真实场景需与真实价格对齐:
% 假设 test_dates 是测试集对应日期(从 Excel 读取) test_dates = data(trainEndIdx+1:end,1); % 日期列 figure; plot(test_dates, YTest, 'b-', 'LineWidth',1.5); % 真实价格,蓝色实线 hold on; plot(test_dates, pred_actual, 'r--', 'LineWidth',1.5); % 预测价格,红色虚线 xlabel('日期'); ylabel('收盘价(元)'); legend('真实价格','LSTM预测'); title('LSTM股票价格预测效果(测试集)'); grid on;关键:
test_dates必须与YTest和pred_actual长度一致。若YTest是 cell 数组,需先cell2mat(YTest);若日期为 Excel 序列号(如 44197),用datestr(test_dates)转为字符串。
5. 避坑:LSTM 股票预测的五个静默陷阱与血泪解法
5.1 现象:训练 loss 快速下降至 0.001,但测试集 MAE > 5,R² < 0
原因:inputSize设置过大(如 120),导致模型记住了训练集特定波动模式,丧失泛化能力。LSTM 对长序列的记忆并非“越长越好”,而是存在有效记忆长度阈值,超过后梯度消失加剧,模型退化为线性外推。
解决:将inputSize从 120 降至 30–60,重新训练。实测 A 股日线数据,60 是最佳平衡点——短于 30 捕捉不到周线周期,长于 60 引入过多噪声。
5.2 现象:trainNetwork报错 “Invalid training data. The output responses must be a column vector or a cell array containing sequences.”
原因:YTrain被构造成矩阵(如 1×N),而非 cell 数组。regressionLayer要求标签为 N×1 向量或 {1×N} cell,矩阵输入会被视为多输出任务。
解决:检查createSequenceData中YTrain构造逻辑,确保YTrain = {y1; y2; ...; yN}(N 行 cell),而非[y1,y2,...,yN](1×N 矩阵)。用iscell(YTrain)验证。
5.3 现象:预测曲线整体上移/下移,MBE 绝对值 > MAE
原因:归一化时用了mapminmax,但反归一化时误用训练集PS_X而非PS_Y。PS_X用于输入,PS_Y用于标签,二者极值不同。
解决:严格区分PS_X和PS_Y。反归一化预测值必须用mapminmax('reverse', pred_norm, PS_Y),反归一化输入(如做特征重要性分析)才用PS_X。
5.4 现象:results.csv为空或只有 1 行
原因:main2.m中writematrix路径错误,或pred_actual为 cell 数组未转double。writematrix要求输入为 numeric matrix,cell 数组会报错或写入空文件。
解决:在writematrix前加pred_actual = cell2mat(pred_actual);(若为 cell)或pred_actual = double(pred_actual);(若为 table)。
5.5 现象:CPU 占用 100%,训练卡在 epoch 1,进度条不动
原因:'ExecutionEnvironment','cpu'下,MiniBatchSize过大(如 256)导致内存交换(swap),硬盘 IO 成瓶颈。
解决:将MiniBatchSize降至 64 或 32,并关闭 MATLAB 其他占用内存的 App(如 Live Script 编辑器)。用 Windows 任务管理器观察“提交队列长度”,若 > 10 则必降 batch size。
6. 进阶技巧:用predict做滚动预测与实时推演,以及我的强制检查清单
6.1 滚动预测:从“预测下一天”升级为“预测未来 N 天”
原项目只做单步预测(numPredictSteps=1),但实际交易需预判未来 5–10 日走势。方法是滚动预测(Rolling Forecast):用第 t 天真实值更新输入窗口,再预测 t+1;用 t+1 预测值更新窗口,预测 t+2……循环 N 次。
% 滚动预测核心函数(需插入 main2.m 末尾) function pred_roll = rollingPredict(net, lastSeq, PS_Y, N) % lastSeq: 最近 inputSize 天的真实价格(1×inputSize 向量) % N: 预测天数 pred_roll = zeros(1,N); currentSeq = lastSeq; % 初始化输入序列 for i = 1:N % 归一化当前输入 currentSeqNorm = mapminmax('apply', currentSeq, PS_X); % 预测下一天 pred_norm = predict(net, currentSeqNorm); % 反归一化 pred_actual = mapminmax('reverse', pred_norm, PS_Y); pred_roll(i) = pred_actual; % 更新输入序列:去掉最旧一天,加入最新预测值 currentSeq = [currentSeq(2:end), pred_actual]; end end % 调用示例 last60 = price(end-59:end)'; % 取最后60天 pred_10days = rollingPredict(net, last60, PS_Y, 10);注意:滚动预测误差会累积。第 10 天预测值的 MAE 通常是第 1 天的 2–3 倍。务必在
pred_roll后加fprintf('第%d天预测误差放大系数: %.2f\n', i, std(pred_roll)/std(price(end-100:end)))监控漂移。
6.2 实时推演:用save保存网络,脱离 MATLAB 环境部署
训练好的net可序列化为.mat文件,供生产环境加载:
% 训练完成后保存 save('lstm_stock_net.mat', 'net', 'PS_X', 'PS_Y'); % 部署端加载(无需 MATLAB 安装,用 MATLAB Runtime) net = load('lstm_stock_net.mat'); % 注意:部署端必须有相同版本 MATLAB Runtime(如 R2023b)关键限制:MATLAB Runtime 不支持
trainNetwork,但支持predict。因此该方案仅适用于预测阶段,训练仍需完整 MATLAB。
6.3 我的强制检查清单:每次运行前必做的五件事
我给自己立了铁律,任何 LSTM 股票预测项目启动前,必须逐项打钩:
| 检查项 | 操作 | 不通过后果 |
|---|---|---|
✅price是否为 double 向量? | class(price)必须返回'double',若为table或cell,用price = cell2mat(price) | sequenceInputLayer拒绝非 numeric 输入 |
✅XTrain是否为 cell 数组? | iscell(XTrain)返回1,且size(XTrain{1}) == [1,60] | 矩阵输入触发维度错误 |
✅PS_Y是否已保存? | exist('PS_Y','var')返回1,且PS_Y.ymin{1} < PS_Y.ymax{1} | 反归一化失败,结果全为 NaN |
✅MiniBatchSize是否 ≤ 128? | 查options.MiniBatchSize,若 >128 则强制设为 128 | CPU 内存溢出,进程崩溃 |
✅ValidationData是否启用? | options.ValidationData非空,且{XVal,YVal}长度匹配 | 无法检测过拟合,模型质量失控 |
从那以后我每次跑main2.m,都强制走一遍这个 checklist,哪怕只是改了个注释。它不花时间,但能让我避开 80% 的“为什么跑不通”问题。希望帮到你。
本文还有配套的精品资源,点击获取