拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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(drop=True) # 缺失值用线性插值补充,避免均值填充抹掉趋势细节 df["value"] = df["value"].interpolate(method="linear") # 看一眼基本统计量,重点检查有没有远超均值量级的异常尖峰 print(df["value"].describe())

这段代码里,interpolate(method="linear")是时序缺失值处理的最常用手段,比fillna(method="ffill")更平滑。检查统计量时重点看 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_len=24,结果换到日粒度数据上效果一塌糊涂,因为 24 天对日数据来说既不是周周期也不是月周期。step=1会让相邻样本大量重叠,训练集膨胀但有效信息增量不大;step设大一点,比如 3 或 5,样本量会明显下降,训练速度更快,代价是能学习的模式密度变低。我一般先用小步长跑通,再用大步长调优。

def make_sequences(values, seq_len=24, step=1): X, y = [], [] for i in range(0, len(values) - seq_len, step): X.append(values[i:i+seq_len]) y.append(values[i+seq_len]) return np.array(X), np.array(y) seq_len = 24 train_X, train_y = make_sequences(train_values, seq_len=seq_len) test_X, test_y = make_sequences(test_values, seq_len=seq_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_size=1, hidden_size=32, num_layers=2, pred_len=1): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.regressor = nn.Linear(hidden_size, pred_len) def forward(self, x): out, _ = self.lstm(x) # 输出每个时间步的隐状态 last = out[:, -1, :] # 只取最后一个时间步的隐状态 return self.regressor(last)

batch_first=True让输入张量形状符合 (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, dtype=torch.float32), torch.tensor(train_y, dtype=torch.float32) ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=False) val_loader = DataLoader( TensorDataset( torch.tensor(valid_X, dtype=torch.float32), torch.tensor(valid_y, dtype=torch.float32) ), batch_size=64, shuffle=False ) model = LSTMPredictor(input_size=1, hidden_size=32, num_layers=2, pred_len=1) optimizer = torch.optim.Adam(model.parameters(), lr=1e-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(f"epoch {epoch:2d}, train loss {epoch_loss / len(train_loader):.6f}")

这里shuffle=False是刻意设置的。滑窗构造的相邻样本高度重叠,打乱顺序虽然不影响单个样本内部结构,但 batch 内数据的分布会被搅乱,训练收敛不稳定。验证集 loss 不参与梯度更新,只在每个 epoch 末尾观察用。训练到后期如果 train loss 持续下降而验证 loss 开始抬升,那就是过拟合信号,应该提前停掉而不是继续跑满 80 个 epoch。

3.3 训练时看什么:loss 曲线、验证窗口和随机种子

训练过程中的观察远比调参本身重要。第一个观测点是 loss 下降曲线:如果前几个 epoch loss 纹丝不动,多半是学习率太大导致震荡,或者归一化没做好数值不稳定;如果 loss 快速降到极小值、之后验证集 loss 反弹,那就是过拟合。第二个观测点是定义一个与训练集不重叠的验证窗口,用于在训练过程中评估真实泛化能力,这个窗口不能参与任何调参决策。第三个容易被忽略的是随机种子:LSTM 初始化权重是随机的,崩溃概率很低,但如果你同一个脚本跑两次结果差异很大,说明没有固定随机种子,排查问题时会非常痛苦。

def set_seed(seed=42): 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, dtype=torch.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, steps=30): 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), dtype=torch.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,但真正做多步预测时,评估口径要跟着变。很多人报告“预测效果挺好”,细问才知道只统计了第一步的误差——第一步的输入全部来自真实历史值,难度最低;第二步起输入开始掺入模型自身输出,误差逐渐积累。所以评估多步预测时,要把 h=1、h=7、h=30 的误差分开计算。

实际操作上,用测试集从第seq_len个点开始,逐点滚动预测,每一步都记录当前步的预测值与真实值之差,最终按步数分桶计算平均绝对误差或均方根误差。不同步数下的误差曲线,能直观展示模型到底能外推多远。如果 h=1 误差很小,h=7 误差翻倍,h=30 误差已经接近基线,那说明这个模型只适合短期预测,强行做长期预测没有意义。

5. 避坑清单:LSTM 时间序列预测翻车的 5 个经典原因

5.1 数据集随机划分,测试集信息泄漏进训练

现象:训练曲线拟合得很好,测试集上的指标也很漂亮,但一旦投入真实未来数据进行预测,曲线立刻偏离真实走势,偏差比设想的要大得多。

