拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于LSTM的股票价格趋势预测Python仿真代码解析

基于LSTM的股票价格趋势预测Python仿真代码解析

简介:压缩包内含基于机器学习预测股票价格趋势的完整Python仿真项目,专为毕业设计、期末大作业和课程设计场景打造。项目以LSTM等机器学习方法为核心,结合股票指数历史数据,完整覆盖数据处理、特征构造、模型训练、预测评估等关键环节。代码包含清晰注释,即使是新手也能理解建模思路,下载部署后即可复现预测流程,适合作为高分课程设计或毕设参考。整个压缩包仅356KB,共13个文件:5个Python源码文件负责模型实现与训练评估,1个Markdown说明文档梳理环境配置与使用步骤,1个CSV历史行情数据文件和1个pkl数据集文件供模型直接调用,2张PNG图片直观展示预测结果与对比效果,另有少量pyc缓存文件。目前已有303人浏览与学习,项目得分98分,导师认可度高,兼具教学与实用价值,可帮助读者快速打通“数据—模型—结果”全链路。

1. 预测股票价格趋势:这份基于机器学习的 Python 仿真代码能干嘛、适合谁

刚拆开这个项目压缩包时,我第一反应是“又是那种跑个 LSTM 就号称预测股票的玩具”。但把train.py、dataset.py、LSTMModel.py和000001SH_index.csv串起来看了一遍后,我得说:这是一份适合拿来当毕业设计、期末大作业和课程设计的完整仿真工程,不是只有几段零散代码的 demo。它的数据是上证指数日线行情,模型走的是 LSTM 时间序列预测路线,代码带注释、带训练好的模型文件、带评估脚本,甚至还把训练过程的可视化结果放在了img目录里。你下载下来要做的就是补环境、调参数、跑通、换数据。适合谁?两类人:一类是机器学习方向的学生,需要一份能讲清楚“数据怎么切、模型怎么训、结果怎么评估”的完整项目;另一类是刚开始接触 Python 量化或时间序列预测的从业者,想找一个从数据到预测曲线全链路可复现的参考实现。

2. 数据与预处理:dataset.py 怎么把行情 CSV 喂给 LSTM

2.1 先看数据:000001SH_index.csv 里到底有什么

整个项目的起点是data/000001SH_index.csv。文件名里的000001SH是上证指数的常见代码写法,index表示这是指数日线数据而不是单只股票。拿到数据后不要急着跑模型,先花两分钟确认字段和日期范围。我一般会直接用 pandas 读进来扫一眼:

import pandas as pd df = pd.read_csv('data/000001SH_index.csv', encoding='gbk') print(df.head()) print(df.tail()) print(df.columns.tolist()) print(df.isnull().sum())

这段代码里encoding='gbk'是第一个可能翻车的点。这类从国内数据源导出的 CSV 大量使用 GBK 编码,直接用 pandas 默认的 UTF-8 读取会报UnicodeDecodeError。列名通常是date, open, high, low, close, volume这一组标准 OHLCV 字段,isnull().sum()是为了确认有没有空值——如果有,要用df = df.dropna()或者df.fillna(method='ffill')补掉,不然后面构造时序窗口时会出现“真空”样本。

确认完数据结构后,我们真正需要喂给模型的特征通常只取close收盘价。原因很简单:趋势预测的首要目标是判断收盘价的走势方向,用 OHLCV 全部字段虽然信息更全,但特征维度变高后训练曲线会明显波动加大,对入门级仿真项目来说得不偿失。这个项目的数据文件只有一根 K 线的字段,走的也是“单变量序列预测”路线。

2.2 窗口切片与归一化:dataset.py 的核心实现

dataset.py文件是整个项目里最值得反复读的部分,因为 LSTM 的数据组织方式和普通 MLP 完全不同。MLP 的输入是一行样本对应一个标签,而 LSTM 的输入要求是“一段连续的时间窗口”。这里我用项目里常见的实现思路把核心逻辑还原一下:

import numpy as np import torch from torch.utils.data import Dataset class StockDataset(Dataset): def __init__(self, data, seq_len=20): self.data = data.astype(np.float32) self.seq_len = seq_len def __len__(self): return len(self.data) - self.seq_len def __getitem__(self, index): x = self.data[index: index + self.seq_len] y = self.data[index + self.seq_len] return torch.from_numpy(x), torch.tensor(y)

