☰
LSTM股票预测实战:模型原理、PyTorch代码与关键避坑指南
2026/10/1 17:22:14 网站建设 项目流程

简介:基于Python与PyTorch实现的LSTM股票价格预测项目,面向正在完成期末大作业、课程设计的计算机相关专业学生,也适合需要时序预测实战代码的初学者。项目经导师指导并通过评审,难度适中,所有源码均调试通过、下载后可直接运行。压缩包共14个文件,包含5个Python脚本(实现数据预处理、LSTM模型定义、训练与评估)、1个CSV历史行情样本、1个模型权重文件、1份依赖清单、1份说明文档,以及2张结果可视化图片;整体仅359KB,结构清晰便于快速部署。目前已有89人学习下载。项目按标准深度学习流程组织,从滑窗构造训练集、定义LSTM网络,到训练循环、损失计算和结果绘图均有对应实现;同时附有requirements依赖安装说明与运行指引,既可支撑期末课程报告或毕业设计的核心代码,也能帮助理解时间序列预测中的数据划分、模型调参与评估方法。

1. 下载即用的LSTM股票预测项目,为什么跑通只是第一步

很多朋友拿到压缩包,解压后习惯先跑 train.py,看到 loss 一路下降就以为成功。实际上基于 Python 实现 LSTM 对股票价格的预测,代码能跑通和预测能用是两回事,中间隔着数据处理和验证。这类项目的价值在于给你一套完整的 LSTM 时间序列预测模板:把收盘价构造成序列、训练模型、反归一化输出预测曲线、再画一张对比图。适合有 Python 基础、想快速落地一个 LSTM demo、但没时间从零搭 pipeline 的从业者。也有不少人是冲着量化策略代码来的,那更要注意:demo 能跑只是起点,下面按选型、预处理、训练、避坑、验证的顺序拆开讲。

2. 为什么选 LSTM 做股价预测:门控机制、适用边界和 PyTorch 最小实现

2.1 从 RNN 到 LSTM:遗忘门、输入门和输出门到底在干什么

这个标题里出现 LSTM,而热词里也总把 LSTM 实现和 LSTM 模型绑在一起搜,说明大家真正想搞清楚的是两件事:它为什么能记住长序列,以及代码里那几行nn.LSTM参数到底怎么设。先回答前者。

RNN 把前一个时间步的隐藏状态h_{t-1}和当前输入x_t一起送进一个全连接层,理论上能记住长序列,实际上相邻时间步的梯度会在时间维度反复相乘,步数一多要么爆炸要么消失,普通 RNN 学超过十步基本就断了。LSTM 用门控机制来对抗这个问题:遗忘门决定上一时刻的记忆细胞保留多少,输入门决定当前写入多少,输出门决定从记忆细胞里读出多少到隐藏状态。三个门本质是带 sigmoid 的加权通道,让信息能以接近常数的方式跨时间步传递,长距离连乘的问题被门控拆成了加法和局部乘法。

对股价序列来说,你需要模型记住“三周前放量突破,最近几天在缩量回踩”这种跨越几十个交易日的关系。普通 RNN 记不住,LSTM 的遗忘门正好可以保留这种中长期模式。这也是为什么同样是时间序列预测,LSTM 在金融序列和工业设备寿命预测实战里都常见——序列长、信号弱、噪声大,门控结构比普通循环网络抗噪。

2.2 股价序列适不适合 LSTM,和 ARIMA、Transformer 怎么选

每次讨论 LSTM 时间序列预测时都会出现一个老问题:股票价格到底适不适合 LSTM。我的判断是:如果只用历史收盘价预测未来价格,ARIMA 在短序列、强季节性的场景下更快;但股价非平稳、没有固定周期,ARIMA 的差分阶数很难定,模型选型成本反而高。LSTM 把特征工程交给网络自己学,省掉的这部分人工成本往往比它多消耗的计算资源更值钱。

