☰
LSTM股票预测实战:从数据到推理的Python项目全链路解析
2026/10/16 6:48:59 网站建设 项目流程

简介:这是一份面向金融量化初学者与Python开发者的LSTM股票预测实战资源,围绕递归神经网络在时间序列上的应用展开,帮助读者理解如何用深度学习处理股票价格预测这一非线性问题。压缩包共12个文件,约959KB,包含3个py脚本、2个pyc缓存、2个csv数据集、1个checkpoint及配套meta、index、data分片等模型权重文件,另有1个docx说明文档,覆盖数据读取、模型训练与结果保存的完整链路。资源中已提供上证综合指数前复权日线数据与单支股票数据,可直接用于训练与验证,省去自行采集清洗的步骤。目前已有22363人学习下载,适合希望快速跑通LSTM预测流程、理解门控机制与数据标准化、训练集划分等关键环节的读者参考,也可作为课程设计或入门项目的实践素材。

1. 用 LSTM 做股票预测,这个 Python 项目把数据到推理的链路全打通了

很多人第一次接触时序预测,都是从股票价格曲线开始的。看着一条上下翻飞的收盘价折线,直觉上会觉得“这玩意儿肯定有规律”,但真动手写代码时,往往卡在第一步:数据怎么对齐、窗口怎么切、模型输出怎么还原成价格。这个基于 LSTM 的股票预测项目,解决的正是这条从原始行情到预测结果的完整链路。它用 Python 实现,核心是长短期记忆网络,适合两类人:一类是想找一个能直接跑通的时序预测入门项目,另一类是想在现有模型上换数据、换特征、换预测目标的开发者。项目本身不追求实盘交易那种毫秒级响应,而是把重点放在“单变量/多变量时间序列 → 滑动窗口 → LSTM 训练 → 未来价格推理”这条主线上,代码结构清晰,依赖也都是常见库,拿来当二次开发的底座比较顺手。

2. 项目结构与环境搭建:从零把工程跑起来

2.1 目录组织与模块职责

拿到源码后,先别急着 pip install。我一般会先花两分钟把目录扫一遍,确认数据、代码、模型权重分别放在哪。这个项目的典型结构如下:

lstm_stock_prediction/ ├── data/ │ ├── raw/ # 原始行情 CSV,按股票代码或日期命名 │ └── processed/ # 归一化、切窗后的中间数据 ├── src/ │ ├── config.py # 路径、超参数、窗口大小等集中配置 │ ├── data_loader.py # 读取 CSV、缺失值处理、归一化 │ ├── dataset.py # 滑动窗口切分,生成 (X, y) 样本 │ ├── model.py # LSTM 网络定义 │ ├── train.py # 训练循环、验证、保存权重 │ └── predict.py # 加载权重,对最新窗口做推理 ├── outputs/ │ ├── checkpoints/ # 保存的 .pth 或 .h5 模型文件 │ └── plots/ # 预测曲线对比图 ├── requirements.txt └── README.md

这种分法不算花哨,但胜在职责边界清楚。config.py把窗口长度、批次大小、学习率、训练轮数都收在一起,改参数不用满项目搜。data_loader.py和dataset.py分开,是为了让“读原始数据”和“构造训练样本”解耦——换数据源时只动前者,换窗口策略时只动后者。model.py只负责网络结构,训练逻辑全在train.py,这样你想把 LSTM 换成 GRU 或加一层 Attention,改动范围可控。

2.2 依赖安装与数据准备

项目依赖基本都是时序建模的常规组合,requirements.txt里通常包含:

pip install numpy pandas matplotlib scikit-learn torch

如果源码用的是 TensorFlow/Keras,把torch换成tensorflow即可。这里以 PyTorch 版本为例,因为它在自定义训练循环时更直观。

数据方面,项目一般要求一份 CSV,至少包含date和close两列。常见做法是从公开行情接口导出,或者用本地已有的历史数据。把 CSV 放进data/raw/,然后在config.py里把DATA_PATH指向它。注意日期列要能被pandas.to_datetime解析,否则后面按时间排序会出问题。