这里的seq_len是时间窗口长度,也就是“用过去多少天的收盘价预测下一天”。20 天是短期均线级别的时间窗口,对趋势跟踪来说够用;如果你想预测周线级别的趋势,可以调到 50 甚至 60。代码里x取的是[index, index + seq_len)这个左闭右开区间,而y是窗口后一天的真实收盘价,这叫“滚动一步预测”。注意没有做数据增强,没有加噪声,数据量不大时不建议乱加,因为金融时序本身就非平稳,人为扰动容易破坏真实趋势结构。

归一化步骤通常也在这个文件里:用MinMaxScaler把价格映射到[0, 1]区间。为什么要归一化?LSTM 内部用的是 tanh 激活函数,输出范围本身就在[-1, 1],如果你把价格 3000、4000 这样的大数值直接丢进去,梯度会在 tanh 的饱和区出现严重的“假死”现象,loss 曲线几乎不动。常见做法是:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) price = df['close'].values.reshape(-1, 1) scaled_price = scaler.fit_transform(price)

fit_transform是对整个历史数据做的,这在单段训练里没有大问题,但做多段滚动回测时要注意:标准做法是只用训练集数据fit,再用同一个 scaler 去transform验证集和测试集,防止把测试集的统计量“泄露”到训练阶段。这个 project 里训练集和验证集是同一段数据切出来的,影响不大;但如果你想拿它改造成真实验证场景,务必记下这一点。

2.3 训练集和验证集的划分边界

这是时间序列任务最容易被新手搞混的地方。分类任务可以train_test_split(random_state=42)随机打乱,时序预测绝对不行。随机打乱意味着模型在训练时已经看过“未来”的数据模式,验证集的评估结果会虚高,答辩被老师一追问就露馅。正确做法是按时序顺序切分:前 80% 的历史行情做训练,中间的 10% 做验证,最后 10% 做测试,也就是“用过去预测未来,而不是用未来预测历史”。

train_size = int(len(scaled_price) * 0.8) val_size = int(len(scaled_price) * 0.1) train_data = scaled_price[:train_size] val_data = scaled_price[train_size:train_size + val_size] test_data = scaled_price[train_size + val_size:]

注意切分要按收盘价序列的原始顺序,不要做任何形式的排序。切完后每一个数据点对应的日期索引仍然连续,这样StockDataset在构造窗口时窗口内部的时间顺序才是对的。参数方面,train_size和val_size比例可以按数据量调整,数据长可以适当调到 85/5/10,数据短就保持 75/10/15,原则是训练集不能太少。另外这个项目里dataset.py下的__pycache__里有dataset.cpython-36.pyc,说明原开发环境是 Python 3.6,你本地如果是 3.8 以上版本,直接删掉__pycache__目录再重新跑即可,避免 Python 加载旧字节码后出现莫名其妙的异常。

3. 模型结构与训练:LSTMModel.py 的参数含义和 train.py 的调参逻辑

3.1 为什么是 LSTM:先定模型再解释数据

在拆LSTMModel.py之前,值得花点篇幅说清楚“为什么这个项目选 LSTM”而不是 ARIMA 或者普通全连接网络。ARIMA 是线性模型,擅长捕捉价格序列里的线性自相关关系,但现实中的行情走势包含明显的非线性特征和长程依赖,比如一个上涨趋势往往由跟风资金推动,这种“趋势惯性”很难用线性差分方程描述。普通 MLP 的问题是输入长度固定且没有“记忆”,你给它 20 天窗口它就只看到这 20 天,天与天之间的递进关系被压扁成了独立的 20 个特征,丢失了时序本身的先后关系。

LSTM 的定位是“带门控记忆的循环网络”,它内部有输入门、遗忘门、输出门三个门控机制,通过一个携带记忆的 cell state 把前面很多步的有效信息继续保留到当前步。用在股票趋势预测上的直观理解是:如果过去 5 天连续放量上涨,LSTM 会通过遗忘门决定保留多少“上涨动能”的记忆,再通过输入门决定当前这天的数据对记忆的更新权重,最后通过输出门决定输出值。这种机制天然适合“趋势延续”或“趋势反转”这类具有状态依赖性的序列问题。这个项目用 LSTM 做预测,选型是对的,不是单纯为了赶深度学习的时髦。

3.2 模型定义的关键参数:input_size、hidden_size、num_layers

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 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, _ = self.lstm(x) out = out[:, -1, :] out = self.fc(out) return out

参数含义拆开讲。input_size=1表示每个时间步喂入的特征维度是 1,因为前面我们只用了收盘价一个特征;如果你把 open、high、low、volume 也拼进输入,这个值就要改成 4 或 5。hidden_size=64是 LSTM 记忆单元的数量,决定模型容量,隐藏层越大拟合能力越强但越容易过拟合,行情数据本身信噪比低,64 是个比较平衡的起点。num_layers=2是堆叠两层 LSTM,第二层把第一层输出的隐藏状态序列作为自己的输入序列,能学到更高层的时间抽象特征;不是越多越好,三层以上在数据量不够时收敛明显变慢。batch_first=True表示输入的维度排列是[batch, seq_len, input_size],习惯上我把这条写死为 True,避免在构造数据时反复调整张量维序。

