☰
PyTorch+LSTM实现股票多步预测:从数据清洗到实战部署全解析
2026/10/21 2:54:00 网站建设 项目流程

简介:基于PyTorch的LSTM多变量多步股票预测完整工程包,面向有深度学习基础、希望将时间序列模型应用到金融场景的开发者。压缩包共11个文件,以Python主程序为核心,附带600519股票Excel数据、项目配置XML、运行结果图等,整体仅222KB,结构紧凑。已有9240人学习下载。代码覆盖数据标准化与缺失值处理、多变量特征构建、编码器-解码器序列到序列结构、教师强制多步预测及MSE/MAE损失优化等关键环节,并配套实际数据集,可直接运行复现。工程配置完整,适合对比学习LSTM在金融时间序列上的建模流程,理解多步预测的输入输出设计与模型评估方法,也便于基于此扩展注意力机制或集成模型。对于从事股票预测相关课题的学生和研究者,是一份轻量实用的参考实现。

1. 股票多步预测的痛点和这个项目怎么回答的

做股票预测的深度学习新手,八成会栽在一个坑里:用历史数据训练时loss很漂亮,一上实盘预测就变成一条水平线或者滞后一天的死板复制。这套基于 PyTorch 和 LSTM 实现的多变量多步预测项目,正是冲着这个痛点来的——它不是简单地把收盘价丢进 LSTM 里做单步回归,而是同时吃进多支股票、多个因子特征,一次性输出未来 5 到 20 个交易日的完整预测序列,把「多步预测」从纸面概念落到实处。适合两类人:一类是想快速跑通时序预测全流程的开发者,另一类是已经在用 ARIMA 或 Prophet 做预测、想转向深度学习方法验证效果的量化爱好者。这套代码覆盖了从数据清洗、滑动窗口构造、LSTM 模型设计到训练评估和结果可视化的完整链路,下载后改改配置文件就能在本地跑起来。

2. 技术架构与目录结构:从数据到预测结果的完整链路

2.1 技术选型:为什么是 PyTorch + LSTM

这个项目选择 PyTorch 而不是 TensorFlow 或 Keras,核心原因在于动态计算图对时序数据更友好。LSTM 的隐状态传播本身是循环结构,在调试时打印每一步的hidden_state形状、检查梯度流向都比静态图直观得多。项目里用到的 LSTM 是标准的多层堆叠结构,input_size对应多变量的特征维度,hidden_size控制记忆容量,num_layers增加网络深度来捕捉更复杂的时序模式。

多变量是指每个时间步的输入不是一个标量价格,而是一个特征向量——通常包括开盘价、最高价、最低价、收盘价、成交量,再加上均线、RSI、MACD 等衍生技术指标。多步预测则有三种常见策略,这个项目采用的是「直接多步」:LSTM 编码完整个输入窗口后,在最后一层接一个全连接层,一次性输出未来 N 个时间步的预测值,避免递归多步预测中误差逐步累积的毛病。

多步策略实现方式优点缺点
直接多步输出层一次预测 N 步训练简单、误差不累积步长固定、无法动态调整
递归多步每步预测值作为下一步输入步长灵活误差随步数放大
seq2seqEncoder-Decoder 结构支持变长输入输出实现复杂、训练更慢

2.2 目录结构与模块职责

这套项目的目录组织按「数据、模型、训练、评估」四层拆开,没有把代码堆在一个文件里,二次开发时找对应模块很方便。

stock-lstm-predictor/ ├── configs/ │ └── config.yaml # 全局参数:seq_len、horizon、学习率等 ├── data/ │ ├── raw/ # 原始CSV行情数据 │ └── processed/ # 清洗后的特征矩阵 ├── src/ │ ├── data_loader.py # 滑动窗口切分、Dataset类、归一化 │ ├── model.py # LSTM多步预测模型定义 │ ├── train.py # 训练循环 + checkpoint保存 │ ├── evaluate.py # MAE/RMSE计算 + 可视化 │ └── utils.py # 技术指标计算、日志工具 ├── notebooks/ │ └── explore.ipynb # 数据探索和特征可视化 ├── requirements.txt └── README.md

