ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

混合模型时间序列预测:SARIMA基线、GMM状态识别与LSTM残差修正

混合模型时间序列预测:SARIMA基线、GMM状态识别与LSTM残差修正

简介:一套面向时间序列预测学习者的实战教程,聚焦 LSTM 与 Transformer 混合模型在复杂非线性序列数据上的应用。资源共 13 个文件,包括 Python 脚本、CSV 数据集、PNG 可视化图、需求说明文档及工程配置等,整体仅 1.74MB,轻量便于下载。压缩包内提供数据读取与预处理脚本,覆盖清洗、归一化等必要步骤;混合模型脚本将 LSTM 的序列记忆能力与 Transformer 的全局注意力机制结合,可同时捕捉局部依赖与长期模式。配套的真实值-预测值对比图能直观评估模型效果,需求文档则明确项目目标与数据规格,两者相互印证,降低了理解门槛。已有 1100 人学习,资源虽小却覆盖了从数据准备、模型构建到结果评估的完整实践链路,适合希望快速上手深度时间序列建模的读者。

1. 混合模型时间序列预测实战:先把“混合”拆成“谁能干、谁不能干”

当你拿 LSTM 去预测一段带工作日/休息日切换的服务器负载,大概率会在状态切换那几天“翻车”;拿 ARIMA 去预测带趋势拐点的电量,又总是滞后一步。混合模型时间序列预测就是把这两类模型拼成一条流水线:统计模型负责线性基线和季节性,神经网络负责残差和突变,再让高斯混合模型 GMM 在中间充当“状态开关”。这套方案特别适合已经在单模型上踩过坑、又想做多步预测的工程师。标题里这套“混合模型时间序列预测实战”资料,核心也就是把三个模块接起来。下面我按自己落地的顺序,把它拆成可直接复用的步骤,并标出哪些参数会卡住新手。

2. 混合为什么比单模型稳:GMM 抓状态、ARIMA 抓基线、LSTM 抓残差

2.1 单模型在非平稳序列上为什么容易翻车

大多数时间序列预测任务里,真正难的不是平均值,而是两个地方:非平稳漂移和运行模式切换。ARIMA 这类统计模型依赖差分和平稳性假设,遇到趋势拐点会有一个“观察—确认—修正”的滞后;LSTM 虽然能拟合非线性,却需要大量覆盖各种状态的样本,否则会把状态突变当成噪声抹平。只拿一个模型去干所有事,总有一类场景是它天生不擅长的。

混合模型时间序列预测的基本逻辑,就是把“总体趋势、周期性、状态切换”拆开,分给不同模型。常见做法是先让 SARIMA 做基线预测,它擅长捕捉线性趋势和强周期;再把真实值减去基线得到残差,用 LSTM 去拟合残差。如果残差里还有状态相关的规律,LSTM 仍然能通过学习修正量把它补回来。

我之所以先把这层概念讲清楚,是因为很多新手把“混合”理解成“所有模型跑一遍再取平均”。平均只能把误差抹平,不能纠正结构性的偏差。残差修正是更可靠的落地方案:你的基线模型还是那个熟悉的 ARIMA,换掉的只是“最后一段预测结果”。这样既能保留原有系统的回退能力,出了问题也更容易定位。

2.2 高斯混合模型 GMM 在这套流水线里的位置

高斯混合模型 GMM 在这里解决的是“状态怎么表达”的问题。我们可以把每个时间点用一个小窗口内的均值、波动率、变化速度来描述,然后用 GMM 把这些特征聚成几个状态。和 KMeans 最大的区别是,GMM 不给出“这个时间点属于哪一类”的硬标签,而是给出属于每个状态的概率。时间序列的状态边界往往是模糊的,比如工作模式的午后和休息模式的下午并没有一条清晰的线,概率形式的表达更适合神经网络的输入。

聚类完成后,每个时间点会得到一个向量,例如[0.7, 0.2, 0.1],表示当前更接近状态 A、B 还是 C。这个向量可以和残差序列拼在一起,作为 LSTM 的额外输入。模型在看到残差序列的同时也知道“现在大概在什么状态”,因此当状态切换时,LSTM 有依据提前改方向,而不是等数值已经明显偏离之后才反应。

2.3 为什么选“SARIMA + GMM + LSTM”而不是其他组合

如果只是想修正残差,很多模型都能做,不一定非用 LSTM。但在状态切换明显的序列里,GMM 概率给了 LSTM 一种“状态感知”能力,这是普通回归模型或 XGBoost 不太容易直接利用的时序特征。反过来,如果只用 GMM 聚类并针对每个状态单独建立模型,又会遇到训练样本被切碎、边界样本难以归属的问题。把它们串成一条从特征到概率再到预测的流水线,是稳定性和复杂度之间比较平衡的方案。