代码里out[:, -1, :]这行是关键。LSTM 对每个时间步都会输出一个hidden_state,但我们做的是“用过去 20 天预测第 21 天”,只需要最后一个时间步的输出作为全序列的“汇总特征”,再通过nn.Linear映射为一个实数——预测的收盘价。model stock.pkl就是把包括state_dict和scaler在内的对象整体序列化保存下来的结果,加载后可以直接 evaluate,不需要重新训练。

3.3 train.py 训练回路与参数备份

train.py的训练回路比较常规,但有几个参数值得单独拎出来讲。完整的关键训练代码如下:

model = LSTMModel(input_size=1, hidden_size=64, num_layers=2) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) num_epochs = 100 for epoch in range(num_epochs): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() output = model(batch_x) loss = criterion(output, batch_y.unsqueeze(-1)) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step()

四件事说明一下。第一,损失函数用的MSELoss(均方误差),这是回归任务的标准选择,预测价格与真实价格之间的平方差越小越好;如果你想更侧重“涨跌方向是否判断对”,可以改成交叉熵做分类,但那是另一个优化目标了。第二,lr=0.001是 Adam 优化器的常规起始学习率,epoch 跑到一半如果 loss 曲线出现剧烈震荡,优先把学习率降到 0.0005 而不是去调网络结构。第三,clip_grad_norm_这条是我建议大家保留的,它对梯度做裁剪,防止 LSTM 在 BPTT 反向传播过程中梯度爆炸导致 loss 变成nan。第四,batch_y.unsqueeze(-1)是因为模型输出的形状是[batch, 1],而batch_y的形状是[batch],不对齐维度直接算 loss 会报形状不匹配错误。

训练过程中保存模型也是必须做的一步。常见做法是每个 epoch 结束后在验证集上算一次 loss,连续 5 个 epoch 验证 loss 不降就早停,并把最好的那轮参数保存下来。Python 里做保存很简单:

torch.save({ 'model_state_dict': model.state_dict(), 'scaler': scaler }, 'model/stock.pkl')

注意:用torch.save保存整个字典对象时,文件后缀建议用.pkl或.pt,加载时用torch.load读进来再恢复state_dict。不要直接保存模型实例,万一换了 Python 或 PyTorch 版本,反序列化会报module找不到之类的错误,保存state_dict是兼容性最好的选择。

3.4 训练后的评估逻辑:evaluate.py 在看什么

evaluate.py做的事就是把训练好的模型拿回来,在测试集上做前向推理,然后把预测结果反归一化回原始价格尺度。评估时要用model.eval()模式,并且把梯度计算关掉:

model.eval() with torch.no_grad(): test_pred = model(test_x) test_pred = scaler.inverse_transform(test_pred.numpy())

model.eval()的作用是关闭 dropout 和 batch normalization 在训练时的随机行为,保证预测结果稳定可控;但 LSTM 在数据量小的时候容易出现“预测值比真实值平滑”的滞后现象,也就是预测曲线比真实曲线平移了约一个窗口长度,这是测试集上最常见的表现,不是代码 bug。后面避坑章节我会展开讲。evaluate.py最终会画出两条曲线:真实收盘价曲线和模型预测曲线,输出到img/17.png和img/18.png,这两张图是你答辩时最直接的展示材料,有对比才有说服力。

4. 避坑与排查:从 PyTorch 版本到数据泄露的常见翻车点

4.1 现象:跑train.py报错UnicodeDecodeError或者module找不到

拿到压缩包直接执行训练,最常见的报错是读 CSV 时编码不对,或者加载stock.pkl时提示某个类反序列化失败。原因通常是两个:CSV 编码不是 UTF-8,以及保存的模型是在不同版本 PyTorch 下生成的,接口不兼容。解决方法是 CSV 读取时指定encoding='gbk'或encoding='gb18030';模型文件则不要强依赖那个现成 pkl,而是自己重新训练一轮,用自己的参数覆盖保存一份。从项目结构看,__pycache__里的cpython-36.pyc已经暗示了这个项目是在 Python 3.6 时代写的,你现在装的新环境大概率是 3.9 甚至 3.11,旧 pyc 文件直接删掉最省事。

4.2 现象:loss 曲线几乎水平不动,训练梯度消失

