☰
时间序列预测模型选型指南:MLP、CNN、LSTM、CNN-LSTM、ConvLSTM2D与SARIMA实战对比
2026/10/26 16:23:54 网站建设 项目流程

简介:这份资源面向深度学习入门与进阶开发者、时间序列预测方向的研究生及算法工程师,提供一套可直接运行的预测系统源码与配套说明。包内围绕MLP、CNN、LSTM、CNN-LSTM混合模型、ConvLSTM2D及SARIMA六类模型展开,覆盖从非线性特征提取、长依赖建模到时空联合学习与季节性统计预测的完整技术路线,适合作为课程设计、论文复现或项目选型的参考。资源共538个文件,以342个csv数据集、142个py脚本为主,辅以txt说明、md文档及少量zip与docx资料,压缩包约103.22MB,目录结构便于按模型与数据模块检索。已有89人学习下载。读者可获取各模型的训练与预测脚本、多组时序数据样例、环境配置与使用说明,以及模型选择建议与性能评估思路,便于在本地快速复现并对比不同模型的预测效果。

1. 从一份“全家桶”标题说起:时间序列预测到底该选哪个模型

同一个预测任务,有人用 MLP 跑出 0.8 的 R²,换 LSTM 反而掉到 0.6,再加 CNN 直接不收敛。这不是玄学,是我在电力负荷预测项目里真实翻过的车。标题里这一长串——MLP、CNN、LSTM、CNN-LSTM、ConvLSTM2D、SARIMA——本质上不是让你全用上,而是给你一套从线性到非线性的模型光谱,让你按数据形态和预测步长去挑。时间序列预测的核心诉求就三件事:抓趋势、抓周期、抓突变。MLP 抓不了时序依赖,CNN 擅长局部突变,LSTM 擅长长程依赖,CNN-LSTM 是折中方案,ConvLSTM2D 处理时空网格,SARIMA 兜底强周期性。这篇笔记按“先立住选型逻辑,再逐个跑通最小可复现实验,最后讲清楚每个模型在什么数据上会翻车”的顺序展开,适合已经会用 Python 处理 CSV、但面对一堆模型不知道从哪下手的从业者。读完你至少能判断:手上这份数据,第一个该试的模型是哪个,第二个该换谁。

2. 数据准备与统一实验框架:让六个模型跑在同一把尺子下

2.1 时间序列预测的输入输出到底怎么切

不管后面用哪个模型,第一步都是把一维时间序列切成监督学习样本。假设原始序列是[x0, x1, x2, ..., xN],用过去lookback个点预测未来horizon个点。这一步做错,后面所有模型的结果都不可比。

import numpy as np import pandas as pd def make_windows(series, lookback=24, horizon=1): """ series: 一维 numpy 数组,已归一化 lookback: 用过去多少个时间步作为输入 horizon: 预测未来第几个点 返回 X shape=(样本数, lookback, 1), y shape=(样本数,) """ X, y = [], [] for i in range(len(series) - lookback - horizon + 1): X.append(series[i : i + lookback]) y.append(series[i + lookback + horizon - 1]) X = np.array(X).reshape(-1, lookback, 1) y = np.array(y) return X, y

逻辑说明:lookback决定模型能看到多长的历史窗口,horizon决定预测未来多远。参数选择上,如果数据是小时级负荷,lookback=24对应一天周期,horizon=1是单步预测;如果做多步预测,把horizon调大或改成返回序列。注意归一化必须在切窗口之前做,且用训练集的最大最小值,否则验证集信息会泄漏。

2.2 训练集/验证集/测试集的时间顺序切分

时间序列不能随机打乱。常见做法是按 7:1.5:1.5 或 8:1:1 顺序切。下面这个函数把窗口化后的数据按时间顺序切三段。

def temporal_split(X, y, train_ratio=0.7, val_ratio=0.15): n = len(X) train_end = int(n * train_ratio) val_end = int(n * (train_ratio + val_ratio)) return (X[:train_end], y[:train_end], X[train_end:val_end], y[train_end:val_end], X[val_end:], y[val_end:])

参数说明:train_ratio和val_ratio按样本量调整,样本少于 2000 时验证集比例可以降到 0.1。切完之后不要再 shuffle,DataLoader里shuffle=False。这一步是后面所有模型共享的,统一框架的好处是换模型只改网络定义,数据管道不动。

2.3 评价指标:别只看 RMSE

