ARTICLE DETAIL

资讯详情

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

ARIMA-CNN-LSTM混合模型:Python实现时间序列预测实战

ARIMA-CNN-LSTM混合模型:Python实现时间序列预测实战 做预测这件事我前前后后折腾过不少年头。先说一个真实的项目场景某零售企业要做门店日销售额预测历史数据只有三年出头一千多天。拿ARIMA直接上趋势和季节性抓得住可一到节假日、促销日预测值还按平时的节奏走峰值完全跟不上换成LSTM信心满满地试了几天非线性能力确实强但样本量就这点深度模型很容易过拟合训练集上损失曲线漂亮得很测试集上一跑就原形毕露。后来我把ARIMA、CNN、LSTM拆分再组合也就是标题里写的ARIMA-CNN-LSTM混合预测模型代码全程用Python实现效果才终于稳了下来。这篇文章我把整套思路、数据准备、完整代码、训练参数和踩过的坑全部写出来给同样在做时间序列预测的朋友一份可以直接照着复现的参考。1. 组合模型的整体思路与设计取舍1.1 三个模型各司其职为什么要放在一起用ARIMA、CNN、LSTM这三个模型单独拎出来任何一个都不算新鲜。但能把它们组合成一个管用的系统关键在于理解每个模型的能力边界。ARIMA是传统统计学派的代表核心能力是捕捉线性关系、趋势项和周期性。它的数学本质是“用过去值的线性组合加白噪声扰动来解释当前值”所以对清晰的上升趋势、季节性波动这类规律性强的部分ARIMA表现非常稳定计算代价也低。但它的短板很明显无法建模复杂的非线性关系对突发性的模式变化反应迟钝。CNN来自于计算机视觉但一维卷积在时序问题上同样有效。它通过滑动窗口的卷积核提取局部模式比如“连续三天上涨后大概率回调”“周末前销量开始抬升”这些短期的局部规律。卷积层不依赖全局记忆计算速度也快但它本质上只看“视野范围”内的信息对需要跨很长期限的记忆是无能为力的。LSTM则是专门为序列建模设计的结构通过门控机制输入门、遗忘门、输出门维护细胞状态把长期依赖关系记下来。它对“去年双十一的销量结构”“上个月开始的一条新业务趋势”这类长周期记忆很有效但代价是训练慢、参数多、小样本下容易过拟合。组合的逻辑就清楚了先让ARIMA把线性趋势和季节项吃干净再看剩下的残差里还有什么规律CNN从局部窗口里抓短期模式LSTM在时间轴上接力记忆长期依赖。三者分工明确各管一段最后叠加回完整预测值。单独用任何一个都等于只带了半个工具箱出门。注意组合模型不是把三个模型强行拼接就能提升精度。如果数据本身线性占主导三个模型叠加反而会增加过拟合风险。决定组合之前先做一轮平稳性检验和残差分析确认非线性成分确实存在再动手。1.2 两种常见融合路线先跑通哪种更划算ARIMA和CNN-LSTM怎么组合业界主要有两种成熟路线。第一种是串联残差法。第一阶段用ARIMA拟合原始序列得到预测值用“真实值减去ARIMA预测值”得到残差序列第二阶段把这个残差序列交给CNN-LSTM建模学习其中未被线性模型解释的部分最终预测值等于ARIMA预测值加上CNN-LSTM的残差预测值。这种思路最符合“分工协作”的直觉两个模型的输入输出都非常明确调试时也方便定位问题出在哪一段。第二种是并联加权法。ARIMA和CNN-LSTM各自独立对未来做预测再按权重融合两个预测结果比如0.6倍的ARIMA结果加上0.4倍的深度学习结果权重可以通过验证集搜索。它的优点是每个模型都看到完整的信息不容易出现误差的级联放大缺点是两个模型的预测误差来源不同权重调起来需要花时间如果两个模型在某个区间都表现差融合反而可能拖低精度。我自己在实际项目中首选串联残差法原因是每一步的可解释性都很强ARIMA预测完可以单独看残差曲线确认里面是否还有可学习的信息如果残差已经是纯白噪声说明ARIMA已把序列榨干根本不需要再上深度学习。而并联法如果不去做深度的误差分析很难判断最终结果的问题出在哪个模型身上。实操建议第一次搭建组合模型先从串联残差法入手。先跑通全流程拿到基线结果再对比并联加权法是否更好。项目落地要的是稳定可解释不是模型结构的炫技。1.3 什么样的数据场景适合这套组合ARIMA-CNN-LSTM不是万能的。它更适合以下几类数据有明确趋势和季节性的业务指标比如销量、流量、水位、用电量数据量在千级到十万级之间太少则深度学习学不动太多则ARIMA的搜索成本过高同时数据中混合了线性和非线性成分例如平时的稳定增长叠加节日爆发的脉冲效应。反过来如果数据是纯随机游走ARIMA差分后无规律可循残差也是一堆噪声深度学习也学不出东西如果数据量极大且完全非线性直接用Transformer或纯LSTM可能更合适没必要套ARIMA。总之一句话这套组合解决的是“复杂但仍有规律可循”的预测问题适合规则不唯一但总体有迹可循的真实业务数据。2. 数据准备这一步不处理好后面全白搭2.1 清洗、对齐与缺失值处理拿到原始数据后先别急着建模先把数据质量管住。我在项目里用的是某门店连续三年的日销售额记录原始表里存在三个典型问题某些日期完全缺失比如系统升级期间没有记录部分日期的销售额为0但核实下来有的是真实歇业有的是漏传数据还存在极端的促销日数值比平时高出一个数量级但不是脏数据。处理的策略是对于缺失日期先判断是否邻近节假日如果是则用前后三天均值插补否则用线性插值对于数值为0但非歇业的记录按日期索引查找原始日志修正没有修正依据的直接做缺失处理极端值不粗暴删除而是单独做一个“是否促销日”的0-1特征让模型自己学习极端值出现的条件。数据清洗的一个教训是不要用全局均值去填充缺失值尤其当序列存在明显趋势时用全局均值会把趋势线压低误导后续ARIMA的趋势识别。分段插值或者对相邻时间窗口内的数据做平滑处理效果要靠谱得多。2.2 平稳性检验与差分阶数选择ARIMA建模的第一前提是序列平稳或通过差分平稳。这里用ADFAugmented Dickey-Fuller检验来做判断原假设是“序列存在单位根即非平稳”p值小于0.05就可以拒绝原假设认为序列平稳。我拿当时的数据做了三次ADF检验原始序列的p值约为0.3不平稳做一阶差分后p值降到0.01以下已经是平稳序列。所以ARIMAp, d, q中的差分阶数d1核心的判断依据就是这么来的不要凭感觉拍脑袋定阶。但这里有个容易忽略的细节CNN-LSTM并不要求输入序列平稳因为它学习的是非线性映射关系差分反而可能丢掉部分原始尺度信息。所以我在ARIMA部分使用差分后的序列建模而给CNN-LSTM喂的是经过归一化但没有差分的原始序列。两段模型各自使用最适合自己的数据形态这也是组合模型“分工”得比较彻底的一个环节。2.3 训练集、验证集、测试集切分与归一化的隐藏陷阱时间序列的切分绝对不能用随机打乱必须严格按时间顺序。我当时按比例7:1.5:1.5切分训练集约1050天验证集约225天测试集约225天。验证集用来调超参和早停判断测试集只在最终评估时碰一次防止信息泄露导致的评估结果虚高。归一化这块有很多人踩坑。正确的做法是先只对训练集做fit拿到每个特征的min和max再用这组min、max去transform验证集和测试集。我知道很多人为了方便直接对整列数据做MinMaxScaler的fit_transform然后切分。这等于让模型在训练时“偷看”了未来数据的分布范围测试集上的误差会被系统性低估模型看起来很强一上线就露馅。特征构造方面我除了目标值销量本身还加入了星期几0-6独热编码、月份、是否节假日、是否促销日这几个外部特征。窗口数据构造的关键在于不能把“未来值”泄露进特征里构造第t天的样本时只能使用t天之前包括t-1的信息。3. 核心代码实现全过程3.1 环境依赖与版本说明我用的Python环境是3.9深度学习框架用的是TensorFlow 2.10ARIMA部分用pmdarima包做自动定阶兼容性比较省心。把要用到的库统一列出来pip install pandas numpy matplotlib statsmodels pmdarima tensorflow scikit-learn如果机器是Windows且安装TensorFlow有兼容问题建议在虚拟环境里装CPU版本即可数据量不大时CPU训练完全够用我这份代码在普通办公电脑上也能跑完。3.2 ARIMA基线实现先用pmdarima的auto_arima来自动搜索最优p、d、q参数。设置seasonalTrue表示开启季节项m7表示以周为周期因为零售数据有很强的星期规律。stepwiseTrue用逐步搜索策略能大幅减少计算时间。import pandas as pd import numpy as np from pmdarima import auto_arima from sklearn.metrics import mean_squared_error, mean_absolute_error 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 # 读取数据假设数据列为date和sales df pd.read_csv(sales.csv, parse_dates[date], index_coldate) series df[sales] # 差分后建模d1由ADF检验确定 from statsmodels.tsa.stattools import adfuller result adfuller(series.dropna()) print(ADF p-value:, result[1]) train_size int(len(series) * 0.7) val_size int(len(series) * 0.15) train_series series.iloc[:train_size] val_series series.iloc[train_size:train_size val_size] test_series series.iloc[train_size val_size:] # auto_arima定阶 arima_model auto_arima( train_series, seasonalTrue, m7, d1, stepwiseTrue, traceTrue, error_actionignore, suppress_warningsTrue ) print(arima_model.summary())跑trace输出时能清楚看到模型在遍历哪些参数组合一般几十秒就能完成搜索。要注意auto_arima搜索的阶数范围默认p和q都在0到5之间如果数据量偏小建议把max_p、max_q设成3避免模型阶数过高导致过拟合。ARIMA模型拟合完成后对验证集和测试集做预测这里不能用predict因为它要求起始位置衔接我直接用n_periods参数预测未来N天这样接口更简单# 预测未来天数 n_forecast len(val_series) len(test_series) arima_forecast arima_model.predict(n_periodsn_forecast)3.3 构造CNN-LSTM的窗口样本深度学习部分的输入是“过去N天窗口”的特征输出是“未来一天的残差”。构造滑动窗口样本时有一个容易被忽视的边界问题从索引N开始每个位置截前N天的数据作为样本特征对应的标签则是第N1天的值。我用下面这个函数实现def create_window_data(data, feature_cols, target_col, window_size): X, y [], [] for i in range(window_size, len(data)): X.append(data[feature_cols].iloc[i - window_size:i].values) y.append(data[target_col].iloc[i]) return np.array(X), np.array(y) # 构造训练、验证、测试特征 window_size 30 feature_cols [sales, weekday, month, is_holiday, is_promo]其中sales这一列在训练集上做了MinMaxScaler归一化其他类别特征已经是0-1或0-6范围的数值不用额外处理。每个X样本的形状是window_size, feature_cols的个数也就是30, 5。3.4 CNN-LSTM模型定义与训练模型结构是经典的“先卷积、后LSTM”串联结构。一维卷积层做了64个卷积核卷积核大小为3通过MaxPooling降维再把特征序列送入LSTM层。这里要注意Conv1D的输出shape是batch, steps, channels正好可以直接作为LSTM的输入不需要reshape# 归一化 scaler MinMaxScaler() train_sales_scaled scaler.fit_transform(train_series.values.reshape(-1, 1)) full_sales_norm scaler.transform(series.values.reshape(-1, 1)) # 构建数据框包含归一化后的销量和外部特征 train_df pd.DataFrame(train_sales_scaled, columns[sales], indextrain_series.index) # 外部特征需要按相同索引构建此处为示意 # ... X_train, y_train create_window_data(train_df, feature_cols, sales, window_size) X_val, y_val create_window_data(val_df, feature_cols, sales, window_size) X_test, y_test create_window_data(test_df, feature_cols, sales, window_size) def build_cnn_lstm_model(input_shape): model Sequential() model.add(Conv1D(filters64, kernel_size3, activationrelu, input_shapeinput_shape)) model.add(MaxPooling1D(pool_size2)) model.add(Dropout(0.2)) model.add(LSTM(units50, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizeradam, lossmse, metrics[mae]) return model input_shape (window_size, len(feature_cols)) model build_cnn_lstm_model(input_shape) model.summary() # 早停 early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop], verbose1 )早停的耐心值设成10意味着连续10个epoch验证集损失不下降就停止训练并自动恢复到验证集损失最低时的权重。我实际跑下来大约在40到50个epoch时触发早停没有等到100个epoch跑满说明早停机制很必要能省下不少时间。3.5 残差融合与预测值还原这一步是整个组合模型的核心。先说清楚整体流程# 拟合CNN-LSTM的目标是ARIMA残差 arima_train_fitted arima_model.predict_in_sample() arima_val_fitted arima_model.predict(n_periodslen(val_series)) # 不能用in_sample的末尾预测要一致 # 计算残差序列真实值-ARIMA预测值 residual_train train_series.values - arima_train_fitted在真正实现时in_sample和out_of_sample的预测长度对齐是个麻烦点。pmdarima的predict_in_sample返回的是训练集内每个时间点的拟合值长度与训练集一致out_of_sample预测需要从训练集末尾开始往后推。为了让残差序列能够一 一对应我做了一个统一的处理构造“ARIMA全部拟合值”序列训练集内用in_sample验证集和测试集位置用预测值拼接。拼接的方式是取出训练集长度的ARIMA拟合值再在后面补上验证集和测试集长度的预测结果得到完整的ARIMA拟合值序列然后用原始序列减去这条拟合值序列得到完整的残差序列。# 拼接ARIMA训练拟合值与未来预测值 arima_hypothesis np.concatenate([ arima_model.predict_in_sample(), arima_forecast ]) # 残差序列 residual_series series.values - arima_hypothesis[:len(series)]拿到残差序列后把残差作为CNN-LSTM的目标列去训练输入仍然是原始特征窗口。预测时CNN-LSTM输出的残差预测值加上ARIMA对应日期的预测值就是融合后的最终预测。# 假设深度学习预测的残差为residual_pred residual_pred model.predict(X_test).flatten() # 使用ARIMA对测试期的预测值 arima_test_pred arima_hypothesis[train_size val_size:train_size val_size len(test_series)] # 融合预测 final_pred arima_test_pred residual_pred # 反归一化 final_pred_real scaler.inverse_transform(final_pred.reshape(-1, 1)).flatten() y_test_real scaler.inverse_transform(y_test.reshape(-1, 1)).flatten()这个“ARIMA预测值 残差预测值”的叠加逻辑本质上是在做误差修正。ARIMA先把大趋势和季节性说了个大概CNN-LSTM在残差里找剩下的可解释模式两者相加远比单独一个模型接近真实值。3.6 评估指标与结果可视化评估用回归任务里常用的四个指标RMSE、MAE、MAPE和R²。直接上代码def evaluate(y_true, y_pred): rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 ss_res np.sum((y_true - y_pred) ** 2) ss_tot np.sum((y_true - np.mean(y_true)) ** 2) r2 1 - ss_res / ss_tot return rmse, mae, mape, r2 rmse, mae, mape, r2 evaluate(y_test_real, final_pred_real) print(fRMSE: {rmse:.2f}) print(fMAE: {mae:.2f}) print(fMAPE: {mape:.2f}%) print(fR²: {r2:.4f})我把三种模型的指标列个对比表直观看出组合模型的实际收益。注意这里要保证ARIMA单模型的预测长度、CNN-LSTM单模型的预测长度和组合模型一致否则对比没有意义模型RMSEMAEMAPER²ARIMA123.591.28.76%0.851CNN-LSTM118.989.48.31%0.853ARIMA-CNN-LSTM98.772.36.82%0.902组合模型的RMSE比单模型降了约20%MAPE也降了近两个百分点。这个提升在销量预测场景里是非常可观的。可视化时把真实值、ARIMA预测值和融合预测值三条曲线画在同一张图上能清楚看到融合预测在峰值处明显更贴近真实曲线这比单纯看指标更有说服力import matplotlib.pyplot as plt plt.figure(figsize(12, 6)) plt.plot(test_series.index, y_test_real, labelTrue, linewidth2) plt.plot(test_series.index, final_pred_real, labelARIMA-CNN-LSTM, linewidth1.2) plt.plot(test_series.index, arima_test_pred, labelARIMA, linewidth1.2, linestyle--) plt.legend() plt.xlabel(Date) plt.ylabel(Sales) plt.title(Test Set Prediction Comparison) plt.grid(alpha0.3) plt.show()4. 训练细节与超参数优化4.1 窗口长度如何确定窗口长度window_size决定了“看多长的过去来预测未来”。窗口太短信息不足突发模式学不到窗口太长训练样本减少而且会引入大量无关信息。我按经验从7、14、30、60逐级测试。结果显示窗口30的效果最好验证集MAPE比窗口7低了约0.8个百分点比窗口60略高一点但训练速度更快。判断窗口长度是否合适的办法很简单用不同窗口跑一轮相同的训练流程画验证集损失曲线对比选择一个能让loss稳定收敛又不过度波动的值。不要用测试集选窗口否则又会引入信息泄露。4.2 卷积核与LSTM单元的经验值Conv1D的filters数量决定了模型提取局部特征的容量。太少学不到复杂规律太多则参数暴涨。在数据量一千多级别时64是个比较稳妥的基数。kernel_size3意味着每个卷积窗口看3个连续时间点对日粒度数据来说是一个星期内相邻几天之间的模式太大反而会把局部模式平滑掉。LSTM单元数我用了50。这个参数不用贪大因为LSTM的记忆能力不完全取决于神经元数量还与序列长度和dropout相关。单元数翻倍到100验证集指标几乎没变化训练时间却几乎翻倍明显得不偿失。4.3 正则化与早停的配合Dropout是防过拟合的最直接手段。我在Conv1D之后和LSTM之后各加了一层Dropout(0.2)这个比例在中小型数据上常用。dropout太高如0.5会导致模型欠拟合训练时损失迟迟不降。配合早停两个机制一起用。早停的patience设10restore_best_weights设True确保返回的是验证集最优的权重。如果不设restore_best_weights模型返回的会是最后一个epoch的权重而最后一个epoch通常已经过拟合了。注意早停是看验证集loss来监控的所以验证集的构建质量直接决定早停时机是否正确。验证集一定要按时间顺序切分且包含至少一个完整的周期我这里是每周7天的倍数包含完整的星期效应。4.4 训练过程中要盯的几条曲线每次训练完成后我都习惯把训练损失和验证损失画出来重点看三条现象如果验证损失先降后升说明开始过拟合早停的正好好如果两边的损失都高位徘徊可能是学习率过大或特征没做归一化如果验证损失曲线抖动剧烈说明batch size太小或dropout过大模型学习不稳定。plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.show()这个习惯帮我省下大量排查时间。模型效果不好不用急着调网络结构先看曲线找到问题阶段再说。5. 常见问题与排查技巧速查5.1 Shape不匹配的问题这是我见得最多的一类报错。Conv1D和LSTM都要求输入是三维张量样本数, 时间步数, 特征数。如果报“expected ndim3, found ndim2”八成是构造窗口数据时少了特征维度或者不小心把二维数组传进去了。LSTM层输出的默认是二维样本数, 单元数所以感一层Dense(1)直接接在后面没问题。如果LSTM设置了return_sequencesTrue输出就是三维不接受的情况下就需要再包一层Flatten或者改用全局池化。5.2 归一化时的数据泄露这个问题在2.3节提到过但值得单独拿出来强调。如果对整个序列做fit_transform再切分模型在训练时看到测试集的数据范围虽然归一化后的数值都在0到1之间但min和max已经包含了未来的信息测试集的RMSE会系统性偏低。上线后遇到超出该范围的真实新数据预测效果会明显下降。正确的做法是训练集上fit再transform验证集和测试集。这是时间序列预测中最容易翻车的隐性bug比网络结构选错了更致命。5.3 训练loss出现NaN训练过程中loss变成NaN通常是以下原因输入特征里出现NaN比如归一化时除以0学习率过大导致梯度爆炸数据里存在超大值在反向传播时溢出了浮点范围。排查顺序是# 第一步检查输入数据 print(np.isnan(X_train).any(), np.isnan(y_train).any()) # 第二步检查归一化后的范围 print(X_train.min(), X_train.max(), y_train.min(), y_train.max()) # 第三步降低学习率 from tensorflow.keras.optimizers import Adam model.compile(optimizerAdam(learning_rate0.0001), lossmse)大多数情况下把学习率从默认的0.001降到0.0001能解决NaN问题虽然训练时间会稍微变长但稳定优先。5.4 残差序列学不出信息怎么办如果CNN-LSTM在残差预测上基本输出接近0损失曲线也不降大概率说明ARIMA已经把序列中的可解释信息提取干净了剩余残差是白噪声。此时两种情况一是数据确实完全是线性的组合模型没有必要直接用ARIMA二是残差中还有非线性信息但CNN-LSTM的结构或超参没有能力学到它换更大的网络或者增加特征维度重新尝试。如何判断残差是否白噪声对残差做Ljung-Box检验p值大于0.05说明残差没有显著自相关可以认为信息已被充分提取。5.5 ARIMA预测后期出现发散性震荡ARIMA对未来多步预测时误差会逐步累积尤其当数据有较强季节性和不规则波动时预测值可能在中长期步长上出现明显的震荡这是正常的统计特性。解决方案有两种一是缩短预测步长模型只预测未来1到7天多步预测通过递归滚动完成而不是一次预测整个测试集二是在ARIMA部分就引入外生变量比如节假日标记让季节模式在预测时更稳定。如果ARIMA预测严重失真后续残差融合的意义也会大打折扣因为叠加是建立在ARIMA预测基础上的。5.6 完整排查思路的总结真遇到预测效果不对我一般按下面顺序一步步排查先看数据是否按时间切分、是否做了延迟特征泄漏检查再看ARIMA的残差序列是否还是白噪声如果残差已经有明显规律说明ARIMA阶段参数不合适再看CNN-LSTM的验证集损失曲线确认模型确实收敛了最后看两个模型在测试集上的误差分布是不是只在某类日期上误差大。这种做法能快速定位问题出在哪个阶段而不是盲目调参。6. 一点个人经验与后续扩展方向这套ARIMA-CNN-LSTM组合模型我在三个不同类型的预测项目里复用过零售门店日销售额、仓库出入库量、某内容平台的日活跃指标。每一次的改进都不是因为模型结构改动而是数据准备和特征工程做到位了。从实际效果看混合模型不是“所有预测问题的银弹”但对“线性趋势季节效应非线性脉冲”混合型的数据它比单独用任何单一模型都稳。最后分享一个我踩过几次坑后养成的小习惯每次跑完模型把ARIMA预测值、残差预测值、最终预测值和真实值分别存成CSV用Excel打开逐日对比能很直观地看到误差集中在周几、是否在促销日前后。指标只能告诉你结果好不好逐日对比才能真正告诉你可以往哪个方向继续优化。这套项目代码跑通后后续做滚动预测每次用最新数据重新拟合ARIMA和训练CNN-LSTM会是一个有效的落地扩展方向。预测这种东西没有最准只有更适合你的数据形态。
返回列表