实际做多步预测时,我的习惯是把第一步预测出来的残差当作下一步已知值,让窗口继续滚动。这种迭代预测肯定会有误差累积,但状态概率仍然能提供整体方向的约束,比每一步都从零开始拍脑袋稳定得多。后面我们写的 Python 代码,就是基于这条路由展开的。

3. 用 Python 串起 GMM + LSTM 的混合预测流水线

在这一章里,我按“数据准备 → GMM 聚类 → 基线模型 → LSTM 残差修正 → 合并评估”的顺序给你一套可直接照抄的流程。假设你已经解压了标题里的“混合模型时间序列预测实战”材料;如果没有,一个至少包含ds和y两列 CSV 就行。

3.1 数据准备与 GMM 状态聚类

import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.mixture import GaussianMixture # 读入两列:ds 是日期,y 是预测目标 df = pd.read_csv("timeseries.csv", parse_dates=["ds"]).sort_values("ds").reset_index(drop=True) series = df["y"].to_numpy() def rolling_features(series, window=12): # 用滚动窗口统计量描述“局部状态” s = pd.Series(series) feat = pd.DataFrame({ "mean": s.rolling(window).mean(), "std": s.rolling(window).std(), "delta": s.diff().rolling(window).mean(), }) return feat feat = rolling_features(series, window=12).dropna() feat_index = feat.index.to_numpy() # 标准化后做 GMM 聚类 X_feat = StandardScaler().fit_transform(feat) gmm = GaussianMixture(n_components=3, covariance_type="full", random_state=42, max_iter=200) gmm.fit(X_feat) state_prob = gmm.predict_proba(X_feat)

这段代码做的事情是:先把原始序列转成滚动窗口特征。窗口里的mean描述局部水平,std描述波动,delta描述趋势变化速度。用这些特征做聚类,比直接对原始数值聚类更稳定,因为时间序列的“状态”本质上是走势形态,而不是某一瞬间的绝对数值。

参数上关键的是window=12和n_components=3。窗口太短,状态标签会在两种模式之间频繁跳动;窗口太长,状态切换会被圆滑掉,GMM 感知不到突变。小时粒度的数据我一般从 12 起步,日粒度的数据会加到 7 或 14。GMM 的状态数先不要贪多,2 到 4 个起步,后面会用 BIC/AIC 去对比。covariance_type="full"允许每个状态有自己的协方差结构,适合方差差异明显的序列;如果聚类结果反复跳,改成"diag"会更稳定。

3.2 拟合 SARIMA 基线并抽取残差

from statsmodels.tsa.statespace.sarimax import SARIMAX # 基线模型:order 是非季节成分,seasonal_order 处理周期 base = SARIMAX(series, order=(1, 1, 1), seasonal_order=(0, 1, 1, 24), enforce_stationarity=False, enforce_invertibility=False) base_res = base.fit(disp=False) baseline = base_res.predict(start=0, end=len(series)-1).to_numpy() residual = series - baseline

基线模型我通常不会调得太复杂。order=(1, 1, 1)已经能应付大多数有漂移的序列;seasonal_order里的24是小时周期。如果你的数据是日粒度,可以改成(0, 1, 1, 7),代表一周七天周期。这里的思路是“基线只需要及格”,真正修正残差的活儿留给 LSTM。如果 SARIMA 拟合报错,常见解法是去掉seasonal_order,或者把order降到(0, 1, 0)退化成随机游走。

抽取残差这一步是整个混合模型的基石。残差 = 真实值 − 基线预测值。如果残差接近白噪声,说明基线已经把序列解释得差不多了,LSTM 就没有可学的东西;如果残差里还有明显波峰或者是周期性的,那这部分信息就是 LSTM 的“学习材料”。

3.3 构造 LSTM 的滑窗样本并训练

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.callbacks import EarlyStopping # 让残差和状态概率对齐到同一组时间点 resid_aligned = residual[feat_index] # 滑窗样本:用过去 steps 时刻的残差和状态概率,预测下一步残差 feats_for_lstm = np.column_stack([resid_aligned, state_prob]) def make_samples(feats, target, steps=12): X, y = [], [] for i in range(steps, len(feats)): X.append(feats[i-steps:i]) # 过去 steps 步的全部特征 y.append(target[i]) # 预测当前残差 return np.array(X), np.array(y) X_lstm, y_lstm = make_samples(feats_for_lstm, resid_aligned, steps=12) # 严格按时间顺序切分 split = int(len(X_lstm) * 0.8) X_train, X_val = X_lstm[:split], X_lstm[split:] y_train, y_val = y_lstm[:split], y_lstm[split:] model = Sequential([ LSTM(32, input_shape=(X_train.shape[1], X_train.shape[2]), return_sequences=False), Dense(1) ]) model.compile(optimizer="adam", loss="mse", metrics=["mae"]) early = EarlyStopping(patience=10, restore_best_weights=True) history = model.fit(X_train, y_train, validation_data=(X_val, y_val), epochs=50, batch_size=32, callbacks=[early], verbose=0)