data_loader.py是整个项目的入口层,负责把 CSV 数据变成模型能吃的张量;model.py只干一件事——定义网络结构,与训练逻辑完全解耦;train.py读取配置、实例化模型和数据加载器、执行训练循环;evaluate.py在训练完成后加载最优权重做测试集评估。这种拆分方式让每个文件都保持短小精悍,出了问题能快速定位。

2.3 数据流:一条数据从 CSV 到损失函数的路径

我把这条链路走了一遍,发现它设计得比较严谨。原始 CSV 数据进入系统后,先做缺失值处理和特征计算,得到完整的特征矩阵;然后用MinMaxScaler做归一化,把所有特征压到 0-1 区间——这一步如果放在切分之后做,会引入未来信息泄露;归一化后的数据送入滑动窗口切片器,按seq_len=60切出历史窗口,按horizon=5切出未来标签;切好的样本组装成 Dataset,经 DataLoader 按 batch 输出形状为(batch_size, seq_len, n_features)的张量;模型输出的形状是(batch_size, horizon),与真实标签直接算 MSE 损失。

整条链路从输入到损失函数都是张量运算,没有在训练循环里混入 Python for 循环逐样本处理,这保证了 GPU 利用率。值得注意的是预测结束后还要做一步反归一化——模型输出的是 0-1 区间内的归一化价格,必须用训练集上拟合的 scaler 逆变换回真实价格,评估指标也全部在真实价格域上计算。

3. 核心模块拆解:数据预处理、LSTM 模型与多步预测策略

3.1 滑动窗口与张量构造

多步预测的第一步是把历史序列切成「窗口输入 + 窗口标签」的样本对。这里的seq_len是回看窗口长度,horizon是预测未来多少天,两者是整个项目最核心的两个超参数。

def create_sequences(data, seq_len=60, horizon=5): X, y = [], [] n_features = data.shape[1] for i in range(len(data) - seq_len - horizon + 1): # 取连续 seq_len 个时间步作为输入 X.append(data[i:i+seq_len, :]) # 取紧接着的 horizon 个步长中的目标列(约定第0列是收盘价) y.append(data[i+seq_len:i+seq_len+horizon, 0]) return np.array(X), np.array(y)

这段代码的逻辑很直白:从第i个位置开始,取 60 天的多变量数据作为输入,取第 61 到 65 天的收盘价作为预测目标。n_features由列数决定,所以换数据源时只要保证目标列在第 0 位,代码不需要改动。滑动窗口的步长默认为 1,即每天都往后挪一格,这样一万条日线数据能切出大约 9900 个样本,训练数据量充足。

我一般会在进入模型前把create_sequences的输出封装成 PyTorch 的TensorDataset,再配合DataLoader做 batch 打乱。注意这里的打乱只能发生在训练集内部,跨样本的时间顺序在训练集内部打乱是可以的——因为每个样本是独立的历史窗口,窗口之间没有重叠依赖。

3.2 LSTM 模型:从多变量输入到多步输出

模型结构是标准的 LSTM 编码器 + 全连接输出层。input_size等于特征数量,hidden_size建议从 64 或 128 开始调参,num_layers=2是时间序列里性价比比较高的选择——一层欠拟合、三层以上在小数据集上容易过拟合。

import torch.nn as nn class LSTMMultiStep(nn.Module): def __init__(self, input_size, hidden_size=128, num_layers=2, horizon=5, device='cpu'): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.device = device # batch_first=True 让输入形状为 (batch, seq_len, input_size) self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 if num_layers > 1 else 0 ) # 直接多步策略:一次性输出 horizon 个时间步 self.fc = nn.Linear(hidden_size, horizon) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的隐状态,形状 (batch, hidden_size) last_hidden = out[:, -1, :] # 全连接层映射到 horizon 维,即未来每个交易日的预测值 return self.fc(last_hidden)

