ARTICLE DETAIL

资讯详情

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

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

ARIMA-CNN-LSTM组合模型:Python实现时间序列预测实战 在开始前先说明这是一篇关于时间序列预测建模的实操性总结围绕 ARIMA、CNN、LSTM 三种模型的组合应用展开基于 Python 实现适合对时序预测有一定基础、想尝试传统统计模型与深度学习融合方案的读者。如果你正卡在ARIMA 只能做线性预测、LSTM 又不容易收敛的两难境地这篇内容应该能给你一个相对完整的解决路径。1. 整体设计思路为什么非要把 ARIMA、CNN、LSTM 拼在一起1.1 单一模型的局限线性与非线性能力很难两全先说一个很多人在实际预测项目里都会撞上的问题单用 ARIMA它对趋势和季节性这种线性规律拟合得确实漂亮解释性也强可一旦数据里混入波动大、非线性强的片段残差就会变得很难看。反过来LSTM 这类深度学习模型处理非线性模式的能力很强却经常忽略时序数据里天然存在的线性结构导致训练时间长、收敛慢还可能学出一堆没意义的噪声规律。我早期做过一个销量预测的项目用 ARIMA 跑出来的均方根误差看着还行但一到促销节点前后预测曲线明显跟不上实际曲线的突变。换成 LSTM 单独跑整体误差虽然降了一点但模型对周期性规律的记忆明显弱于 ARIMA尤其在长周期数据上表现得不够稳定。后来才意识到问题不在某个模型不好而是我没有把模型的优势组合起来。1.2 ARIMA-CNN-LSTM 组合架构的分工逻辑这套组合的核心思路是这样的ARIMA 负责捕捉序列中的线性成分和趋势项把它的预测结果作为一个基准特征CNN 这块其实不是用来做图像识别的它工作在时间维度上通过卷积核提取序列的局部特征相当于给数据做了一次局部模式扫描把突变点、短期形态这些信息先提炼一层LSTM 再接棒处理 CNN 输出中仍然存在的长期依赖关系。一个容易踩的坑是有人会把三个模型做成串联结构也就是 ARIMA 预测完把结果塞给 CNN再塞给 LSTM最后直接出最终值。实际上这样做误差会叠加而且 ARIMA 的残差信息会被下一层模型当成输入特征反而引入了更多噪声。我建议采用并联加融合的方式准确来说是让 ARIMA 处理原始序列的线性部分同时让 CNN-LSTM 这块处理原始序列的残差部分最后把两部分的预测结果叠加在一起得到最终的预测值。这种结构里每个模型都只处理自己擅长的那部分信息不会互相干扰。2. 数据准备与特征工程整套模型的起点其实在这里2.1 数据清洗别让脏数据把三个模型一起带偏任何预测模型的第一步都是数据质量检查这一步做不好后面调参调得再勤快也是白费。对于时间序列数据我一般会按几个固定动作来检查缺失值处理连续缺失超过 5% 的序列段直接标记为异常区间结合业务场景判断是否需要插补。对一般的销量、流量类数据线性插值就够了但对波动率很大的数据用前向填充加后向填充的组合方式会更稳妥。异常值检测这里推荐用残差法也就是先跑一个简单模型把残差异常大的点位揪出来再人工判断是真实波动还是数据采集错误。直接按 3 倍标准差删点很容易误伤真实规律。时间戳对齐多个数据源拼接时时间戳不一致是常见的坑必须统一成同一个时区、同一个采样频率否则 CNN 的卷积核扫描窗口会把不同频率的信息混在一起。2.2 三种模型的输入格式差异与统一策略这里要特别提醒一点ARIMA、CNN、LSTM 三个模型的输入格式是不一样不能直接拿同一份数据丢给三个模型需要做一次格式适配。ARIMA 的输入是一维序列直接传入完整的时间序列值它在建模时会自己处理滞后项和差分。CNN 的输入要求是二维矩阵形状是样本数时间步长特征数。对时间序列来说这里的特征数不是指多变量特征而是指你把多少个历史值堆叠成一个样本。比如用过去 10 天预测第 11 天那每个样本就是一个 10 维向量。LSTM 的输入是三维张量形状是样本数时间步长每个时间步的特征维度。注意这里的时间步长和 CNN 里的时间步长含义不太一样LSTM 是把序列按时间展开每个时间步是一个观测点特征维度指的是每个观测点上有几个变量。统一策略上我会先把原始序列做一阶差分得到一个平稳序列ARIMA 在这个差分序列上建模同时把差分序列按滑动窗口切成固定长度的样本用于 CNN 和 LSTM 的训练。这样三个模型实际上是共享了同一份差分后的信息避免了水平差异造成的模型偏差。2.3 滑动窗口长度怎么定一个实操经验值滑动窗口长度是最影响模型效果的超参数之一。太短模型看不到足够的历史规律太长CNN 的卷积核覆盖范围会被稀释LSTM 的梯度传播也会变得困难。我的经验是先从数据本身的周期长度入手。如果数据有以 7 天为单位的周期性窗口长度至少覆盖两个周期也就是 14 天如果有月度效应窗口长度要覆盖至少 30 天。窗口长度的确定本质上是一个通过实验验证的过程建议在 10、14、21、30 这几个值上用验证集分别跑一轮看验证集误差最小的是哪个。不要一上来就凭感觉选个 20那样后面调参时很难判断是模型问题还是窗口问题。3. Python 环境与核心库配置跑通模型前的最后一道关3.1 版本兼容性深度学习框架的老大难问题我在配置环境时踩过很多次坑最典型的就是 tensorflow、numpy、pandas 三者版本不兼容装完库之后 import 直接报错。为了防止读者在环境配置上浪费大量时间我直接给出一套经过验证的版本组合库名推荐版本说明Python3.9 或 3.103.11 下部分深度学习库的兼容性还有问题numpy1.24.3不要装高于 2.x很多库还没适配但后续会好一些pandas2.0.3与 numpy 1.24.x 配套statsmodels0.14.0ARIMA 模型实现库tensorflow2.13.0CPU 环境就够用GPU 可选scikit-learn1.3.0用于数据标准化和误差计算matplotlib3.7.2可视化预测结果3.2 安装命令与快速验证装完立刻能确认环境可用建议用虚拟环境隔离项目依赖避免多个项目的包之间互相干扰。安装命令整理如下conda create -n ts_forecast python3.9 -y conda activate ts_forecast pip install numpy1.24.3 pandas2.0.3 statsmodels0.14.0 pip install tensorflow2.13.0 scikit-learn1.3.0 matplotlib3.7.2装完之后跑一段快速验证代码确认环境没问题import tensorflow as tf import statsmodels.api as sm import numpy as np print(TensorFlow version:, tf.__version__) print(NumPy version:, np.__version__) print(Statsmodels version:, sm.__version__) # 快速验证一个最小 LSTM 模型能否构建 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense model Sequential([ LSTM(16, input_shape(10, 1)), Dense(1) ]) model.compile(optimizeradam, lossmse) print(LSTM model built successfully)如果这段代码能顺利跑通说明基础环境没问题后面专心调模型逻辑就行。3.3 CPU 与 GPU 的取舍中小数据集根本不需要 GPU很多人一提到深度学习就觉得必须上 GPU其实对于时间序列预测这种任务绝大多数业务场景下的数据集规模都很有限。我自己测过几万条数据用 CPU 跑 LSTM 训练一轮 epoch 也就是几秒到十几秒的事完全在可接受范围内。如果你只是在学习或者做原型验证不需要配 GPU 环境反而省去了驱动配置的一大堆麻烦。只有当数据量达到百万级、特征维度很高时GPU 的加速效果才值得考虑。4. 模型构建与代码实现分层拆解三个模型的落地细节4.1 ARIMA 部分从定阶到拟合的完整代码ARIMA 的建模核心是 p、d、q 三个参数的确定。d 是差分阶数一般通过 ADF 检验来判断序列平稳性p 和 q 可以通过自相关函数 ACF 和偏自相关函数 PACF 的截尾特征来初判也可以用 AIC/BIC 信息准则来自动搜索。我通常的做法是先画 ACF 和 PACF 图确认范围再用 AIC 做网格搜索。import statsmodels.api as sm from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import itertools import warnings warnings.filterwarnings(ignore) # 1. ADF 检验判断平稳性 def check_stationarity(series): result adfuller(series) print(fADF Statistic: {result[0]:.4f}) print(fp-value: {result[1]:.4f}) return result[1] 0.05 # 2. 网格搜索最优 (p, d, q) def find_best_arima(train_series, max_p5, max_q5, d_rangerange(0, 3)): best_aic float(inf) best_order None for d in d_range: for p in range(max_p 1): for q in range(max_q 1): try: model sm.tsa.ARIMA(train_series, order(p, d, q)) result model.fit() if result.aic best_aic: best_aic result.aic best_order (p, d, q) except: continue print(fBest ARIMA order: {best_order}, AIC: {best_aic:.4f}) return best_order # 3. 拟合与预测 best_order find_best_arima(train_series) arima_model sm.tsa.ARIMA(train_series, orderbest_order) arima_result arima_model.fit() # 预测未来 N 步 forecast_steps len(test_series) arima_forecast arima_result.forecast(stepsforecast_steps)这里的网格搜索会有一点耗时但数据量不大的情况下完全可控。一个很重要的细节是ARIMA 拟合的序列必须是差分后的平稳序列如果原始序列本身就不平稳直接传入建模结果会非常不稳定这是统计学模型和神经网络模型一个很大的差异。4.2 CNN 部分时间序列上的卷积如何理解CNN 在时间序列预测里通常用一维卷积 Conv1D它的工作方式和图像卷积不太一样。图像卷积是在二维空间上滑动时间序列的一维卷积则是在时间轴上滑动一个窗口把窗口内的几个连续时间点的值做加权求和从而提取局部模式。假设输入序列长度为 10每个样本是一个 10 维向量那么 Conv1D 的卷积核大小设置为 3 时它每次扫描连续 3 个时间点输出一个特征值然后把窗口往后挪一步继续扫描。这样扫完整个序列就得到了一个新的特征序列。多个卷积核就相当于用多组不同的权重模板去提取不同的局部模式。在代码层面以下是一个可跑的 CNN 部分实现from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense def build_cnn_model(input_shape): model Sequential([ Conv1D(filters64, kernel_size3, activationrelu, input_shapeinput_shape), MaxPooling1D(pool_size2), Conv1D(filters32, kernel_size3, activationrelu), MaxPooling1D(pool_size2), Flatten(), Dense(units50, activationrelu), Dense(units1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) return model这里有两个参数需要解释。filters 是卷积核数量相当于提取多少种局部模式过多容易过拟合过少则特征提取不足一般从 32 到 64 开始尝试。kernel_size 是卷积核大小对时间序列来说通常取 3 到 7 之间的小值因为局部模式一般不会跨越很长的区间。池化层的作用是降维但池化步长不能太大否则会丢掉重要的时序位置信息。4.3 LSTM 部分核心原理与关键结构调整LSTM 是这套组合里的主力记忆单元。它和普通 RNN 的区别在于引入了细胞状态和三个门结构遗忘门、输入门、输出门。用大白话说遗忘门决定哪些历史信息要丢弃输入门决定哪些新信息要写入记忆输出门决定当前时刻输出什么。正是这个机制让 LSTM 能在长序列上保持梯度稳定不至于因为序列太长而丢失早期的重要规律。在实际编码中有几个容易出问题的地方。第一个是 return_sequences 参数的设置它是控制 LSTM 层连接方式的关键。如果这一层后面还有 LSTM 层那么 return_sequences 必须设为 True因为要输出每个时间步的结果给下一层如果后面直接接全连接层就设为 False只输出最后一个时间步的结果。第二个是 dropout 的设置。LSTM 非常容易过拟合尤其是在数据量不是特别大的情况下。建议在 LSTM 层之间加入 dropout 层丢弃比例从 0.2 开始根据验证集情况逐步增加。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_lstm_model(input_shape): model Sequential([ LSTM(units50, return_sequencesTrue, input_shapeinput_shape), Dropout(0.2), LSTM(units50, return_sequencesFalse), Dropout(0.2), Dense(units25, activationrelu), Dense(units1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary() return modelinput_shape 这里是一个二元组比如101表示时间步长为 10每个时间步的特征维度为 1。如果数据是多元的特征维度就对应变量的数量。units 是 LSTM 隐藏单元的数量它决定了记忆容量50 是一个比较稳妥的起点。4.4 融合层将 ARIMA 与 CNN-LSTM 的预测合并融合方式上我采用加法融合而非拼接融合原因是两者的预测目标不同。ARIMA 的预测结果是差分序列上的值CNN-LSTM 的输出也是差分序列上的值但两者捕捉的信息侧重点不同一个偏线性趋势一个偏非线性残差。将两者直接相加就得到了最终的差分预测值再还原差分得到最终结果。# 假设 arima_pred 是 ARIMA 模型的预测结果差分序列 # cnn_lstm_pred 是 CNN-LSTM 模型的预测结果差分序列 # 二者长度一致 final_pred_diff arima_pred cnn_lstm_pred # 还原差分差分逆变换 def inverse_diff(original_series, diff_pred, steps): last_value original_series.iloc[-1] restored [] current last_value for v in diff_pred: current current v restored.append(current) return np.array(restored) final_predictions inverse_diff(train_series, final_pred_diff, len(test_series))这段代码是融合的骨架实际使用时需要注意 ARIMA 的预测步长、CNN-LSTM 的预测窗口长度必须一致否则没法直接相加。另外差分还原时所用的最后一个原始值应该是训练集的最后一位而不是测试集的第一个值这一步容易搞混。5. 训练流程与模型评估光会搭结构还远远不够5.1 数据划分与归一化验证集是用来调参的测试集是用来交成绩的一般情况下我会把数据集按 6:2:2 划分训练集用于拟合模型参数验证集用于选择超参数测试集只用来最终评估。测试集在模型调优阶段是不能碰的否则你评估出的指标就失去了意义这相当于考试前先看了答案。CNN-LSTM 这部分在设计上对数据的绝对值范围敏感激活函数在输入过大或过小的数值下容易饱和所以必须做归一化。常用的两种方式MinMaxScaler把数据压缩到 0 到 1 之间适合数据没有极端异常值、且分布相对稳定的情况。StandardScaler把数据变成均值 0、方差 1 的标准正态分布适合数据存在长尾或波动较大的情况。我自己的习惯是如果后续要还原预测值做业务解释用 MinMaxScaler 更直观还原很简单如果模型训练中出现梯度异常换成 StandardScaler 往往就能解决。归一化的关键点是一定要用训练集的统计量去转换验证集和测试集不能把训练集和测试集混在一起计算均值、方差否则会造成数据泄漏模型在测试集上的表现会虚高。5.2 训练策略batch_size、epochs 与早停机制训练神经网络模型最典型的毛病就是欠拟合和过拟合来回横跳。我的做法是设一个较大的 epochs 上限比如 100然后配合 EarlyStopping 回调监控验证集损失当连续 10 个 epoch 验证损失没有改善时就自动停止训练并恢复到验证损失最低时的权重。这一步非常关键能省掉很多手动调节的功夫。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-6 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop, reduce_lr], verbose1 )ReduceLROnPlateau 是另一个实用工具当验证损失陷入平台期时它会把学习率自动减半帮助模型跳出局部最优。batch_size 的选择需要兼顾内存和稳定性32 是一个通用的起点如果数据波动大可以降到 16训练会慢一些但会更稳定。5.3 评估指标的组合使用别只盯着一个数值看单一指标很容易被数据分布欺骗。均方根误差 RMSE 对大误差很敏感如果预测在某些时间点上偏得离谱RMSE 会一下变得很大MAE 则对大误差的惩罚相对温和更能反映整体水平的偏差MAPE 是相对误差适合业务方理解预测精度但遇到接近零的真实值时会爆炸。指标公式适用场景注意事项RMSEsqrt(mean((y_true - y_pred)^2))关注大误差场景对异常点敏感MAEmean(abs(y_true - y_pred))整体偏差评估忽略误差分布差异MAPEmean(abs((y_true - y_pred)/y_true)) * 100%业务汇报常用真实值接近0时失效我建议三个指标一起打印既能看整体水平又能观察是否存在个别异常大的预测偏差。用下面的代码可以一次性计算清楚from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_model(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 r2 1 - np.sum((y_true - y_pred)**2) / np.sum((y_true - np.mean(y_true))**2) return { RMSE: rmse, MAE: mae, MAPE (%): mape, R2: r2 } metrics evaluate_model(test_series.values, final_predictions) for name, value in metrics.items(): print(f{name}: {value:.4f})6. 踩坑实录与问题排查实操中反复遇见的九个典型问题6.1 ARIMA 部分的两个高频问题问题一模型报错提示non-stationary data。这说明你输入的序列没有做平稳化处理或者差分阶数 d 选择不对。解决办法是重新做 ADF 检验必要时增加差分阶数。如果差分后仍然不平稳检查是否数据里存在结构性突变比如某一天突然换了统计口径。问题二ARIMA 预测结果是一条直线或者近似直线。这通常发生在差分阶数 d 设置过高时序列原有的变化信息被过度差分化了。另一个可能是预测步长太大超出了模型的有效记忆范围。我的经验是ARIMA 的预测步长最好不要超过训练集长度的三分之一超出这个范围后预测值会逐渐趋于序列均值这是 ARIMA 模型本身的数学性质决定的。6.2 CNN-LSTM 部分的高频问题问题一LSTM 训练损失不下降一直卡在某个值上。先检查数据是否做了归一化再看学习率是否设置过大或过小。还有一个经常被忽视的原因即输入张量的形状不对例如 LSTM 拿到的 input_shape 三维信息有误模型的结构虽然能建立但学到的规律完全是乱的。问题二训练集损失很低验证集损失很高。这就是典型的过拟合。优先增加 dropout 比例降低 LSTM 单元的个数再看看训练数据量是否太少。对于小样本数据可以考虑在滑动窗口生成样本时加入步长扩大样本量但要注意别引入太多重叠样本造成数据的有效多样性不足。问题三预测结果整体比真实值滞后一个周期。这本质上是因为模型学到的只是一个复制前一期的模式对冲数据的突变规律捕捉不到。建议在特征工程中加入时间戳特征比如星期几、是否节假日、月份等让模型有更多依据去理解周期性突变。问题四验证集和测试集指标差异特别大。先检查是否在数据划分时混入了未来信息比如归一化时用了全局统计量或者滑动窗口生成样本时把测试集的数据也卷进了训练集。这类数据泄漏问题在时序模型里特别容易发生因为样本之间存在时间关联必须严格按时间顺序切分。6.3 融合阶段容易忽略的坑ARIMA 和 CNN-LSTM 的预测结果在融合后误差反而变大了。这种情况一般出在最简单的加法融合上说明两者的预测误差不是互补关系而是同向关系叠加后误差被放大了。解决办法是给两者的预测结果分别加上权重用验证集跑一遍搜索最优权重让误差小的模型多贡献一点from scipy.optimize import minimize_scalar def weighted_combination(w): combined w * arima_pred (1 - w) * cnn_lstm_pred return mean_squared_error(y_val, combined) optimal_w minimize_scalar(weighted_combination, bounds(0, 1), methodbounded) print(fOptimal weight for ARIMA: {optimal_w.x:.4f}) final_pred optimal_w.x * arima_pred (1 - optimal_w.x) * cnn_lstm_pred权重搜索的思路很简单核心目的就是让融合系数的选择依据实际表现说话而不是拍脑袋定一个 0.5 的固定数。7. 模型扩展方向这套组合还能怎么接着玩7.1 引入 XGBoost 作为集成学习的补充如果你已经跑通了 ARIMA-CNN-LSTM 这套结构一个非常自然的扩展是把 XGBoost 加入进来作为第三个基础模型参与融合。XGBoost 在表格型数据上的拟合能力很强尤其擅长捕捉特征的交叉作用。可以让 XGBoost 直接吃原始序列的统计特征均值、方差、最大值、最小值、滞后值等然后和前面三个模型的输出一起做加权融合或者训练一个简单的线性回归层来实现堆叠。思路本质上还是让不同模型各自贡献信息只不过最终融合的手段从人工固定权重升级到了用模型学出来的权重。7.2 注意力机制加持 LSTM让模型学会自己挑重点LSTM 在处理长序列时虽然比普通 RNN 好很多但信息承载能力仍然有限。在 LSTM 层之上加一层注意力机制可以让模型在解码阶段自动关注输入序列中更重要的时间点而不是把所有信息均匀对待。这一改动往往能带来比较明显的精度提升尤其当序列中存在明显的重点事件比如促销、节假日效应时注意力模块能捕捉到这些关键位置的模式。代码实现上Keras 里可以直接用 Attention 层也可以手动实现一个简单的注意力函数。7.3 多步预测策略的切换如果你需要预测未来 14 天而不是只需要预测下一天预测策略会完全不同。直接多步预测一次性输出 14 个未来值训练容易但误差会累积递归预测把预测值当作输入继续预测思路简单但长期效果不稳定序列到序列的模型也就是 Seq2Seq 架构则更适合中长期的预测任务编码器负责压缩历史信息解码器逐步生成未来值。组合框架下建议根据业务需求评估是多步直出还是递归递推不要盲目套用一个结构。根据我个人在这些项目里的体会ARIMA-CNN-LSTM 这套组合的真正价值不在于某一个模型有多强而在于把统计模型的可解释性、卷积的局部特征提取能力和循环网络的长程记忆能力放在了一个框架里各取所长。对于刚入门时间序列预测的读者建议先不要一口气把三个模型都跑通而是按照 ARIMA 单独跑通、CNN-LSTM 跑通、最后融合的顺序分步实现。每一步的中间结果都用图例画出来亲眼看到每个模型的效果差异你对这套组合的理解会比看任何教程都深入得多。
返回列表