简介:基于 LSTM 的时序收益预测系统代码包,适合 Python 与深度学习初学者,以及希望快速上手金融时序建模的开发者。项目围绕大宗商品(化工、有色、贵金属)价格数据展开,覆盖数据预处理、模型构建、训练、预测与效果评估;操作文档中介绍了 LSTM 输入门、遗忘门、输出门等门控机制,并涉及 Adam 优化器、均方误差损失、MAE/RMSE 评估指标,能够帮助读者理解 RNN/LSTM 建模流程和时序数据切分方法。资源共 55 个文件,除 Python 脚本外,还包含训练好的 TensorFlow 模型文件、NumPy 数据、Excel 行情数据、训练日志、实验截图与操作说明文档,压缩包大小约 2.27 MB。目前已有 2912 人学习。借助这套代码,读者可以拿到可复现的完整建模过程,既能跟随文档逐步搭建实验,也能参考图表快速对比化工、有色、贵金属三类品种的拟合效果,为后续调整参数或扩展到其他序列预测任务提供基线,对金融时序预测实验和课程设计具有直接参考价值。
1. LSTM时序收益预测:这套代码包解决的不只是股价预测问题
拆开这份「基于LSTM的股票预测系统.rar」,里面不是一堆孤立的模型文件,而是一条按数据、训练、预测三脚本串起来的完整流水线。它的预测对象是化工、有色、贵金属三个行业的大宗商品收益率,而不是直接预测价格,这个设计本身就绕开了金融序列非平稳的坑。适合两类人:一类是做LSTM时序预测相关毕设、需要完整工程参考的学生;另一类是正在摸索时间序列预测、想直接拿到可复现项目的从业者。对我来说,它最有价值的地方在于数据准备、模型保存、反归一化这些环节的工程处理——正是这些细节决定了模型训练完能不能真正落地使用。
2. 数据与预处理:从xlsx到npy,1data_create.py到底做了什么
2.1 数据形态与预测目标:为什么用收益率而不是原始价格
拿到资源后先看data目录,里面有三份Excel:data_化工.xlsx、data_有色.xlsx、data_贵金属.xlsx,另外还有一份大宗商品指数.xlsx。这说明数据源是三个行业各自的指数或价格序列,不是单只股票。核心设计在于预测目标——收益率而不是价格序列。金融价格序列通常是非平稳的,直接拿原始价格喂给LSTM,模型很容易学到「昨天价格高,今天预测价格也高」这种伪规律,训练集loss很低,测试集一测就露馅。收益率序列则相对平稳,更接近LSTM擅长的模式学习范畴。
这份资源里三个行业的数据是分开存放的,y_.npy和test_y_.npy分别对应三个行业的标签数据。1data_create.py这个名字暗示了它的职责:把xlsx原始数据转成模型能吃的npy格式。整个预处理流程我在复现时重构了核心逻辑,大致是读取Excel、计算收益率、做滑动窗口、归一化、切分训练测试集、保存npy。
# 1data_create.py 核心逻辑(按资源内流程重构) import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler import pickle def load_and_preprocess(excel_path, column_name='close'): # 读取原始数据,这里以收盘价序列为例 df = pd.read_excel(excel_path) # 计算收益率:当前价格相对前一期的变化率 df['return'] = df[column_name].pct_change() # 删除首行空值 df = df.dropna().reset_index(drop=True) return df['return'].values.reshape(-1, 1) def create_sequences(data, window=20): X, y = [], [] for i in range(len(data) - window): # 用过去window期收益率预测下一期收益率 X.append(data[i:i+window]) y.append(data[i+window]) return np.array(X), np.array(y) # 以化工行业为例 returns = load_and_preprocess('data_化工.xlsx') # 归一化:注意只能对训练集fit,测试集用transform scaler = MinMaxScaler(feature_range=(0, 1)) returns_scaled = scaler.fit_transform(returns) X, y = create_sequences(returns_scaled, window=20) # 按时间顺序切分,不能随机打乱 split_idx = int(len(X) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:] # 保存npy与scaler np.save('X_train.npy', X_train) np.save('y_train.npy', y_train) with open('MinMaxScaler_model.pkl', 'wb') as f: pickle.dump(scaler, f)这段代码有三个关键点需要说明。第一,pct_change()计算收益率时,首行会产生NaN必须dropna,否则后面构造窗口会带进空值。第二,MinMaxScaler的fit_transform只做在训练集上,测试集只用transform——这里如果偷懒对全量数据一起归一化,就造成了数据泄漏,测试集的信息提前进入了训练流程。第三,切分时严格按时间顺序,绝对不能用train_test_split的默认shuffle,时序数据一旦打乱顺序,LSTM学到的时序依赖关系就全废了。window参数我用了20,也就是过去20个交易周期预测下一个周期的收益率,这个值对日频数据比较常见,对更低频的数据可以适当调小。
2.2 滑动窗口构造与归一化:两个影响训练结果的细节
滑动窗口是LSTM处理时序数据的基本形态。窗口长度window决定了模型能「回看」多长的历史。窗口太小,模型看不够历史趋势;窗口太大,样本数量减少,还容易把过多噪声带进来。我一般会先用20做个基线,再对比10和30的效果,看验证集loss再定。注意每个样本的shape是(window, 1),1是收益率这一个特征维度。
另一个细节是MinMaxScaler的feature_range我设成了(0,1),也就是把收益率序列压缩到0到1之间。LSTM内部用tanh和sigmoid激活函数,输入范围在0到1附近梯度传播最稳定。如果原始数据是收益率,数值通常比较小(百分之几的量级),不归一化直接训练也能收敛,但归一化后收敛更快、更稳。训练完的scaler一定要pickle保存下来,预测时要靠它把模型输出还原成真实收益率,这个对象在后面的3pred_test.py里还会用到。
2.3 数据去向与文件对应关系:npy、csv各自扮演什么角色
跑完1data_create.py,data目录下会生成或保留多组npy文件:y_化工.npy这类是完整标签集,test_y_*.npy是测试集标签。pre_data.csv这个文件我推测是最终预测结果的汇总表,把三个行业的真实收益率和预测收益率按时间对齐后落盘,方便后续画图和复盘。这种命名习惯在工程里很常见:原始数据不动,预处理产物用npy保存,可读性强的结果用csv导出。
3. 模型构建与训练:2train.py里的LSTM结构与训练策略
3.1 模型结构:LSTM层、Dense层与参数怎么搭配
2train.py负责模型的构建和训练。LSTM是RNN的一种变体,通过输入门、遗忘门、输出门三个门控机制解决传统RNN的梯度消失和梯度爆炸问题。在收益率预测这个场景里,我们关心的是序列的长期依赖——比如过去几期的趋势是否延续,LSTM的门控结构恰好擅长捕捉这类模式。
模型结构我按资源内可能的实现重构如下:
# 2train.py 模型定义核心部分 import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint def build_lstm_model(input_shape, units=64): model = Sequential() # 第一层LSTM,return_sequences=True表示返回完整序列,方便堆叠第二层LSTM model.add(LSTM(units=units, return_sequences=True, input_shape=input_shape)) model.add(Dropout(0.2)) # 第二层LSTM,不返回序列,只返回最后一个时间步的输出 model.add(LSTM(units=units // 2, return_sequences=False)) model.add(Dropout(0.2)) # 全连接层,输出一个值:下一期收益率 model.add(Dense(1)) # Adam是时序预测里最常用的优化器,学习率从1e-3起步 model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='mse', metrics=['mae']) return model model = build_lstm_model(input_shape=(20, 1), units=64) model.summary()两层LSTM加Dropout的结构,比单层LSTM稍重但拟合能力更强。第一层返回完整序列是因为第二层还需要按时间步展开计算。Dense(1)输出层没有激活函数,因为这是一个回归任务,输出范围不限制。损失函数用mse(均方误差),它对大的误差惩罚更重,适合金融收益这种希望避免大偏差的场景。units=64是我起步的推荐值,三个行业如果数据量差异大,可以分别试32和64,看验证集表现再定。
3.2 训练过程:fit参数、早停与模型保存
训练部分的代码涉及几个关键回调。ModelCheckpoint按验证集loss保存最优模型,EarlyStopping在验证集loss连续多轮不下降时提前终止训练,避免过拟合。
# 2train.py 训练部分 checkpoint = ModelCheckpoint( 'models/model_lstm_ys.tf', # 保存为TF格式目录 monitor='val_loss', # 以验证集loss为准 save_best_only=True, # 只保存最优epoch verbose=1 ) early_stop = EarlyStopping( monitor='val_loss', patience=15, # 连续15轮不改善就停 restore_best_weights=True # 恢复最优权重 ) # 训练:epochs设100,实际由early stop控制 history = model.fit( X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[checkpoint, early_stop], verbose=1 )batch_size=32是个稳妥默认值,数据量大可以调64提速,数据小用16防止梯度震荡。patience=15是早停的核心参数——如果验证集loss连续15个epoch都没有创新低,训练就停下。这里有个容易被忽略的细节:restore_best_weights=True必须开,否则early stop触发时模型停留在最后一个epoch的权重,而不是验证集最优的那个epoch。资源里models目录下有三个行业的tf格式模型文件和checkpoint文件,正是ModelCheckpoint保存的结果。日志目录logs下每个行业有train和validation两个子目录,说明训练时还挂了TensorBoard回调,我建议有条件也加上,loss曲线动态看比事后看截图直观得多。
3.3 训练曲线怎么看:模型收敛的三个信号
资源配了多张模型训练曲线截图,覆盖三个行业。看这些图重点看三件事:一是训练loss和验证loss是否同步下降,如果训练降而验证不降,说明过拟合已经开始;二是下降是否平滑,剧烈震荡说明学习率可能偏大;三是最终验证loss是否收敛到平台期。理想状态是两条曲线在几十个epoch后都走平,间隔不大。如果验证loss一直下不去,优先怀疑窗口长度和units,其次是数据质量问题——比如某段时期市场波动异常放大,模型会把噪声当规律学进去。
4. 预测与评估:3pred_test.py如何还原真实收益率
4.1 加载模型与反归一化:顺序出错预测值就全偏了
3pred_test.py是整个流水线的最后一步。它要做的事不是把模型输出直接当作预测值,而是先反归一化。这是一个非常容易翻车的环节。模型在归一化后的数值空间里做预测,输出的0到1之间的数必须经过scaler.inverse_transform才能还原成真实收益率。常见的错误是重新new一个MinMaxScaler再fit一遍测试集——这得到的scaler和训练时的scaler完全不是一回事,反归一化出来的值会整体偏移。
# 3pred_test.py 预测与反归一化核心逻辑 import numpy as np import pickle from tensorflow.keras.models import load_model # 加载训练好的模型与归一化器 model = load_model('models/model_lstm_ys.tf') with open('MinMaxScaler_model.pkl', 'rb') as f: scaler = pickle.load(f) # 加载测试集(这里是示例,脚本里可循环处理三个行业) X_test = np.load('X_test_ys.npy') y_test = np.load('test_y_ys.npy') # 模型预测,输出仍是归一化空间的值 pred_scaled = model.predict(X_test) # 关键步骤:用训练时保存的scaler反归一化 pred = scaler.inverse_transform(pred_scaled.reshape(-1, 1)) y_true = scaler.inverse_transform(y_test.reshape(-1, 1)) # 计算评估指标 mae = np.mean(np.abs(pred - y_true)) rmse = np.sqrt(np.mean((pred - y_true) ** 2)) print(f'MAE: {mae:.6f}, RMSE: {rmse:.6f}') # 保存为csv,便于对比和绘图 import pandas as pd result_df = pd.DataFrame({ '真实收益率': y_true.flatten(), '预测收益率': pred.flatten() }) result_df.to_csv('pre_data.csv', index=False)这段代码里model.predict的输出shape是(num_samples, 1),reshape(-1,1)是为了匹配scaler.inverse_transform的输入要求。MAE和RMSE这两个指标的意义在于:MAE告诉你平均偏差多少,RMSE因为对误差平方求和,会更放大大的偏差,如果RMSE明显大于MAE,说明存在少数预测偏差特别大的点,需要警惕。资源里的pre_data.csv应该就是这一步生成的结果汇总。
4.2 拟合曲线怎么看:贴合是好事,但别只看贴合
资源里有收益率拟合曲线_化工、收益率拟合曲线_有色、收益率拟合曲线_贵金属三张图。这些图通常是测试集上真实收益率和预测收益率的时序曲线对比。看图时有一个规律要知道:LSTM在金融时序上的预测值往往比真实值平滑,且存在滞后。如果图里预测曲线比真实曲线整体右移或更平缓,这不是代码bug,而是模型倾向输出接近前值或均值的保守预测。判断模型好坏不能只看曲线重合度,还要看方向——预测的涨跌方向和真实涨跌方向一致的比例有多少。
4.3 多行业结果对比:为什么有的行业拟合好有的差
三个行业的拟合效果大概率是有差异的。化工、有色、贵金属受不同的供需逻辑驱动,波动特征不同。波动率越小的品种,收益率序列越平稳,LSTM拟合起来越容易;波动率大的品种,比如某段时间受消息面冲击的有色金属,测试集上出现极端值时预测曲线往往会跟不上。资源里三个行业的训练曲线分开放,就是方便分别排查问题。
5. 避坑:时序预测项目的五个常见坑与排查方法
5.1 反归一化结果整体偏移
现象:预测曲线形状和真实曲线几乎一致,但整体上移或下移,数值明显不在一个量级。 原因:预测时新建了一个MinMaxScaler并对测试集重新fit,导致缩放参数和训练时不一致。 解决:训练时把scaler用pickle保存,预测时只load不重新fit。也就是我前面说的——fit_transform只用训练集,测试集和预测集一律只用transform。
5.2 数据泄漏:归一化时混入了测试集
现象:训练时验证集loss很低,模型看起来收敛得很好,但真正上线预测时效果大打折扣。 原因:预处理时先对全量数据fit了scaler,再切分训练测试集,测试集分布信息提前进入了训练环节。 解决:先切分再归一化,或者先fit_transform训练集,再transform测试集。我强烈建议在1data_create.py里就把这一步的顺序固化下来,不要每次写完再改。
5.3 预测值严重滞后,看起来像「昨天的预报」
现象:预测曲线几乎就是真实曲线向右平移了一个周期,滞后感明显。 原因:LSTM在金融数据的保守预测倾向。当序列随机性较强时,模型学到的最优策略往往是输出接近最近观测值,导致预测值滞后一拍。 解决:方向准确率才是这类模型的更重要指标。我还会额外计算「预测方向准确率」,也就是预测值相对上一期真实值的方向是否和真实方向一致,以此判断模型是否学到了有效规律。滞后本身不是灾难,但如果你拿预测值做交易决策,滞后就是致命问题。
5.4 训练loss震荡不降,甚至出现NaN
现象:训练到中途loss开始上下剧烈震荡,或者直接变成NaN。 原因:最常见的是学习率过大导致梯度爆炸,另一种可能是数据里有异常大或异常小的值(比如极端行情下的收益率),归一化后依然影响梯度。 解决:先把学习率从1e-3降到1e-4试跑50轮;同时检查收益率序列里有没有超出合理范围的异常值,比如超过50%的单期变动。
5.5 模型文件损坏或格式不兼容
现象:load_model时报错,提示无法识别模型文件格式。 原因:TF的SavedModel格式在版本间存在兼容性问题。资源里的model_lstm_ys.tf.index和data-00000-of-00001是标准的TensorFlow SavedModel结构,但如果本机TensorFlow版本和训练时差异过大,加载可能报错。 解决:用与训练时相近的TensorFlow版本,或改用只保存权重的.h5格式重新存储。我通常同时保存tf格式和h5格式,一个保兼容,一个保灵活。
6. 进阶:方向准确率验证与三行业模型对比
6.1 方向准确率:比拟合曲线更诚实的指标
对这个场景,方向准确率比MAE更有参考价值——它直接回答「模型能不能判断收益率是涨还是跌」这个投资决策层面的问题。我通常会把测试集里真实收益率绝对变化很小(比如小于0.001)的样本过滤掉,因为接近0的涨跌方向本身没有意义。
# 方向准确率计算示例 import numpy as np def direction_accuracy(y_true, y_pred, threshold=0.001): # 过滤掉变动幅度过小的样本 mask = np.abs(y_true) > threshold y_true_f = y_true[mask].flatten() y_pred_f = y_pred[mask].flatten() # 方向一致:预测方向和真实方向相同 correct = np.sign(y_true_f) == np.sign(y_pred_f) return np.mean(correct) # 假设pred和y_true是反归一化后的结果 acc = direction_accuracy(y_true, pred) print(f'方向准确率: {acc:.2%}')代码里np.sign比较的是正负号,也就是涨跌方向。threshold过滤的作用是排除那些收益率几乎为零的样本,这些样本方向判断本身就是随机游戏。方向准确率大于50%说明模型比抛硬币强,对金融时序模型来说,稳定在55%以上就已经有参考价值了。
6.2 三行业对比与模型选型
做完三个行业的预测后,把MAE、RMSE、方向准确率汇总成一张表。以我的经验,波动率低的品种方向准确率往往更高,这是数据本身的性质,不代表某个行业的模型结构更好。对比的价值在于:如果某个行业效果特别差,可以单独给它调window或units,而不是三个行业共用一个超参数。
用这份资源做毕设或项目落地时,真正值得花时间打磨的是数据预处理和评估体系——模型结构本身已被研究得很成熟,工程上的细节才是拉开差距的地方。我把这套流程跑通后最大的教训是:永远不要让测试集的信息提前泄漏到训练里,也永远不要只看loss曲线就判断模型可用。从那以后我每次跑时序模型,都强制先检查一遍数据切分顺序、归一化方式、反归一化对象,再训练、再预测、再算方向准确率。希望帮到你。
本文还有配套的精品资源,点击获取