ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CNN-Attention-LSTM期货价格预测:相关性分析驱动的完整工程实践

CNN-Attention-LSTM期货价格预测:相关性分析驱动的完整工程实践 简介这是一份基于相关性分析的CNN-Attention-LSTM期货价格预测模型Python项目面向计算机相关专业正在做课程设计、期末大作业的学生也适合想实践深度学习时序预测的初学者。项目经导师指导并获98分完整覆盖从相关性分析、时间步处理到CNN-Attention-LSTM模型构建与预测的全流程。压缩包共29个文件约30.29MB包含Python源码、npy数据文件、Excel期货数据表、模型权重文件、PDF使用教程及说明文档其中py脚本负责数据预处理、模型训练与预测npy为中间过程数据ckpt为训练好的模型权重xlsx为原始及处理后的数据。已有258人学习/下载。资源自带详细中文注释并附Web前端配置教程和独立算法说明能帮助读者理解特征相关性筛选、注意力机制与LSTM结合的思路可直接运行复现也可作为课程设计或毕业设计的参考模板。1. 相关性分析驱动的 CNN-Attention-LSTM 期货价格预测先看清这条工程链路期货价格预测的难点不在模型而在从行情到可复现结果之间的每一层工程。相关性分析、CNN-Attention-LSTM、源码加数据集加模型加注释对应四段路径先筛特征再定结构再训练再验证。下面按这条路径顺序给出可直接复制的代码与参数依据。适合已有 Python 和 TensorFlow 基础、想把深度学习用于行情的读者。重点在 2.2 的相关性判读、3.2 注意力的放置位置、4.1 的数据泄露陷阱以及第 5 章的滚动回测新手可以整体跟一遍再跳读。数据使用日频期货行情预测目标是下一交易日收益率。这个标题对应的 .zip 拆开后通常是数据准备、模型、训练、评估四个文件本文的章节顺序就是阅读这类源码的索引。2. 相关性分析选特征从原始行情到模型输入的过滤方法2.1 为什么先做相关性分析而不是直接喂数据直接把开高低收、成交量、持仓量、RSI、MACD 等几十列一起塞给模型听起来省事实际上大部分列和目标之间要么无关联要么彼此高度共线。共线特征会让 LSTM 的门控计算把容量浪费在冗余信号上无关特征则被当成噪声反复调整权重。相关性分析的目的是在进模型前做一次低成本筛选让 CNN 和 LSTM 把容量集中在少数有信息量的序列上。注意这里的相关性有两层含义。第一层是特征与目标变量下一交易日收益率之间的相关性决定哪些特征留下第二层是特征彼此之间的相关性决定哪些特征重复。两层的阈值和方法不同下面分开处理。2.2 用 Pearson、Spearman 与互信息筛选候选特征先给出最常用的筛选代码import pandas as pd import numpy as np from scipy.stats import pearsonr, spearmanr from sklearn.feature_selection import mutual_info_regression df pd.read_csv(rb_daily.csv, parse_dates[date], index_coldate) # 构造候选特征收益率、振幅、量变化 df[ret_1] df[close].pct_change() df[ret_5] df[close].pct_change(5) df[amp] (df[high] - df[low]) / df[close] df[vol_chg] df[volume].pct_change() # rsi、macd 等指标由 ta 库自行计算后并入 df此处从略 # 目标变量下一交易日收益率。shift(-1) 保证不含当天未来信息 df[target] df[close].shift(-1) / df[close] - 1.0 df df.dropna() features [ret_1, ret_5, amp, vol_chg, rsi, macd] for col in features: p, _ pearsonr(df[col], df[target]) s, _ spearmanr(df[col], df[target]) print(f{col:10s} pearson{p:6.4f} spearman{s:6.4f}) # 互信息不假设线性关系对低信噪比数据更宽容 X df[features] mi mutual_info_regression(X, df[target], random_state42) print(pd.Series(mi, indexfeatures).sort_values(ascendingFalse))逻辑说明pct_change计算收益率序列shift(-1)把未来价格前移一天让target成为下一交易日的收益。这是整个工程里最容易出错的一行——如果顺手写成当天收益模型就会学到“用当天数据预测当天”的无意义映射。Pearson 对线性关系敏感Spearman 对单调关系敏感两者符号一致时特征更可靠互信息能抓到 U 型、非线性关联适合 rsi、macd 这类有上下界的指标。参数说明mutual_info_regression的n_neighbors默认是 3样本量上万时可调到 510值越大估计方差越小计算越慢random_state固定是为了每次特征排序结果一致。价格类序列做 Pearson 相关时系数通常都很小因为日频收益率本身信噪比极低r0.03 在统计上显著不代表有预测力。判断标准要结合样本量看 p 值并确认多个时期方向一致。各指标判读方式如下指标取值范围判断依据筛选参考阈值Pearson 相关系数-1 到 1线性相关与 target 的 |r| 大于 0.05 且 p 值显著才留Spearman 相关系数-1 到 1单调相关与 Pearson 同号且 |r| 大于 0.05互信息大于等于 0任意非线性关联按排序取前一半或保留相对最大值的 30% 以上特征间 Pearson-1 到 1冗余程度两两大于 0.8 时删掉其一提示shift(-1)后最后一行的target一定是 NaN计算相关性前必须先dropna()否则相关系数会被缺失值污染。相关性矩阵也只在训练段上计算验证和测试段沿用同样的筛选规则不能整段数据一起算否则会引入未来信息。2.3 构造滑动窗口把单日特征拼成序列样本相关性筛选结束后剩余特征需要被组织成模型能读的序列。CNN-Attention-LSTM 的输入形状是(样本数, 序列长度, 特征数)序列长度就是滑动窗口大小含义是“用过去 30 个交易日预测下一个交易日的收益”。窗口太小模型看不到中期节奏窗口太大样本数量下降且训练变慢。def make_dataset(feat_arr, target_arr, seq_len30): 把二维特征表切成 (n, seq_len, n_features) 的序列样本 X, y [], [] for i in range(len(feat_arr) - seq_len): X.append(feat_arr[i:i seq_len]) # 窗口第 i 天到第 iseq_len-1 天 y.append(target_arr[i seq_len - 1]) # 窗口最后一天到次日的收益率 return np.array(X), np.array(y)参数说明target_arr[k]定义为第 k 天收盘到第 k1 天收盘的收益率所以窗口结束后第一个收益对应下标iseq_len-1。seq_len在日频数据上常取 2060对应 13 个自然月5 分钟级别的盘中数据可取 4896。切窗口时相邻样本重叠了seq_len-1天验证集看起来会比真实情况好所以训练集和验证集之间要留间隔这个问题在 4.1 展开。一两年的日频数据大约能切出 400600 个样本这个量级决定了下文 LSTM 的隐藏单元不能开太大。3. CNN-Attention-LSTM 结构拆解与参数依据3.1 CNN 先做局部特征提取卷积核与步长的选择行情序列里真正可用的信号往往是短期局部形态连续三天缩量、一根放量突破又把价格拉回某个区间。Conv1D 做的事就是在序列上滑动一个窗口把窗口内几个交易日的特征组合成一个新模式。卷积核大小决定一次看几天过滤器数量决定提取多少种模式paddingsame保证输出长度和输入一致这样后续的注意力层和 LSTM 才能按时间步对齐。import tensorflow as tf from tensorflow.keras import layers x layers.Input(shape(30, 8)) x layers.Conv1D(filters32, kernel_size3, paddingsame, activationrelu)(x) # 输出形状仍是 (batch, 30, 32)kernel_size3 表示一次覆盖 3 个交易日 x layers.MaxPooling1D(pool_size2)(x) # 池化把序列长度压到 15减少后续注意力与 LSTM 的计算量参数说明卷积核取 35对应日频行情的 35 个交易日不确定时从 3 开始用验证集比较 kernel3 和 kernel5 的误差再定。不要一上来就用大卷积核日频序列本身很短大卷积核会把注意力集中到整体趋势上丢掉反转信号。filters太少特征提不全太多则在几百个样本上很快过拟合一般从 32 起步。3.2 注意力层放在 CNN 与 LSTM 之间实现与权重含义注意力机制最早在神经机器翻译里出现后续 transformer 中的 scaled dot-product attention 也是同一思路。放到行情预测里注意力解决的问题是过去 30 天里哪几天对预测更关键。CNN 之后的每个时间步是一个由局部特征组成的向量注意力给这些向量分别打分softmax 归一后作为权重去缩放原始序列。class TemporalAttention(layers.Layer): 时间注意力对每个时间步打分输出 (batch, time, 1) 的权重 def __init__(self, units16): super().__init__() self.dense1 layers.Dense(units, activationtanh) # 打分网络 self.dense2 layers.Dense(1, use_biasFalse) # 压缩到单值 def call(self, x): score self.dense2(self.dense1(x)) # (batch, time, 1) weight tf.nn.softmax(score, axis1) # 沿时间维做概率归一 return weight # 使用方式加权后的序列再进 LSTM weight TemporalAttention(units16)(conv_out) # (batch, time, 1) attended conv_out * weight # 逐时间步缩放 lstm_out layers.LSTM(64, dropout0.2)(attended)逻辑说明dense2用use_biasFalse因为打分只需要相对量bias 只会整体平移 score而 softmax 对平移不敏感留着徒增参数。softmax 沿axis1即时间维做归一保证每个样本的权重和为 1。这样权重可以直接画出来检查如果模型把 80% 的权重压在预测点前两三天的某个特征上这个行为可解释也和短线动量一致。参数说明units取 LSTM 隐藏单元的四分之一到二分之一LSTM 用 64 时这里取 832。units 太大会让打分网络记住训练集噪声太小则所有时间步得分趋同注意力退化成平均池化。3.3 LSTM 与时序建模完整模型和参数表LSTM 的输入门、遗忘门、输出门决定序列信息如何累积和丢弃。在 CNN 已经提取局部特征、注意力已经完成时间加权的前提下LSTM 的职责是捕捉这些加权特征之间的顺序关系比如“放量下跌之后几天内的修复概率”。对这个任务单层 LSTM 基本够用两层只在样本量超过五万或序列长度超过六十时才明显获益。def build_model(seq_len, n_features, filters32, kernel3, lstm_units64, dropout0.2): inp layers.Input(shape(seq_len, n_features)) conv layers.Conv1D(filters, kernel, paddingsame, activationrelu)(inp) conv layers.MaxPooling1D(pool_size2)(conv) # 序列长变为 seq_len/2 attn_w TemporalAttention(16)(conv) # (batch, t/2, 1) attn conv * attn_w # 加权后的特征序列 lstm layers.LSTM(lstm_units, dropoutdropout)(attn) # 取最后隐藏状态 out layers.Dense(1)(lstm) # 输出下一交易日收益率 return tf.keras.Model(inp, out) model build_model(seq_len30, n_features8) model.summary()参数说明MaxPooling1D把序列长度从 30 压到 15注意力层处理 15 个时间步计算量只有直接丢给 LSTM 的一半。dropout作用于输入到 LSTM 的循环连接用于抑制过拟合。完整模型训练前的核心参数推荐如下参数常见取值过大/过小表现调整方向filters1664过大易过拟合过小特征提不全特征 8 个以下用 16以上用 32kernel_size35过大拖慢且忽视局部形态日频固定 3 或 5用验证集比较lstm_units32128过大收敛慢且易过拟合按样本量调整几千样本用 64dropout0.20.4过小过拟合过大欠拟合样本少于两千时从 0.3 起步Attention units832过大注意力退化成噪声取 lstm_units 的 1/41/2不少复现代码喜欢再叠 BatchNormalization 或多层 LSTM我一般不建议。期货日频数据样本少、信噪比低结构越复杂验证集表现和真实表现的差距越大。先跑通这个最简结构确认相关性分析和数据切分没问题再考虑加深。4. 训练与调参让 python 源码跑出可复现的预测4.1 归一化与带间隔的时序划分两类数据泄露常见做法是先对整个数据集做 MinMaxScaler 再切分这是最隐蔽的错误。scaler 在全部数据上拟合过训练时模型已经间接“见过”后面一段的极值分布验证集指标会虚高。正确顺序是只用训练段拟合 scaler再 transform 验证和测试段训练集内部可以随机打乱三个集合之间必须严格按时间切。from sklearn.preprocessing import MinMaxScaler # 1. 只对训练段拟合 scaler MinMaxScaler(feature_range(0, 1)) scaler.fit(train_df[features]) # 2. 全部数据统一 transform统计量来自训练段 for part in (train_df, val_df, test_df): part[features] scaler.transform(part[features]) X_tr, y_tr make_dataset(train_df[features].values, train_df[target].values) X_va, y_va make_dataset(val_df[features].values, val_df[target].values) X_te, y_te make_dataset(test_df[features].values, test_df[target].values)逻辑说明pct_change产生的 NaN 要在 4.1 之前丢掉否则填 0 会让模型误以为第一天出现巨大涨跌。三组数据之间不加间隔直接切是第二类问题滑动窗口的重叠让验证集和训练集共享大部分日期验证误差被严重低估。常见做法是在训练段末尾和验证段开头之间再砍掉一个seq_len的长度让两者在时间上完全重叠不到。注意训练集内部可以 shuffle训练、验证、测试三个集合之间绝不能 shuffle必须保持时间顺序否则等于把未来样本混进训练过程。4.2 训练参数与回调函数早停、学习率衰减与断点保存复现前先把 Python 环境装齐pandas、scipy、scikit-learn、tensorflow 缺一不可。训练脚本的核心配置如下model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossmse ) callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5), tf.keras.callbacks.ModelCheckpoint( cnn_att_lstm_best.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X_tr, y_tr, validation_data(X_va, y_va), epochs200, batch_size64, callbackscallbacks, verbose1 ) # 评估时用断点保存的最优权重而不是训练结束时的权重 model.load_weights(cnn_att_lstm_best.h5)参数说明Adam 的learning_rate从 1e-3 起步验证损失前 10 轮不下降就降到 1e-4 重新训练。EarlyStopping的patience15表示验证损失连续 15 轮不创新低就停止restore_best_weightsTrue让模型回滚到最优权重期货数据噪声大val_loss 曲线经常先降后升不回滚拿到的是一组已经退化的参数。ReduceLROnPlateau在验证损失 5 轮不改善时把学习率砍半让模型在低学习率下继续微调。ModelCheckpoint保存 val_loss 最优的权重文件和早停双保险。batch_size 按样本量调整样本量batch_sizeepochs 上限备注少于 5001632100更新次数少epoch 不必太多50050003264150200本项目常见区间配合早停大于 500064128200可适当加大 LSTM 容量4.3 评价指标收益率回归误差与方向准确率模型输出的是下一交易日收益率属于回归任务MAE、RMSE 都能用。但对交易而言绝对值误差远不如“涨跌方向判断对没对”重要。两个模型的 RMSE 相同方向准确率可能差三个百分点后者直接决定策略盈亏。评估脚本里必须同时打印两类指标from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np pred model.predict(X_te).flatten() mae mean_absolute_error(y_te, pred) rmse float(np.sqrt(mean_squared_error(y_te, pred))) acc (np.sign(y_te) np.sign(pred)).mean() print(fMAE{mae:.5f} RMSE{rmse:.5f} 方向准确率{acc:.2%})逻辑说明sign把收益率映射成 1、0、-1逐样本比较预测方向与真实方向。日频收益率在 0 附近的样本很多把小涨判断成小跌也算错所以方向准确率天然偏低。源码注释里通常还会补一个只统计 |y_te| 排前 50% 样本的“大波动方向准确率”这个指标更能反映模型吃到趋势行情的能力。如果源码带详细注释优先读数据准备和评估两段这两部分最容易被抄错也最影响最终结果。5. 预测结果验证滚动回测、注意力权重检查与模型融合5.1 滚动回测一次性切分的替代方案固定切一次训练集和测试集结果只代表某一段行情。更接近实盘的做法是滚动回测每个月用过去一年的数据重新训练一遍预测下一个月把预测结果按时间拼起来。这样每个样本的预测都来自没见过它的权重同时能暴露数据分布偏移对模型的影响。def walk_forward(X, y, train_len240, step20): 滚动训练训练集固定 train_len预测接下来的 step 天 preds np.full(len(y), np.nan) for i in range(0, len(y) - train_len - step, step): model build_model(seq_len30, n_featuresX.shape[-1]) model.compile(optimizeradam, lossmse) model.fit(X[i:itrain_len], y[i:itrain_len], epochs60, verbose0) preds[itrain_len:itrain_lenstep] \ model.predict(X[itrain_len:itrain_lenstep]).flatten() return preds说明train_len240对应一年左右的交易日step20相当于每月重训一次。每段重新实例化模型训练权重互不继承避免模型记住特定交易时段的噪声。5.2 注意力权重与三种子融合两道检查滚动回测之后把注意力权重拉出来看一遍。softmax 的权重如果接近均匀分布说明注意力模块没学到时间优先级问题多半出在打分网络 units 太小或 CNN 卷积核被 padding 稀释如果权重集中在预测点前 25 天说明模型学到了短期动量的逻辑结果可信。最后一步是模型融合同一个结构换三个随机种子各训一次对预测值取平均。日频收益信噪比低单次训练的随机性足以让方向准确率波动两三个百分点简单平均通常能把波动压到 0.5 个百分点以内代价只是训练时间翻三倍是性价比最高的融合方式。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进