ARTICLE DETAIL

资讯详情

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

相关性分析+CNN-Attention-LSTM期货价格预测模型复现指南

相关性分析+CNN-Attention-LSTM期货价格预测模型复现指南 简介这是一份面向深度学习和量化交易初学者的期货价格预测完整项目基于相关性分析的CNN-Attention-LSTM组合模型适合毕业设计、课程设计及实战练习。压缩包共29个文件、约30.29MB涵盖8个Python脚本模型构建、预测、相关性分析与API服务、训练好的TensorFlow模型权重、NumPy处理好的特征数组、Excel数据表、SQL原始数据以及两份PDF使用教程和README说明结构与注释清晰便于逐模块复现。内容包含数据预处理、时间步处理、相关性热力图分析等关键环节并提供checkpoint可直接加载运行避免重复训练。目前已有809人学习下载对有LSTM/注意力机制基础、希望快速搭建价格预测Pipeline的读者尤其实用。1. 相关性分析 CNN-Attention-LSTM这套源码值不值得复现先看它解决了什么问题拿到「基于相关性分析的CNN-Attention-LSTM期货价格预测模型python源码数据集模型详细注释.zip」这类资源很多人第一反应是先解压跑起来再一头扎进网络结构里调参数。但做过期货量价预测的人多半有这种体验真正卡住你的往往不是模型不够深而是输入特征里混着一堆跟预测目标不相关甚至互相干扰的因子。这套方案把相关性分析放在最前面先用统计方法把几十个候选特征收敛到十来个真正有用的再用 CNN-Attention-LSTM 在这一小撮特征上建模相当于先把搜索空间缩小再让网络去学时序规律。对正在做量化研究、时序预测课程设计或者想把手头 LSTM 预测效果往上提一档的人来说这条技术链路是完整且可参照的。下面我按复现这套源码的推进顺序从相关性分析讲到模型结构再到训练、排坑和验证。2. 相关性分析先行用数据筛选缩短模型输入的搜索空间2.1 为什么先用相关性分析期货因子太多模型学不过来拿一份期货日线数据来看候选特征动不动就是几十个开盘价、最高价、最低价、收盘价、成交量、持仓量再加上 RSI、MACD、布林带宽度这些衍生指标。把这些全部塞进网络问题不是训练慢而是模型会把大量参数浪费在无关噪声上验证集指标越调越差。相关性分析在这里的最大价值是趁早砍掉那些和目标变量几乎无关的列给后续的 CNN-Attention-LSTM 留下一个干净的输入。我一般会把「下一周期收益率」作为预测目标逐个计算每个特征与它的相关系数。这里的相关系数分两种Pearson 相关系数衡量线性相关性Spearman 相关系数衡量单调相关性。期货数据里经常出现涨跌停、成交量突变这类极端值Pearson 对它们非常敏感一个异常点就能把相关系数从 0.3 拉到 0.1Spearman 用的是排名信息扛极端值能力明显更强。所以我通常两个都算一遍重点看 Spearman 的结果。import pandas as pd from scipy.stats import pearsonr, spearmanr # raw 是已经按日期升序排列、做过缺失值处理的 DataFrame # feature_cols 手动指定避免把日期、合约代码这类非数值列混进来 feature_cols [open, high, low, close, volume, open_interest, rsi_14, macd_dif, macd_dea, boll_width] target_col ret_next X raw[feature_cols].copy() y raw[target_col].copy() result [] for c in feature_cols: # 个别常数列会导致 spearmanr 返回 nan这里要过滤掉 p_rho, p_pval pearsonr(X[c], y) s_rho, s_pval spearmanr(X[c], y) result.append({ feature: c, pearson: round(p_rho, 4), spearman: round(s_rho, 4), pearson_p: p_pval, spearman_p: s_pval }) feat_corr pd.DataFrame(result).sort_values(spearman, keyabs, ascendingFalse) print(feat_corr)这段代码的核心是同时输出两个相关系数和对应的 p 值。p 值的作用是判断相关性是否显著当样本量几千条时0.05 的相关系数也可能 p 值很小但业务上基本没意义所以 p 值只能用来剔明显不可靠的结果最终选特征还是得看相关系数的大小和方向是否符合业务直觉。2.2 Pearson 与 Spearman两种相关系数的适用场景与计算代码很多人在相关性分析这一步习惯直接用df.corr()拿出来的默认是 Pearson。这在数据干净、线性关系明显时没问题但期货价格序列不是这样不同品种的波动结构差异很大有的品种存在明显的跳空和涨跌停限制产生大量极端值Pearson 一下子就被带偏了。Spearman 不关心具体数值只关心排名所以它更适合期货数据这种带肥尾、非平稳特征的场景。对比项PearsonSpearman衡量关系线性相关单调相关不要求线性对极端值极度敏感不敏感适用数据连续、近似正态有序、偏态、带异常值期货数据表现容易失真相对稳健代码里feat_corr.sort_values(spearman, keyabs, ascendingFalse)是按 Spearman 系数的绝对值排序因为正负号只代表方向建模时我们需要的是「相关强度」。这一步结束后你会得到一张候选特征的排序表但先别急着定下来我后面还会做一步人工复核。2.3 特征筛选的落地阈值热力图、相关系数矩阵与人工复核通常的做法是把 |Spearman 相关系数| 大于 0.15 的特征挑进候选池同时检查候选特征两两之间的相关系数如果 |r| 0.8 就只保留其中一个。比如 close 和 high 的相关系数经常高达 0.95两个都进模型只会放大共线性让网络学到重复信号。把阈值定为 0.15 而不是 0.3是因为单因子的相关性本来就弱期货收益率能被单个因子解释的部分非常有限0.15 已经算有信号的候选了。import matplotlib.pyplot as plt import seaborn as sns selected feat_corr[feat_corr[spearman].abs() 0.15][feature].tolist() sub X[selected].copy() # 检查特征之间的共线性避免高度重复的信号一起进模型 corr_matrix sub.corr(methodspearman) plt.figure(figsize(10, 8)) sns.heatmap(corr_matrix, annotTrue, fmt.2f, cmapcoolwarm, center0) plt.title(Spearman Correlations among Selected Features) plt.savefig(feature_corr_selected.png, dpi120, bbox_inchestight)加人工复核这步是我自己的习惯。相关系数只告诉你统计关联不告诉你因果所以我会把选出来的特征逐个对照业务逻辑检查一遍比如「成交量放大通常伴随波动率抬升」是合理的但「某两个指标符号相反且相关系数极高」就很可疑可能是数据对齐出了问题。这类问题靠代码查不出来只能靠人工看。如果你发现某个高相关特征背后的业务逻辑说不通宁可丢掉也别留着。3. 把序列喂给 CNN-Attention-LSTM网络结构拆解与 torch 实现3.1 输入张量怎么组织样本、时间步、特征的维度设计相关性分析给出的是横截面特征而 LSTM 需要的是带时间维度的序列。常见做法是取过去 N20 个交易日的数据预测未来 1 个交易日的收益率方向N 取 20 约等于一个月的交易日数量对月度级别的交易逻辑比较贴合。输入张量的形状是[batch_size, seq_len, fea_dim]其中seq_len20fea_dim相关性分析筛选后的特征数。import numpy as np def build_windows(data, seq_len20, pred_step1): data: 按时间升序排列的 DataFrame最后一列是标签 ret_next 返回 X: [样本数, seq_len, fea_dim]y: [样本数] X, y [], [] fea_dim data.shape[1] - 1 # 去掉标签列 for i in range(len(data) - seq_len - pred_step 1): X.append(data.iloc[i : i seq_len, :fea_dim].values) y.append(data.iloc[i seq_len pred_step - 1, -1]) return np.array(X), np.array(y)这里有两个细节要注意一是data必须已经按日期升序排列不能在切窗口前打乱顺序否则窗口内部的时间先后关系就乱了二是pred_step1表示预测窗口结束后下一个交易日的标签标签是第i seq_len天的ret_next也就是用第 i 天到第 i19 天的数据预测第 i20 天的收益率。如果未来函数混进来测试集指标会虚高这一点在第 5 章会专门展开。3.2 CNN 层做局部特征提取一维卷积参数与感受野CNN 在这个模型里不是用来做图像识别的而是用一维卷积在时间维上滑动提取相邻交易日之间的局部模式。kernel_size3时一个卷积窗口覆盖连续 3 个交易日天然对应「近三天量价形态」这类短期特征。之所以把 CNN 放在最前面是因为 LSTM 对局部模式的敏感度不如卷积网络先让 CNN 把细粒度特征抽出来LSTM 再处理起来更容易。import torch.nn as nn class ConvBlock(nn.Module): def __init__(self, fea_dim, conv_filters64, kernel3, dropout0.3): super().__init__() # 输入通道是特征数输出通道是卷积核数量 self.conv nn.Conv1d(fea_dim, conv_filters, kernel_sizekernel, paddingkernel // 2) self.bn nn.BatchNorm1d(conv_filters) self.relu nn.ReLU() self.dropout nn.Dropout(dropout) def forward(self, x): # x 形状是 [batch, seq_len, fea_dim]Conv1d 期望 [batch, channels, length] x x.permute(0, 2, 1) x self.conv(x) # 输出 [batch, conv_filters, seq_len]原始 seq_len 被 padding 保持住了 return self.dropout(self.relu(self.bn(x)))paddingkernel // 2是为了让卷积输出时间长度保持不变这样后面接 LSTM 时不需要调整序列长度。BatchNorm1d在训练时对每个卷积核的输出做标准化让梯度传播更稳定dropout0.3 用来抑制过拟合期货数据本身噪声大这个值不建议低于 0.2。3.3 Attention 层怎么定位关键时间步打分函数与权重可视化LSTM 会把整个序列压缩成最后一个隐状态但期货市场里并不是每个过去时刻都一样重要。典型场景是跳空高开后的第一根K线可能对后续走势有决定性影响而一段平淡的横盘对预测几乎没有帮助。Attention 的作用就是对 LSTM 每个时间步的输出都打一个分通过 softmax 归一化成权重再把所有时间步的隐状态按权重加权求和得到一个带重点的上下文向量。import torch import torch.nn.functional as F class AttentionBlock(nn.Module): def __init__(self, hidden_dim): super().__init__() # 加性注意力先经过一个可学习的线性变换再打分 self.W nn.Linear(hidden_dim, hidden_dim, biasFalse) def forward(self, lstm_out): # lstm_out: [batch, seq_len, hidden_dim] scores self.W(torch.tanh(lstm_out)) # [batch, seq_len, hidden_dim] scores scores.mean(dim2) # 聚合到每个时间步一个分数 weights F.softmax(scores, dim1) # [batch, seq_len] # bmm: 权重 [batch,1,seq_len] 与输出 [batch,seq_len,hidden_dim] 加权求和 context torch.bmm(weights.unsqueeze(1), lstm_out).squeeze(1) return context, weights这里用的是加性注意力也就是对隐状态做一次非线性变换后再打分比直接点积更灵活。scores.mean(dim2)把每个时间步在 hidden_dim 方向上的分数平均成一个标量这是为了能对时间步做 softmax。weights就是后面可视化要用的注意力权重它告诉你模型把注意力放在了过去哪些天。如果你发现所有时间步的权重都差不多说明 Attention 没学到有效信息这个问题的排查方法放到第 5 章。3.4 LSTM 层负责时序记忆hidden_size、num_layers 与 dropout整个模型的标准组合是 CNN 抽取局部特征、LSTM 建模时序依赖、Attention 对时间步重新加权最后接全连接层输出单步预测。LSTM 这层比较关键的是 hidden_size 和 num_layershidden_size64 对单品种日线数据已经够用调大到 128 不一定会变好反而更容易在训练集上过拟合num_layers2 可以让模型捕捉更长时间跨度的依赖但超过 2 层在几千条样本量的期货数据上几乎必然过拟合。class CNNAttentionLSTM(nn.Module): def __init__(self, fea_dim, hidden_size64, num_layers2, conv_filters64, kernel3, dropout0.3): super().__init__() self.conv_block ConvBlock(fea_dim, conv_filters, kernel, dropout) # input_size 是 CNN 输出通道数不是原始特征数 self.lstm nn.LSTM(input_sizeconv_filters, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0) self.attention AttentionBlock(hidden_size) self.fc nn.Linear(hidden_size, 1) def forward(self, x): x self.conv_block(x) # [batch, conv_filters, seq_len] x x.permute(0, 2, 1) # [batch, seq_len, conv_filters] lstm_out, _ self.lstm(x) # [batch, seq_len, hidden_size] context, weights self.attention(lstm_out) out self.fc(context) # [batch, 1] return out, weightsinput_sizeconv_filters这一步比较容易踩坑因为 LSTM 接收的不是原始特征而是 CNN 的输出通道数。nn.LSTM的 dropout 参数只在num_layers 1时才会生效官方文档也这么要求所以代码里加了条件判断。输出层只有一个神经元对应下一个交易日的收益率预测值正负。整个模型在forward里把weights一并返回方便训练完后拿出来分析。4. 数据集构建与训练配置从原始数据到可复现的训练脚本4.1 数据切分与归一化避免未来函数和归一化泄漏时序预测和图像分类最大的区别是数据不能随机洗牌。预测模型的训练集、验证集、测试集必须按时间顺序切分训练集在前 70%验证集占中间 15%测试集占最后 15%。如果按随机切分模型的验证集指标会虚高换到真实场景立刻现原形这是我见过最多人翻车的细节之一。归一化的坑也出在这里。很多源码图省事把整个数据集丢进MinMaxScaler一次性 fit这会把测试集的取值范围和分布提前暴露给训练过程属于典型的未来函数。正确做法是先对训练集 fit再用同一个 scaler 去 transform 验证集和测试集。from sklearn.preprocessing import MinMaxScaler train_size int(len(df) * 0.7) val_size int(len(df) * 0.15) train_df df.iloc[:train_size].copy() val_df df.iloc[train_size : train_size val_size].copy() test_df df.iloc[train_size val_size :].copy() scaler MinMaxScaler(feature_range(0, 1)) # 只对训练集 fit避免统计量泄漏到 val/test train_scaled scaler.fit_transform(train_df) val_scaled scaler.transform(val_df) test_scaled scaler.transform(test_df)注意如果测试集里出现训练集未曾见过的极端值归一化后可能落在 [0,1] 区间之外这是正常现象不要为了让数值落在区间内而重新混合数据再 fit。4.2 模型训练的最小脚本损失函数、优化器与早停损失函数用 MSE 回归损失优化器用 Adam初始学习率 1e-3这两项是这套模型最常见的组合。额外加weight_decay做 L2 正则加ReduceLROnPlateau在验证集 loss 不再下降时自动减半学习率。早停的 patience 我一般设 10因为期货数据噪声大验证集 loss 会有天然起伏patience 太小很容易在短暂回落后误判。import copy import torch.optim as optim model CNNAttentionLSTM(fea_dimfea_dim) optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) criterion nn.MSELoss() scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5) best_loss float(inf) best_weights copy.deepcopy(model.state_dict()) patience_counter 0 for epoch in range(60): model.train() for Xb, yb in train_loader: optimizer.zero_grad() pred, _ model(Xb) loss criterion(pred.view(-1), yb) loss.backward() # 梯度裁剪LSTM 训练常见的梯度爆炸防护手段 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() model.eval() val_loss 0.0 with torch.no_grad(): for Xb, yb in val_loader: pred, _ model(Xb) val_loss criterion(pred.view(-1), yb).item() scheduler.step(val_loss) if val_loss best_loss: best_loss val_loss best_weights copy.deepcopy(model.state_dict()) patience_counter 0 else: patience_counter 1 if patience_counter 10: breakclip_grad_norm_会把梯度的整体范数限制在 1.0 以内LSTM 在长序列上训练时经常出现梯度爆炸这行代码能省掉很多后续排错时间。best_weights保存的是验证集最优时刻的模型状态而不是训练结束时那一轮的权重这是训练循环里必须处理的细节否则早停触发时模型权重可能已经退化。训练参数我一般按下面这个范围设置也是这套源码的默认参数参数推荐值说明seq_len20约一个月的交易日数量conv_filters64卷积核数量调大增加模型容量kernel3卷积窗口覆盖 3 个交易日hidden_size64LSTM 隐状态维度num_layers2超过 2 层容易过拟合dropout0.3抑制过拟合lr1e-3配合 ReduceLROnPlateaubatch_size64数据量大时可提到 128patience10验证集早停阈值4.3 预测效果怎么衡量MAE / RMSE / 方向准确率模型收敛后要在测试集上算三个指标而不是只看 loss。MAE 和 RMSE 衡量预测值与真实值的误差大小方向准确率则更贴近期货交易的实际需求预测涨跌方向对不对。一个 RMSE 很小的模型若方向准确率只有 0.5等于没有任何预测能力反过来方向准确率到 0.55 以上即使 RMSE 略大也有实际参考价值。from sklearn.metrics import mean_absolute_error, mean_squared_error test_pred, _ model(test_X_tensor) test_pred test_pred.view(-1).detach().numpy() y_true test_y mae mean_absolute_error(y_true, test_pred) rmse np.sqrt(mean_squared_error(y_true, test_pred)) direction_acc np.mean(np.sign(test_pred) np.sign(y_true)) print(fMAE{mae:.4f} RMSE{rmse:.4f} direction_acc{direction_acc:.4f})在单因子层面上方向准确率达到 0.52 到 0.58 已经算是有效信号。超过 0.6 反而要警惕先检查是不是数据泄漏而不是高兴得太早。这是时序预测里最需要养成的条件反射。5. 避坑与排查期货时序预测最容易翻车的 5 个细节5.1 回测很好、换段数据就翻车现象测试集上方向准确率 0.6看起来很不错把同一套代码换到另一段行情数据上结果直接掉到 0.5 附近等于抛硬币。原因绝大多数情况是数据泄漏。要么数据切分时没有按时间顺序用了随机洗牌要么归一化时把整个数据集一起 fit 了测试集的统计信息提前进入了训练过程。另一种常见情况是数据里包含了未来信息比如用当天的收盘价去预测当天的涨跌方向标签和特征重叠。解决先检查切分逻辑确认训练、验证、测试严格按时间先后划分再看归一化是否只对训练集 fit最后检查标签是否错位。我遇到这种情况时会在训练前用df.index和df[date]打印一遍顺序确认没有在数据预处理阶段被意外打乱。5.2 相关性高的因子加进模型反而掉点现象辛辛苦苦算出来某个因子与预测目标的相关性有 0.5加进模型后验证集 loss 反而变差去掉后指标又回来了。原因时间对齐错位。直接用df[factor].shift(1)时如果 DataFrame 的索引不是严格的日期索引而是经过多重索引或者拼接后残留了重复索引shift 会对错位置。比如跨合约拼接的数据前一行的最后一天和后一行的第一天会被错误地当成相邻日期处理。解决统一日期索引、排序后再 shiftshift 完检查非空值数量是否和预期一致。两种行情拼接时我会按groupby(contract)分别做 shift 再合并避免边界处的错位。5.3 训练 loss 降得很快但验证集剧烈震荡现象训练集 loss 一路下降验证集 loss 像锯齿一样上下跳动甚至在某些轮次突然飙升。原因学习率太大模型在验证集的最佳点附近来回震荡跳过了最优点或者 patience 设得太小模型还没走到真正的谷底就被早停机制掐断。解决初始学习率降到 1e-3 并配ReduceLROnPlateau让学习率在验证集 plateau 时自动减半。patience 设到 10。判断模型是否稳定不要盯着单轮的验证集 loss看连续 10 轮的滚动均值。5.4 注意力权重几乎平均分布Attention 形同虚设现象把训练好的模型拿到样本上跑一遍画出来的注意力权重所有时间步都差不多没有明显的峰值去掉 Attention 层模型表现几乎没有变化。原因一是输入特征尺度差异太大某个特征数值在几千量级其他特征在 0 到 1 之间打分函数被大数值特征主导softmax 输出趋于均匀分布二是 hidden_size 太小隐状态携带的信息不足以区分不同时间步的重要性三是打分函数里的 tanh 在输入过大时进入饱和区梯度消失。解决先确保归一化严格做在模型输入之前再看看 hidden_size 是不是小于 32太小就往上调到 64最后试着把打分函数从加性注意力换成点积注意力或对 scores 除以温度系数。这步排查做完权重分布通常会有明显改善。5.5 换了台机器重训结果完全对不上现象同一份数据和同一个脚本换台电脑重新训练方向准确率从 0.56 变成 0.51校验集 loss 也差很多。原因默认情况下 PyTorch 的随机种子每次启动都不一样模型参数初始化和 dropout 的随机行为都会影响结果。LSTM 内部还有随机的状态初始化加剧了不稳定性。解决在训练脚本最前面固定所有能固定的随机源包括 Python 内置的 random、NumPy 和 PyTorch 的随机种子同时把torch.backends.cudnn.deterministic打开。还要把 seed 值记录在模型文件名里比如model_seed42.pt这样每次对比实验都能确保同一起点。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False固定随机种子是复现工作的底线。如果你在多台机器上训练同一个模型建议把 seed 也作为参数保存下来否则后期排查问题时分不清是代码改动还是随机性造成的结果差异。6. 用注意力权重校验模型一张图看模型在用什么信息模型训练完后注意力权重是最值得先画出来的东西。它直接告诉你模型在做预测时把注意力放在了过去 20 个交易日里的哪几天。这个信息比 loss 曲线更接近模型的实际思考过程我每训练完一个模型都会固定走一遍这个验证流程。def plot_attention_weights(model, X_sample, save_pathattention_weights.png): model.eval() with torch.no_grad(): _, weights model(X_sample) # 多个样本求平均避免单一样本的偶然性 avg_w weights.mean(dim0).numpy() plt.figure(figsize(8, 3)) plt.bar(range(len(avg_w)), avg_w, colorsteelblue) plt.xlabel(time step (0 最远)) plt.ylabel(attention weight) plt.title(Average Attention over Time Steps) plt.tight_layout() plt.savefig(save_path, dpi120)拿一批测试集样本的平均权重来看如果权重明显集中在最近 3 到 5 个交易日说明模型倾向于用近期走势做判断这和期货市场「近因效应」的业务直觉一致如果权重集中在某个特定日期附近比如跳空或放量那天说明模型确实抓到了事件型信号。反过来如果权重几乎是均匀分布Attention 就是摆设模型等同于普通的 CNN-LSTM这时要么调整模型结构要么检查特征预处理。我自己的习惯是把注意力权重那一列的 date 信息打印出来和当天的成交量、涨跌幅对照着看。有一次我发现模型把接近 40% 的注意力给了一个成交量突增的日子而这个日子对应的因子在相关性分析里系数很高两个信号相互印证模型的行为才真正可信。如果你画出来的权重分布和相关性分析的主要信号对不上别急着调网络结构先回头检查数据对齐。后来我也养成了一个固定习惯每次换数据集先跑一遍相关性分析再固定同一个随机种子训练三遍取指标中位数作为最终结果。这个习惯帮我少走了很多弯路也让模型的每一次改动都能被准确评估希望帮到你。本文还有配套的精品资源点击获取
返回列表