首页 / 资讯中心 / 文章详情

LSTM股票预测实战:从数据到推理的Python项目全链路解析

LSTM股票预测实战:从数据到推理的Python项目全链路解析 ★ FEATURED ARTICLE
简介这是一份面向金融量化初学者与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.8WINDOW_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_colclose): df pd.read_csv(path) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) 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, yload_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_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.hidden_size hidden_size self.num_layers num_layers self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout 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_firstTrue让输入维度变成(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, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) ) val_ds TensorDataset( torch.tensor(X_val, dtypetorch.float32), torch.tensor(y_val, dtypetorch.float32).unsqueeze(-1) ) train_loader DataLoader(train_ds, batch_sizeconfig.BATCH_SIZE, shuffleTrue) val_loader DataLoader(val_ds, batch_sizeconfig.BATCH_SIZE) model LSTMModel( input_size1, hidden_sizeconfig.HIDDEN_SIZE, num_layersconfig.NUM_LAYERS, dropoutconfig.DROPOUT ) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lrconfig.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(fEpoch {epoch1: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, dtypetorch.float32).unsqueeze(0) # (1, seq, 1) model LSTMModel( input_size1, hidden_sizeconfig.HIDDEN_SIZE, num_layersconfig.NUM_LAYERS, dropoutconfig.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_firstTrue输入必须是三维。逆变换这一步不能省否则输出的是 0 到 1 之间的数没有参考价值。如果要做多步预测可以把预测值追加到窗口末尾滚动生成但误差会累积一般不建议一次预测太多天。4. 避坑与常见问题排查那些让模型“看起来能跑”的坑4.1 损失下降但预测曲线是一条直线现象训练日志里 loss 确实在降但把预测结果画出来几乎是一条水平线完全没有跟随原始价格波动。原因最常见的是归一化方式不对。如果用了全局MinMaxScaler但数据里存在极端值缩放后大部分点挤在很窄的区间模型学到的就是“输出均值”。另一个可能是学习率太小模型还没跳出初始平坦区域。解决先检查缩放后的数据分布画个直方图。如果确实被极端值压缩改用RobustScaler或对价格取对数后再归一化。学习率可以从1e-3调到1e-2试一轮看 loss 是否明显下降。4.2 验证集损失比训练集低很多现象train loss 0.01val loss 0.001看起来“泛化很好”。原因这通常不是好事而是验证集和训练集分布不一致。时序数据按顺序切分时如果验证集恰好落在一段波动很小的区间loss 自然低。另外如果shuffleTrue用在验证集上也会造成这种假象。解决确认验证集没有 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(dropTrue) scaler MinMaxScaler() scaled scaler.fit_transform(df[feature_cols].values) return scaled, scaler # 模型 input_size 改为 len(feature_cols) model LSTMModel(input_sizelen(feature_cols), hidden_size64)注意make_windows也要相应调整X的最后一维不再是 1而是特征数量。逆变换时只对收盘价那一列做inverse_transform其他列不用管。5.2 改成多步预测单步预测改成多步有两种做法。一种是直接让模型输出多个值把fc层改成nn.Linear(hidden_size, n_steps)另一种是滚动预测每次把预测值拼回输入窗口。前者训练更稳定后者实现简单但误差累积。# 直接多步输出 self.fc nn.Linear(hidden_size, 5) # 预测未来 5 天对应标签也要从y[iwindow]改成y[iwindow:iwindow5]。这种改法适合预测周期固定的场景比如“未来一周收盘价”。5.3 用早停和学习率调度提升训练稳定性原始训练循环跑满EPOCHS才停容易过拟合。加一个早停和ReduceLROnPlateau能省不少调参时间。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) # 在验证 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) breakpatience5表示验证 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, squaredFalse) print(fMAE: {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。这个习惯帮我省掉了很多“训练时好好的推理时对不上”的玄学问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站