ARTICLE DETAIL

资讯详情

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

LSTM金融时序预测:收益率建模与多品种实战解析

LSTM金融时序预测:收益率建模与多品种实战解析 简介基于LSTM的时序收益预测系统完整项目包聚焦金融序列数据建模场景适合希望掌握RNN变体实际应用的Python开发者与量化入门者。资源共55个文件涵盖1data_create.py数据预处理、2train.py模型训练、3pred_test.py预测评估三个核心脚本并附有多个行业的实验截图、收益拟合曲线以及npy测试数据、xlsx原始数据表、训练好的tf模型与MinMaxScaler模型压缩包仅2.27MB轻量但结构完整。已有2908人学习下载。通过该资源可完整走通数据清洗、LSTM网络搭建、训练调参到结果可视化的项目流程操作文档对步骤和注意事项作了说明适合对照实践在真实大宗商品数据集上验证深度学习预测效果同时也可作为课程设计或毕业设计的参考模板。1. 从收益率序列到LSTM模型这个项目到底拆出了什么做大宗商品时序预测的人大概率都遇到过同一个尴尬拿原始价格直接扔进LSTM训练出来的曲线看着拟合得挺好一放到真实交易里就失效。原因不在模型而在你喂给模型的数据形态。这个基于LSTM的时序收益预测系统核心思路是绕开价格序列直接对收益率序列建模——三个品种化工、有色、贵金属各自独立训练每个品种一套模型参数预测目标从下一个收盘价变成下一期收益率再通过收益率反推价格区间。这样做的好处是序列本身更平稳LSTM不需要去拟合价格里的趋势项和周期性噪音门控机制能把精力集中在收益率的波动模式上。项目代码拆成三个脚本1data_create.py做数据清洗和归一化2train.py负责模型训练3pred_test.py做测试集预测和误差分析。数据文件里还带了每个品种的test_y_*.npy和y_*.npy说明作者已经按时间顺序切好了训练集和测试集并且把原始收益率和归一化后的数值分开存储。适合谁看想用深度学习做金融时序建模、但不想在数据预处理和反归一化上踩坑的人这份代码能省掉至少一周的调试时间。2. LSTM门控机制与金融时序预测的匹配逻辑2.1 为什么选LSTM而不是RNN或TransformerRNN在处理长序列时有一个致命问题反向传播过程中梯度要沿着时间步逐层回传序列超过几十步梯度要么指数级衰减到零梯度消失要么爆炸式增长。LSTM引入门控机制每个时间步有三个门——遗忘门、输入门、输出门——通过sigmoid激活函数输出0到1之间的值控制信息的保留比例。遗忘门决定上一时刻的细胞状态中哪些信息要丢弃输入门决定当前候选值中哪些新信息要写入输出门决定当前隐状态对外输出多少。这套机制让梯度可以沿着细胞状态这条传送带跨时间步传导衰减速度远慢于普通RNN。对金融时序来说收益率的自相关性往往在滞后5到20个交易日之间还有显著影响LSTM恰好能覆盖这个跨度。Transformer理论上能捕获更长依赖但金融时序的信噪比太低自注意力机制很容易把噪声也当成重要特征效果反而不如参数量更小的LSTM稳定。在2train.py里可以看到作者用的是keras的Sequential模型LSTM层后面没有接复杂的注意力模块这个选择是务实的。2.2 数据预处理的三个关键步骤打开1data_create.py数据处理流程分三步走每一步都直接影响后面的训练效果。首先是收益率计算。原始数据是价格序列需要先转成对数收益率或简单收益率。常见做法是pct_change()算简单收益率但更推荐对数收益率log(price_t / price_{t-1})因为对数收益率在数学上具有时间可加性多期累乘可以直接变成累加统计特性也更接近正态分布。代码里用的是哪种可以通过y_*.npy的实际数值范围判断——如果数值集中在-0.05到0.05之间大概率是日收益率不是对数收益率。第二步是窗口化。LSTM的输入是三维张量(样本数, 时间步长, 特征数)所以需要把收益率序列切成长度为lookback的滑动窗口。比如用前15天的收益率预测第16天的收益率lookback15特征数就是1。窗口太小模型学不到趋势太大则引入过多噪声金融日频数据通常取5到30之间。第三步是归一化。代码里有一个MinMaxScaler_model.pkl这是用sklearn的MinMaxScaler在训练集上拟合后保存的。注意一个关键细节只能用训练集的min和max去transform测试集和未来的数据不能用全量数据拟合scaler否则相当于把测试集信息泄露给模型预测效果会被虚高。检查代码时重点看这个scaler是不是在切分训练集之后、训练模型之前fit的——这一点上1data_create.py的命名顺序暗示它是先做数据准备再训练。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler df pd.read_excel(data/data_化工.xlsx, engineopenpyxl) df[ret] np.log(df[close]).diff().dropna() scaler MinMaxScaler(feature_range(0, 1)) # 先切分再fit避免数据泄露 train_size int(len(df) * 0.8) train_ret df[ret].iloc[:train_size].values.reshape(-1, 1) test_ret df[ret].iloc[train_size:].values.reshape(-1, 1) scaler.fit(train_ret) train_scaled scaler.transform(train_ret) test_scaled scaler.transform(test_ret)这段逻辑里有个容易犯的错np.log(df[close]).diff()之后第一行会是NaN必须dropna()否则后面滑动窗口切出来全是NaN模型直接失去学习能力。切分的比例这里用80/20实际项目中也可以按时间点而不是比例切分比如用2022年之前做训练、之后做测试更贴近真实交易场景中用过去预测未来的约束。2.3 三个品种为什么要分开归一化数据目录里每个品种是独立的Excel文件和独立的y_*.npy归一化也是各做各的。这不是代码冗余而是正确做法。化工、有色、贵金属的价格波动率差异很大——黄金的年化波动率可能不到15%而化工品如甲醇、PTA的年化波动率能到40%以上。如果混在一起做全局归一化低波动品种的收益率数值会被压缩到很窄的区间LSTM的梯度更新会被高波动品种主导低波动品种几乎学不到有效特征。MinMaxScaler_model.pkl文件名是单数看起来只保存了一个scaler。如果是三个品种共用一个scaler那训练流程里做的可能是所有品种的收益率拼接后统一归一化这在横向对比实验结果时是合理的——确保三组模型的输入分布在同一个量纲下Loss曲线可以直接对比。如果用三个单独的scaler每个模型输入分布各不相同训练曲线的可比性就弱一些。判断方式很简单加载pkl后看scaler.data_min_和scaler.data_max_的数组长度是否等于各品种收益率的总样本数。3. 从2train.py拆解Keras LSTM的训练配置3.1 模型结构与参数选择的依据2train.py中的模型结构最可能是三层到四层一个输入层一个或两个LSTM层最后接全连接层输出。第一层LSTM通常设置return_sequencesTrue这样能输出完整的隐藏状态序列给下一层最后一层LSTM用return_sequencesFalse只保留最后那个时间步的隐藏状态输入到稠密层做回归输出。损失函数的选择上回归任务默认用mean_squared_error但金融收益率预测里MSE有个问题——它对大误差的惩罚是平方级别的而金融数据里的极端值如单日跌5%往往就是那几次关键的波动MSE会让模型过度关注这些极端样本忽略大部分普通交易日的模式。MAEmean_absolute_error对异常值更鲁棒但梯度在零点不可导训练后期收敛慢。折中方案是用Huber loss或者像代码里那样直接用MSE也问题不大因为归一化之后极端值的影响被压缩了。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam def build_lstm(lookback15, n_features1): model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(lookback, n_features)), Dropout(0.2), LSTM(units32, return_sequencesFalse), Dropout(0.2), Dense(units16, activationrelu), Dense(units1, activationlinear) ]) model.compile( optimizerAdam(learning_rate0.001), lossmean_squared_error, metrics[mae, root_mean_squared_error] ) return model参数含义这里要说明白units64和units32是LSTM隐藏状态的维度。第一个LSTM层用64维第二个用32维逐层降维是为了让网络从64维的中间表示中提炼出最重要的32维特征避免过拟合。这两个值不是越大越好金融时序样本量通常只有几千条hidden size超过128之后模型容量过剩训练集Loss降得很低测试集却一塌糊涂。Dropout(0.2)是训练时随机丢弃20%的神经元等效于训练多个子网络的集成是防止过拟合最直接的招数。注意Dropout在LSTM里有两种模式recurrent_dropout作用在循环连接上dropout作用在输入连接上。上面代码用的Dropout层是作用在输出上的如果数据量更小可以换成LSTM(..., dropout0.2, recurrent_dropout0.1)。activationlinear是因为做回归任务输出层的激活函数必须是线性的不能用sigmoid或tanh把输出限制在固定范围里。learning_rate0.001是Adam优化器的常见默认值。如果训练Loss震荡不收敛可以降到0.0003或0.0001。实际上训练时用ReduceLROnPlateau回调逐步降低学习率效果更好。3.2 回调函数与训练日志记录logs目录下按品种分了三组train和validation子目录每组保存了训练过程的Loss曲线数据。这说明2train.py里配置了TensorBoard回调或CSVLogger回调。TensorBoard回调的典型配置是记录训练集和验证集的Loss曲线方便后面在浏览器里观察过拟合时机——验证集Loss开始上升到拐点附近就是该早停的位置。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue), ModelCheckpoint( filepathmodels/model_lstm_{}.tf.format(hg), monitorval_loss, save_best_onlyTrue ), ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size32, callbackscallbacks, verbose1 )EarlyStopping的patience15意思是连续15个epoch验证集Loss不改善就停止训练。这个参数值要根据数据集大小调数据越少模型越容易过拟合patience应该越小5到10数据量大时可以放宽到20以上。restore_best_weightsTrue很关键它会把模型权重回滚到验证集Loss最低的那个epoch而不是用最后一步的权重。不设这个参数ModelCheckpoint虽然保存了最佳模型但内存里的模型是最后一轮的状态后面model.save()保存的也是这个终态效果会差一截。项目里的模型文件以.tf.index和.tf.data-00000-of-00001的格式保存这是TensorFlow 2.x SavedModel格式的目录结构。加载时用tf.keras.models.load_model(models/model_lstm_hg.tf)可以直接恢复模型结构和权重不需要重新构建。三个品种化工hg、有色ys、贵金属gjs各自有独立的模型文件这印证了前面的分析——每个品种单独训练。从文件列表看没有model_lstm_hg.tf.index对应的checkpoint文件说明ModelCheckpoint保存的是TensorFlow格式不是HDF5格式。这两种格式在加载方式上没有区别但SavedModel格式自带计算图跨环境部署时更稳定不容易因为keras版本不同导致custom_objects报错。3.3 模型训练曲线怎么读logs目录下训练曲线的图片给出了train和validation两套曲线。正常情况下两条曲线都下降验证集Loss略高于训练集Loss。如果验证集Loss降到某个值后转头向上而训练集还在继续下降说明模型开始死记训练集的特征这就是过拟合发生的时点。金融时序数据比图像数据更容易过拟合原因是样本之间有时间相关性不像图像样本是独立均匀分布的。这里有个细节值得留意如果用原始的随机切分方式把连续时间序列切成训练集和验证集验证集里会出现紧挨着训练集的样本模型相当于偷看了未来验证集Loss会异常低但泛化能力差。正确做法是在1data_create.py里按时间顺序切分——前80%的时间点做训练后20%做验证。从项目的目录结构看训练曲线横坐标是按epoch数画的没有标注时间对齐信息所以最好在2train.py里确认一下数据切分是否基于时间顺序。4. 多品种预测实战与评价指标对比4.1 数据加载与测试集分割方式data目录下的pre_data.csv和各个品种的data_*.xlsx是两种数据形态的原始文件。pre_data.csv可能是合并后的全品种数据集列结构可能是日期、品种代码、价格。y_*.npy和test_y_*.npy的配对关系是训练和测试阶段的标签数据——y_后缀对应训练集的收益率真实值test_y_后缀对应测试集的收益率真实值。打开3pred_test.py加载逻辑应该是先读Excel文件、做收益率转换、切窗口然后从models目录加载对应品种的模型权重。这段流程在代码里体现为三层结构数据层负责把Excel和npy文件读成Numpy数组模型层负责load_model评估层负责把预测结果从归一化空间逆变换回原始收益率空间。import numpy as np import tensorflow as tf from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate_model(model_path, X_test, y_test_scaled, scaler): model tf.keras.models.load_model(model_path) pred_scaled model.predict(X_test, verbose0).flatten() # 逆变换回原始收益率空间 pred_ret scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() y_test_ret scaler.inverse_transform(y_test_scaled.reshape(-1, 1)).flatten() mae mean_absolute_error(y_test_ret, pred_ret) rmse np.sqrt(mean_squared_error(y_test_ret, pred_ret)) # 方向准确率预测符号与真实符号一致的比例 direction_acc np.mean(np.sign(pred_ret) np.sign(y_test_ret)) return { MAE: mae, RMSE: rmse, Direction_Accuracy: direction_acc, Pred_Returns: pred_ret, True_Returns: y_test_ret }这段代码补全了一个项目里可能没有但应该有的模块。scaler.inverse_transform这一步是整个评估过程中最容易出错的地方——如果在1data_create.py里做归一化时用的是全量数据拟合的scaler那么这里逆变换出来的预测值会被压缩或拉伸MAE和RMSE会系统性偏小给人模型很准的错觉。代码里的Direction_Accuracy是方向准确率它比MAE更有交易指导意义——预测收益率符号正确才能做出正确的做多或做空决策。4.2 三个品种的评估结果解读项目里三组测试集结果和拟合曲线图分别对应化工、有色、贵金属。从典型的LSTM收益率预测实验经验看结果会呈现以下规律具体数值需要跑完代码看输出表现最好的通常是波动率中等的品种。贵金属的收益率序列波动率低、趋势性强LSTM容易学到均值回归的模式MAE会相对小RMSE也会比较低。有色的趋势性介于两者之间但交易时间受外盘影响大隔夜跳空多模型预测的踩不准。化工品波动率高、日内噪声大LSTM会倾向于预测一个接近零的收益率因为这样MSE最小——这就是模型学聪明了但没有任何交易价值的典型表现。判断模型是否失效不要只盯着MAE。比较合理的做法是拿预测收益率和真实收益率做一个简单的交易回测如果预测收益率为正就做多为负就做空统计策略的累计收益率和最大回撤。如果策略累计收益率的走势跟基准买入持有差不多说明LSTM没有捕获到有效信息规律的波动被当成噪声丢弃了。模型的test_y_*.npy文件里已经存了真实测试数据做这个回测不需要额外准备数据直接把预测结果和真实结果对齐就能画曲线。4.3 预测曲线与真实曲线错位的原因模型训练曲线和收益率拟合曲线的图片里展示的拟合效果细看往往会发现预测曲线比真实曲线平滑一些几个尖峰位置对不上或者幅度差很多。这背后是LSTM回归模型天然的问题训练时用MSE作为损失函数最优预测是条件期望E[y_t | x_{t-1}, ..., x_{t-lookback}]条件期望天然会把预测拉向序列的均值所以尖峰和暴跌会被系统性地低估收益率分布的高峰度特征没有被完整还原。针对这个问题的常用优化手段有两个。第一个是把损失函数从MSE换成pinball loss也就是分位数回归的思路分别预测收益率分布的中位数或上下分位点。这样能看到不确定性区间比纯点预测信息量丰富得多。第二个是直接在损失函数里加方向惩罚项比如当真实收益率为正而预测为负时损失翻倍让模型为方向错误付出更高代价。5. 模型加载推理与MinMaxScaler逆变换的排错实战最后这一节说一个实操中几乎必踩的坑模型保存的是归一化空间的预测但你要的是原始收益率逆变换这一层处理错了整个预测流程就废了。MinMaxScaler_model.pkl保存的scaler对象除了data_min_和data_max_之外还有一个容易忽略的属性scale_和min_。逆变换的公式是X_orig X_scaled * (data_max_ - data_min_) data_min_这个公式等价于先乘scale_再加min_。如果你把scaler重新fit到了某段数据上再去做inverse_transform得到的结果就不是原始收益率了。三个品种如果共用同一个scaler测试集的逆变换一定走同一个pkl这在代码里要保持一致。加载模型做推理时还有个输入形状的问题。2train.py训练时用的输入形状是(batch_size, lookback, features)预测时如果你读到的测试数据是二维的(samples, lookback)需要先做X_test.reshape(-1, lookback, 1)再传入模型。keras的模型对输入形状是强校验的形状不匹配会直接报错。更隐蔽的问题是如果训练时用的是多个特征比如不止收益率还叠加了成交量、波动率等因子那么特征数和特征顺序不能变拼接的时候要按1data_create.py里的顺序来。import pickle import numpy as np def load_and_predict(model_path, scaler_path, X_test_raw): # 加载scaler和模型 with open(scaler_path, rb) as f: scaler pickle.load(f) model tf.keras.models.load_model(model_path) # 检查输入维度 if len(X_test_raw.shape) 2: X_test X_test_raw.reshape( X_test_raw.shape[0], X_test_raw.shape[1], 1 ) else: X_test X_test_raw # 预测并逆变换 pred_scaled model.predict(X_test, verbose0) pred_original scaler.inverse_transform(pred_scaled) # 如果保存的是价格而你要的是收益率这里还要再做一次差分逆运算 if pred_original.shape[1] 1: pred_returns pred_original.flatten() return pred_returns最后的建议是验证阶段至少要做两件事第一把预测结果和test_y_*.npy直接相减计算残差的均值——残差均值如果显著偏离零说明逆变换有系统性偏差第二检查model_lstm_*.tf目录下的checkpoint文件用SavedModel加载时如果报错提示找不到variables目录说明文件传输不完整三个品种的模型文件大小是相近的如果某一个明显偏小很可能损坏。验证通过后把预测收益率序列按日期对齐导出到CSV配合实际的买入持有策略对比才能判断这套系统在你的数据上到底有没有超额收益能力。本文还有配套的精品资源点击获取
返回列表