简介:基于LSTM的时间序列分析预测Python源码包,面向希望掌握深度学习时序建模的Python开发者与数据科学初学者,解决从数据预处理到模型训练、评估与预测的完整链路问题。压缩包共126个文件,以75个Python脚本为核心,覆盖LSTM模型构建、训练与预测逻辑;26个CSV文件提供空气污染等多组实验数据;另有TensorFlow模型检查点、H5模型文件及说明文档,便于复现与二次开发。资源包大小仅5.42MB,轻量易下载。已有5095人学习使用。通过阅读与运行源码,读者可理解LSTM门控机制、Keras建模流程、归一化及序列构造等关键知识点,并掌握用MSE、MAE等指标评估预测效果的方法。项目包含从原始数据读取到最终预测的整套实现,代码结构清晰,适合结合理论教程进行动手实践,是入门时序预测与人工智能应用的实用素材。
1. 基于 LSTM 的时间序列分析预测源码包:先别急着跑 demo,看这三处
拿到带「Python 源码」的 LSTM 时间序列预测压缩包,我一般不会先点开训练脚本。先看三处:数据从哪进、窗口怎么滑、预测完之后怎么回到原始尺度。这三处决定了这份源码是真能用于生产,还是只能跑个示意图。LSTM 解决的是带时间依赖的预测问题,比如设备寿命预测、水位流量预测、电力负荷预测。适合已经有 Python 基础、手头有"一条条按时间排好序数据"的从业者。核心思路不复杂:用过去 N 个时间步去预测未来 M 个时间步。但落地时,数据形状、归一化、反归一化这些环节才是真正决定成败的地方。
2. 时间序列预测为什么用 LSTM:选型逻辑与数据准备
2.1 LSTM 在时序预测里的定位:它解决了 RNN 的什么问题
传统 RNN 处理序列时,信息每经过一个时间步就要乘一次权重矩阵,梯度在反向传播中会指数级衰减或爆炸。时间跨度过长,前面几步的信息根本传不到输出的损失里,这就是常说的长期依赖缺失。LSTM 引入了一个贯穿所有时间步的 cell state 通道,信息可以在这个通道里近乎无损地流动,配合输入门、遗忘门、输出门决定"记什么、忘什么、放什么出去"。因此在中等长度的时序数据上,LSTM 比 RNN 稳定得多。
与 GRU 相比,GRU 把三个门简化成两个门,参数更少、训练更快。但在工业界和学术界,LSTM 在时间序列预测里的生态最成熟,参考实现、调参经验都最丰富。我的习惯是数据量少于 1 万条时先试单层 LSTM,数据量大再加层数。下面是选型上的直观对比:
| 模型 | 参数数量 | 长期依赖能力 | 训练速度 | 适用场景 |
|---|---|---|---|---|
| RNN | 少 | 差,梯度易消失 | 快 | 极短序列、基线对比 |
| LSTM | 多 | 强,cell state 通道 | 较慢 | 中等长度时序预测 |
| GRU | 中 | 较强 | 较快 | 数据量小、对推理速度敏感 |
| Transformer | 最多 | 最强,但依赖大样本 | 视规模而定 | 长序列、大规模数据 |
实际做预测时,LSTM 最常见的用法是"滑窗监督学习":把时间序列切成一段段的窗口,用窗口内的数据作为特征,窗口之后的一个或几个时间点作为标签,变成一个标准回归问题。所以拿到源码先看它怎么构造这个窗口,等于看懂了整份代码的骨架。
2.2 滑窗构造样本集:一份可直接改用的数据处理代码
一份合格的 LSTM 预测源码里,数据准备代码占了至少一半的工作量。下面是我把单变量序列转成监督学习格式的常用写法,你拿到源码后可以直接对照:
import numpy as np from sklearn.preprocessing import MinMaxScaler def build_dataset(series, window=24, horizon=1, train_ratio=0.8): """ 将一维时间序列构造成 LSTM 训练样本 参数: series: 原始一维序列, numpy array, 形状 (n,) window: 每个样本用过去多少个时间步作为特征 horizon: 预测未来多少个时间步, 默认 1 train_ratio: 训练集占比, 按时间顺序切分 """ # 归一化必须在切分之前做, 避免测试集信息混入训练集 scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(series.reshape(-1, 1)).flatten() X, y = [], [] for i in range(len(scaled) - window - horizon + 1): # 特征: [i, i+window) 之间的数据 X.append(scaled[i:i + window]) # 标签: [i+window, i+window+horizon) 之间的数据 y.append(scaled[i + window:i + window + horizon]) X = np.array(X).reshape(-1, window, 1) # (样本数, window, 特征数) y = np.array(y) # 按时间顺序切分, 不能用随机打乱 split = int(len(X) * train_ratio) return X[:split], X[split:], y[:split], y[split:], scaler几个关键点。归一化必须在构造样本前做,MinMaxScaler只调一次fit,把序列压到 0 到 1 之间,这样 LSTM 的输入输出都在同一量级,训练收敛快得多。X的形状是(样本数, window, 特征数),PyTorch 的 LSTM 层默认接受这种(batch, seq_len, input_size)结构。window大小取多少,跟业务周期相关:预测小时级电力负荷,24 就是一天;设备振动数据预测寿命,window 可能要 100 以上。训练集和验证集只能按时间顺序连续切分,千万不能随机打散——时间序列的验证集必须是"未来",否则模型在训练阶段偷看不到的"未来",验证分数就失真了。
3. 用 Python 复现 LSTM 预测核心流程:模型定义、训练与预测接口
3.1 模型定义:PyTorch 里写一个最小可用的 LSTM 类
拿到源码后先找模型定义部分。一个标准做法是nn.LSTM负责提取序列特征,再接一个全连接层输出预测值。下面是完整的最小实现:
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1, dropout=0.2): super(LSTMPredictor, self).__init__() 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 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x 形状: (batch, window, input_size) lstm_out, _ = self.lstm(x) # 取最后一个时间步的隐藏状态作为整条序列的总结 last_step = lstm_out[:, -1, :] pred = self.fc(last_step) return pred这里三个参数最影响效果。hidden_size是 LSTM 隐藏单元数,64 到 128 是中等规模时序的常见起点,太小拟合不了复杂依赖,太大在小数据集上容易过拟合。num_layers=2表示堆叠两层 LSTM,层数越多模型越深,但训练更慢,数据少于 1 万条时 2 层基本够用。batch_first=True让输入形状从(seq_len, batch, input_size)变成(batch, seq_len, input_size),不用每次手动转置,代码可读性高很多。
lstm_out[:, -1, :]是所有时间步的输出,维度是(batch, window, hidden_size)。这里只取每个序列最后一个时间步的隐藏状态,因为在这个框架下,最后一步隐含了整条历史窗口的信息,全连接层拿它回归出预测值。
3.2 训练循环:损失函数、优化器与 early stopping
训练循环是源码里改动最频繁的部分。损失函数一般用均方误差 MSE,因为它对大的预测偏差惩罚更重,与回归任务的目标一致。优化器优先 Adam,学习率 1e-3 起步。下面这段代码可以直接替换源码里的训练主循环:
def train_model(model, train_loader, val_loader, epochs=100, lr=1e-3, patience=10): optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() best_val_loss = float('inf') bad_epochs = 0 for epoch in range(epochs): model.train() train_loss = 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) # (batch, horizon) loss = criterion(pred, y_batch) loss.backward() optimizer.step() train_loss += loss.item() * x_batch.size(0) # 每个 epoch 结束跑一遍验证集 model.eval() val_loss = 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: pred = model(x_batch) val_loss += criterion(pred, y_batch).item() * x_batch.size(0) train_loss /= len(train_loader.dataset) val_loss /= len(val_loader.dataset) # early stopping: 验证集不再下降就停止 if val_loss < best_val_loss: best_val_loss = val_loss bad_epochs = 0 torch.save(model.state_dict(), 'best_model.pt') else: bad_epochs += 1 if bad_epochs >= patience: print(f'epoch {epoch}: early stop') break if epoch % 10 == 0: print(f'epoch {epoch}: train_loss={train_loss:.5f}, val_loss={val_loss:.5f}')训练循环里有几个值得注意的细节。model.eval()配合torch.no_grad(),告诉框架关闭 Dropout 和梯度计算,验证阶段的输出才是稳定的。loss.item()取出的是标量损失值,而loss本身还挂着计算图,直接累加会导致内存不断膨胀,训练到几百个 epoch 后越来越慢,这是新手跑源码最容易忽略的问题。
lr=1e-3是 Adam 的常见默认值。如果验证集 loss 震荡,优先把 lr 降到 3e-4 或 1e-4,而不是调整模型结构。patience=10表示连续 10 个 epoch 验证集没有改进就提前结束,避免白白浪费算力。torch.save只保存模型参数,不保存整个模型对象,加载时要求先实例化一个结构相同的模型。
3.3 预测阶段:滚动预测与误差累积
训练完成后进入预测阶段。这个阶段最容易踩坑,因为训练时可以看到标签,标注"下一个值应该是什么";预测时标签不存在,模型得用自己的输出作为下一步的输入,这就是滚动预测。
def rolling_forecast(model, x_seed, scaler, n_steps): """ 从 x_seed(形状 1, window, input_size) 出发 滚动预测未来 n_steps 个时间步 """ model.eval() forecast = [] with torch.no_grad(): window = x_seed.clone() for _ in range(n_steps): pred = model(window) # (1, horizon) pred_value = pred[0, 0].item() forecast.append(pred_value) # 把预测值追加到窗口末尾, 丢掉窗口最前面的旧值 new_value = torch.tensor([[[pred_value]]]) window = torch.cat([window[:, 1:, :], new_value], dim=1) forecast = np.array(forecast).reshape(-1, 1) return scaler.inverse_transform(forecast).flatten()这段代码的核心是torch.cat那条线:每一步把模型刚生成的预测值拼到窗口末尾,丢掉时间最早的一个值,窗口长度保持window不变。预测出来的序列会存在误差累积——后面步长的输入是前面的预测结果,预测的偏差会不断被放大。预测步数越多,曲线越平滑,甚至趋于一个平台。这份源码如果你要拿去生产,一般只会用未来 5 到 10 步的滚动预测结果。
4. 评估预测结果:先回归到真实尺度,再谈指标
4.1 反归一化与评估指标计算顺序
训练时光看归一化后的 MSE 是片面的,因为 0 到 1 区间的误差放到原始单位下,用户根本无感。正确顺序是先反归一化、再算指标。下面给出一段能直接替换源码评估部分的代码:
from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_forecast(model, X_test, y_test, scaler): model.eval() with torch.no_grad(): pred_norm = model(X_test).numpy() # 归一化尺度下的预测 # 反归一化: 把预测值和真实值都还原到原始单位 y_test_np = y_test.numpy() # 形状 (n, horizon) y_test_inv = scaler.inverse_transform( y_test_np.reshape(-1, 1)).reshape(y_test_np.shape) pred_inv = scaler.inverse_transform( pred_norm.reshape(-1, 1)).reshape(pred_norm.shape) rmse = np.sqrt(mean_squared_error(y_test_inv, pred_inv)) mae = mean_absolute_error(y_test_inv, pred_inv) mape = np.mean(np.abs((y_test_inv - pred_inv) / y_test_inv)) * 100 return {'rmse': rmse, 'mae': mae, 'mape': mape}反归一化时要特别注意scaler对象必须来自训练集。MinMaxScaler在构建数据集时已经fit过了,保存了训练集的最小值和最大值,测试集和预测值都只能调用同一个 scaler 的inverse_transform,不能再重新 fit,否则还原出来的数值完全对不上。
MAPE 会有一个会被忽略的坑:如果真实值接近 0,这个指标会趋近无穷大,看起来模型"彻底失败",其实是分母问题。遇到这种情况,我一般直接用 RMSE 加 MAE 汇报。
4.2 多步预测的三种推进策略与选择逻辑
horizon大于 1 时,预测未来多步有三种常见策略,源码里可能只实现了其中一种,你得先看清楚是哪种。递归多步预测最直观:预测完第 1 步,把结果拼回窗口再预测第 2 步,上一节代码就是这个思路。优点是模型结构简单、输出维度固定,缺点是误差随步数累积,步数越长预测越不可信。直接多步预测则是把输出层改成linear(hidden_size, horizon),一次吐出未来多个时间步。损失是每一步的误差同时反传,训练快,但各步之间没有显式约束,曲线可能出现不合理的跳变。Seq2Seq 结构在两者之间做了折中:编码器压缩历史信息,解码器逐步生成未来序列,精度最高,但训练和调试成本都上了一个台阶。
| 策略 | 实现复杂度 | 误差累积 | 适合场景 |
|---|---|---|---|
| 递归多步 | 低 | 高 | 预测步数少(≤5) |
| 直接多步 | 低 | 低 | 步数固定、短期预测 |
| Seq2Seq | 高 | 中等 | 长序列、业务要求高 |
选择时不用过度纠结。数据量少、步数短,直接多步最省事;步数多、对精度有要求,再考虑 Seq2Seq。许多宣称多步预测的源码包,本质只是把单步预测的结果拼在一起画了个图,评估时要注意它有没有真的用滚动方式推理。
5. LSTM 落地常见问题排查:4 个容易翻车的坑
5.1 训练 loss 一直不下来或反复震荡
现象:训练了 50 个 epoch,train_loss一直在 0.5 以上,或者降一阵又弹回去。原因可能有三种:学习率太大导致参数在最优解附近来回震荡;数据没有归一化,输入量级在几百到几千,梯度也处于放大状态;window取得太小,信息量不足以支撑预测。排查时我先看归一化代码,再打印每次x_batch的最大值和最小值,确认在 0 到 1 区间内。如果数据没问题,把学习率从 1e-3 降到 3e-4,同时给梯度加个裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)。这两个操作能解决九成的训练不收敛问题。最后检查window,把它调大到至少覆盖一个完整业务周期。
5.2 预测曲线整体滞后一拍
现象:测试集上 loss 很低,画出来的预测曲线和真实曲线高度重合,但仔细看是真实曲线往右平移了一格——模型几乎只是"复制了上一个真实值"。原因是滑窗构造时,窗口最后一位就是t-1时刻的真实观测值,而真正要预测的t时刻往往与t-1高度相关。模型学到的最省力方案就是直接输出t-1的值,整个模型退化成"延迟器"。这条曲线对运维没有参考价值,因为真正要预测的时候t-1的值根本不存在。解决方法是调整标签构造:把标签从t时刻改成t+horizon时刻,强迫模型跳过最近的时间步,学习更本质的趋势。另一种做法是把窗口末尾的最近一两个时间步从特征中剔除,训练时人为制造"信息缺口"。
5.3 双向 LSTM 把未来信息泄漏给了训练过程
现象:模型用了nn.LSTM(..., bidirectional=True),验证集指标好到不真实,但上线后效果断崖式下跌。原因是双向 LSTM 在编码一个时间步时,会同时读取它后面的时间步——如果验证集是从完整序列尾部切出来的一段,训练阶段双向 LSTM 的后向传播实际上接触了验证集区间内的数据,这属于数据泄漏。即使验证 loss 再好看,部署后也只能用过去预测未来,双向结构完全失去优势。解决:纯时间序列预测场景默认使用单向 LSTM,把bidirectional参数去掉;如果业务确实需要双向特征,必须把训练、验证、测试三段数据划分后在时间上有足够的间隔,确保尾部数据不参与任何一步前向计算。我在实际项目中吃过这个亏,从此凡是时序任务先检查模型定义里有没有bidirectional。
5.4 反归一化后指标暴涨
现象:归一化尺度上的 loss 是 0.001,反归一化后 RMSE 算出来几千甚至几万。原因多数不是模型坏了,而是scaler被重新fit过。某些源码为了省事,在评估脚本里对y_test又调用了一次scaler.fit_transform,这相当于把测试集的统计量也用了进来,反归一化自然错位。还有一层常见错误是反归一化时把(n, 1)的数组传入inverse_transform,得到的却是(n,)的原形状,后续广播出错。排查时先检查评估代码里scaler对象是不是训练集那个;再打印pred_inv的前五个值和真实值的前五个值,当场能看出量级差距。面板数据多时,可以用百分比误差来比较不同量级变量的效果,但前提是真实值都远离 0。
6. 进阶做法:多变量输入与注意力融合
如果这源码已经跑通了单变量预测,下一步是把真实业务里更丰富的特征加进去。多变量输入只需要改两个地方:build_dataset里把多个特征列拼成一个二维数组,模型里input_size改成特征列数。例如同时输入负荷值和温度值,窗口形状变成(window, 2):
def build_multivariate_dataset(df, feature_cols, window=24): scaler = MinMaxScaler() scaled = scaler.fit_transform(df[feature_cols].values) # (n, n_features) X, y = [], [] for i in range(len(scaled) - window): X.append(scaled[i:i + window]) # (window, n_features) y.append(scaled[i + window, 0]) # 预测第一个特征列 return np.array(X), np.array(y), scalerLSTMPredictor里的input_size同步改成len(feature_cols),其余结构不变。注意力机制在 LSTM 输出之后做融合,本质是对不同时间步分配权重,避免"最后一帧吞掉所有信息"的缺陷。在lstm_out[:, -1, :]改成先对lstm_out做注意力加权再回归,短期预测的提升在生产数据有限时并不总是明显,但长序列场景下值得尝试。验证多步预测时,我习惯做滚动回溯验证:从第 N 个时间点开始,每次只追加真实值、不更新窗口以外的信息,连续滚动预测多个步长,再分段计算误差。这个方法很朴素,但比单次预测要诚实得多。我自己改源码的习惯是每个模型实验固定一个 seed,记录 window、hidden_size、lr 三个参数。这样出了新数据,回来看记录就知道哪个配置更稳定。希望这些经验能帮你在同样的踩坑路上少走几步。
本文还有配套的精品资源,点击获取