# src/config.py import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_PATH = os.path.join(BASE_DIR, "data", "raw", "stock.csv") PROCESSED_DIR = os.path.join(BASE_DIR, "data", "processed") CHECKPOINT_DIR = os.path.join(BASE_DIR, "outputs", "checkpoints") WINDOW_SIZE = 30 # 用过去 30 个交易日预测下一天 BATCH_SIZE = 32 EPOCHS = 100 LR = 1e-3 HIDDEN_SIZE = 64 NUM_LAYERS = 2 DROPOUT = 0.2 TRAIN_RATIO = 0.8

WINDOW_SIZE是最关键的参数之一。30 意味着模型每次看过去 30 天的收盘价,输出第 31 天的预测值。这个值太小,模型看不到足够的历史上下文;太大,样本数量会明显减少,训练容易过拟合。我一般会从 20 到 60 之间试几组,看验证集损失再定。

2.3 数据加载与滑动窗口切分

数据进入模型前要过三步:排序、归一化、切窗。排序是为了防止 CSV 里日期乱序;归一化是因为 LSTM 对输入尺度敏感,价格动辄几十上百,不缩放会导致梯度不稳定;切窗则是把一维序列变成监督学习需要的(样本数, 时间步, 特征数)三维张量。

# src/dataset.py import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def load_and_scale(path, feature_col="close"): df = pd.read_csv(path) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) scaler = MinMaxScaler(feature_range=(0, 1)) values = df[[feature_col]].values scaled = scaler.fit_transform(values) return scaled, scaler def make_windows(data, window_size): X, y = [], [] for i in range(len(data) - window_size): X.append(data[i:i + window_size, 0]) y.append(data[i + window_size, 0]) X = np.array(X) y = np.array(y) # LSTM 输入要求 (batch, seq_len, input_size) X = np.expand_dims(X, axis=-1) return X, y

load_and_scale返回scaler是有意为之:训练时用训练集拟合,推理时要用同一个 scaler 做逆变换,否则预测出来的价格没有物理意义。make_windows里np.expand_dims那一步经常被忽略,导致输入维度对不上,报错信息又不直观。切完窗后,按TRAIN_RATIO划分训练集和验证集,注意不要打乱顺序,时序数据的顺序本身就是信息。

3. LSTM 模型定义与训练循环:把网络和优化器接起来

3.1 网络结构设计

这个项目的 LSTM 结构不复杂,通常是“两层 LSTM + 全连接输出”。输入维度是 1(单变量收盘价),隐藏层维度由HIDDEN_SIZE控制,最后取最后一个时间步的输出,经过一个线性层映射成标量预测值。

# src/model.py import torch import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers 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 ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, _ = self.lstm(x) # 取最后一个时间步的隐藏状态 last = out[:, -1, :] return self.fc(last)

batch_first=True让输入维度变成(batch, seq_len, feature),和前面make_windows的输出对齐。dropout只在num_layers > 1时生效,这是 PyTorch 的一个细节:单层 LSTM 加 dropout 会报警告。取out[:, -1, :]而不是取h_n,是因为out包含了每个时间步的输出,取最后一步更直观,也方便以后改成 Attention 池化。

3.2 训练循环与损失曲线

训练部分的核心是:前向传播、计算损失、反向传播、更新参数。损失函数用 MSE,优化器用 Adam。每轮结束后在验证集上算一次损失,方便判断有没有过拟合。

# src/train.py import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from model import LSTMModel from dataset import load_and_scale, make_windows import config def train(): data, scaler = load_and_scale(config.DATA_PATH) X, y = make_windows(data, config.WINDOW_SIZE) split = int(len(X) * config.TRAIN_RATIO) X_train, X_val = X[:split], X[split:] y_train, y_val = y[:split], y[split:] train_ds = TensorDataset( torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32).unsqueeze(-1) ) val_ds = TensorDataset( torch.tensor(X_val, dtype=torch.float32), torch.tensor(y_val, dtype=torch.float32).unsqueeze(-1) ) train_loader = DataLoader(train_ds, batch_size=config.BATCH_SIZE, shuffle=True) val_loader = DataLoader(val_ds, batch_size=config.BATCH_SIZE) model = LSTMModel( input_size=1, hidden_size=config.HIDDEN_SIZE, num_layers=config.NUM_LAYERS, dropout=config.DROPOUT ) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=config.LR) best_val = float("inf") for epoch in range(config.EPOCHS): model.train() train_loss = 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() train_loss += loss.item() * xb.size(0) train_loss /= len(train_ds) model.eval() val_loss = 0.0 with torch.no_grad(): for xb, yb in val_loader: pred = model(xb) val_loss += criterion(pred, yb).item() * xb.size(0) val_loss /= len(val_ds) if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), f"{config.CHECKPOINT_DIR}/best.pt") if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1:03d} | train {train_loss:.6f} | val {val_loss:.6f}") if __name__ == "__main__": train()