注意resid_aligned = residual[feat_index]这一步。因为feat在做dropna()之后,前window-1个点被丢弃了,所以用它去索引残差,才能保证 GMM 概率、残差、原始序列三个对象长度相同、位置对应。这是新手最容易漏掉的对齐细节。

make_samples里,feats[i-steps:i]是形状为(steps, 2 + 状态数)的矩阵。第一列是残差值,后面列是 GMM 状态概率。LSTM 拿到的不只是一串残差数字,还有“当前处于什么状态”的概率提示,这就是混合模型的关键信息。

LSTM 参数里面,steps=12与前面的滚动窗口保持一致,这样模型感知的状态时长是统一的。32是隐藏单元数,数据量小时再往上涨容易过拟合;数据量大时可以试着提高到 64。EarlyStopping我建议一定加上,patience 取 10,省得在验证集上反复横跳。

3.4 合并预测结果并评估

from sklearn.metrics import mean_squared_error # 验证段的残差预测 pred_residual = model.predict(X_val).flatten() # 对应到原始时间轴上的索引 val_idx = feat_index[split:split + len(pred_residual)] val_baseline = baseline[val_idx] # 基线在验证段的预测 val_true = series[val_idx] # 真实值 val_pred = val_baseline + pred_residual # 混合模型的最终预测 print("MSE:", mean_squared_error(val_true, val_pred)) print("Residual MSE:", mean_squared_error(residual[val_idx], pred_residual)) # 顺便看一眼单模型的结果,方便对比 print("Baseline-only MSE:", mean_squared_error(val_true, val_baseline))

合并的逻辑就是一句val_baseline + pred_residual。基线负责总体水平,LSTM 负责修正残差。如果你发现Residual MSE几乎等于Baseline-only MSE,说明 LSTM 没有学到东西,混合模型实际退化成基线模型;如果Residual MSE比基线自身的误差还大,则很可能数据切分或状态对齐出了问题。

这里有一个容易被忽略的点:val_true用的是真实的未来值,但如果你要做的是多步预测,每一步的“残差预测”也要迭代式推进。也就是说,预测完第 t+1 步之后,把它当作已知值滑进窗口,再去预测 t+2 步。上面的代码演示的是单步预测的评估,落地到多步预测时,我会再写一层循环,避免一次性输入未来窗口信息造成数据泄露。

4. 避坑:混合模型时间序列预测的 5 个翻车现场和排查顺序

“1+1 大于 2”是理想情况,实战里更常见的是“1+1 小于 1”。下面几个坑是我自己在混合模型上踩过的,也基本覆盖了新人最常遇到的问题。

4.1 现象:LSTM 没学到残差,集成退化成基线

现象:混合模型跑完后,预测曲线和 SARIMA 基线几乎重合,残差预测全是一堆接近 0 的数字。

原因:残差序列没做标准化。LSTM 的激活函数对输入尺度很敏感,残差量级如果太小,网络会宁愿把输出全设成 0,原因是有 MLE 那样。另一个常见原因是残差本身接近白噪声,没有可学的信息量。

解决方式先检查残差的 ACF 图,跑一个 Ljung-Box 检验,p 值大于 0.05 时不要强行上 LSTM。如果自相关显著,就对残差做标准化,把均值方差记录清楚,训练完之后再把预测值乘回原尺度。注意一定要“还原残差尺度”,很多人忘了这一步,导致最终预测偏差很大。

4.2 现象:GMM 状态数设太大,预测跟着状态抖动

现象:预测曲线在状态切换点附近出现明显的阶跃或“撕裂”形状,平滑度比单模型还差。

原因:状态数设了 5 个甚至更多,但序列实际上只有两三种运行模式。多余的状态在随机抓取噪声,GMM 把一些波动点单独聚成一类,状态概率当然不稳。

解决方式把n_components调回 2 到 4,用 BIC 或 AIC 做对比。还有两个隐藏项:一个是random_state必须固定,否则每次聚类结果不一样,LSTM 学到的概率分布不连续;另一个是如果状态概率仍然跳,可以给概率做一步简单的滑动平均,把软概率拉平滑。

4.3 现象:SARIMA 在季节项上反复报错,或者预测曲线在末端弯过头

