ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LSTM股票预测模型实战:从数据处理到PyTorch实现与避坑指南

LSTM股票预测模型实战:从数据处理到PyTorch实现与避坑指南 简介面向机器学习期末大作业、金融量化入门及课程设计的一款基于长短期记忆网络的股票/基金预测模型项目完整覆盖数据准备、模型训练与预测结果展示环节。资源共11个文件包含5个Excel数据文件、3个Python脚本、1份Markdown说明、1张预测效果图与1个缓存文件压缩包整体仅155KB轻量易部署。已有224人学习下载。数据文件提供基金训练集、测试集、一年期收益排行以及预测结果对比等素材Python脚本包含LSTM模型定义、视图逻辑与基金净值数据获取功能配合说明文档与可视化结果可清晰理解各模块的调用关系与预测流程。读者可在此基础上替换成自己的股票或基金行情数据调节批大小、时间步长等参数完成可复现的预测实验适合课程设计、毕业设计或项目复现参考。资源虽小但结构完整适合快速上手验证长短期记忆网络在金融场景中的预测效果。1. 用 LSTM 预测股价之前先弄清这个“高分项目”到底在预测什么拿到一个名为“基于机器学习 LSTM 的股票/基金预测模型高分项目”的压缩包很多人的第一反应是解压、跑通、看曲线然后幻想明天该买哪只。这个思路恰恰是错的。这类项目的核心价值不是给你一个“稳赚不赔的选股器”而是让你完整走一遍“机器学习应用流程”从原始行情数据到特征工程从 LSTM 神经网络搭建到时序预测模型评估最后在回测中理解预测模型的边界。适合谁适合正在做课程设计、毕设或者想入门时间序列预测的开发者。它帮你把“机器学习能做什么”和“股票数据长什么样”两件事串起来。但如果你指望它直接指导实盘交易我建议趁早打消这个念头——作为工程师我会把这类项目定位成“练手的高分模板”而不是“印钞机”。2. 数据是预测模型的黑匣子从原始行情到 LSTM 输入张量的完整处理流程2.1 用机器学习处理行情数据的第一步先决定预测目标而不是先找数据大多数从网上下载的 LSTM 股票预测项目代码里都会有这么一行df[close]然后直接构造滑窗。但我在实际做预测模型时第一步永远是反问自己你到底要预测什么是预测明天收盘价的具体数值还是预测未来 5 天的涨跌方向这两种目标对应的数据标签构造方式完全不同模型架构和评估指标也不一样。常见做法是预测“下一个交易日的收盘价”这是一个回归问题。标签就是t1时刻的 close 价格特征是t时刻之前一段窗口内的所有可用维度——通常是开盘价、最高价、最低价、收盘价、成交量再可选地加上技术指标如 MA5、RSI 等。如果你要预测涨跌方向那就把连续标签变成二分类1表示明天收盘价比今天高0表示低。这个选择直接决定损失函数是MSELoss还是CrossEntropyLoss也决定输出层是Linear(隐藏层, 1)还是Linear(隐藏层, 2)。我强烈建议新手先从“预测收盘价数值”入手因为回归任务更容易收敛也更容易画出“预测值 vs 真实值”的曲线可视化效果好答辩的时候也更好讲故事。但要注意数值预测的评估门槛很低——你随便拿昨天的价格当预测值RMSE 都没那么难看所以后面一定要补方向准确率这类指标。2.2 特征工程与滑窗构造把时间序列切成 LSTM 能吃的样本LSTM 神经网络处理的是序列数据输入形状是(batch, seq_len, feature_dim)。原始行情是一张长表需要切成一个个固定长度的窗口。比如我们用过去 60 个交易日的特征预测第 61 天的收盘价那 seq_len 就是 60feature_dim 就是特征列数。下面是我常用的滑窗构造代码用纯 Python 实现避免引入复杂依赖import numpy as np def create_sequences(features, target, seq_len60): 把特征矩阵和标签切成滑动窗口样本 features: shape (n_samples, n_features) target: shape (n_samples,) seq_len: 过去多少个时间步 返回 X: (n_samples-seq_len, seq_len, n_features) y: (n_samples-seq_len,) X, y [], [] for i in range(seq_len, len(features)): X.append(features[i-seq_len:i, :]) # 取前 seq_len 行所有特征 y.append(target[i]) # 当前时刻的真实标签 return np.array(X), np.array(y) # 假设 raw_feature 是已经归一化后的特征矩阵, raw_target 是价格序列 X, y create_sequences(raw_feature, raw_target, seq_len60) print(X shape:, X.shape) # 例如 (941, 60, 8) print(y shape:, y.shape) # 例如 (941,)这段代码的逻辑非常直接从第seq_len个位置开始不断往后滑一个单位每次都取前seq_len行作为模型输入当前行作为标签。注意这里y[i]是target[i]也就是说我们用 0~59 天的数据预测第 60 天的值。如果你想预测未来第 2 天、第 5 天就把索引改成target[idelta]并相应地调整返回的样本总数。参数说明seq_len是最敏感的超参数之一。设为 60 意味着模型依赖过去约 3 个月的交易日A 股大概 20 个交易日一个月。时间序列预测模型里窗口太短学不到趋势窗口太长容易引入旧数据噪声。实际调参时我一般从 20、30、60、90 几档里选结合训练集大小来定后面避坑章会专门讲。2.3 归一化与训练集/验证集/测试集划分最容易翻车的时序切分数据处理里最隐蔽的坑是直接用全量数据的均值和标准差做归一化。这在普通机器学习任务里很常见但用在时间序列预测模型上就是数据泄漏——因为测试集的信息在训练之前就被模型“看到”了。正确的做法是先把数据按时间顺序分成三段——训练集比如前 70%、验证集15%、测试集最后 15%然后只用训练集的统计量去归一化所有数据。看代码from sklearn.preprocessing import MinMaxScaler # 按时间顺序切分绝不随机打乱 train_size int(len(features) * 0.7) val_size int(len(features) * 0.15) train_data features[:train_size] val_data features[train_size:train_sizeval_size] test_data features[train_sizeval_size:] # 只拟合训练集 scaler MinMaxScaler(feature_range(0, 1)) scaler.fit(train_data) train_scaled scaler.transform(train_data) val_scaled scaler.transform(val_data) test_scaled scaler.transform(test_data)注意scaler.fit(train_data)这一步。fit 只会计算训练集的 min/max然后用同一套参数去 transform 验证集和测试集。这样测试集的信息不会污染训练过程。如果偷懒写成scaler.fit(features)再全量 transform模型在训练时就已经“见过”测试集的数值分布测试集就失去了检验泛化能力的作用。另外绝对不要用train_test_split(random_state42)这类函数做随机切分因为时间序列一旦乱序模型学到的就是“未来预测过去”的伪规律训练损失可能很漂亮实际效果一塌糊涂。这个踩坑点我在下面避坑章会再强调一次。3. 搭建 LSTM 股票预测模型PyTorch 实现与关键参数调优3.1 LSTM 神经网络的核心结构为什么它比全连接更适合时序预测模型很多机器学习入门资料里LSTM 被描述成一个很神秘的“记忆结构”其实拆开看并不复杂。相比全连接网络LSTM 引入了“细胞状态”和“门控机制”遗忘门决定过去哪些信息要丢弃输入门决定新信息如何写入输出门决定当前时刻要输出什么。这让网络在长序列上拥有选择性记忆能捕捉价格数据的趋势和周期性。当然股票价格噪声极大LSTM 不会创造信息它只是从特征中拟合一个非线性映射。我用 PyTorch 实现过一个最小可运行的 LSTM 预测模型结构如下输入经过一个 LSTM 层所有时间步的输出取最后一个时间步的隐状态再接一个全连接层输出预测值。下面给出完整代码可以直接跑通。3.2 最小可运行模型代码从单层 LSTM 到多层堆叠import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size8, hidden_size64, num_layers2, output_size1, dropout0.2): input_size: 特征维度比如 8 列特征 hidden_size: 隐藏层神经元数 num_layers: LSTM 层数 output_size: 预测目标维度回归任务设置为 1 dropout: 多层 LSTM 时层间随机失活比例 super(LSTMPredictor, self).__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) # out 包含所有时间步输出 # 取最后一个时间步的输出 last_out out[:, -1, :] # (batch, hidden_size) pred self.fc(last_out) # (batch, output_size) return pred # 实例化模型 model LSTMPredictor(input_size8, hidden_size64, num_layers2) # 随机造一批数据: 4 个样本每个样本 60 个时间步8 个特征 dummy_x torch.randn(4, 60, 8) print(output shape:, model(dummy_x).shape) # (4, 1)这段代码里batch_firstTrue表示输入张量的第一维是 batch这样更符合 PyTorch 习惯。out[:, -1, :]是取最后一个时间步的隐藏层输出这是回归任务最常见的做法。为什么不取所有时间步的平均或拼接因为最后一个时间步的隐状态已经包含了前面所有信息的压缩表达如果取平均会稀释近期信息。当然你可以在上面加注意力机制但基础模型先用最简单的结构。num_layers2是经验值。单层 LSTM 表达能力有限难以拟合复杂趋势三层以上在股票这种高噪声数据上很容易过拟合而且训练时间明显变长。我建议新手固定为 2等模型收敛后再试 1 或 3。hidden_size64也是一个起步值数据量小的时候 32 也够用数据量大可以上 128。3.3 必调的三个参数序列长度、隐藏层大小、学习率第一个必调参数是序列长度seq_len前面提过。它决定模型“回头看多远”。有量化研究说股票价格短期动量效应大约在 5~20 个交易日中期在 60 天左右。你可以做一个小实验把 seq_len 从 10 逐步调到 120每个值跑一遍训练比较验证集 RMSE。你会发现 seq_len 从 20 到 60 时性能提升明显超过 90 后往往开始变差——因为太久远的市场环境对当前价格影响已经很小反而把模型参数往噪声里带。第二个是hidden_size。隐藏单元数决定了 LSTM 的记忆容量。太小模型记不住必要的模式太大参数量暴增训练集不大时很快就出现过拟合。我习惯用“数据量除以参数量的经验法则”训练样本 1000 条以内hidden_size 不超过 325000 条左右可以到 64超过一万条再考虑 128。当然这个法则不是严格公式但能避免一上来就把模型堆到 256 导致训练半天不收敛。第三个是学习率。PyTorch 里我常用Adam优化器它的默认学习率是1e-3但这个默认值在 LSTM 上并不总是好用。股票价格范围如果被归一化到 0~11e-3通常可以接受如果数据没归一化或者分布偏斜损失很容易 NaN。我在实践中会先用1e-3跑 10 个 epoch如果损失震荡不降就降到3e-4或1e-4。也可以加学习率调度器import torch.optim as optim optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) for epoch in range(50): train_loss train_one_epoch(model, optimizer, train_loader) val_loss evaluate(model, val_loader) scheduler.step(val_loss) # 验证损失不降则自动减半学习率注意scheduler.step要传入验证损失因为ReduceLROnPlateau监控的是验证集指标而非训练集。这是很多人容易忽略的细节调度器如果踩在训练损失上基本没意义因为训练损失总是降的。学习率衰减的 patience 设 5意味着连续 5 个 epoch 验证损失没刷新最低记录就把学习率乘以 0.5给模型一个“精细调整”的机会。4. 训练与评估别被回测曲线骗了高分项目的高分从哪来4.1 损失函数与优化器选择回归预测不是分类股票价格预测通常是回归任务最常用的是均方误差MSE公式是预测值和真实值差的平方的平均。MSE 对大误差有很强的惩罚这既是优点也是缺点——LSTM 训练时如果某一天价格突然暴涨暴跌个别大误差会让损失剧烈波动。因此也有人用平均绝对误差MAE或者 Huber Loss后者结合了两者特性。PyTorch 里可以直接用nn.MSELoss或nn.HuberLoss。优化器我基本只用 Adam因为它在梯度稀疏和噪声大的场景下表现稳定。需要注意的是 weight decayL2 正则不要设太高常见做法的1e-4到1e-3之间。股票数据本身噪声大不加正则化很容易把训练集背得滚瓜烂熟加多了模型又会变得过于平滑连趋势都拟合不出来。我一般固定weight_decay1e-4。训练代码要写好批量迭代和梯度清零这是 PyTorch 初学者的高频翻车点def train_one_epoch(model, optimizer, data_loader, loss_fn): model.train() total_loss 0 for X_batch, y_batch in data_loader: optimizer.zero_grad() # 清空上一步梯度 y_pred model(X_batch) # 前向传播 loss loss_fn(y_pred, y_batch) # 计算损失, y_batch 需要 reshape 成和 y_pred 一致 loss.backward() # 反向传播 optimizer.step() # 更新参数 total_loss loss.item() return total_loss / len(data_loader)特别注意y_batch的形状。nn.Linear输出形状是(batch, 1)而标签形状可能是(batch,)直接算 MSE 时 PyTorch 会广播但容易出错。最好在构造数据集时就把标签 reshape 成(batch, 1)或者用y_batch.view(-1, 1)。4.2 评估指标用 RMSE、MAE 和方向准确率判断模型好坏很多课程设计项目只画一条“预测值 vs 真实值”的折线图看起来贴合得不错然后宣称“准确率达到 95%”。这其实是自欺欺人因为价格预测的误差只要不超过几个点肉眼看起来就高度重合。我评估时序预测模型时必看三个指标RMSE均方根误差单位和价格一致。比如 RMSE 为 2.5 元代表平均预测偏差 2.5 元。MAE平均绝对误差更接近人直觉和 RMSE 的差距能反映误差分布是否有极端值。方向准确率预测涨跌方向今天预测明天涨明天实际是否涨的正确比例。这个指标才是对交易最有意义的但往往被忽略。方向准确率的计算很有讲究。即使模型预测的数值偏差很大只要方向对就能赚钱反过来数值贴得很紧但方向错了实盘照样亏。一个能用的方向准确率应该在 52%~55% 以上对 A 股这种接近随机的市场51% 已经不算差如果只有 48%~50%说明模型基本没学到方向信息只能当预测均值用。下面是一个简单评估函数def evaluate_model(model, data_loader): model.eval() preds, trues [], [] with torch.no_grad(): for X_batch, y_batch in data_loader: y_pred model(X_batch) preds.append(y_pred.numpy().flatten()) trues.append(y_batch.numpy().flatten()) preds np.concatenate(preds) trues np.concatenate(trues) rmse np.sqrt(np.mean((preds - trues) ** 2)) mae np.mean(np.abs(preds - trues)) # 方向准确率忽略相等的情况 direction_pred np.sign(np.diff(preds)) direction_true np.sign(np.diff(trues)) direction_acc np.mean(direction_pred direction_true) return rmse, mae, direction_acc注意np.diff是相邻两个预测值的差如果差为 0 则 sign 为 0股票预测模型很少出现完全相同的连续预测但最好过滤掉这些样本否则会虚高。这个方向准确率的计算前提是预测序列和真实序列按时间对齐并且是连续预测值而不是单步预测后拼接的离散点。4.3 训练过程监控早停法与模型保存的后悔药训练 LSTM 就像煮一碗面火候过了就糊了。神经元网络的深度学习训练中常见现象是训练损失持续下降验证损失先降后升这就是过拟合的开始。我没有耐心每次手动盯着曲线最有效的做法是早停如果验证损失连续 N 个 epoch 没有创新低就停止训练并恢复到最后一次验证损失最低的模型权重。这需要写一个“模型保存 恢复”的流程也是项目里经常缺的一块。没有早停最终保存的往往是最后一个 epoch 的模型而不是验证集上表现最好的那个。这就像考试成绩最后关头没发挥好却拿期末成绩当最终分数。代码如下best_val_loss float(inf) patience 15 wait 0 for epoch in range(100): train_loss train_one_epoch(...) val_loss evaluate(...) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pt) # 保存最优权重 wait 0 else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break # 训练结束后加载最优模型 model.load_state_dict(torch.load(best_model.pt))这是一个典型的“后悔药”设计每次验证损失刷新最低值就保存一份快照后续哪怕模型继续训练过拟合了还能回滚到最优状态。patience15意味着最多允许 15 个 epoch 不进步。这个值在股票预测任务里比较合理因为验证损失本来就有随机波动太小像 5 可能把进行一次正常波动误判为停滞太大会浪费训练时间。这里还有个细节保存模型时只保存state_dict不要保存整个model对象因为重建模型结构后加载权重更灵活而且文件更小也不容易因为 PyTorch 版本不同而报错。5. LSTM 预测模型的常见坑与排查从数据泄漏到预测值平移5.1 数据泄漏归一化时用全量统计量是最隐蔽的坑现象测试集上的 RMSE 比训练集还低模型“完美”预测未来行情实盘却完全失效。原因数据归一化时对整个数据集调用了scaler.fit(features)。MinMaxScaler 在计算最大值最小值时已经包含了测试集的价格范围相当于把测试集的最高价、最低价信息偷偷告诉了模型。训练时模型看到的输入分布里混入了未来信息所以它学到的“规律”里包含了对未来数据的记忆。解决按时间顺序先切训练、验证、测试然后只对训练集做fit再用同样的 scaler 转换验证集和测试集。代码在第 2.3 节已经给出这是最简单的修复。另外要检查是否误用了shuffleTrue在时间序列上下文中。DataLoader 里千万别开shuffleTrue否则训练集和验证集的数据会被打乱时间顺序完全破坏。5.2 预测值滞后为什么模型输出的曲线总是比真实值“慢半拍”现象预测曲线和真实曲线形状非常相似但明显向右平移即真实值涨到高点后预测值才跟着往上涨真实值跌了预测值还维持原样。这样的模型看起来“贴合度极高”其实只是把昨天的价格搬到了今天。原因这是纯回归模型在随机数据上的必然现象。因为股票价格接近随机游走最好的回归预测最小化 MSE就是预测条件均值而条件均值在无明确趋势时最接近上一个观测值。LSTM 学到的最优策略就是“复制上一个时刻的价格”。因此预测值等于前一日收盘价加上一个小修正曲线自然滞后一天。解决不要再用 RMSE 作为唯一指标加入方向准确率。如果方向准确率只有 50% 左右说明模型本质是“昨天价格预测法”。可以尝试改变预测目标——不直接预测价格而是预测价格差分即涨跌幅这样模型不能依赖“复制上一个价格”被迫学习涨跌模式。代码上只需要把标签从close[t]改成close[t] - close[t-1]预测后再加回基准价格。这个简单变更通常能让方向准确率有明显提升。5.3 过拟合与欠拟合训练损失降了测试却一塌糊涂现象训练损失很低验证和测试损失高得离谱或者训练损失一直不降。原因过拟合是因为模型容量太大、训练数据太少或者循环训练了太多 epoch。欠拟合则相反——隐藏层太小、序列长度太短、模型没有学到足够模式。还有一类特殊情况是梯度爆炸或梯度消失LSTM 虽然比普通 RNN 好些但多层堆叠后深层梯度仍可能异常。解决先看训练曲线。如果训练损失和验证损失都在下降但差距大是过拟合对策依次为减小 hidden_size、减少 num_layers、增大 dropout、增加 weight_decay、加早停。如果训练损失本身就不降是欠拟合或学习率问题先调低学习率试几个 epoch如果仍然不降增大 hidden_size 和 seq_len。还有一个我常用的快速排查先拿一小段训练数据比如 100 个样本跑通一轮看模型能不能把这一批数据过拟合到损失基本为 0。如果不能说明代码有 bug——比如梯度没清零、标签形状不对、归一化有 NaN。5.4 股票涨跌随机性当模型告诉你明天涨你该怎么办现象模型在测试集上方向准确率 53%看似比随机好一点但一实盘就亏手续费和滑点吃掉了收益。原因股票市场接近弱式有效。LSTM 模型能捕捉到的“规律”往往非常微弱而且会在不同时间段失效。课程设计项目里的回测是静态的——用过去的数据测试模型测完就结束。但真实市场是动态的模型训练时的规律可能在下个月就不存在了。解决调低预期。在做这类预测模型时我会把目标设定为“比随机略好”而不是“必赚”。你可以用滚动回测来验证稳定性训练集每次向前滚动 20 个交易日重新训练模型再预测下 20 个交易日重复多次看方向准确率是否稳定。如果只在某一段特定行情里有效那基本说明模型只是在拟合历史记忆。另外严格把交易成本计入模拟——每次预测涨了就买入跌了就空仓假设每次买卖手续费 0.1%滑点 0.05%看看最终收益是否仍为正。大部分课程项目做到这里就会现原形。6. 把模型用在基金预测上迁移要点与一个可落地的验证技巧6.1 从股票到基金数据频率、复权与净值处理的差异很多高分项目会同时演示股票和基金预测但直接从股票迁移到基金会踩数据坑。基金净值不像股票有分钟级或日级高频数据普通开放式基金一天只有一个单位净值样本量小。而且基金净值已经经过平滑处理涨跌波动更小LSTM 模型会学得更“懒”——直接预测净值持平损失也能控制在很小的范围。我一般会改用周频数据扩大时间跨度或引入指数对比基准作为额外特征。还要注意分红和拆分股票用前复权价格基金净值要用累计净值或者把分红再投资处理否则会出现净值断崖式下跌模型误以为学到了暴跌规律。6.2 验证技巧用滚动回测替代一次性划分看稳定性前面提过滚动回测是评估时序预测模型的更好方式。这里给出一个具体实现思路把完整数据集分为若干段每次用前 80% 的数据训练后 20% 预测然后整体向后滑动 20 个交易日重复 5 次。记录每次的 RMSE、方向准确率和模拟收益。如果多次结果的标准差很小说明模型稳定如果第一次效果很好、之后效果变差说明模型只是过拟合了某段行情。def rolling_backtest(features, target, model_fn, seq_len60, step20, n_folds5): results [] total_len len(features) start 0 for i in range(n_folds): train_end start int(total_len * 0.6) # 每次只取固定比例作为训练 test_end train_end step train_data features[start:train_end] test_data features[train_end:test_end] # ... 这里执行归一化、滑窗、训练、预测 ... # 记录 rmse, direction_acc results.append((rmse, direction_acc)) start step if test_end total_len: break return np.array(results)这个函数的参数一概按需求调整。要点是每次只在训练段上做归一化绝不能把 test 段的信息混进 scaler。如果滚动 5 次的结果差异太大我会回头检查特征比如某种技术指标在不同行情下是否失效。6.3 进阶多因子输入与注意力机制是否值得加如果你已经完成了基础 LSTM 预测模型想让它更像一个“高分项目”可以往两个方向加东西。一是多因子输入不止用 OHLCV还可以加入技术指标如 MACD、RSI、布林带甚至加入其他股票或指数的走势特征让input_size从 8 变成十几。这会增加模型表达能力但也要注意特征之间的量纲差异归一化必须分别处理。二是注意力机制在 LSTM 的输出上叠加一个 Attention 层让模型对序列中更重要的时间步赋予更高权重。比如“放量突破”那天注意力权重会集中在突破时段。PyTorch 实现注意力不算难但在股票预测上注意力带来的提升往往有限因为有效规律本身就弱。我的建议是先把基础模型做扎实如果提升方向准确率遇到瓶颈再考虑注意力这样项目更有层次感也不会因为过度复杂而难以答辩。我自己的习惯是每轮实验都先把随机种子固定住再把数据划分文件存成 CSV这样不同模型之间对比才公平。很多项目翻车不是因为模型不好而是因为每次跑数据切分都不一样把随机性当成了模型能力。这个教训让我在做任何时序预测模型时都先把数据流固定再谈调参。希望帮到你。本文还有配套的精品资源点击获取
返回列表