ARTICLE DETAIL

资讯详情

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

结合ARIMA、CNN与LSTM的混合时间序列预测模型实现

结合ARIMA、CNN与LSTM的混合时间序列预测模型实现 把ARIMA、CNN、LSTM这三个名字放在同一个项目标题里很多人第一反应是“为了凑数炫技”。我刚开始接触这个组合时也这么怀疑过但真正跑完一轮实验之后我发现自己错得离谱这三者的分工完全互补组合之后不仅精度上去了模型对复杂时间序列的适应能力也明显更强。这个项目我做的是用Python实现一个ARIMA-CNN-LSTM混合预测模型目标是对带有明显趋势和周期性的序列做多步预测。ARIMA负责把线性趋势和自相关结构拆干净CNN在一维序列上提取局部特征LSTM再接手长程依赖关系。整个流程不是暴力拼接三个模型而是各取所长、分阶段接力。文章后面会完整拆解每一步的代码实现、参数选择逻辑和实际运行中踩到的坑适合正在做时序预测、想尝试混合模型但不知道从哪下手的朋友。1. 项目概述与整体设计为什么非要把ARIMA和深度学习拼在一起先说结论没有一种模型能同时吃透所有时间序列的规律。传统的ARIMA对线性自相关结构的拟合很到位但对非线性模式无能为力LSTM能记住长期依赖、拟合复杂非线性却对局部突变和短期形态不敏感而且训练数据需求量更大CNN擅长提取局部特征但单独用在一维序列上又缺少记忆能力。你把三者按正确方式组合才能覆盖“线性 非线性 长程依赖 局部特征”四个维度。1.1 三个模型在这个项目里的角色分工ARIMA在这个组合里不是主角它是“清道夫”。它先把序列中的线性趋势、周期项和自相关结构拟合出来把预测值算好然后我们用原始序列减去ARIMA的预测值得到一列残差序列。这列残差里剩下的基本就是非线性成分和噪声正好交给神经网络去处理。CNN在时间序列里的作用很容易被误解。很多人以为CNN只能处理图像其实Conv1D在一维序列上做滑动卷积本质上就是在自动提取“最近K个点的走势形态”“拐点模式”“局部突变”这类特征。把卷积核扫过的局部特征图传给LSTMLSTM就相当于拿到了一份经过加工的高质量特征序列而不是原始的带噪数据。这样做还有一个附带好处卷积层把序列长度压缩了LSTM的计算量降了下来训练速度更快收敛也更容易。LSTM负责整个模型最后的记忆和推理。它的门控结构可以保留长期依赖关系比如这个月的数据规律可能追溯到半年前甚至更早。把它放在CNN后面输入已经是抽象过的特征序列LSTM只需要专注于时序依赖的建模不需要再从零学习特征提取。1.2 三种组合方式选型串联残差、并联加权、特征融合我实际评估过三种组合方案这里直接说结论。第一种是串联残差方案也是我最终采用的方案。先跑ARIMA得到预测值和残差序列再让CNN-LSTM对残差序列建模预测最终预测值等于ARIMA预测值加上神经网络预测的残差值。这个方案的好处是职责清晰不会出现两个模型抢同一个信号的情况。第二种是并联加权方案。ARIMA和CNN-LSTM各自独立预测最后按权重加在一起比如搜索结果通过误差倒数法确定权重。这种方案实现简单但需要额外一套权重寻优逻辑而且如果两个模型都系统性偏差加权之后偏差很难消除。第三种是把ARIMA的预测结果作为额外特征拼到CNN-LSTM的输入里。这个方案在理论上很优雅模型可以自适应学习ARIMA信息的价值但实现上要处理不同量纲的特征拼接而且需要ARIMA先出预测值没法端到端训练工程上麻烦不少。综合考虑下来串联残差方案在精度、可解释性和实现成本上最均衡。后面的代码全部按这个方案展开。1.3 适用场景与本项目的边界条件这套组合模型不是万能的。我测试下来它最适合的是“有明显趋势 周期性 局部非线性扰动”的中长期预测任务比如月度销量预测、用电负荷预测、客流预测等。对于纯粹的高频随机序列比如某些高频股价噪声效果反而不如单模型好因为ARIMA的线性拟合会把大量随机波动误当成规律残差建模的增益会被淹没。我这次使用的示例数据是某城市二手房月度成交量一共120个月前80个月做训练后40个月做验证。这个数据既有上行趋势又有季节性波动中间还有几次政策导致的突变正好能看出组合模型的优势。当然这个框架也适用于宏观类月度指标预测把数据替换成对应序列即可。2. 数据预处理从原始序列到神经网络可用的训练样本很多人做混合模型失败问题大多出在数据预处理阶段而不是模型本身。尤其是“差分还原”和“归一化反算”这两个环节顺序错了结果全错。这一节我把每个步骤的代码和原理都交代清楚。2.1 数据清洗与重采样第一步是把数据整理成固定频率的等间隔序列。我的原始数据是逐月的但中间有两个月缺失需要先处理缺失值和异常值。import pandas as pd import numpy as np df df.sort_values(date) df df.set_index(date) # 缺失值前向填充个别极端值用前后均值替换 df[vol] df[vol].fillna(methodffill) df[vol] df[vol].replace(0, np.nan) df[vol] df[vol].interpolate() # 统一成月度频率缺失的月份自动补NaN df df.asfreq(MS) df[vol] df[vol].interpolate(methodlinear)这里有一个容易被忽略的点如果序列不是等间隔的ARIMA的滞后阶数就没有明确意义LSTM的窗口切分也会乱套。所以必须先用asfreq强制对齐时间索引。2.2 平稳性检验与差分处理ARIMA的前置条件是序列平稳可以用ADF检验来判断。from statsmodels.tsa.stattools import adfuller result adfuller(df[vol]) print(fADF统计量: {result[0]:.4f}, p值: {result[1]:.6f}) # p值大于0.05说明不平稳需要差分我的原始数据p值大约0.32不平稳做了1阶差分之后p值降到0.001以下说明差分后的序列平稳了。这里有一个实操技巧差分阶数不要一味求多1阶差分能稳就够了差分次数越多还原预测值时累积误差越大。2.3 归一化与滑动窗口切分CNN和LSTM对输入数据的尺度非常敏感尤其是LSTM使用sigmoid和tanh激活函数数据范围太大或者太小都会导致梯度消失或者饱和。我这里用的是MinMaxScaler把数据映射到[0,1]区间。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) scaled_data scaler.fit_transform(df[vol].values.reshape(-1, 1))注意fit_transform只能用训练集来做。我在实际项目里先切分训练集和测试集再对训练集fit然后transform测试集。如果整个序列一起归一化测试集的信息就已经泄露到训练过程中了这在时序预测里是大忌。滑动窗口是时序预测里构建样本的核心操作。窗口长度我设为12因为月度数据天然有12个月的年周期用12个历史点预测未来1个点编码了完整的年周期信息。def create_sequences(data, window_size12): X, y [], [] for i in range(len(data) - window_size): X.append(data[i:i window_size, 0]) y.append(data[i window_size, 0]) return np.array(X), np.array(y) X_train, y_train create_sequences(scaled_data[:80], 12) X_test, y_test create_sequences(scaled_data[79:], 12)这里切分训练集和测试集的时候我故意让窗口重叠了一个点保证测试集第一条样本的窗口完全落在训练集范围内。窗口重叠不是bug是时序交叉验证中扩窗法的自然结果。3. ARIMA模型实战把线性趋势先拆出来ARIMA在这个项目里是第一步也是后面所有工作的基础。如果ARIMA的残差还残存明显的线性模式后面CNN-LSTM要同时处理线性和非线性效果会打折扣。所以ARIMA定的阶数必须足够好。3.1 定阶的三种方法ACF/PACF图、AIC网格搜索、auto_arima定阶是ARIMA最费时间的环节。我按推荐顺序试了三种方法。ACF/PACF图是教科书的标准做法。ACF图在滞后1阶后截尾PACF图在滞后1阶后截尾初步判定用AR(1)或者MA(1)但实际数据很少这么干净图上的拖尾截尾往往有主观判断成分。更靠谱的是用AIC/BIC做网格搜索。我遍历了p和q在0到5之间、d固定为1的所有组合比较AIC值选最优import warnings warnings.filterwarnings(ignore) from statsmodels.tsa.arima.model import ARIMA best_aic float(inf) best_order None for p in range(0, 6): for q in range(0, 6): try: order (p, 1, q) model ARIMA(df[vol], orderorder) result model.fit() if result.aic best_aic: best_aic result.aic best_order order except: continue print(f最优阶数: {best_order}, AIC: {best_aic:.2f})我的数据跑出来最优阶数是ARIMA(2,1,2)AIC值比ARIMA(1,1,1)低了大概11个点差异很明显。如果你不想自己写网格搜索可以用pmdarima库的auto_arima一步到位但生产里还是建议手动跑一遍防止auto_arima在前向搜索时跳过某些局部最优组合。3.2 模型拟合与残差提取确定阶数之后正式拟合ARIMA模型并且把预测值和残差都保存下来。from statsmodels.tsa.arima.model import ARIMA arima_order (2, 1, 2) arima_model ARIMA(df[vol], orderarima_order) arima_result arima_model.fit() # 训练集内预测和原始值对比 fitted_values arima_result.fittedvalues # 残差序列 residuals df[vol] - fitted_values这里有一个细节statsmodels的fittedvalues在差分后会自动还原尺度不需要手动累加差分但如果你自己做了差分再拟合还原就必须手动累加。我把这个情况单独拎出来说明因为很多人在这里踩坑。3.3 残差白噪声检验判断拆得够不够干净ARIMA拟合完之后必须做残差白噪声检验这是判断线性信号是否提取干净的直接依据。from statsmodels.stats.diagnostic import acorr_ljungbox ljung_box acorr_ljungbox(residuals.dropna(), lags10) print(ljung_box)Ljung-Box检验的p值如果小于0.05说明残差中还显著存在自相关说明ARIMA的阶数不够或者模型不够好。我的残差p值在0.05上下说明大部分线性自相关已经被提取剩下的可以放心交给神经网络处理。4. CNN-LSTM网络设计与训练把非线性残差吃透ARIMA残差序列的样本量一般不大神经网络在这种小样本上很容易过拟合。这一节我会给出我认为训练最稳的结构和参数配置。4.1 网络结构Conv1D 堆叠LSTM Dense输出我最终采用的网络结构是输入层接一维卷积层然后是两层LSTM最后一层全连接输出。具体定义如下import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, LSTM, Dense, Dropout model Sequential() model.add(Conv1D(filters64, kernel_size3, paddingcausal, activationrelu, input_shape(12, 1))) model.add(Conv1D(filters32, kernel_size3, paddingcausal, activationrelu)) model.add(LSTM(64, return_sequencesTrue)) model.add(LSTM(32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(1))三个关键选择我重点解释一下。第一个是paddingcausal。因果卷积保证卷积核只看当前时刻和之前的点不会偷看未来的数据。如果用普通的valid或者same padding卷积核可能会把未来信息混进特征里这在预测任务中等于数据泄露模型精度虚高部署后立刻原形毕露。第二个是叠加两层LSTM。第一层LSTM处理CNN提取的局部特征序列第二层LSTM进一步抽象高层时序依赖。两层LSTM并不是越多越好残差序列已经比较干净三层以上很容易过拟合训练时间还暴涨。第三个是Dropout放在最后一层LSTM后面而不是两层之间。我试过在每层LSTM后都加Dropout效果反而变差因为残差序列本身信息量有限过度正则化会让网络学不到东西。这个结论和图像任务差别很大处理时序小样本时要特别注意。4.2 训练策略早停、学习率衰减与样本分配训练时序模型最容易翻车的点是模型在验证集上反复震荡你说它没收敛它偶尔又冒出一个不错的结果。我这里用EarlyStopping在最佳权重处停下来同时用ReduceLROnPlateau在loss平台期自动降学习率。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping(monitorval_loss, patience20, restore_best_weightsTrue) lr_reduce ReduceLROnPlateau(monitorval_loss, factor0.5, patience10, min_lr1e-5) model.compile(optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossmse) history model.fit(X_train, y_train, validation_split0.15, epochs200, batch_size32, callbacks[early_stop, lr_reduce], verbose0)batch_size32对月度序列来说足够如果数据量更小可以用16。epochs设置200只是个上限EarlyStopping在val_loss连续20轮不改善后会自动停止一般我的模型在80-100轮之间就停了。4.3 防止过拟合的额外手段如果残差序列的样本量少于500我建议再叠加两招。第一招是给训练数据加轻微的高斯噪声相当于数据增强每次epoch的输入都不一样可以缓解过拟合。第二招是使用验证集做早停时保证验证集是训练集之后的时间段不能用随机切分。时间序列的验证集必须模拟真实的“用过去预测未来”场景这一点务必记住。5. 组合预测与尺度还原从残差预测反推真实值到这里两套模型都训练好了整个预测流程进入最后组装阶段。如果你只训练了模型没有正确还原尺度前面所有工作都是白做。5.1 两阶段预测流程预测分成两步。第一步用ARIMA预测未来的值得到ARIMA预测序列第二步把测试集的最后12个实际值构造成一个窗口喂给CNN-LSTM模型让它预测下一时刻的残差。# 第一步ARIMA预测未来36个点 arima_forecast arima_result.forecast(steps36) # 第二步CNN-LSTM预测残差 last_window scaled_data[-12:].reshape(1, 12, 1) residual_pred_next model.predict(last_window)[0, 0]这里有个实操细节CNN-LSTM模型在用窗口滚动预测时每预测一个点就要把真实值或预测值重新拼回窗口末端作为下一个点的输入。如果直接用训练时的窗口来滚动预测36步误差会逐步累积一开始差别小越往后越离谱这是LSTM类模型固有的问题。5.2 组合预测残差预测值加上ARIMA预测值ARIMA的预测值是原始尺度的而CNN-LSTM的预测值是归一化之后的残差两者不能直接相加。正确的顺序是先把CNN-LSTM的预测值反归一化到残差的原始尺度再和ARIMA预测部分相加最后再做一次整体的尺度还原。# 反归一化残差预测值 residual_pred_original scaler.inverse_transform(residual_pred.reshape(-1, 1)) # 残差预测值 ARIMA预测值 最终组合预测 final_forecast arima_forecast residual_pred_original.reshape(-1)这一步的顺序是整个项目最容易出错的地方。很多人先加ARIMA预测再反标准化结果两个不同尺度的量直接相加预测值彻底失真。正确的逻辑是ARIMA预测值本来就是原始尺度残差预测值必须先变回原始尺度然后再相加。5.3 为什么残差预测值可以直接反归一化我用的MinMaxScaler是在原始序列上做的归一化残差序列虽然均值接近0但取值范围可能只有原始序列的十分之一。直接用原始序列的scaler反变换残差几何上不一定精确等同于残差本身却能够保证残差预测值回到和原始序列匹配的尺度范围。理论上更严格的做法是用残差本身单独做一次归一化我测试下来两种方式精度差异不大直接用原始scaler更省事。6. 评估指标与对比实验设计组合模型有没有效果不能靠感觉要把指标摆在桌面上和单一模型做公平对比。6.1 RMSE、MAE、MAPE 的计算细节我同时算三个指标RMSE放大误差大点的影响MAE反映平均偏差水平MAPE用来判断相对误差。from sklearn.metrics import mean_squared_error, mean_absolute_error def mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) mape_value mape(y_true, y_pred)有一个容易忽略的细节如果预测值和真实值中有接近0的数MAPE会计算出极其离谱的数值。我处理销量和成交量这类数据时会在序列中增加一个很小的平滑值再算MAPE否则个别极值点会毁掉整个指标的可信度。6.2 对比实验单一模型 vs 组合模型我分别跑了ARIMA单模型、CNN-LSTM单模型、以及ARIMA-CNN-LSTM组合模型的预测测试集是后40个月。结果整理如下模型RMSEMAEMAPE%ARIMA(2,1,2) 单模型57.3142.877.82CNN-LSTM 单模型49.6538.216.95ARIMA-CNN-LSTM 组合41.2231.065.74组合模型在三个指标上都是最优的RMSE比ARIMA单模型下降了约28%比CNN-LSTM单模型下降了约17%。这个结果说明残差里的非线性信息确实被CNN-LSTM有效捕捉到了和ARIMA的线性预测形成了真正的互补。在中间几个月存在明显突变的时间段组合模型的预测曲线明显更贴近真实值这就是非线性建模的价值。6.3 残差分析模型是否还有可利用的信号预测做完不代表收工一定要对最终预测的残差再做一次自相关检验。如果组合预测残差的Ljung-Box p值大于0.05说明已经提取干净了模型可以收工。如果p值显著小于0.05说明还有结构信息没被利用需要回头调整ARIMA阶数或CNN-LSTM的结构。7. 实践中的坑与排查记录最后这部分是我最想写的内容。整个项目我前后跑了三轮前期大部分时间都花在调试各种莫名其妙的问题上。我把最有代表性的几个问题和排查过程整理出来你遇到类似情况可以直接按这个思路排查。7.1 数据泄露归一化和交叉验证的顺序问题第一版代码我为了简单直接对整个序列做MinMaxScaler再切分训练集和测试集。结果测试集上的RMSE低到不可思议我心里就觉得不对劲一查果然数据泄露了。测试集的最大最小值已经把信息透露给scaler了模型在训练时“偷看”了测试集的统计量。后来改成先切分再归一化指标立刻正常了。7.2 维度不匹配Conv1D输入shape理解错误Keras的Conv1D和LSTM要求输入格式是三维的(样本数, 时间步长, 特征数)。我第一次把X_train直接传进去报错说维度是2维。这个问题新手特别常见解决办法就是reshape(-1, 12, 1)。但还有一种更隐蔽的情况如果特征数写错比如原始数据有两列特征你却写1模型不会报错但精度会显著下降。建议每次构建序列后打印X_train.shape确认维度。7.3 差分还原后的预测偏移ARIMA在差分后预测需要累加差分值还原真实尺度。我在做手动差分时踩过这个坑某一期预测值的还原公式算错导致整条预测序列逐渐漂移越往后偏移越严重。后来我全部改用statsmodels的fittedvalues和forecast方法它内部自动处理差分还原不再手写累加逻辑问题彻底解决。如果你非要手动实现务必把累计求和公式单独写单元测试验证。7.4 序列预测滞后效应与提前多步的误差累积LSTM类模型在单步预测时表现很好但在多步预测时会出现一个典型现象预测曲线比真实曲线滞后尤其是在拐点处。原因是模型本质上学到的是“用最近几点的趋势外推”在数据突变时反应不过来。我的处理办法是把预测窗口拆成滚动预测每预测一步就更新一次输入窗口虽然不能彻底消除滞后但误差增长被明显抑制了。7.5 训练过程不收敛或NaN损失如果你的loss变成了NaN第一个检查点是学习率。Adam默认学习率0.001在大多数场景够用但在小样本时序任务上偶尔会炸。我遇到过一次loss在第30轮直接变成NaN排查之后发现是数据里有NaN没清理干净梯度回传时传了NaN。优先检查数据里的NaN和无穷值再调学习率。问题现象排查思路解决办法lossNaN检查数据是否含NaN/无穷值检查学习率是否过大清理数据学习率降到0.0005验证集指标虚高检查是否先归一化再切分改为先切分后归一化预测曲线滞后明显检查是否使用滚动窗口预测每步更新输入窗口再预测下一步ARIMA残差仍有自相关Ljung-Box p值小于0.05增大ARIMA阶数或改用SARIMA模型过拟合训练集指标远好于验证集加Dropout或EarlyStopping7.6 数据量不足时的替代思路如果数据总量少于100个月度数据点神经网络很容易过拟合组合模型的增益会被噪声吞掉。这时候我不建议硬上CNN-LSTM可以考虑把LSTM换成GRU减少参数量或者把残差建模换成简单得多层感知机让模型更小。等数据量积累到200个点以上再切换回CNN-LSTM结构更稳妥。我在实际使用中还有一个体会这套组合模型的调参顺序非常重要先用默认参数跑通流程确认数据和代码链路没有问题再逐步调ARIMA的阶数和CNN-LSTM的层数最后才动学习率、batch_size这些细节参数。一轮只改一个变量否则你根本不知道是哪个改动带来了精度提升。做完这轮项目我现在对“混合模型一定是堆料炫技”这种说法彻底改观了关键是看模型之间的分工是不是真正互补拼接方式是否合理。
返回列表