简介:基于相关性分析的CNN-Attention-LSTM期货价格预测模型是一套完整可运行的深度学习项目,面向金融量化初学者、高校毕业设计与课程设计开发者,帮助解决期货价格序列特征提取不足、长程依赖捕捉困难等问题。资源共29个文件,压缩包大小30.29MB,包含Python源码、数据集、训练好的模型权重、Excel数据表与SQL数据库文件、使用教程PDF等,代码中已按照相关性分析、时间步处理、模型定义、训练与预测等模块拆分,并配有详细注释。目前已有808人学习下载。读者可获得从数据预处理到模型训练、评估和接口调用的全链路实现,包括CNN-Attention-LSTM核心模型、checkpoint权重文件、预测脚本以及配置与使用说明,方便快速复现实验、改造自身任务或作为答辩展示素材。
1. 用相关性分析给CNN-Attention-LSTM期货预测模型喂数据,比直接堆特征更值钱
期货价格预测的难点从来不在模型,而在数据。多因子策略里的候选特征动辄几十上百个:不同周期的动量、持仓量变化、跨品种价差、宏观指标,全塞进神经网络里,最先被淹没的反而是真正有预测力的信号。相关性分析在这个链路里是价值被严重低估的第一步——它不是为了删掉“跟价格不相关”的变量,而是为了去掉“彼此高度相关”的冗余变量,让CNN-Attention-LSTM模型的每个输入维度都携带独立信息。否则模型会把算力浪费在重复学习上,注意力权重也会被一组同源特征带偏。这篇内容从特征筛选开始,把相关性分析的落地写法、CNN-Attention-LSTM各层的源码实现、训练时的参数设置和坑,以及最终怎么把预测输出转换为可用的交易信号串起来讲一遍。
2. 相关性分析落地:从候选因子里筛出期货价格预测真正需要的特征
2.1 为什么选Spearman秩相关而不是Pearson
做特征筛选,第一步是选对相关性度量。Pearson相关系数假设变量之间是线性关系,但期货市场里的量价特征几乎都是非线性、带尖峰厚尾的分布,直接用Pearson会严重低估变量间的关联强度。比如持仓量在价格快速拉升时可能呈指数级放大,两者之间的Pearson系数可能只有0.2,但秩相关却能捕捉到这种单调递增的关系,算出来可能是0.7。
另一个更实际的理由是,期货数据的“脏”更多体现在量纲差异和极端值上。不同品种的合约乘数不同,价格区间差异巨大,Pearson对异常值极其敏感,一个数据错误就可能把相关性从0.6拉到-0.3。Spearman先把两组变量分别排序,再对排序后的秩次做Pearson计算,把极端值的影响压缩到最小。所以在做CTA策略或价格预测的特征筛选时,Spearman是这个场景下更稳健的默认选择。
#### 2.1.1 Pandas里一行算完Spearman系数 ```python import pandas as pd # df: 包含价格、各类候选因子的DataFrame,索引为时间戳 corr_matrix = df.corr(method='spearman') # 只看各因子与未来N周期收益率的相关性 target = 'return_5d' factor_cols = [c for c in df.columns if c != target] sorted_corr = corr_matrix[target].drop(target).abs().sort_values(ascending=False) print(sorted_corr.head(20))df.corr(method='spearman')返回的是一个完整的相关系数矩阵,取数理逻辑上只依赖factor_cols这一组候选特征。.drop(target)把目标变量自身那行去掉,.abs()取绝对值是因为负相关跟正相关同样有预测力,比如“持仓量下降”和“未来价格上涨”如果稳定呈现负相关,这个因子照样有效。.sort_values(ascending=False)把强相关因子排到最前面。
2.2 用Python做相关性筛选的完整过程
只按与目标变量的相关性排序还不够,还需要再走一步去冗余。假设两个候选因子A和B与目标变量的相关系数分别是0.65和0.62,但A和B之间的相关系数是0.93,说明它们本质上在描述同一种市场状态,保留一个就够了。
常见做法是按下面这个流程做递进筛选。先删除与目标变量相关系数绝对值低于0.1的因子,这些是纯粹的噪音;然后按与目标的相关性从高到低排序,依次纳入特征集,每新纳入一个特征时,检查它与特征集内已有特征的最大相关系数,如果超过0.7就跳过。这个阈值不是死规矩,特征总量少可以放宽到0.8,特征特别多就要收紧到0.6。
def select_factors(df, target, corr_threshold=0.1, dedup_threshold=0.7): corr = df.corr(method='spearman')[target].drop(target) # 第一轮:删掉与目标相关性过低的因子 candidates = corr[corr.abs() >= corr_threshold].sort_values(key=lambda x: x.abs(), ascending=False) selected = [] for factor in candidates.index: if len(selected) == 0: selected.append(factor) continue max_corr_with_selected = df[selected].corrwith(df[factor], method='spearman').abs().max() if max_corr_with_selected < dedup_threshold: selected.append(factor) return selectedcorrwith在这里是关键,它逐个计算候选因子与已入选特征之间的相关性,取最大值后跟dedup_threshold比较。循环结束后selected里就是既与预测目标有较强关联、彼此又不高度冗余的特征集。这个函数可以直接嵌入特征工程的pipeline里,每次训练前自动跑一遍,不用手动维护特征名单。
2.3 相关性矩阵的可视化与阈值判定
除了数值筛选,热力图是发现异常依赖关系的最直观工具。seaborn.clustermap比普通heatmap更好用,因为它会做层次聚类,把相关性接近的特征自动排列到一起,一眼就能看到成组的冗余特征块。
#### 2.3.1 画热力图把冗余特征块找出来 ```python import seaborn as sns import matplotlib.pyplot as plt selected_corr = df[selected + [target]].corr(method='spearman') sns.clustermap(selected_corr, annot=True, fmt='.2f', cmap='RdBu_r', center=0, figsize=(12, 10)) plt.show()annot=True会在每个格子里显示具体数值,否则图缩小时颜色深浅很难精确判断。center=0让红蓝配色以0为分界,正相关和负相关一眼可辨。如果热力图上出现两个因子在所有其他特征上的颜色行几乎一致,说明它们在数据层面已经接近重复表达,即使相关性系数没过阈值,也应该只保留其中一个——阈值是死的,图形里能看出阈值捕捉不到的潜在共线性。
提示:特征筛选结果要定期重跑。期货市场存在明显的状态切换,一个在趋势行情里有预测力的因子,进入震荡行情后可能完全失效。建议每两周重新跑一次2.2节里的筛选函数,观察入选特征是否发生漂移。
3. CNN-Attention-LSTM模型结构:局部特征、权重聚焦与时序记忆的配合
3.1 CNN层在期货数据上提取的是什么
CNN在序列预测里处理的是形状为(batch, seq_len, num_features)的三维张量。跟处理图像不同,这里的一维卷积是在时间维上滑动的,目的是提取相邻时间步之间的局部模式。比如连续三根K线的量价配合形态——放量突破后缩量回调——这种模式在原始数据里是分散的,但经过卷积核的滑动窗口扫描后,会被压缩成一组局部特征图。
通常第一个卷积层用较宽的卷积核捕捉稍长一点的局部模式,比如kernel_size=5,它覆盖过去5个时间步的信息;第二层再用kernel_size=3在粗粒度特征上做细提取。这样做的收益是:LSTM后面接到的不是高维噪音,而是已经过压缩和提纯的局部特征,计算量下降的同时预测效果反而更好。
#### 3.1.1 一维卷积与池化的PyTorch实现 ```python import torch.nn as nn class CNNBlock(nn.Module): def __init__(self, n_features, seq_len, num_filters=64): super().__init__() self.conv1 = nn.Conv1d(in_channels=n_features, out_channels=num_filters, kernel_size=5, padding=2) self.conv2 = nn.Conv1d(in_channels=num_filters, out_channels=num_filters * 2, kernel_size=3, padding=1) self.relu = nn.ReLU() self.pool = nn.MaxPool1d(kernel_size=2) def forward(self, x): # x: (batch, seq_len, n_features) x = x.permute(0, 2, 1) # Conv1d要求通道维在第1维 x = self.relu(self.conv1(x)) x = self.relu(self.conv2(x)) x = self.pool(x) # 时间维减半 x = x.permute(0, 2, 1) # 换回 (batch, seq_len', channels) return xpermute在卷积前后各出现一次,是因为nn.Conv1d的输入格式是(batch, channels, length),而时序建模习惯用(batch, seq_len, features)。padding=2配上kernel_size=5保证卷积后时间维长度不变,padding=1配上kernel_size=3同理;MaxPool1d(kernel_size=2)把时间维压缩一半,这一步直接降低了后续Attention和LSTM的计算量。
3.2 Attention层解决的是LSTM的遗忘问题
LSTM对长序列的记忆能力是有上限的。虽然门控机制比RNN强得多,但序列长度超过50个时间步后,早期的关键信息仍然会逐渐被稀释。Attention机制完全绕开了“靠隐状态一路传递”的思路,它让模型在每一步解码时,都能直接回看编码器所有时刻的输出,用打分函数学习“当前时刻应该重点关注哪个历史时刻的信息”。
#### 3.2.1 可学习的加性注意力源码 ```python class AttentionLayer(nn.Module): def __init__(self, hidden_size): super().__init__() self.score_linear = nn.Linear(hidden_size, hidden_size) def forward(self, lstm_outputs): # lstm_outputs: (batch, seq_len, hidden_size) scores = self.score_linear(lstm_outputs) # 对每个时刻做一个线性变换 weights = torch.softmax(scores, dim=1) # 在时间维上做归一化 context = torch.sum(weights * lstm_outputs, dim=1) # 加权求和 return context, weightsscore_linear是一个可学习的线性层,等价于注意力打分函数。softmax沿时间维做归一化,保证所有时刻的注意力权重之和为1。weights * lstm_outputs是逐元素相乘,再用torch.sum沿时间维压缩,得到携带全局上下文信息的向量。这个context向量会拼接到解码器的输入上,让预测同时看到局部特征、时序记忆和全局重点信息。
3.3 LSTM层与完整的CNN-Attention-LSTM模型封装
LSTM在这个架构里不是主力特征提取器,而是把CNN提好的局部特征在时间维度上串起来。hidden_size=64是一个性价比比较高的设置:太小记忆容量不够,再增大收益递减。LSTM的方向有两种选择,bidirectional=True可以让每个时刻同时看到历史和未来信息,适合做预测;如果将来要改成滚动实时预测,建议用单向,因为实时场景拿不到未来数据。
#### 3.3.1 完整模型源码:CNN-Attention-LSTM整体封装 ```python class CNNAttentionLSTM(nn.Module): def __init__(self, n_features, seq_len, hidden_size=64, num_layers=2, num_filters=64): super().__init__() self.cnn = CNNBlock(n_features, seq_len, num_filters) self.lstm = nn.LSTM(input_size=num_filters * 2, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True) self.attention = AttentionLayer(hidden_size * 2) # 双向LSTM输出维度翻倍 self.fc = nn.Linear(hidden_size * 2, 1) # 输出预测价格或收益率 def forward(self, x): x = self.cnn(x) lstm_out, _ = self.lstm(x) context, _ = self.attention(lstm_out) out = self.fc(context) return out.squeeze(-1)bidirectional=True时LSTM每个时间步的输出是正反两个方向隐状态的拼接,维度是hidden_size * 2,所以后面两个全连接层的输入维度必须同步调整。num_layers=2表示堆叠两层LSTM,第二层拿第一层的全部时刻的输出作为自己的输入序列。batch_first=True让输入形状为(batch, seq_len, features),与前面CNN模块的输出格式保持一致。
3.4 每次序列长度设定的依据
seq_len是该模型最重要的超参数之一,它决定模型看多长的历史窗口。期货市场日内交易通常用30到60个时间步,日线级别一般用20到40天。比这个更长并不会持续提升效果,因为过远的历史行情对当前走势的边际贡献递减,反而会把注意力权重摊薄。做特征筛选时使用的相关分析时间窗口,应该与这个seq_len保持同一量级,避免用日线级别的相关关系去指导分钟级模型的输入长度。
4. 训练与评估:把模型跑在真实期货数据上的关键设置
4.1 数据集切分与归一化处理
时序数据的切分跟普通分类任务完全不同,随机打乱是绝对禁止的,因为那样会让模型“偷看”未来数据,训练出的指标毫无意义。正确做法是按时间顺序切分为训练、验证、测试三段,比例常见为7:1.5:1.5,并且要特别注意测试集要选最近一段时间的行情,尽可能贴近真实的使用场景。
归一化也必须基于训练集的统计量。要用fit_transform在训练集上算出均值和标准差,然后在验证集和测试集上只用transform,不能混用。常见错误是把同一组统计量放到全部数据上计算后再切分,这等于在归一化环节引入了未来信息,会高估模型表现。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() train_x = scaler.fit_transform(train_x.reshape(-1, train_x.shape[-1])).reshape(train_x.shape) valid_x = scaler.transform(valid_x.reshape(-1, valid_x.shape[-1])).reshape(valid_x.shape) test_x = scaler.transform(test_x.reshape(-1, test_x.shape[-1])).reshape(test_x.shape)fit_transform只调用一次,计算并应用训练集的均值和标准差。后面两步都只调transform,保证验证集和测试集使用完全相同的归一化参数。
4.2 训练参数的选择与模型保存
学习率和batch size是这里最值得反复调节的两个参数。结合期货数据普遍存在的强噪音特点,推荐用0.001的初始学习率配合ReduceLROnPlateau策略——验证集loss连续5个epoch不下降时,学习率自动乘以0.5。batch size的选择取决于序列长度和显存,32是多数任务的安全起点;如果训练震荡严重,降低到16通常会稳定一些。
import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau model = CNNAttentionLSTM(n_features=len(selected), seq_len=30, hidden_size=64, num_layers=2) optimizer = optim.Adam(model.parameters(), lr=0.001) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5) criterion = nn.MSELoss() for epoch in range(100): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_x) loss = criterion(pred, batch_y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() with torch.no_grad(): val_pred = model(valid_x) val_loss = criterion(val_pred, valid_y) scheduler.step(val_loss) torch.save(model.state_dict(), 'cnn_attention_lstm_futures.pth')上面这段代码里,clip_grad_norm_是训练稳定性最关键的一行。期货价格序列里偶尔会出现剧烈的异常波动,梯度的范数可能瞬间膨胀,不裁剪的话权重更新会直接把模型震散。max_norm=1.0是经验值,数据噪音大时可以降到0.5。模型保存用state_dict()而不是整个模型对象,这样部署时只需要同样的模型定义即可加载权重,不受PyTorch版本变化影响。
4.3 评估指标与Baseline对比
回归类预测任务最常用的评估指标是RMSE和MAE,但在这类预测里,更看重的是方向准确率。模型预测明天涨1%,结果跌了0.8%,RMSE会把这个预测判为不错的近似,但实际做交易这个方向就错了。所以除了衡量误差大小的指标外,还需要单独统计预测方向与真实方向一致的比率。
比较务实的做法是跑两个Baseline做参照:第一个是直接预测上一天价格的随机游走模型,第二个是去掉Attention层的CNN-LSTM。如果模型在这两个Baseline上都没有显著优势,说明注意力机制并没有学到有意义的时间权重分配,需要回看第3.2节中的注意力权重分布,观察权重是集中在少数关键时刻,还是近似平均分配——后者通常意味着训练没有收敛到理想状态。
缺省情况下,可以用下面这个函数快速评估:
def evaluate(y_true, y_pred): rmse = np.sqrt(np.mean((y_true - y_pred) ** 2)) mae = np.mean(np.abs(y_true - y_pred)) direction_acc = np.mean(np.sign(y_pred[1:] - y_pred[:-1]) == np.sign(y_true[1:] - y_true[:-1])) return {'RMSE': rmse, 'MAE': mae, 'DirectionAcc': direction_acc}direction_acc的实现逻辑是分别对预测值和真实值做一阶差分,再比较符号是否一致。注意这里的y如果是对收益率的预测,即可直接使用;如果是对价格的预测,最好先转成收益率再算方向准确率,避免趋势行情下天然偏向某个方向,这个指标在震荡市参考价值更大。
5. 把预测值转化为可执行的期货信号时,一个值得做的技巧是阈值分位法
5.1 抑制小噪音预测的干扰
训练过程中模型的预测输出通常带有大量小幅度波动,如果直接把这些波动全部当作交易信号,结果必然是高频换手,手续费和滑点成本远超收益。常见做法是设定一个固定阈值,预测值超过阈值才触发买入。但固定阈值的问题在于不同期货品种的波动率差异很大,螺纹钢的绝对波动和黄金的绝对波动完全是两个量级,固定阈值无法适应不同品种的特性。
更实用的替代方案是滚动分位阈值法:获取最近一段时间的预测值分布,取第70到第90百分位作为买入触发线,取第10到第30百分位作为卖出触发线。这样做的本质是让阈值跟随当前品种的波动状态自动调整——波动大的时候阈值自动抬高,波动小的时候阈值自动降低,始终截取预测分布中最极端的那部分才交易。
5.2 分位阈值与回测验证
import numpy as np def signal_to_position(predictions, history, low_percentile=20, high_percentile=80): if len(history) < 60: return 0 low_threshold = np.percentile(history, low_percentile) high_threshold = np.percentile(history, high_percentile) if predictions[-1] > high_threshold: return 1 elif predictions[-1] < low_threshold: return -1 else: return 0history保存最近60个交易日的模型预测值,每次预测后把新的预测值append进去并控制长度。low_percentile和high_percentile分别对应做空和做多的触发线,中间状态一律保持空仓。这个函数本身没有引入未来数据——history只包含当前时刻之前的预测值,不包含之后的任何数据。
5.3 回测里有哪些容易被忽视的成本项
任何策略在接入真实行情前的最后一关都是回测,但回测里最容易被低估的是滑点和手续费两笔成本。回测框架中常用的做法是每次开仓平仓双边各加一个最小变动价位作为滑点,比如螺纹钢期货的tick大小为1元/吨,那就每次开仓成本加1元/吨,平仓时再加1元/吨。每手开平总计约4元/吨的成本,对于一个目标收益为10元/吨的策略来说,这个成本占比已经超过40%,直接决定策略在真实环境下是否还赚钱。
验证分位阈值设置是否合理的方式是查看对应的换手率:如果回测区间内年化换手率超过20倍,大概率是阈值设置太低,在过度交易;如果换手率长期为0,再放宽分位范围,让模型在明确信号出现时敢于下注。
本文还有配套的精品资源,点击获取