简介:混合模型时间序列预测实战资源,面向具备一定深度学习基础、希望提升时序建模能力的开发者。压缩包内基于LSTM与Transformer构建混合预测模型,兼顾序列局部依赖与全局模式,配套data.csv、ceshi.csv训练测试数据,以及数据读取测试.py、lstm+transformer.py等核心脚本,并给出真实值预测值画图.png以直观评估效果。需求文档.txt明确数据结构和预期输出,便于复现与调优。资源共13个文件,涵盖python脚本、csv数据、png图表、txt说明及项目配置等,整体仅1.74MB,轻量易用。已有1100人下载学习,适合希望快速上手混合时序预测、对照完整流程进行实战的读者。
1. 混合模型时间序列预测:别急着调参,先搞清“混合”的到底是什么
做过一段时间序列预测的人应该都有这种经历:单独把 LSTM 训到验证集 loss 不再下降,换 XGBoost 又总觉得它学不到序列感,两边都试过之后,预测曲线还是在高点滞后、在拐点钝化。这时候你会自然想到一个词:混合模型。它不是一个新算法,而是把几种模型按某种结构组合起来分工——线性模型抓趋势,树模型抓特征交叉,神经网络抓序列依赖,最后用验证集误差决定谁的话语权更大。这个思路在真实业务里很常见,适合你已经有一个能跑的基线模型、但精度卡在某个瓶颈上不去的情况。这篇就围绕“混合模型时间序列预测”讲清楚组合逻辑、最小可复现实验、参数起步值和最容易被忽略的几个坑。
2. 混合模型的组合逻辑:残差叠加与加权融合是两条主路
2.1 先建基线:为什么单模型总会留一块“学不动”的结构
在把两个模型拼起来之前,最好先各自跑一遍。常见的做法是先用一个线性模型——ARIMA 也好,Prophet 也好——把趋势和季节性吃掉,然后看残差曲线。你会发现大多数真实业务数据的残差并不白噪,还有明显的自相关:比如连续几个点都偏高,或者碰到促销/节假日就系统性偏离。
这个剩余部分是线性模型学不动的,但往往是 LSTM 这类序列模型擅长捕捉的短期动态。于是第一阶段就这么定下来:线性模型负责“大趋势 + 周期”,LSTM 负责在残差上“小步快跑”。这就是残差叠加思路的基本单元,也是混合模型最常见、最容易落地的一种切入方式。
另一种常见情况是树模型路线。把时间序列转成回归问题——滞后项、滚动均值、星期几、是否节假日——然后丢给 XGBoost 或 LightGBM。树模型在这类特征交叉上有天然优势,但对窗口内部的先后顺序不敏感:它只看特征值,不看“前天→昨天→今天”的路径。而 LSTM 恰恰对路径敏感。这两者互补方向不同,组合起来往往比单纯的残差叠加效果更稳。
2.2 加权融合与 stacking:权重不是拍脑袋,是按误差反推的
混合模型有一个很现实的问题:LSTM 说自己的答案对,XGBoost 也说自己的答案对,最后听谁的?如果直接用 0.5/0.5 平均,通常不是最优解,因为两个模型在同一个时间点的误差分布不一样——一个可能在拐点误差大,一个在平稳段误差大。
我一般会在验证集上做一个简单加权融合。具体做法是,把两个模型的预测结果作为两个新特征,用线性回归或者简单的 scipy 最小二乘去拟合真实值,得到权重。这个方法既避免了手工拍脑袋定系数,又让权重是在“历史真实表现”上反推出来的,有据可依。
注意一点:权重必须在时间上做滚动拟合,不能用全量验证集一次性拟合。因为模型在不同时间段的相对表现会变化,长期固定一个静态权重,等于还是没做到混合,只是简单平均。
2.3 GMM 高斯混合模型在这里的用武之地:用状态切换代替一刀切
最新热词里反复出现的高斯混合模型(GMM),在这个语境下也有一条靠谱的用法。GMM 本身不是预测模型,它做的是聚类——把数据按波动状态分成若干类。时间序列里常有这种场景:工作日低波动、周末突变、大促期间方差拉大。把这些状态提前用 GMM 聚类出来,当作外生特征或切换因子,再让下游的预测模型分别学习不同状态的规律。
具体操作上,对滑动窗口内的序列提取一组统计量——均值、方差、偏度、峰度——然后丢给 GaussianMixture(n_components=3) 去聚类,得到每个样本属于哪个波动状态的标签。这个标签作为一列特征拼进 XGBoost,或者作为 LSTM 的一个额外输入通道。这样模型不用自己去隐式推断“现在是不是异常状态”,而是直接拿到一个明确条件,训练会更容易收敛。
GMM 的优势是软聚类:它给每个样本一个属于每个簇的概率,而不是非黑即白的标签。你可以把这个三列概率直接拼进特征表,保留“这个时刻既像平稳又像突变前夜”的模糊信息,对混合模型来说更友好。
2.4 选型对照:不同业务场景怎么选混合结构
| 场景特征 | 推荐主模型 | 搭配模型 | 混合方式 |
|---|---|---|---|
| 长周期趋势明显,周期规律强 | ARIMA / Prophet | LSTM 或 LightGBM | 残差叠加 |
| 特征多、业务规则复杂(促销/新品) | XGBoost / LightGBM | LSTM 做序列嵌入 | 特征拼接 + 融合 |
| 波动剧烈,状态切换频繁 | LSTM + GMM 状态标签 | XGBoost 做门控切换 | 状态分流 |
| 没有明显周期,全靠短期惯性 | LSTM | 基线均值模型 | 加权融合验证提升 |
这张表不只是给你做选型,更是在提醒你:混合模型的第一步不是写代码,而是画出你这条序列“趋势/周期/残差/突变”各占多少。画不出来时,先跑一版 EDA,再决定结构。大多数翻车都死在结构选错,而不是参数没调好。
3. 把数据拆成“能分工”的样子:特征工程与窗口划分
3.1 构造时序特征:滞后项、滚动统计、日历特征一个都不能少
混合模型喂给下游的数据,不能只是一串原始数值。就像两个人接手同一个难题,分工前得先把材料拆开归类。对时间序列,我先造的永远是三类特征。第一类是滞后项,即 t-1、t-7、t-24(看你的采样周期定)的原始值,这是给树模型学“昨天什么样”用的。第二类是滚动统计,比如过去 7 天的均值、标准差、最大值和最小值,刻画局部形态。第三类是日历特征,星期几、是否月初、是否节假日、当天的第几个小时,这些特征是业务规律的“索引”。
下面是造这三类特征最核心的代码片段,用 pandas 就能完成。别小看这一步,特征表的质量决定了树模型的底线。
import pandas as pd import numpy as np def build_ts_features(df, value_col='y', lags=[1, 7, 24], windows=[7, 14]): df = df.copy() # 1) 滞后特征:拿“昨天/上周/昨天同一时刻”的值 for lag in lags: df[f'lag_{lag}'] = df[value_col].shift(lag) # 2) 滚动统计:窗口内的均值、标准差、最大最小 for w in windows: df[f'rolling_mean_{w}'] = df[value_col].transform( lambda x: x.rolling(w, min_periods=1).mean() ) df[f'rolling_std_{w}'] = df[value_col].transform( lambda x: x.rolling(w, min_periods=1).std() ) # 3) 日历特征(假设索引是 datetime) df['hour'] = df.index.hour df['dayofweek'] = df.index.dayofweek df['is_weekend'] = (df.index.dayofweek >= 5).astype(int) # 删除因滞后产生的 NaN 行,保留有效训练区间 df = df.dropna().sort_index() return df逻辑上,这段代码的核心意图是让时间信息“显性化”。滞后项和滚动统计是最原始也最可靠的特征,特征工程里要求必须包含 fallback(退路)版本——如果复杂的特征没效果,至少滞后项能保证基线不崩。
参数说明方面,lags 列表的选择取决于你序列的周期:当日数据通常放 1、7、24;周数据放 1、2、4;月数据放 1、12。滚动窗口同理,w 取周期的倍数最有效,但窗口太大会吃掉敏感度,太小全是噪声。上面代码里 min_periods=1 保证序列开头不直接填空,但算出来的早期统计量样本不足,上线前最好用前几期数据补足。
3.2 窗口长度怎么定:自相关分析比拍脑袋可靠
从滞后项到 LSTM 的输入窗口,本质是同一件事的不同尺度。很多人在 LSTM 里习惯性用 10 或 20 作为时间步数,但这是把他自己的经验硬套到你的数据上。一个可靠的判断工具是自相关图(ACF)。在 Python 里用 statsmodels 的 plot_acf 画一张图,横轴是延迟阶数,纵轴是相关系数。落在置信区间外的那几阶,基本就是你的二期滞后周期。
我的做法是:取自相关图上明显显著的几个阶数的最大公约数或最小覆盖段,作为窗口长度下限。如果数据显示滞后 7 和滞后 14 强相关,那窗口至少取 14,让模型能看到至少两个完整周期。窗口太小,LSTM 只看得到一条斜线,学不到周期更替;窗口太大,又会把远古信息当噪声引入。
对于不规律的数据集,可以用相同模型在不同窗口下跑一组小实验,把验证集误差画成曲线,取拐点处的窗口值。这个实验成本远低于后面从头调整个网络,值得事先做。
3.3 时序切分的红线:验证集必须晚于训练集
这个坑几乎每个刚上手的人都会踩。普通机器学习里随意用 train_test_split 打散数据没问题,但时间序列一旦乱序切分,等于让模型偷看了未来的答案。训练阶段 loss 很低,验证阶段看似也不差,上线后完全不是那么回事。
正确动作是设定一个时间锚点:训练集是锚点之前的历史,验证集是锚点之后的连续一段,测试集再往后推一段。这三个区间之间不能有任何重叠。更严格的写法是 walk-forward 滚动切分,即训练集随时间逐步向前推进,每推一步做一次预测,模拟线上每一天的真实情况。这个后面专门说。
def temporal_split(df, train_ratio=0.7, val_ratio=0.15): n = len(df) train_end = int(n * train_ratio) val_end = int(n * (train_ratio + val_ratio)) train = df.iloc[:train_end] val = df.iloc[train_end:val_end] test = df.iloc[val_end:] return train, val, test这段代码实现的是最朴素的切分策略。注意它只按位置切,不关心业务特殊日期。如果你的数据里后半段恰好全是旺季,那么验证集就都是旺季样本,模型在淡季表现会被高估或低估。一个改进是,把业务分类(淡旺季、大促是否开启)单独作为一个字段,做分层切分,保证训练和验证里都有淡季和旺季样本。但即便如此,时间边界永远不能突破。
4. 跑通 LSTM + XGBoost 混合模型:最小可复现实验
4.1 先用 LSTM 把“路径信息”压缩成特征
混合模型里 LSTM 的定位不是最终输出,而是一个特征提取器。这句话是最关键的认知转变。你不需要把 LSTM 调到完美预测,只需要让它把原始时间序列压缩成一个有意义的嵌入表示。就像给树模型增加一位“时间感很强”的同事,他先看完一段序列,告诉你这段序列处于什么趋势状态,然后交给树模型做最终判断。
import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm_feature_extractor(window_size=7, n_features=5, latent_dim=16): model = Sequential([ LSTM(latent_dim, return_sequences=True, input_shape=(window_size, n_features)), Dropout(0.2), LSTM(latent_dim, return_sequences=False), Dense(latent_dim, activation='relu'), Dense(1) # 预测目标 ]) model.compile(optimizer='adam', loss='mse') return model # 数据准备:假设已经构造好 LSTM 需要的 3D 输入 X_seq # X_shape = (样本数, window_size, n_features) # 训练过程只做回归,同时把第二层 LSTM 的输出取出来 lstm_model = build_lstm_feature_extractor() lstm_model.fit( X_train_seq, y_train, epochs=50, batch_size=64, validation_data=(X_val_seq, y_val), callbacks=[EarlyStopping(patience=5, restore_best_weights=True)], verbose=0 ) # 提取中间表示(没有用 predict,用中间层模型) from tensorflow.keras.models import Model feature_extractor = Model( inputs=lstm_model.input, outputs=lstm_model.layers[2].output # 第二个 LSTM 层的输出 ) lstm_features_train = feature_extractor.predict(X_train_seq) lstm_features_val = feature_extractor.predict(X_val_seq)这段代码有两个关键参数先说清楚:窗口大小 window_size 沿用上一节的自相关分析结果,n_features 是你喂进去的特征通道数,latent_dim 别贪大,16 起步足够了。LSTM 隐层太大只会让特征提取器过拟合到训练序列的细节上,嵌入的泛化能力反而变差。
第二个关键点是 dropout 的位置和取值。我把 Dropout 放在两个 LSTM 之间,0.2 起步比较稳妥。Dropout 对时序模型的作用是强制它不要过度记忆训练段,逼它学到更一般的序列变化规律。EarlyStopping 的 patience 设 5 就够,时序模型在验证集上的 loss 波动大,太小的 patience 容易被提前掐断。
注意这里没有纠结预测精度,因为把 LSTM 输出作为最终预测,只是它的一半使命。更重要的一半是把第二层 LSTM 的输出向量接入树模型。这个 latenet_feature 就相当于一个“语义摘要”,核心信息被保留下来,冗余细节被压缩掉。这一步就是“混合模型”里神经网络承担的角色。
4.2 特征拼接后交给 XGBoost:两阶段训练流程
LSTM 层产出的嵌入特征,加上前面 pd.DataFrame 构造的滞后项、滚动统计和日历特征,合并成一张宽表,然后进入 XGBoost 做最终预测。这不叫 ensemble,而是两阶段特征堆叠。树模型在这个阶段的任务是尽量挖掘“手动特征 + 网络嵌入”之间的非线性交叉。
import xgboost as xgb def build_stacked_dataset(df_features, lstm_features, target_col='y'): X = df_features.copy() X['lstm_embedding'] = lstm_features y = X.pop(target_col) return X, y X_train_wide, y_train_wide = build_stacked_dataset(train_feats, lstm_features_train, target_col='y') X_val_wide, y_val_wide = build_stacked_dataset(val_feats, lstm_features_val, target_col='y') xgb_model = xgb.XGBRegressor( n_estimators=500, max_depth=4, learning_rate=0.05, subsample=0.8, colsample_bytree=0.7, reg_alpha=0.1, reg_lambda=1.0, random_state=42 ) xgb_model.fit( X_train_wide, y_train_wide, eval_set=[(X_val_wide, y_val_wide)], verbose=False )XGBoost 参数里,n_estimators 我给到 500 但靠 early stopping 去掐,而不是靠它硬跑完;learning_rate 从 0.05 起步,学习率小一点让树走得更稳,配合足够的树数量才能发挥融合特征的作用。max_depth 设 4 是因为此时传入的特征维度一般不会超过 20 列,树太深只是纯增加过拟合风险,并不会带来更多有效交叉。
subsample 和 colsample_bytree 都是防过拟合的旋钮。时间序列的特征之间天然多重共线性大,如果树每次都看全部特征,很容易锁定某两列的固定组合;随机抽样特征能迫使每棵树从不同角度逼近。reg_alpha=0.1 和 reg_lambda=1.0 是 L1/L2 正则,它们的实际作用是让模型对微小波动不敏感,避免在验证集上的表现被个别离群点牵着走。
4.3 参数起步表:不用全调,先跑这些数
| 模块 | 参数 | 起步值 | 调整方向 |
|---|---|---|---|
| LSTM | latent_dim | 16 | 特征少用小值,序列复杂度高才加大 |
| LSTM | dropout | 0.2 | 过拟合加大到 0.3,欠拟合降到 0.1 |
| XGBoost | learning_rate | 0.05 | 降到 0.02 配合加树,升到 0.1 配合早停 |
| XGBoost | max_depth | 4 | 调高到 6 前先检查特征数量 |
| XGBoost | subsample | 0.8 | 数据量少时升到 0.9 以上 |
| 融合权重 | scipy 最小二乘 | 全量拟合 | 必须滚动拟合,窗口 7 天 |
5. 混合模型常见翻车现场:5 条踩坑与排查清单
5.1 现象:LSTM 在验证集上 loss 像锯齿,原因出在归一化
这是高频翻车点。训练集 loss 曲线平滑下降,验证集曲线像心电图。很多人第一反应是调学习率,调 dropout,但真正的原因是:你用的是全量数据的 mean/std 做归一化,导致验证集上的数值范围被训练集“剧透”了——验证集最大值超出训练集范围内,模型见都没见过。
解决:先切分数据,再分别对训练集、验证集、测试集用训练集的统计量做 transform。标准动作是 fit 在训练集,transform 在所有集合。
5.2 现象:XGBoost 验证集分数极高,上线崩盘,原因在数据泄露
你可能在构造滞后特征时忽略了一件事:当前行的 lag_1 其实是上一行的 y。如果 test 阶段不能拿到真实的上一行 y(因为预测未来时根本没有真实值),这个特征就是未来数据。混合模型里 LSTM 和 XGBoost 的特征拼接阶段最容易出这种问题,因为滞后特征显眼、惯性思维强。
解决:写一个“预测时点”审查清单,逐列确认该特征在 T 时刻是否已知。凡是用到 T 时刻之后信息的列,一律删除或改成可用延迟版本。
5.3 现象:加权融合后反而比单模型差,原因在权重过拟合验证集
用最小二乘在验证集上求权重,本质上是把验证集当年当训练集再用了一次。如果验证集只有一两周数据,权重会过度逼近这一段的表现,未来表现自然掉下去。
解决:把权重求解放到滚动窗口里。每 7 天重新拟合一次权重,且权重求解窗口和权重应用窗口必须连续但不重叠。这样模型权重本身也有了时序适应能力。
5.4 现象:GMM 聚类一日三变,同一时刻今天分到类 1,明天分到类 2
如果你没固定随机种子,也不做标准化,GMM 的聚类结果每次跑出来都可能不同。它对特征尺度极其敏感,不标准化时方差大的特征直接霸榜决定簇结构。
解决:提取完特征后,先用 StandardScaler 标准化,再固定 GaussianMixture 的 random_state,并且手动设定 n_init(聚类初始化次数)到 10 以上。初始化次数多了再投票,簇结构才稳定,下游模型才能学到不变的切换规则。
5.5 现象:前几个预测点误差巨大,原因在滚动统计的前向泄漏
rolling_mean 和 rolling_std 在 pandas 里默认是左闭右闭窗口,即包含当前时刻。如果当前预测目标是 T+1,那么 T 时刻的滚动均值可用是没错的——但如果你 3.1 的代码没做 shift,T+1 的聚合特征实际包含了 T+1 当天的数据,当场泄露。
解决:构造特征时统一 shift 一期。比如 df['rolling_mean_7'] 计算的是截至 T 日的均值,要作为 T+1 的特征,必须 shift(1)。这是一个看似不起眼、实际非常致命的细节。
6. 上线前最该多花一小时做的一件事:用“无未来函数”回测框架收尾
混合模型和单模型上线前的区别是,前者往往是多条数据链路串在一起:LSTM 特征提取、宽表拼接、XGBoost 预测、权重滚动拟合,每一条链路都可能藏一个“未来函数”。我刚上线第一个混合模型时,前三天表现不错,第五天开始误差剧烈放大,查了一晚上发现是滚动权重拟合时用了未来 7 天的真实值来更新权重——训练时代码没问题,一部署立刻暴露。那以后我养成一个习惯,上线前写一套 walk-forward 回测框架,强制模拟“从 T 日看未来”的真实场景。
def walk_forward_evaluate(df, feature_builder, models, horizon=7): results = [] split_dates = pd.date_range(start=df.index[100], end=df.index[-horizon], freq='7D') for anchor in split_dates: # anchor 是当前最后已知日期,只用它之前的数据训练 train = df[df.index <= anchor] test = df[(df.index > anchor) & (df.index <= anchor + pd.Timedelta(days=horizon))] # 特征全部在 train 内部 fit_transform,禁止用 test 的信息 X_tr, y_tr = feature_builder(train) X_te, y_te = feature_builder(test) # 每个模型分别预测,再滚动拟合权重(只用 train 部分) preds = [] for name, model in models.items(): model.fit(X_tr, y_tr) preds.append(model.predict(X_te)) final_pred = weighted_combine(preds, X_tr, y_tr) results.append((anchor, test.index, final_pred, y_te)) return results这段代码做的事只有一个:从历史中挑出一系列时间锚点,每到一个锚点就只用这个点之前的数据重新训练全部模型,再预测锚点之后一段时间的值。每次滚动训练都重新拟合权重,不偷看未来。虽然耗时,但它是验证混合模型真实增量最可靠的手段。如果你跑完 walk-forward 后发现混合模型比最好的单模型只提升了 1% 以内,那通常不值得上线——不值得为这点精度付出多一倍的维护复杂度。
另一个我在逃过一劫后坚持的习惯是:回测榜单上永远同步记录“最接近你的混合模型的单模型”的成绩。混合模型有一个隐蔽的陷阱,它会掩盖个别模型之间相互抵消的误差,看起来线下指标很漂亮,实际是因为结构复杂、偶然性高。所以每次评估,我都强制自己回答一个问题:这个混合,比最好的单模型好在哪。回答不出来,就不上线。希望这些基于真实场景的经验能帮你在自己的序列上跑得更顺畅,尤其是当你想把 LSTM、XGBoost 和 GMM 这些模型搅到一起时,能少走几步弯路。
本文还有配套的精品资源,点击获取