现象:SARIMAX训练时频繁出现Non-stationary starting autoregressive parameters之类的警告,或者预测最后几十个点快速上扬/下跌。

原因:基线模型的阶数设得太高,seasonal_order里的周期项和真实周期不匹配,模型拿高频噪声当周期去拟合。这是我见过最多的一种误用:以为阶数越高越准。

解决方式order尽量控制在(1, 1, 1),seasonal_order写一个分量就好,比如(0, 1, 1, 24)或(1, 1, 0, 24)。如果序列没有明显的季节周期,干脆不用seasonal_order,让 SARIMA 退化成 ARIMA,剩下的周期交给 LSTM 去摸。

4.4 现象:训练集损失漂亮,验证集却跑出局部大误差

现象:训练集误差很低,验证集在某一小段出现明显的离群残差,其他段倒是正常。

原因:数据切分用了随机切分或者随机抽样,把未来段混进了训练集。时间序列一旦混入未来信息,滚动窗口特征会提前“看到答案”,模型在验证段的所谓“高准确率”是作弊,一上真实环境就露馅。

解决方式按时间顺序切分,直接把split放在训练段和验证段之间。同时检查滚动特征的窗口边界:构造第 t 个样本时,只能用 t 时刻及其之前的数据,不能用 t 之后的任何统计量。这也是我在代码里特意用feat_index对齐的原因。

4.5 现象:整体 MSE 不高,但关键高峰和低谷预测普遍“变钝”

现象:均方误差看起来不错,但把预测曲线画出来会发现,峰值总是被压低,低谷总是被抬高,总体趋势变成了“平均曲线”。

原因:MSE 对误差平方取平均,常态样本占多数,会主导损失。峰值和低谷出现次数少,累计损失占比低,神经网络为了降低整体损失,选择把极端样本忽略掉。这在混合模型里非常隐蔽,因为基线 SARIMA 已经修了一部分,剩下 LSTM 修不动的高频尖峰就被“战略性放弃”。

解决方式是同时看 MAE 和 MAPE,并且分状态去看误差。比如用 GMM 已经分出来的状态,分别统计每个状态下的预测误差;如果高峰状态误差明显大,可以考虑在损失函数里按状态频率给样本加权,或者把学习率降到 1e-4 左右,让网络有更多机会拟合少数样本。见过太多人只盯着 MSE,最后预测曲线温和得像条直线,这就是典型的“平均数陷阱”。

5. 把混合模型调稳:参数、回测和一条经验法则

5.1 三组参数用默认值起步,别一步到位

参数之间是相互影响的,我习惯用一组默认值跑通,再逐步调整。

参数我的起步值调整方向
GMMn_components2 或 3超过 5 基本在拟合噪声,状态越多切换越抖
滚动窗口window12(小时粒度)慢变化序列取 24,快变化序列取 6
LSTM 时间步steps12多步预测时尽量与预测步长一致,过大会稀释样本

刚开始就追求复杂参数组合,只会让排查变难。先把n_components固定成 2,跑通一轮完整流程,观察状态概率是不是真的和肉眼看到的状态切换对得上;对不上,再调窗口长度。

5.2 用滚动回测确认混合模型真的更好

验证混合模型是否值得保留,我的习惯是做滚动回溯测试。固定一个训练长度,比如 80% 数据,训练完成后验证 10% 数据;然后把训练段整体往后推 10%,重新拟合 GMM、SARIMA 和 LSTM,再到下一段验证。这样得到的误差分布,才更接近真实线上表现的分布。

评估指标除了均方误差,还要记录平均绝对误差和方向准确率。对运维场景来说,最高点偏低比整体误差高几毛钱伤害更大;对交易场景而言,方向比精度更值钱。混合模型如果只在一个指标上领先,我一般不会上线,至少两个指标明显优于单模型才保留。

5.3 一条经验法则:先看残差是否可学,再谈混合

混合模型的起点不是模型,而是残差里到底还剩多少信号。如果基线模型的残差接近白噪声,LSTM 强行拟合只会把噪声背在身上。判断方法很简单:画残差的 ACF/PACF 图,或者跑一次 Ljung-Box 检验,p 值大于 0.05 就直接把网络砍掉,用基线即可。这是我踩得最深的一个坑,一开始总觉得模型越深越强,后来把每条残差序列都先做检验,省下了大量无用训练时间。

混模型时间序列预测这件事,难点不在模型本身,而在判断“哪个部分该信哪个模型”。我不会告诉你 GMM 一定能把状态分得完美,也不保证 LSTM 每次都能学到残差规律,但先拆成基线和残差、先看残差里有没有货、再用状态概率辅助神经网络,这条路径能帮你少走很多弯路。希望这套思路对你手上的预测任务有用。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进