相关性分析+CNN-Attention-LSTM的期货价格预测工程解析
2026/9/23 22:40:26 网站建设 项目流程

简介:一套基于相关性分析的CNN-Attention-LSTM期货价格预测模型完整Python实现,主要面向深度学习方向毕业设计、课程设计以及时间序列预测研究人群。资源覆盖特征相关性分析、时间步滑动窗口处理、混合神经网络构建、训练与预测全流程,并对数据预处理、模型参数、训练过程等关键步骤附详细注释,代码结构清晰,可直接复现。压缩包共29个文件,约30.29MB,包括8个Python脚本(数据预处理、模型训练、预测及API接口)、6个npy特征与标签数组、3个Excel数据表、预训练模型检查点文件、SQL原始数据库以及两份使用教程PDF,同时提供相关性热力图辅助分析结果,方便对照理解特征筛选与预测效果。当前已有809人学习下载,适合需要快速搭建期货价格预测基线、理解注意力机制与CNN-LSTM融合思想、完成课程设计或毕业论文实验的读者。读者可获得一套可运行的完整工程,并据此修改特征、调整网络结构,迁移至其他金融时序预测任务,具备较高参考价值。

1. 相关性分析加持的CNN-Attention-LSTM:一份能跑通的期货价格预测工程

做课程设计或者毕业设计,最怕拿到一个既跑不通又看不懂的深度学习源码包。这份基于相关性分析的CNN-Attention-LSTM期货价格预测模型,是我见过少数几份能把“数据预处理→相关性分析→时间步滑窗→CNN提取局部特征→Attention加权→LSTM捕捉时序→输出预测价格”整条链路串起来,并且自带玉米期货数据、两个版本checkpoint权重、预测API和Web前端的中文注释工程。核心思路很直接:先用相关性分析把无关特征筛掉,再让CNN-Attention-LSTM学习已经降噪的序列。适合想复现完整训练流程、需要毕业设计或课程设计项目源码,以及第一次接触注意力机制和LSTM序列预测的读者。

2. 相关性分析:先让数据说话,再决定喂给模型什么

2.1 为什么这份工程把相关性分析放在最前面

很多上手LSTM预测的人都犯过一个错:把Excel里能拿到的字段一股脑灌进模型。开盘价、收盘价、最高价、最低价、成交量、持仓量、各种技术指标全部堆进去,以为特征越多信息越全。实际跑下来,CNN和LSTM的参数量被无关特征拉高,训练变慢,val_loss反而不降。

这份工程在相关性分析.py里先做了一步降维:计算每个候选特征与目标列的相关系数,画出热力图,只保留与目标相关性超过阈值的特征。这样做的理由有两个。第一,期货价格序列信噪比本来就低,特征越多噪音源越多,模型很难在有限样本里学到稳定映射。第二,相关性分析结果本身就是一张可解释的中间产物,写论文或做答辩展示时能直接当论据,比丢出一张loss曲线更有说服力。

从资源包里的文件能看出这条链路是完整的:相关性分析用表.xlsx是原始特征表,相关性分析数据.npy保存了筛选后的特征顺序,我是热力图.png是相关性矩阵的可视化结果。后面时间步处理.pytrain_v2.py读取的npy文件,本质上是这一步产出的特征清单和归一化参数。

2.2 相关性分析.py 的核心逻辑与参数含义

我按照工程里的文件逻辑复现了最关键的一段,因为原文件带详细注释,我删掉打印信息后保留核心结构:

import pandas as pd import numpy as np def build_feature_table(raw_path): # 读取周报Excel,原始字段包含open/high/low/close/volume等 df = pd.read_excel(raw_path) # 目标列:未来第5个交易日的收盘价 # shift(-5) 表示用 t 时刻的特征预测 t+5 时刻的收盘价 df['target'] = df['close'].shift(-5) # 删除末尾因shift产生的空值行 df = df.dropna().reset_index(drop=True) return df if __name__ == '__main__': df = build_feature_table('玉米期货数据周报7.25.xlsx') # Pearson相关系数矩阵,看每个特征与target的线性关系 corr = df.corr(method='pearson') target_corr = corr['target'].drop('target').sort_values() # 只保留相关系数绝对值超过0.3的特征,过滤弱相关字段 selected = target_corr[target_corr.abs() > 0.3] print('筛选后的特征:', selected.index.tolist()) # 把特征名列表保存下来,后续时间步处理时按这个顺序取列 np.save('相关性分析数据.npy', selected.index.tolist(), allow_pickle=True)

