简介:这是一份面向序列预测学习者与初学者的多输入单输出LSTM模型完整实现,以股票收盘价预测为示例,展示了如何融合开盘价、最高价、最低价等多维特征进行建模,适用于时间序列分析、金融预测、气象预报等需要对序列数据做前瞻判断的场景。压缩包内共10个文件,主要包含Jupyter Notebook源代码、CSV训练数据集、YAML项目说明及若干辅助压缩包,整体大小约1.05MB,目录结构清晰,便于直接运行与二次开发。代码覆盖数据预处理、滑动窗口样本构造、LSTM网络搭建、训练验证与结果可视化全流程,并附有数据来源与参数选择说明,可帮助理解LSTM如何通过门控机制缓解梯度消失、有效捕捉长期依赖,从而输出单一数值预测。已有618人学习下载,适合希望系统掌握多输入单输出建模思路的数据科学初学者和相关方向开发者。
1. 多输入单输出LSTM预测股票收盘价:这不是黑匣子,是能复现的完整流程
拿到这份lstmPred.ipynb的时候,我第一反应是终于有人把「多输入单输出」这件事讲明白了。很多开源项目里的LSTM时间序列预测,输入就一个特征列,跑出来的效果图看着漂亮,换到自己的数据上就翻车。这个项目不一样——它用开盘价、最高价、最低价三个历史特征,去预测下一个交易日的收盘价。三个特征进,一个数值出,这就是多输入单输出的标准形态。
我把它完整拆了一遍,从train.csv的数据预处理,到滑动窗口怎么切,到Keras里LSTM层的参数怎么设,再到训练完怎么验证结果可信,每一块都跑通并做了边界测试。这套流程不只能预测股票,换成电力负荷、交通流量、设备温度预测,结构完全通用,换数据换特征列就能用。下面把我拆包过程中的关键代码、参数依据和踩过的坑,一条条写出来。
2. 数据预处理与滑动窗口构造:从train.csv到LSTM能吃的三维张量
这是整个项目里最容易被跳过、也最值得细看的部分。LSTM 要求输入是(样本数, 时间步长, 特征数)的三维结构,而 train.csv 是典型的二维表——一行一个交易日。中间这一步转换,决定了模型学到的是「序列规律」还是「噪声记忆」。
2.1 先看数据形态与缺失值:别急着归一化
拿到 train.csv 先不要着急写模型,用 pandas 读进来,看一眼数据到底是什么结构。我在 Notebook 里通常先跑这几行:
import pandas as pd df = pd.read_csv('train.csv') print(df.shape) print(df.head()) print(df.isnull().sum())输出结果一般是这样:每一行是一个交易日,列包含date、open、high、low、close。shape 大概在(300~2000, 5)左右,缺失值一列一列地检查,日期列确认是按时间升序排列的。注意:LSTM 依赖的是时序关系,数据排序错了,后面全废。如果你的数据本身没有按日期排序,先执行一次df = df.sort_values('date'),这是很多初学lstm 数据集处理时容易忽略的动作,排序这一步做错,模型训练得再漂亮,预测出来也是错位的。
2.2 归一化的位置与数据泄漏:这是第一道真正的坑
接下来是关键一步:归一化。这个项目里用的是最常规的 MinMaxScaler,代码就是下面这段:
from sklearn.preprocessing import MinMaxScaler feature_cols = ['open', 'high', 'low'] target_col = 'close' scaler_x = MinMaxScaler() scaler_y = MinMaxScaler() # 只对特征列做缩放 scaled_features = scaler_x.fit_transform(df[feature_cols].values) # 目标列单独缩放 scaled_target = scaler_y.fit_transform(df[[target_col]].values)逻辑说明:分两个 scaler 分别处理特征和目标,是因为预测完还要用scaler_y.inverse_transform()把数值还原成真实的收盘价,特征列和目标列的数值范围不同,混用一个 scaler 会出现还原困难。参数上,MinMaxScaler 默认是压缩到[0, 1],对 LSTM 这种用 tanh 或 sigmoid 激活函数的网络来说是最合适的区间。如果数据里有极端值,可以改成MinMaxScaler(feature_range=(-1, 1)),配合 tanh 效果更好。
注意:
fit_transform只能用在训练集上。如果整个数据集一起 fit,测试集的信息在训练阶段就被模型「看到」了,这叫数据泄漏,会让验证结果虚高。后面避坑章节我会单独展开。
2.3 滑动窗口切片:look_back 参数怎么设
归一化完成之后,要把二维表切成「多个连续时间窗口」。这里就是多输入单输出模型的形状核心——用过去look_back天的三个特征,预测未来一天的收盘价。
import numpy as np def create_sequences(features, target, look_back=10): X, y = [], [] for i in range(len(features) - look_back): X.append(features[i:i + look_back]) y.append(target[i + look_back]) return np.array(X), np.array(y) look_back = 10 X, y = create_sequences(scaled_features, scaled_target, look_back) print(X.shape, y.shape) # 输出示例:(990, 10, 3) (990, 1)逻辑说明:X的形状是(样本数, look_back, 特征数),比如(990, 10, 3)表示 990 个样本,每个样本包含 10 天、每天 3 个特征;y是(990, 1),每个样本对应一个收盘价数值。参数look_back的选择是门学问——取太短,比如 3~5,模型看不到中期趋势;取太长,比如 60,数据量和训练速度会明显下降,而且过远的行情对明天的价格未必有用。金融时序数据上我一般从 10~20 开始试,看验证集 loss 再调,别拍脑袋直接上 50。
2.4 训练集与测试集切分:按时间切,别随机打乱
split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:]这一步必须用顺序切片,不能用train_test_split(shuffle=True)。LSTM 是时序模型,用未来的数据训练、过去的数据测试,等于开卷考试。顺序切分之后,X_test就是模型从未见过的最后 20% 时间段的行情,验证结果才有说服力。
3. 搭建多输入单输出LSTM:Keras层配置与参数选型
数据张量形状确认没问题之后,才轮到网络构建。这个项目的模型结构是比较典型的「LSTM 层 + 全连接输出层」,单输出结构。这里我把每层的作用和参数怎么定讲清楚,你照着能改出自己的版本。
3.1 网络结构:LSTM 层 + Dense 层,不要一上来就堆三层
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential() model.add(LSTM(units=64, return_sequences=True, input_shape=(look_back, 3))) model.add(Dropout(0.2)) model.add(LSTM(units=32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(units=1)) model.summary()逻辑说明:第一个 LSTM 层设return_sequences=True,输出的是完整序列给第二层;最后一层 LSTM 设return_sequences=False,只输出最终状态,然后接Dense(units=1)输出收盘价预测值——单输出就是体现在最后的units=1。input_shape=(look_back, 3)里的 3 对应特征数,你加了一个特征就改成 4。
units 参数的选择:64 和 32 是这个数据规模下比较稳的配置,数据量只有一千条左右时,units 太大(比如 256)很容易过拟合,验证集 loss 在第三个 epoch 就开始往上走。Dropout(0.2)是防止过拟合的常规手段,如果训练 loss 下降正常、验证 loss 波动大,可以提高到 0.3。激活函数默认用 tanh,LSTM 内部机制决定了对数值范围敏感,前面归一化到[0, 1]正好匹配。
3.2 编译参数:优化器选 Adam,损失函数选 MSE
model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae'])为什么用mean_squared_error:收盘价预测是回归问题,MSE 是连续数值预测的默认选择,对大误差惩罚更狠,能让模型更重视明显预测错误的天数。MAE 作为辅助监控指标,方便你直观看到误差大概多少个点。Adam 优化器是自适应学习率的代表,对 LSTM 这种参数较多的网络,比 SGD 省心很多,基本不需要手动调整学习率。如果你发现训练震荡,再把 Adam 的learning_rate从默认的 0.001 向下调一档到 0.0005。
3.3 训练过程:早停机制一定要加
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ) history = model.fit( X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[early_stop], verbose=1 )monitor='val_loss'是盯着验证集 loss;patience=10表示验证集 loss 连续 10 个 epoch 没有下降就停止;restore_best_weights=True是自动回滚到验证集 loss 最低时的权重,这是后悔药,一定要开。batch_size=32是训练稳定性与速度的折中,数据量大时可以提到 64,数据量小的时候建议保持 16~32。epochs 设 100 只是上限,实际上早停会在 20~40 个 epoch 时就触发,不要傻等 100 轮。
4. 跑通lstmPred.ipynb:从环境配置到训练曲线解读
这一章说说从一个空的 Jupyter Notebook 到最终跑出预测曲线图,整个过程中你需要关心的环境依赖和输出结果怎么判断。
4.1 环境配置:TensorFlow 版本别乱用
我拆包时用的环境是 Python 3.9 + TensorFlow 2.10 + Keras 2.10,这一套组合兼容性比较好。TensorFlow 2.15 之后 API 有变动,直接用老代码可能报AttributeError。安装就两行命令:
pip install tensorflow==2.10 pip install pandas numpy scikit-learn matplotlib jupyter如果你用的是 Python 3.11 以上,先pip install tensorflow看能不能装上,能装上就用默认版本,报错就换 Python 3.9 建虚拟环境——这是 python 安装环节最常见的坑。项目说明.yaml 里通常记录了作者用的依赖版本和参数设定,我推测这份项目说明里写的就是 TensorFlow 2.x 与 Keras 2.x 的组合,如果代码有兼容问题,优先按 yaml 里记录的版本号对齐。
JupyterNotebook 启动很简单:在项目根目录执行jupyter notebook,然后在浏览器里打开lstmPred.ipynb,按单元格顺序从上往下执行即可。我的习惯是先用Kernel -> Restart & Run All全跑一遍,确认没报错,再一步步回头细看每个单元格的输出。
4.2 Notebook 逐块解读:哪段代码在干什么
整个 Notebook 的执行流大致是:读数据 → 归一化 → 构造序列 → 切分数据集 → 建模型 → 训练 → 预测 → 可视化。有几个关键输出值得停下来细看:
第一,model.summary()会打印参数量。这个项目规模的模型,参数量应该在几万量级。如果看到上百万参数,说明 units 或者层数堆过头了,你的数据量撑不起这个复杂度,过拟合风险极高。
第二,训练输出的日志里要同时观察loss和val_loss。两条曲线都在下降,说明模型在正常学习;val_loss先降后升,而loss还在降,就是典型的过拟合信号,早停应该会在这个区间触发。
第三,预测部分的代码会把scaler_y.inverse_transform()之后的预测值和真实收盘价画在同一个坐标系里。这一段通常是 matplotlib 画的折线对比图,train.csv 数据量够大时,预测曲线和真实曲线贴合度会不错;尾部测试集部分出现偏差是正常现象,市场行情本身存在不可预测成分,别期望完全重合。
4.3 怎么判断模型比「昨天收盘价」强
有个最土也最有效的基准测试:用y_test[-1]也就是上一个交易日的收盘价,当作所有测试样本的预测值,计算这个「哑模型」的 MSE。如果你的 LSTM 测试集 MSE 低于这个基线,说明模型真的学到了规律;如果只是持平甚至更差,那就是特征没选对、look_back 不合适,或者模型层数堆错了。这个对比方法我在多个 lstm 时间序列预测项目里都用,能快速过滤掉「看着像那么回事、实际毫无用处」的模型。
验证可视化还有一个小技巧——把预测值和真实值的散点图画出来,算相关系数 R²。R² 大于 0.8 说明趋势捕捉得力;如果你的序列信噪比很低,R² 落在 0.5~0.7 区间也还算能接受。
5. 避坑与常见问题排查:五个高频翻车点
拆这套代码和跑通复现的过程中,我遇到过的和预判到的问题集中在下面五个点上。每一条都是真实踩过或者帮别人调代码时见过的,按「现象 → 原因 → 解决」的方式写。
5.1 验证集 loss 极低,实盘预测一塌糊涂
现象:测试集上预测曲线贴合完美,但换一段新数据预测误差大得离谱。
原因:这是数据泄漏的典型表现。最常见的是在切分训练集测试集之前,就对整个数据集做了MinMaxScaler.fit(),测试集数值范围信息混进了训练阶段;另一种是对全量数据做了标准化之后再切分,同理。
解决:严格按我前面写的流程,先切分数据再做 scaler 拟合。严格按「拟合训练集 → 变换训练集 → 变换测试集」的顺序执行:
scaler_x = MinMaxScaler() X_train_scaled = scaler_x.fit_transform(X_train.reshape(-1, X_train.shape[-1])) X_test_scaled = scaler_x.transform(X_test.reshape(-1, X_test.shape[-1])) # 然后再 reshape 回 (样本数, look_back, 特征数)5.2 训练 loss 下降,val_loss 不降反升
现象:epoch 训练日志里 loss 一路向下,val_loss 在第 15 个 epoch 后呈现明显上升趋势。
原因:模型过拟合了,网络容量超过数据量能支撑的信息量。units 太大、层数太多、训练轮次太长,都会引起这个现象。
解决:优先把早停的patience从 10 降到 5,让训练更早停下;同时检查 units 是不是超过了 128,是的话减半。Dropout 从 0.2 加到 0.3~0.4 通常也能肉眼可见地改善。如果你不调结构只想快速压过拟合,把batch_size降到 16 也能起到一定的正则化效果。
5.3 预测结果是一条水平线
现象:预测曲线几乎不波动,像是把所有预测值都输出成同一个常数。
原因:特征对目标没有预测能力,或者说 LSTM 学习到的只是训练集收盘价的均值。look_back 太短时这个问题尤其明显——模型只看到三五天的行情,无法建立趋势概念,最后会退化成「输出历史均值」。还有一种情况是学习率太高,模型在训练初期震荡后卡在局部最小值。
解决:先看look_back,低于 5 就调高到 10~20;再看学习率,Adam 默认 0.001 的话改到 0.0005。还有一个容易忽略的检查点——训练日志里 final loss 是否在 0.01 量级附近,如果 loss 高达 0.1 以上,说明训练根本没收敛,优先调学习率和训练轮次。
5.4 反归一化后数值范围不对
现象:用inverse_transform还原的预测值,最小值不到 0,或者最大值远超真实股价范围。
原因:scaler_y在 fit 时的数据范围与预测值所在的范围不一致。预测值是从[0, 1]区间逆变换回去的,如果模型输出超过 1 或低于 0,还原出来的自然就不在正常股价范围内。
解决:检查模型最后的 Dense 层是否默认使用了linear激活函数——必须保持线性,不能套 sigmoid 或 tanh,否则输出被限制在固定区间。另外在反变换前确认你用的是scaler_y而不是scaler_x,我见过把两个 scaler 搞混导致结果完全离谱的。
5.5 训练极慢,每个 epoch 耗时几十秒
现象:数据量只有一千条左右,但 GPU 占满、每个 epoch 耗时很长。
原因:最常见的是batch_size=1或者look_back设置过大(超过 100),导致梯度更新次数过多;另外 Notebook 里如果同时打开了多个模型训练单元格且没有释放显存,也会越跑越慢。
解决:batch_size=32是稳妥起步值,显存不够就降到 16;look_back 控制在 10~30 之间。如果 TensorFlow 装的是 CPU 版本,数据量几千条的时候,可以考虑直接把网络压缩成单层 LSTM 加 Dense 输出,训练速度能提升数倍。用 GPU 跑的话,在 Notebook 开头加一句tf.config.set_memory_growth = True可以避免显存被一次性占满导致后续单元格崩掉。
6. 进阶:递归多步预测与 MACD 特征扩展,以及一个验证技巧
如果你把上面的流程跑通,并且用测试集验证了单步预测的可行性,下一步就是把它用在实际场景里。LSTM 预测股票收盘价最大的现实约束是:你需要的是未来 3~5 天的预测,但模型只能给你明天一个点。解决这个问题的常见做法是递归多步预测——把预测出来的收盘价当成新的输入特征,滚动预测下一天。
具体操作流程:要预测未来第 3 天的收盘价,先用已有的最后look_back天数据预测第 1 天,然后把这个预测值拼到序列末尾、丢掉序列开头最早一天,形成新的 look_back 序列,继续预测第 2 天、第 3 天。代码结构大致如下:
def recursive_predict(model, last_seq, steps=5, scaler_y=None): predictions = [] current_seq = last_seq.copy() for _ in range(steps): next_val = model.predict(current_seq.reshape(1, look_back, 3))[0, 0] predictions.append(next_val) next_feature = np.concatenate([current_seq[0, 1:, :], [[next_val, next_val, next_val]]], axis=0) current_seq = np.vstack([current_seq[0, 1:], next_feature]) return np.array(predictions).reshape(-1, 1)注意上面的递归拼接里,因为预测的是收盘价,收盘价本身并不直接作为下一轮的开盘价、最高价、最低价特征,所以在做特征拼接时需要额外处理。比较实用的处理方式是:把预测值同时作为 open/high/low 的下一个值填进去,或者用当前窗口的均值填充。这是递归多步预测最大的误差放大点——预测偏差会逐轮累积,步数越多误差越大。所以实际使用时,建议最多滚动预测 5 天。
特征扩展方面,如果你想让模型捕捉更多信息,可以考虑加入成交量、MACD 指标、RSI 等衍生特征。这里有一个用 talib 库快速计算 MACD 的示例:
import talib df['macd'], df['macd_signal'], df['macd_hist'] = talib.MACD( df['close'], fastperiod=12, slowperiod=26, signalperiod=9 )计算完成之后把macd列加进feature_cols,同时把input_shape里的特征数从 3 改成 4。需要提醒的是:特征越多,需要的训练数据也越多,不是无脑加。一千条数据配 3 个特征是合理的,配 10 个特征就开始危险了——模型很容易过拟合。
最后说一个我自己养成的验证习惯。每次训练完模型,我会强制自己走一遍「滚动回测」:从测试集的第一个样本开始,模拟真实交易环境,每次只让模型看到过去的数据,递归预测未来第 N 天,然后与真实行情对比,记录每次的误差方向。然后算一个「方向命中率」——也就是预测涨跌方向与实际涨跌方向一致的百分比。对股票预测来说,方向命中率比 MSE 更能说明实际价值。如果 LSTM 的方向命中率长期在 55% 以下,我不会着急投入真金白银,而是先回头检查特征工程和 look_back 参数。从那以后,我每次训练完 lstm 预测模型都会强制把这条验证流程过一遍——方向命中率不达标,训练曲线再好看也要推倒重来。希望帮到你。
本文还有配套的精品资源,点击获取