
简介这份资源面向具备一定深度学习基础、希望用PyTorch实践金融时间序列预测的学习者聚焦LSTM多变量多步股票预测这一典型任务帮助读者打通从数据预处理、特征构建到序列到序列建模的完整链路。压缩包共11个文件约222KB以Python主程序为核心辅以xlsx格式的股票数据样本、png结果图以及xml、iml等IDE工程配置便于直接导入运行与对照调试。资源已有9250人学习下载热度较高。内容围绕LSTM门控机制、编码器—解码器结构、教师强制策略、MSE/MAE损失与Adam优化、训练验证划分及早停法等知识点展开并涉及皮尔逊相关系数评估与预测对比图绘制读者可据此理解多变量多步预测的建模思路与调参方向同时明确金融市场非线性带来的预测局限。1. 一份能跑通的 LSTM 多变量多步预测源码到底解决了什么股票预测这个方向网上教程多如牛毛但真正能跑通、能改参数、能解释清楚每一步在干什么的完整源码包并不多。我见过太多人卡在同一个地方单变量单步的 LSTM 跑得挺顺一换成多变量多步就翻车——输入维度对不上、标签窗口切错、归一化在训练集和测试集之间泄漏、预测出来的曲线永远滞后一天。这份基于 PyTorch 的 LSTM 多变量多步预测资源核心价值不在于“预测准不准”这种玄学问题而在于它把从原始行情数据到多特征输入、再到未来 N 步输出的完整链路拆开了每一步都有对应的代码和参数可调。适合已经会写基础 PyTorch 训练循环、想搞清楚时序预测工程细节的从业者也适合拿它当模板改造成销量预测、负荷预测、传感器趋势预测的同行。下面我按自己拆包复现的顺序把关键环节和踩过的坑摊开讲。2. 多变量多步预测的数据管线从原始行情到模型输入张量2.1 为什么多变量比单变量难难在哪单变量预测只喂收盘价一列模型输入形状是(batch, seq_len, 1)输出(batch, 1)或(batch, pred_len)维度干净。多变量就不一样了假设你用开盘、最高、最低、收盘、成交量五列输入变成(batch, seq_len, 5)模型要学的不只是时间维度的模式还有特征之间的耦合关系。更麻烦的是多步预测——你要一次性输出未来 5 天或 10 天的值标签的切法、损失的计算方式、评估指标的定义全都跟着变。常见做法是把多步预测拆成两种策略递归预测predict one, feed back和直接多输出direct multi-output。这份源码走的是直接多输出路线一次前向传播吐出pred_len个值。选它的理由很实际递归预测误差会累积第一步偏一点后面越滚越歪调试时很难定位是模型问题还是反馈回路问题直接多输出虽然对模型容量要求高一些但训练目标清晰损失函数直接对着未来 N 步算排查起来有据可依。2.2 特征工程与归一化的正确姿势多变量输入绕不开归一化但这里有个血泪经验归一化必须只用训练集拟合再 transform 到验证集和测试集。我见过不少代码直接在全体数据上fit_transform回测曲线漂亮得不像话一上实盘就原形毕露。这份资源的处理逻辑是先用训练集算均值和标准差保存下来后续所有集合统一用这组参数变换。import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler def build_features(df, feature_cols, target_col, seq_len, pred_len): df: 原始 DataFrame含 OHLCV 等列 feature_cols: 输入特征列名列表 target_col: 预测目标列名通常是收盘价 seq_len: 回看窗口长度 pred_len: 预测未来步数 data df[feature_cols].values.astype(np.float32) target df[target_col].values.astype(np.float32).reshape(-1, 1) # 按时间顺序切分前 70% 训练后 30% 测试不做随机打乱 split int(len(data) * 0.7) train_data, test_data data[:split], data[split:] train_target, test_target target[:split], target[split:] # 关键scaler 只在训练集上 fit feat_scaler StandardScaler().fit(train_data) target_scaler StandardScaler().fit(train_target) train_scaled feat_scaler.transform(train_data) test_scaled feat_scaler.transform(test_data) train_target_scaled target_scaler.transform(train_target) test_target_scaled target_scaler.transform(test_target) def make_windows(feat, tgt): X, y [], [] for i in range(len(feat) - seq_len - pred_len 1): X.append(feat[i:i seq_len]) y.append(tgt[i seq_len:i seq_len pred_len].flatten()) return np.array(X), np.array(y) X_train, y_train make_windows(train_scaled, train_target_scaled) X_test, y_test make_windows(test_scaled, test_target_scaled) return X_train, y_train, X_test, y_test, feat_scaler, target_scaler这段代码有几个参数值得说清楚。seq_len决定模型能看多远的历史设太小模型学不到趋势设太大训练慢且容易过拟合日线数据一般从 20 到 60 之间试。pred_len是你要预测的未来天数设 5 就是一次输出未来一周。make_windows里标签的切片是tgt[i seq_len : i seq_len pred_len]意思是“用第 i 到 iseq_len-1 步的输入预测紧接着的 pred_len 步”这个偏移量错一位整个训练就白做。另外注意flatten()把(pred_len, 1)压成(pred_len,)方便后面和模型输出对齐。提示切分必须按时间顺序绝对不能train_test_split(shuffleTrue)。时序数据一打乱未来信息泄漏到训练集回测指标会好得离谱但没有任何参考价值。2.3 Dataset 与 DataLoader 的封装细节窗口切好之后要包成 PyTorch 的 Dataset这一步本身不难但有几个细节容易埋雷。比如张量类型必须是float32用默认的float64会在训练时报类型不匹配再比如__getitem__返回的 y 形状要和模型输出严格一致差一个维度就会在算 loss 时广播出诡异的结果。import torch from torch.utils.data import Dataset, DataLoader class StockWindowDataset(Dataset): def __init__(self, X, y): # X: (N, seq_len, n_features), y: (N, pred_len) self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_ds StockWindowDataset(X_train, y_train) test_ds StockWindowDataset(X_test, y_test) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, drop_lastTrue) test_loader DataLoader(test_ds, batch_size64, shuffleFalse)训练集shuffleTrue是为了打散样本顺序、让每个 batch 的梯度估计更稳但注意这里打乱的是“窗口样本”的顺序不是时间步内部的顺序窗口内部的时间关系仍然完整。drop_lastTrue是为了避免最后一个不满 64 的 batch 在 BatchNorm 或某些损失计算时出问题如果你的模型没用 BatchNorm这个参数可留可去。测试集必须shuffleFalse否则预测结果和真实标签对不上号画图时曲线会乱成一团。3. LSTM 模型搭建与多步输出头结构选型和参数怎么定3.1 单层还是多层hidden_size 给多大PyTorch 的nn.LSTM用起来简单但参数选择有讲究。input_size必须等于特征列数这个对不上直接报错反而是最好排查的。hidden_size是隐藏状态维度日线数据我一般从 64 起步数据量大、特征多可以上到 128 或 256再大就容易过拟合尤其样本只有几千条的时候。num_layers设 2 层是常见折中单层表达能力有限三层以上训练慢且梯度传播路径变长没有明显收益。多步输出头的接法有两种一种是把 LSTM 最后时刻的 hidden state 接一个Linear(hidden_size, pred_len)直接吐 pred_len 个值另一种是每个时间步都接输出再取最后几个。这份源码用的是前者结构干净参数量可控。我倾向这种接法的原因是多步预测本质是“基于整段历史做一个多目标回归”最后时刻的 hidden state 已经浓缩了 seq_len 步的信息直接映射到 pred_len 维输出比逐时间步解码更不容易引入额外噪声。import torch.nn as nn class MultiStepLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, pred_len5, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.head nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size // 2, pred_len) ) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) last_hidden out[:, -1, :] # 取最后一个时间步的输出 return self.head(last_hidden)batch_firstTrue这个参数必须显式设PyTorch 的 LSTM 默认输入形状是(seq_len, batch, input_size)不设的话你的(batch, seq_len, features)会被当成序列长度在前训练能跑但语义全错预测结果毫无意义。dropout只在num_layers 1时生效单层 LSTM 传了也白传代码里做了条件判断避免误导。输出头加了一层hidden_size // 2的降维和 ReLU是为了在映射到 pred_len 之前再做一次非线性压缩实测比直接一层 Linear 收敛更稳当然这不是铁律你可以按自己的数据试。3.2 损失函数、优化器和学习率调度多步回归默认用 MSELoss但如果你更关心方向对不对而不是数值差多少可以换成 HuberLoss 或加一个方向惩罚项。优化器 Adam 起步学习率 1e-3 是常规操作配合ReduceLROnPlateau在验证损失停滞时降学习率比固定学习率稳得多。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model MultiStepLSTM(input_sizeX_train.shape[2], hidden_size64, num_layers2, pred_len5).to(device) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 )weight_decay1e-5是轻量 L2 正则防止 LSTM 权重长得太大。patience5表示验证损失连续 5 个 epoch 没降就砍半学习率这个值设太小会频繁降 lr 导致欠拟合设太大又起不到调度作用5 到 10 之间比较稳妥。训练循环里记得每个 epoch 结束调scheduler.step(val_loss)传的是验证损失而不是训练损失否则调度依据就错了。3.3 训练循环与验证监控训练循环本身是标准套路但多步预测有个细节验证时要把模型输出和标签都反归一化回原始价格尺度才能算有业务意义的 MAE 或 MAPE。如果只在归一化空间里看 loss数值很小但说明不了实际问题。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss 0.0 for X_batch, y_batch in loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * len(X_batch) return total_loss / len(loader.dataset) def evaluate(model, loader, criterion, device): model.eval() total_loss 0.0 with torch.no_grad(): for X_batch, y_batch in loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) pred model(X_batch) total_loss criterion(pred, y_batch).item() * len(X_batch) return total_loss / len(loader.dataset)clip_grad_norm_这行是多变量 LSTM 的后悔药。特征尺度差异大、序列又长的时候梯度爆炸是家常便饭表现是 loss 突然变成 nan 或者剧烈震荡。max_norm1.0是常用起点如果发现梯度经常被裁到说明学习率可能偏大先降 lr 再调这个阈值。训练和验证的 loss 都要记录两条曲线一画过拟合还是欠拟合一目了然训练降验证不降是过拟合两条都高是欠拟合或学习率不对。4. 预测结果反归一化与评估别被归一化空间的 loss 骗了4.1 反变换与多步预测的对齐模型输出是归一化空间的(batch, pred_len)要还原成价格得用之前保存的target_scaler。这里有个容易翻车的点StandardScaler.inverse_transform要求输入是二维(n_samples, n_features)而你的 pred_len 可能大于 1直接传会报维度错误或者还原错位。正确做法是 reshape 成(-1, 1)反变换再 reshape 回来。def inverse_target(scaled_arr, target_scaler): scaled_arr: (N, pred_len) 归一化空间的预测或标签 返回同形状的原始尺度数组 shape scaled_arr.shape flat scaled_arr.reshape(-1, 1) inv target_scaler.inverse_transform(flat) return inv.reshape(shape)这个函数看着简单但reshape(-1, 1)再reshape(shape)的顺序不能反。如果先转置或者按列拆反变换用的均值和标准差就对不上对应步还原出来的价格会整体偏移。我一般会在小样本上手动验算一遍取一条预测反变换后和原始价格比误差应该在合理范围内对不上就说明对齐逻辑有问题。4.2 评估指标怎么选才不误导MSE 和 RMSE 对量纲敏感价格高的时候数值大跨股票没法比。MAPE 直观但有除零风险价格接近零时爆炸。这份资源里我建议同时看三个指标MAE绝对误差单位是价格、RMSE对大误差更敏感、以及方向准确率预测涨跌方向和真实方向一致的比例。方向准确率对交易场景尤其重要因为数值差一点但方向对了策略可能仍然有效方向反了数值再准也没用。指标计算方式适用场景注意点MAE绝对误差均值看平均偏差多大单位同价格直观RMSE均方误差开根惩罚大误差对异常值敏感MAPE绝对百分比误差跨标的比较价格近零时失效方向准确率涨跌符号一致比例交易信号评估需定义涨跌阈值评估时按 pred_len 的每一步分别算而不是把所有步混在一起平均。第 1 步通常最准越往后越差分步看才能知道模型在多远的未来还可信。如果第 3 步之后方向准确率掉到 50% 附近说明预测窗口设太长了该缩pred_len。5. 避坑与排查多变量多步 LSTM 最常见的五个翻车现场5.1 现象训练 loss 正常下降预测曲线却整体平移原因几乎都是归一化泄漏或反变换错位。要么 scaler 在全体数据上 fit 了要么反变换时 reshape 顺序搞错导致还原后的价格整体偏高或偏低一个固定量。排查方法取训练集第一条样本手动走一遍 scaler.transform 再 inverse_transform看能否还原原始值不能就是变换链路有问题。5.2 现象预测值几乎是一条直线没有波动模型欠拟合的典型表现常见于 hidden_size 太小、num_layers 只有 1 层、或者学习率过低训练轮次不够。也可能是输入特征全是价格类高度相关的列模型学不到额外信息。解决办法先把 hidden_size 提到 128、num_layers 设 2学习率调到 1e-3 跑 50 个 epoch 看 loss 是否还在降如果仍然平检查特征列是否该加入成交量、换手率、技术指标等差异化信息。5.3 现象loss 突然变 nan 或剧烈震荡多变量 LSTM 的经典问题梯度爆炸。先确认有没有加clip_grad_norm_没加就加上加了还炸检查输入特征是否有极端异常值比如未复权的价格跳变、成交量单位错误做一遍异常值截断或对数变换。另外学习率 1e-3 对某些数据偏大降到 5e-4 或 1e-4 试试。5.4 现象验证集表现远差于训练集过拟合但别急着加 dropout。先看样本量如果总窗口数只有几百条模型参数却几十万过拟合是必然的。优先做的是减小 hidden_size、减层数、增大 dropout 到 0.3再考虑加 weight_decay。另一个常被忽略的点是特征太多而样本太少五六个特征配几千条日线数据是合理的十几个特征就需要更长的历史或更强的正则。5.5 现象换了只股票或换了时间段效果断崖式下跌这不是代码 bug是时序预测的固有边界。不同标的的波动率、趋势性、噪声水平差异很大在一只票上调好的超参数搬到另一只上未必成立。我的做法是每次换数据都重新走一遍特征缩放和窗口切分超参数至少重调 hidden_size 和 pred_len 两个不要指望一套参数打天下。另外注意停牌、除权除息造成的价格跳变这些点在训练前要清洗掉否则模型会把跳空当成可学习的模式。6. 进阶技巧用 walk-forward 验证替代单次切分单次 70/30 切分只能告诉你“模型在这段时间上表现如何”但时序数据的分布会漂移一次切分的结论很可能只是运气。更靠谱的做法是 walk-forward 验证用前一段训练、紧接着的一段验证然后窗口整体前移重复多次把每折的指标平均。这样得到的评估更接近实盘滚动使用的场景。def walk_forward_eval(df, feature_cols, target_col, seq_len, pred_len, n_splits5, train_ratio0.6): 滚动切分每次训练集占 train_ratio验证集为紧随其后的一段 total_len len(df) fold_size int(total_len * (1 - train_ratio) / n_splits) results [] for k in range(n_splits): train_end int(total_len * train_ratio) k * fold_size val_end train_end fold_size if val_end total_len: break train_df df.iloc[:train_end] val_df df.iloc[train_end - seq_len:val_end] # 带上下文重叠 # 此处调用前面的 build_features / 训练 / 评估流程 # 记录该折的 MAE、RMSE、方向准确率 results.append({fold: k, train_end: train_end}) return results关键在val_df的起点要回退seq_len步因为验证集的第一条窗口需要前面的历史才能构造出来不回退就会丢掉开头几个样本折与折之间的评估口径不一致。n_splits设 5 折是精度和耗时的折中数据量小可以设 3数据多可以设 8 到 10。每折的指标单独记录再取均值和标准差标准差大说明模型对时间段敏感实盘要谨慎。还有一个我常用的技巧把方向准确率按 pred_len 分步画出来横轴是预测步数纵轴是准确率。如果第 1 步 65%、第 5 步掉到 48%那 pred_len 设 5 就是自欺欺人实际可用的只有前两三步。这个图比任何单一数字都更能说明模型的真实能力边界。从那以后我每次拿到一份时序预测代码都强制先跑一遍 walk-forward再决定要不要信它 README 里写的指标。单次切分的漂亮数字翻车概率太高了。希望这份拆解能帮你少走几个弯路把这份 LSTM 多变量多步预测的源码真正用起来。本文还有配套的精品资源点击获取