out[:, -1, :]取的是 LSTM 输出序列中最后一个时间步的隐状态,它浓缩了整个输入窗口的信息。这里没有用h_n[-1],是因为out的最后一层输出在语义上更直接。dropout只对非最后一层生效,PyTorch 的nn.LSTM里dropout参数指定的是层之间的 dropout 概率,不会作用到输出层。

如果想让模型在训练时更快收敛,可以在这里加一个nn.BatchNorm1d(hidden_size)再进全连接层;但要注意批量归一化在时间序列预测里有时会让测试集表现不稳定,因为训练和测试时 batch 统计量不同。我一般先不加,等出现明显的过拟合迹象再调整。

3.3 训练循环与多步损失计算

多步预测的损失函数和单步预测没有本质区别——把horizon个输出当成一个整体,直接对每个时间步的预测值和真实值求 MSE。这里不需要对不同步长的误差做加权,因为股票预测中远期误差本来就比近期大,模型会自然学到这一点。

def train_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss = 0.0 for X_batch, y_batch in dataloader: # y_batch 形状: (batch, horizon),整体作为回归目标 X_batch = X_batch.to(device) y_batch = y_batch.to(device) optimizer.zero_grad() pred = model(X_batch) # (batch, horizon) loss = criterion(pred, y_batch) # 多步一起算损失 loss.backward() optimizer.step() total_loss += loss.item() * X_batch.size(0) return total_loss / len(dataloader.dataset)

这里有个容易被忽视的细节:criterion也就是nn.MSELoss(),默认返回 batch 内所有元素损失的平均值。如果某天数据方差特别大,单个异常样本会把整体损失拉得很高,模型会在几个 epoch 内反复纠正这个离群点,导致训练曲线抖动。遇到这种情况有两种解法:一是用HuberLoss(Smooth L1)替换 MSE,降低离群点权重;二是在数据预处理阶段把明显的异常交易日(如涨跌停导致的极端值)做 winsorize 截断。项目里默认用 MSE,好算且敏感,但落到真实数据上我会先看一眼训练曲线的抖动程度再决定要不要换成 Huber。

3.4 预测与反归一化

训练完的模型在预测阶段要进eval()模式,并包在torch.no_grad()里,避免梯度计算占额外显存。预测代码的另一个关键点是归一化的逆操作,很多人在这上面翻车。

def predict(model, X_tensor, scaler): model.eval() with torch.no_grad(): pred_norm = model(X_tensor) # 输出归一化空间的值 # 反归一化:只反归一化目标列(收盘价) # 注意:scaler 是在训练集上 fit 的,不能用全量数据 fit pred_full = scaler.inverse_transform( _pad_for_inverse(pred_norm.cpu().numpy()) ) y_true_full = scaler.inverse_transform( _pad_for_inverse(y_test_numpy) ) return pred_full[:, 0], y_true_full[:, 0]

scaler.inverse_transform要求输入的特征维度与 fit 时一致,所以需要把(batch, horizon)的预测结果先填充回原始特征形状,再取第 0 列。这个填充逻辑通常在utils.py里做——先初始化一个全零数组,把预测值填到第 0 列,其他列保持 0,逆变换后只取第 0 列。如果预测时报错维度不匹配,八成是这里填充的形状对不上。反归一化之后才能计算 RMSE、MAE 这些真实价格域指标,同时画预测曲线和真实曲线的对比图。

4. 运行指南:环境配置、数据准备与训练评估全流程

4.1 环境配置与依赖安装

项目基于 PyTorch 2.x 开发,安装时先确认本机是否有可用的 CUDA 计算平台。PyTorch 官方安装命令会根据 CUDA 版本自动选择对应的 wheel 包,这里给出 CPU 和 GPU 两种场景下的依赖清单。