这段代码有几个参数值得抠一下。

shift(-5)里的5代表预测周期,也就是拿过去20根K线数据去预测未来第5个交易日的收盘价。这个数字不是拍脑袋定的,5个交易日正好是一个自然周,对玉米期货这类农产品来说,周度级别的预测比日度级别噪音更小。如果换成股指期货,我一般会测3、7、10几个周期再选。

相关系数阈值0.3是个经验值。阈值太高会筛掉太多特征,模型输入维度太少学不到有效模式;阈值太低等于没筛。0.3在经济学相关分析里属于中等偏弱相关,对金融数据这种高噪音场景已经够用。跑完看一眼我是热力图.png,如果某两个特征之间颜色特别深,说明它们彼此强相关,通常只保留其中一个。

相关性分析数据.npy保存的是一个Python列表,里面是字符串形式的特征名。这是个很容易被忽略的细节,后续所有脚本都用它来固定特征顺序,保证训练和预测阶段读到的列顺序完全一致。如果不做这一步,Excel列顺序一改动,后面模型输入全乱。

2.3 怎么判断筛选结果靠不靠谱

相关性分析做完不是直接相信那张热力图,我会额外做三件事验证。

第一,把选出来的特征打印出来,逐个人工看一眼逻辑。比如成交量与收盘价相关性强是合理的,但如果某个字段和目标的相关性高到接近1,要警惕是不是目标列构造时发生了数据错位,常见错误是shift方向写反,或者dropna之后行索引没重置。

第二,用选出来的特征和全量特征分别训一个基线模型对比。常见做法是固定LSTM参数不变,只改变输入特征数量,看验证集误差。如果筛选后的特征训练出来的val_loss明显更低,说明相关性分析有效;如果差不多,说明特征冗余本来就不严重。

第三,检查相关系数的显著性。df.corr()默认只输出相关系数数值,不给p值。当样本量只有几十个时,0.3的相关系数可能根本不显著,这时候要手动算一下t统计量。资源包里的玉米期货周报数据量不大,建议直接把相关性分析当作特征粗筛,不要当作严格统计检验。

3. 时间步处理与训练流程:npy文件怎么来、checkpoint怎么存

3.1 时间步处理.py:把表格数据变成滑窗样本

LSTM和CNN1D的输入要求是三维张量,形状是(样本数, 时间步长, 特征数)。原始Excel是二维表格,每一行是一个交易日,必须通过滑窗把连续多个交易日拼成一个样本。这一步工程里叫时间步处理.py,输出就是train_x.npytrain_y.npytest_x.npytest_y.npy这四个文件。

核心转换逻辑如下:

import numpy as np from sklearn.preprocessing import MinMaxScaler def create_sequences(data, time_step=20): # data 的最后一列必须是target # 每个样本取连续 time_step 行的特征,预测第 time_step+1 行的target X, y = [], [] for i in range(len(data) - time_step): X.append(data[i:i + time_step, :-1]) y.append(data[i + time_step, -1]) return np.array(X), np.array(y) # 读取相关性分析阶段保存的特征清单 feature_names = np.load('相关性分析数据.npy', allow_pickle=True) # 这里省略了从Excel读取并选择特征列的步骤,df_selected是筛选后的DataFrame # 关键点:先按时间顺序排序,再划分,最后归一化,顺序不能乱 split_idx = int(len(df_selected) * 0.7) train_df = df_selected.iloc[:split_idx] test_df = df_selected.iloc[split_idx:] # 先fit训练集,再分别transform训练集和测试集 scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_df) test_scaled = scaler.transform(test_df) # 生成滑窗样本 train_x, train_y = create_sequences(train_scaled, time_step=20) test_x, test_y = create_sequences(test_scaled, time_step=20) np.save('train_x.npy', train_x) np.save('train_y.npy', train_y) np.save('test_x.npy', test_x) np.save('test_y.npy', test_y)

