时间序列预测六模型对比:MLP、CNN、LSTM、CNN-LSTM、ConvLSTM2D与SARIMA选型指南

发布时间:2026/10/26 13:43:22
时间序列预测六模型对比:MLP、CNN、LSTM、CNN-LSTM、ConvLSTM2D与SARIMA选型指南

简介:这份资源面向深度学习入门与进阶开发者、时间序列预测方向的研究者及需要完成相关课程设计或竞赛任务的学生,系统整理了基于神经网络与统计模型的时序预测实现方案。包内共538个文件,以342个csv数据集、142个py源码脚本为主,辅以txt说明、md文档及少量arff、data等格式文件,压缩包约103.22MB,涵盖数据预处理、模型定义、训练与评估等完整环节。内容覆盖MLP、CNN、LSTM、CNN-LSTM混合模型、ConvLSTM2D以及SARIMA等多种方法,既包含神经网络的非线性与长依赖建模能力,也保留经典统计模型对季节性模式的捕捉思路,便于横向对比不同模型在时序任务中的表现。项目文件夹提供可本地复现的源码与训练脚本,说明文档则给出安装配置与使用指引,附赠文档补充模型选择建议与评估指标解读。目前已有89人学习,适合希望快速搭建实验基线、理解多模型融合预测流程的读者参考。

1. 时间序列预测的六种武器:从 MLP 到 ConvLSTM2D 该怎么选

手里只有一份带时间戳的数值表,老板要你下周给出未来 30 天的走势,还问“用深度学习是不是更准”——这是我过去几年反复遇到的场景。标题里这套组合拳,本质上就是围绕同一个目标:把历史序列映射到未来值,区别只在于模型怎么看待时间与特征之间的关系。MLP 把时间窗拍平当普通特征,CNN 在时间轴上滑窗提局部模式,LSTM 用门控记住长期依赖,CNN-LSTM 先卷积提特征再喂给 LSTM,ConvLSTM2D 则把时间也当成一个空间维度来处理,SARIMA 是统计派的季节性基线。选错模型,调参调到怀疑人生;选对结构,同样的数据误差能差一个量级。这篇笔记面向已经会用 Python 处理数据、想把这六种模型跑通并对比的从业者,从数据切分讲到每个模型的实现细节和翻车点,最后给一套可复现的对比流程。

2. 数据切分与窗口构造:六种模型共用的一套流水线

2.1 为什么时间序列不能随机切分

先把最容易翻车的地方说清楚:时间序列的 train/test 划分绝对不能train_test_split(shuffle=True)。随机打乱会让未来信息泄漏到训练集,验证集上的 loss 好看得不像话,一上真实预测就崩。常见做法是按时间顺序切,比如前 70% 训练、中间 15% 验证、最后 15% 测试。如果做多步预测,还要在切分点留出预测步长的 gap,否则测试集的前几个点仍然和训练集尾部重叠。

另一个坑是标准化。均值和方差必须只用训练集计算,然后应用到验证和测试集。用全量数据算 scaler 是隐蔽的泄漏,尤其在数据量小的时候会让结果虚高。我一般把 scaler 的 fit 和 transform 分开写,fit 只在训练集上调用一次。

import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def split_and_scale(series, train_ratio=0.7, val_ratio=0.15): """ series: 一维或二维 numpy 数组,形状 (T, F) 返回按时间顺序切分并仅用训练集拟合的 scaler """ n = len(series) train_end = int(n * train_ratio) val_end = int(n * (train_ratio + val_ratio)) train = series[:train_end] val = series[train_end:val_end] test = series[val_end:] scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train) # 只在训练集上 fit val_scaled = scaler.transform(val) test_scaled = scaler.transform(test) return train_scaled, val_scaled, test_scaled, scaler

逻辑说明:fit_transform只在训练集调用,验证和测试集只做transform。参数train_ratio和val_ratio根据总样本量调整,样本少于 1000 时建议 80/10/10,样本多时可以 70/15/15。feature_range默认 (0,1),如果数据有极端离群值,改用StandardScaler更稳。

2.2 滑动窗口构造:look_back 和 horizon 怎么定

所有六种模型都需要把序列转成监督学习格式。核心参数两个:look_back(回看多少步)和horizon(预测未来多少步)。look_back 太小,模型看不到周期性;太大,参数量和训练时间暴涨,还容易过拟合。经验值:如果数据有明显周周期,look_back 至少覆盖 2 个完整周期;日频数据做短期预测,look_back 取 30 到 60 比较常见。

def make_windows(data, look_back=30, horizon=1): """ data: 形状 (T, F) 的缩放后数组 返回 X: (N, look_back, F), y: (N, horizon, F) """ X, y = [], [] for i in range(len(data) - look_back - horizon + 1): X.append(data[i : i + look_back]) y.append(data[i + look_back : i + look_back + horizon]) return np.array(X), np.array(y) # 单变量示例 train_x, train_y = make_windows(train_scaled, look_back=30, horizon=1) val_x, val_y = make_windows(val_scaled, look_back=30, horizon=1) test_x, test_y = make_windows(test_scaled, look_back=30, horizon=1) print(train_x.shape, train_y.shape) # (N, 30, 1) (N, 1, 1)

逻辑说明:循环从i到len(data) - look_back - horizon + 1,保证每个样本的标签都在数据范围内。horizon=1是单步预测,改成 7 就是预测未来 7 步。注意验证集和测试集的窗口构造要单独做,不能把训练集尾部拼过来,否则又引入泄漏。

提示:如果特征是多维的(比如同时有温度、湿度、风速),make_windows返回的 X 形状是 (N, look_back, F),LSTM 和 CNN 都能直接吃;MLP 需要额外 reshape 成 (N, look_back*F)。

3. MLP 与 CNN:把时间序列当普通特征和局部模式

3.1 MLP 做时间序列:最简单的基线,但别小看它

MLP 处理时间序列的思路很直接:把 look_back 个时间步的所有特征拍平成一个长向量,然后接全连接层。它没有任何时间归纳偏置,全靠数据量硬学。优点是实现快、调参少、训练稳定;缺点是 look_back 一长,输入维度爆炸,而且它不知道相邻时间步的关系。

import torch import torch.nn as nn class MLPForecaster(nn.Module): def __init__(self, look_back, n_features, horizon, hidden=128): super().__init__() self.flatten = nn.Flatten() self.net = nn.Sequential( nn.Linear(look_back * n_features, hidden), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden, hidden // 2), nn.ReLU(), nn.Linear(hidden // 2, horizon * n_features) ) self.horizon = horizon self.n_features = n_features def forward(self, x): # x: (B, look_back, n_features) x = self.flatten(x) out = self.net(x) return out.view(-1, self.horizon, self.n_features)

逻辑说明:Flatten把 (B, look_back, F) 变成 (B, look_back*F)。两层隐藏层加 Dropout 是常见配置,hidden取 64 到 256 之间。输出层维度是horizon * n_features,再 reshape 回 (B, horizon, F)。参数Dropout(0.2)是正则化,数据量小于 5000 时建议保留;数据量大可以降到 0.1 或去掉。

MLP 的关键参数就三个:look_back、hidden、dropout。我一般先用 look_back=30、hidden=128、dropout=0.2 跑一版,看验证集 loss 再调。如果验证 loss 远高于训练 loss,先加 dropout 或减 hidden;如果两个都高,说明欠拟合,加层或加 hidden。

3.2 CNN 提局部模式:一维卷积在时间轴上的滑窗

CNN 处理时间序列用的是Conv1d,卷积核在时间轴上滑动,提取局部模式(比如连续三天的上升趋势)。相比 MLP,它参数共享,对局部形变更鲁棒,而且能通过堆叠卷积层扩大感受野。常见结构是两到三层 Conv1d + Pooling,最后接全连接输出。

class CNNForecaster(nn.Module): def __init__(self, look_back, n_features, horizon, channels=(32, 64)): super().__init__() layers = [] in_ch = n_features for out_ch in channels: layers += [ nn.Conv1d(in_ch, out_ch, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2) ] in_ch = out_ch self.conv = nn.Sequential(*layers) # 计算卷积后的时间维度 reduced_len = look_back // (2 ** len(channels)) self.fc = nn.Linear(reduced_len * channels[-1], horizon * n_features) self.horizon = horizon self.n_features = n_features def forward(self, x): # x: (B, look_back, F) -> (B, F, look_back) x = x.permute(0, 2, 1) x = self.conv(x) x = x.flatten(1) out = self.fc(x) return out.view(-1, self.horizon, self.n_features)

逻辑说明:Conv1d要求输入形状 (B, C, L),所以先permute把特征维换到通道维。kernel_size=3配padding=1保持时间长度不变,MaxPool1d(2)每次减半。两层池化后时间维度变成 look_back//4,如果 look_back=30,就是 7。channels元组控制每层输出通道数,常见 (32, 64) 或 (64, 128)。注意reduced_len必须算对,否则全连接层维度对不上会直接报错。

CNN 的坑在于kernel_size和padding的配合。kernel_size=3 配 padding=1 是保持长度的标准做法;如果 padding=0,每层卷积会吃掉两个时间步,堆几层后序列就没了。另外池化层不要太多,时间序列本身就不长,池化两次以上容易丢信息。

3.3 MLP 和 CNN 的选型边界

什么时候用 MLP:数据量小、特征维度高、时间依赖弱、需要快速出基线。什么时候用 CNN:序列有明显的局部模式(比如突变、短周期波动)、数据量中等、想比 MLP 提升但不想上 RNN。如果数据有强长期依赖(比如季度周期),CNN 的感受野可能不够,这时候该上 LSTM 或 CNN-LSTM。

注意:MLP 和 CNN 的输出层都是全连接,horizon 和 n_features 的乘积决定输出维度。多变量预测时,如果某些特征不需要预测,可以在 loss 里只对目标特征算误差,但输出层仍然要保留全部维度,否则推理时形状对不上。

4. LSTM 与 CNN-LSTM:门控记忆和混合结构的落地细节

4.1 LSTM 的核心参数:hidden_size、num_layers、dropout

LSTM 通过输入门、遗忘门、输出门控制信息流,天然适合处理长期依赖。PyTorch 的nn.LSTM关键参数:input_size(特征数)、hidden_size(隐藏状态维度)、num_layers(堆叠层数)、dropout(层间 dropout)、batch_first(输入是否 (B, L, F))。做预测时通常取最后一个时间步的 hidden state,接全连接输出。

class LSTMForecaster(nn.Module): def __init__(self, n_features, horizon, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden_size, num_layers=num_layers, dropout=dropout if num_layers > 1 else 0, batch_first=True ) self.fc = nn.Linear(hidden_size, horizon * n_features) self.horizon = horizon self.n_features = n_features def forward(self, x): # x: (B, look_back, F) out, (h_n, c_n) = self.lstm(x) last = out[:, -1, :] # 取最后时间步 pred = self.fc(last) return pred.view(-1, self.horizon, self.n_features)

逻辑说明:batch_first=True让输入保持 (B, L, F),不用手动 permute。num_layers=2时dropout生效,单层 LSTM 的 dropout 参数会被忽略。取out[:, -1, :]是标准做法,也可以用h_n[-1],两者等价。hidden_size常见 32 到 128,num_layers1 到 3 层,再多容易过拟合。

LSTM 最常翻车的地方是序列长度和 batch 的配合。如果 look_back 很长(比如 200),LSTM 训练会很慢,而且梯度容易出问题。这时候可以先用 CNN 降维再喂 LSTM,也就是 CNN-LSTM。

4.2 CNN-LSTM 混合:先卷积提特征,再 LSTM 记时序

CNN-LSTM 的结构是:Conv1d 层提取局部特征,把输出序列喂给 LSTM,最后全连接输出。这样 CNN 负责降维和提局部模式,LSTM 负责长期依赖,两者互补。实现上就是把前面 CNN 的卷积部分和 LSTM 拼起来。

class CNNLSTMForecaster(nn.Module): def __init__(self, n_features, horizon, cnn_channels=32, hidden_size=64): super().__init__() self.conv = nn.Sequential( nn.Conv1d(n_features, cnn_channels, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2) ) self.lstm = nn.LSTM( input_size=cnn_channels, hidden_size=hidden_size, num_layers=1, batch_first=True ) self.fc = nn.Linear(hidden_size, horizon * n_features) self.horizon = horizon self.n_features = n_features def forward(self, x): # x: (B, L, F) -> (B, F, L) x = x.permute(0, 2, 1) x = self.conv(x) # (B, C, L//2) x = x.permute(0, 2, 1) # (B, L//2, C) out, _ = self.lstm(x) last = out[:, -1, :] pred = self.fc(last) return pred.view(-1, self.horizon, self.n_features)

逻辑说明:卷积后时间维度减半,LSTM 的input_size等于cnn_channels。两次 permute 是为了适配 Conv1d 和 LSTM 的不同输入格式。cnn_channels一般取 32 或 64,hidden_size取 64 或 128。这个结构比纯 LSTM 快,因为卷积先把序列缩短了。

CNN-LSTM 的坑在于卷积层和 LSTM 层的维度衔接。卷积输出是 (B, C, L'),LSTM 要 (B, L', C),permute 顺序别搞反。另外如果池化层太多,L' 太小,LSTM 就没东西可学了,一般池化一次就够。

4.3 训练循环和早停:六种模型通用

不管哪种模型,训练循环的骨架是一样的。关键点:用验证集做早停,保存验证 loss 最低的权重,而不是最后一个 epoch 的权重。

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 = float('inf') wait = 0 best_state = None for epoch in range(epochs): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() model.eval() val_loss = 0 with torch.no_grad(): for xb, yb in val_loader: val_loss += criterion(model(xb), yb).item() val_loss /= len(val_loader) if val_loss < best_val: best_val = val_loss best_state = model.state_dict() wait = 0 else: wait += 1 if wait >= patience: break model.load_state_dict(best_state) return model

逻辑说明:patience=10表示验证 loss 连续 10 个 epoch 不降就停。best_state保存最优权重,训练结束后加载回来。lr=1e-3是 Adam 的常用起点,如果 loss 震荡就降到 5e-4,如果下降太慢就升到 2e-3。MSELoss适合回归,如果数据有离群值可以换HuberLoss。

5. ConvLSTM2D 与 SARIMA:时空结构和统计基线

5.1 ConvLSTM2D 的输入形状:把时间当空间维度

ConvLSTM2D 原本用于降水临近预报这类时空序列,输入是 (B, T, H, W, C)。用在普通时间序列上,需要把一维序列 reshape 成二维网格。常见做法是把 look_back 个时间步排成 (H, W) 的矩阵,比如 look_back=30 可以排成 5x6 或 6x5。这样卷积核同时在时间和特征维度上滑动,能捕捉更复杂的局部时空模式。

class ConvLSTM2DForecaster(nn.Module): def __init__(self, height, width, n_features, horizon, filters=32): super().__init__() self.convlstm = nn.ConvLSTM2d if hasattr(nn, 'ConvLSTM2d') else None # PyTorch 原生没有 ConvLSTM2d,需要自定义或用第三方实现 # 这里用 Conv3d 模拟:把时间维当深度 self.conv3d = nn.Sequential( nn.Conv3d(n_features, filters, kernel_size=(3, 3, 3), padding=1), nn.ReLU() ) self.pool = nn.AdaptiveAvgPool3d((1, 1, 1)) self.fc = nn.Linear(filters, horizon * n_features) self.horizon = horizon self.n_features = n_features def forward(self, x): # x: (B, T, H, W, C) -> (B, C, T, H, W) x = x.permute(0, 4, 1, 2, 3) x = self.conv3d(x) x = self.pool(x).flatten(1) pred = self.fc(x) return pred.view(-1, self.horizon, self.n_features)

逻辑说明:PyTorch 没有官方ConvLSTM2d,常见做法是用Conv3d把时间维当深度,或者用第三方实现。kernel_size=(3,3,3)同时在时间、高、宽三个方向卷积。AdaptiveAvgPool3d((1,1,1))把时空维度压成 1,再接全连接。这个结构参数量大,适合数据量充足(至少几万条)的场景,小数据上容易过拟合。

ConvLSTM2D 的坑在于 reshape 方式。把一维序列排成二维网格时,相邻时间步在网格上的位置关系会影响卷积效果。常见做法是按行填充,比如 30 步排成 5x6,第 1 到 6 步在第一行,第 7 到 12 步在第二行。这样卷积核在行方向滑动时能捕捉跨周期的模式。如果随便排,效果可能不如纯 LSTM。

5.2 SARIMA 作为统计基线:别跳过这一步

SARIMA 是 Seasonal ARIMA,参数 (p,d,q)(P,D,Q,s),其中 s 是季节周期。它不需要训练,用统计方法拟合,适合做基线。如果深度学习模型跑不赢 SARIMA,说明数据里的线性成分占主导,或者模型没调好。

from statsmodels.tsa.statespace.sarimax import SARIMAX def fit_sarima(train_series, order=(1,1,1), seasonal_order=(1,1,1,7)): """ train_series: 一维训练序列 order: (p, d, q) 非季节部分 seasonal_order: (P, D, Q, s) 季节部分 """ model = SARIMAX( train_series, order=order, seasonal_order=seasonal_order, enforce_stationarity=False, enforce_invertibility=False ) result = model.fit(disp=False) return result # 预测未来 30 步 # result = fit_sarima(train_scaled.flatten()) # forecast = result.forecast(steps=30)

逻辑说明:order=(1,1,1)是常见的 ARIMA 起点,seasonal_order=(1,1,1,7)表示周周期。enforce_stationarity=False和enforce_invertibility=False能加快拟合,但可能得到非平稳解。实际用的时候先用 ADF 检验确定 d,再看 ACF/PACF 定 p 和 q。SARIMA 的预测是线性的,对非线性模式无能为力,但作为基线非常可靠。

提示:SARIMA 的输入必须是一维序列,多变量场景要么对每个变量单独拟合,要么用 SARIMAX 加外生变量。拟合时间可能较长,数据量大时先用 auto_arima 搜参。

5.3 六种模型的对比维度

跑完六种模型后,对比不能只看 MSE。我一般看四个指标:MSE、MAE、MAPE、以及预测曲线的形状是否合理。MSE 对大误差敏感,MAE 更稳健,MAPE 是百分比误差,跨数据集可比。另外要看模型在验证集和测试集上的表现是否一致,差距大说明过拟合。

模型优势劣势适用场景
MLP实现快、调参少无时间归纳偏置小数据、快速基线
CNN提局部模式、参数共享感受野有限局部突变、短周期
LSTM长期依赖、门控记忆训练慢、易过拟合长周期、强依赖
CNN-LSTM兼顾局部和长期结构复杂、调参多多尺度模式
ConvLSTM2D时空联合建模参数量大、需大数据时空网格数据
SARIMA无需训练、可解释只能线性统计基线

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

6.1 验证 loss 不降反升,训练 loss 一路降

现象:训练集 loss 持续下降,验证集 loss 先降后升,差距越来越大。原因:过拟合。解决:先加 dropout(LSTM 和 MLP 都加),再减 hidden_size 或 num_layers,还不行就加 L2 正则(weight_decay=1e-4)。如果数据量确实小,考虑用 CNN 替代 LSTM,或者用 SARIMA 做基线。

6.2 预测值全是常数或接近均值

现象:模型输出的预测曲线几乎是一条水平线。原因:MSE loss 下,模型倾向于预测条件均值,如果序列本身噪声大、可预测性低,最优解就是均值。解决:检查 look_back 是否太小,增大到覆盖至少两个周期;检查特征是否做了标准化;如果序列本身接近随机游走,任何模型都预测不了,这时候该用差分或 SARIMA。

6.3 ConvLSTM2D 的 reshape 顺序搞错

现象:模型能跑通但效果极差,或者直接报维度错误。原因:一维序列转二维网格时,填充顺序和卷积核的滑动方向不匹配。解决:固定一种填充方式(比如按行),在文档里写清楚,推理时保持一致。如果 look_back 不能整除,用 padding 补齐或在末尾截断。

6.4 多变量预测时输出维度对不上

现象:训练时正常,推理时形状报错。原因:输出层维度是horizon * n_features,但推理时只取了部分特征,或者 batch 维度没处理好。解决:统一用view(-1, horizon, n_features),推理时保持和训练一样的输入形状。如果只需要预测目标变量,在 loss 里做 mask,但输出层维度不变。

6.5 SARIMA 拟合不收敛或预测发散

现象:model.fit()报错或预测值跑到无穷大。原因:d 或 D 设置不当,序列不平稳;或者季节周期 s 和数据实际周期不匹配。解决:先用 ADF 检验确定 d,用季节分解确定 s;enforce_stationarity=False和enforce_invertibility=False能缓解但不治本;数据量太小时改用简单 ARIMA 或移动平均。

7. 用滚动预测验证真实泛化能力

单次切分的测试集评估有个问题:只测了最后一个时间段,如果那段恰好有异常,结论就偏了。更稳的做法是滚动预测(walk-forward validation):每次用历史数据训练,预测下一步,然后把真实值加入历史,再预测下一步。这样能模拟真实上线后的使用方式。

def walk_forward_eval(model, series, look_back=30, horizon=1, n_steps=100): """ series: 缩放后的一维或二维数组 返回每一步的预测值和真实值 """ preds, trues = [], [] start = len(series) - n_steps - horizon for i in range(start, len(series) - horizon): window = series[i - look_back : i] x = torch.tensor(window, dtype=torch.float32).unsqueeze(0) model.eval() with torch.no_grad(): pred = model(x).squeeze(0).numpy() preds.append(pred) trues.append(series[i : i + horizon]) return np.array(preds), np.array(trues)

逻辑说明:每次取look_back长度的窗口做输入,预测horizon步,然后窗口向前滑动。n_steps控制评估步数,一般取测试集长度的 1 到 2 倍。这个评估方式计算量大,但最能反映模型在真实场景下的表现。如果滚动预测的误差远大于单次切分,说明模型对时间敏感,上线后需要定期重训。

我自己的习惯是:先用 SARIMA 跑一个基线,再用 MLP 和 CNN 快速试,如果 CNN 比 MLP 好但还不够,就上 LSTM 或 CNN-LSTM。ConvLSTM2D 只在数据量足够且确实有空间结构时才用,否则参数量大、训练慢、效果还不一定好。每次调参只改一个变量,记录验证 loss,别一次改一堆然后不知道哪个起了作用。这套流程跑下来,至少能保证你不被“深度学习一定比统计方法好”这种话带偏。希望帮到你。

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