# 基础依赖 pip install torch pandas numpy matplotlib scikit-learn pyyaml # 如果训练中要用技术指标,可选装 TA-Lib(需要系统编译器支持) pip install TA-Lib

安装完成后验证一下 CUDA 是否可用:

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

如果输出True就能用 GPU 训练,否则代码会自动切到 CPU。项目代码里在train.py开头做了设备判断——device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),所以两条路都能跑。CPU 训练一万条日线数据、60 步窗口、128 隐层,大概每轮几秒钟;GPU 几乎瞬时。数据量上万条时差距不明显,十万元素以上的矩阵运算差距才会拉开。

4.2 数据准备与字段说明

项目默认的 CSV 格式是标准的 OHLCV 五列加日期,但多变量预测真正厉害的地方在于可以额外添加技术因子。下面是一个示例:

dateopenhighlowclosevolumema5ma20rsimacd
2023-01-0310.210.59.910.42350010.19.855.30.12
2023-01-0410.510.810.310.72890010.39.958.10.15

日期列需要解析成datetime类型并按升序排列,这一点很关键——LSTM 是按照时间顺序读取序列的,如果原始数据是倒序或者中间有缺失交易日,切出来的窗口会错乱。缺失值的常见处理方式是前向填充,也就是用前一个交易日的值补今天的缺失,这比用均值填充或删除整行更符合金融数据的连续性。

技术指标的计算如果不想引入 TA-Lib,可以用 pandas 手写简化版——ma5就是close.rolling(5).mean(),RSI是过去 N 日涨跌幅均值的比值。这些衍生特征和原始价格放在同一特征矩阵里,作为 LSTM 每个时间步的输入向量。

4.3 训练参数配置文件

项目用 YAML 文件统一管理训练参数,不硬编码在代码里。打开configs/config.yaml能看到一组默认配置:

data: raw_path: "data/raw/stock.csv" processed_path: "data/processed/features.csv" seq_len: 60 # 回看窗口:60个交易日 horizon: 5 # 预测未来:5个交易日 train_ratio: 0.8 # 训练集比例,按时间顺序切分 model: hidden_size: 128 num_layers: 2 dropout: 0.2 train: batch_size: 64 epochs: 100 lr: 0.001 weight_decay: 1e-5 seed: 42

seq_len=60对应约 3 个月的交易日数——大多数技术分析派认为 3 个月窗口能覆盖中期趋势;horizon=5对应一周的交易日。这两个参数是预测效果最敏感的部分,建议先按默认值跑通,再对比不同组合的效果。train_ratio=0.8是按时间顺序切分的,前 80% 数据训练、后 20% 测试,不能打乱后随机切——否则未来数据会泄漏进训练集。

训练命令可以直接跑:

python src/train.py --config configs/config.yaml

训练过程中控制台会打印每个 epoch 的训练损失和验证损失,同时每 10 个 epoch 把当前 checkpoint 存到checkpoints/目录。日志里出现loss: 0.0032 - val_loss: 0.0041这种数值时,说明模型在正常收敛;如果训练损失持续下降但验证损失不降反升,就是过拟合信号,需要增加 dropout 或减小hidden_size。

4.4 评估指标与可视化

评估脚本输出的指标有三个,都在真实价格域上计算:MAE 衡量平均绝对误差,RMSE 对较大误差更敏感,MAPE 把误差转成百分比便于跨股票比较。对于一个价格在 10 元附近的标的,MAE=0.15意味着平均每天预测偏差约 1.5%。

# src/evaluate.py 输出的核心部分 def compute_metrics(y_true, y_pred): mae = np.mean(np.abs(y_true - y_pred)) rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return {"MAE": mae, "RMSE": rmse, "MAPE (%)": mape}