这种问题大多出在不做归一化,或者学习率设置过小。价格原始数值是 3000 多,经过 tanh 激活函数后落在饱和区域,梯度几乎为 0。解决方法是把输入数据用MinMaxScaler压到[0, 1]区间,同时检查学习率不低于1e-4。如果归一化做了 loss 还是不动,把num_layers从 2 降到 1 再试,层数多时梯度回传路径变长,梯度更容易在链式传播里消失。

4.3 现象:验证集 loss 很低但预测曲线严重滞后,整体右移

这是时间序列预测最经典的陷阱。原因是窗口长度太长,模型其实没学会“趋势判断”,而是学会了“复制前一天的数值”。我自己的排查习惯是把窗口长度减半看滞后是否缓解,如果减半后滞后明显缩小,说明当前seq_len超过了数据的有效记忆长度。另外要检查数据集切分有没有按时间顺序,如果你用了train_test_split函数并且没有传shuffle=False,数据被随机打乱,模型就等于提前看过了测试集的价格形态,训练评估整体虚高。严格按时间顺序切分后,曲线才“真实”。

4.4 现象:loss 变成nan,训练中断

通常原因是学习率过大导致梯度爆炸,或者数据里有NaN值没有清理干净。排查顺序是:先df.isnull().sum()检查数据,再在 optimizer.step 之前加nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),最后的兜底手段是学习率降到1e-4。LSTM 在长序列上的 BPTT 梯度累计量很大,加上我们只用了单序列数据,梯度裁剪基本是标配而不是可选项。

4.5 现象:预测结果看起来像一条平滑的线,完全跟不上实际波动

这其实是 LSTM 在低信噪比金融数据上的“正常表现”,不是代码坏了。价格趋势预测本质上是回归问题,回归输出的期望值天然向均值回归。要改善这个观感,可以把 MSE 损失换成HuberLoss,它对离群点更鲁棒;或者干脆换预测目标,从“预测具体价格”改成“预测 N 天后价格相对当前值的涨跌幅标签”,这样就从回归变成了二分类问题,评价指标换成准确率和 F1。对毕业设计来说,在答辩里主动说出“回归模型输出天然平滑,所以我同时做了方向准确率统计”,反而比只展示一条拟合很好的曲线更有深度。

5. 换数据、换参数、验效果的进阶玩法:从“跑通”到“讲得清”

如果你只是把代码跑出来,答辩时只能说我运行成功了。想拿高分,至少要会做三件事:换数据、改参数、做评估对比。

换数据是最快的“伪创新”。讲上证指数的人太多了,你可以去下载一只个股的日线数据,比如贵州茅台或者招商银行,把 CSV 替换成新标的,注意列名和字段顺序要和原文件保持一致。新数据拿来后重新执行dataset.py之前的预处理步骤,观察不同股票的数据特征怎么影响模型表现。茅台这类高价股的波动率低,预测曲线可能比指数更平滑;银行股偶尔出现除权缺口,归一化后窗口切片时会遇到脉冲点,这些差异都是你可以在论文里讨论的话题。

改参数要成组改,不要只改一个数就下结论。你可以固定seq_len=20对比hidden_size=32/64/128三组结果的验证集 loss,再固定hidden_size=64对比seq_len=10/20/40。把结果画在同一张图上,会发现窗口增加时模型更平滑但滞后更严重,隐藏层越大收敛越慢但未必更准。这些结论讲出来,比“我调参后准确率提高了”这种空话有说服力得多。

验证指标上,除了看测试集 loss,我建议额外算两个数字:方向准确率(预测值和真实值相对前一天的涨跌方向是否一致)和最大回撤对比。方向准确率能直接说明模型“趋势判断”的价值:

import numpy as np real = test_pred.flatten() true = test_y.flatten() direction = (np.diff(real) > 0) == (np.diff(true) > 0) acc = np.mean(direction) print(f'方向准确率: {acc:.2%}')

这段代码先对预测值和真实值分别做差分,再比较差分符号是否一致。注意test_pred要先反归一化回原始价格再算差分,在归一化后的[0,1]区间上算方向虽然在数学上等价,但答辩时用真实价格展示更容易让老师理解。最后提一句我踩过的坑:有次我换数据后忘了重新 fit scaler,直接把旧的stock.pkl里的 scaler 拿来 transform 新数据,导致预测结果整体偏移了一个常数,图上看已经猜不出价格区间了。从那以后我每次都强制走一遍“载入数据 → 重新归一化 → 切窗口 → 重训模型”全流程,不在旧模型上偷懒。希望帮到你。

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

返回列表