PyTorch LSTM股票价格预测实战:从数据处理到评估避坑全解析

发布时间:2026/10/1 12:48:40
PyTorch LSTM股票价格预测实战:从数据处理到评估避坑全解析 简介一套基于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(dropTrue) # 顺手看一眼数据头尾确认时间范围 print(df.head()) print(df.tail())这段代码做了三件事只保留日期和收盘价两列删掉停牌导致的空值行按日期升序排列并重置索引。parse_dates让 pandas 把 date 列解析成时间类型后面画图时横轴才是连续的时间线。有的源码里还会把close转成 float 再存一遍检查一下数据集的 dtype避免后续做滑窗时维度报错。如果 CSV 里除了交易日还有重复日期drop_duplicates(subsetdate)可以顺手加上。股票数据是金融数据脏值很少像爬虫文本那样五花八门最需要注意的其实是后续分割逻辑也就是下一节讲归一化时的“训练集测试集顺序”。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是每个时间步的特征数收盘价单特征就是 1hidden_size是隐藏状态维度决定模型记忆容量num_layers是堆叠层数dropout是层间随机失活比例防止过拟合。模型定义我通常这样写import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) 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_size1, hidden_size64, num_layers2, dropout0.2) print(model)batch_firstTrue让输入输出都保持(batch, seq_len, hidden_size)否则 PyTorch 默认把序列长度放在第 0 维初学时很容易在这一步绕晕建议大家一直开着它。前向传播里out是每个时间步的隐藏状态组成的完整序列而回归目标是未来某一天的价格所以我们只取最后一个时间步的输出再经过一个全连接层压缩成单值。hidden_size在 32 到 128 之间调64 是折中方案。太小了记不住中长期信息太大了训练慢且容易过拟合。num_layers2是因为一层 LSTM 的表达能力有限两层可以在不显著增加训练时间的情况下捕捉更高层的模式超过 4 层在股票预测里几乎不会带来收益反而加重过拟合。如果你用的是 CPU 环境跑纯日线数据两层 64 的配置训练速度完全可以接受。3.2 训练循环epochs、batch 与 Adam 的参数搭配模型定义完就该写训练循环。损失函数用均方误差MSELoss这是回归任务默认选择它会把大误差放大逼着模型更关注价格剧烈波动的时段。优化器选 Adam学习率lr0.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_sizebatch_size, shuffleTrue ) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001, weight_decay1e-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(fepoch {epoch 1}/{epochs}, loss{avg_loss:.6f})shuffleTrue是期末项目里的常见做法它能避免模型按顺序看到同一天的数据而产生偏置但代价是相邻滑窗样本重叠严格意义上样本间不独立。在作业场景里这完全可以接受只要测试集的shuffleFalse且模型评估时用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 torchPyPI 默认源里的 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(fRMSE: {rmse:.2f} 元) print(fMAE: {mae:.2f} 元) print(f方向准确率: {direction_acc:.2%})这三个指标适合放进答辩 PPT 的表格里RMSE 和 MAE 说明模型平均偏差多少方向准确率说明模型在“涨跌”这个更接近交易决策的问题上正确率是多少。用表格呈现时可以用下面这种格式指标含义合理范围日线预测RMSE均方根误差惩罚大偏差与股价量级相关越小越好MAE平均绝对误差通常比 RMSE 略小Direction Acc预测方向与实际方向一致占比50%~70% 可接受超过 70% 需警惕过拟合一张图上既能展示预测曲线旁边再用这个小表格总结数值答辩时老师第一印象就是“这个学生有评估意识”。4.3 画图的三个细节中文字体、横轴对齐、滞后现象最后一步画图但很多人在这一步翻车。第一是中文乱码默认字体不支持中文时标题和图例会变成方块。Windows 下用 SimHeimacOS 下用 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真实价格, linewidth1.2) plt.plot(y_pred, label预测价格, linewidth1.2) plt.xlabel(交易日) plt.ylabel(收盘价元) plt.title(LSTM 股票价格预测对照) plt.legend() plt.grid(alpha0.3) plt.savefig(prediction.png, dpi150, bbox_inchestight) plt.show()dpi150保证 PPT 放大后不模糊bbox_inchestight避免图片四周留白过多。这张图基本就是期末报告的主图画完之后盯着看 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[t1] - 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 位 pythonpip 装到了错误的环境里。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 shuffleTrue 后 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_size1只用收盘价改成把open、high、low、volume也加进来输入就从(batch, 60, 1)变成(batch, 60, 5)。实现方式是分别归一化后拼到最后一维LSTM 定义里的input_size改成 5其余代码不用动。加了成交量和日内价格区间之后模型更难通过单纯复制上一天来偷懒滞后现象通常会减轻。这是低成本高回报的改进推荐优先做。第三个动作学会主动解释滞后现象。如果答辩被问到“你的预测曲线为什么向右平移”不要慌直接说这是单步滑动窗口回归的固有滞后因为模型默认前一交易日收盘价是最强参考要缓解必须引入交易成本、涨跌幅目标或方向分类改造。这个回答可以把劣势变成亮点说明你理解模型行为而不只是会调库。我自己带过的期末项目里凡是能主动说出滞后原因的同学分数都比闷头调参的高一截。这份源码项目的核心价值不在代码量而在你能否把数据处理、模型训练、评估解释这条链路完整讲通。把第五章的坑提前踩一遍把第六章的两个改进做完再固定随机种子跑出一份可复现的实验记录期末答辩基本就稳了。我自己做这类项目时养成的习惯是先把评估指标写出来再调参指标没有就绝不动模型结构避免陷入调参玄学。希望这个流程对你这次期末大作业有所帮助。本文还有配套的精品资源点击获取