可视化脚本会生成两张图:一张是测试集上预测值与真实值的对比曲线,另一张是最近一次预测的多步展开(从第 60 天窗口预测未来 5 天)。对比曲线能直观地看出模型是否存在滞后问题——如果预测曲线整体向右偏移一位,多半是模型学到了「今天的价格约等于昨天」这种惰性模式,此时特征工程里要加入差分项或收益率。

5. 踩坑记录:时间序列预测里最常见的 5 个问题

5.1 训练损失很低,测试集预测却是一条平线

现象:训练迭代到 50 轮时 loss 降到 0.002 以下,看起来收敛得很好,但预测曲线几乎是一条水平直线,完全没有跟随真实价格的波动。

原因:这是我在时间序列预测里见过最多的问题——归一化泄露。如果MinMaxScaler是在整个数据集(包括测试集)上 fit 的,那么训练时模型就偷看了未来数据的最大值和最小值。更隐蔽的原因是窗口切分时没有做时间偏移,有些实现会在构造样本时把未来信息带进特征列。

解决:严格先在训练集上单独fit_transform,测试集只做transform;并且做train_test_split时用data[:split_idx]和data[split_idx:]的有序切分,而不是随机抽样。项目代码里已经把这一步封装好了,但如果你从别处拷贝代码,这是第一个要检查的位置。

5.2 多步预测越往后越偏离真实值

现象:预测未来 20 天时,第 1 天还算准,第 10 天开始逐渐偏移,到第 20 天几乎偏出合理区间。

原因:这是直接多步预测的固有现象。LSTM 编码器学到的时间依赖是「最近几天的变化趋势」,预测远处目标时隐状态里携带的近期信息已经衰减。另一个原因是损失函数给了 5 个步长同样的权重,模型在权衡后把资源更多地分配给了近期步长。

解决:一个可行办法是损失函数分步加权——近期步长权重大、远期步长权重小,让模型有针对性地优化近期。另一个更激进的做法是换用 seq2seq 架构,让解码器逐步「生成」未来序列,我在第 6 章会给出具体改造方向。

5.3 预测曲线比真实曲线滞后一天

现象:预测值的转折点总是比真实值晚出现一天,预测曲线看起来像是把真实曲线向右平移了一格。

原因:股票价格序列具有极强的自相关——今天的收盘价和昨天高度相关。LSTM 在 MSE 损失下发现最稳妥的策略是「近似复制最近一天的值」,因为这样损失最小。这不是模型欠拟合,而是目标函数导致的惰性预测。

解决:在特征矩阵中加入一阶差分列,即close.diff(),让模型预测的是「变化量」而不是「价格绝对值」,最后用上一日真实价 + 预测变化量还原预测价格。这个方法从根上切断了复制行为,是处理金融时序预测最有效的特征工程技巧之一。

5.4 显存不足或训练中途进程被杀

现象:训练到第 30 轮时 PyTorch 报CUDA out of memory错误,进程直接退出。

原因:滑动窗口构造的样本矩阵如果全部载入内存,(样本数, 60, 特征数)的 numpy 数组可能占用数 GB 内存;再叠加 DataLoader 的 batch 张量,显存就吃不消了。如果seq_len从 60 加到 240,显存占用线性增长。

解决:把滑动窗口处理改成自定义Dataset类,每次__getitem__时才切窗口,避免一次性生成全量大数组。另外训练时可以在 DataLoader 里设pin_memory=True,在train_epoch里用scaler.scale(loss).backward()和梯度裁剪配合混合精度训练——这个项目没默认启用 AMP,但留了接口。

5.5 盲用 K 折交叉验证导致数据泄露

现象:用KFold对训练集做交叉验证选超参数,验证集表现好得离谱,但换到真实测试集直接崩。

原因:KFold默认随机打乱后切分,这意味着某些训练折里包含了时间上晚于验证折的数据——未来信息渗透进了训练过程,模型等于开卷考试。时间序列场景里这是致命的架构错误。

