
简介基于LSTM的多输入多输出时间序列预测项目面向深度学习和数据挖掘方向的初学者与研究者适用于空气质量、电力负荷、股票走势等多变量回归与预测场景。压缩包共126个文件主体为75个Python脚本、26个CSV数据文件和15个TXT说明另含模型权重h5、checkpoint检查点及TensorFlow数据分片整个RAR包仅5.12MB轻量且目录结构完整。内容覆盖从CSV/Excel数据清洗、缺失值处理、标准化到LSTM模型构建、训练与评估的完整流水线以真实空气污染监测数据驱动多变量预测实例可清晰看到输入门、遗忘门等门控机制如何捕捉长期依赖并能通过修改特征维度和输出步长适配不同任务。项目保留LSTM-master工程目录附有配置文本与模型存档方便复现和二次开发。当前已有3855人浏览学习适合需要系统掌握RNN深层建模技巧的读者参考。1. 多输入多输出 LSTM为什么单变量模型总是不够用做时间序列预测时很多刚上手的同学先跑通单变量 LSTM觉得效果还行可一旦进入真实业务——比如要同时预测多个站点的客流量、多台设备的温度或者电价和负荷两个指标单变量模型立刻翻车要么把多个输出拆开训练导致信息割裂要么把所有变量塞进一个Dense层导致特征混杂。这份项目资源的核心就是解决“多输入多输出”这个真实场景你喂进去的是一个多维特征窗口模型吐出来的是一组未来值。它不是调包教程而是带着数据预处理、模型结构、训练策略和验证方法的一整套可复现方案。适合已经跑通 Keras 或 PyTorch 基础 LSTM 的从业者想往前再走一步把多变量预测落到自己的数据上。2. 数据构造从原始 CSV 到监督学习格式这一步决定 80% 的结果2.1 为什么 LSTM 的输入必须是 [样本, 时间步, 特征]而不是一维序列几乎所有 LSTM 相关的报错最后都回溯到数据维度搞错了。Keras 里 LSTM 层的输入要求是三维张量[batch_size, 时间步长, 特征数]PyTorch 则是[序列长度, batch_size, 特征数]或[batch_size, 序列长度, 特征数]取决于batch_first参数。但这个形状不是你随便 reshape 出来的它对应的是监督学习的逻辑你要使用过去 N 个时间步的数据去预测未来 M 个时间步的数据。所以第一步是把原始的时间序列转换成“样本-标签”对。常见做法是滑窗法也就是用固定长度的窗口在序列上滑动窗口内的数据作为特征窗口后面的数据作为标签。以 6 个输入特征为例原始数据是[t0, t1, t2, ..., tn]每个时间点上是一个长度 6 的向量。如果你要用过去 24 个小时预测未来 12 个小时那么第一个样本的输入是第 0 到第 23 个小时的 24×6 矩阵标签是第 24 到第 35 个小时的 12×6 矩阵。这个数据转换逻辑直接决定了你的代码怎么写。很多初学者直接把原始数据reshape成三维就丢给模型结果模型看到的每一个样本都是不重叠的独立片段时序的连续性完全丢失训练出来的模型毫无泛化能力。项目资源里给出的sliding_window函数本质上就是帮你把序列切成重叠的窗口同时保证样本之间有信息交集。# 滑窗转换函数把二维时序数据变成 LSTM 所需的监督学习格式 import numpy as np def sliding_window(data, input_steps, output_steps): data: 二维数组形状为 [时间步, 特征数] input_steps: 用过去多少个时间步作为输入 output_steps: 预测未来多少个时间步 返回 (X, y)X 形状 [样本数, input_steps, 特征数] y 形状 [样本数, output_steps, 特征数] X, y [], [] for i in range(len(data) - input_steps - output_steps 1): # 输入窗口从 i 到 iinput_steps闭区间取 input_steps 个时间点 X.append(data[i : i input_steps, :]) # 输出窗口紧接着输入窗口之后的 output_steps 个时间点 y.append(data[i input_steps : i input_steps output_steps, :]) return np.array(X), np.array(y)参数说明input_steps和output_steps是这套程序里最核心的两个超参数前者控制模型“回顾”的长度后者控制“预见”的长度。如果你的数据是周期性很强的比如电力负荷按天波动input_steps至少要覆盖一个完整周期否则模型永远看不到规律。output_steps越长预测难度越大误差累积越严重一般建议从短预测开始验证流程跑通再逐步拉长。这里还要注意一个数据泄漏的经典坑滑窗生成样本时相邻样本的输入窗口是有重叠的这会导致训练集和验证集如果不按时间切分而是随机切分验证集的样本会“偷看”到训练集的信息。项目里直接按时间顺序切分数据——前 80% 做训练后 20% 做验证就是因为这种任务本身就不是一个随机抽样问题。2.2 特征归一化与多输出标签的处理顺序多输入多输出任务里特征归一化不是可选项是必选项。LSTM 内部用的是 tanh 和 sigmoid 激活函数输出范围有限如果某个特征的数值量级是几千另一个是零点几梯度更新会被大数值特征主导小数值特征几乎学不到东西。最常见的是 MinMaxScaler把数据压缩到 [0, 1] 区间。但这里有一个容易被忽视的坑如果对全量数据一起 fitMinMaxScaler 会用到未来数据的信息这同样是数据泄漏。正确做法是只用训练集去 fit再用同一个 scaler 去 transform 训练集、验证集和未来的测试集。项目资源里明确给出了这个顺序# 特征归一化只用训练集 fit避免未来数据泄漏 from sklearn.preprocessing import MinMaxScaler # 假设 raw_data 是 [时间步, 特征数] 的原始数据 train_size int(len(raw_data) * 0.8) train_data raw_data[:train_size] val_data raw_data[train_size:] # 实例化 scaler 并只在训练集上 fit scaler MinMaxScaler(feature_range(0, 1)) train_data_scaled scaler.fit_transform(train_data) # 对验证集做同样的 transform而不是重新 fit val_data_scaled scaler.transform(val_data) # 之后再把归一化后的序列送入滑窗函数 X_train, y_train sliding_window(train_data_scaled, input_steps24, output_steps6) X_val, y_val sliding_window(val_data_scaled, input_steps24, output_steps6)逻辑说明scaler.fit_transform(train_data)这一步是在计算训练集每个特征的 min 和 max并完成归一化。随后scaler.transform(val_data)用的是训练集的 min 和 max不是验证集自己的。如果有人不小心在完整数据上调用fit_transform验证集的归一化就携带了未来信息模型在验证集上的表现会虚高上真实场景时立刻打回原形。2.3 多输出标签的 reshape 细节与常见维度报错滑窗函数返回的y形状是[样本数, output_steps, 特征数]但 LSTM 模型的输出层往往只需要后两维或者需要把三维压成二维。如果你用的是 Keras输出层直接定义一个Dense(output_steps * 特征数)然后通过Reshape恢复成[output_steps, 特征数]这样得到的预测可以直接和原始标签做损失计算。# Keras 中多输入多输出 LSTM 的输出层设计 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Reshape # 假设特征数为 6 INPUT_STEPS 24 OUTPUT_STEPS 6 NUM_FEATURES 6 model Sequential([ LSTM(units64, return_sequencesFalse, input_shape(INPUT_STEPS, NUM_FEATURES)), Dense(OUTPUT_STEPS * NUM_FEATURES), Reshape((OUTPUT_STEPS, NUM_FEATURES)) ])参数说明return_sequencesFalse表示 LSTM 只返回最后一个时间步的隐藏状态而不是每个时间步的输出。此处用这种设定是因为我们需要从整个输入窗口中提炼信息然后一次性映射到多个输出时间步而不是逐时间步输出。Dense层的神经元数量是OUTPUT_STEPS * NUM_FEATURES意味着模型把 LSTM 的 64 维隐藏向量映射到一个中间向量再通过Reshape还原成多步多特征的结构。如果这里直接写Dense(OUTPUT_STEPS)模型把多个特征混合在一个数值里输出就废了。3. 模型构建与训练选择合适的损失函数和优化器3.1 回归任务不选交叉熵多输出任务要考虑特征权重多输入多输出时间序列预测是回归问题不是分类问题。Keras 默认的categorical_crossentropy在这里完全不适用必须用回归损失。最常用的是mse均方误差它把所有输出特征、所有预测时长的误差都等权平均了。这在特征量级差异不大时没问题但如果某个特征本身波动范围大它的误差天然就大会主导整个损失值的走向。更精细的做法是对不同输出特征施加不同的权重。例如在电力场景里电压数值稳定但波动大电流数值小但重要你希望模型更关注电流的预测精度。这时候可以改用自定义损失函数或者在模型输出端分别加输出头各接各的损失。项目资源里没有强行要求用哪种而是提供了一个折中方案# 自定义加权 MSE 损失按特征给误差分配权重 import tensorflow as tf def weighted_mse(y_true, y_pred): # 假设第 0 个特征权重为 2.0其余为 1.0 weights tf.constant([2.0, 1.0, 1.0, 1.0, 1.0, 1.0]) squared_error tf.square(y_true - y_pred) return tf.reduce_mean(squared_error * weights)逻辑说明tf.square(y_true - y_pred)计算每个位置上的平方误差形状与y相同。权重向量会沿着特征维度广播乘以平方误差后取均值。这个函数传入model.compile(lossweighted_mse)即可使用。如果你更看重某些时间步的精确度也可以用类似思路给时间维度加权重。3.2 训练策略Teacher forcing、批量大小和 EarlyStopping多输出 LSTM 训练时Keras 的标准做法是把整个输出序列一次性作为标签模型一次性生成完整预测而不是像 NLP 任务那样逐字生成。这里面没有 teacher forcing 的问题因为训练和推理用的是同一个结构输入历史窗口直接输出未来窗口。所以别把 LSTM 单元堆得太多层大多数场景下 1 到 2 层已经够用。层数一深参数量上去了训练时间翻倍效果却不一定变好。训练时批量大小也有讲究。时间序列预测的标签在时间维度上是强相关的如果 batch 太小比如 16梯度更新频繁模型容易在训练集上震荡batch 太大比如 512训练过程平滑了但模型更容易陷入局部最优而且占用内存大。项目资源里给的默认值是 64适配大部分中等规模的数据集。# 模型训练设置回调函数防止过拟合 from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callback_list [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ModelCheckpoint( filepathbest_lstm_model.h5, monitorval_loss, save_best_onlyTrue, verbose1 ) ] model.compile(optimizeradam, lossweighted_mse, metrics[mae]) model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size64, callbackscallback_list, verbose1 )参数说明EearlyStopping里的patience10表示验证集损失连续 10 个 epoch 不下降就提前终止训练restore_best_weightsTrue会让训练结束后模型权重复位到验证集表现最好的那一轮。这两个回调是常规操作真正加分的是ModelCheckpoint——它在每个 epoch 结束后检查验证损失如果比之前的最好记录更低就保存当前模型。训练中断或过拟合后代价不是重新跑一遍而是直接加载best_lstm_model.h5。3.3 输出反归一化预测值必须回到原始量纲才有意义模型输出的是归一化后的值范围在 [0, 1] 之间。你不能直接把这个值当成真实预测结果交给业务方必须用训练时那个 scaler 做逆变换。这里有个细节直接对整个三维预测数组调用scaler.inverse_transform是不行的因为 scaler 是针对二维的[时间步, 特征数]设计的。# 预测值反归一化恢复原始量纲 # y_val_scaled 形状是 [样本数, OUTPUT_STEPS, 特征数] # 预测结果 pred_scaled 形状相同 # 取第一个样本作为演示 sample_pred_scaled pred_scaled[0] # 形状 [OUTPUT_STEPS, 特征数] sample_pred_original scaler.inverse_transform(sample_pred_scaled) # 注意inverse_transform 要求输入形状为 [n_samples, n_features] # 这里 OUTPUT_STEPS 相当于 n_samples特征数据是 n_features刚好吻合逻辑说明scaler是在二维[时间步, 特征数]上训练的它的inverse_transform期望输入样本数是时间步数、特征数保持不变。预测值的三维数组中第二维正是时间步维所以取单个样本后形状变为[OUTPUT_STEPS, 特征数]可以直接逆变换。项目中常见的报错是把整个三维pred_scaled传入此时需要先np.concatenate或循环处理每个样本。4. 避坑指南多输入输出 LSTM 最常见的五个翻车现场4.1 验证集损失很低但真实预测曲线滞后一个周期现象训练完成后在验证集上画图预测曲线和真实曲线有明显的时间滞后整体看着像真实曲线向右平移了一段距离。原因这是自回归模型预测的典型特征常见于input_steps过短模型只学到了“上一时刻的值等于下一时刻的粗估计”本质上是把预测退化成了“拷贝最近值”。验证集虽然按时间切分但如果序列本身高度自相关short-window 模型在一步预测时仍然能拿到足够近的历史值来“抄袭”。解决拉长input_steps让窗口覆盖至少一个周期或足够长的趋势段。同时检查output_steps是否太短尝试先预测中间态而不是直接预测末端。项目资源里常用做法是先在input_steps24、output_steps1上验证模型能学到动态再逐步加大output_steps。4.2 输入特征包含未来信息验证集指标虚高现象验证集上 MAE 降到不敢相信的程度几乎接近零但换到新数据上误差飙升。原因原始数据在构造特征时把当前时刻的某些统计量比如移动平均、未来窗口的均值也当成输入特征了。这类特征是“未来函数”在预测时刻根本拿不到。更隐性的泄漏是直接对完整数据做归一化后再切分数据scaler 已经见过验证集的极值。解决明确每条特征的时点属性凡是使用未来窗口窗口内数据计算得到的特征一律剔除或进行滞后处理。归一化严格按“先切分后 fit”的顺序执行之前代码已经演示过。建议在项目里加一步事后检查在训练完成后打印训练集和验证集的特征统计量如果差异异常大基本可以确定泄漏存在。4.3 多输入变成多特征堆叠导致模型参数爆炸现象模型参数量几十万甚至上百万训练一个 epoch 要几分钟效果却不如一个简单的线性回归。原因输入端堆了太多特征但各特征间高度线性相关LSTM 的隐藏单元数量又被调得很大模型学习到的大多是冗余表达。多输入不等于多特征拼盘真正有价值的输入往往是通过相关性分析筛选出来的那几个。解决先做特征筛选用简单方法如皮尔逊相关系数或随机森林特征重要性剔掉相关性大于 0.95 的特征。LSTM 隐藏单元从 32 开始每次翻倍观察验证集误差是否明显下降如果没有说明不是容量不够而是特征信息不足。4.4 预测起点偏移训练时乱序 shuffle导致时序信息错乱现象训练期间损失下降很快但验证损失波动巨大无法收敛。原因在model.fit里没设置shuffleFalse。Keras 默认 shuffleTrue会把时间序列样本打乱相邻样本本属于不同时间段模型无法学到递进关系序列的日历顺序被破坏了。虽然 LSTM 在单个样本内部仍然按时间步读取但样本与样本之间的时间关联已经丢失。解决训练时显式传shuffleFalse或干脆把batch_size设置为 1 并完全按序列顺序输入。对于超长序列可以用TimeseriesGenerator或自定义数据迭代器但核心是保证样本顺序不被随机打乱。4.5 模型收敛到平坦区梯度消失损失不再下降现象训练到第几十个 epoch 后验证损失几乎不动调低学习率也没明显改善。原因多输出任务里输出层需要同时拟合多个目标如果各目标间的量纲差异大且未做归一化梯度会被大量纲目标主导小量纲目标对应的权重停在初始值附近。另一个常见原因是 LSTM 激活函数饱和——输入归一化后仍然可能有极端值导致 tanh 进入饱和区梯度趋近于零。解决确保每个特征都经过了归一化且训练过程中监控每个输出特征的单独误差而非只看总损失。如果发现某个特征误差一直不下降可以加大该特征在损失函数中的权重或者单独给该特征增加一层Dense输出分支让模型学习解耦。5. 进阶验证与实用技巧从模型训练到多步预测的完整闭环训练完成只是起点把模型落到真实预测里还有许多细节。这一章我会给出一个完整的预测流程脚本以及多步递归预测与直接多步预测的选择逻辑。多输出时间序列预测有两种推理方式。第一种是直接多步预测模型一次性输出未来 N 个时间步训练和推理结构一致速度快但误差在所有预测步上同时被优化随着预测距离增加精确度会均匀衰减。第二种是递归多步预测模型只预测下一步然后把预测值作为特征拼回输入窗口再预测下一步如此循环。两种方式各有适用场景。模拟项目X里对比了这两种策略结论很明确如果output_steps小于等于 6直接多步预测更稳定如果预测长度超过 12 个时间步递归预测反而能保持更好的短期精度但误差会累积越往后越夸张。原因是直接多步预测在训练时模型对所有时间步一视同仁但长预测步上有效信息本来就少强制学反而学习到噪声。递归预测用真实历史值引导每一步每一步只做一个短预测单步精度高误差随迭代步数线性增长。# 递归多步预测用模型预测值逐步替代历史窗口中的旧值 def recursive_predict(model, initial_sequence, scaler, num_predict_steps, feature_index0): initial_sequence: 归一化后的输入窗口形状 [input_steps, 特征数] num_predict_steps: 递归预测多少个未来时间步 返回原始量纲的预测值列表形状 [num_predict_steps, 特征数] preds [] current_window initial_sequence.copy() for _ in range(num_predict_steps): # 输入模型形状需要增加 batch 维度 current_input current_window[np.newaxis, :, :] next_step_scaled model.predict(current_input, verbose0) # 形状 [1, 1, 特征数] # 取出预测值并把窗口向前移一格 next_step_scaled next_step_scaled[0, 0, :] current_window np.vstack([ current_window[1:], # 丢弃最旧的一行 next_step_scaled # 拼接新的预测行 ]) preds.append(next_step_scaled) # 反归一化恢复原始量纲 preds_array np.array(preds) # 形状 [num_predict_steps, 特征数] return scaler.inverse_transform(preds_array)逻辑说明current_window在每次循环中丢掉第一行再把预测的下一步拼到最后一行模拟“用预测值滚动更新输入窗口”。model.predict返回形状[1, OUTPUT_STEPS, 特征数]如果OUTPUT_STEPS设为 1取[0, 0, :]就是单个时间步的预测结果。这个函数的feature_index参数用于某些场景下只关注一个目标特征但在多输出全特征预测时不需要传值。落到实际项目时我还会强制加一道可视化验证把预测结果和真实值画在同一张图上按时间步对齐。这个动作虽然简单但能暴露出 70% 的隐性问题——滞后、偏移、预测过早发散这些在损失指标里看不到的毛病画图后一眼就能看出来。这类模型谁跑谁翻车的位置基本集中在数据处理而不是模型结构。从那以后我每接一个时间序列项目都会强制先走一遍滑窗、归一化、切分的全流程把数据形状打印一遍再做模型。项目资源里之所以把数据处理代码和训练代码拆成两个模块就是为了让这个流程可审计。你可以先只跑数据部分看看样本维度再决定模型结构。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。