简介:一套基于Python与LSTM循环神经网络的股票价格预测源码,以上证指数CSV历史数据为分析对象,面向高校期末大作业和课程设计场景,适合需要快速搭建预测模型并梳理数据预处理、网络训练与效果评估全流程的学习者参考。压缩包共13个文件,体积约360KB,主要包含Python脚本(数据加载及标准化、LSTM模型定义、模型训练、价格预测)、模型保存文件、指数数据、说明文档与预测效果图等,目录结构简洁,便于按模块查阅。已有463人学习,适用于金融数据分析方向的练手项目。代码可直接运行:train.py负责完成模型训练并保存参数,evaluate.py用于加载模型输出预测结果,同时封装了训练集/测试集划分等常用处理逻辑,可帮助理解循环神经网络在时间序列预测中的实际应用,也方便在此基础上修改参数或更换数据做二次开发。
1. 期末大作业拿到手,先分清这是“预测问题”还是“分类问题”
“基于Python的神经网络股票价格预测源码”这类期末大作业 zip,解开后东西通常不多:一个 Python 入口脚本、一份股票历史 CSV、几张训练损失图和预测对照图,偶尔带一个答辩 PPT。把 zip 解压后第一件事不是急着跑代码,而是先确认它的任务定义,因为“预测股票价格”在深度学习里有两种做法:回归式地预测下一交易日收盘价,或者分类式地预测明天涨跌。两者数据预处理几乎一样,但损失函数、评估口径和答辩话术完全不同。
这套流程适合两类人:刚把 python 安装和环境跑通、想用一份完整项目把数据处理和 PyTorch 串起来的入门实践者,以及拿到源码想换上自己股票代码、能把结果讲圆的期末选手。神经网络在这里不需要选特别花哨的结构,LSTM 是因为时间序列上下文建模的定位而成为默认选项,后面会解释它跟普通前馈神经网络的区别。接下来按“数据 → 滑窗 → LSTM → 评估 → 踩坑 → 进阶”的顺序,把整条链路拆开讲完。
2. 先别急着训练:股票数据要过三道关才能喂网络
2.1 为什么时间序列场景默认选 LSTM,而不是前馈神经网络
股票价格是一维时序数据,今天收盘价和昨天、前天的价格强相关,甚至和十几天前的趋势有关。普通前馈神经网络把每一天当作独立样本,输入输出之间没有先后概念,模型天然丢掉了“上下文”。卷积神经网络虽然在图片分类里是主力,但它擅长的是局部空间特征提取,硬套到一维价格序列上,需要刻意设计一维卷积核的感受野,入门阶段并不划算。
LSTM 神经网络在循环神经网络的基础上加了三个门:遗忘门决定上一时刻的记忆保留多少,输入门决定当前信息写进多少,输出门决定把多少记忆传给下一时刻。三个门组合起来,让网络可以选择性记住“五天前放量突破”这类相对长期的信息,这正是股价预测里很需要的记忆能力。所以期末大作业的主流选择是 LSTM,而不是前馈神经网络或 CNN。而且 PyTorch 里实现 LSTM 只需要一行nn.LSTM,答辩时能把门控机制说清楚,老师基本不会再追问模型复杂度。
如果你拿到的源码里不是 LSTM,而是多层感知机加了一堆归一化,也不是不能用,但效果通常表现在“滞后一条”上,后面第五章会讲这个典型翻车现象。先记住一个结论:时间序列回归预测,LSTM 是最稳的期末选题方向。
2.2 数据源与清洗:本地 csv 最省事,别让前复权坑了你
做期末大作业不需要实时行情,一份历史日线数据就够。常见的数据来源有三种:教师直接给的示例 csv、tushare 接口获取、自己用爬虫从财经网站抓取整理成 csv。我最推荐第一种或第二种,因为 tushare 可以指定股票代码和起止日期,几分钟内拿到一份规整的日线数据,省去手工整理的时间。用 tushare 需要先注册拿到 token,免费额度对日线数据完全够用。
拿到 CSV 后先检查列名。常见的日线数据包含date、open、high、low、close、volume六列,做价格预测时最核心的是close,也就是收盘价。注意两个细节:第一,数据要按日期升序排列,很多接口返回的是降序,不排序会导致训练集和测试集的时序倒置;第二,除权除息会导致价格跳空,如果数据源没有复权,直接用原始收盘价训练,模型会把除权当成的价格暴跌特征,预测结果毫无意义。正规数据接口一般默认返回前复权价格,如果你手里是手动导出的 Excel,务必确认复权方式并且统一口径。
清洗代码我一般这样写:
import pandas as pd df = pd.read_csv('stock_data.csv', parse_dates=['date']) df = df[['date', 'close']].dropna().sort_values('date').reset_index(drop=True) # 顺手看一眼数据头尾,确认时间范围 print(df.head()) print(df.tail())这段代码做了三件事:只保留日期和收盘价两列,删掉停牌导致的空值行,按日期升序排列并重置索引。parse_dates让 pandas 把 date 列解析成时间类型,后面画图时横轴才是连续的时间线。有的源码里还会把close转成 float 再存一遍,检查一下数据集的 dtype,避免后续做滑窗时维度报错。
如果 CSV 里除了交易日还有重复日期,drop_duplicates(subset='date')可以顺手加上。股票数据是金融数据,脏值很少像爬虫文本那样五花八门,最需要注意的其实是后续分割逻辑,也就是下一节讲归一化时的“训练集测试集顺序”。
2.3 归一化和滑窗:测试集只能用训练集的 scaler
知道“要归一化”是一回事,知道“怎么归一化不泄漏”是另一回事。常见的错误写法是先对整列close做MinMaxScaler,再切训练集和测试集,这等于让测试集参与了最小值最大值的计算,测试集的分布信息提前流进了训练过程,这就是数据泄漏。正确的顺序是:先按时间切分,只用训练集部分fit缩放器,然后用同一个缩放器去transform全序列。
滑窗是第二个关键步骤。LSTM 的输入不是单天的价格,而是一段连续的价格序列,比如用过去 60 天预测第 61 天。这样构造出来的样本叫滑窗样本,窗口宽度就叫window_size。window_size 太小,比如取 5,模型只能看到一周走势,学不到中短期趋势;取太大,比如 300,训练样本数量骤减,而且早期信息对明天的价格几乎没有影响,反而引入噪声。对日线数据,我一般取 20 到 60 之间,60 是相对通用的默认值。
完整的数据准备代码:
import numpy as np from sklearn.preprocessing import MinMaxScaler # 取收盘价,转成 (n, 1) 形状 close_values = df['close'].values.reshape(-1, 1) train_size = int(len(close_values) * 0.8) # 1. 只对训练段 fit,再统一 transform scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(close_values[:train_size]) scaled_full = scaler.transform(close_values) # 2. 构造滑窗样本 def create_sequences(data, window_size): X, y = [], [] for i in range(len(data) - window_size): X.append(data[i:i + window_size]) y.append(data[i + window_size]) return np.array(X), np.array(y) window_size = 60 X_all, y_all = create_sequences(scaled_full, window_size) # 3. 按时间切分训练集和测试集,注意切分下标 split_idx = train_size - window_size X_train, y_train = X_all[:split_idx], y_all[:split_idx] X_test, y_test = X_all[split_idx:], y_all[split_idx:] print(X_train.shape, y_train.shape) print(X_test.shape, y_test.shape)这里有个容易算错的位置:create_sequences返回的样本数比原始序列少window_size个,所以切分时要用train_size - window_size作为边界,而不是直接用train_size。如果不做这个偏移,训练集的末尾会滑进测试集,测试集的第一批样本里包含训练集最后 60 天的数据。预测时这些数据确实是已知信息,不算严格泄漏,但答辩时被问到“你的测试集第一天的上下文哪来的”,解释起来很麻烦。用split_idx一刀切开,语义更干净。
X_train.shape打印出来的结果是(样本数, 60, 1),这就是 PyTorch LSTM 的输入标准形状:(batch, seq_len, input_size)。很多报错都出在这一步,如果后面训练时报维度不匹配,先回来打印这三个数确认。
3. 用 PyTorch 写 LSTM:模型定义、训练循环与学习率
3.1 模型该有几层:hidden_size 与 num_layers 怎么定
数据形状确定后,LSTM 模型本身并不复杂。核心参数只有四个:input_size是每个时间步的特征数,收盘价单特征就是 1;hidden_size是隐藏状态维度,决定模型记忆容量;num_layers是堆叠层数;dropout是层间随机失活比例,防止过拟合。模型定义我通常这样写:
import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) last = out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(last) # 映射成单日收盘价 model = StockLSTM(input_size=1, hidden_size=64, num_layers=2, dropout=0.2) print(model)batch_first=True让输入输出都保持(batch, seq_len, hidden_size),否则 PyTorch 默认把序列长度放在第 0 维,初学时很容易在这一步绕晕,建议大家一直开着它。前向传播里out是每个时间步的隐藏状态组成的完整序列,而回归目标是未来某一天的价格,所以我们只取最后一个时间步的输出,再经过一个全连接层压缩成单值。
hidden_size在 32 到 128 之间调,64 是折中方案。太小了记不住中长期信息,太大了训练慢且容易过拟合。num_layers=2是因为一层 LSTM 的表达能力有限,两层可以在不显著增加训练时间的情况下捕捉更高层的模式;超过 4 层在股票预测里几乎不会带来收益,反而加重过拟合。如果你用的是 CPU 环境跑纯日线数据,两层 64 的配置训练速度完全可以接受。
3.2 训练循环:epochs、batch 与 Adam 的参数搭配
模型定义完就该写训练循环。损失函数用均方误差MSELoss,这是回归任务默认选择,它会把大误差放大,逼着模型更关注价格剧烈波动的时段。优化器选 Adam,学习率lr=0.001,这两个组合基本不用调就能稳定收敛。训练循环长这样:
epochs = 80 batch_size = 64 train_dataset = torch.utils.data.TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train) ) train_loader = torch.utils.data.DataLoader( train_dataset, batch_size=batch_size, shuffle=True ) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5) for epoch in range(epochs): model.train() total_loss = 0 for batch_x, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_x) # (batch, 1) loss = criterion(pred, batch_y.view(-1, 1)) # 对齐形状 loss.backward() optimizer.step() total_loss += loss.item() if (epoch + 1) % 10 == 0: avg_loss = total_loss / len(train_loader) print(f"epoch {epoch + 1}/{epochs}, loss={avg_loss:.6f}")shuffle=True是期末项目里的常见做法,它能避免模型按顺序看到同一天的数据而产生偏置,但代价是相邻滑窗样本重叠,严格意义上样本间不独立。在作业场景里这完全可以接受,只要测试集的shuffle=False且模型评估时用model.eval()关掉 dropout 就行。batch_y.view(-1, 1)这行特别容易被漏掉:y_train的形状是(样本数,),而模型输出是(batch, 1),不 reshape 的话 MSE 会报形状不匹配,或者静默广播出错误结果。
epochs 不要盲目加大。我见过同学把 epochs 设到 500,训练 loss 能压到 0.0001,测试集上却预测成一条近似水平的直线,这就是过拟合。日线数据样本量通常只有几百到一千多条,80 到 150 个 epoch 足够。判断标准很简单:每 10 个 epoch 打印一次 loss,如果连续 20 个 epoch loss 不再下降,就停下来。
还有个容易被忽略的参数是weight_decay,它是对权重做 L2 正则化,取值1e-5左右能让预测曲线更平滑,防止模型死记训练集的每一个波动。这对股票这类噪声极强的数据很有效,答辩时可以说“加了权重衰减抑制过拟合”。
3.3 本地环境怎么配:从 python 安装到 torch 安装
代码本身能看懂之后,最大的现实障碍是环境。期末季最常见的问题是 torch 装不上,或者装上之后 import 报错。如果是 Windows 且只是跑 CPU 训练,建议直接用python -m pip install torch,PyPI 默认源里的 torch 会同时装 CPU 版本和必要的依赖,并不需要特意去官网选 CUDA 版本。GPU 版本的坑在于 CUDA 和 cuDNN 版本匹配,期末作业用 CPU 完全足够,win10 上装 CPU 版 torch 最省心。
python 版本建议 3.8 或 3.9,这两个版本对 torch 的兼容性最好。太新的 python 3.12 以上偶尔会遇到某些轮子还没有对应版本的问题,如果安装时提示找不到匹配的 whl,不用折腾,直接把 python 降到 3.9 最省事。装完记得在命令行里跑一句:
python -c "import torch; print(torch.__version__)"能打印版本号就说明环境没问题。之后用 vscode 打开项目目录,选好 python 解释器,直接运行入口脚本。如果你看到ModuleNotFoundError: No module named 'numpy'这一类报错,说明当前解释器和 pip 装包的解释器不是同一个,在 vscode 右下角重新选择解释器即可,这是新手踩得最多的环境坑。
4. 预测结果如何变成一张能答辩的图
4.1 反归一化:模型输出的是 0 到 1 的数,不是股价
训练完成后,模型输出的是归一化区间的预测值,数值在 0 到 1 之间,直接画图横轴纵轴都对不上。这一步必须用之前同一套scaler做反变换,把预测值和真实值都还原成真实价格。很多人遗忘这一步,最后画出来的图“看起来趋势差不多,但价格只有零点几”,就是这个原因。
反归一化和预测代码:
model.eval() with torch.no_grad(): y_pred_scaled = model(torch.FloatTensor(X_test)).numpy() # 反变换回真实价格 y_pred = scaler.inverse_transform(y_pred_scaled) y_true = scaler.inverse_transform(y_test) # 打印前 5 个预测值与真实值对照 for i in range(5): print(f"{i + 1}: 真实 {y_true[i][0]:.2f} 预测 {y_pred[i][0]:.2f}")这里要注意y_test同样来自scaled_full的滑窗截取,所以也要用同一个scaler.inverse_transform,而不是直接用原始 CSV 里 df 的价格。因为模型看到的价格是缩放过的,真实值必须和它对齐在同一个坐标体系里再比较。打印前 5 条能快速判断预测值是否合理:如果预测值明显偏低,通常是归一化或滑窗切分出了偏差,先回头检查split_idx。
4.2 RMSE、MAE 与方向准确率:三个指标各说明什么
期末答辩最怕被问“预测准不准”,只拿一张图不够,得有量化指标。三个指标就够用:RMSE、MAE、方向准确率。RMSE 对大误差敏感,能看出有没有个别预测偏差特别大;MAE 是平均绝对误差,更直观地反映平均差了多少元;方向准确率是“预测明天涨跌方向是否正确”的比例,对股票来说比价格误差更实用。
代码和含义如下:
from sklearn.metrics import mean_squared_error, mean_absolute_error rmse = np.sqrt(mean_squared_error(y_true, y_pred)) mae = mean_absolute_error(y_true, y_pred) # 方向准确率:比较相邻两天的预测差值方向 true_diff = np.diff(y_true.flatten()) > 0 pred_diff = np.diff(y_pred.flatten()) > 0 direction_acc = np.mean(true_diff == pred_diff) print(f"RMSE: {rmse:.2f} 元") print(f"MAE: {mae:.2f} 元") print(f"方向准确率: {direction_acc:.2%}")这三个指标适合放进答辩 PPT 的表格里:RMSE 和 MAE 说明模型平均偏差多少,方向准确率说明模型在“涨跌”这个更接近交易决策的问题上正确率是多少。用表格呈现时可以用下面这种格式:
| 指标 | 含义 | 合理范围(日线预测) |
|---|---|---|
| RMSE | 均方根误差,惩罚大偏差 | 与股价量级相关,越小越好 |
| MAE | 平均绝对误差 | 通常比 RMSE 略小 |
| Direction Acc | 预测方向与实际方向一致占比 | 50%~70% 可接受,超过 70% 需警惕过拟合 |
一张图上既能展示预测曲线,旁边再用这个小表格总结数值,答辩时老师第一印象就是“这个学生有评估意识”。
4.3 画图的三个细节:中文字体、横轴对齐、滞后现象
最后一步画图,但很多人在这一步翻车。第一是中文乱码,默认字体不支持中文时,标题和图例会变成方块。Windows 下用 SimHei,macOS 下用 Arial Unicode MS,提前设置好。第二是横轴对齐,y_pred和y_true的第 i 个值对应的是同一天,直接用索引画图没问题,但如果把 df 的日期也带进来,注意索引偏移,因为滑窗会导致测试集起点比train_size晚window_size天。第三是注意观察滞后现象,详见第五章。
画图代码:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.figure(figsize=(12, 5)) plt.plot(y_true, label='真实价格', linewidth=1.2) plt.plot(y_pred, label='预测价格', linewidth=1.2) plt.xlabel('交易日') plt.ylabel('收盘价(元)') plt.title('LSTM 股票价格预测对照') plt.legend() plt.grid(alpha=0.3) plt.savefig('prediction.png', dpi=150, bbox_inches='tight') plt.show()dpi=150保证 PPT 放大后不模糊,bbox_inches='tight'避免图片四周留白过多。这张图基本就是期末报告的主图,画完之后盯着看 10 秒:如果预测曲线比真实曲线整体向右平移了一天,那就是典型的滞后现象,很多同学在这种情况下还觉得自己预测很准,其实是模型在复制上一天的收盘价,这一点在下一章的避坑里详细展开。
5. 避坑合集:5 条血泪经验
5.1 数据泄漏:归一化先用全序列 fit,测试集偷看了未来
现象:训练 loss 下降得又快又低,测试集上预测曲线贴合得几乎完美,但换一支股票跑就完全失效,预测值全是几毛钱的偏低区间。
原因:写代码时先scaler.fit_transform(close_values)再切分训练集测试集。MinMaxScaler在全序列上计算了最大值和最小值,而最大值如果出现在测试集区间,测试集的价格范围信息就已经提前进入了缩放公式,模型等于考试时看到了部分答案。
解决:严格按 2.3 节顺序,先切分再只对训练段fit。判断有没有踩坑有一个快速办法:打印缩放后的训练集最大值和测试集最大值,如果测试集最大值明显超过 1,说明测试集参与了fit。因为feature_range=(0,1)约束变换结果,测试集最大值超过 1 或小于 0 才是正确状态。
5.2 预测曲线整体滞后一天:模型学到的是“复制上一天”
现象:预测曲线和真实曲线形状几乎一样,但整条线像拍照手抖了一样往右平移了一天。RMSE 还挺低,答辩时老师一眼看穿。
原因:这是单步滑窗回归最常见的陷阱。用前 60 天预测第 61 天,而第 61 天和第 60 天高度线性相关,模型发现最省力的方式就是“记住最后一个值,预测时直接输出它”。均方误差优化下,复制上一天几乎总是比真学习趋势更划算。
解决:最常见的手段是改预测目标,不直接预测收盘价,预测“下一日涨跌幅 = (close[t+1] - close[t]) / close[t]”,让模型去学习变化量而不是绝对值,滞后现象会明显缓解。如果不想改数据,也可以把评估重点从 RMSE 转移到方向准确率上,并在答辩时主动承认滞后现象是单步回归的通病,体现自己知道这个问题的存在。提前在 PPT 里写一句“单步预测存在固有滞后”,比被老师追问后回答要好得多。
5.3 不设随机种子:每次跑出来的图都不一样,答辩被追问
现象:白天跑一次 RMSE 是 2.8,晚上跑变成 3.5,同学电脑上又变成另一个数,三个人结果对不上。
原因:PyTorch 初始化权重是随机的,Dataloader 的 shuffle 也是随机的,没有固定随机种子,每次运行都是不同的模型初始状态。
解决:在入口脚本最前面加上几行:
import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42)这几行能在 CPU 环境下保证结果可复现。如果你的环境是 GPU,还需要加torch.cuda.manual_seed_all(42)。固定种子后,同一份代码在任何机器上跑出来的 RMSE 应该基本一致,报告里也敢写“本次实验 RMSE 为多少”而不是“大约多少”。种子取多少都行,关键是固定下来。
5.4 torch 装不上或 import 报错:版本与 python 版本不匹配
现象:pip install torch提示找不到满足要求的版本,或者装完后import torch直接报错,紧接着是一长串 DLL load failed。
原因:python 版本太新,或者电脑上同时存在 32 位和 64 位 python,pip 装到了错误的环境里。python 3.12 刚发布那阵,torch 的 Windows 轮子还没跟上,这类报错很常见。
解决:最稳妥的组合是 python 3.9 + torch CPU 版。先确认python --version和pip --version指向同一个解释器,再安装。如果已经有多个 python 环境,直接在 vscode 里新建一个虚拟环境再装:
python -m venv venv venv\Scripts\activate # Windows pip install torch numpy pandas matplotlib scikit-learn虚拟环境能把依赖和系统 python 隔离,换机器时用requirements.txt一键重装。期末交作业时把 requirements.txt 一起放 zip 里,老师换台机器也方便复现。
5.5 shuffle=True 后 loss 忽高忽低,训练曲线像锯齿
现象:每个 epoch 的 loss 一会儿 0.0008 一会儿 0.002,打印出来的曲线剧烈波动,看起来像没收敛。
原因:滑窗样本之间重叠度很高,相邻样本只有一天之差,batch 里如果碰巧抽到高度相似的样本,这个 batch 的 loss 就会很低,下一批又换了一批波动大的样本。这不是 bug,而是时序样本的特性。
解决:不要用每个 batch 的 loss 判断收敛,用每个 epoch 的平均 loss,或者每隔 5 个 epoch 不看训练集,而是在验证集上算一次 RMSE。如果只关心趋势,可以把学习率调小一点,比如从0.001降到0.0005,锯齿幅度会变小。另外训练完再输出一个测试集 RMSE 作为最终结论,训练过程的 loss 曲线只是辅助展示,不需要每个点都平滑。
6. 想从“完成作业”升级成“能讲出东西”:三个小动作
第一个小动作:把回归目标改成涨跌方向。保存原模型不动,新增一个二分类输出头,预测次日相对当日上涨还是下跌。做法很简单,把最后一层的输出从 1 维改成 2 维,损失函数换成交叉熵,评估指标用准确率。答辩时能多说一句“股票预测的最终目的是判断方向而非精确价格”,整个项目的实践价值就上了一个台阶。数据上把y从价格改成涨跌标签即可,前后端代码改动不到 20 行。
第二个小动作:从单特征改成多特征。input_size=1只用收盘价,改成把open、high、low、volume也加进来,输入就从(batch, 60, 1)变成(batch, 60, 5)。实现方式是分别归一化后拼到最后一维,LSTM 定义里的input_size改成 5,其余代码不用动。加了成交量和日内价格区间之后,模型更难通过单纯复制上一天来偷懒,滞后现象通常会减轻。这是低成本高回报的改进,推荐优先做。
第三个动作:学会主动解释滞后现象。如果答辩被问到“你的预测曲线为什么向右平移”,不要慌,直接说这是单步滑动窗口回归的固有滞后,因为模型默认前一交易日收盘价是最强参考,要缓解必须引入交易成本、涨跌幅目标或方向分类改造。这个回答可以把劣势变成亮点,说明你理解模型行为而不只是会调库。我自己带过的期末项目里,凡是能主动说出滞后原因的同学,分数都比闷头调参的高一截。
这份源码项目的核心价值不在代码量,而在你能否把数据处理、模型训练、评估解释这条链路完整讲通。把第五章的坑提前踩一遍,把第六章的两个改进做完,再固定随机种子跑出一份可复现的实验记录,期末答辩基本就稳了。我自己做这类项目时养成的习惯是先把评估指标写出来再调参,指标没有就绝不动模型结构,避免陷入调参玄学。希望这个流程对你这次期末大作业有所帮助。
本文还有配套的精品资源,点击获取