原因:滑窗构造的样本之间高度重叠,相邻样本共享大部分输入。如果用train_test_split(random_state=42)这类随机划分方式,测试集里的某一个样本,很可能它的整个输入窗口也出现在训练集里。模型等于直接背过答案,测试指标虚高。

解决:严格按时间顺序切分,先按df.iloc[:train_size]取出训练段,再取测试段,不做任何打乱。这也是前面第 2 章里强调的时间切分方式,属于整个流程里最基础也最要命的一道防线。

5.2 MinMaxScaler 先 fit 全量数据再做归一化

现象:预测曲线整体看起来和真实曲线像一对平移的平行线,边界处数值始终差一截。

原因:MinMaxScaler.fit()在全部数据上执行,等于让模型在训练阶段就看到了测试集的最大值和最小值。模型输出被压缩在一个偏向测试分布的空间里,反归一化后自然产生系统性偏移。

解决:只对训练段数据调用fit_transform,验证段和测试段统一用这个已经定型的transform。推理阶段拿到的新数据,也要用同一个 scaler 做transform,预测完成后再用同一个 scalerinverse_transform还原。

5.3 多步预测只报告第一步误差

现象:汇报里写着“预测误差 2%”,但业务方实际使用后说你的预测根本没法看,尤其是第 7 天之后的预测,偏差大到失去参考价值。

原因:第一步预测的输入完全来自真实历史观察值,模型没有机会自我污染;第二步开始输入窗口里有模型上一轮输出的预测值,误差开始累积。如果只统计 h=1 的误差,测的根本不是模型在真实使用场景下的表现。

解决:多步预测必须按步数分层评估。记录预测序列每一个时间步与真实值的误差,按 h=1、h=5、h=10、h=30 分别计算 MAE,把这条误差增长曲线暴露出来,再决定模型能承诺多少步以内的预测。

5.4 缺失值用均值填充、异常尖峰不清理

现象:训练过程中 loss 下降正常,但预测出的曲线比真实数据平滑得多,很多正常波动都被吞掉了。

原因:均值填充会把序列里的局部波动抹平,模型学到的规律变得过于平滑;异常尖峰没清除的话,滑窗会把同一个坏点复制进几十个训练样本,导致模型专门学习这个错误的模式。

解决:列缺失用interpolate(method="linear")保留趋势;对于明显超出正常波动范围的脉冲点,用前后窗口的中位数替换,或者直接剔除再插值。总之先让序列本身干净,再谈训练效果。

5.5 换数据集后直接照搬 seq_len=24

现象:上一个项目里seq_len=24效果好,换到新的业务数据后怎么调都上不去,模型似乎永远慢半拍。

原因:24 这个值通常是为小时粒度数据准备的,因为一天有 24 小时。如果换到日粒度数据,24 天既不是一个完整的周周期,也不是月周期;换到分钟粒度数据,24 分钟又太短。

解决:拿到新数据集先做周期探测。画出数据的自相关图或直接按不同seq_len(7、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, steps=30)) preds = np.array(preds) # shape: (5, 30) lower = np.percentile(preds, 10, axis=0) upper = np.percentile(preds, 90, axis=0)

第二个做法是滚动重训。业务数据不断更新,模型不可能训一次用一年。我一般以一周为周期,把新观测追加到训练集尾部,用上一次训练得到的模型参数作为初始权重,继续训练 10~20 个 epoch。这种方式叫 warm start,比每周从头训练快得多,也更容易适应缓慢的数据漂移。重训后用最新一段真实数据做一轮 h=7 的验证,误差如果明显恶化,说明数据分布发生了结构性变化,需要重新审视特征和窗口设置。

最后分享一个我自己的习惯:任何新数据集跑 LSTM 之前,先跑一个最简单的基线——直接用上一个值当预测值(persistence model)。如果 LSTM 连这个傻瓜基线的误差都压不下来 10% 以上,那不是模型能力问题,而是前面的数据处理或参数选择出了问题。这个习惯帮我挡掉了大量盲目调参的时间,也让我更容易分辨模型是真的学到了规律,还是仅仅在复读历史。希望这些内容能帮你在 LSTM 时间序列预测这条路上少踩几个坑,把代码包真正变成可上线、可解释的预测工具。

本文还有配套的精品资源,点击获取

返回列表