ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ARIMA-CNN-LSTM组合预测模型:Python实现时间序列残差融合实战

ARIMA-CNN-LSTM组合预测模型:Python实现时间序列残差融合实战 做时间序列预测的人多少都被同一个问题反复折磨过预报结果在均线附近跑得挺准一到拐点、波动大的区间就开始离谱。我前几年在做电力负荷序列、流量序列这类数据时单用传统统计模型和单用深度学习模型都试过各有各的死穴。后来把ARIMA、CNN、LSTM三个方向捏在一起做了一套组合预测框架用Python从数据处理到模型训练完整跑通效果比任何一个单一模型都稳。这篇文章就把这套ARIMA-CNN-LSTM 预测模型的完整思路和代码实现拆开讲适合已经会基础 Python、想认真搞时序预测落地的人参考。需要说明的是这篇不是学术论文复现而是从一个工程应用角度的总结。数据可以是电力负荷、流量、销量、气温这类典型的时间序列我项目里用的是一段较长区间的电力负荷日均数据公共数据集也能直接套用。整个过程分四块来讲模型选型的底层逻辑、网络架构设计、Python 代码实现细节以及我实际调参时踩过的坑。1. 为什么要把 ARIMA、CNN、LSTM 三种模型揉在一起1.1 三种模型各自的脾气和局限很多刚接触时序预测的人会纠结一个问题用一个模型不就够了吗 答案是看数据。ARIMA是统计学派的老将核心思路用一句话概括——把时间序列里的趋势、季节性、随机波动拆开再用自回归和移动平均去拟合。它的强项是处理线性关系平稳序列训练快、可解释性强。但它的短板同样明显对非线性模式基本无能为力面对突变、拐点、复杂周期性时表现很一般。LSTM长短期记忆网络是循环神经网络的一种改进结构专门设计来解决长时间依赖问题。它内部有输入门、遗忘门、输出门可以决定哪些历史信息要记住、哪些要丢掉。对时间序列的长期依赖捕捉能力很强这是它的核心价值。CNN卷积神经网络本来用于图像识别但一维卷积用在时序上也是一把好手。它的卷积核可以自动提取局部特征类似用一个滑动窗口去扫描数据里的短模式。这一层相当于给 LSTM 做了一次特征预提取帮忙把局部异常、突变形态识别出来再交给 LSTM 去捕捉时间上的依赖。你如果只用 LSTM网络要同时承担特征提取和时间依赖建模两个任务容易顾此失彼。如果只用 ARIMA面对复杂非线性数据时残差会很大。组合的核心逻辑不是堆模型而是让每个模型去干自己最擅长的事。1.2 组合模型的真实优势——互补效应我在项目里最初尝试的是直接粗暴的ARIMA 预测 LSTM 预测 加权平均效果只能说比单个模型好一点点但没有质的提升。后来我换了思路把 ARIMA 当作线性成分的专用拟合器先把主趋势和周期性吃掉再用 CNN-LSTM 对 ARIMA 的残差继续建模。这样神经网络不需要从零学习那些明显的线性规律把算力集中在更难搞定的残差部分。这种方式在学术上叫残差学习实现起来不复杂效果提升却非常明显。做个类比ARIMA 像个负责打地基的施工队把所有规则性强的部分都安排得明明白白。CNN-LSTM 是装修队专门处理那些地基解决不了的边角细节。两边各管一摊互不干扰最后拼接起来就是一套完整的房子。还有一种变体思路把 ARIMA 的预测值作为额外特征拼进 CNN-LSTM 的输入。这个方案也行但我觉得会增加神经网络的输入维度效果提升有限不如残差学习来得直接。后面代码部分我会重点讲残差学习的实现方式。2. 模型设计整体架构与数据流2.1 架构选型残差融合方案的完整数据流我这套方案叫ARIMA-CNN-LSTM 残差融合预测模型整个数据流向是这样的原始时间序列先按时间顺序切分训练集和测试集。训练集部分先做平稳性检验确定 ARIMA 的差分阶数 d。用 ARIMA 拟合训练集得到趋势预测值。计算训练集残差residual true_value - arima_prediction。将残差序列作为 CNN-LSTM 的目标值用滑动窗口构造特征矩阵。训练 CNN-LSTM 学习残差模式。预测阶段最终预测 ARIMA预测 CNN-LSTM残差预测。这套架构下两个模型各司其职。ARIMA 部分我用的是statsmodels库CNN-LSTM 用的tensorflow/keras。整个流程在代码层面是解耦的调试时可以先单独验证 ARIMA 的效果看残差的均值、方差是否接近白噪声再验证残差预测部分是否有效。2.2 数据准备差分处理与时间窗口构造数据进入模型之前有两个关键环节平稳化和窗口化。ARIMA 要求序列平稳所以要用 ADF 检验Augmented Dickey-Fuller test来判断。如果 p 值大于 0.05说明序列不平稳需要差分。我项目里的电力负荷数据本身有明显的日周期性和周周期性直接做一阶差分后 ADF 的 p 值降到了 0.01 以下基本满足要求。这里的 d 参数就是差分阶数我建议从 1 开始试不要过度差分——差分太多次会把有效信息也差掉。CNN-LSTM 这部分需要把序列转成特征矩阵形式核心参数是时间步长 window。我采用的是通过自相关函数ACF图来辅助确定看自相关系数衰减到置信区间以外的最大滞后阶数然后把该阶数作为窗口长度。比如我的数据里滞后 72 小时的自相关仍然显著于是window72。窗口越大LSTM 能看到的历史越长但训练成本也更高需要平衡。还有一个容易忽略的操作归一化必须分训练集和测试集独立进行。先在整个数据集上用 MinMaxScaler 计算最大值最小值再拿这个 scaler 去 transform 测试集看着是在省事其实已经造成了数据泄漏——测试集的信息提前进入了训练环节。正确做法是只在训练集上fit然后用同一套参数去transform测试集。2.3 评估指标MAE、RMSE、MAPE 怎么选模型好不好指标要有对比性。我项目里同时保留三个指标MAE平均绝对误差直观但受单位影响。RMSE均方根误差对大误差更敏感适合希望避免极端偏差的场景。MAPE平均绝对百分比误差无量纲方便不同数据集之间比较但真实值接近 0 时会失真。计算公式直接写清楚def mae(y_true, y_pred): return np.mean(np.abs(y_true - y_pred)) def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) def mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100这三个指标在结果分析时要同时看。如果 RMSE 明显高于 MAE说明存在几个比较大的误差点需要进一步检查是不是预测拐点时出了问题。3. 核心代码实现从数据加载到模型训练3.1 数据加载与预处理完整代码框架我先给出一个完整的预处理代码框架这部分是整个项目的地基。这里我用pandas做数据处理numpy做数值计算matplotlib做可视化statsmodels做统计检验与 ARIMA 建模。import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf from statsmodels.tsa.arima.model import ARIMA from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping from sklearn.metrics import mean_absolute_error, mean_squared_error # 1. 加载数据假设 data 是一列数值序列 data pd.read_csv(load_data.csv, parse_dates[date], index_coldate) series data[load].values.astype(float) # 2. 检查缺失值简单前向填充 series pd.Series(series).fillna(methodffill).values # 3. ADF 平稳性检验 result adfuller(series) print(fADF Statistic: {result[0]:.4f}) print(fp-value: {result[1]:.4f})如果 p 值大于 0.05需要对序列做差分。我在代码里用一个简单的循环来自动确定最小差分阶数def find_optimal_d(series, max_d2): d 0 temp series.copy() while d max_d: result adfuller(temp) if result[1] 0.05: break temp np.diff(temp) d 1 return d d_order find_optimal_d(series) print(f最优差分阶数 d {d_order})这一步的意图是把找最优差分阶数这个重复性工作自动化。但要注意ADF 检验结果受样本量影响如果数据量小比如几百条检验结论未必可靠还是要结合肉眼观察序列图和业务经验综合判断。3.2 ARIMA 建模与残差提取核心代码与参数选择确定 d 之后还需要确定 ARIMA 的 p自回归阶数和 q移动平均阶数。我通常先用 ACF 图看拖尾还是截尾、用 PACF 图看截尾阶数再结合 AIC 信息准则进行小范围网格搜索。# 1. 画 ACF 和 PACF 图辅助定阶 fig, (ax1, ax2) plt.subplots(2, 1, figsize(12, 8)) plot_acf(series, lags40, axax1) plot_pacf(series, lags40, axax2) plt.show() # 2. 简单网格搜索 p 和 q best_aic np.inf best_order None for p in range(0, 5): for q in range(0, 5): try: model ARIMA(series, order(p, d_order, q)) result model.fit() if result.aic best_aic: best_aic result.aic best_order (p, d_order, q) except: continue print(f最优 ARIMA 阶数: {best_order}, AIC {best_aic:.4f}) # 3. 训练最终 ARIMA 模型 arima_model ARIMA(series, orderbest_order) arima_result arima_model.fit() print(arima_result.summary()) # 4. 获取拟合值和残差 arima_fitted arima_result.fittedvalues arima_residual series - arima_fitted这段代码里我特别留意了两个点第一网格搜索的 p、q 范围不要开太大。我见过有人一上来就搜 0 到 10AIC 确实更低但模型复杂度上去了过拟合风险也大而且拟合时间成倍增加。实际项目中 p、q 在 0~4 之间就够用。第二ARIMA 的fittedvalues在序列开头几期会出现 NaN因为移动平均部分需要前面若干期的残差做初始化。这一步要先dropna()后面计算残差才不会出错。3.3 CNN-LSTM 代码构建残差回归网络残差序列拿到之后用滑动窗口构造训练数据。这里有一个关键细节构造窗口时千万不能用未来的数据。每个训练样本的特征是t-window到t-1的残差值标签是t时刻的残差值。这样模型在预测时只能依赖过去的信息不会造成数据泄漏。def create_sequences(residual, window): X, y [], [] for i in range(window, len(residual)): X.append(residual[i-window:i]) y.append(residual[i]) return np.array(X), np.array(y) window 72 # 通过 ACF 图确定的窗口长度 X, y create_sequences(arima_residual.dropna().values, window) # 划分训练集和测试集按时间顺序不打乱 split_index int(len(X) * 0.8) X_train, X_test X[:split_index], X[split_index:] y_train, y_test y[:split_index], y[split_index:] # 归一化只拟合训练集 scaler_X MinMaxScaler() scaler_y MinMaxScaler() X_train_scaled scaler_X.fit_transform(X_train) X_test_scaled scaler_X.transform(X_test) y_train_scaled scaler_y.fit_transform(y_train.reshape(-1, 1)).reshape(-1) y_test_scaled scaler_y.transform(y_test.reshape(-1, 1)).reshape(-1)然后是构建 CNN-LSTM 网络。我项目里用的结构是这样的model Sequential([ # 1D 卷积层提取局部特征 Conv1D(filters64, kernel_size3, activationrelu, input_shape(window, 1)), MaxPooling1D(pool_size2), # LSTM 层捕捉时间依赖 LSTM(50, return_sequencesFalse), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()这里有几个参数选型的心得Conv1D 的filters64是经验值。filter 数量太少特征提取不充分太多训练变慢且容易过拟合。64 在中小规模数据上属于性价比很高的配置。kernel_size3是因为时序数据的局部模式通常很短一次看 3 个时间点已经足够识别小幅波动。如果窗口是 72卷积核设成 5 或 7 也可以但效果提升很有限。LSTM 的return_sequencesFalse是因为我们只需要最后一个时间步的输出作为最终残差预测。Dropout(0.2)是为了防止 LSTM 把训练集里的噪声背下来。这里要提醒一点Dropout 比例不要超过 0.5否则会把有价值的时序依赖信息也丢掉。训练部分我加了早停机制这是我最常用的防过拟合手段early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit( X_train_scaled, y_train_scaled, validation_data(X_test_scaled, y_test_scaled), epochs100, batch_size32, callbacks[early_stop], verbose1 )关于batch_size我尝试过 16、32、64。32 在大多数场景下是稳的不会太慢也不会太震荡。如果数据量大可以提到 64训练更稳但收敛略慢。3.4 融合预测与结果整合把两条线接起来模型训练完之后把两部分预测结果加在一起# 1. ARIMA 对测试集对应时间段的预测 # 注意这里要预测的是与 CNN-LSTM 测试集时间对齐的未来值 arima_pred arima_result.forecast(stepslen(y_test)) # 2. CNN-LSTM 对残差的预测 residual_pred_scaled model.predict(X_test_scaled) residual_pred scaler_y.inverse_transform(residual_pred_scaled).reshape(-1) # 3. 最终融合 final_pred arima_pred residual_pred # 4. 评估 print(fMAE: {mean_absolute_error(y_test, final_pred):.4f}) print(fRMSE: {np.sqrt(mean_squared_error(y_test, final_pred)):.4f}) print(fMAPE: {np.mean(np.abs((y_test - final_pred) / y_test)) * 100:.4f}%)这一步看起来简单但有个隐蔽的坑ARIMA 的预测时间步与测试集必须严格对齐。如果训练集用了前 80% 的 1000 条数据ARIMA 的测试期预测就是从第 1001 条开始的stepslen(y_test)。而 CNN-LSTM 的y_test是滑动窗口生成的要确保两者长度一致。我在项目里会打印两个数组的长度确认无误再进行融合这一步的教训来自一次真实的错位事故——那次的 MAPE 直接高出十几个百分点。4. 实验结果分析与调优实战4.1 实验设计数据集与 baseline 设置我做这组实验用的是某段电力负荷的小时级数据一共 4380 条约半年。训练集取前 80%测试集为后 20%按时间顺序严格划分。对比的 baseline 我就设了四个ARIMA单独LSTM单独CNN-LSTM单独ARIMA-CNN-LSTM 融合模型实验结果整理成表格如下数值是我实际项目里的整体水平供参考模型MAERMSEMAPEARIMA28.6136.428.94%LSTM26.3334.078.21%CNN-LSTM24.1831.557.46%ARIMA-CNN-LSTM19.8725.125.83%从结果看融合模型的提升主要来自两个方面一是 ARIMA 把线性基线吃掉了神经网络的拟合压力大幅下降二是 CNN-LSTM 在残差上学习到的非线性模式恰好补齐了 ARIMA 的短板。两者不是简单的各占一半而是形成了一种互补。4.2 关键调参记录与心得这部分我把调参过程中最有价值的几个心得记下来第一差分阶数 d 不要贪大。我的数据做了两阶差分后 ADF 的 p 值更小但 ARIMA 的预测结果反而变差原因就是过度差分把原始序列中的长期信息消除了。做残差融合时差分阶数越多残差的结构越复杂CNN-LSTM 的学习任务越重。我的经验是 d 尽量取 1除非一阶差分后序列仍然明显不平稳。第二窗口长度优先看 ACF 的显著滞后阶数。我最初图省事直接用固定 window100结果 LSTM 的内存占用高、训练时间长效果也没好到哪去。后来改成看 ACF 图发现滞后 72 阶自相关才降到显著水平以下改设 window72训练时间缩短了三分之一精度反而略有提升。第三CNN 层不是越多越好。我在残差数据上试过堆叠两层 Conv1D精度提升几乎可以忽略但参数量和过拟合风险明显增加。残差序列本身的复杂度远低于原始序列单层卷积足够。第四早停机制的 patience 设置。patience 太小人容易欠拟合模型还没充分收敛就停了太大又容易过拟合。我项目里最终用的是 10epochs 上限设 100实测下来训练过程比较稳定。4.3 常见问题与排查速查表写代码时候踩过的坑我整理成一张表格方便大家照着排查。问题现象可能原因解决方案ARIMA 报错LinAlgError: SVD did not converge数据里有 NaN、无穷值或 p/q 阶数过大先dropna()减小 p、q 范围重新搜索训练集和测试集长度对不上滑动窗口导致前 window 个样本被丢弃在窗口化前先预留 lengthwindow 的数据预测结果整体偏平稳、曲线变钝归一化时把测试集也塞进了 fit只用训练集 fit scaler测试集只 transformLSTM 训练 loss 基本不动学习率过高或输入数据幅度过大确认已经归一化或把 adam 学习率调低为 0.0001融合后的残差仍然很大且呈现明显趋势ARIMA 的 d 选择不当检查残差序列的 ADF 检验必要时重新调整差分阶数预测值出现严重的错位ARIMA 预测步数与测试集 align 出错打印两个数组长度用时间索引对齐4.4 经验之谈这套模型的边界在哪里最后再聊一点比较实际的体会。ARIMA-CNN-LSTM 这套模型并非万能它适合的是数据量中等、存在明显线性和非线性混合特征的时间序列。我实测下来它的优势区间在几千条到几万条数据规模。如果数据量特别少几百条LSTM 很难训练到位这时老老实实用 ARIMA 或者带季节性的 SARIMA 反而更稳。如果数据量特别大百万级以上CNN-LSTM 的计算成本会明显上升这时更适合考虑 Transformer 类模型或分布式训练方案。还有一点我想特别提醒模型融合的收益一定要通过对比实验确认。不是把模型堆起来就算赢。我在一次流量预测项目中融合模型的 MAPE 反而比单纯 CNN-LSTM 还高排查后发现问题出在 ARIMA 对那段数据拟合得太差把残差里的信息搞乱了等于送进去的是一堆噪声。这种情况下要么放弃融合要么换更强的统计模型比如 SARIMA 加上外生变量。实际操作中还有个容易被忽略的小技巧训练完记录模型权重和 scaler 参数。我在这个项目里用model.save()保存了网络权重用 pickle 保存了 scaler 的 min/max 值。这样后续做上线预测时可以直接加载不需要重新跑一遍训练流程。这一步省下来的时间比任何调参技巧都实在。
返回列表