这里最需要注意是划分顺序。split_idx = int(len(df_selected) * 0.7)是按行号直接切的,因为期货数据本身就是强时间序列,随机划分会让模型在训练时看到未来数据。先切分再归一化也是必须的,如果对整个数据集先做MinMaxScaler再切分,测试集的均值和最大值会泄漏到训练里,结果虚高。

time_step=20的含义是每个样本包含最近20个交易日的特征,大约一个月的数据。这个值决定了模型的记忆窗口。窗口太短,模型看不到中期趋势;窗口太长,样本数量减少,因为滑窗会吃掉头部数据。20对周度数据来说是合理起点。

3.2 train_v2.py 的训练流程与两个checkpoint版本

训练脚本train_v2.py做的事情很规整:加载npy文件、构建模型、配三个回调函数、训练并保存权重。核心代码逻辑如下:

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint from cnn_attention_lstm import build_model # 构建模型,输入形状由时间步数和特征数决定 model = build_model(time_step=20, n_features=train_x.shape[2]) # 回归任务用MSE,MAE作为可读性更强的辅助指标 model.compile(optimizer='adam', loss='mse', metrics=['mae']) callbacks = [ # 验证集loss连续10轮不降就停止,防止过拟合 EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), # 验证集loss停滞时学习率减半 ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5), # 只保存验证集最优权重 ModelCheckpoint('checkpoint/my_modelv1.ckpt', save_best_only=True) ] model.fit(train_x, train_y, validation_data=(test_x, test_y), epochs=50, batch_size=32, callbacks=callbacks)

资源包里有my_modelv1.ckptmy_modelv2.ckpt两组文件,从命名习惯看,v1是基础版,v2是调整网络结构或训练参数后的版本。实际使用时我会先加载v1复现论文里的结果,再用v2对比效果差异。每个checkpoint目录下都有.data-00000-of-00001.index两个文件,这是TensorFlow 2.x SavedModel格式的正常结构,不是文件损坏。

batch_size=32在大多数机器上都不会爆显存,CPU也能跑动。epochs=50配合patience=10意味着最多跑50轮,但大概率在30轮左右就触发了early stopping。关键是restore_best_weights=True,很多人在回调里没加这个参数,训练结束后拿到的权重不是val_loss最低的那个点,而是最后一轮可能已经过拟合的权重。

3.3 训练日志怎么看

训练过程中主要盯三列:lossval_lossval_mae

正常情况是lossval_loss同步下降,并且val_loss略高于loss。如果val_loss在某个epoch开始反弹而loss还在降,说明模型开始死记训练集样本,这是过拟合的典型信号,EarlyStopping会在这个点附近停下。

如果训练一开始val_loss就在震荡甚至上升,先不要怀疑网络结构,按这三个顺序排查:第一,检查训练集和测试集是否发生特征顺序错位;第二,检查是否先归一化再切分导致测试集信息泄漏;第三,把学习率从默认的0.001降到0.0005,金融时间序列对学习率很敏感。

pred.npytest_y.npy不是训练阶段产生的,是pred.py推理阶段保存的预测结果和真实标签。训练完成后先把这两个文件跑出来,用绘图脚本画一条对比曲线,比看十个指标都直观。

4. cnn_attention_lstm.py 拆解:三段模型的衔接细节

4.1 整体结构:Conv1D → Attention → LSTM → Dense

资源包里没有用堆叠式LSTM,而是选择了CNN-Attention-LSTM的混合结构,这在期货预测里是常见做法。先说清楚每一段负责什么:Conv1D用来提取短期局部模式,比如连续三根K线的形态组合;Attention在时间维度上分配权重,让模型关注最近哪些时点对预测更关键;LSTM继续捕捉残留下来的中长期依赖;最后接Dense输出预测价格。

