ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LSTM时序收益预测系统搭建指南:从数据准备到避坑实践

LSTM时序收益预测系统搭建指南:从数据准备到避坑实践 简介一套基于长短期记忆网络LSTM的时序收益预测系统代码包面向具备Python基础、希望将深度学习应用于金融时序分析的开发者也适合毕业设计或竞赛快速起步。项目以大宗商品收益预测为实际场景完整覆盖数据预处理、模型构建、训练调参与效果评估全流程可帮助理解LSTM输入门、遗忘门、输出门如何解决RNN梯度消失问题。压缩包共55个文件包含3个Python脚本数据创建、模型训练、预测测试、TensorFlow模型权重及checkpoint文件、npy格式的预测数组、Excel原始数据、模型训练曲线和拟合效果截图、docx操作说明及doc开题报告整体约2.27MB目录按data、models、logs清晰分区便于按流程学习。目前已有2911人学习。通过运行脚本并对照实验截图可复现贵金属、化工、有色三类大宗商品的收益预测实验掌握数据标准化、MinMaxScaler保存、训练日志分析以及MAE/RMSE评估方法并可直接调用模型文件与预处理脚本为后续参数调优和二次开发提供完整起点。1. 基于 LSTM 的时序收益预测系统为什么值得自己写一版如果你接手过一套基于 LSTM 的时序收益预测系统应该能理解我最初的状态模型能跑、损失在降可一到真实决策方向命中率还不如抛硬币。后来我把这套系统从数据定义开始重写了一遍才意识到问题不在 LSTM 本身而在序列构造、归一化和评估口径。标题里的工程重点不是背一套 PyTorch 源码就完事而是回答三个实际问题预测什么收益、用什么特征、如何防止模型学到未来的信息。这篇文章面向想用深度学习做金融时序预测、收益预测的工程师会给出完整可运行的代码、参数边界和几个我踩过的坑照着做能跑通一个能用于后续迭代的基线系统。2. 数据准备先定义收益率再构造滑动窗口特征2.1 预测收益率而不是价格平稳性决定模型上限做收益预测系统第一件事不是打开 Jupyter 直接写 LSTM而是把预测目标定死。我见过很多从价格预测转到收益预测的团队直接拿close价格做标签让 LSTM 去拟合价格曲线。价格序列是非平稳的有趋势、有漂移模型学到的是“上一段时间的价格水平”而不是“下一段会涨还是会跌”。一旦价格中枢变化模型立刻失效。收益率序列pct_change通常近似平稳均值和方差在长期统计上相对稳定这才是 LSTM 能稳定学习的对象。这里推荐用简单收益率而不是对数收益率。对数收益率在数学上更好加总但回测时需要用exp(cumsum)还原价格多一步转换就容易把符号搞错。日频收益率的取值一般在 -0.1 到 0.1 之间对数值差异不大。目标变量的定义也要注意你到底是预测下一个交易日的收益还是未来 5 个交易日的累计收益我一般先做单步预测也就是用第t天的数据预测第t1天的收益。单步预测稳定、信号干净后续要扩展成多步滚动预测也方便。2.2 特征设计哪些输入值得喂给 LSTM输入特征不需要多但要有经济含义。我做这套系统时特征就四类基础收益、波动率、成交量变化和星期几的 one-hot。收益历史本身就是最重要的特征LSTM 能从过去的收益形态里捕捉动量或反转20 日滚动标准差代表近期波动状态成交量变化可以反映资金活跃度。外生特征不一定每个都有效但至少要在数据准备阶段留好接口后面想加资金流、舆情分数时不用重写整个数据管线。这里要强调一个问题特征的时间对齐。所有特征必须是在t时刻能拿到的数据不能用t1及以后的信息。常见做法是统一用shift操作把未来信息全部排掉再做滑动窗口。我在早期版本里把交易量数据直接拼进去没注意量价在盘中是同步发生的等于把未来成交信息透传给了模型回测漂亮得不可思议。2.3 滑动窗口切分与按时间顺序的数据集划分核心代码分为三步读数据算收益、构造滑动窗口样本、按时间切分并归一化。import numpy as np import pandas as pd def build_dataset(csv_path, seq_len20): df pd.read_csv(csv_path, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 收益率前向差分第一个值为 NaN df[ret] df[close].pct_change() # 滚动波动率近 20 个交易日收益的标准差 df[vol_ma20] df[ret].rolling(20).std() # 成交量变化率 df[vol_chg] df[volume].pct_change() # 预测目标下一个交易日的收益率shift(-1) 保证不泄漏 df[target] df[ret].shift(-1) df df.dropna().reset_index(dropTrue) feature_cols [ret, vol_ma20, vol_chg] values df[feature_cols].values.astype(np.float32) targets df[target].values.astype(np.float32) X, y [], [] # 对每个时刻 t取 [t-seq_len, t) 作为输入t 时刻的收益作为标签 for i in range(seq_len, len(df)): X.append(values[i - seq_len:i, :]) y.append(targets[i]) return np.array(X), np.array(y), df逻辑说明pct_change()得到日收益率后第一行是 NaNrolling(20).std()计算的是最近 20 个交易日的波动率target用shift(-1)把下一天的收益作为标签。代码里特意把dropna()放在target构造之后否则最后一行的target变成 NaN 会影响对齐。滑动窗口的索引从seq_len开始保证每个样本都有完整的seq_len个时间步。参数说明seq_len20对应一个月的日频交易窗口这是金融时序预测里比较稳妥的默认值。特征维度是 3如果你的数据里加了星期几 one-hot这里会自动变成 7。from sklearn.preprocessing import StandardScaler def time_split(X, y, train_ratio0.7, valid_ratio0.15): n len(X) t1 int(n * train_ratio) t2 t1 int(n * valid_ratio) return (X[:t1], y[:t1]), (X[t1:t2], y[t1:t2]), (X[t2:], y[t2:]) def fit_scaler_on_train(X_train, X_valid, X_test): # 关键scaler 只能 fit 训练集验证集和测试集用同一套统计量 scaler StandardScaler() n_feat X_train.shape[-1] scaler.fit(X_train.reshape(-1, n_feat)) X_train scaler.transform(X_train.reshape(-1, n_feat)).reshape(X_train.shape) X_valid scaler.transform(X_valid.reshape(-1, n_feat)).reshape(X_valid.shape) X_test scaler.transform(X_test.reshape(-1, n_feat)).reshape(X_test.shape) return X_train, X_valid, X_test, scaler这段代码强调两个点。第一切分必须按时间顺序不能随机打散。时序样本之间有重叠窗口随机切分等于让模型同时看到过去和未来。第二归一化只用训练集的均值和方差。如果对整个数据集做fit验证集和测试集的统计信息就泄漏进了训练过程最终评估结果会虚高上实盘就翻车。3. 模型搭建与训练PyTorch 实现 LSTM 收益预测3.1 模型结构单层 LSTM 加线性输出头很多教程一上来就堆两层 LSTM 加注意力再接全连接但在收益预测这种低信噪比场景模型容量越大越容易把噪声背下来。我一般先用单层 LSTM隐藏单元 64输出直接用线性层压到 1 个数值。收益率的分布虽然有少量极端值但整体是有界的线性输出就能回归不需要在输出端加 tanh 或 sigmoid 去压缩范围。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden_size64, num_layers1, dropout0.0): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0, ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x shape: (batch, seq_len, n_features) out, _ self.lstm(x) # 取最后一个时间步的隐藏状态作为序列表示 last_hidden out[:, -1, :] return self.fc(last_hidden)逻辑说明batch_firstTrue让输入张量的维度是(batch, seq_len, features)这样更符合读数据的直觉。out[:, -1, :]取的是最后一个时间步的输出它等价于 LSTM 最后一步的隐状态h_n。注意这里没有用h_n直接切片是因为直接在输出序列上取更清晰能避免对 PyTorch LSTM 返回值顺序的困惑。dropout参数在num_layers1时会自动置零因为单层 LSTM 内部没有额外层可以丢。这个结构要注意的点是你喂进去的特征数量必须和n_features一致。如果第 2 章的数据准备里加了 one-hot 特征这里的n_features要相应调整否则维度对不上。3.2 训练循环MSE 损失、梯度裁剪与学习率调度收益预测的损失函数我常用 MSE它会让模型重点拟合大波动样本方向命中率会有一定牺牲。如果要更稳可以换成 Huber Loss对异常值不那么敏感。下面是完整训练循环包含梯度裁剪和早停逻辑。from torch.utils.data import TensorDataset, DataLoader def make_loader(X, y, batch_size128, shuffleFalse): ds TensorDataset(torch.from_numpy(X), torch.from_numpy(y)) return DataLoader(ds, batch_sizebatch_size, shuffleshuffle) def train_model(model, train_loader, valid_loader, epochs60, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, patience5, factor0.5 ) criterion nn.MSELoss() best_valid_loss float(inf) patience_counter 0 best_state None for epoch in range(epochs): model.train() train_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb).squeeze() loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 训练中梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * xb.size(0) model.eval() valid_loss 0.0 with torch.no_grad(): for xb, yb in valid_loader: pred model(xb).squeeze() loss criterion(pred, yb) valid_loss loss.item() * xb.size(0) train_loss / len(train_loader.dataset) valid_loss / len(valid_loader.dataset) scheduler.step(valid_loss) if valid_loss best_valid_loss: best_valid_loss valid_loss best_state {k: v.clone() for k, v in model.state_dict().items()} patience_counter 0 else: patience_counter 1 if patience_counter 10: print(fearly stop at epoch {epoch 1}) break if (epoch 1) % 10 0: print(fepoch {epoch1:3d} | train_loss {train_loss:.6f} | valid_loss {valid_loss:.6f}) model.load_state_dict(best_state) return model逻辑说明ReduceLROnPlateau会在验证损失连续 5 轮不降时把学习率减半这比固定学习率更省心。梯度裁剪是 LSTM 训练的必备操作max_norm1.0表示把梯度的 L2 范数限制在 1.0 以内防止长期依赖计算中梯度爆炸。早停我设在 10 轮也就是验证损失连续 10 轮没有新低就停止训练并回滚到最佳状态。参数说明batch_size128对日频数据来说比较合适样本量几千到几万条都能跑得动。如果数据只有几百条batch_size 建议降到 32否则一个 batch 就代表整个数据集梯度更新会非常不稳定。epochs60配合早停通常够用收益序列信号弱训练太久反而过拟合。3.3 评估指标RMSE 之外必须看方向命中率收益预测系统里RMSE 只能反映预测值和真实值的数值接近程度不能直接反映预测方向是否正确。涨跌方向才是决策者真正关心的事。我习惯在测试集上同时算 RMSE 和方向命中率方向命中率定义为预测收益与实际收益同向乘积大于零的样本占比。def evaluate(model, X_test, y_test): model.eval() with torch.no_grad(): xb torch.from_numpy(X_test) pred model(xb).squeeze().numpy() rmse float(np.sqrt(np.mean((pred - y_test) ** 2))) direction_acc float(np.mean((pred * y_test) 0)) return {rmse: rmse, direction_acc: direction_acc}一个合格的基线方向命中率应该在 52% 到 55% 以上。低于 50% 说明模型不仅没学到东西还在系统性反向预测这时候先别急着调参回头检查数据切分和特征对齐。我经常在模型效果差时先把预测值画出来如果预测序列像一条水平线在零附近震荡说明模型收敛到了一个“预测均值”的解这通常是信号太弱或特征没用对。4. 必调的 5 个超参数序列长度、隐藏单元、学习率、dropout 与 batch size4.1 序列长度金融记忆的长度边界seq_len是收益预测系统里最该先调的参数。日频数据的记忆长度不是越长越好。金融市场的信息衰减很快20 个交易日的窗口约一个月通常已经能覆盖短期动量和反转效应。我试过把seq_len拉到 120结果验证损失显著上升原因是窗口里混入了太多与当前收益无关的噪声LSTM 的隐状态被历史信息稀释。如果数据是小时级或分钟级seq_len要相应放大因为单位时间内的信息量下降了。一个可参考的做法是让窗口长度覆盖 20 到 30 个自然的信息周期日频数据就用 20 到 60小时频数据可以用 120 到 240。调seq_len时要同步重新跑数据准备流程因为样本总量会变。4.2 隐藏单元数与模型容量隐藏单元数决定了 LSTM 的表示能力。64 是我的默认值128 是上限。收益预测的有效样本量其实不大一条股票 20 年日频数据也就 5000 条左右隐藏单元加到 256 以上后训练损失可以压得很低验证损失反而上升这是典型的容量过冲。判断隐藏单元是否过大有个简单方法把训练损失和验证损失曲线画在一起如果训练损失持续下降而验证损失在第 10 轮附近开始反弹说明模型在背训练集。此时优先减小隐藏单元数而不是加 dropout。dropout 只治标减小模型容量才治本。4.3 学习率与批大小训练稳定性的来源Adam 默认学习率 1e-3 在大多数场景可用但收益序列梯度方差大我经常降到 3e-4。判断学习率是否偏高看训练早期 loss 是否出现震荡或 NaN。调学习率的同时要配合批大小批大小越小梯度越震荡需要的学习率越低批大小越大梯度估计越稳定但泛化能力可能下降。我自己的调参顺序是先把seq_len定在 20hidden_size定在 64batch_size定在 128然后单独调学习率看训练曲线的平滑度。确认训练稳定后再回头调seq_len和hidden_size。不要同时调多个参数否则翻车了你都定位不到是谁的问题这是时序预测调参最耗时间的坑。4.4 dropout 和早停收益预测的过拟合防线单层 LSTM 里dropout参数不会生效我一般只在num_layers2时才开 dropout。另一种更常用的做法是在最后的全连接层前加一个 Dropout 层值为 0.2 到 0.3对抑制过拟合更直接。早停是收益预测系统里最有效、也最容易被忽略的正则化手段。上一章代码里已经实现了早停逻辑我建议把patience设在 10 到 15 轮。收益序列噪声大验证损失经常要好几轮才能刷新新低patience太小会把还没训练充分的模型提前掐死。超参数推荐表参数默认值建议范围一句话说明seq_len2020-60日频数据一个月左右的记忆窗口hidden_size6432-128容量太大容易背噪声num_layers11-2低信噪比场景不推荐堆多lr1e-31e-4-3e-3训练不稳就先降到 3e-4batch_size12832-256小数据集用 32大数据用 256dropout0.00-0.3单层时只加在 fc 前early_stop_patience1010-15防止验证损失反复震荡被误杀5. 避坑清单时序收益预测最常见的 5 个翻车点5.1 归一化用了全量统计量模型验证结果虚高现象回测曲线非常漂亮训练集和验证集 RMSE 都很低但换到最新一段数据上预测几乎全错。原因我在早期版本里先对整个X做StandardScaler.fit再切分数据集。这等于用未来区间的均值和方差去归一化训练数据验证集的信息在归一化阶段就泄漏给了模型。解决归一化必须在切分之后做并且scaler.fit只接受训练集输入。测试集和验证集只是调用transform不参与统计量计算。这个坑隐蔽在它不会报错只能靠回测和实盘表现不一致来发现是时序预测里最典型的血泪经验。5.2 shuffle 打乱时序把未来数据喂给了模型现象验证损失比训练损失还低模型表现好得不真实。原因滑动窗口构造的样本之间高度重叠第t个样本和第t1个样本共享 19/20 的输入数据。一旦打开shuffleTrue训练集里混入的大量样本其实和验证集样本只有 1 天之差LSTM 直接记住了这些重叠窗口验证集就形同虚设。解决DataLoader里必须设置shuffleFalse。如果确实想打乱训练顺序只能在每个 epoch 内对训练集样本做一次性 shuffle但不能跨越验证集和测试集的边界。金融时序的滚动切片机制天然限制了交叉验证的用法别拿处理图像数据那套思路套在时序上。5.3 LSTM 背下了噪声训练损失低、方向命中率却只有一半现象train_loss一路降到 0.001 以下但测试集方向命中率停在 50% 附近预测值几乎全部接近 0。原因收益信号在日频尺度上非常弱LSTM 很容易把训练样本里的随机波动背下来。因为训练集收益均值为 0模型学到的“最优策略”是输出一个接近 0 的常数这样 MSE 不会太差。这个解在数学上合理但完全没有预测价值。解决使用方向命中率作为主要评估指标。如果方向命中率在 52% 以下优先减小hidden_size到 32并把seq_len缩短到 10 到 20让模型被迫关注近期最强烈的信号。同时可以把损失函数从 MSE 换成方向损失例如对符号预测错误的大误差样本加倍惩罚但一开始建议先用 MSE 跑通流程。5.4 市场状态漂移模型一段时间后集体失效现象模型在某段区间表现正常过 3 到 6 个月后方向命中率掉到 48% 以下。原因金融时间序列存在概念漂移不同市场状态下收益的生成机制会变。模型是在历史数据上训练的只学对了那一段的规律一旦市场风格切换原来的隐藏状态分布就失效了。解决做滚动重训练而不是一次性训练完就长期使用。我一般每 20 到 30 个交易日用最近一年的数据重训一次模型。频繁重训成本高但收益预测系统的上线周期本来就该以周或月为单位重训是常态不是例外。如果重训后方向命中率持续低迷说明当前状态根本没有可预测性这时候停用模型本身就是正确的决策。5.5 输出层的激活选择tanh 输出导致梯度更新微弱现象模型完全收敛不了损失在某个数值附近卡住不动。原因有些教程在输出层加了tanh因为收益可以看成有界值。但tanh的导数在输出接近 ±1 时趋近 0收益预测的目标值大多集中在 -0.05 到 0.05 之间tanh在这个区域的梯度还算正常可一旦模型输出被压到饱和区梯度几乎消失训练就停止了。解决输出层用纯线性层不接任何激活函数。把收益放缩到 [-1, 1] 再训练的做法也可以但必须同步调整损失函数和反归一化逻辑多此一举。线性输出配合 MSE 是最稳定的组合这一点不值得为了趋势向新颖折腾。6. 进阶滚动预测、方向命中率与更复杂的时序模型6.1 多步滚动预测的快速实现单步预测只能给下一日收益实际决策往往需要未来 5 到 10 天的路径。多步预测的常用做法是滚动预测每次用模型输出更新窗口的最末行再作为下一轮的输入。def multi_step_forecast(model, last_window, steps, ret_feature_index0): model.eval() window last_window.copy() preds [] for _ in range(steps): x_t torch.from_numpy(window.astype(np.float32)).unsqueeze(0) with torch.no_grad(): pred model(x_t).item() preds.append(pred) # 把预测收益填入窗口最后一行的收益特征位 next_row window[-1].copy() next_row[ret_feature_index] pred window np.vstack([window[1:], next_row]) return np.array(preds)注意这个实现里只更新了收益特征波动率和成交量变化特征没有重算数值会有偏差但作为趋势预览足够。想更严谨可以在每次滚动时用最新窗口里的预测收益重新计算滚动波动率再把全部特征重排成一行填进去。6.2 用方向命中率做决策依据我最后留下一个习惯任何模型测试结果先看方向命中率的分段表现再看总体均值。把测试集按时间切成 4 段分别统计每段的方向命中率如果有一段显著高于其他段而其余段落接近 50%说明模型只在特定市况下有效这样的模型不能全时段使用必须加状态过滤。方向命中率稳定在 53% 以上时配合仓位控制才能产生正期望。这个门槛听上去很低但长期稳定超过 53% 的时序模型已经很难得了别贪。如果后续想继续提升可以去看 PyTorch 的nn.LSTM源码理解h_t和c_t在时间步间如何流动这是换到更深模型的基础。再往后可以尝试在最后一个时间步后面加一层时序注意力机制让模型对窗口内不同日期的信息做加权求和这比直接堆 LSTM 层数更有效。Transformer 时序预测模型在长序列上有优势但在日频收益这种短窗口、强噪声场景LSTM 的参数量和训练稳定性反而更友好。我的做法是先把 LSTM 基线跑扎实再逐步加复杂度每一步都对比方向命中率有没有真实提升。希望这套流程和踩坑记录能帮你在收益预测这个方向上少走几次弯路。本文还有配套的精品资源点击获取
返回列表