简介:这份资源面向计算机、人工智能、通信工程、自动化等专业的在校学生与教师,也适合希望进阶学习时间序列预测的小白开发者,可用于课程设计、毕业设计、大作业或项目初期立项演示。包内共6个文件,以3个Python源码、2个CSV数据集和1份Markdown说明文档为主,压缩包约929KB,涵盖数据预处理、序列可视化与模型训练预测等模块,围绕PM2.5预测场景展开,代码均经测试可运行,答辩评审平均分达96分。目前已有96人学习下载。读者可据此获得一套完整可复现的LSTM时间序列分析方案,包括原始数据与处理后数据、数据预处理脚本、序列展示脚本及主程序,便于快速理解建模流程、对照运行结果并在此基础上修改扩展,实现其他预测功能。下载后建议先阅读说明文档,仅供学习参考,切勿用于商业用途。
1. LSTM 时间序列预测:从一份能跑通的代码说起
很多人第一次接触 LSTM 时间序列分析预测,是在课程大作业或者项目截止前一周。网上搜到一堆lstm模型代码,clone 下来发现路径不对、依赖缺失、数据集找不到,改了半天 loss 还是 nan。这篇笔记不讲空泛概念,直接围绕一套可运行的 LSTM 时间序列预测 Python 方案,把数据准备、模型搭建、训练调参、结果验证这条链路拆开讲清楚。适合两类人:一是需要完成时间序列预测大作业的学生,二是想把 LSTM 用到设备寿命预测、销量预测、传感器趋势分析等实际场景的工程师。读完你应该能自己搭出一套结构清晰、参数可调、结果可复现的预测流程,而不是只会跑别人的黑匣子脚本。
2. 数据准备与序列构造:把原始时间序列变成 LSTM 能吃的张量
LSTM 时间序列预测翻车最多的地方,往往不在模型本身,而在数据阶段。原始数据通常是一列带时间戳的数值,可能是传感器读数、股票收盘价、电力负荷,也可能是设备振动幅值。LSTM 需要的是三维张量,形状为(样本数, 时间步长, 特征数)。这个转换过程如果搞错,模型训练时 loss 下降但预测结果完全对不上,属于典型的“看起来在学,其实在背噪声”。
2.1 时间序列的归一化与缺失值处理
时间序列数据几乎不可能干干净净。常见问题包括:传感器断连导致整段缺失、单位切换导致量纲突变、人工记录引入异常值。我一般按这个顺序处理:
第一步,把时间列解析成统一格式,按时间排序,去重。第二步,检查缺失比例。如果连续缺失超过 5 个时间步,不要简单用前值填充,而是标记为无效段,后续构造序列时跳过跨越缺失段的窗口。第三步,做归一化。对于 LSTM,MinMax 归一化到[0,1]通常比 Z-Score 更稳,因为 LSTM 的激活函数对输入范围敏感。
import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取数据,假设两列:timestamp, value df = pd.read_csv("data/series.csv", parse_dates=["timestamp"]) df = df.sort_values("timestamp").drop_duplicates("timestamp").reset_index(drop=True) # 标记缺失 df["is_missing"] = df["value"].isna() # 连续缺失段标记:连续缺失超过5个点视为无效段 miss_group = (df["is_missing"] != df["is_missing"].shift()).cumsum() miss_len = df.groupby(miss_group)["is_missing"].transform("sum") df["valid"] = ~((df["is_missing"]) & (miss_len > 5)) # 对有效段做插值,再归一化 df["value_filled"] = df["value"].interpolate(method="linear", limit=5) scaler = MinMaxScaler(feature_range=(0, 1)) df["value_scaled"] = scaler.fit_transform(df[["value_filled"]])这段代码的关键参数是limit=5,表示最多插值 5 个连续缺失点,超过就保留 NaN 并在后续窗口构造时排除。valid列用来控制哪些时间步可以参与训练。归一化器必须只在训练集上 fit,否则验证集和测试集的信息会泄漏,导致评估结果虚高。
2.2 滑动窗口构造与训练集划分
LSTM 时间序列预测的核心是把单变量序列变成监督学习问题。假设用过去 24 个时间步预测下一个点,窗口就是 24,标签是第 25 个点。这里有两个容易踩的坑:一是先划分训练测试再构造窗口,还是先构造窗口再划分;二是窗口之间是否允许重叠。
我的做法是先按时间顺序划分训练、验证、测试三段,比例通常 7:1.5:1.5,然后在各自段内独立构造窗口。这样避免未来信息泄漏到训练集。窗口重叠步长一般设为 1,如果数据量太大可以设为窗口的一半来降采样。
def make_windows(values, valid_flags, window_size=24, horizon=1): X, y = [], [] for i in range(len(values) - window_size - horizon + 1): # 窗口内所有点必须有效 if not valid_flags[i:i+window_size+horizon].all(): continue X.append(values[i:i+window_size]) y.append(values[i+window_size:i+window_size+horizon]) return np.array(X), np.array(y) window_size = 24 horizon = 1 n = len(df) train_end = int(n * 0.7) val_end = int(n * 0.85) train_X, train_y = make_windows( df["value_scaled"].values[:train_end], df["valid"].values[:train_end], window_size, horizon ) val_X, val_y = make_windows( df["value_scaled"].values[train_end:val_end], df["valid"].values[train_end:val_end], window_size, horizon ) test_X, test_y = make_windows( df["value_scaled"].values[val_end:], df["valid"].values[val_end:], window_size, horizon ) # LSTM 输入需要三维:(样本, 时间步, 特征) train_X = train_X.reshape(-1, window_size, 1) val_X = val_X.reshape(-1, window_size, 1) test_X = test_X.reshape(-1, window_size, 1)window_size的选择取决于数据周期性和预测跨度。如果数据有明显日周期,窗口至少覆盖一个完整周期。horizon是预测步数,单步预测设为 1,多步预测可以设为 6、12、24。注意make_windows里用valid_flags过滤了跨越缺失段的窗口,这一步不做的话,插值出来的假数据会污染训练。
2.3 数据集划分的边界与常见误用
很多人把时间序列当普通回归问题,直接train_test_split(random_state=42),这是典型误用。时间序列的顺序本身就是信息,随机打乱会让模型在训练时看到未来数据,评估指标好看但上线就崩。正确做法永远是按时间切分。
另一个边界问题是验证集和测试集的窗口是否应该与训练集有重叠。如果数据量很小,允许少量重叠可以增加样本,但要在论文或报告中说明。如果数据量充足,建议严格不重叠。
| 划分方式 | 适用场景 | 风险 |
|---|---|---|
| 按时间 7:1.5:1.5 | 大多数单变量预测 | 无泄漏,评估可信 |
| 随机打乱 | 不适用于时间序列 | 未来信息泄漏,指标虚高 |
| 交叉验证 | 数据极少时 | 需用时间序列专用 CV,普通 KFold 不可用 |
3. PyTorch 搭建 LSTM 预测模型:层数、隐藏单元与 Dropout 怎么定
数据准备好之后,模型结构决定了你能不能学到长期依赖。LSTM 的核心是门控机制,但实际写代码时,真正影响结果的是层数、隐藏单元数、Dropout 位置和学习率调度。这一章给出一套我常用的 PyTorch LSTM 结构,并解释每个参数的取舍。
3.1 LSTM 层参数:hidden_size、num_layers 与 batch_first
PyTorch 的nn.LSTM有几个关键参数。input_size是特征数,单变量预测就是 1。hidden_size是隐藏状态维度,常见取值 32、64、128。num_layers是堆叠层数,1 到 3 层足够大多数任务。batch_first=True让输入形状为(batch, seq, feature),不设的话是(seq, batch, feature),很容易搞混。
import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2, horizon=1): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0 ) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_size, horizon) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的隐藏状态 last = out[:, -1, :] last = self.dropout(last) return self.fc(last)dropout只在num_layers > 1时生效,这是 PyTorch 的设计。如果只有一层 LSTM,Dropout 要加在 LSTM 输出之后,代码里已经用self.dropout处理了。horizon控制输出维度,单步预测为 1,多步预测直接改成对应步数,不需要改 LSTM 部分。
3.2 训练循环:损失函数、优化器与早停
时间序列回归常用 MSE 或 MAE。MSE 对大误差敏感,适合希望预测曲线贴合峰值的场景;MAE 更鲁棒,适合有异常值的场景。优化器我一般用 Adam,学习率 1e-3 起步,配合ReduceLROnPlateau在验证损失不下降时减半。
from torch.utils.data import DataLoader, TensorDataset from torch.optim.lr_scheduler import ReduceLROnPlateau device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = LSTMForecaster(hidden_size=64, num_layers=2, dropout=0.2).to(device) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=5) train_loader = DataLoader( TensorDataset(torch.FloatTensor(train_X), torch.FloatTensor(train_y)), batch_size=64, shuffle=True ) val_loader = DataLoader( TensorDataset(torch.FloatTensor(val_X), torch.FloatTensor(val_y)), batch_size=64, shuffle=False ) best_val = float("inf") patience_counter = 0 for epoch in range(100): model.train() for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() val_loss = 0.0 with torch.no_grad(): for xb, yb in val_loader: xb, yb = xb.to(device), yb.to(device) val_loss += criterion(model(xb), yb).item() * len(xb) val_loss /= len(val_loader.dataset) scheduler.step(val_loss) if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), "best_lstm.pt") patience_counter = 0 else: patience_counter += 1 if patience_counter >= 10: breakclip_grad_norm_是 LSTM 训练稳定的关键,梯度爆炸在时间序列里很常见。早停耐心值设为 10,配合学习率衰减,通常 50 个 epoch 内收敛。保存best_lstm.pt而不是最后一个 epoch 的权重,避免过拟合。
3.3 多步预测与序列到序列的取舍
单步预测只输出一个值,多步预测有两种做法:直接多输出和自回归滚动。直接多输出是把fc输出维度改成horizon,一次预测未来多个点。自回归是用预测值填回输入窗口,逐步滚动。前者训练稳定但长跨度精度下降快,后者灵活但误差会累积。
我一般先用直接多输出做基线,如果horizon超过 12 且精度不够,再考虑 Seq2Seq 结构。对于大作业场景,直接多输出足够,代码改动最小。
4. 训练结果验证与调参:loss 曲线、反归一化与指标解读
模型跑起来只是第一步,能不能交差取决于验证环节。这一章讲怎么判断模型是真的学到了模式,还是只是记住了训练集的均值。
4.1 反归一化与预测曲线对比
模型输出是归一化后的值,必须用训练集的 scaler 反变换回原始量纲,否则指标没有物理意义。反归一化时注意 scaler 是在训练集上 fit 的,验证和测试集只能用 transform,不能重新 fit。
model.load_state_dict(torch.load("best_lstm.pt")) model.eval() with torch.no_grad(): test_pred = model(torch.FloatTensor(test_X).to(device)).cpu().numpy() # 反归一化 test_pred_inv = scaler.inverse_transform(test_pred) test_true_inv = scaler.inverse_transform(test_y) import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.plot(test_true_inv, label="true") plt.plot(test_pred_inv, label="pred") plt.legend() plt.title("LSTM 时间序列预测:测试集对比") plt.show()如果预测曲线整体滞后于真实曲线,说明窗口太小或模型没学到趋势。如果预测曲线过于平滑,说明 Dropout 太大或隐藏单元太少。如果只在峰值处偏差大,考虑换 MAE 损失或增加峰值样本权重。
4.2 评估指标:MAE、RMSE、MAPE 的适用边界
| 指标 | 公式含义 | 适用场景 | 注意 |
|---|---|---|---|
| MAE | 平均绝对误差 | 有异常值 | 量纲与原数据一致 |
| RMSE | 均方根误差 | 关注大误差 | 对大误差惩罚重 |
| MAPE | 平均绝对百分比误差 | 跨量纲对比 | 真实值接近 0 时爆炸 |
| R² | 拟合优度 | 报告整体解释力 | 非平稳序列可能为负 |
我通常三个都算,但报告里以 MAE 和 RMSE 为主。MAPE 在电力负荷、销量预测里常用,但如果序列有接近零的值,MAPE 会失真,这时候用 SMAPE 替代。
4.3 调参顺序:先窗口,再层数,最后学习率
调参不要一上来就网格搜索,浪费时间。我的顺序是:先固定模型结构,调window_size,因为窗口决定输入信息量;然后调hidden_size和num_layers,观察验证损失;最后调学习率和 Dropout。每次只动一个参数,记录验证集 MAE。
常见经验值:window_size取 24、48、72;hidden_size取 32、64、128;num_layers取 1、2;dropout取 0.1 到 0.3。如果验证损失震荡,降低学习率或增大 batch size。如果训练损失下降但验证损失上升,加 Dropout 或减层数。
5. 避坑与排查:LSTM 时间序列预测最常见的 5 个翻车现场
这一章按「现象 → 原因 → 解决」写,都是我在实际项目和帮人改代码时反复遇到的。
5.1 现象:loss 变成 nan
原因:学习率太大、梯度爆炸、输入里有 NaN 或 inf。解决:先检查数据里有没有未处理的缺失值,用np.isnan(train_X).sum()确认。然后加clip_grad_norm_,学习率降到 1e-4。如果还不行,把 LSTM 换成 GRU 试试,GRU 参数少,训练更稳。
5.2 现象:预测结果是一条直线
原因:模型没学到任何东西,输出接近均值。常见于窗口太小、隐藏单元太少、或者归一化后数据方差极小。解决:增大window_size到至少覆盖一个周期,hidden_size提到 64 以上。检查归一化后的数据标准差,如果小于 0.01,说明原始数据变化太小,考虑做差分或对数变换。
5.3 现象:训练集 loss 很低,测试集一塌糊涂
原因:过拟合。LSTM 参数多,小数据集上很容易记住训练样本。解决:加 Dropout、减num_layers、加 L2 正则化、早停。如果数据量确实太少,考虑用数据增强,比如加轻微噪声、时间缩放。
5.4 现象:预测曲线整体滞后一个时间步
原因:窗口构造时标签对齐错了,或者模型只学会了用前一个值预测当前值。解决:检查make_windows里y的索引是不是i+window_size,不是i+window_size-1。如果对齐正确但还滞后,说明模型太依赖短期记忆,增大window_size或加注意力机制。
5.5 现象:GPU 显存不够,batch 调小后训练不稳定
原因:LSTM 的显存占用与batch_size × seq_len × hidden_size成正比。解决:用梯度累积模拟大 batch,或者把seq_len截断成更小的窗口。另外,num_layers增加也会线性增加显存,优先减层数而不是减hidden_size。
6. 把 LSTM 预测做成可复用的工程模块:配置化与滚动预测
大作业交完不是终点。如果你打算把 LSTM 时间序列预测用到实际项目里,最后这一步很关键:把训练、验证、预测拆成配置驱动的模块,而不是一堆写死的脚本。我一般会建一个config.yaml,把window_size、hidden_size、num_layers、dropout、lr、batch_size、horizon全部外置。这样换数据集时只改配置,不动代码。
import yaml with open("config.yaml", "r") as f: cfg = yaml.safe_load(f) model = LSTMForecaster( input_size=cfg["input_size"], hidden_size=cfg["hidden_size"], num_layers=cfg["num_layers"], dropout=cfg["dropout"], horizon=cfg["horizon"] )滚动预测是另一个实用技巧。训练时用固定窗口,预测时每次把最新预测值追加到窗口末尾,去掉最旧的值,逐步输出未来 N 步。这样做的好处是不用重新训练就能得到任意长度的预测,代价是误差会累积。我的习惯是滚动步数不超过窗口的一半,超过就重新训练一个直接多输出模型。
验证模块是否可复用,有个简单方法:换一份新数据,只改config.yaml里的路径和窗口参数,跑一遍完整流程,看能不能在 10 分钟内得到预测曲线和指标。如果还要改代码,说明耦合太深。我踩过最深的坑是把归一化器、窗口参数、模型结构散落在三个文件里,换数据集时漏改一处,结果指标看着正常但预测完全错位,排查了一下午。后来所有可变参数全部进配置文件,代码只读配置,再也没出过这类问题。希望帮到你。
本文还有配套的精品资源,点击获取