模型定义的核心代码:

import tensorflow as tf from tensorflow.keras.layers import Input, Conv1D, MaxPooling1D, LSTM, Dense, Dropout from tensorflow.keras.layers import Layer from tensorflow.keras import Model class AttentionLayer(Layer): # 自定义时间维注意力层 def __init__(self, units=64, **kwargs): super().__init__(**kwargs) self.units = units def build(self, input_shape): # input_shape: (batch, time_step, features) self.W = self.add_weight(shape=(input_shape[-1], self.units), initializer='glorot_uniform') self.v = self.add_weight(shape=(self.units, 1), initializer='glorot_uniform') def call(self, inputs): # 打分函数:先映射到注意力空间,再算权重 score = tf.tanh(tf.tensordot(inputs, self.W, axes=1)) weights = tf.nn.softmax(tf.tensordot(score, self.v, axes=1), axis=1) # 按权重对时间步做加权求和,得到上下文向量 return tf.reduce_sum(inputs * weights, axis=1) def build_model(time_step=20, n_features=8, filters=64, lstm_units=64): inputs = Input(shape=(time_step, n_features)) # CNN段:在时间维度上做一维卷积 x = Conv1D(filters=filters, kernel_size=3, padding='same', activation='relu')(inputs) x = MaxPooling1D(pool_size=2)(x) x = Dropout(0.2)(x) # LSTM段:返回完整序列,供Attention计算权重 x = LSTM(lstm_units, return_sequences=True)(x) # Attention段:把 (batch, time_step, lstm_units) 压缩成 (batch, lstm_units) x = AttentionLayer(units=lstm_units)(x) x = Dropout(0.2)(x) x = Dense(16, activation='relu')(x) output = Dense(1)(x) model = Model(inputs, output) return model

这里的维度变化值得讲清楚。输入inputs的形状是(batch, 20, 8),经过Conv1D后变成(batch, 20, 64),因为filters=64MaxPooling1D(pool_size=2)把时间步从20压到10,这一步不仅降维,还让模型对局部平移更鲁棒。LSTM设置return_sequences=True后输出形状是(batch, 10, 64),每个时间步都有一个隐状态输出。AttentionLayer接着把这些隐状态加权求和,得到(batch, 64),最后通过两个全连接层输出标量价格。

kernel_size=3的含义是卷积核覆盖3个连续时间步,对周度数据来说,这相当于一次看三周的价格形态。如果想捕捉更长的局部模式,可以改成5,代价是参数量上升且训练变慢。

4.2 Attention 层在代码里是怎么实现的

资源包里的注意力机制属于加性注意力,和Transformer里的多头自注意力不是一回事,但原理一致:让模型对每个时间步的隐状态打分,再用softmax转成权重,最后加权求和。

def call(self, inputs): # 把每个时间步的隐状态投射到注意力空间 score = tf.tanh(tf.tensordot(inputs, self.W, axes=1)) # 再通过向量v把每个位置的分数压成标量 weights = tf.nn.softmax(tf.tensordot(score, self.v, axes=1), axis=1) # 加权求和得到上下文向量 return tf.reduce_sum(inputs * weights, axis=1)

tf.tensordotaxes=1时等价于矩阵乘法。第一次tensordot(batch, time_step, 64)映射成(batch, time_step, units),第二次把每个时间步的向量压缩成一个标量,得到(batch, time_step, 1)softmax在时间轴axis=1上做归一化,保证所有权重和为1。最后用inputs * weights对每个时间步加权,再沿时间轴求和。

这个层放在LSTM之后而不是之前,是因为LSTM已经完成了时序编码,Attention负责从编码结果里挑重点。如果放在LSTM之前,attention作用于原始特征,语义上更像特征选择而不是时序聚焦。我习惯把LSTM的units和Attention的units设成一样,这样两次矩阵变换维度一致,参数也省。

