LSTM时间序列预测实战:从数据预处理到多步外推的完整指南

发布时间:2026/10/5 21:00:03
LSTM时间序列预测实战:从数据预处理到多步外推的完整指南
简介基于长短期记忆网络的时间序列分析预测源码包面向Python开发者和人工智能入门者提供从数据清洗、特征工程、模型构建、训练到评估的完整解决方案适用于股票价格、销售需求、空气质量等典型序列预测场景。压缩包共一百二十六个文件其中七十五个py脚本覆盖数据加载、归一化、滑窗生成、LSTM定义与训练测试二十六个csv文件提供多组实验数据包含空气污染指标十五个txt说明标注参数与环境配置另有h5格式训练好的模型权重和md项目文档整包约五点四二兆字节轻量易上手。已有五千零九十六人学习下载社区验证度较高。通过该项目可深入理解LSTM输入门、遗忘门、输出门的作用掌握时序数据归一化、滑动窗口构建、均方误差与平均绝对误差评估等方法并能基于附带数据集复现结果快速迁移至自己的预测任务。1. 先认清LSTM 预测脚本为什么“跑得通”不等于“测得准”拿到这套基于 LSTM 的时间序列分析预测代码包第一件事不是找哪个文件是训练入口而是先想清楚一个问题你要的预测到底是“把历史曲线拟合得像”还是“未来一段时间的走势真正可参考”。这两个目标在代码里对应完全不同的数据处理方式和评估口径。很多跑通了这个源码的人对着训练集曲线满意地点头然后一换到未来数据就翻车——问题几乎都不在 LSTM 模型本身而是滑窗怎么切、数据怎么归一化、测试集怎么划分。这套源码的核心是把“近 N 个点预测未来 M 个点”的完整流程串起来数据预处理、构造样本、训练 LSTM 神经网络、输出预测值再反归一化。适合做销量、气温、设备指标、流量这类带时间戳的回归预测也适合刚入门深度学习、想把 LSTM 落地而不是停留在教程 demo 上的从业者。2. 数据准备把原始序列切成 LSTM 能吃的样本两个参数决定上限2.1 先别训练单变量还是多变量、缺失值怎么补常见的 LSTM 时间序列项目里数据格式形形色色。极简的是一列数值比如每天的电价、每个小时的在线人数复杂一点的是多列除了目标值还有辅助特征。拿到数据后第一个动作不是写模型而是确认两件事目标列是哪一列、时间顺序是否已经被打乱。CSV 读进来如果是乱序的必须先按时间戳排序这一步漏掉后面所有滑窗样本的时间含义就全错了。缺失值处理也要区分场景。普通表格可以用均值填充时序数据我会优先用前后合法值插值因为序列里的值是连续变化的直接取均值会把局部波动抹平。异常值的处理更要慎重一个脉冲尖峰在普通回归里只是损失一个点但在滑窗构造样本的时候这个坏点会被包含进多个窗口等于把一个错误复制了十几遍。检查序列里有没有超过正常范围几个数量级的跳变先用可视化扫一遍比急着调模型参数重要得多。import pandas as pd import numpy as np df pd.read_csv(data.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) # 缺失值用线性插值补充避免均值填充抹掉趋势细节 df[value] df[value].interpolate(methodlinear) # 看一眼基本统计量重点检查有没有远超均值量级的异常尖峰 print(df[value].describe())这段代码里interpolate(methodlinear)是时序缺失值处理的最常用手段比fillna(methodffill)更平滑。检查统计量时重点看 max 和 mean 之间的差距如果 max 是 mean 的几十倍就要怀疑是否存在坏点而不是直接拿去做训练。2.2 训练集和测试集必须按时间切这是时序跟普通监督学习的边界普通机器学习里随机划分训练集和测试集没问题因为样本独立。时间序列不行因为相邻样本之间存在天然相关性尤其在滑窗重叠构造样本的场景下如果随机划分训练集里很容易出现“测试样本的上一段窗口”模型等于提前看到了测试段的输入部分评估结果当然虚高。这种泄漏不会报错但会给你一个根本不真实的测试指标。正确做法是严格按时间切开比如前 80% 做训练后 20% 做测试验证集再从训练段尾部切一部分出来不能打乱。切完后做归一化时顺序也很有讲究。MinMaxScaler只能 fit 训练段的数据然后用这个已经学习好参数的标准去 transform 测试段。如果对整个序列统一 fit测试段的最大值和最小值会提前混入缩放逻辑同样属于数据泄漏。train_size int(len(df) * 0.8) valid_size int(train_size * 0.8) train_df df.iloc[:valid_size] valid_df df.iloc[valid_size:train_size] test_df df.iloc[train_size:] from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_values scaler.fit_transform(train_df[[value]]) valid_values scaler.transform(valid_df[[value]]) test_values scaler.transform(test_df[[value]])这里fit_transform只出现在训练段上验证段和测试段只用transform。验证段用来在训练过程中挑超参数测试段只做最终评估严格保持“三次隔离”。我习惯再用df.iloc[:]的时间切法而不是随机抽样强调的是顺序敏感。2.3 滑窗构造样本seq_len 和 step 两个参数决定你能预测什么LSTM 不能直接吃一长串原始序列它的输入是固定长度的窗口。比如用过去 24 个小时预测下 1 个小时那就把序列切成每 24 个点一组第 25 个点作为标签。这个滑动窗口机制里有两个参数往往比网络结构本身还影响最终效果窗口长度seq_len和滑动步长step。seq_len决定模型能“回头看”多远必须结合数据本身的周期来选。如果数据是日粒度通常用 7 的倍数因为要覆盖一周的周期如果数据是小时粒度才优先考虑 24。很多人照搬别人项目里的seq_len24结果换到日粒度数据上效果一塌糊涂因为 24 天对日数据来说既不是周周期也不是月周期。step1会让相邻样本大量重叠训练集膨胀但有效信息增量不大step设大一点比如 3 或 5样本量会明显下降训练速度更快代价是能学习的模式密度变低。我一般先用小步长跑通再用大步长调优。def make_sequences(values, seq_len24, step1): X, y [], [] for i in range(0, len(values) - seq_len, step): X.append(values[i:iseq_len]) y.append(values[iseq_len]) return np.array(X), np.array(y) seq_len 24 train_X, train_y make_sequences(train_values, seq_lenseq_len) test_X, test_y make_sequences(test_values, seq_lenseq_len) # LSTM 期望的输入形状: (样本数, 时间步数, 特征维度) train_X train_X.reshape(-1, seq_len, 1) test_X test_X.reshape(-1, seq_len, 1)最后reshape(-1, seq_len, 1)这一步非常关键1代表单变量特征数量如果是多变量预测这个维度等于特征列数。注意测试集构造样本时只需要从原始测试值中取窗口即可标签就是下一个点但实际预测场景里最后一个窗口之后再无标签那就是真正的外推预测——后面第 4 章专门展开。3. 模型搭建与训练把 LSTM 调到“记得住又不死记”的实用参数3.1 LSTM 解决什么问题门控结构在时序回归里的作用为什么不直接用普通全连接网络做时间序列回归时序数据的特点是当前值常常依赖于很多个时间步之前的信息。普通网络把所有输入特征平等对待既没有先后概念也没有“记忆”机制。LSTM 在循环结构基础上加入了三个门遗忘门决定过去的信息保留多少输入门决定当前时间步的新信息写入多少输出门决定当前时间步输出什么。这套机制理论上能捕捉“昨天这个时候的值对今天有影响”和“一周前同一天的模式也有影响”这类跨步长依赖。对你写代码的人来说理解到这一层已经足够。真正要调的是让这种“记忆”既不要太长造成过拟合也不要太短记不住周期。这一节不展开数学推导重点放在怎么搭一个能跑起来、能调参的模型结构。3.2 用 PyTorch 搭一个可直接运行的 LSTM 回归网络常见做法是用 PyTorch 实现一个 LSTM 层接收滑窗序列输出最后一个时间步的隐藏状态再接一个全连接层映射到预测值。这个结构简洁、稳定适合绝大多数单变量或多变量预测任务。下面这个网络结构是每个时间序列预测任务的基本起点。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers2, pred_len1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.regressor nn.Linear(hidden_size, pred_len) def forward(self, x): out, _ self.lstm(x) # 输出每个时间步的隐状态 last out[:, -1, :] # 只取最后一个时间步的隐状态 return self.regressor(last)batch_firstTrue让输入张量形状符合 (batch, seq_len, features)省去转置的麻烦pred_len是你要预测的未来步数默认 1。如果做多步预测可以直接把pred_len设为目标步数全连接层输出对应数量的值这与后面要讲的滚动预测走的是两条路各有适用场景。训练环节的代码同样有讲究。损失函数用均方误差即可优化器选 Adam学习率从 0.001 起步。最关键的是训练时数据处理别踩乱序的坑。from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset( torch.tensor(train_X, dtypetorch.float32), torch.tensor(train_y, dtypetorch.float32) ) train_loader DataLoader(train_dataset, batch_size64, shuffleFalse) val_loader DataLoader( TensorDataset( torch.tensor(valid_X, dtypetorch.float32), torch.tensor(valid_y, dtypetorch.float32) ), batch_size64, shuffleFalse ) model LSTMPredictor(input_size1, hidden_size32, num_layers2, pred_len1) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() for epoch in range(80): model.train() epoch_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() pred model(batch_x).squeeze(-1) loss loss_fn(pred, batch_y) loss.backward() optimizer.step() epoch_loss loss.item() print(fepoch {epoch:2d}, train loss {epoch_loss / len(train_loader):.6f})这里shuffleFalse是刻意设置的。滑窗构造的相邻样本高度重叠打乱顺序虽然不影响单个样本内部结构但 batch 内数据的分布会被搅乱训练收敛不稳定。验证集 loss 不参与梯度更新只在每个 epoch 末尾观察用。训练到后期如果 train loss 持续下降而验证 loss 开始抬升那就是过拟合信号应该提前停掉而不是继续跑满 80 个 epoch。3.3 训练时看什么loss 曲线、验证窗口和随机种子训练过程中的观察远比调参本身重要。第一个观测点是 loss 下降曲线如果前几个 epoch loss 纹丝不动多半是学习率太大导致震荡或者归一化没做好数值不稳定如果 loss 快速降到极小值、之后验证集 loss 反弹那就是过拟合。第二个观测点是定义一个与训练集不重叠的验证窗口用于在训练过程中评估真实泛化能力这个窗口不能参与任何调参决策。第三个容易被忽略的是随机种子LSTM 初始化权重是随机的崩溃概率很低但如果你同一个脚本跑两次结果差异很大说明没有固定随机种子排查问题时会非常痛苦。def set_seed(seed42): np.random.seed(seed) torch.manual_seed(seed) set_seed(42)固定种子之后再调参每次对比才有意义。否则你改了hidden_size从 32 换成 64效果变好还是变坏无法分辨是参数起作用还是运气起作用。这是很多调参花大量时间却原地踏步的重要原因。4. 预测执行从预测下一点到外推未来 30 步4.1 单步预测的最小调用注意张量形状和反归一化模型训练完真正去预测的时候最容易出问题的是形状和量纲。训练时输入是 (batch, seq_len, features)推理时只有一条输入所以要构造 (1, seq_len, features)。预测出来的值也别忘了它是在 0~1 区间经过缩放的必须用前面同一个 scaler 反归一化还原成真实量纲。model.eval() # 用测试集最后 seq_len 个点作为输入窗口 current_seq test_values[-seq_len:].reshape(1, seq_len, 1) with torch.no_grad(): pred_scaled model(torch.tensor(current_seq, dtypetorch.float32)).item() pred_value scaler.inverse_transform(np.array([[pred_scaled]])) print(f预测值: {pred_value[0][0]:.2f})这段代码的关键在最后一步scaler.inverse_transform期望输入形状是 (n_samples, n_features)所以这里必须包一层np.array([[pred_scaled]])否则会报维度错或者得到错误结果。单步预测本身逻辑简单但它是一切多步预测的基石。4.2 多步外推两种思路滚动预测和直接多输出如果目标是预测未来 7 天、30 天就不能只做一步。两种常见做法滚动预测和直接多输出。滚动预测的思路是用当前窗口预测出下一个值把这个预测值拼进窗口尾部再扔掉最前面一个值保持窗口长度不变继续预测下一步。实现简单完全复用单步模型但误差会累积——第二步的输入里已经包含了第一步的预测误差随着步数增加预测序列会逐渐漂移。直接多输出的思路是把模型最后一层全连接的pred_len设为目标步数比如 7训练时直接把标签数组当成 7 个值来监督。推理时一次输出未来 7 个点误差不累积但需要重新准备多步标签数据训练成本稍高。def rolling_forecast(model, init_seq, steps30): model.eval() window init_seq.copy().reshape(-1) preds [] with torch.no_grad(): for _ in range(steps): x torch.tensor( window.reshape(1, seq_len, 1), dtypetorch.float32 ) y_scaled model(x).item() preds.append(y_scaled) # 滚动窗口丢最老的点拼入新预测值 window np.append(window[1:], y_scaled) return scaler.inverse_transform(np.array(preds).reshape(-1, 1))滚动预测里window[1:]丢一个点、np.append补一个新点这个操作本身简单但要注意窗口里的值必须是归一化状态不能混入原始量纲否则模型输入分布突变预测直接发散。我一般把滚动预测用在短期外推不超过 7 步更长的预测建议用直接多输出模型或者两者结合用直接多输出出 7 个点再用这 7 个点作为新的窗口做滚动延伸。4.3 评估必须用 rolling 指标只看第一步的结果会骗人训练过程中评估用的是单步预测 loss但真正做多步预测时评估口径要跟着变。很多人报告“预测效果挺好”细问才知道只统计了第一步的误差——第一步的输入全部来自真实历史值难度最低第二步起输入开始掺入模型自身输出误差逐渐积累。所以评估多步预测时要把 h1、h7、h30 的误差分开计算。实际操作上用测试集从第seq_len个点开始逐点滚动预测每一步都记录当前步的预测值与真实值之差最终按步数分桶计算平均绝对误差或均方根误差。不同步数下的误差曲线能直观展示模型到底能外推多远。如果 h1 误差很小h7 误差翻倍h30 误差已经接近基线那说明这个模型只适合短期预测强行做长期预测没有意义。5. 避坑清单LSTM 时间序列预测翻车的 5 个经典原因5.1 数据集随机划分测试集信息泄漏进训练现象训练曲线拟合得很好测试集上的指标也很漂亮但一旦投入真实未来数据进行预测曲线立刻偏离真实走势偏差比设想的要大得多。原因滑窗构造的样本之间高度重叠相邻样本共享大部分输入。如果用train_test_split(random_state42)这类随机划分方式测试集里的某一个样本很可能它的整个输入窗口也出现在训练集里。模型等于直接背过答案测试指标虚高。解决严格按时间顺序切分先按df.iloc[:train_size]取出训练段再取测试段不做任何打乱。这也是前面第 2 章里强调的时间切分方式属于整个流程里最基础也最要命的一道防线。5.2 MinMaxScaler 先 fit 全量数据再做归一化现象预测曲线整体看起来和真实曲线像一对平移的平行线边界处数值始终差一截。原因MinMaxScaler.fit()在全部数据上执行等于让模型在训练阶段就看到了测试集的最大值和最小值。模型输出被压缩在一个偏向测试分布的空间里反归一化后自然产生系统性偏移。解决只对训练段数据调用fit_transform验证段和测试段统一用这个已经定型的transform。推理阶段拿到的新数据也要用同一个 scaler 做transform预测完成后再用同一个 scalerinverse_transform还原。5.3 多步预测只报告第一步误差现象汇报里写着“预测误差 2%”但业务方实际使用后说你的预测根本没法看尤其是第 7 天之后的预测偏差大到失去参考价值。原因第一步预测的输入完全来自真实历史观察值模型没有机会自我污染第二步开始输入窗口里有模型上一轮输出的预测值误差开始累积。如果只统计 h1 的误差测的根本不是模型在真实使用场景下的表现。解决多步预测必须按步数分层评估。记录预测序列每一个时间步与真实值的误差按 h1、h5、h10、h30 分别计算 MAE把这条误差增长曲线暴露出来再决定模型能承诺多少步以内的预测。5.4 缺失值用均值填充、异常尖峰不清理现象训练过程中 loss 下降正常但预测出的曲线比真实数据平滑得多很多正常波动都被吞掉了。原因均值填充会把序列里的局部波动抹平模型学到的规律变得过于平滑异常尖峰没清除的话滑窗会把同一个坏点复制进几十个训练样本导致模型专门学习这个错误的模式。解决列缺失用interpolate(methodlinear)保留趋势对于明显超出正常波动范围的脉冲点用前后窗口的中位数替换或者直接剔除再插值。总之先让序列本身干净再谈训练效果。5.5 换数据集后直接照搬 seq_len24现象上一个项目里seq_len24效果好换到新的业务数据后怎么调都上不去模型似乎永远慢半拍。原因24 这个值通常是为小时粒度数据准备的因为一天有 24 小时。如果换到日粒度数据24 天既不是一个完整的周周期也不是月周期换到分钟粒度数据24 分钟又太短。解决拿到新数据集先做周期探测。画出数据的自相关图或直接按不同seq_len7、14、24、28、48分别训练几次看验证集 loss 谁最低。滑窗长度应该和数据本身的周期对齐而不是沿用别人项目里的习惯值。6. 让它能真正投入多种子评估与滚动重训的落地做法前面几章已经覆盖了从数据准备到多步预测的完整流程但真要把这套代码用在业务决策里还差两个非常实用的做法。第一个做法是多随机种子评估。LSTM 初始权重随机单次训练的结果带有运气成分。我会用 5 个不同种子分别训练保存每个种子的预测序列把所有预测放在同一张图上。预测线比较粗、互相分散说明模型对数据模式的把握还很不稳定如果几条线几乎重合说明模型确实学到了稳定的规律。更进一步的可以从多次预测中取 10% 和 90% 分位数画一条预测区间业务汇报时给区间比给单点有用得多。preds [] for seed in [42, 123, 999, 7, 2024]: set_seed(seed) model LSTMPredictor(...) # 重新训练并预测 preds.append(rolling_forecast(model, init_seq, steps30)) preds np.array(preds) # shape: (5, 30) lower np.percentile(preds, 10, axis0) upper np.percentile(preds, 90, axis0)第二个做法是滚动重训。业务数据不断更新模型不可能训一次用一年。我一般以一周为周期把新观测追加到训练集尾部用上一次训练得到的模型参数作为初始权重继续训练 10~20 个 epoch。这种方式叫 warm start比每周从头训练快得多也更容易适应缓慢的数据漂移。重训后用最新一段真实数据做一轮 h7 的验证误差如果明显恶化说明数据分布发生了结构性变化需要重新审视特征和窗口设置。最后分享一个我自己的习惯任何新数据集跑 LSTM 之前先跑一个最简单的基线——直接用上一个值当预测值persistence model。如果 LSTM 连这个傻瓜基线的误差都压不下来 10% 以上那不是模型能力问题而是前面的数据处理或参数选择出了问题。这个习惯帮我挡掉了大量盲目调参的时间也让我更容易分辨模型是真的学到了规律还是仅仅在复读历史。希望这些内容能帮你在 LSTM 时间序列预测这条路上少踩几个坑把代码包真正变成可上线、可解释的预测工具。本文还有配套的精品资源点击获取