Transformer 的注意力机制在长序列上很强,但股价日线通常只有几百到几千个样本,注意力机制的优势发挥不出来,反而更容易过拟合。数据量到分钟级别、百万条以上再看 Transformer 也不迟。和这两类模型比,LSTM 在这个数据量级下的性价比最好,这就是它成为 LSTM 预测类项目默认主力的原因。

提示:选型不是越复杂越好,而是在能跑通、能收敛、能泛化三个条件里取交集。几百条日线数据用单层或两层就够,隐藏单元 32 到 64 已经足够。

模型适合场景短板数据量要求
ARIMA短期、平稳、季节性明确差分阶数难定,非线性弱几百条即可
LSTM非线性、长依赖、多特征调参多,训练慢几百到几十万条
Transformer超长序列、大数据量小样本容易过拟合最好十万条以上

这个表格不是拍脑袋,是我在 LSTM 模型代码相关项目里反复看到的选型结论。模型黑匣子不可怕,可怕的是拿黑匣子硬套不合适的数据形态。

2.3 最小可运行的 LSTM 模型代码:PyTorch 版本与参数说明

下面给一个最精简的 PyTorch 实现,所有下载即用项目里的 LSTM 模型基本都长这个形状。我习惯把input_size、hidden_size、num_layers都写成构造参数,方便后面做实验对比。

import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=32, num_layers=2, output_size=1, 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 if num_layers > 1 else 0.0, ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x 的形状:(batch, seq_len, input_size) out, _ = self.lstm(x) # out 形状:(batch, seq_len, hidden_size) out = out[:, -1, :] # 只取最后一个时间步 return self.fc(out) # 返回 (batch, output_size)

这里最值得弄清楚的是batch_first=True的含义。它让输入的第一维是 batch,第二维是序列长度,第三维是特征数;如果不设这个参数,PyTorch 默认第一维是序列长度,新手最容易在这里维度对不上。out[:, -1, :]取的是最后一个时间步的隐层输出,因为我们要预测的是窗口末尾那一个价格,而不是把每个时间步都预测一遍再拼起来。如果要把成交量、换手率加进来,只要把input_size从 1 改成特征列数,输入数据的最后一维拼上对应特征即可,模型其他部分不用动。

参数方面,hidden_size=32对小样本足够,数据量大可以提到 64 或 128。num_layers=2是下载即用项目里最常见的层数,再往上在几百条样本上几乎必然过拟合。dropout只在多层 LSTM 时生效,单层时写了也没用,这是 PyTorch LSTM 源码里容易被忽略的细节。

很多照着 PyTorch LSTM 源码抄实现的同学会有个疑问:为什么项目里常见的是torch.nn.LSTM而不是手写三个门的实现。原因是 PyTorch 的 LSTM 模块已经把遗忘门、输入门、输出门封装在 cuDNN 和自身实现里,手写版主要用于学习和调试。下载即用项目的高效点,正在于用成熟实现替换了手写三门的复杂度,你只需要关心数据维度和超参数。

3. 股票数据预处理与序列构造:归一化、look_back 窗口和数据源取舍

这一章是翻车重灾区。很多下载即用项目跑不出理想曲线,问题不在 LSTM 模型代码,而在数据进模型之前已经被处理坏了。

3.1 数据源怎么选:Tushare、yfinance 和本地 CSV 的取舍与坑

数据源是新手踩坑的第一站。Tushare 要注册并申请 token,部分接口有积分门槛,积分不够就拿不到分钟级数据;yfinance 用 pip 安装后能直接取历史行情,但复权口径不太透明,偶尔会缺交易日;本地 CSV 最可控,但很多打包项目里的 CSV 是早期下载的,没做复权处理,除权后价格跳空会被 LSTM 当成一次真实暴跌。

我一般建议先把本地 CSV 跑通,再换真实数据源。这样能把数据处理逻辑和数据源波动分开排查。如果你在 VSCode 或 PyCharm 里调试,换 Python 解释器后 pandas 和 numpy 版本一变,日期解析、类型转换都很容易出问题,这类环境问题排查起来比模型本身还耗时。下面我按已经拿到一个按日期升序排列、包含 close 列的 DataFrame 来写,这是整个方案最稳的起点。

