简介:一份面向时间序列预测入门及进阶学习者的 LSTM+Transformer 混合模型实战资源,适合具备一定 Python 与深度学习基础、希望将序列建模落地到金融、气象、销量等场景的开发者。压缩包共13个文件、约1.74MB,主体包含2个CSV数据集、2个Python脚本(模型训练与数据读取预处理)、1份需求说明文档、1张预测效果对比图,以及少量IDE工程配置;结构紧凑,便于直接运行、对比结果与二次修改。已有1100人学习/下载。资源重点演示了 LSTM 捕捉序列短期依赖、Transformer 建模全局模式,并将两者融合以提升预测精度;同时覆盖缺失值处理、归一化、训练集/测试集划分等关键预处理环节,以及真实值与预测值可视化评估方法。借助需求文档中的目标与数据说明,读者能完整走通混合时间序列模型的建模流程,理解数据准备、架构设计、训练评估各环节的衔接,为后续更复杂的时序项目提供可复用的实践参考。
1. 拿到「混合模型时间序列预测实战」压缩包,先别急着跑代码
用 7-Zip 解开一个命名「混合模型时间序列预测实战」的 RAR 压缩包,目录里通常躺着训练脚本、数据文件和一些实验记录。很多人拿到手的第一反应是找主模型代码,然后盯着某个单模型的参数调一整个下午,最后测试集分数卡在一个平台上不去。问题很少出在某一个模型身上,而在于你从头到尾只让一个模型在硬扛。ARIMA 对非线性拐点反应迟钝,树模型对趋势外推乏力,深度学习模型又经常因为数据量不够而欠拟合。混合模型的价值,就是让这几类模型互为补充,再用一层融合逻辑把各自的预测拧成一个更稳的结果。这篇笔记不逐行拆某份资料包的源码,而是把混合模型最常用的组合方式、最小可跑的 Python 管道、关键参数边界和真实翻车点一次讲清楚。
2. 混合模型的组合逻辑:残差补偿、堆叠与分解-重构怎么选
2.1 单模型的天花板:先学会看残差里藏着什么
判断一个模型是否还有提升空间,我一般不看训练集分数,而是直接看残差的自相关。所谓残差,就是真实值减去模型预测值的序列。如果残差里还残留明显的周期或趋势,说明模型有一块规律没有学到,这时候硬调参不如换思路,把这块规律交给另一个模型。
用 statsmodels 拟合一个 ARIMA 后,可以这样快速检查残差:
from statsmodels.graphics.tsaplots import plot_acf import matplotlib.pyplot as plt # 假设 arima_fit 是已经拟合好的 ARIMA 模型 resid = arima_fit.resid # 看残差序列的自相关图:如果多个滞后阶显著越出置信带,说明还有模式没学完 plot_acf(resid, lags=20, alpha=0.05) plt.show()这段代码的关键是plot_acf的lags=20,对日频数据看 20 阶足够覆盖一周到两周的周期残留;alpha=0.05画出 95% 置信带,柱子明显超出带宽的位置就是残差里还带周期性相关性的位置。如果残差 ACF 在滞后 7 和滞后 14 处持续越界,说明 ARIMA 没有吃干净周季节效应,这正是残差学习要补的洞。这一步做扎实了,后面混合模型的路线选择才有依据。
2.2 三条可靠混合路线:误差补偿、堆叠融合与分解-重构
混合模型不是把两个模型的结果随便一拼,而是有清晰的三条技术路线,各自解决不同的问题。
第一条是误差补偿,也叫残差学习。步骤是先用一个主模型吃下线性趋势和季节骨架,得到残差序列,再用第二个模型去拟合残差中的非线性模式。这种组合适合主模型已经能用、但预测始终差一点点的场景。典型搭配是 ARIMA 加 XGBoost 或 LightGBM,ARIMA 负责把趋势和季节性拉平,树模型负责捕捉节假日、突变这类残差中的非线性信息。要注意的是,第二个模型如果太强,会把噪声也一并学进去,所以它的复杂度要克制。
第二条是堆叠融合(Stacking),让多个异构基模型各自训练并输出预测,再用一层元模型(通常是线性回归或 Ridge)去学习如何组合它们的预测。这条路线适合基模型差异很大的场景,比如线性模型、树模型、神经网络三者并存。核心前提是基模型之间要“吵得起来”,如果两个模型相关性太高,堆叠的提升会非常有限。
第三条是分解-重构。先对序列做 STL、EMD 或 VMD 分解,得到趋势、季节、残差等分量,对每个分量分别建模,最后叠加输出。适合强趋势加强季节的数据,比如零售销量、流量曲线。代价是分解本身的参数(周期、分解层数)很难一次定准,而且分解算法在序列两端都有边界效应,这一点我会在第五章专门展开。
三条路线的选型对照可以压缩成下面这张表:
| 路线 | 典型组合 | 适合的数据形态 | 主要风险 |
|---|---|---|---|
| 误差补偿 | ARIMA + XGBoost | 趋势明显、含少量非线性突变的序列 | 把噪声当信号学进去 |
| 堆叠融合 | 线性 + 树 + 神经网络 | 基模型差异大、样本量尚可 | 融合层过拟合 |
| 分解-重构 | STL/VMD + 各分量独立建模 | 强趋势 + 强季节 | 分解边界失真 |
2.3 高斯混合模型 GMM 的真实位置:状态识别与误差分布
热词里经常把“高斯混合模型 GMM”和“混合模型”混着提,但两者不是一回事。GMM 是一种概率聚类模型,它假设数据来自若干个高斯分布的混合;而标题里的混合模型,通常指的是多个预测模型的集成。GMM 在时间序列预测里不是拿来当主预测器的,它主要有两个可靠的位置。
第一个位置是状态识别。把历史序列按滑动窗口切成片段,每个片段计算均值、方差、斜率等统计特征,然后用 GMM 聚类出“高波动期”和“低波动期”两种状态,再对每个状态分别训练预测模型。这种做法的好处是能显式建模波动率切换,适合金融波动率或流量突增这类局部特征差异大的序列。第二个位置是误差分布建模,用 GMM 去拟合多个基模型预测误差的混合分布,从而给出比单一正态假设更合理的预测区间。
用 sklearn 对误差序列做 GMM 拟合非常简短:
from sklearn.mixture import GaussianMixture # errors 是形状为 (n_samples, 1) 的预测误差数组 gmm = GaussianMixture(n_components=2, covariance_type='full', random_state=42) gmm.fit(errors) # 用拟合好的GMM计算新的误差落在各状态的概率 state_prob = gmm.predict_proba(errors)这里n_components=2对应将误差分成两个隐状态,比如“正常误差”和“极端误差”;covariance_type='full'允许两个状态各自的方差不同,对异方差误差更友好。拟合之后,每个误差点都会得到属于各状态的后验概率,既可以用概率最大的状态做分类,也可以把概率作为特征喂给后面的融合层。但要注意,GMM 聚类的稳定性和样本量关系很大,几百个点以下拟合出的组件参数相当不稳定,不建议一上来就在短序列上使用。
3. 用 Python 搭一条可复现的混合预测管道:从滚动窗口到加权融合
3.1 数据准备与滚动窗口划分:评估标尺先立住
混合模型的第一个坑往往不是模型选错,而是数据切分不对。时间序列的验证集必须是时间上靠后的连续一段,不能随机抽样。我习惯把数据切成三段:训练段、验证段、测试段。验证段专门用来标定融合权重,测试段只在最终评估时碰一次,平时绝不回头看它。
下面这段代码生成一个带趋势、季节和噪声的示例序列,并完成三段切分:
import numpy as np import pandas as pd from sklearn.metrics import mean_squared_error np.random.seed(42) t = np.arange(0, 365 * 3) season = 10 * np.sin(2 * np.pi * t / 365) # 年度周期 trend = 0.02 * t # 线性趋势 noise = np.random.normal(0, 1.5, size=t.shape[0]) y = 50 + trend + season + noise df = pd.DataFrame({ 'ds': pd.date_range('2021-01-01', periods=len(t), freq='D'), 'y': y }) train_len, val_len, test_len = 530, 30, 30 train = df.iloc[:train_len] val = df.iloc[train_len:train_len + val_len] test = df.iloc[train_len + val_len:train_len + val_len + test_len]切分逻辑上,train_len=530覆盖了一年半的日频数据,能容纳完整的年度季节周期;val_len=30和test_len=30是一步到未来一个月的典型设置。需要特别注意的是,验证段和测试段必须紧贴训练段之后,中间不能留空档,否则模型外推的时间跨度变长,验证结果会被低估。如果你的业务预测步长是一个月,建议验证段和测试段都设置成和预测步长一致的长度,这样融合权重标定的场景和实际推理场景才对齐。
3.2 基模型训练:ARIMA 管线性骨架,XGBoost 管非线性残差
混合模型的基模型选择,常见做法是让线性模型和树模型组队。ARIMA 负责把趋势、季节这类线性结构吃掉,XGBoost 用滞后特征去捕捉残差里的非线性依赖。我一般不会在第一步就用复杂的神经网络,因为样本量不够时,神经网络在时序外推上的表现往往不如树模型稳定。
下面是两个基模型的训练代码:
from statsmodels.tsa.arima.model import ARIMA import xgboost as xgb # 基模型1:ARIMA,在训练段上拟合,外推 val+test 共60步 arima_model = ARIMA(train['y'], order=(1, 1, 1)) arima_fit = arima_model.fit() arima_pred = arima_fit.forecast(steps=val_len + test_len) # 为XGBoost构造滞后特征:用最近 n_lags 天的值预测下一天 def build_lag_features(series, n_lags): X, y = [], [] for i in range(n_lags, len(series)): X.append(series[i - n_lags:i]) y.append(series[i]) return np.array(X), np.array(y) n_lags = 7 xgb_model = xgb.XGBRegressor( n_estimators=300, max_depth=4, learning_rate=0.05, subsample=0.8, random_state=42 ) X_train, y_train = build_lag_features(train['y'].values, n_lags) xgb_model.fit(X_train, y_train) # XGBoost 递归外推:把上一步预测值滚动进历史窗口 history = list(train['y'].values[-n_lags:]) xgb_pred = [] for i in range(val_len + test_len): next_pred = xgb_model.predict(np.array([history]))[0] xgb_pred.append(next_pred) history = history[1:] + [next_pred] xgb_pred = np.array(xgb_pred)逻辑说明:ARIMA 的forecast(steps=60)一次外推 60 步,不做滚动重拟合,这样演示逻辑最清晰;严格场景下应该每预测一步就推进训练集重新拟合,但那会让示例代码变复杂,后面的滚动回测章节会补上这种做法。XGBoost 的部分,build_lag_features用最近 7 天的值作为特征,n_estimators=300和max_depth=4是控制模型容量不要过大的保守设置,因为树模型对滞后特征的拟合能力很强,太深容易把噪声也背下来。预测阶段用递归方式把上一步的预测值推进历史窗口,这是时序外推的标准姿势,避免了测试期数据泄露。
这两个基模型的角色分工是:ARIMA 对趋势的延续能力强,但对最近几天的突变反应很慢;XGBoost 对近期模式的拟合能力强,但外推超过特征窗口后预测会快速回落向均值。正好互补。
3.3 融合层:简单平均、误差反比加权与网格搜索
基模型预测出来了,融合层才是混合模型的灵魂。我见过不少项目直接在测试集上试权重,这等于把验证信息提前透支了。正确做法是只在验证段上标定权重,然后把权重固定住去预测测试段。
下面这段代码用网格搜索在验证段上找最优线性权重:
val_true = val['y'].values arima_val_pred = arima_pred[:val_len] xgb_val_pred = xgb_pred[:val_len] # 在验证段上网格搜索权重 w,最小化加权预测的 RMSE best_w, best_rmse = 0.5, np.inf for w in np.arange(0, 1.0001, 0.05): combined = w * arima_val_pred + (1 - w) * xgb_val_pred rmse = mean_squared_error(val_true, combined, squared=False) if rmse < best_rmse: best_rmse, best_w = rmse, w # 用标定好的权重组合测试段预测,并计算最终RMSE arima_test_pred = arima_pred[val_len:] xgb_test_pred = xgb_pred[val_len:] final_pred = best_w * arima_test_pred + (1 - best_w) * xgb_test_pred test_rmse = mean_squared_error(test['y'].values, final_pred, squared=False) print(f"最优权重: ARIMA={best_w:.2f}, XGBoost={1-best_w:.2f}") print(f"验证RMSE={best_rmse:.4f}, 测试RMSE={test_rmse:.4f}")网格搜索的步长0.05意味着权重在 0 到 1 之间只有 21 个候选值,足够细也不会过拟合。这里有一个容易被忽略的细节:验证段权重寻优出的 RMSE 不能拿来当最终成绩,因为权重是在这段数据上调出来的,它必然偏好验证段,所以最终只认测试段 RMSE。如果验证段上最优权重明显偏向了某个基模型(比如 ARIMA 权重接近 0.9),说明另一个基模型在当前场景下贡献很小,这时候不应该硬加融合,而应该回到特征层面去找原因。
4. 决定混合模型成败的参数边界:窗口、分解层数与融合权重
4.1 窗口长度与预测步长:先定标尺再谈混合
混合模型的参数不是一个个独立调的,它们之间存在先后关系。第一步永远是定预测步长和训练窗口。
预测步长由业务决定,比如要提前 30 天做库存计划,horizon 就是 30。训练窗口的长度至少覆盖两个完整的季节周期,如果你面对的是日频带年度季节的数据,train_len低于 365 基本没有讨论意义,模型连一个完整周期都没见过,趋势和季节根本分不开。验证段的长度建议和预测步长相等,这样融合权重标定的时间跨度和实际推理一致。
| 参数 | 常见起始值 | 判断依据 |
|---|---|---|
| 预测步长 horizon | 7 / 30 | 业务决策周期 |
| 训练窗口 train_len | 覆盖 2 个完整季节周期 | 看 ACF 的季节峰位置 |
| 滞后阶数 n_lags | 7(日频) | 自相关图显著滞后范围内 |
| 验证段长度 | 等于 horizon | 和实际推理场景对齐 |
滞后阶数n_lags的选择我会看训练序列的偏自相关图:如果偏自相关在滞后 7 处仍有显著峰值,说明过去一周的信息对今天有直接解释力,n_lags=7就是合理的。盲目加大到 30 会让 XGBoost 的特征维度上升,但特征里大量是冗余信息,模型反而更容易过拟合。
4.2 分解层数与残差阈值:STL 与 VMD 怎么设才不把噪声当信号
分解-重构路线里,最难定的是分解参数。STL 相对温和,核心参数只有周期和稳健性设置:
from statsmodels.tsa.seasonal import STL # 日频数据,周期设为365(年度),robust=True 抵抗异常值影响 stl = STL(train['y'], period=365, robust=True).fit() # 分解出趋势、季节、残差三个分量 trend_comp = stl.trend seasonal_comp = stl.seasonal resid_comp = stl.resid # 残差分量的标准差可以作为“信号阈值”参考 resid_std = resid_comp.std()period=365对应日频数据的年度季节,如果你的数据是周频,这里就要改成 52。robust=True会让 LOESS 平滑对异常值更迟钝,适合带突刺的业务数据。分解之后的策略通常是:趋势分量用线性回归外推,季节分量复用去年的同期形态,残差分量交给 XGBoost 去拟合。resid_std在这里的用途是判断哪些残差值得学:如果残差标准差和原始序列标准差相比低于 5%,说明主成分已经把信息吃得差不多了,这时候强行对残差建模大概率是学噪声。
VMD 的分解层数 K 值就麻烦一些。K 太小,趋势和季节混在一个模态里,分不干净;K 太大,会出现模态混叠,相邻分量的中心频率重叠。常见做法是从 K=3 开始试,画出各分量的中心频率,频率分离度明显下降的那一档就是上限。这个判断有点玄学成分,但实操里比单纯看重构误差靠谱,因为重构误差总是越小越好,很容易骗你选一个过大的 K。
4.3 融合权重怎么定:固定、寻优与动态三条路
融合权重有三种标定方式,我不建议一上来就网格搜索。样本量小、基模型差异不明显的时候,直接固定 0.5/0.5 反而更稳,因为寻优对验证段噪声极其敏感,权重会被一段偶然的波动带跑偏。
第二种是验证段网格搜索,上一章的 3.3 已经给出了完整实现。它的适用前提是验证段长度不少于预测步长,且验证段的分布和测试段接近。如果验证段里恰好有一段异常波动,搜出来的权重会过度迎合这段时间,投入测试段立刻失效。
第三种是动态权重,按最近 N 步的预测误差反比加权,适合概念漂移明显的业务场景。实现很轻:
def error_inverse_weights(recent_errors, alpha=1.0): # recent_errors: 各基模型最近N步的绝对误差 inv = 1.0 / (recent_errors + 1e-8) weights = inv ** alpha return weights / weights.sum()alpha控制灵敏度:alpha=1.0时权重与误差倒数成线性反比,alpha越大,权重越倾向于把宝压在最近表现最好的模型上;alpha=0时退化为简单平均。动态权重的代价是每次预测前都要回看一段误差窗口,在推理链路里多一步状态维护。我一般只在误差波动明显的场景启用动态权重,平稳场景用固定权重或验证段寻优就够了。
5. 混合模型时间序列预测避坑清单:五个亲测翻车的场景
5.1 数据泄露:归一化算子提前看到了未来
现象:验证集 RMSE 好得出奇,测试集表现却大幅退步,两个分数严重不对齐。
原因:在切分数据之前就对全序列做了MinMaxScaler().fit(df[['y']]),scaler 的 min/max 是整段数据算出来的,等于把未来信息告诉了训练过程。时间序列的归一化必须在切分之后做,而且只 fit 训练段。
解决:
# 错误示范:fit 用的是整段 df scaler = MinMaxScaler() scaler.fit(df[['y']]) scaled_all = scaler.transform(df[['y']]) # 正确做法:只 fit 训练段,验证/测试段沿用同一参数 scaler = MinMaxScaler() scaler.fit(train[['y']]) train_scaled = scaler.transform(train[['y']]) val_scaled = scaler.transform(val[['y']]) test_scaled = scaler.transform(test[['y']])顺带提醒,差分操作同样有这个问题。如果先对全序列做差分再切分,前几个差分值会依赖未来数据,必须按段做差分。
5.2 随机 K 折交叉验证:时序数据里的秩序不能打乱
现象:用KFold跑出来的交叉验证分数虚高,把模型换到真正的未来数据上,效果明显缩水。
原因:时间序列天然有自相关,随机打乱后,验证集里会出现训练集某个样本的“邻居”,模型相当于提前见过了答案。时序数据里,相邻观测高度相似,随机划分本质上是在作弊。
解决:改用TimeSeriesSplit,它保证每一折的训练集都严格在验证集之前:
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_index, test_index in tscv.split(X): # train_index 永远全部小于 test_index 中的最小下标 pass5.3 概念漂移:验证段上找的权重到了测试期失效
现象:融合权重在验证段上效果显著,投入测试段后预测误差反弹,甚至还不如单一模型。
原因:业务规律在变。验证段是过去一段时间的规律,测试段是未来,两者分布有偏移时,权重寻优只是在拟合过去,对未来没有任何保证。这不是代码 bug,是时序预测的固有矛盾。
解决:控制权重寻优的历史窗口不要过长,比如只用最近 30 天的数据做标定;或者改用动态误差倒数权重,让权重随最近表现自动调整。核心认知是:没有任何权重选择方式能保证未来最优,只能通过对最近规律的跟踪来贴近未来。
5.4 分解边界效应:STL/VMD 两端失真,预测偏偏从这儿开始
现象:用 STL 分解后,序列最末尾一段分量曲线明显漂移,和原始序列对不上。而混合模型的预测恰好就是从末尾开始外推的。
原因:STL 的 LOESS 平滑在序列边界缺少足够邻居,平滑结果在两端不可靠;VMD 的迭代求解在输入信号的起始和末尾段收敛慢,同样会产生边界振荡。
解决:分解前把序列两端各自延展一段,比如用最近一段周期的镜像波形向外补 20 到 30 个点,分解完成后裁剪掉延展部分;另一种做法是先用全序列分解,但训练和验证只用中间“稳定段”的数据,避免两端失真区域污染模型。我一般优先用延展法,因为裁剪法会浪费掉最新的真实信息,而最新信息往往是预测最需要的东西。
5.5 融合层过拟合:权重成了跟屁虫
现象:混合模型训练集 RMSE 显著下降,测试集 RMSE 却不降反升,简单平均反而更稳。
原因:融合层在验证段上寻优时,验证段太短或噪声太大,权重被一段偶然波动带偏,变成了过度拟合验证段的“跟屁虫”。模型集成理论上不会比最差基模型更差,但前提是融合方式足够简单、融合层容量不足以记住噪声。
解决:限制融合层复杂度,使用线性权重而不要用复杂元模型;网格搜索步长不要太细(0.05 起步);验证段长度至少等于一个完整预测周期;如果条件允许,用第 6 章的滚动回测替代单次验证段寻优,用多段平均削弱噪声影响。
6. 验证没有白做:滚动回测与基准对比让混合模型现原形
6.1 滚动回测:多个起点多次前向验证
固定一次的训练/验证/测试切分只能说明一个时间窗口上的表现,很容易被某一段偶然波动带偏判断。滚动回测的核心做法是固定预测步长,让训练终点不断前进,在每个起点上各自做一次完整的“拟合并预测”,然后汇总所有起点的误差。
def rolling_backtest(df, horizon=30, n_splits=6, first_train=300): total = len(df) rmses = [] for k in range(n_splits): train_end = first_train + k * horizon if train_end + horizon > total: break train_part = df.iloc[:train_end] test_part = df.iloc[train_end:train_end + horizon] # 这里复用前面的 ARIMA 与 XGBoost 训练逻辑 # 注意:每轮都要重新 fit 两个基模型,再在验证段上重新标定权重 # 省略具体拟合代码,仅记录最终预测误差 pred = run_hybrid_pipeline(train_part, horizon) rmse = mean_squared_error(test_part['y'].values, pred, squared=False) rmses.append(rmse) return np.mean(rmses), np.std(rmses) mean_rmse, std_rmse = rolling_backtest(df) print(f"滚动回测平均RMSE={mean_rmse:.4f},标准差={std_rmse:.4f}")这里的run_hybrid_pipeline就是把第 3 章从基模型训练到权重标定的一套流程封装起来,每轮都重新拟合。平均 RMSE 衡量整体水平,标准差衡量稳定性——标准差太大,说明模型在某些时间段的预测极不稳定,比平均误差高一点更值得警惕。
6.2 和基准对比:混合模型必须赢过“单一最好的模型”
混合模型不是做得越多越好的摆设,它的存在必须有一个金标准:能不能稳定跑赢当时表现最好的单模型。我习惯同时跑三个基准线:单 ARIMA、单 XGBoost、简单平均。混合模型的滚动回测平均 RMSE 如果只能和简单平均打平,那说明两个基模型的互补性不够,加融合层只是徒增复杂度。
诊断口径是看每个滚动起点上,混合模型赢了多少次、输了多少次。如果只在某几个特定时段赢,大多数时候和单模型差不多甚至更差,我会重新审视基模型的选择,而不是继续调融合权重。这些判断在样本量允许时可以用 Diebold-Mariano 检验做显著性确认,日常项目里直接看多次滚动起点上的胜负分布就够用了。
我自己做混合模型早期吃过不少亏,最深的教训是:混合模型永远要在评估上保持“保守”,融合权重只能靠验证段标定一次,测试段结果无论如何都不能回头去改参数。后来我把滚动回测固化成项目的标准动作,所有模型改动都先过一遍多起点验证,才真正避免了拿单次切分结果自嗨。希望帮到你。
本文还有配套的精品资源,点击获取