
简介这是一份面向序列预测学习者与初学者的多输入单输出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_back10): 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(shuffleTrue)。LSTM 是时序模型用未来的数据训练、过去的数据测试等于开卷考试。顺序切分之后X_test就是模型从未见过的最后 20% 时间段的行情验证结果才有说服力。3. 搭建多输入单输出LSTMKeras层配置与参数选型数据张量形状确认没问题之后才轮到网络构建。这个项目的模型结构是比较典型的「LSTM 层 全连接输出层」单输出结构。这里我把每层的作用和参数怎么定讲清楚你照着能改出自己的版本。3.1 网络结构LSTM 层 Dense 层不要一上来就堆三层from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(units64, return_sequencesTrue, input_shape(look_back, 3))) model.add(Dropout(0.2)) model.add(LSTM(units32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(units1)) model.summary()逻辑说明第一个 LSTM 层设return_sequencesTrue输出的是完整序列给第二层最后一层 LSTM 设return_sequencesFalse只输出最终状态然后接Dense(units1)输出收盘价预测值——单输出就是体现在最后的units1。input_shape(look_back, 3)里的 3 对应特征数你加了一个特征就改成 4。units 参数的选择64 和 32 是这个数据规模下比较稳的配置数据量只有一千条左右时units 太大比如 256很容易过拟合验证集 loss 在第三个 epoch 就开始往上走。Dropout(0.2)是防止过拟合的常规手段如果训练 loss 下降正常、验证 loss 波动大可以提高到 0.3。激活函数默认用 tanhLSTM 内部机制决定了对数值范围敏感前面归一化到[0, 1]正好匹配。3.2 编译参数优化器选 Adam损失函数选 MSEmodel.compile(optimizeradam, lossmean_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( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs100, batch_size32, callbacks[early_stop], verbose1 )monitorval_loss是盯着验证集 losspatience10表示验证集 loss 连续 10 个 epoch 没有下降就停止restore_best_weightsTrue是自动回滚到验证集 loss 最低时的权重这是后悔药一定要开。batch_size32是训练稳定性与速度的折中数据量大时可以提到 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 tensorflow2.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_size1或者look_back设置过大超过 100导致梯度更新次数过多另外 Notebook 里如果同时打开了多个模型训练单元格且没有释放显存也会越跑越慢。解决batch_size32是稳妥起步值显存不够就降到 16look_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, steps5, scaler_yNone): 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]]], axis0) 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], fastperiod12, slowperiod26, signalperiod9 )计算完成之后把macd列加进feature_cols同时把input_shape里的特征数从 3 改成 4。需要提醒的是特征越多需要的训练数据也越多不是无脑加。一千条数据配 3 个特征是合理的配 10 个特征就开始危险了——模型很容易过拟合。最后说一个我自己养成的验证习惯。每次训练完模型我会强制自己走一遍「滚动回测」从测试集的第一个样本开始模拟真实交易环境每次只让模型看到过去的数据递归预测未来第 N 天然后与真实行情对比记录每次的误差方向。然后算一个「方向命中率」——也就是预测涨跌方向与实际涨跌方向一致的百分比。对股票预测来说方向命中率比 MSE 更能说明实际价值。如果 LSTM 的方向命中率长期在 55% 以下我不会着急投入真金白银而是先回头检查特征工程和 look_back 参数。从那以后我每次训练完 lstm 预测模型都会强制把这条验证流程过一遍——方向命中率不达标训练曲线再好看也要推倒重来。希望帮到你。本文还有配套的精品资源点击获取