3.2 归一化必须放在切分之后:代码与理由

归一化这一步能不能做对,直接决定测试集 loss 是不是自欺欺人。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) data = df[["close"]].values.astype("float32") train_size = int(len(data) * 0.7) val_size = int(len(data) * 0.1) train_data = data[:train_size] val_data = data[train_size:train_size + val_size] test_data = data[train_size + val_size:] train_scaled = scaler.fit_transform(train_data) val_scaled = scaler.transform(val_data) test_scaled = scaler.transform(test_data)

这段代码里最容易被忽略的是顺序:先按时间切分,再在训练集上 fit,然后复用同一个 scaler 去 transform 验证集和测试集。如果先对整个序列 fit 再切分,训练阶段就已经偷看了测试集的最高价和最低价,样本外误差会被人为压低,画出来的预测曲线贴合得非常漂亮,但那不是模型的能力。

用 MinMaxScaler 而不是 StandardScaler 的理由是:LSTM 输出层常用 tanh 激活,值域在 [-1, 1],把价格映射到 [0, 1] 更贴近激活函数的敏感范围。标准化会把价格压成均值为 0、方差为 1 的分布,对小尺度价格变化反而没那么友好。记住 scaler 只在训练集上 fit,后面的反归一化还要用同一个对象。

3.3 用 look_back 窗口把收盘价变成监督学习的输入输出对

LSTM 不是把整段价格序列一次性吞进去,而是切成长度固定的窗口。比如 look_back=30,表示用过去 30 个交易日的收盘价预测第 31 天的价格。这个窗口每天滑动一格。

import numpy as np def create_sequences(scaled_data, look_back=30): X, y = [], [] for i in range(look_back, len(scaled_data)): X.append(scaled_data[i - look_back:i, 0]) y.append(scaled_data[i, 0]) X = np.array(X, dtype=np.float32).reshape(-1, look_back, 1) y = np.array(y, dtype=np.float32).reshape(-1, 1) return X, y X_train, y_train = create_sequences(train_scaled, look_back=30) X_val, y_val = create_sequences(val_scaled, look_back=30) X_test, y_test = create_sequences(test_scaled, look_back=30)

循环从i = look_back开始,意味着每个序列恰好能取到前面 30 个点作为特征、第 31 个点作为标签,而不是从 0 开始导致特征不足。开头那 30 条数据没有对应的完整窗口,直接丢弃,不要补零,补零会让模型学到价格等于 0 的假样本。reshape(-1, look_back, 1)是为了匹配上一章模型里的batch_first=True输入格式。

look_back 是这类项目里第一个值得认真调的参数,比换hidden_size见效快得多。日线数据我一般试 20 到 60;分钟级或高频数据波动快,5 到 10 就够;数据量小就保守一点。有人把 look_back 当玄学拍脑袋,其实有个笨办法:先用 30 跑一版,对比预测曲线和真实曲线的滞后程度,滞后明显就减小窗口,噪声过大就加大窗口。

3.4 DataLoader 批量喂数据:batch 大小和 shuffle 是否该开

窗口构造好后,用 TensorDataset 和 DataLoader 把它们喂给模型。

from torch.utils.data import TensorDataset, DataLoader train_dataset = TensorDataset( torch.tensor(X_train), torch.tensor(y_train) ) val_dataset = TensorDataset( torch.tensor(X_val), torch.tensor(y_val) ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=128, shuffle=False)

训练集开 shuffle,验证集不开。原因是训练时每个样本都是一个独立窗口,乱序能让梯度下降更平稳;验证时我们要按时间顺序看预测曲线的连续性,一旦 shuffle,预测点的时间顺序就打乱了,画曲线、算滞后误差都会错位。batch_size 在几百条样本上用 64 就够,数据量到上万条再上调到 128 或 256;小数据集硬上 512 以上反而容易震荡不收敛。