4.3 模型文件中没有明说的训练细节

Dropout(0.2)放在CNN输出和Attention输出之后,位置有讲究。放在卷积后是常规操作,用于抑制局部过拟合;放在Attention之后很少见,但效果很好,因为加权求和本身已经聚合成一个向量,再接一次dropout相当于对特征做随机遮罩,能提升泛化能力。

工程里没有单独设置learning_rate,用的是Adam默认的0.001。金融时间序列上我常常会调低到0.0005,然后配合ReduceLROnPlateau让它在训练中期自动衰减。

还有一个小细节:Dense(16, activation='relu')中间层有16个神经元,这个数字决定了全连接段的特征容量。复现时如果发现训练集拟合得很好但验证集差,可以把这个16改成8试试;如果两者都差,可以加到32。

5. 避坑指南:复现这套模型时我踩过的坑

5.1 归一化泄漏:验证集误差好看,实盘一测就翻车

现象:训练和测试loss都很低,验证集曲线和真实价格贴合得几乎完美,但用最新一周数据去做单条预测,预测值明显偏离真实走势。

原因:把MinMaxScaler在整个数据集上fit_transform,再划分训练集和测试集。这样测试集的均值和取值范围已经进入scaler的统计量里,模型等于间接看到了测试集信息,误差自然虚低。

解决:严格先切分再归一化,测试集只用scaler.transform,不用fit_transform。同时把scaler对象保存下来,预测阶段加载同一个scaler对输入做变换,否则训练和预测的数值范围不一致。从资源包结构看,npy文件是已经处理好的,我自己复现时会重跑一遍时间步处理.py确认归一化顺序。

5.2 目标列用shift构造时,行索引错位导致相关系数虚高

现象:相关性分析阶段某个特征与target的相关系数高达0.95,明显不合理,选出来的特征集合也怪怪的。

原因:shift(-5)之后,DataFrame末尾有5行target是NaN,直接dropna()后如果忘了reset_index(drop=True),后续拼接特征时行与行之间错位,相当于让模型用当天数据预测当天价格,相关系数当然虚高。

解决:构造完target后立即reset_index(drop=True),然后打印前10行核对日期列和target是否对应。更稳妥的做法是改用未来第5个交易日的收益率而不是绝对价格作为target,收益率是平稳序列,不容易因为价格趋势产生伪相关。

5.3 单条预测时报维度错误:expected ndim=3, found ndim=2

现象:用pred_API.py跑接口,前端传过来最近20天的数据,调用model.predict(x)时报错,提示期望3维输入但得到2维。

原因:模型输入要求(batch, time_step, n_features),单个样本拿到的是(time_step, n_features),缺了batch维度。很多人在训练时用批数据没问题,到了单条推理就忘了reshape。

解决:在接口层把输入统一强制reshape成(1, time_step, n_features),参数time_step必须和训练时保持一致。我一般会在拿到请求数据后先打印x.shape确认维度再送进模型。

5.4 加载checkpoint时提示变量不匹配或模型未构建

现象:model.load_weights('checkpoint/my_modelv1.ckpt')报错,提示模型里某些变量不存在,或报“模型尚未build”之类的问题。

原因:TensorFlow 2.x按名称加载权重,如果build_modelInput层没有显式指定shape,模型内部变量创建的时机被推迟到第一次predictfit之后。此时load_weights找不到对应变量。

解决:在build_modelInput里显式写shape=(time_step, n_features),加载权重前先调用一次model.predict(tf.zeros((1, time_step, n_features)))触发变量初始化,再执行load_weights。资源包里的checkpoint文件分.index.data-00000-of-00001两份,加载时必须指定不带文件后缀的路径前缀,比如checkpoint/my_modelv1.ckpt

5.5 用MSE训练,预测曲线在均值附近被磨平

现象:预测价格画出来几乎是一条水平线,数值集中在训练集收盘价的均值附近,方向基本预测不中。

