简介:这份资源面向希望快速上手深度学习时间序列预测的开发者与学习者,提供一套基于LSTM神经网络的完整Python实现方案,覆盖从数据清洗、特征提取到建模与预测的全流程,适合具备一定Python基础、想深入理解循环神经网络在时序任务中应用的人群。压缩包共125个文件,约5.42MB,其中75个py脚本承担数据处理与模型训练逻辑,26个csv提供空气质量等时序数据,另有txt说明、checkpoint与h5权重文件等,便于直接复现与二次开发。已有1356人学习下载,说明该方案在同类资源中具备一定参考价值。代码下载即用、无需修改即可运行,读者可借此掌握LSTM时序建模的完整链路,理解数据预处理与特征工程对预测效果的影响,并基于现有模型结构替换自有数据集开展实验,快速搭建可运行的预测基线。
1. 从一份 95 分的时间序列预测源码说起:LSTM 到底解决了什么
拿到「python实现基于LSTM神经网络的时间序列预测源码+模型(95分以上).zip」这个标题,很多人第一反应是去找一份能直接跑的 lstm模型代码,把数据喂进去看 loss 曲线往下掉。但真正落地过时间序列预测的人都知道,分数高不高,八成取决于数据怎么切、特征怎么造、评估指标怎么定,模型结构反而是最后才调的那一环。这份源码之所以能打到 95 分以上,核心不在 LSTM 本身有多神,而在于它把「单步预测」和「多步滚动预测」的边界划清楚了,把归一化、滑窗、反归一化这三件事串成了一条不会漏数据的流水线。
这篇文章面向两类人:一类是刚学完 python入门、想拿 lstm预测练手的新手,跟着步骤能复现一条完整的训练加推理链路;另一类是做业务预测的熟手,关心滑窗长度、batch 组织、评估口径这些参数怎么设才不翻车。我会按「数据怎么变成监督样本 → 模型怎么搭 → 训练怎么稳 → 预测怎么不泄漏 → 坑在哪」的顺序讲,中间给可直接抄的代码块和参数表。需要说明的是,LSTM 不是万能钥匙,它对周期性明显、噪声可控的序列效果好,对突变型、强外生变量的场景,往往要配合前馈神经网络或一维卷积神经网络做特征提取,这点后面会展开。
2. 把原始序列切成 LSTM 能吃的监督样本
2.1 为什么时间序列不能直接丢进 LSTM
LSTM 的输入张量形状是(batch, timesteps, features),而原始时间序列通常是一列(N, 1)的数值。中间这一步转换,就是所谓的「滑窗构造监督学习样本」。假设用过去 24 个点预测下一个点,那第 i 个样本的输入是data[i-24:i],标签是data[i]。这一步做错,后面模型再准也是假的,因为标签和特征错位会导致验证集分数虚高。
常见做法是先做差分或去趋势,再归一化。归一化必须只用训练集的 min/max 或 mean/std,验证集和测试集要用训练集的统计量来变换,否则就是典型的数据泄漏。我一般会把整个流程封装成一个类,避免手写索引时把i和i+1搞混。
import numpy as np import pandas as pd class TimeSeriesWindow: def __init__(self, look_back=24, horizon=1): self.look_back = look_back # 输入窗口长度 self.horizon = horizon # 预测未来第几步 def build(self, series): X, y = [], [] # 从 look_back 开始,保证每个样本都有完整历史 for i in range(self.look_back, len(series) - self.horizon + 1): X.append(series[i - self.look_back:i]) y.append(series[i + self.horizon - 1]) return np.array(X), np.array(y) # 归一化只用训练段统计量 def normalize(train, others): mu, sigma = train.mean(), train.std() return (train - mu) / sigma, [(o - mu) / sigma for o in others], mu, sigma逻辑说明:build里循环上界是len(series) - self.horizon + 1,这样当horizon=1时最后一个标签正好是序列最后一个点,不会越界。normalize返回训练段统计量,反归一化时用同一组mu/sigma,这是避免泄漏的关键。参数上,look_back决定模型能看多远的历史,太小会欠拟合周期性,太大会引入过多噪声并拖慢训练,实践中先用一个完整周期长度试,比如日数据用 7 或 30,小时数据用 24 或 168。
2.2 训练/验证/测试的切分顺序不能乱
时间序列不能随机 shuffle 后再切分,必须按时间先后切。正确顺序是:先按时间切出 train / val / test 三段,再对 train 做滑窗,val 和 test 各自独立滑窗。很多人图省事把全量数据滑窗后再切,结果验证集的窗口里混进了训练期的点,分数自然好看,上线就崩。
| 切分方式 | 是否泄漏 | 适用场景 |
|---|---|---|
| 随机 shuffle 后切分 | 严重泄漏 | 不适用于任何时序任务 |
| 全量滑窗后切分 | 中度泄漏 | 仅当窗口间完全独立时勉强可用 |
| 先切时间段再各自滑窗 | 无泄漏 | 推荐,本文采用 |
切分比例上,数据量小于 1 万条时我一般用 70/15/15,大于 10 万条时用 90/5/5,因为大样本下验证集不需要那么多也能稳定评估。验证集的作用是早停和调参,测试集只在最后跑一次,跑多了就等于拿测试集调参,指标会失真。
3. 搭一个不玄学的 LSTM 预测模型
3.1 网络结构:几层、多少隐藏单元、要不要 dropout
标题里的 lstm神经网络,落到代码上就是nn.LSTM或keras.layers.LSTM。结构上我建议从「单层 LSTM + 全连接输出」起步,隐藏单元数取look_back的 1 到 2 倍,比如look_back=24就用 32 或 64。层数超过两层在中小规模时序上收益很小,反而容易过拟合。dropout 放在 LSTM 输出和全连接之间,比例 0.1 到 0.3,数据越少取值越大。
下面用 PyTorch 写一个最小可用模型,输入维度是 1(单变量),输出维度也是 1。
import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=1, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, # 输入形状 (batch, seq, feature) dropout=dropout if num_layers > 1 else 0.0 ) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, look_back, 1) out, (h_n, c_n) = self.lstm(x) last = out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(self.dropout(last))逻辑说明:batch_first=True让输入按(batch, seq, feature)排列,和前面构造的样本形状一致。out[:, -1, :]取序列最后一步的输出,因为预测目标依赖最近的历史。num_layers=1时 dropout 参数在 LSTM 内部不生效,所以单独加了一个nn.Dropout。参数上,hidden_size是主要容量旋钮,从 32 开始试,验证集 loss 不降再往上加;dropout超过 0.5 通常会导致欠拟合。
3.2 训练循环:损失、优化器、早停三件套
损失函数用 MSE 或 Huber,序列里有离群点时 Huber 更稳。优化器用 Adam,学习率 1e-3 起步,配合ReduceLROnPlateau在验证 loss 停滞时降学习率。早停的 patience 设 10 到 20 个 epoch,保存验证 loss 最低的权重,这是最省事的后悔药。
from torch.utils.data import DataLoader, TensorDataset def train_model(model, X_train, y_train, X_val, y_val, epochs=100, patience=15): train_ds = TensorDataset(torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)) loader = DataLoader(train_ds, batch_size=64, shuffle=True) # 训练集可 shuffle optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5) criterion = nn.HuberLoss() best_loss, wait, best_state = float('inf'), 0, None for epoch in range(epochs): model.train() for xb, yb in loader: optimizer.zero_grad() pred = model(xb).squeeze(-1) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() with torch.no_grad(): val_pred = model(torch.tensor(X_val, dtype=torch.float32)).squeeze(-1) val_loss = criterion(val_pred, torch.tensor(y_val, dtype=torch.float32)).item() scheduler.step(val_loss) if val_loss < best_loss: best_loss, wait = val_loss, 0 best_state = {k: v.clone() for k, v in model.state_dict().items()} else: wait += 1 if wait >= patience: break model.load_state_dict(best_state) return model, best_loss逻辑说明:训练集shuffle=True是允许的,因为样本之间已经通过滑窗构造,打乱不破坏时间顺序;验证集不 shuffle,保持评估稳定。clip_grad_norm_防止 LSTM 梯度爆炸,这是血泪经验,尤其在学习率偏大时。best_state保存最优权重,避免最后一个 epoch 恰好过拟合。参数上,batch_size在 32 到 128 之间试,太小训练慢,太大泛化差;patience太小会早停过早,太大浪费算力。
4. 预测阶段:单步、滚动多步与评估口径
4.1 单步预测和滚动多步预测的区别
单步预测是每次用真实历史预测下一个点,评估时每个点都有真实输入。滚动多步预测是用模型自己的输出作为下一步输入,误差会累积。业务上如果只关心「下一时刻」,用单步;如果要预测未来 24 小时,必须用滚动,并且要接受误差随步数放大。源码里 95 分以上的关键,往往是评估时明确写了是哪种模式,而不是混着算。
def rolling_forecast(model, last_window, steps, mu, sigma): model.eval() window = last_window.copy() # 形状 (look_back,) preds = [] with torch.no_grad(): for _ in range(steps): x = torch.tensor(window, dtype=torch.float32).view(1, -1, 1) p = model(x).item() preds.append(p) window = np.append(window[1:], p) # 滚动:丢掉最老的点,加入预测值 return np.array(preds) * sigma + mu # 反归一化逻辑说明:window[1:]加p实现滚动,注意这里p是归一化空间的预测值,直接拼回窗口,最后统一反归一化。如果每步都反归一化再拼回去,会引入额外的非线性误差。参数上,steps就是预测步数,超过look_back后模型输入里全是自己的预测,误差会明显上升,这时候要考虑多模型或直接多输出结构。
4.2 评估指标怎么选才不骗自己
MAE、RMSE、MAPE 是最常用的三个。MAPE 在真实值接近 0 时会爆炸,序列里有零值就别用。我一般主看 RMSE,辅看 MAE,再画一张预测 vs 真实的对比图,肉眼看相位有没有偏移。如果验证集 RMSE 很低但图上明显滞后一个周期,说明模型只是学会了「复制上一个点」,这时候要检查look_back是否太短或特征是否太单一。
| 指标 | 公式含义 | 适用与陷阱 |
|---|---|---|
| MAE | 绝对误差均值 | 对离群点稳健,量纲与原序列一致 |
| RMSE | 均方误差开根 | 对大误差敏感,适合关注极端偏差 |
| MAPE | 绝对百分比误差 | 真实值含 0 或接近 0 时不可用 |
评估一定要在反归一化后的原始量纲上算,在归一化空间算出来的指标没有业务解释力。另外,测试集只跑一次,跑完记录指标和随机种子,方便复现。
5. 避坑与排查:LSTM 时序预测最常见的 5 个翻车点
5.1 现象:验证 loss 远低于测试 loss
原因:滑窗时验证集窗口和训练集窗口有重叠,或者归一化用了全量统计量。解决:先按时间切段再各自滑窗,归一化统计量只从训练段计算,验证和测试用同一组mu/sigma。
5.2 现象:预测曲线整体滞后一个周期
原因:look_back太短,模型只能依赖最近一个点;或者损失函数对相位不敏感。解决:把look_back加到一个完整周期以上,检查输入里是否包含时间特征(小时、星期),必要时改用多变量输入。
5.3 现象:训练 loss 震荡不收敛
原因:学习率过大或 batch 太小,LSTM 梯度爆炸。解决:学习率降到 1e-3 或 5e-4,加clip_grad_norm_,batch 提到 64 以上,检查输入是否已归一化。
5.4 现象:滚动多步预测几步后变成一条直线
原因:模型输出趋近均值,误差累积后失去变化。解决:减少预测步数,或改用直接多输出结构(一次输出未来 N 步),避免用预测值反复喂回输入。
5.5 现象:换一批数据分数暴跌
原因:模型对训练段的均值和方差过拟合,新数据分布不同。解决:用滚动窗口重新训练,或改用对分布偏移更稳健的差分输入,评估时同时看多个时间段。
6. 让 95 分真正可复现:固定种子、多变量扩展与验证习惯
想把这份源码的分数稳定复现,第一件事是固定随机种子。PyTorch、NumPy、Python 内置随机都要设,否则同一份代码两次跑出来的 RMSE 能差出几个百分点,这在汇报时非常尴尬。
import random, numpy as np, torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False逻辑说明:cudnn.deterministic=True会让卷积和部分算子走确定性实现,速度略降但结果可复现。benchmark=False关闭自动调优,避免不同输入尺寸下选不同算法。参数上,seed随便定,但一旦定了就别改,所有对比实验用同一个。
第二件事是考虑多变量扩展。单变量 LSTM 只看历史值,遇到促销、节假日、天气这类外生变量就无能为力。常见做法是把这些变量作为额外特征拼到输入张量的最后一维,input_size从 1 改成特征数。如果外生变量本身也是序列,可以先用一维卷积神经网络做局部特征提取,再送进 LSTM,这是工业界比较稳的组合。
第三件事是建立验证习惯:每次改完参数,先跑验证集,记录 RMSE 和曲线图,再决定要不要动测试集。我自己的习惯是维护一个实验记录表,列清楚look_back、hidden_size、dropout、学习率、验证 RMSE,跑过十几组之后就能看出哪些参数是真正敏感的。LSTM 调参没有玄学,只有记录和对比。希望帮到你。
本文还有配套的精品资源,点击获取