这里有几个容易翻车的点。第一,y_train要unsqueeze(-1),因为模型输出是(batch, 1),如果标签是(batch,),MSE 会广播成错误形状,损失值看起来在降但其实是错的。第二,验证集不要shuffle,时序数据的验证集顺序有意义。第三,保存best.pt而不是最后一轮,因为 LSTM 在验证集上经常后期过拟合。训练日志里如果 train loss 一直降、val loss 先降后升,基本就是过拟合了,可以调大 dropout 或减小 hidden size。

3.3 推理与逆归一化

训练完拿到best.pt,推理时要做两件事:取最新的一段窗口数据,前向传播得到预测值,然后用训练时的 scaler 做逆变换。

# src/predict.py import torch import numpy as np from model import LSTMModel from dataset import load_and_scale import config def predict_next(): data, scaler = load_and_scale(config.DATA_PATH) last_window = data[-config.WINDOW_SIZE:] x = torch.tensor(last_window, dtype=torch.float32).unsqueeze(0) # (1, seq, 1) model = LSTMModel( input_size=1, hidden_size=config.HIDDEN_SIZE, num_layers=config.NUM_LAYERS, dropout=config.DROPOUT ) model.load_state_dict(torch.load(f"{config.CHECKPOINT_DIR}/best.pt")) model.eval() with torch.no_grad(): pred_scaled = model(x).item() pred_price = scaler.inverse_transform(np.array([[pred_scaled]]))[0][0] print(f"下一日预测收盘价: {pred_price:.2f}") if __name__ == "__main__": predict_next()

unsqueeze(0)是为了补上 batch 维度,因为模型定义时batch_first=True,输入必须是三维。逆变换这一步不能省,否则输出的是 0 到 1 之间的数,没有参考价值。如果要做多步预测,可以把预测值追加到窗口末尾,滚动生成,但误差会累积,一般不建议一次预测太多天。

4. 避坑与常见问题排查:那些让模型“看起来能跑”的坑

4.1 损失下降但预测曲线是一条直线

现象:训练日志里 loss 确实在降,但把预测结果画出来,几乎是一条水平线,完全没有跟随原始价格波动。

原因:最常见的是归一化方式不对。如果用了全局MinMaxScaler但数据里存在极端值,缩放后大部分点挤在很窄的区间,模型学到的就是“输出均值”。另一个可能是学习率太小,模型还没跳出初始平坦区域。

解决:先检查缩放后的数据分布,画个直方图。如果确实被极端值压缩,改用RobustScaler或对价格取对数后再归一化。学习率可以从1e-3调到1e-2试一轮,看 loss 是否明显下降。

4.2 验证集损失比训练集低很多

现象:train loss 0.01,val loss 0.001,看起来“泛化很好”。

原因:这通常不是好事,而是验证集和训练集分布不一致。时序数据按顺序切分时,如果验证集恰好落在一段波动很小的区间,loss 自然低。另外,如果shuffle=True用在验证集上,也会造成这种假象。

解决:确认验证集没有 shuffle,并且画一下训练集和验证集的价格曲线,看两段区间的波动幅度是否接近。如果差异太大,考虑用滚动窗口交叉验证,而不是简单按 80/20 切一刀。

4.3 推理时输入维度报错

现象:RuntimeError: input must have 3 dimensions, got 2。

原因:make_windows返回的X是二维(样本数, 窗口大小),忘了expand_dims。或者推理时直接拿了last_window一维数组,没有补 batch 和 feature 维度。

解决:在make_windows里统一加np.expand_dims(X, axis=-1),推理时用unsqueeze(0)补 batch。养成习惯:模型输入永远是(batch, seq_len, input_size),不管 batch 是不是 1。

4.4 预测结果滞后于真实价格

现象:预测曲线形状和真实曲线很像,但整体向右偏移一两天。

