
简介面向金融时序分析与深度学习入门者这套基于 Python 与 LSTM 的收益预测系统代码包覆盖从原始数据预处理、模型构建到训练评估与预测的完整流程可用于理解 RNN 变体在股票、大宗商品等序列数据上的建模思路。资源共 55 个文件压缩包 2.27MB主要包括 Python 脚本1data_create、2train、3pred_test、TensorFlow 模型文件、npy/xlsx 数据文件、训练曲线与收益率拟合截图以及操作文档和开题报告目录按 data、models、logs 分区便于按阶段对照学习。已有 2911 人学习使用适合希望结合代码实操掌握 LSTM 时序预测、并参考金融场景应用细节的读者。通过三个脚本可复现数据归一化、模型训练、测试集预测与误差评估等环节多品种实验数据和可视化曲线也能帮助理解模型在不同市场行情下的表现。1. 从 LSTM 到收益预测一套能跑通全流程的代码该怎么用用 LSTM 做时序收益预测最怕遇到两件事一是模型训练时梯度爆炸loss 直接变 NaN白等一晚上二是模型能跑通但预测的收益曲线跟实际值总错开一个身位肉眼看着拟合得挺好一对照方向就发现下单大概率亏钱。这套资源把大宗商品场景下的 LSTM 收益预测全流程走了一遍从原始 Excel 数据到训练好的模型再到收益率拟合曲线都有对应脚本和中间文件。适合两类人想完整跑通 LSTM 时序预测全流程的新手以及手上有行情数据、想改造成收益预测系统的从业者。它把数据预处理、模型训练、预测回测之间的衔接关系讲清楚了而不是只给一段孤立代码。2. 数据侧先行从 xlsx 到 npyLSTM 吃的是滑窗不是整段行情2.1 LSTM 学的是窗口规律不是某一天的涨跌LSTM 的输入不是一根K线也不是一整段历史行情而是固定长度的滑动窗口。模型的任务是给定过去 60 个交易日的收益序列预测下一个交易日的收益。这就是时序预测里最常见的监督化过程——把时间序列切成 (窗口, 标签) 的样本对。为什么切窗口而不是直接把整段序列塞进去因为 LSTM 虽然能处理长序列但窗口太大会引入大量与预测目标无关的噪声训练时间也会成倍增加。常见做法是用 20 到 60 个交易日作为 lookback 窗口。这份代码包里用的是 60对大宗商品日频数据来说差不多是过去一个季度的交易节奏既能捕捉中期趋势又不至于把太久远的结构性强加给当前状态。收益序列还有一个特殊性它比价格序列更适合做回归目标。价格是非平稳的十年前的价格水平和今天没有可比性模型很难从绝对价格里学到稳定规律而收益是价格的一阶差分近似平稳均值回归特征明显LSTM 在这种序列上学到的模式更容易迁移到未来。所以数据处理的第一步就是把价格换算成收益率再用收益率去构造滑窗样本。2.2 1data_create.py 的四步读表、算收益、滑窗、切分打开1data_create.py核心流程可以拆成四步读 Excel、算收益率、滑窗构造 X/y、按时间切分并归一化。这套逻辑在任何一个时序预测项目里都能复用我把代码按常见做法整理出来# 1data_create.py 核心流程以化工数据为例 import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler import joblib df pd.read_excel(data_化工.xlsx, parse_dates[日期]) df df.sort_values(日期).reset_index(dropTrue) # 1. 用收盘价算日收益率pct_change 会把第一个值算成 NaN直接丢掉 df[ret] df[收盘价].pct_change() df df.dropna(subset[ret]) LOOKBACK 60 # 用过去 60 个交易日的收益序列预测下一个交易日 # 2. 滑窗构造样本 X, y [], [] for i in range(LOOKBACK, len(df)): X.append(df[ret].values[i - LOOKBACK:i]) y.append(df[ret].values[i]) X np.array(X).reshape(-1, LOOKBACK, 1) y np.array(y).reshape(-1, 1) # 3. 按时间顺序切分前 80% 训练后 20% 做验证/测试 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] # 4. 归一化scaler 只在训练集上 fit测试集只 transform scaler MinMaxScaler(feature_range(-1, 1)) X_train_scaled scaler.fit_transform( X_train.reshape(-1, LOOKBACK)).reshape(-1, LOOKBACK, 1) X_test_scaled scaler.transform( X_test.reshape(-1, LOOKBACK)).reshape(-1, LOOKBACK, 1) np.save(X_train_hg.npy, X_train_scaled) np.save(X_test_hg.npy, X_test_scaled) np.save(y_train_hg.npy, y_train) np.save(y_test_hg.npy, y_test) joblib.dump(scaler, MinMaxScaler_hg.pkl)这段代码有几个关键参数需要留意。LOOKBACK 60决定了每个样本的时间跨度它直接影响模型能记忆多长的依赖关系后面训练和预测时所有脚本必须用同一个值。pct_change()计算的是百分比收益率它是无量纲的天然去掉了价格绝对水平的影响。reshape(-1, LOOKBACK, 1)这一步把样本整理成 LSTM 需要的三维结构样本数、时间步数、特征维度这里特征只有收益率一个维度所以是 1。切分时按时间顺序而不是随机打乱是金融时序和普通分类任务最大的区别。随机打乱会破坏时间依赖关系让模型提前看到未来数据。训练集和测试集的边界就是时间边界训练集只包含历史测试集只包含未来这样才能保证评估结果真正反映模型的泛化能力。2.3 归一化只该拿训练集统计量scaler 文件是后悔药MinMaxScaler_model.pkl这个文件容易被忽略但它比模型权重还重要。归一化用的是 MinMaxScaler把数据压缩到 [-1, 1] 区间这对 LSTM 的 tanh 激活函数很友好能让梯度保持在一个合理范围内。问题是scaler 的 fit 必须只发生在训练集上。如果用全量数据的最大值和最小值去归一化测试集的信息就提前泄露给了训练过程。模型的训练目标变成了拟合一个已经知道未来边界的数值范围测试误差会被严重低估。这就是时序预测里最隐蔽的数据泄露。更麻烦的是预测阶段如果有新数据进来它的值可能超出原来训练集的 min/max 范围归一化后会被截断所以真实项目里还要处理边界外样本。把这套代码跑完你会发现pre_data.csv是数据准备的中间产物三个标的的收益序列被拼在了一张表里方便统一查看。我的做法是每个标的单独算收益率、单独做滑窗、单独归一化因为化工、有色、贵金属的波动区间差异很大共用一套 scaler 等于强行让三个不同市场共享同一个尺度。3. 训练侧核心两层 LSTM 加 dropout收益序列的收敛配置怎么定3.1 模型结构为什么是 64 → 32 的两层 LSTM2train.py里构建的模型结构很典型第一层是 64 个单元的 LSTM设置return_sequencesTrue因为还要把完整的隐藏状态序列传给下一层第二层是 32 个单元的 LSTMreturn_sequencesFalse只输出最后一个时间步的隐藏状态再接一个 16 个神经元的全连接层和一个单输出回归层。两层 LSTM 之间夹着 dropout 层丢弃率 0.2。这个结构在日频金融收益预测里是一个很稳的起点。收益序列本身信噪比低模型不需要太深三层以上的 LSTM 在小样本金融数据上几乎必然过拟合。64 维隐藏状态用来捕捉窗口内的大幅波动32 维做特征压缩dropout 0.2 既起到正则化作用又不至于让信息流动被切断。注意第一层 LSTM 的return_sequencesTrue很关键——如果设成 False第二层 LSTM 的输入就只有最后一个时间步的输出了序列信息直接丢了大半。层类型单元数/参数说明1LSTM64, return_sequencesTrue保留完整时间步输出2Dropout0.2随机丢弃部分神经元3LSTM32, return_sequencesFalse压缩为最后一个隐藏状态4Dropout0.2继续正则化5Dense16, ReLU非线性映射6Dense1, 线性回归输出不加激活这里有一个容易被忽视的细节输出层直接用线性激活而不是 tanh 或 relu。因为收益的预测值可正可负幅度也不固定线性输出让模型自己去学输出范围而不是被激活函数限制住。这一层加任何激活函数都是多余的约束。3.2 训练配置MSE 损失、Adam 优化器和 early stopping 的组合模型编译和训练部分同样有规律可循。损失函数用均方误差 MSE优化器用 Adam这个组合在回归类时间序列预测里最省心。MSE 对大的预测误差惩罚更重能让模型优先拟合那些幅度大的收益变动Adam 自带自适应学习率不用手动做学习率衰减。# 2train.py 训练流程以某标的为例 import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint LOOKBACK 60 X_train np.load(X_train_hg.npy) y_train np.load(y_train_hg.npy) X_val np.load(X_test_hg.npy) y_val np.load(y_test_hg.npy) model Sequential([ LSTM(64, return_sequencesTrue, input_shape(LOOKBACK, 1)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(16, activationrelu), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue) checkpoint ModelCheckpoint( models/model_lstm_hg.tf, monitorval_loss, save_best_onlyTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop, checkpoint])patience10的意思是验证集 loss 连续 10 个 epoch 不下降就停止训练restore_best_weightsTrue会把模型参数回滚到验证集最优的那一步。这是防止过拟合的保底机制。ModelCheckpoint只在val_loss更好的时候保存模型所以models/目录里最终留下的.tf文件就是整个训练过程中泛化能力最好的版本而不是最后一个 epoch 的版本。batch_size 选 32在行情数据这种规模下是合理默认值。太小会让梯度震荡剧烈太大又会拖慢收敛。如果训练数据只有几千条样本epochs100配合 early stopping 一般会在 30 到 50 个 epoch 内收敛。logs 目录里 ys、hg、gjs 三个子目录对应三个标的的训练日志可以用 TensorBoard 打开查看 loss 和指标曲线的详细变化tensorboard --logdir logs3.3 三个标的为什么拆成三个模型化工、有色、贵金属的数据分布差异压缩包里的models目录有三个独立的模型文件model_lstm_hg.tf、model_lstm_ys.tf、model_lstm_gjs.tf对应化工、有色、贵金属。这三个标的虽然都属于大宗商品但驱动因素完全不同化工品受原油价格和产能周期影响有色金属跟宏观需求和库存周期高度相关贵金属则更多反映避险情绪和实际利率。它们的收益序列波动率、自相关结构都有明显差异。如果把三个标的的数据混在一起训练一个模型模型学到的只能是三者的平均规律对任何一个单一品种来说都不够精准。拆开训练的一个额外好处是可以针对每个标的分开调参。比如贵金属数据波动大可以把 lookback 调长一些来平滑噪声化工品数据受事件驱动明显dropout 可以调高一点增强鲁棒性。另一个容易被忽略的问题是loss曲线的对比。三个标的的val_loss绝对值不能直接放在一起比因为不同品种的收益波动区间不同MSE 的量级天然不一样。对比时应该看各自验证集 loss 是否收敛、训练集和验证集的差距是否在可接受范围内而不是看谁的数字更小。4. 预测与回测从 SavedModel 加载到画出拟合曲线差一步就翻车4.1 加载模型前先确认文件结构.tf 目录和 checkpoint训练完成后保存的是 TensorFlow SavedModel 格式不是一个单一文件。models目录里每个标的都是一个独立的命名空间拿model_lstm_hg.tf来说它下面包含.index、.data-00000-of-00001和checkpoint文件。加载这个模型不需要逐文件处理load_model会自动识别目录结构# 3pred_test.py 预测与还原以化工为例 import numpy as np import joblib from tensorflow.keras.models import load_model name hg model load_model(fmodels/model_lstm_{name}.tf) scaler joblib.load(fMinMaxScaler_{name}.pkl)这里有一个常见坑如果你在训练完模型之后、保存之前没有重新编译模型或者模型里包含自定义层直接load_model可能会报错。这套代码用的是标准 Keras 层SavedModel 里会序列化完整的网络结构和优化器状态所以可以放心加载。加载后可以用model.summary()确认网络结构没有变化再用model.predict()做推理。4.2 反归一化是预测的最后一步也是最容易漏的一步训练时归一化只用训练集做了 fit预测时测试集数据在送入模型前也走了 transform所以模型输出的预测值处于 [-1, 1] 的归一化空间里必须用之前保存的 scaler 做 inverse_transform才能得到真实收益量纲的预测值。跳过这一步预测曲线会和真实收益曲线在数值上完全对不上。X_test np.load(fX_test_{name}.npy) y_test np.load(ftest_y_{name}.npy) # 原始量纲的实测收益 pred_scaled model.predict(X_test, verbose0) pred scaler.inverse_transform(pred_scaled) # 还原到真实收益率 # 保存预测结果和 test_y 对齐后就能画拟合曲线 np.save(fpred_{name}.npy, pred)注意这里scaler.inverse_transform的输入形状要和训练时一致。训练时 X 的最后一维是(LOOKBACK, 1)但 y 的形状是(样本数, 1)而scaler是在 X 上 fit 的不能直接拿它去 transform y。正确做法是预测结果的结构和 y 一致如果 y 没走缩放那 y 的测试标签本身就是真实收益量纲只要把模型输出的归一化预测值反变换回来即可。这一点搞错了整个回测曲线的对比就没有意义了。4.3 训练曲线和收益率拟合曲线怎么读loss 收敛与滞后实验截图里有几组关键图片模型训练曲线、收益率拟合曲线分别覆盖化工、有色、贵金属三个标的。训练曲线看的是 loss 走势——训练集和验证集的 loss 都在下降且验证集没有明显反弹说明模型收敛正常如果训练集 loss 一直降、验证集 loss 先降后升就是过拟合的典型信号需要提高 dropout 或增大数据量。收益率拟合曲线是预测值和真实收益的时间序列叠图。看清楚这张图有两个重点。第一是趋势方向预测线能不能跟随真实收益的大波段在拐点处是否同步转向第二是幅度预测值通常会比真实值更平缓因为 LSTM 在回归任务里倾向于输出条件均值天然会抹平极端波动。一个值得警惕的现象是滞后。如果预测线和真实线长得一模一样但整体向右偏移了几个交易日说明模型没有在预测未来而是在复述过去。这种情况在做滑窗预测时特别常见——因为收益序列本身有自相关性模型学到的最优策略就是输出前一天的收益。判断方法也很直接把预测序列和真实序列做时序对齐看它们在拐点处的对应关系如果预测拐点总是晚于真实拐点出现基本可以断定模型学到的是滞后关系而不是因果关系。5. 避坑指南时序收益预测里最常见的四个翻车现场5.1 训练集 loss 极低、测试集炸掉归一化泄露现象训练集上 MSE 很低曲线拟合得几乎完美但换到测试集上预测误差突然放大好几倍画出来的拟合曲线严重偏离真实值。原因大概率是归一化时对全量数据做了 fit。MinMaxScaler 用训练集加测试集的最大最小值计算缩放参数等于让模型在训练阶段就偷看了测试集的数值分布。测试集信息混进训练过程后训练损失会被虚假压低一到真预测就原形毕露。解决检查数据创建脚本里 scaler 的调用位置。fit_transform只能用在训练集上测试集数据只能走transform。如果你是在 1 和 2 之外另写了脚本每次都现用现 fit就会埋下这个雷。正确做法是训练阶段把 scaler 用joblib.dump存下来预测阶段用同一个文件还原。5.2 训练 loss 降了但验证 loss 不降lookback 和学习率打架现象训练集 loss 稳步下降验证集 loss 一直在某个数值附近横盘甚至缓慢上升模型看起来根本没学到可泛化的规律。原因有两个参数在打架。一是 lookback 窗口太长模型把过去一个季度的每个微小波动都当成特征验证集上的表现自然不佳二是 Adam 的默认学习率在某些数据分布下偏激进前几个 epoch 就把模型推到了过拟合区域后面再怎么训练都在原地绕。解决先把 lookback 缩短到 30 到 40 试一轮观察验证集是否开始跟着训练集下降。如果仍然横盘把学习率从默认的 1e-3 降到 1e-4同时把 dropout 从 0.2 提到 0.3。调参时一次只改一个变量不然永远定位不到问题根源。5.3 模型加载后预测结果对不上实验截图忘了反归一化或随机性现象用load_model加载权重后跑预测得到的预测数值范围和实验截图里的拟合曲线差了很远甚至量级都不一样。原因两个可能。一是直接用模型输出当最终结果没做 inverse_transform预测值被压缩在 [-1, 1] 区间里二是模型里的 Dropout 层在推理模式下仍然生效——如果加载模型后调用了model.train_on_batch或某些自定义推理逻辑dropout 会随机丢弃神经元每次预测结果都不一样。解决预测前显式调用一次模型构建确保走推理路径。model.predict本身会冻结 dropout但如果你手动操作了中间层的输出就要确认没有触发训练逻辑。收敛检查的方法是对同一批测试数据连续预测两次如果两次结果不一致说明有随机性混进来了优先排查 dropout 和未固定随机种子的问题。5.4 拟合曲线好看但方向老错MSE 不代表方向准确率现象收益率拟合曲线上预测线和真实线看起来贴得很近MSE 数值也不高但如果统计预测收益的符号方向和真实方向是否一致准确率可能只有 50% 出头跟抛硬币差不多。原因MSE 衡量的是数值误差不是方向准确率。LSTM 在回归任务里学到的是条件均值而条件均值在收益序列这种高噪声数据里趋向于零也就是说模型输出的预测值会整体向均值收缩。收缩后的数值虽然和真实值的平均距离不大但符号可能大量错误——预测出一个很小的正收益实际却是负收益。解决评估时不要只看 MSE 和拟合曲线单独计算方向准确率。把预测值和真实值都取符号然后比较相同位置上的符号是否一致。如果方向准确率持续低于 55%这个模型对实际交易的参考价值就非常有限与其用它来判断方向不如只把它当作波动强度估计。6. 预测结果落地先过方向准确率和滞后性这两道关模型能跑通只是第一步真正决定你能不能拿它做决策辅助的是方向准确率和滞后性。下面这段代码把这两道关的检查硬编码进了我习惯的验证流程# 预测结果落地验证方向准确率 滞后性 import numpy as np pred np.load(pred_hg.npy).flatten() y_true np.load(test_y_hg.npy).flatten() # 方向准确率预测收益符号与真实收益符号一致的比例 direction_acc np.mean(np.sign(pred) np.sign(y_true)) print(fDirection Accuracy: {direction_acc:.2%}) # 滞后性检验依次平移预测序列看相关系数峰值出现在哪个偏移量 best_corr, best_shift -np.inf, 0 for shift in range(0, 10): if shift 0: r np.corrcoef(y_true, pred)[0, 1] else: r np.corrcoef(y_true[shift:], pred[:-shift])[0, 1] if r best_corr: best_corr, best_shift r, shift print(fBest correlation at shift{best_shift}, r{best_corr:.3f})滞后性检验的算法逻辑并不复杂把预测序列和真实序列做错位对齐逐一测试预测结果比真实值晚 0 到 9 个交易日时相关系数哪个最大。如果峰值出现在 shift0说明预测和当前状态同步模型没有滞后如果峰值出现在 shift2 或更大说明预测结果表面吻合实际只是把前几天的走势搬家到了后面这种预测不能用于决策。方向准确率的及格线取决于你的使用场景。单日收益预测方向准确率能稳定在 55% 以上就已经有交易参考价值了低于 52%基本就是噪声。看实验截图里的收益率拟合曲线时务必同时跑一遍这两个检查因为肉眼判断曲线的贴合度很容易被好图片误导。从那以后我每次拿到任何一份时序预测代码都强制自己先跑方向准确率和滞后性检验再决定要不要相信它的拟合曲线。模型输出好看是基本功能说清楚它到底在预测什么、什么时候失效才是拿来用的前提。希望帮到你。本文还有配套的精品资源点击获取