简介:面向高校机器学习与金融数据分析类课程的LSTM股票价格预测Python完整项目,尤其适合课程设计、期末大作业或毕业设计参考。项目覆盖行情数据读取、时间序列预处理、LSTM模型构建与训练、预测结果对比可视化等核心环节,并附有使用说明文档,按文档配置环境即可直接运行复现实验效果。压缩包共89个文件,以Python脚本、txt说明文档、网页前端样式与交互脚本、Excel及SQLite数据文件等类型为主,整体约8.89MB,目录结构清晰,便于按模块阅读、调试和二次改造。已有738人浏览学习,适合需要快速完成高分课程设计,同时想深入掌握LSTM建模流程、股票序列特征与预测评估细节的读者。
1. 基于LSTM的股票价格预测:课程设计里最值得下的一份源码
基于LSTM的股票价格预测,是课程设计和期末大作业里出现频率极高的题目。原因很直白:题目自带热度,LSTM是时序建模主力,代码量不大,可视化效果好,答辩时好讲。这份源码就是冲这个场景准备的:解压后code.py是核心训练代码,django目录提供网页展示,README.md写清了运行步骤和参数说明,属于拿到手就能跑的那一类。
它解决的事很具体:用过去一段时间的股价序列训练LSTM,预测下一交易日的收盘价,再把预测曲线和真实价格画在同一张图上。适合三类人——要做课程设计但不想从零写代码的学生,想把LSTM时序预测完整跑通一遍的入门开发者,以及在找可复现参考实现的从业者。这里多说一句:不是所有股票预测代码都能下载即用,数据接口失效、归一化还原报错、PyTorch版本不匹配都是常见坑,这份源码带使用说明文档,按README准备数据就能跑。
2. 源码结构与数据准备:从CSV到训练集的完整流水线
2.1 源码包结构:code.py、django目录和README.md怎么分工
解压后是一个标准的Git仓库结构,顶层目录是stock_prediction。先看文件清单:
| 路径 | 作用 |
|---|---|
| stock_prediction/.gitattributes | Git属性配置文件,不影响运行 |
| stock_prediction/README.md | 使用说明文档,含环境安装、运行步骤、参数说明 |
| stock_prediction/code.py | 核心训练代码,数据预处理、模型构建、训练、预测、绘图全在这里 |
| stock_prediction/django/ | 基于Django的网页展示端,用于答辩演示 |
code.py是主程序,整个流程可以拆成九个步骤:
# code.py 主流程 import numpy as np import pandas as pd import torch import torch.nn as nn from sklearn.preprocessing import MinMaxScaler # 1. 读取数据 # 2. 提取收盘价并归一化 # 3. 按 time_step 构造监督学习样本 # 4. 按时间顺序切分训练集/测试集 # 5. 定义 LSTM 模型 # 6. 训练并保存模型 # 7. 预测、还原、绘图这个结构几乎是所有LSTM价格预测项目的标准骨架。code.py偏训练,django目录偏展示,README.md是说明书。如果你只想要预测结果,跑code.py就够了;如果要答辩演示,django端可以把曲线和预测结果放到网页上,效果比终端输出直观得多。
2.2 数据准备:读取CSV、归一化与滑动窗口
股票数据的原始格式一般是date, open, high, low, close, volume。做价格预测时最常用的是close收盘价,原因有两个:一是收盘价是当天交易的最终共识,噪声相对小;二是单变量序列最容易把LSTM跑通,后续要加成交量、开盘价做多特征,只需把input_size改成对应维数。常见做法是拿tushare或akshare拉数据,但课程设计阶段更省事的是直接读本地CSV。CSV列名必须对得上,否则pandas读出来全是空值。
归一化这步很多人不理解为什么必须做。LSTM内部用的是tanh和sigmoid激活函数,输入数值太大时梯度会饱和,训练直接卡死;把价格压到0到1之间,梯度才稳定。MinMaxScaler是按列找min和max再线性映射,还原时用inverse_transform就能回到原始价格量纲。
import pandas as pd from sklearn.preprocessing import MinMaxScaler df = pd.read_csv("stock_data.csv") # 列: date, open, high, low, close, volume prices = df["close"].values.reshape(-1, 1) # 转成二维列向量 scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(prices) # 归一化到 [0, 1] print(f"原始价格范围: {prices.min():.2f} ~ {prices.max():.2f}") print(f"归一化后范围: {scaled.min():.2f} ~ {scaled.max():.2f}")参数说明:reshape(-1, 1)是必须的,sklearn的Scaler要求输入是二维数组(n_samples, n_features);feature_range设为(0, 1)是为了配合tanh和sigmoid的敏感区间。如果股票价格本身波动不大,也可以直接标准化(StandardScaler),但MinMaxScaler在还原预测值时更直观,因为inverse_transform直接回到价格量纲。
接着构造监督学习样本。LSTM的单步预测逻辑是:用第t天往前60天的收盘价,预测第t天的价格。这个60就是time_step,也叫滑动窗口。窗口太短,模型看不到趋势;窗口太长,计算量上去了但收益有限。课程设计里60是常见配置,对应约三个月的交易日。
def create_sequences(data, time_step=60): X, y = [], [] for i in range(len(data) - time_step - 1): X.append(data[i:i + time_step, 0]) # 窗口内60天 y.append(data[i + time_step, 0]) # 第61天 return np.array(X), np.array(y) time_step = 60 X, y = create_sequences(scaled, time_step) split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] X_train = X_train.reshape(-1, time_step, 1) X_test = X_test.reshape(-1, time_step, 1) print(f"X_train: {X_train.shape}, X_test: {X_test.shape}") print(f"y_train: {y_train.shape}, y_test: {y_test.shape}")逻辑说明:create_sequences返回的X是二维(time_step,)的集合,最后再reshape成(batch, time_step, 1),这是PyTorch LSTM的标准输入格式——第一维是batch,第二维是时间步,第三维是特征数。划分比例0.8/0.2是课程设计常见配置,如果数据量小可以改成0.7/0.3,让测试曲线更完整。
注意:这里划分训练/测试集不能用random.shuffle,时间序列一旦打乱,训练集就混进了未来数据,测试集指标会虚高。必须按时间顺序硬切。
3. LSTM模型构建与参数选型:网络结构、超参和设计理由
3.1 为什么选LSTM:梯度问题与门控机制
RNN在处理长序列时有个致命问题:梯度在反向传播过程中会反复相乘,序列一长梯度就指数级衰减或爆炸,前面的价格信息根本传不到后层,模型只能记住最近几天。LSTM在循环单元里加了三道门——遗忘门决定过去信息丢多少,输入门决定新信息记多少,输出门决定输出什么。三道门让梯度有了一条相对平缓的高速通道,所以能记住几十天前的趋势。
对股票价格来说这很有用:今天的价格既受昨天影响,又受一个月前的趋势区间影响,LSTM能同时抓住这两个尺度。ARIMA和线性回归也能做价格预测,但它们对非线性关系建模能力弱,课程设计里用LSTM在答辩时也更有说头。这也是为什么网上搜lstm模型代码和lstm神经网络,出来最多的是价格预测方向的demo。
3.2 模型定义与超参表
import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1, dropout=0.2): super(LSTMPredictor, self).__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, output_size) def forward(self, x): # x 形状: (batch, time_step, input_size) out, _ = self.lstm(x) out = out[:, -1, :] # 取最后一个时间步的隐状态 out = self.fc(out) # 全连接层映射到价格 return out逻辑说明:关键在out[:, -1, :]。LSTM返回所有时间步的隐状态序列,但预测任务只需要最后一步的输出,因为最后一步的隐状态已经包含了整个窗口的信息。中间所有隐状态在序列标注或编码器场景里才用得上,这里直接丢弃。
| 参数 | 默认值 | 作用 | 建议范围 |
|---|---|---|---|
| input_size | 1 | 每步输入的特征数 | 只用收盘价时填1 |
| hidden_size | 64 | 隐层记忆维度 | 32~128 |
| num_layers | 2 | LSTM层数 | 1~3 |
| dropout | 0.2 | 随机丢弃比例,防过拟合 | 0~0.5 |
| output_size | 1 | 输出维度 | 回归任务固定1 |
hidden_size是记忆容量,64起步,数据量小就32,数据量大可以试128。num_layers超过3层对几百条价格序列几乎没有收益,反而训练更慢、更容易过拟合。dropout只在训练时生效,模型eval()模式下会自动关闭。
3.3 损失函数、优化器与梯度裁剪
回归任务用MSELoss,它把预测和真实的平方差作为损失,大误差会被放大,促使模型优先修正偏离大的点。优化器选Adam,原因是它对学习率不敏感,课程设计场景不需要精细调lr,0.001一般就能收敛。
import torch model = LSTMPredictor() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) print(f"模型参数量: {sum(p.numel() for p in model.parameters())}")训练时一个容易被忽略的细节是梯度裁剪。LSTM在长序列上反向传播时梯度范数可能暴涨,loss直接变NaN,多步预测值变成天文数字。我一般会在每次backward之后加一行nn.utils.clip_grad_norm_,把梯度范数钳制在1.0,训练会稳很多。这个技巧在数据本身波动大、序列长的时候尤其重要。
4. 训练、预测与评估指标:让预测结果从数字变成对比曲线
4.1 训练循环:DataLoader、epochs与loss监控
用TensorDataset和DataLoader把训练集包装成可以按批次取的组织形式。这里有个常见的认知偏差:很多人习惯直接把整个数组扔给模型训练,数据量小还行,数据量上来之后显存吃不消,而且分批次训练本身就有一定的正则化效果。
from torch.utils.data import TensorDataset, DataLoader epochs = 100 batch_size = 32 train_dataset = TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train).view(-1, 1) ) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=False) for epoch in range(epochs): model.train() total_loss = 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_x) loss = criterion(pred, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() if (epoch + 1) % 20 == 0: print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(train_loader):.6f}")参数说明:shuffle=False是刻意的,时间序列一旦打乱就破坏了时间依赖关系,模型会学到错误的规律。epochs选100是保守值,判断依据是看最后20轮loss是否还在明显下降,是就加到150,已经平缓就保持100。batch_size=32在价格预测场景里够用,如果数据只有几百条,batch_size可以降到16,让每次参数更新更频繁。
4.2 预测与还原:inverse_transform和两条曲线
训练完成后,模型输出的是归一化值,必须用训练时fit的scaler还原成原始价格。还原这一步是报错重灾区,因为scaler在训练时输入的是二维列向量,还原时如果传一维数组就直接抛异常。
model.eval() with torch.no_grad(): train_predict = model(torch.FloatTensor(X_train)).numpy() test_predict = model(torch.FloatTensor(X_test)).numpy() # 还原归一化:预测值和真实值都映射回原始价格 train_predict = scaler.inverse_transform(train_predict) y_train_orig = scaler.inverse_transform(y_train.reshape(-1, 1)) test_predict = scaler.inverse_transform(test_predict) y_test_orig = scaler.inverse_transform(y_test.reshape(-1, 1)) from sklearn.metrics import mean_squared_error, mean_absolute_error rmse = np.sqrt(mean_squared_error(y_test_orig, test_predict)) mae = mean_absolute_error(y_test_orig, test_predict) print(f"测试集 RMSE: {rmse:.2f}, MAE: {mae:.2f}")逻辑说明:train_predict和test_predict已经是(n, 1)的二维形状,可以直接inverse_transform;y_train和y_test是create_sequences时留下的一维数组,必须先reshape(-1, 1)再还原。最后用matplotlib把test_predict和y_test_orig画成两条曲线,一眼就能看出拟合程度。
4.3 评估指标怎么读
| 指标 | 含义 | 对股票预测的参考价值 |
|---|---|---|
| RMSE | 均方根误差 | 对大误差敏感,能暴露极端偏差 |
| MAE | 平均绝对误差 | 单位是价格,直观看偏差多少元 |
| MAPE | 平均绝对百分比误差 | 适合对比不同价位的股票 |
课程设计报告里通常写RMSE和MAE两个就够。如果RMSE明显大于MAE,说明预测在少数几天出现了较大偏差,这在股票数据里很正常,因为偶发暴涨暴跌很难被模型捕捉。
5. 避坑:课程设计里最常翻车的五个细节
5.1 inverse_transform报维度错误
现象:还原预测值时抛出ValueError: Expected 2D array, got 1D array instead。
原因:scaler拟合时输入的是(n, 1)二维数组,还原时如果传入一维(n,)数组,sklearn直接拒绝执行。这是LSTM预测项目里出现频率最高的报错。
解决:预测结果先reshape(-1, 1)再inverse_transform,还原后如果需要一维数组再调用ravel()。记住一个原则:scaler训练时输入了什么形状,还原时就必须传什么形状。
5.2 预测曲线整体滞后一天
现象:训练loss很低,但预测曲线和真实曲线几乎平行,整体向右平移了一个周期。
原因:time_step过小时,模型学到的最优策略就是「今天的价格约等于昨天的价格」,本质上是在拟合恒等映射,根本没有学到趋势。网上不少lstm预测demo都有这个毛病,只是没被指出来。
解决:把time_step加大到40到60,同时用第6章的滚动多步预测验证模型是否真的有外推能力。单步预测的滞后现象是这个任务里最难摆脱的通病,答辩时被老师追问的可能性极高。
5.3 两次运行结果差异大
现象:同一份代码连续跑两遍,loss曲线和预测图都不一样。
原因:模型初始化、DataLoader顺序、dropout都有随机性,没有固定随机种子,结果自然漂移。这在课程设计里特别容易被质疑实验结果不可复现。
解决:文件开头统一固定种子。
import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42)5.4 测试集数据泄漏
现象:测试集RMSE异常低,但换一段新数据预测效果崩了。
原因:对全部数据执行了scaler.fit_transform,scaler提前看到了测试集的min和max,相当于考试前看了答案。这是最隐蔽的一个坑,因为训练曲线看起来一切正常。
解决:scaler只在训练集上fit,测试集只用transform。数据划分必须发生在归一化之前,或者至少保证fit_transform只作用在训练集切片上。
5.5 CSV中文路径或中文列名报错
现象:pd.read_csv读文件时报UnicodeDecodeError,或者列名取不到。
原因:Windows默认编码是GBK,而CSV文件通常是UTF-8保存的;路径带中文也可能导致某些环境下解码失败。
解决:路径和文件名全部用英文,read_csv时指定encoding="utf-8"。这是环境问题,跟模型本身无关,但每年都有人在这个上面浪费半天时间。
6. 进阶:多步预测与滚动回测验证
要证明LSTM模型真的有预测能力,单看单步拟合曲线不够。单步预测本质是拿真实历史去预测下一天,很多情况下模型学到的只是「今天的价格和昨天差不多」,曲线看起来拟合得很好,实际上没有外推能力。答辩时最经典的问题就是:「你预测的是哪一段?未来数据你能预测吗?」这时候需要做多步预测。
多步预测的思路是滚动:把当前窗口的最后一天预测值拼到序列尾部,同时丢掉最旧的一天,让窗口整体往前挪一步,接着预测下一天。这样连续预测出未来N天的价格。
def multi_step_predict(model, initial_seq, steps=5, scaler=None): model.eval() seq = initial_seq.copy() # shape: (time_step, 1) predictions = [] with torch.no_grad(): for _ in range(steps): input_tensor = torch.FloatTensor(seq).view(1, seq.shape[0], 1) next_val = model(input_tensor).item() # 单日预测 predictions.append(next_val) # 滚动窗口:删除最旧一天,拼接预测值 seq = np.vstack([seq[1:], [[next_val]]]) if scaler is not None: predictions = scaler.inverse_transform( np.array(predictions).reshape(-1, 1) ).ravel() return predictions参数说明:initial_seq取测试集最后time_step个真实值,steps是要预测的天数,scaler传训练时那个scaler,输出就是原始价格单位。steps设得越大,预测误差会累积得越明显,这是递归多步预测的固有特性,课程设计里建议预测5天以内。
有了这个函数,回测就很容易写:从测试集第一个点开始,滑动取窗口,预测未来5天,与真实5天对比,画一条虚线扩展。如果模型的预测曲线在5步之后仍然能大致跟随趋势方向,说明模型确实学到了价格动量,而不是单步滞后拟合。
模型训练完记得保存:
torch.save(model.state_dict(), "lstm_model.pth") # 加载 model = LSTMPredictor() model.load_state_dict(torch.load("lstm_model.pth")) model.eval()加载模型时必须重新实例化同一个LSTMPredictor类,再load_state_dict,否则结构对不上会报错。保存state_dict而不是整个模型,兼容性更好,换PyTorch版本也不容易出问题。
我以前栽过一个大跟头:训练时单步拟合漂亮,就没做多步验证,答辩时被老师追问「你模型外推能力怎么样」,当场答不上来。从那以后,我每次跑股票预测类项目,都会强制自己把多步回测和数据泄漏检查走一遍,确认曲线不是延迟复制,再下结论。希望帮到你。
本文还有配套的精品资源,点击获取