原因:MSE对大误差的惩罚是平方级的,模型发现预测值接近均值时,整体MSE反而最小。期货价格序列信噪比低,训练集里真正的涨跌信号占比小,模型选择“躺平”来降低平均误差,这是回归任务预测价格类序列的经典问题。

解决:把损失换成maelog_cosh,降低异常值对梯度的主导作用;更彻底的做法是对收盘价做一阶差分,目标变成预测涨跌幅度,训练完成后预测值再累加还原为价格。资源包里train_v2.py默认用mse,复现时如果发现预测曲线太平,优先改这里。

6. 推理与验证:pred.py / pred_API.py 的正确打开方式

6.1 pred.py:批量预测并反向还原价格

训练完之后第一件事不是看指标,而是跑一次完整的批量预测,把pred.npytest_y.npy生成出来,直接对比预测曲线和真实曲线。

import numpy as np from cnn_attention_lstm import build_model time_step = 20 # 从npy文件加载测试数据 test_x = np.load('test_x.npy') test_y = np.load('test_y.npy') # 构建模型并加载v1权重 model = build_model(time_step=time_step, n_features=test_x.shape[2]) model.load_weights('checkpoint/my_modelv1.ckpt') # 批量预测,输出形状是 (样本数, 1) pred_scaled = model.predict(test_x).flatten() # 假设归一化阶段用的是MinMaxScaler,这里反向还原为真实价格 # 实际工程中scaler对象在时间步处理.py里保存,这里直接加载 pred_price = scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true_price = scaler.inverse_transform(test_y.reshape(-1, 1)).flatten() np.save('pred.npy', pred_price) np.save('test_y.npy', true_price) # 方向准确率:预测涨跌方向与实际方向是否一致 pred_direction = np.diff(pred_price) > 0 true_direction = np.diff(true_price) > 0 direction_acc = np.mean(pred_direction == true_direction) print('Direction Accuracy:', direction_acc)

这里打印的Direction Accuracy比RMSE重要得多。期货交易关心的是方向,预测误差再小,方向反了照样亏钱。如果方向准确率在0.55以上,这个模型就有参考价值;0.5以下说明预测基本没有信息量,需要回头调特征或换损失。

6.2 pred_API.py:把模型包装成HTTP接口

pred_API.py做的事情是Flask封装,把训练好的模型变成POST接口,前端Web页面通过Ajax调它拿预测结果。接口部分的关键代码:

from flask import Flask, request, jsonify import numpy as np from cnn_attention_lstm import build_model app = Flask(__name__) # 初始化模型并加载v2权重 model = build_model(time_step=20, n_features=8) model.load_weights('checkpoint/my_modelv2.ckpt') @app.route('/predict', methods=['POST']) def predict(): data = request.get_json(force=True) # 前端传入的特征数组,形状必须是 (20, n_features) features = np.array(data['features'], dtype=np.float32) # 单条预测必须补上batch维度 x = features.reshape(1, 20, -1) pred = model.predict(x)[0, 0] return jsonify({'pred_price': float(pred)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

这里的features字段需要前端把最近20个交易日、经过相同顺序和归一化处理的8个特征传过来。注意字段顺序必须和相关性分析数据.npy里保存的特征名顺序一致,这是整套工程里最容易在联调时翻车的地方。Web前端配置及使用教程.pdf里写了页面启动步骤,前端页面把用户选择的日期范围、特征数据组装成JSON发送到这个接口,拿到pred_price后在前端画走势图。

从那以后我每次复现这类时序预测工程,都强制自己走一遍完整闭环:先跑相关性分析确认特征合理,再重放时间步处理确认归一化和划分顺序,然后训练并记录checkpoint版本号,最后用方向准确率而不是纯误差来判断模型好坏。这个习惯帮我挡掉了至少三次因为数据泄漏或维度错位导致的“假阳性”结果。希望这份工程的拆解思路对你有帮助,拿回去跑通后,试着把阈值、时间步长和预测周期各改一组对比出来,你会对CNN-Attention-LSTM的边界有更实在的判断。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询