资讯详情

资讯详情

Python LSTM股市预测源码全链路解析:从数据清洗到模型评估

简介基于LSTM的股市预测完整项目面向本科毕业设计、期末大作业及课程设计场景适合已有Python基础、希望系统了解深度学习时序建模的学习者。资源将行情数据读取、LSTM网络定义、训练与评估等流程封装为独立脚本并附带可直接加载的模型文件与CSV行情数据代码分段注释清晰便于读懂数据标准化、滑窗切分、模型训练和预测可视化等关键环节。压缩包共13个文件包括5个Python源码、3个编译生成的缓存、2张预测效果示意图、说明文档以及模型文件和行情数据整体仅357KB部署十分轻量。目前已有392人学习下载作为个人手打高分项目曾被导师认可并推荐用于毕设或大作业展示。运行依赖简单适合直接作为课程设计、期末大作业甚至毕业设计的核心参考。1. 为什么 Python 的 LSTM 股市预测源代码项目最值得下载的是“完整链路”如果你正在找一份能直接提交的 Python LSTM 股市预测源代码大概率是被“股票预测”这四个字勾过来的。这份代码包的核心是用 lstm 神经网络读取上证指数日线数据训练一个回归模型再用独立的 evaluate.py 算出误差并画出预测图。它完整到 data、parser_my.py、dataset.py、LSTMModel.py、train.py、evaluate.py 全都给你配好甚至连训练好的 model/stock.pkl 都放包里了。导师要看效果你不用现场跑半小时训练直接加载模型文件出图。适合三类人大四做金融科技方向的毕设、期末大作业想快速出结果、以及第一次接触时间序列预测、想拿现成代码一步步学的人。我拿到手的第一反应是先别急着脸滚模型把数据链路吃透这才是这个 98 分包最值钱的部分。2. 先吃掉数据链路parser_my.py 与 dataset.py 怎么把 K 线转成 LSTM 能吃的样本股票预测项目最容易翻车的地方不在模型而在数据输入。LSTM 不会读 CSV它只认三维张量(batch, sequence_length, input_size)。所以从data/000001SH_index.csv到你手里的X_train中间必须过两道闸parser_my.py 负责把原始表格清洗成干净的行情序列dataset.py 负责把连续K线切成一个个滑动窗口。2.1 拿到压缩包之后按这个顺序读源码解压后你会看到一个主目录里面文件不多但依赖关系很明确。不要一上来就双击 train.py先把这个顺序走一遍读 README.md看环境依赖和数据说明打开data/000001SH_index.csv确认列名和日期格式读 parser_my.py看它到底读取了哪几列读 dataset.py确认 lookback 窗口和归一化方式再读 LSTMModel.py、train.py、evaluate.py。这五步走完你基本能在心里把一条数据从 CSV 到预测图完整跑通。文件各司其职我用一个表总结文件职责运行时机data/000001SH_index.csv上证指数日线数据原始输入无parser_my.py解析 CSV清洗列名和缺失值训练前dataset.py滑动窗口切样本生成 LSTM 三维输入训练前LSTMModel.py定义 LSTM 全连接网络结构训练/推理train.py训练主循环保存模型训练时evaluate.py加载模型算误差画图训练后model/stock.pkl训练好的模型参数或模型对象推理时img/17.png, 18.png预测结果可视化图产出物注意__pycache__下面一堆.cpython-36.pyc文件说明作者环境是 Python 3.6。你自己的机器如果是 3.8 以上这些 pyc 用不了直接用.py源文件即可。2.2 parser_my.py不要直接 pandas.read_csv 就开练parser_my.py这个文件名有点随意但它的作用很核心把000001SH_index.csv变成干净、有序、只有关键列的 DataFrame。常见做法是读入 CSV 后把日期列变成索引按时间升序排序然后只保留收盘价或者包括成交量在内的若干列。典型的解析逻辑等价于下面这段# parser_my.py 的等价流程 import pandas as pd def load_stock_data(csv_pathdata/000001SH_index.csv): df pd.read_csv(csv_path, index_col0, parse_datesTrue) df df.sort_index() # 行情数据必须按时间升序 df.columns [c.strip() for c in df.columns] # 有些下载源会在停牌日生成空行这里统一丢掉 df df.dropna(subset[Close]) return df if __name__ __main__: df load_stock_data() print(df.info()) print(df.head())这里有几个容易忽略的细节parse_datesTrue把第一列索引自动转成DatetimeIndex后面画图时 x 轴才不会是乱码sort_index()很有必要因为很多数据源导出的 CSV 是日期倒序而 LSTM 窗口是按时间顺序切割的顺序反了整个训练过程学到的是反因果dropna(subset[Close])处理的是非交易日或停牌产生的空值如果直接让 NaN 进模型loss 会变成 NaN。参数上重点关注列名。000001SH_index.csv如果是从量化平台下的一般表头是Date, Open, High, Low, Close, Volume但有的平台会写成date, open, close或者带 BOM 头。parser_my.py 里通常会把列名 strip 掉就是防这个。实际处理时如果报KeyError: Close先用 Excel 打开 CSV 看真实列名。2.3 dataset.py滑动窗口和归一化谁先谁后很重要dataset.py 干两件事切窗口、归一化。第一件事很简单给定一个长度为T的收盘价序列用一个固定长度lookback的窗口从前往后滑动窗口内前lookback个点当输入第lookback1个点当标签。这样一条序列能切成T - lookback个样本。代码层面常见写法是# dataset.py 中切片逻辑的核心 import numpy as np def create_sequences(data, lookback10): X, y [], [] for i in range(len(data) - lookback): X.append(data[i:i lookback]) y.append(data[i lookback]) return np.array(X), np.array(y)这段代码作用是把一维收盘价数组切成很多个长度为lookback的小窗口。X的形状是(样本数, lookback, 1)正好符合batch_firstTrue的 LSTM 输入要求。归一化这一步我单独说因为顺序错了会出大问题。常见做法是用MinMaxScaler把收盘价缩放到[0,1]防止 LSTM 梯度爆炸。但你一定要先切训练集、再在训练集上fit这个 scaler而不是拿整条序列直接fit_transform。推荐写法from sklearn.preprocessing import MinMaxScaler close df[Close].values.reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) split int(len(close) * 0.8) train_close, test_close close[:split], close[split:] train_close_scaled scaler.fit_transform(train_close) # fit 只发生在训练集 test_close_scaled scaler.transform(test_close) # transform 沿用训练集的缩放参数 X_train, y_train create_sequences(train_close_scaled, lookback10) X_test, y_test create_sequences(test_close_scaled, lookback10)注意 fit 和 transform 的顺序不能反过来。如果先对全量数据fit_transformscaler 已经见过测试集的最大值和最小值这属于典型的数据泄漏虽然模型代码看起来能跑但评估指标会虚高。2.4 数据链路里的三个参数怎么定这套链路里最值得调的参数是lookback、train_split和batch_size。我按常见项目默认值给一张参考表参数常见值影响lookback10窗口太小捕捉不到趋势太大会减少样本数量train_split0.8训练集比例时间序列不能用随机交叉验证batch_size32显存不够就降到16数据量小影响不大hidden_size64单特征输入时64够用128以上容易过拟合num_layers2层数多不代表更好样本量不足会适得其反如果你是新手先别盲目上lookback20。指数日线一天一根 K 线10 天正好是两周交易信息既能看到短期动量又不至于把样本数砍掉太多。000001SH_index.csv如果大概两三千条数据切完后还能剩两千多个样本够训练了。这份资源里的 dataset.py 大概率也是这个思路窗口和归一化的逻辑不会跑偏。3. 拆开 LSTMModel.py输入是三维输出是一维中间发生了什么网络结构是整个项目里最短的源码但导师最可能挑刺的也是这里。很多人能把 train.py 跑通却说不清楚out[:, -1, :]为什么这么取答辩时一问就露馅。3.1 为什么选 LSTM 而不是 ARIMA 或 Transformer这个项目叫“Python基于LSTM模型实现预测股市”选 LSTM 是兼顾效果和可实现性的合理选择。ARIMA 对线性过程更友好但上证指数这种序列带有明显的非线性特征ARIMA 的差分和定阶本身就是一门玄学写进毕设容易在数据预处理环节劝退。Transformer 理论上更强但需要位置编码和更大的语料规模几百上千条日线数据根本喂不饱它。LSTM 正好在中间结构简单、PyTorch 封装完善、RNN 门控机制可以用“记住长期趋势”一句话跟导师解释。3.2 LSTMModel.py 的网络定义这个项目中 LSTMModel.py 的结构不复杂但有几个细节值得抄。我按等价逻辑还原一段# LSTMModel.py结构等价版本 import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1): super(LSTMModel, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 if num_layers 1 else 0.0 ) self.reg nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, output_size) ) def forward(self, x): out, (h_n, c_n) self.lstm(x) last_step out[:, -1, :] pred self.reg(last_step) return pred每个点单独说batch_firstTrue让输入维度变成(batch, seq_len, input_size)而不是 PyTorch 默认的(seq_len, batch, input_size)。新手最容易在这报错报错信息往往是Expected 3D input或Expected target size [32, 1]out的形状是(batch, seq_len, hidden_size)它保存了每个时间步的输出。我们要预测未来一天所以取最后一个时间步的输出也就是out[:, -1, :]最后再接一个两层全连接做非线性回归。直接nn.Linear(hidden_size, 1)也能跑但中间加一层32 ReLU的拟合能力更强导师问起来你也有话说。3.3 输入形状验证模型一定要吃 (N, 10, 1)很多翻车现场都发生在训练循环之外比如进模型前没有 reshape。拿到这个项目后建议先做一次 shape 验证用下面这个命令直接测python -c import torch; from LSTMModel import LSTMModel; mLSTMModel(); xtorch.randn(2,10,1); print(m(x).shape)输出应该是torch.Size([2, 1])意思是 2 个样本每个样本 10 个时间步、1 个特征最后输出 1 个预测值。如果你看到这条命令报错多半是 LSTMModel.py 里的类名不是LSTMModel或者 PyTorch 版本太老不支持batch_firstTrue。在 train.py 里从 DataLoader 拿出来的xb通常已经是(batch, lookback, 1)但如果自己手动构造 batch一定要显式 reshapexb xb.reshape(batch_size, lookback, -1)-1会自动推断特征维度。这里不要写成(lookback, batch_size, 1)因为模型设了batch_firstTrue顺序反了预测全乱。3.4 为什么 stock.pkl 和两张 PNG 能帮你“作弊”复现model/stock.pkl是这份资源里最值钱的文件之一。它相当于训练好的“后悔药”你在答辩现场没时间跑几百轮迭代直接加载就能画出预测曲线。但要搞清楚它是怎么存的后面加载才不会踩坑。img/17.png和img/18.png是两种典型图一个是训练 loss 下降曲线一个是真实值 vs 预测值的收盘价对比图。你不需要先跑一遍训练就能看到成品长什么样这也是这个高分项目“拿到手就能演示”的核心优势。但我不建议你只加载模型就交差还是得从 train.py 过一遍原因在下一章说。4. train.py 与 evaluate.py一节课跑完训练两分钟算出导师要的指标train.py 和 evaluate.py 是一对搭档一个负责训练一个负责验收。这个项目把两者拆开是非常符合工程习惯的训练时只需要关心 loss评估时再加载模型算指标互不污染。4.1 train.py 里最值得抄的配置都写在顶部好的训练脚本超参数应该集中在文件顶部而不是散落在循环里。拿到的版本大概率也是这个结构核心配置大概长这样# train.py 顶部配置 import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset EPOCHS 200 BATCH_SIZE 32 LEARNING_RATE 0.001 LOOKBACK 10 TRAIN_SPLIT 0.8 DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) torch.manual_seed(42)这里每个参数都有存在意义不是凑数EPOCHS200对日线数据来说刚好够用太少欠拟合太多反而过拟合到历史曲线LEARNING_RATE0.001Adam 的默认学习率一般不用动调成 0.01 很容易看到 loss 变成 NaNDEVICE先用cuda判断没有 GPU 就回落 CPU。这个数据集量级 CPU 也能跑但 GPU 会快很多torch.manual_seed(42)固定随机种子让训练结果可复现这是毕设答辩的加分项。4.2 训练循环三个小时不要乱改有了配置后训练循环主体其实很短# train.py 训练主循环 model LSTMModel(input_size1, hidden_size64, num_layers2, output_size1).to(DEVICE) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lrLEARNING_RATE) train_set TensorDataset( torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32) ) train_loader DataLoader(train_set, batch_sizeBATCH_SIZE, shuffleTrue) for epoch in range(EPOCHS): model.train() for xb, yb in train_loader: xb, yb xb.to(DEVICE), yb.to(DEVICE) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() if (epoch 1) % 20 0: print(fepoch {epoch1:03d}/{EPOCHS} loss {loss.item():.6f})两个细节你别乱改model.train()要放在循环里因为后面调model.eval()时 Dropout 和 BatchNorm 行为会变每个 batch 开始前optimizer.zero_grad()是必须的否则梯度会累加。有人为了省一行把loss.backward()改成loss.backward(retain_graphTrue)纯属给自己挖坑。4.3 evaluate.py 的指标怎么算图怎么画训练完成后evaluate.py 会承接后续工作。它要做的不只是跑一遍模型而是把归一化的预测值还原成真实指数点位。很多新手直接打印loss.item()给导师看导师一看 0.0001 觉得很厉害但这不是一个能解释业务价值的指标。严谨的做法是算三类回归指标# evaluate.py 关键流程 from sklearn.metrics import mean_absolute_error, mean_squared_error model.eval() with torch.no_grad(): pred_raw model(torch.tensor(X_test, dtypetorch.float32).to(DEVICE)) pred_np pred_raw.cpu().numpy() y_true scaler.inverse_transform(y_test.reshape(-1, 1)) y_pred scaler.inverse_transform(pred_np.reshape(-1, 1)) mae mean_absolute_error(y_true, y_pred) rmse np.sqrt(mean_squared_error(y_true, y_pred)) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 print(fMAE: {mae:.2f}) print(fRMSE: {rmse:.2f}) print(fMAPE: {mape:.2f}%)MAE 是平均绝对误差单位是“点”比如预测上证指数偏离 30 点就是 MAE30RMSE 对大偏差更敏感同样条件下它通常会比 MAE 大一点MAPE 是百分比适合和导师直观汇报。如果你是金融商贸方向的毕设建议把 MAPE 放主要位置导师容易理解。4.4 模型保存与加载stock.pkl 的正反两种用法这个项目把模型文件命名为model/stock.pkl而不是常见的.pth或.pt。PyTorch 并不看重后缀它只看文件内部是什么。我复现时一般会这么保存# 训练结束后的保存代码 torch.save(model.state_dict(), model/stock.pkl)预测阶段再加载model LSTMModel(input_size1, hidden_size64, num_layers2, output_size1) model.load_state_dict(torch.load(model/stock.pkl, map_locationDEVICE)) model.eval()如果项目作者图省事用torch.save(model, model/stock.pkl)保存的是整个模型对象那加载时就要用torch.load(model/stock.pkl)整体加载再调用.eval()。怎么判断是哪种直接跑上面的load_state_dict如果报Missing key(s) in state_dict说明文件里不是 state_dict而是完整模型对象。两种都试一下就能确认这不是玄学是 torch.save 两种常见用法。5. 避坑与排查复现这份 LSTM 股市预测项目时的 5 个真实卡点源码可以跑通和你能顺利复现是两回事。这一章我整理了 5 个在这个项目里最容易出现的坑每条都是“现象 → 原因 → 解决”的结构。5.1 预测曲线是“一条平移的线”现象用 evaluate.py 画出来的预测曲线和真实收盘价几乎重合但仔细看会发现预测值整体右移或左移了一个位置误差看着很大。原因股票序列接近随机游走每一天的收盘价和前一天高度相关。模型如果只学“复制昨天”也能拿到很低的 loss但画出来就是一条滞后一天的曲线。这不是代码 bug而是单步回归预测的通病。解决把预测目标从原始收盘价改成“未来一天与当前窗口最后一日的收盘价之差”也就是构造 y 时做一阶差分。在 dataset.py 里把y.append(data[i lookback])改成基于差分的结果。或者在评估时改用滚动预测不要让预测值之间共享未来信息。5.2 Python 版本与__pycache__冲突现象解压后直接运行脚本控制台报Invalid or corrupt pyc或 import 错误。原因压缩包里带的是dataset.cpython-36.pyc这类 Python 3.6 编译缓存而本地环境是 3.8 甚至 3.11解释器无法直接复用旧 pyc 文件。解决删除整个__pycache__目录然后直接用.py源文件运行。如果想要完全复刻作者环境用 conda 创建 Python 3.6 环境再跑。不要试图把.pyc当加密保护文件保留它只会制造问题。5.3 用 pickle.load 打开 stock.pkl 失败现象写了一段pickle.load(open(model/stock.pkl, rb))结果报ModuleNotFoundError: No module named LSTMModel或者报维度不匹配。原因这个.pkl大概率是torch.save产物而不是 Python 标准库 pickle 保存的数据。torch.save的默认格式里包含 torch 内部结构必须用 torch 自己的load才能正确还原。解决改用torch.load(model/stock.pkl, map_locationcpu)。如果文件是完整模型对象直接拿到 model如果是 state_dict就先把模型实例化再load_state_dict。map_location 传cpu是为了避免在cpu构建的机器上加载时出现 CUDA 张量错误。5.4 loss 训练到一半变成 NaN现象训练前十几轮 loss 正常后面突然变成nan预测结果全部变成空。原因最常见的是学习率太大或者输入数据没有归一化。LSTM 内部累乘较多数值很容易溢出。如果用了MinMaxScaler但 fit 的是全量数据也可能导致训练集最大值和实际分布不一致间接放大梯度。解决先把LEARNING_RATE降到 0.0005 或 0.001确保只有Close列参与训练。再给梯度裁剪兜底在optimizer.step()前加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)梯度裁剪不会直接提升准确率但能防止训练崩掉算是给毕设加了一道保险。5.5 训练集里 shuffleTrue为什么测试集还是乱现象训练时 loss 下降很漂亮但 evaluate 时预测曲线和真实曲线根本对不上或者测试集误差异常大。原因滑动窗口生成的样本不是独立的第 i 个样本的预测目标很可能被包含在第 i1 个样本的输入窗口里。如果对全量样本做随机 shuffle 再切分同一天的行情会同时出现在训练集和测试集形成隐形泄漏。解决必须先按时间顺序切分再对训练集 loader 做 shuffle。测试集永远保持原始时间顺序这样画出来的预测图才是一条能看的连续曲线。具体做法我在 2.3 节已经给出切分顺序不要反过来。6. 把 98 分项目改成自己的一个多步预测的滚动窗口技巧很多导师看到“预测下一日收盘价”会觉得格局小了因为实务中更关心未来一周的走势。你可以在训练好 stock.pkl 的基础上加一个滚动预测函数把单步预测能力扩展成多步预测。6.1 滚动预测代码把预测值当作新输入喂回去# 利用训练好的模型做未来 5 天滚动预测 def rolling_predict(model, initial_window, steps5): model.eval() window initial_window.reshape(1, lookback, 1) preds [] with torch.no_grad(): for _ in range(steps): p model(torch.tensor(window, dtypetorch.float32).to(DEVICE)).item() preds.append(p) # 窗口左移一格把最新预测放在最后 window np.roll(window, -1, axis1) window[:, -1, 0] p return scaler.inverse_transform(np.array(preds).reshape(-1, 1))核心思路是先用最后 10 个真实交易日作为初始窗口预测第 11 天把第 11 天的预测值补到窗口末尾丢掉最老的第 1 天再次预测第 12 天。这样循环 5 次就得到未来 5 个交易日的收盘价预测。缺点是误差会逐步累积预测越往后越不可靠所以结果只能叫“趋势参考”不能当实盘信号。我从这个项目里学到的最实用教训是拿到任何时间序列预测代码第一步永远是先把数据和时间轴画出来看序列是不是升序、有没有断点再谈改模型。从那以后我每次复现新的股票预测项目都会强制先跑一遍parser_my的可视化输出把真实曲线和预测曲线放在同一张图里看而不是只看 loss 数字。希望帮到你少走这几个弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →