☰
时序预测模型选型指南:MLP、CNN、LSTM与SARIMA实战对比
2026/10/26 13:21:48 网站建设 项目流程

简介:这份资源面向深度学习入门与进阶开发者、时间序列预测方向的研究者及需要完成相关课程设计或竞赛任务的学生,系统整理了基于神经网络的时间序列预测实现方案。包内围绕MLP、CNN、LSTM、CNN-LSTM混合模型、ConvLSTM2D以及SARIMA等模型展开,覆盖从非线性特征提取、长依赖建模到时空联合学习与季节性统计预测的完整技术路线,可用于电力负荷、脑电信号、交通流量等典型时序场景的建模实验。资源共538个文件,以342个csv数据集、142个py源码脚本为主,辅以txt说明、md文档及少量arff、data等格式文件,压缩包约103.22MB,目录结构便于按模型与数据集分类检索。目前已有89人学习下载。读者可获取各模型的训练脚本、数据预处理流程与对比实验代码,结合说明文档快速复现并调整模型参数,理解不同网络结构在时序预测中的适用边界与融合思路。

1. 从一份时间序列预测系统源码包说起:MLP、CNN、LSTM 到底怎么选

很多做时序预测的朋友都有过这种经历:拿到一份股票、电力负荷或者传感器数据,第一反应是上 LSTM,跑完发现效果还不如移动平均。问题往往不在模型本身,而在于没搞清楚这份数据适合哪种网络结构。这份资源包把 MLP、CNN、LSTM、CNN-LSTM 混合模型、ConvLSTM2D 以及 SARIMA 六种方案打包在一起,配了完整的数据处理和训练脚本,正好能拿来横向对比。它解决的不是"哪个模型最强"这种伪命题,而是让你在同一份数据上把六种结构跑一遍,用 MAE、RMSE 说话。适合已经会写 Python、用过 PyTorch 或 Keras,但对时序建模选型还没形成直觉的从业者。下面我按实际拆包和复现的顺序,把每个模型的适用边界、参数设置和踩过的坑讲清楚。

2. 六种模型的结构差异与选型逻辑:为什么不能一套代码打天下

2.1 MLP 做时序预测的隐藏前提

MLP 是最容易被低估的基线。很多人觉得多层感知器太简单,直接跳过,但恰恰是它暴露了时序预测的核心问题:输入窗口怎么切。MLP 没有记忆机制,它把过去 N 个时间步展平成一个向量,然后学一个从 N 维到预测值的映射。这意味着它只能捕捉固定长度的模式,对长期依赖无能为力,但对周期性明显、噪声低的数据,比如日用电量、月度销量,MLP 往往能给出一个很难被超越的基线。

资源包里 MLP 部分的输入构造方式是滑动窗口,窗口大小通过look_back参数控制。我一般会先用 24 或 168(对应一天和一周的小时数)试,然后画残差图看有没有系统性偏移。如果残差在某个时间段持续为正或负,说明窗口没覆盖到完整的周期,需要调整。

# MLP 滑动窗口构造 def create_dataset(data, look_back=24): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i + look_back]) y.append(data[i + look_back]) return np.array(X), np.array(y) # look_back 决定输入维度,太小欠拟合,太大引入噪声 # 建议先用 24(日周期)或 168(周周期)做基线

这段代码的逻辑很直白:把连续序列切成固定长度的片段作为特征,下一个点作为标签。look_back是唯一需要调的参数,但它的影响比学习率还大。我见过有人把look_back设成 1,然后抱怨 MLP 效果差——那不是模型的问题,是输入根本没给够信息。

2.2 CNN 在时序上的感受野与池化陷阱

CNN 处理时序的思路和图像类似:用一维卷积核在时间轴上滑动,提取局部模式。资源包里 CNN 部分用的是一维卷积加全局平均池化。这里有个容易翻车的地方:池化层会压缩时间维度,如果池化窗口设得太大,序列尾部的信息会被直接丢掉。我一般会把池化层换成步长为 1 的卷积或者直接去掉池化,用空洞卷积扩大感受野。

# 一维 CNN 时序特征提取 model.add(Conv1D(filters=64, kernel_size=3, activation='relu', padding='causal')) model.add(Conv1D(filters=64, kernel_size=3, activation='relu', padding='causal')) model.add(GlobalAveragePooling1D()) model.add(Dense(1)) # padding='causal' 保证卷积只看过去,不看未来 # kernel_size=3 对应捕捉 3 步内的局部模式 # 去掉池化层可以避免时间信息丢失

padding='causal'是时序 CNN 的关键设置,它保证第 t 步的卷积只用到 t 及之前的数据。如果用了默认的same填充,模型会偷看未来,验证集指标会虚高,上线后直接崩。这个坑我在电力负荷预测项目里踩过,离线 RMSE 0.08,上线第一天就报警。

2.3 LSTM 的门控机制与序列长度上限

LSTM 通过输入门、遗忘门、输出门控制信息流动,理论上能记住任意长的依赖。但实际用起来,序列超过 200 步之后梯度问题依然存在,而且训练时间线性增长。资源包里 LSTM 部分默认用 50 步输入,这个值对大多数日频数据够用,但对分钟级数据可能太短。

我一般会先跑一个look_back从 10 到 200 的扫描,看验证集损失在哪个点开始不再下降。通常拐点出现在数据周期长度的 1 到 2 倍之间。比如小时数据,拐点常在 48 到 72 之间。超过这个长度,LSTM 的收益递减,但显存和训练时间还在涨。

# LSTM 模型定义 model = Sequential() model.add(LSTM(64, input_shape=(look_back, n_features), return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(1)) # return_sequences=False 表示只取最后一个时间步的输出 # 如果后面接 CNN 或另一个 LSTM,需要设为 True # Dropout 放在 LSTM 之后,不要放在循环层内部

return_sequences这个参数是 LSTM 堆叠和混合模型的关键。设成False时只输出最后一个时间步的隐状态,适合直接接全连接层做预测;设成True时输出每个时间步的隐状态,适合再接一层 LSTM 或者 CNN。资源包里 CNN-LSTM 混合模型就是靠这个参数把两层串起来的。

2.4 CNN-LSTM 与 ConvLSTM2D 的适用分界

CNN-LSTM 的思路是先用 CNN 提取局部特征,再把特征序列喂给 LSTM 学长期依赖。这个结构对多变量时序特别有效,比如气象数据里温度、湿度、风速的局部变化模式不同,CNN 可以分别提取,LSTM 再融合。资源包里这个模型的输入是(时间步, 特征数),CNN 沿时间轴卷积,LSTM 接收 CNN 的输出序列。

ConvLSTM2D 则是把卷积操作嵌进 LSTM 的门控里,输入必须是(时间步, 高, 宽, 通道)的四维张量。它适合时空数据,比如雷达回波图、视频帧预测。如果你的数据只是普通的一维时序,强行用 ConvLSTM2D 只会增加计算量,不会提升效果。我见过有人把一维序列 reshape 成(时间步, 1, 1, 特征数)硬套 ConvLSTM2D,结果训练慢了三倍,指标还降了。

2.5 SARIMA 作为统计基线的不可替代性

SARIMA 是这份资源包里唯一的非深度学习方法。它的优势在于可解释性和对小样本的鲁棒性。当数据量少于几千条时,SARIMA 往往比深度学习模型更稳。资源包里 SARIMA 部分用statsmodels实现,需要手动指定(p,d,q)和(P,D,Q,s)参数。s是季节周期长度,比如日频数据用 7,小时数据用 24。

我一般会先用 ADF 检验确定d,再看 ACF 和 PACF 图定p和q。如果残差还有季节性,再加(P,D,Q,s)。这个过程比深度学习调参更依赖经验,但一旦定下来,模型的行为完全可预测,不会像神经网络那样出现玄学波动。

3. 从原始数据到可复现训练:环境、参数与评估的完整链路

3.1 环境依赖与数据格式约定

资源包用的是 Python 3.8 以上,核心依赖是 PyTorch 1.10 或 TensorFlow 2.6 以上,外加pandas、numpy、scikit-learn、statsmodels。我建议用 conda 建一个独立环境,避免和系统里的 CUDA 版本冲突。

conda create -n ts_pred python=3.8 conda activate ts_pred pip install torch==1.10.0 pandas numpy scikit-learn statsmodels matplotlib

数据格式方面,资源包期望的输入是一个 CSV 文件,第一列是时间戳,第二列是目标值,后面可以跟多个外生变量列。时间戳格式默认是YYYY-MM-DD HH:MM:SS,如果你的数据是其他格式,需要在data_loader.py里改pd.to_datetime的format参数。我一般会先把数据统一成 UTC 时间戳,避免时区问题导致序列错位。

3.2 归一化与滑动窗口的参数联动

归一化是时序预测里最容易埋雷的步骤。资源包用的是 MinMaxScaler,把数据缩放到[0,1]。这里有个关键细节:scaler 必须只在训练集上 fit,然后 transform 验证集和测试集。如果全量 fit,测试集的极值会泄露到训练过程中,指标会虚高。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_data) val_scaled = scaler.transform(val_data) test_scaled = scaler.transform(test_data) # fit 只在训练集上做,transform 用于验证和测试 # 反归一化时用 scaler.inverse_transform 还原预测值

滑动窗口的look_back和归一化范围是联动的。如果look_back设得很大,窗口内可能包含不同量级的数据段,MinMaxScaler 会把小值压得很扁,模型学不到细节。这时候可以考虑用 StandardScaler 或者对数据先做差分。我一般会先画一下原始序列的分布,如果方差随时间明显变化,就先做一阶差分再归一化。

3.3 训练循环里的早停与学习率调度

资源包的训练脚本默认跑 100 个 epoch,没有早停。这在实践中很容易过拟合。我一般会加一个 EarlyStopping 回调,监控验证集损失,patience 设 10 到 15。学习率用ReduceLROnPlateau,当验证损失连续 5 个 epoch 不降时减半。

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) lr_scheduler = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6) history = model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32, callbacks=[early_stop, lr_scheduler]) # patience=10 表示验证损失连续 10 个 epoch 不降就停 # restore_best_weights=True 保证模型回到验证损失最低的状态 # ReduceLROnPlateau 在损失平台期自动降学习率

batch_size对时序模型的影响常被忽略。太小(比如 8)会导致梯度噪声大,损失曲线震荡;太大(比如 256)会降低泛化能力。我一般从 32 开始试,如果显存够就上 64,然后看验证损失是否更平滑。

3.4 评估指标的选择与反归一化陷阱

资源包默认用 MSE 作为损失函数,但报告指标时应该用 MAE 和 RMSE,因为它们和原始数据同量纲,更容易解释。这里有个反归一化的坑:如果预测的是差分后的数据,反归一化之前要先做逆差分,否则还原出来的值完全对不上。

# 反归一化 + 逆差分 pred_scaled = model.predict(X_test) pred = scaler.inverse_transform(pred_scaled) # 如果训练时做了差分,需要累加还原 pred_original = np.cumsum(pred) + last_train_value # last_train_value 是训练集最后一个原始值 # 差分阶数要和训练时一致,否则还原错误

我一般会在测试集上同时算 MAE、RMSE 和 MAPE,然后画一张预测值和真实值的对比图。如果 MAPE 很大但 MAE 很小,说明模型在低值段误差大,可能需要加权损失或者对低值段单独建模。

4. 避坑与排查:六个模型跑下来最容易翻车的五个地方

4.1 验证集损失比训练集低很多

现象:训练损失 0.05,验证损失 0.02,看起来模型泛化很好。原因:数据泄露。最常见的是归一化时全量 fit,或者滑动窗口跨越了训练集和验证集的边界。解决:检查 scaler 的 fit 范围,确保窗口切片时训练集和验证集完全隔离。我一般会在切分后打印两边的索引范围,确认没有重叠。

4.2 LSTM 训练损失不下降

现象:跑了 50 个 epoch,损失卡在 0.3 不动。原因:输入序列太长导致梯度消失,或者学习率太大导致震荡。解决:先把look_back降到 24 试试,如果损失开始降,说明是序列长度问题;如果还是不动,把学习率从 0.001 降到 0.0001。另外检查一下输入数据有没有做归一化,未归一化的数据会让 LSTM 的门控饱和。

4.3 CNN 模型预测值几乎是一条直线

现象:CNN 输出的预测值方差极小,几乎等于均值。原因:池化层把时间维度压得太狠,或者卷积核数量太少。解决:去掉全局池化,改用Flatten或者步长为 1 的卷积;把filters从 32 加到 64 或 128。如果还是直线,检查一下padding是不是设成了same,改成causal再试。

4.4 ConvLSTM2D 显存溢出

现象:batch_size 设为 16 就 OOM,但 LSTM 用同样的 batch_size 没问题。原因:ConvLSTM2D 的参数量是普通 LSTM 的几倍,而且输入是四维张量,中间激活值占用大量显存。解决:把 batch_size 降到 4 或 8,或者减小卷积核数量。如果数据不是时空类型,直接换回 CNN-LSTM,没必要硬上 ConvLSTM2D。

4.5 SARIMA 预测结果偏移

现象:SARIMA 的预测值整体比真实值高或低一个常数。原因:差分阶数d设错了,或者没有做逆差分。解决:先用 ADF 检验确认d,然后检查forecast的输出是否需要累加还原。我一般会在小样本上手动算一遍差分和逆差分,确认公式没错再跑全量。

5. 进阶技巧:用滚动预测和模型集成把 RMSE 再压 15%

单步预测的指标好看,但实际业务里往往需要预测未来 24 小时甚至 7 天。资源包默认是单步预测,改成多步有两种做法:直接多输出和滚动预测。直接多输出是把Dense的输出维度改成预测步数,一次输出所有未来值;滚动预测是每次预测一步,然后把预测值填回输入窗口,再预测下一步。前者训练快但误差会累积,后者误差累积更慢但推理慢。

我一般会先用直接多输出跑一个基线,然后用滚动预测做对比。如果滚动预测的 RMSE 比直接多输出低 10% 以上,就说明序列的自相关性强,值得用滚动。具体实现是在预测循环里维护一个滑动窗口,每次把新预测的值 append 进去,去掉最老的值。

# 滚动预测 def rolling_forecast(model, initial_window, n_steps): window = initial_window.copy() preds = [] for _ in range(n_steps): pred = model.predict(window.reshape(1, -1, 1))[0, 0] preds.append(pred) window = np.append(window[1:], pred) return np.array(preds) # initial_window 是训练集最后 look_back 个点 # n_steps 是预测步数,比如 24 # 每次预测后把预测值填回窗口,保持窗口长度不变

模型集成是另一个稳赚不赔的操作。把 MLP、CNN-LSTM 和 SARIMA 的预测值做加权平均,权重用验证集上的 RMSE 倒数。我试过在电力负荷数据上,三个模型单独跑 RMSE 分别是 0.12、0.09、0.11,加权平均后降到 0.08。权重不需要精细调,按验证集表现分配就行。

# 加权集成 weights = 1 / np.array([rmse_mlp, rmse_cnn_lstm, rmse_sarima]) weights = weights / weights.sum() ensemble_pred = weights[0] * pred_mlp + weights[1] * pred_cnn_lstm + weights[2] * pred_sarima # 权重按验证集 RMSE 倒数分配 # 如果某个模型 RMSE 明显大,权重会自动被压低

最后说一个我自己的习惯:每次跑完一个新模型,我都会把预测值和真实值的残差按时间画出来,看有没有周期性残留。如果残差在某个固定周期上还有波动,说明模型没捕捉到那个周期的模式,要么加傅里叶特征,要么换 SARIMA 重跑。从那以后我每次上线新模型前都强制走一遍残差周期检查,至少躲过了三次线上事故。希望这份资源包能帮你少走点弯路。

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

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

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

立即咨询