☰
Python LSTM股市预测源码全链路解析:从数据清洗到模型评估
2026/10/10 8:23:33 网站建设 项目流程

简介:基于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,先把这个顺序走一遍:

  1. 读 README.md,看环境依赖和数据说明;
  2. 打开data/000001SH_index.csv,确认列名和日期格式;
  3. 读 parser_my.py,看它到底读取了哪几列;
  4. 读 dataset.py,确认 lookback 窗口和归一化方式;
  5. 再读 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_path="data/000001SH_index.csv"): df = pd.read_csv(csv_path, index_col=0, parse_dates=True) 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_dates=True把第一列索引自动转成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个点当输入,第lookback+1个点当标签。这样一条序列能切成T - lookback个样本。

代码层面常见写法是:

# dataset.py 中切片逻辑的核心 import numpy as np def create_sequences(data, lookback=10): 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_first=True的 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, lookback=10) X_test, y_test = create_sequences(test_close_scaled, lookback=10)

注意 fit 和 transform 的顺序不能反过来。如果先对全量数据fit_transform,scaler 已经见过测试集的最大值和最小值,这属于典型的数据泄漏,虽然模型代码看起来能跑,但评估指标会虚高。

2.4 数据链路里的三个参数怎么定

这套链路里最值得调的参数是lookback、train_split和batch_size。我按常见项目默认值给一张参考表:

参数常见值影响
lookback10窗口太小捕捉不到趋势,太大会减少样本数量
train_split0.8训练集比例,时间序列不能用随机交叉验证
batch_size32显存不够就降到16,数据量小影响不大
hidden_size64单特征输入时64够用,128以上容易过拟合
num_layers2层数多不代表更好,样本量不足会适得其反

如果你是新手,先别盲目上lookback=20。指数日线一天一根 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_size=1, hidden_size=64, num_layers=2, output_size=1): super(LSTMModel, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.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_first=True让输入维度变成(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; m=LSTMModel(); x=torch.randn(2,10,1); print(m(x).shape)"

输出应该是torch.Size([2, 1]),意思是 2 个样本,每个样本 10 个时间步、1 个特征,最后输出 1 个预测值。如果你看到这条命令报错,多半是 LSTMModel.py 里的类名不是LSTMModel,或者 PyTorch 版本太老不支持batch_first=True。

在 train.py 里,从 DataLoader 拿出来的xb通常已经是(batch, lookback, 1),但如果自己手动构造 batch,一定要显式 reshape:

xb = xb.reshape(batch_size, lookback, -1)

-1会自动推断特征维度。这里不要写成(lookback, batch_size, 1),因为模型设了batch_first=True,顺序反了预测全乱。

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)

这里每个参数都有存在意义,不是凑数:

  • EPOCHS=200:对日线数据来说刚好够用,太少欠拟合,太多反而过拟合到历史曲线;
  • LEARNING_RATE=0.001:Adam 的默认学习率,一般不用动,调成 0.01 很容易看到 loss 变成 NaN;
  • DEVICE:先用cuda判断,没有 GPU 就回落 CPU。这个数据集量级 CPU 也能跑,但 GPU 会快很多;
  • torch.manual_seed(42):固定随机种子,让训练结果可复现,这是毕设答辩的加分项。

4.2 训练循环:三个小时不要乱改

有了配置后,训练循环主体其实很短:

# train.py 训练主循环 model = LSTMModel(input_size=1, hidden_size=64, num_layers=2, output_size=1).to(DEVICE) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE) train_set = TensorDataset( torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32) ) train_loader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True) 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(f"epoch {epoch+1:03d}/{EPOCHS} loss {loss.item():.6f}")

两个细节你别乱改:

  • model.train()要放在循环里,因为后面调model.eval()时 Dropout 和 BatchNorm 行为会变;
  • 每个 batch 开始前optimizer.zero_grad()是必须的,否则梯度会累加。有人为了省一行把loss.backward()改成loss.backward(retain_graph=True),纯属给自己挖坑。

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, dtype=torch.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(f"MAE: {mae:.2f}") print(f"RMSE: {rmse:.2f}") print(f"MAPE: {mape:.2f}%")

MAE 是平均绝对误差,单位是“点”,比如预测上证指数偏离 30 点就是 MAE=30;RMSE 对大偏差更敏感,同样条件下它通常会比 MAE 大一点;MAPE 是百分比,适合和导师直观汇报。如果你是金融商贸方向的毕设,建议把 MAPE 放主要位置,导师容易理解。

4.4 模型保存与加载:stock.pkl 的正反两种用法

这个项目把模型文件命名为model/stock.pkl,而不是常见的.pth或.pt。PyTorch 并不看重后缀,它只看文件内部是什么。我复现时一般会这么保存:

# 训练结束后的保存代码 torch.save(model.state_dict(), "model/stock.pkl")

预测阶段再加载:

model = LSTMModel(input_size=1, hidden_size=64, num_layers=2, output_size=1) model.load_state_dict(torch.load("model/stock.pkl", map_location=DEVICE)) 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_location="cpu")。如果文件是完整模型对象,直接拿到 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_norm=1.0)

梯度裁剪不会直接提升准确率,但能防止训练崩掉,算是给毕设加了一道保险。

5.5 训练集里 shuffle=True,为什么测试集还是乱

现象:训练时 loss 下降很漂亮,但 evaluate 时预测曲线和真实曲线根本对不上,或者测试集误差异常大。

原因:滑动窗口生成的样本不是独立的,第 i 个样本的预测目标,很可能被包含在第 i+1 个样本的输入窗口里。如果对全量样本做随机 shuffle 再切分,同一天的行情会同时出现在训练集和测试集,形成隐形泄漏。

解决:必须先按时间顺序切分,再对训练集 loader 做 shuffle。测试集永远保持原始时间顺序,这样画出来的预测图才是一条能看的连续曲线。具体做法我在 2.3 节已经给出,切分顺序不要反过来。

6. 把 98 分项目改成自己的:一个多步预测的滚动窗口技巧

很多导师看到“预测下一日收盘价”会觉得格局小了,因为实务中更关心未来一周的走势。你可以在训练好 stock.pkl 的基础上,加一个滚动预测函数,把单步预测能力扩展成多步预测。

6.1 滚动预测代码:把预测值当作新输入喂回去

# 利用训练好的模型做未来 5 天滚动预测 def rolling_predict(model, initial_window, steps=5): model.eval() window = initial_window.reshape(1, lookback, 1) preds = [] with torch.no_grad(): for _ in range(steps): p = model(torch.tensor(window, dtype=torch.float32).to(DEVICE)).item() preds.append(p) # 窗口左移一格,把最新预测放在最后 window = np.roll(window, -1, axis=1) window[:, -1, 0] = p return scaler.inverse_transform(np.array(preds).reshape(-1, 1))

核心思路是:先用最后 10 个真实交易日作为初始窗口,预测第 11 天;把第 11 天的预测值补到窗口末尾,丢掉最老的第 1 天;再次预测第 12 天。这样循环 5 次,就得到未来 5 个交易日的收盘价预测。缺点是误差会逐步累积,预测越往后越不可靠,所以结果只能叫“趋势参考”,不能当实盘信号。

我从这个项目里学到的最实用教训是:拿到任何时间序列预测代码,第一步永远是先把数据和时间轴画出来,看序列是不是升序、有没有断点,再谈改模型。从那以后,我每次复现新的股票预测项目,都会强制先跑一遍parser_my的可视化输出,把真实曲线和预测曲线放在同一张图里看,而不是只看 loss 数字。希望帮到你少走这几个弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询