4. 训练 LSTM 模型:损失函数、优化器、早停和预测反归一化

前面把数据处理好了,这一章解决怎么把训练过程调稳,以及如何把网络输出的 0 到 1 之间的数字翻译回真实价格。

4.1 训练循环怎么搭:MSELoss、Adam 和梯度裁剪

训练循环是每个 LSTM 模型代码项目的骨架。下面是一段可以直接复用的模板。

model = StockLSTM(input_size=1, hidden_size=32, num_layers=2, output_size=1) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) best_val_loss = float("inf") patience_count = 0 for epoch in range(200): model.train() train_loss = 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) loss = criterion(pred, y_batch) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * x_batch.size(0) model.eval() val_loss = 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: pred = model(x_batch) val_loss += criterion(pred, y_batch).item() * x_batch.size(0) val_loss /= len(val_dataset) train_loss /= len(train_dataset) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pt") patience_count = 0 else: patience_count += 1 if patience_count >= 10: print(f"早停于 epoch {epoch}") break

几个关键点拆开解释。回归任务用 MSELoss,预测价格是连续值而不是分类,交叉熵在这里没有意义。nn.utils.clip_grad_norm_把梯度的范数截断到 1.0,防止 LSTM 在长序列反向传播时梯度爆炸,这是 LSTM 训练里最常见的翻车点之一。weight_decay=1e-5做 L2 正则,几百条样本的小模型很容易过拟合,这一行能让验证集曲线更平缓。

注意保存模型的条件是验证集 loss 创新低,而不是每个 epoch 都存。早停条件设为累计 10 个 epoch 验证集 loss 没有突破,这样训练循环会被自动切断,不用死等 200 个 epoch。train_loss和val_loss要在每个 epoch 算完后分别除以各自的样本数,否则 batch 大小不一样时 loss 没有可比性。

每个 epoch 都要在model.train()和model.eval()之间切换。model.train()打开 dropout 和 batch norm 的动态行为,model.eval()关闭它们。验证阶段如果不切回 eval,dropout 会给预测带来随机性,验证集 loss 会忽高忽低,早停判断就会失真。

4.2 早停和学习率调度的两个基准线

早停是止损的默认选项,学习率调度是救命的选项。我一般会在模型卡住不降的时候,把学习率乘以 0.5 再继续训练,而不是从头重训。更讲究一点可以用 PyTorch 的ReduceLROnPlateau,让它自动在验证集 loss 平台期降学习率:

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5 ) # 每个 epoch 结束后调用: # scheduler.step(val_loss)

判断训练状态有两组简单的基准线:如果 train_loss 远低于 val_loss,说明模型把训练集背下来了,优先降低hidden_size或增大weight_decay;如果两边 loss 都在高位震荡不下降,先怀疑 lr 写大了,降到 1e-4 重新跑;如果 val_loss 比 train_loss 还低,多半是验证集太小或验证集数据太简单,这时要回头检查切分有没有把相近的交易日同时分进训练和验证。

参数基准值何时调整
look_back30滞后明显调小到 15-20,噪声过大调大到 40-60
hidden_size32数据量大、loss 不降时提到 64
num_layers2小样本只用 1 层,数据充足再上 3 层
lr1e-3震荡不降时降到 1e-4
batch_size64上万条数据调到 128-256
patience10收敛慢但稳定时可以放宽到 15

另外要区分早停和学习率调度各自的任务:早停帮你节省算力,学习率调度帮你在卡住时继续向下走。两者都依赖验证集 loss 这个唯一信号,前提是验证集切分干净。验证集一旦被污染,两个机制都会失真。

4.3 用训练好的模型预测未来 N 天:反归一化与可视化

模型保存好后,加载、预测、反归一化这一段是 LSTM 预测项目里最容易被写错的地方。