原因:这是 LSTM 做单步预测的典型现象。模型学到的最优策略往往是“输出接近上一天的值”,因为价格序列自相关性很强。这不是代码 bug,而是任务设定导致的。

解决:如果目标是捕捉方向而不是精确点位,可以把预测目标改成涨跌分类,或者用差分序列(今日减昨日)作为输入和输出。另外,加入更多特征(成交量、均线、RSI)也能缓解滞后。

4.5 换股票数据后效果骤降

现象:在 A 股票上调好的参数,换到 B 股票后预测完全不能用。

原因:不同股票的波动率、价格量级、趋势周期差异很大。窗口大小、学习率、甚至归一化范围都需要重新调整。

解决:把config.py里的参数做成按股票代码区分的配置字典,换数据时先跑一轮小 epoch 的快速实验,看验证集 loss 是否在合理范围。不要指望一套参数打天下。

5. 二次开发与改造思路:从单变量到多特征、从预测到策略

5.1 加入多变量特征

单变量收盘价能跑通,但信息量有限。改造的第一步通常是加入成交量、开盘价、最高价、最低价,甚至技术指标。改动集中在data_loader.py和model.py的input_size。

# 多特征版本的数据加载 def load_multi_feature(path, feature_cols): df = pd.read_csv(path) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) scaler = MinMaxScaler() scaled = scaler.fit_transform(df[feature_cols].values) return scaled, scaler # 模型 input_size 改为 len(feature_cols) model = LSTMModel(input_size=len(feature_cols), hidden_size=64)

注意make_windows也要相应调整,X的最后一维不再是 1,而是特征数量。逆变换时只对收盘价那一列做inverse_transform,其他列不用管。

5.2 改成多步预测

单步预测改成多步,有两种做法。一种是直接让模型输出多个值,把fc层改成nn.Linear(hidden_size, n_steps);另一种是滚动预测,每次把预测值拼回输入窗口。前者训练更稳定,后者实现简单但误差累积。

# 直接多步输出 self.fc = nn.Linear(hidden_size, 5) # 预测未来 5 天

对应标签也要从y[i+window]改成y[i+window:i+window+5]。这种改法适合预测周期固定的场景,比如“未来一周收盘价”。

5.3 用早停和学习率调度提升训练稳定性

原始训练循环跑满EPOCHS才停,容易过拟合。加一个早停和ReduceLROnPlateau,能省不少调参时间。

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5 ) # 在验证 loss 计算后 scheduler.step(val_loss) # 早停 if val_loss < best_val: best_val = val_loss patience_counter = 0 torch.save(model.state_dict(), "best.pt") else: patience_counter += 1 if patience_counter >= 10: print("Early stopping") break

patience=5表示验证 loss 连续 5 轮不降就降低学习率,patience_counter >= 10表示连续 10 轮不降就停。这两个值可以根据数据量调整,数据少就设小一点。

5.4 验证改造效果的方法

改完模型后,不要只看 loss。我一般会做三件事:第一,画预测值和真实值的对比曲线,看趋势是否跟随;第二,算 MAE 和 RMSE,和基线(比如“直接用昨天价格作为预测”)对比,如果还不如基线,说明模型没学到东西;第三,看不同时间段的预测误差,避免只在某一段表现好。

from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(y_true, y_pred) rmse = mean_squared_error(y_true, y_pred, squared=False) print(f"MAE: {mae:.4f}, RMSE: {rmse:.4f}")

基线对比很重要。股票价格自相关性强,很多时候“昨天等于今天”的朴素预测已经不差。如果 LSTM 跑不赢这个基线,就要回头检查数据泄漏、归一化或窗口设置。

5.5 一个容易忽略的细节:训练集和验证集的 scaler

最后说一个我踩过的坑。早期图省事,训练集和验证集分别fit了各自的 scaler,结果验证集的逆变换和训练集不一致,预测值整体偏移。正确做法是:只用训练集fitscaler,然后transform验证集和测试集。推理时加载同一个 scaler,或者把 scaler 的参数保存下来。

# 正确做法 scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_values) val_scaled = scaler.transform(val_values)

从那以后,我每次做时序项目,都会在config.py旁边单独存一份 scaler 的min_和scale_,推理脚本直接读,不再重新fit。这个习惯帮我省掉了很多“训练时好好的,推理时对不上”的玄学问题。希望帮到你。

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

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

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

立即咨询