解决:改用TimeSeriesSplit,它按时间顺序切分,每个后续折的训练集永远比验证集更早。如果样本量太少,使用扩窗(expanding window)比滚动窗(rolling window)能保留更多训练数据。记住一条准则:任何数据打乱操作,只要跨越了时间边界,都是在给模型透题。

6. 二次开发:把多步预测改造成自己的策略引擎

6.1 从直接多步切换到 seq2seq 架构

直接多步输出的问题在于未来 N 步是一次性生成的,没有建模步长之间的自相关性。改成 seq2seq 后,编码器 LSTM 编码输入窗口,解码器 LSTM 逐步生成未来序列,每一步的输入是上一步的预测值。改造的核心是把model.py里的单层 LSTM 拆成两个模块,并引入教师强制机制——训练时按比例用真实值作为解码器输入,测试时用上一步预测值。

class Seq2SeqPredictor(nn.Module): def __init__(self, input_size, hidden_size, horizon): super().__init__() self.encoder = nn.LSTM(input_size, hidden_size, batch_first=True) self.decoder = nn.LSTM(input_size=1, hidden_size=hidden_size, batch_first=True) self.fc_out = nn.Linear(hidden_size, 1) def forward(self, x, y_true=None, teacher_forcing_ratio=0.5): _, (h, c) = self.encoder(x) inputs = x[:, -1, 0:1] # 用最后一天收盘价作为解码器初始输入 outputs = [] for t in range(self.horizon): out, (h, c) = self.decoder(inputs, (h, c)) pred = self.fc_out(out[:, -1, :]) outputs.append(pred) # 训练阶段按概率选择真实值或预测值作为下一步输入 use_true = (y_true is not None) and (torch.rand(1) < teacher_forcing_ratio) inputs = y_true[:, t:t+1] if use_true else pred return torch.cat(outputs, dim=1)

这里teacher_forcing_ratio=0.5表示 50% 的概率用真实值喂给解码器,其余用预测值。训练结束后推理时把该值设为 0,完全自回归。改造后你会发现远期预测的误差累积问题明显改善,代价是训练时间翻倍——解码器内部多了 N 步循环。

6.2 加入注意力机制增强特征对齐

LSTM 编码器最后一个时间步的隐状态要浓缩全部历史信息,信息瓶颈非常明显。加入注意力机制的思路是:解码器的每一步都回头查看编码器的所有隐状态,计算加权和作为当前步的上下文向量。具体实现是在解码器每一步计算注意力权重softmax(score(h_decoder, h_encoder)),然后用权重求和得到新的上下文向量,再拼接到当前输入上。改造量大约 40 行代码,对预测精度的提升通常比增加hidden_size更有效,但对新手来说调试也更耗时——score函数的设计(点积、拼接、加性)直接决定训练是否稳定。

6.3 多股票联合预测与策略回测入口

项目默认每次训练单支股票,但量化场景中多支股票联合建模能学到板块联动特征。改造方法有两个选择:简单做法是把多支股票的收益率序列按列拼接,输入维度变为特征数 × 股票数;更优雅的做法是引入股票 ID 嵌入向量,拼接到特征矩阵中,让模型显式感知「这是在预测哪只股票」。改造完成后,预测结果可以直接接到回测模块里——把预测值与阈值比较生成买卖信号,再用收益率序列计算累计收益和最大回撤。这个项目没有内置回测,但evaluate.py输出的y_true和y_pred已经是干净的价格数组,接任何回测库都方便。

我现在的习惯是:每次改了数据预处理或者模型结构,都强制跑一遍notebooks/explore.ipynb里写好的边界样本检查脚本,先打印一个窗口的输入和标签,确认窗口切分、归一化、步长对齐都没问题,再开始训练。这一段检查只需要两分钟,但节省过无数次排查 bug 的时间。希望帮到你。

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

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

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

立即咨询