model.load_state_dict(torch.load("best_model.pt")) model.eval() with torch.no_grad(): X_test_tensor = torch.tensor(X_test) y_test_tensor = torch.tensor(y_test) pred_scaled = model(X_test_tensor).numpy() pred_price = scaler.inverse_transform(pred_scaled) true_price = scaler.inverse_transform(y_test)

反归一化必须复用训练时的 scaler,不能重新 fit。原因是 scaler 的 min 和 max 来自训练集分布,测试集一旦参与 fit,就相当于把测试集的信息泄露给了预测结果。model.eval()和torch.no_grad()分别关闭 dropout 和梯度记录,去掉它们预测结果会带随机性且白耗内存。

预测未来 N 天在实操里要打折:LSTM 的一步预测比较可靠,多步预测超过 5 到 10 天后,输出会逐步衰减,要么贴近最近一个窗口的复制品,要么趋于均值。如果哪个项目声称能精准预测未来 30 天,基本可以判断它是拿训练集末尾的历史数据替换了真实未来再画图,属于自欺。

可视化时我习惯把测试集真实价格和预测价格画在同一张图上,并标出 test 切分点。曲线整体贴得越紧越要警惕滞后,而不是高兴。滞后模型的 RMSE 很低,但它本质上是在复制昨天,并没有真正预测。

5. 股价预测最常见的 5 个坑与排查建议:从归一化泄露到复现翻车

免费源码和打包项目跑不起来,十个里有八个死在这五个坑上。每条我都按现象、原因、解决三步写清楚。

5.1 归一化泄露未来信息,预测曲线好看得可疑

现象:测试集预测曲线和真实曲线几乎完全重合,RMSE 小到不可思议,正准备庆功上实盘。

原因:最常见的是把 MinMaxScaler 在整段数据上 fit 后再切分,或把fit_transform用在了包含测试集的数据上。这种泄露会让模型在训练时就拿到测试集的价格区间,预测结果当然贴合。

解决:严格按先切分、再 fit 训练集、transform 验证和测试的顺序执行。检查方法很简单:打印scaler.data_min_和scaler.data_max_,看它是否等于训练集的最小值和最大值。如果发现它包含了测试集的极值,说明处理顺序写错了。

5.2 look_back 窗口乱设,模型要么学噪声要么滞后

现象:预测曲线上有肉眼可见的平移滞后,每次转折都比真实价格慢一两拍;另一种是曲线极度毛糙,几乎跟着每天的噪声跳。

原因:窗口太小,特征里只有最近几天的短期波动,学不到趋势;窗口太大,输入维度变高,小数据集上过拟合,而且窗口末尾的历史信息被稀释。日线数据硬套 look_back=90,训练样本量急剧缩水,症状就是滞后加毛糙并存。

解决:先用 30 跑一版基线。滞后明显就降到 15 到 20;毛糙严重就升到 40 到 60。调 look_back 比调hidden_size见效快,因为它直接改变模型看到的时间跨度。

5.3 反复看测试集调参,测试集失去了验证意义

现象:为了把测试集误差压下去,连续几天改了十几次超参数,每次都看测试集曲线,最后发现测试集调好了,换一段新数据就崩。

原因:把测试集当成验证集用。测试集本来只应该在所有调参定稿后跑一次,反复看它,相当于把测试集的信息一点一点喂进模型选择过程。这和归一化泄露一样是信息泄露,只是泄露通道不同,而且没有后悔药可吃。

解决:把数据切成训练、验证、测试三段,只认验证集调参,测试集锁定。每次调参只许看验证集 loss 和曲线,测试集完全不动。定稿后再跑一次测试集,并把这个结果记为模型的真实样本外水平,之后不要再改动。

5.4 只看 RMSE,忽略 LSTM 的滞后效应

现象:RMSE 很小,方向准确率却不到 50%,实际交易里模型判断明天涨跌跟抛硬币一样。

原因:LSTM 在预测带趋势的价格序列时很容易学会复制最近一个点,明天预测等于今天,这种滞后预测在 MSE 口径下误差很小,因为它只差一两天的小波动。但方向预测没有任何信息量。