RMSE 对量纲敏感,MAE 更直观,MAPE 在接近零的值上会爆炸。我一般同时看三个:RMSE、MAE、以及预测曲线和真实曲线的重合度。如果数据有强周期,再加一个“周期内误差”——把同一相位点的误差单独算。表格里给出我常用的指标组合。

指标公式含义适用场景坑
RMSE均方根误差整体误差,对大误差敏感量纲不同不可比
MAE平均绝对误差直观,鲁棒掩盖大偏差
MAPE平均绝对百分比误差量纲无关真值接近 0 时爆炸
周期误差同相位点误差均值强周期数据需要先做周期检测

提示:归一化后的 RMSE 要反归一化再报告,否则读者无法判断实际误差大小。

3. MLP 与 CNN:从全连接基线到局部特征提取

3.1 MLP 做时间序列预测的最小可用代码

MLP 是最容易上手的基线。把lookback个点展平成一维向量,过两层全连接。别小看它,很多数据集上 MLP 调好参数能打平 LSTM。

import torch import torch.nn as nn class MLPForecaster(nn.Module): def __init__(self, lookback, hidden=64, dropout=0.2): super().__init__() self.net = nn.Sequential( nn.Flatten(), # (B, lookback, 1) -> (B, lookback) nn.Linear(lookback, hidden), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden, hidden // 2), nn.ReLU(), nn.Linear(hidden // 2, 1) ) def forward(self, x): return self.net(x).squeeze(-1)

逻辑说明:Flatten把时间维压平,MLP 不区分时间顺序,所以它学不到“昨天比前天更重要”这种关系。hidden一般取 32 到 128,dropout在 0.1 到 0.3 之间。如果验证 loss 震荡,先降学习率到 1e-3 以下,再加 dropout。MLP 的翻车点在于:当lookback超过 50,参数量暴涨,容易过拟合,这时候要么降lookback,要么换 CNN。

3.2 一维 CNN 提取时序局部模式

CNN 在时间序列上的直觉是:用卷积核扫过时间轴,捕捉“连续几个点一起变化”的模式,比如突然上升、短时波动。相比 MLP,CNN 参数量少,对局部突变更敏感。

class CNN1DForecaster(nn.Module): def __init__(self, lookback, channels=(32, 64), kernel=3, dropout=0.2): super().__init__() layers = [] in_ch = 1 for out_ch in channels: layers += [ nn.Conv1d(in_ch, out_ch, kernel, padding=kernel // 2), nn.BatchNorm1d(out_ch), nn.ReLU(), nn.MaxPool1d(2) ] in_ch = out_ch self.conv = nn.Sequential(*layers) self.head = nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(in_ch, 1) ) def forward(self, x): # x: (B, lookback, 1) -> (B, 1, lookback) x = x.permute(0, 2, 1) return self.head(self.conv(x)).squeeze(-1)

参数说明:kernel=3是最常用的起点,奇数核配合padding=kernel//2保持长度。channels逐层翻倍是常见做法,但时间序列上 32/64 足够,再大容易过拟合。MaxPool1d(2)每次把时间维减半,lookback=24经过两层池化剩 6,再AdaptiveAvgPool1d(1)压成向量。注意输入要permute成(B, C, L),这是 PyTorch Conv1d 的要求,很多人在这里翻车。

3.3 MLP 和 CNN 的选型边界

MLP 适合lookback短(小于 30)、数据量小、趋势平稳的场景。CNN 适合lookback中等(30 到 100)、有明显局部突变、需要控制参数量的场景。如果数据里周期性强但局部突变少,两者都不如后面要讲的 LSTM 或 SARIMA。我一般先跑 MLP 拿基线,再跑 CNN 看局部特征有没有增益,如果 CNN 比 MLP 还差,说明数据里局部模式不明显,直接跳到 LSTM。

4. LSTM 与 CNN-LSTM:长程依赖和混合结构的取舍

4.1 LSTM 单步预测的完整训练循环

LSTM 通过门控机制记住长程依赖,适合lookback较长、周期复杂的序列。下面是一个可复现的训练循环,包含早停。

class LSTMForecaster(nn.Module): def __init__(self, input_size=1, hidden=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_size, hidden, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden, 1) def forward(self, x): out, _ = self.lstm(x) # out: (B, L, hidden) return self.fc(out[:, -1, :]).squeeze(-1) def train_model(model, train_loader, val_loader, epochs=50, lr=1e-3, patience=5): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) opt = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.MSELoss() best_val, wait = float("inf"), 0 for epoch in range(epochs): model.train() for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() opt.step() model.eval() val_loss = 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb = xb.to(device), yb.to(device) val_loss += loss_fn(model(xb), yb).item() val_loss /= len(val_loader) if val_loss < best_val: best_val, wait = val_loss, 0 torch.save(model.state_dict(), "best_lstm.pt") else: wait += 1 if wait >= patience: break model.load_state_dict(torch.load("best_lstm.pt")) return model

参数说明:hidden=64是起点,数据量大可以到 128;num_layers=2足够,再深容易过拟合且训练慢;dropout只在num_layers>1时生效。patience=5是早停耐心值,验证 loss 连续 5 轮不降就停。注意out[:, -1, :]取最后一个时间步,这是单步预测的标准做法;如果做多步,改成接一个Linear(hidden, horizon)。

4.2 CNN-LSTM 混合结构:先卷积再循环

CNN-LSTM 的思路是:CNN 先提取局部特征,把每个时间步的原始值变成特征向量,再喂给 LSTM 学长程依赖。适合既有局部突变又有长周期依赖的数据,比如带噪声的传感器序列。

class CNNLSTMForecaster(nn.Module): def __init__(self, lookback, cnn_channels=32, hidden=64, dropout=0.2): super().__init__() self.conv = nn.Sequential( nn.Conv1d(1, cnn_channels, 3, padding=1), nn.ReLU(), nn.MaxPool1d(2) ) self.lstm = nn.LSTM(cnn_channels, hidden, batch_first=True) self.fc = nn.Linear(hidden, 1) def forward(self, x): x = x.permute(0, 2, 1) # (B,1,L) x = self.conv(x) # (B,C,L/2) x = x.permute(0, 2, 1) # (B,L/2,C) out, _ = self.lstm(x) return self.fc(out[:, -1, :]).squeeze(-1)

逻辑说明:卷积层把lookback减半,LSTM 的输入长度变短,训练更快。cnn_channels一般取 16 到 64,太大反而丢细节。这个结构比纯 LSTM 多了一层非线性特征提取,但参数量也增加,数据少于 5000 条时容易过拟合,建议加 dropout 或减小cnn_channels。

4.3 LSTM 和 CNN-LSTM 的选型对比

模型优势劣势推荐场景
LSTM长程依赖强,结构简单训练慢,对局部突变不敏感周期长、噪声低
CNN-LSTM兼顾局部和长程参数多,调参难局部突变+长周期
纯 CNN快,局部特征强长程依赖弱短窗口、突变多

我一般先跑 LSTM,如果验证集误差在局部突变处明显偏大,再换 CNN-LSTM。如果 CNN-LSTM 比 LSTM 还差,多半是数据量不够,回到 LSTM 加正则。

5. ConvLSTM2D 与 SARIMA:时空网格和强周期兜底

5.1 ConvLSTM2D 处理时空数据的输入构造

ConvLSTM2D 把 LSTM 的门控搬到卷积上,输入是(B, T, C, H, W)五维张量。适合雷达回波、气象网格、交通流量网格这类时空数据。构造输入时,把时间序列按空间网格重排。

class ConvLSTM2DForecaster(nn.Module): def __init__(self, in_channels=1, hidden=(16, 32), kernel=3): super().__init__() self.convlstm = nn.Sequential( nn.ConvLSTM2d if False else None # 占位,实际用下方自定义 ) # PyTorch 无内置 ConvLSTM2D,常见做法是用 ConvLSTM 单元堆叠 # 这里给出简化版:用 Conv3d 模拟时空卷积 self.net = nn.Sequential( nn.Conv3d(in_channels, hidden[0], (3, kernel, kernel), padding=(1,1,1)), nn.ReLU(), nn.Conv3d(hidden[0], hidden[1], (3, kernel, kernel), padding=(1,1,1)), nn.ReLU(), nn.AdaptiveAvgPool3d(1), nn.Flatten(), nn.Linear(hidden[1], 1) ) def forward(self, x): # x: (B, T, C, H, W) -> (B, C, T, H, W) x = x.permute(0, 2, 1, 3, 4) return self.net(x).squeeze(-1)

说明:PyTorch 没有官方ConvLSTM2D,常见做法是自己实现 ConvLSTM 单元或用Conv3d近似。上面用Conv3d在时间维和空间维同时卷积,是一种可跑的简化方案。参数上,hidden两个通道数,kernel=3是空间核。输入构造时,每个时间步是一张H×W的图,T个时间步叠成序列。这个模型训练成本高,数据量少于 10000 个网格样本不建议上。

5.2 SARIMA 作为强周期基线

SARIMA 不是深度学习,但在强周期数据上经常打得过 LSTM。用statsmodels几行就能跑。

from statsmodels.tsa.statespace.sarimax import SARIMAX def fit_sarima(train_series, order=(1,1,1), seasonal_order=(1,1,1,24)): model = SARIMAX(train_series, order=order, seasonal_order=seasonal_order, enforce_stationarity=False, enforce_invertibility=False) return model.fit(disp=False) # 预测 # result = fit_sarima(train) # pred = result.forecast(steps=24)

参数说明:order=(p,d,q)是非季节部分,seasonal_order=(P,D,Q,s)中s是周期长度,小时数据s=24,日数据s=7。d和D是差分阶数,一般先做 ADF 检验确定d。SARIMA 的坑在于参数靠 AIC 网格搜索,慢且容易过拟合;但作为基线,它能在几分钟内告诉你数据里有多少线性周期成分。

5.3 六个模型的适用边界汇总

模型数据形态最小样本量训练成本主要坑
MLP短窗口、平稳500低长窗口过拟合
CNN局部突变1000低长程依赖弱
LSTM长周期2000中慢,需早停
CNN-LSTM突变+周期5000中高参数多
ConvLSTM2D时空网格10000高输入构造复杂
SARIMA强周期200低参数搜索慢

注意:样本量是经验下限,实际要看信噪比。噪声大的数据需要更多样本。

6. 避坑与排查:六个模型跑下来最容易翻车的五件事

6.1 现象:验证 loss 不降,训练 loss 一直降

原因:过拟合。时间序列数据量小,模型参数量大。解决:先加 dropout,再降hidden,再减num_layers。如果还不行,检查是不是归一化用了全量数据,导致验证集泄漏。

6.2 现象:LSTM 预测曲线整体平移一个周期

原因:lookback和horizon对不齐,或者归一化反变换时用了错误的均值。解决:打印前 10 个样本的X和y,确认y确实是X后面第horizon个点。反归一化时用训练集的scaler,不要重新 fit。

6.3 现象:CNN 训练时 loss 变 NaN

原因:学习率太大,或者输入没有归一化。解决:学习率降到 1e-3 以下,检查输入是否在 0 到 1 之间。如果用了BatchNorm1d,确认 batch size 大于 1。

6.4 现象:CNN-LSTM 比纯 LSTM 还差

原因:数据量不够,CNN 层引入了额外参数。解决:减小cnn_channels到 16,或者去掉 CNN 层直接跑 LSTM。如果数据少于 3000 条,别用 CNN-LSTM。

6.5 现象:SARIMA 预测是一条直线

原因:差分阶数d或D选错,或者数据本身没有周期性。解决:先画 ACF/PACF 图,确认周期长度。如果 ACF 衰减很慢,说明d不够;如果季节项不显著,把seasonal_order的P和Q设为 0。

7. 进阶技巧:用残差诊断决定下一个模型

跑完一轮之后,别急着调参。把最优模型的残差画出来,看三件事:残差有没有自相关、有没有周期残留、方差是否恒定。如果残差 ACF 在滞后 24 处还有尖峰,说明周期没抓干净,换 SARIMA 或加周期特征;如果残差在突变处方差变大,说明模型对突变不敏感,换 CNN 或 CNN-LSTM;如果残差整体白噪声,说明模型够了,再调参收益很小。

我一般会写一个残差诊断函数,把 ACF 和残差分布一起打出来。

import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf def diagnose_residuals(residuals, lags=48): fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(residuals) axes[0].set_title("Residuals over time") plot_acf(residuals, lags=lags, ax=axes[1]) axes[1].set_title("ACF of residuals") plt.tight_layout() plt.show()

参数说明:lags至少覆盖两个周期,小时数据取 48。如果 ACF 在周期滞后处超出置信区间,说明模型没学到周期。这个诊断比单纯看 RMSE 有用得多,因为 RMSE 会掩盖结构性误差。

还有一个习惯:每次换模型,固定随机种子,记录lookback、horizon、学习率、batch size、早停轮数。我吃过亏,有一次 CNN-LSTM 跑出很好的结果,回头复现时忘了lookback是 48 还是 72,白白重跑两天。现在我用一个config.yaml存所有超参,跑之前先打印出来。这个习惯帮我省了很多后悔药。

最后说一句选型上的个人教训:别一上来就堆 CNN-LSTM 或 ConvLSTM2D。先用 MLP 和 SARIMA 拿两个基线,一个抓非线性、一个抓周期,然后看残差缺什么再补什么。大部分业务数据,MLP 加周期特征就能到 80 分,LSTM 到 85 分,再往上每分都要花几倍时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询