解决:在评估指标里加方向准确率。相邻两天真实涨跌方向与预测涨跌方向的匹配比例,加上这个指标之后,滞后的复制型预测会立刻被打回原形。这一步是量化场景里最容易被忽略的检验。

5.5 随机种子和依赖版本,导致复现结果对不上

现象:同一份代码,今天跑出一个结果,明天跑出另一个结果;换一台机器或换一次 Python 环境,loss 曲线差很多。

原因:PyTorch 和 NumPy 的随机数初始化在默认状态下每次不同;换成 GPU 后,cuDNN 的算法存在不确定性问题,同一份输入重复执行结果也会有微小差异。还有一环是依赖版本:sklearn 接口、numpy 数据类型在不同版本下处理结果不一致,很多下载即用项目在你电脑上跑不起来,就是这么来的。

解决:在训练脚本入口固定随机种子,设置确定性计算开关。

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

这能解决绝大多数随机性问题。另外先核对 Python 版本和 requirements.txt 里的依赖版本,再跑训练;换机器时优先保证 pytorch、numpy、pandas 三个库版本不动。还有一个容易被忽略的细节:不同操作系统或 CPU 架构上科学计算库的底层实现不同,结果也会有轻微差异,遇到差异先看版本,不要怀疑模型代码写错了。

6. 让 LSTM 预测真正能用的分寸感:方向准确率与样本外验证

模型跑通、避坑梳理完之后,下一步不是急着扩大投入,而是先给模型定义一个可用的及格线。价格预测是回归,但用在交易场景时,方向比数值更值钱。你预测到明天价格是 101.2,实际是 101.1,RMSE 固然小,但真正交易时你依赖的是这个数字比今天高还是低,方向错了,点位再精确也是亏钱。这也是为什么量化交易策略代码相关的项目普遍收敛到方向准确率这个指标上。

方向准确率的计算非常轻量,把测试集的真实标签和预测结果相邻做差,比较符号即可:

import numpy as np def direction_accuracy(true_price, pred_price): true_diff = np.diff(true_price.flatten()) pred_diff = np.diff(pred_price.flatten()) hit = (true_diff * pred_diff) > 0 return hit.mean() acc = direction_accuracy(true_price, pred_price) print(f"方向准确率: {acc:.4f}")

这份代码的边界是:它只看相邻两天的方向是否一致,没有考虑价格涨跌幅的权重。小幅震荡的日子成交密集,方向对错的意义不大;如果想让指标更贴近交易,可以给每条样本按真实涨跌幅加权,或者只看涨幅超过 0.5% 的交易日。方向准确率在 0.52 以上,意味着模型有一定可用的信息量;长期在 0.5 以下,说明预测方向和噪声没区别,这时不要把模型接进任何自动决策流程。

样本外验证是另一道锁。我习惯在训练和测试之外再留一截冷数据:定稿后的模型参数完全冻结,把最近三个月的真实行情按相同预处理流程过一遍,测一次方向准确率和误差分布。这期间不改任何超参数、不重新 fit scaler。如果模型在训练集和测试集上表现好,在这段冷数据上崩盘,通常是 look_back 窗口或隐藏单元数过拟合了这段行情的特定形态。

如果方向准确率能站住、冷数据没有崩盘,还有一个实用的进阶口径:只拿模型当涨跌过滤器。不直接预测精确价格,而是用 LSTM 的输出和上一日收盘价比对,仅当方向概率超过某个阈值时,才产生信号。绝大多数个人实践里,模型判断方向的贡献比精确点位可靠得多,这是把价格预测落进策略时最容易被低估的一环。

这些验证手段本身不复杂,难的是管住自己不去美化结果。我自己的血泪经验是:一份曲线漂亮的 demo,和我最终敢用的模型之间,差的就是三次严格的样本外验证。方向准确率过不了 52% 就不投入策略,这个习惯帮我避开过很多次看起来很美好的翻车。希望帮到你。

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

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

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

立即咨询