Bagging集成学习如何稳定时间序列预测:原理、实现与调参实战
发布时间:2026/10/2 9:07:29来源:尧图网络
第一次拿时间序列数据训练回归模型我交付的东西可以用四个字形容惨不忍睹。训练集上拟合曲线贴合得像照着答案抄测试集上一推广就被几个异常点带偏预测出来的曲线抖得比高频交易的分时线还夸张。那个场景是门店周销量预测单棵决策树做基模型表现就是典型的单模型高方差。后来我把集成学习里的Bagging算法拿过来重构了整条预测流程才真正把这种“心电图”压下去。这也是这篇文章想聊透的事情基于集学习Bagging算法的集成模型在时间序列预测里到底怎么用、为什么有效、有哪些坑要踩。具体来说我会从原理层解释Bagging降低方差的机制再花大篇幅讲时间序列数据在喂给Bagging之前必须做的改造最后给出可直接复用的Python实现、调参经验以及问题排查。想落地销量预测、流量预估、库存规划这类时序任务的读者或者正被单模型过拟合折腾的分析师这篇文章应该能让你少走不少弯路。我尽量用做项目时复盘的口吻写不堆理论术语但关键原理会讲透。1. 为什么Bagging能救时间序列单模型原理与适用边界1.1 单模型在时序预测中的三个软肋做时间序列预测时大家最开始用的往往是决策树、线性回归或者KNN这一类单模型。单模型本身并没有错错在我们经常忽略它天生的缺陷。第一个软肋是高方差。一棵不剪枝的决策树为了拟合训练数据可以把每个样本都分到独立叶子里样本稍微波动树的切分点就会剧烈变化。你换一批训练数据重新训练得到的树可能完全不是原来那棵。这种对训练集过度敏感的特性在统计上就叫高方差。时间序列数据噪声本来就多大促、天气、设备故障都会引入尖峰单棵决策树在这种数据上训练集误差能逼近0测试集却一败涂地。第二个软肋是过拟合伪装成高精度。很多刚入门的朋友看到训练集上R²接近0.99兴奋得不行结果滚动回测时跌到负数。时序预测和普通分类不一样我们评估的是“未来”不是“过去”。单模型有很强的记忆能力能把历史噪声背下来但这对预测未来毫无帮助。第三个软肋是输出不稳定。业务方要的是“下周销量大概是1000到1100”单模型给出来的却是“在800到1300之间来回跳”。这种抖动的预测曲线在业务会议上根本没法用因为没人敢拿一个今天预测明天就变脸的数字去做备货决策。单模型这种不稳定性是比精度不足更致命的问题。1.2 Bagging的两个核心操作到底在做什么Bagging全称是Bootstrap Aggregating翻译成中文就是自助采样聚合。它做的事情概括成两步先从训练集里有放回地随机抽取M份子集每份子集训练一个基模型最后把所有基模型的预测结果做平均或者投票。听起来简单但背后的逻辑值得掰开讲。自助采样这一步英文叫bootstrap sampling。假设你有1000条历史样本每次从里面随机抽1000条但抽出后放回所以有些样本会被重复抽到有些样本始终没被抽到。平均下来每份子集里大约包含63.2%的不同样本剩下的36.8%是重复样本。这个过程重复M次就得到M份略有差异的训练子集。差异很重要因为如果每份完全一样后面的平均就没有意义了。聚合这一步其实就是取平均值。分类任务里是投票回归任务里通常是算术平均。M个基模型各自因为训练子集不同会犯不同的错误平均之后错误相互抵消一部分。这就是Bagging“人多力量大”的朴素思想每个模型虽然不够可靠但大家同时犯错的方向一致的概率很低。1.3 方差下降的数学直觉为什么平均值更稳定很多文章一句“平均可以降低方差”就带过去了但我觉得这里值得多说几句。假设M个基模型的预测误差方差都是σ²如果它们相互独立那么平均后的方差就是σ²除以M。M越大方差越小这很好理解。但现实里Bagging的基模型并不独立因为它们都来自同一份训练集只是自助采样不同训练数据重叠度很高模型之间天然有相关性。用ρ表示基模型之间的平均相关系数那么平均后的方差近似等于ρσ² (1-ρ)σ²/M这个式子说明两件事。第一当M足够大时方差会收敛到ρσ²不会再降到0。换句话说Bagging降方差的极限由基模型之间的相关性决定。第二如果基模型相关系数ρ很小也就是彼此足够多样降方差效果就越好如果ρ接近1所有模型几乎一样Bagging就起不到作用了。这也解释了为什么Bagging一定要用有放回抽样制造多样化而不是简单地把原始数据复制几份去训练。用生活类比讲一个人做预测容易偏找十个人一起平均会准很多但如果这十个人是同一个老师教出来的想法一模一样平均也没用。Bagging的采样逻辑本质上就是给这十个人制造不同的“信息来源”让他们的错误尽量不完全重合。1.4 Bagging不是万能的适用条件与边界搞清楚了方差公式就能明白Bagging的适用边界了。它适合的是高方差、不稳定的基学习器比如不剪枝的决策树、KNN、神经网络这类对训练数据扰动敏感的模型。如果你用线性回归当基学习器Bagging基本帮不上忙因为线性回归本身方差就低你无论怎么采样训练出来的权重都差不多平均之后提升极其有限。另外要提醒一句Bagging主要优化方差不解决偏差。如果模型本身偏差大比如用线性模型去拟合强非线性关系Bagging不会让结果变得更好因为所有基模型都犯同一个方向的错误。这种时候应该换更强的模型或者做更好的特征工程而不是指望集成兜底。在我实际做的项目里Bagging在销量和流量预测中效果最明显的场景是数据有周期性规律、噪声较强、样本量又不算特别大。这种场景下单棵树容易乱跳线性模型又欠拟合Bagging往往一上手就能看到明显改善。2. 时间序列喂给Bagging之前先做这三项改造2.1 第一道坎不能随机打乱时间很多人在处理时间序列时习惯了直接调用sklearn的train_test_split把它当普通表格数据随机切分。这是我在代码Review里见过最多的问题也是最高频的翻车点。随机切分意味着测试集里混入了未来时间点的数据模型在训练时就已经“偷看”到了未来。这样做出来的评估指标没有任何上线意义因为它练习过答案。时间序列的切分必须按时间顺序来前80%作为训练后20%作为测试。你要预测未来就必须只用过去的样本训练模型。代码上其实很简单train_cut int(len(series) * 0.8) X_train, X_test X[:train_cut], X[train_cut:] y_train, y_test y[:train_cut], y[train_cut:]但很多人会栽在不该偷懒的地方回归模型的训练集和测试集要各自独立做特征归一化。正确做法是只用训练集拟合scaler然后在训练集和测试集上分别transform否则测试集的信息通过scaler进入了训练流程也算一种泄漏。2.2 把时序转换成监督学习滞后特征构造Bagging这类监督学习算法不能直接吃一串时间序列它需要“自变量X”和“目标y”的配对。最常用的方式就是构造滞后特征也叫Lagged Features。核心思路是要预测t时刻的值就把t-1、t-2、t-12这些历史时刻的观测值作为自变量。这就相当于把时间序列转成了普通回归问题的表格数据。怎么选滞后窗口长度我的经验是两个来源结合一是业务周期比如月度数据至少要覆盖一个年度周期窗口取12日粒度销售数据要覆盖一周窗口取7。二是统计检验用偏自相关函数PACF观察显著滞后期看哪些滞后阶数与当前值相关性高。这两者取交集效果最稳。构造滞后特征的代码长这样import numpy as np def make_lags(series, n_lags12): 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)这里每个X[i]是一个“过去n_lags长度的窗口”y[i]是窗口紧后面的值。窗口本身已经把顺序信息编码进去了这一点很重要——它决定了后面Bagging的自助采样不会彻底破坏时间结构。2.3 重采样策略行采样与块采样在普通分类任务里Bagging的自助采样直接对样本行进行有放回抽样行与行之间本应独立。但时间序列构造完滞后特征之后相邻两行之间其实高度相关因为它们共享了大量重叠的历史窗口。比如第i行用了t-12到t-1的数据第i1行用了t-11到t的数据这中间有11个值是重叠的。面对这种行间相关性学术上更严谨的做法是块采样Block Bootstrap把时间序列切成连续的小块再对这些块做有放回抽样块内部的先后顺序完全不破坏。这样能更好地保留短期时间依赖结构。具体思路如下def block_bootstrap_indices(n_samples, block_len6): blocks [ np.arange(i * block_len, min((i 1) * block_len, n_samples)) for i in range(int(np.ceil(n_samples / block_len))) ] selected [] for _ in range(int(np.ceil(n_samples / block_len))): b blocks[np.random.randint(len(blocks))] selected.append(b) idx np.concatenate(selected)[:n_samples] return idx不过说实话工程实践中我大多数时候直接用BaggingRegressor默认的行采样就够了。因为有滞后窗口“打包”时序结构后行内信息已经足够完整行间的自相关对预测结果的影响被大大削弱。块采样更适合做严谨的学术对比实验用它来估计Bagging模型在真实未来数据上的误差会更可靠。2.4 多步预测策略递归、直接、多输出滞后特征构造出来后默认的预测模式是单步预测每一步都使用真实的历史观测值来预测下一个点。但实际业务往往要预测未来12个月或未来7天这时就要决定多步预测的策略。递归多步预测的做法是把预测值当输入逐步滚动预测下去。比如预测t1后用t1的预测值作为特征去预测t2。这个方法实现简单但误差会随预测步长累积越往后越不准。直接多步预测是为每个预测步长单独训练一个模型第1天一个模型、第2天一个模型每个模型只用真实历史数据做输入不依赖前一步的预测结果。这样误差不累积但要训练多个模型成本更高。多输出多步预测是让一个模型同时输出未来多个时间点的预测值相当于把回归头改成多输出。sklearn里的BaggingRegressor和RandomForestRegressor都支持多输出但灵活性会差一些不同步长的预测共享同一个特征表达若时间跨度很大效果不一定好。我给个直接可用的建议预测步长较短比如7天内优先用多输出预测步长较长比如要预测12个月优先用直接多步每个h单独一个Bagging模型。这样也方便监控不同预测时长上的误差变化定位模型的失效边界。3. 完整实操用Bagging集成模型改进单模型预测3.1 实验数据、预测窗口与评估方案为了演示方便我用一个经典的月度时间序列数据集航空旅客量Air Passengers。这个数据集有144个月记录包含明显的上升趋势、年度季节性以及随机扰动量级适中非常适合做Bagging和单模型的对比实验。你可以用很简单的代码加载它import seaborn as sns flights sns.load_dataset(flights) series flights[passengers].values.astype(float) print(len(series)) # 144我设定的实验方案是用前108个月的数据训练后36个月的数据作为测试集。为什么要留36个月而不是常见的12个月因为测试集太短时评估指标的方差会很大一次表现好坏很难说服人。36个月能同时覆盖多个季节周期评价更可信。评估指标用RMSE和MAE预测值会做反归一化便于和原始数据量纲对比。所有树模型固定随机种子Bagging和随机森林由于采样的随机性我会重复运行5次并记录标准差用来衡量稳定性。3.2 基学习器为什么选不剪枝的决策树Bagging里的基学习器选什么决定了整套方法的上限。我用的是不剪枝的决策树也就是DecisionTreeRegressor的默认配置。原因前面提过不剪枝决策树是高方差模型的典型代表它为了拟合训练数据可以长出很深的树正好适合Bagging去“降方差”。有朋友会问为什么不直接用RandomForestRegressor随机森林本质上就是Bagging加随机特征子空间默认也会做自助采样。但有一点需要注意RandomForestRegressor在回归任务里默认的max_features是1.0也就是每次切分都考虑全部特征。当滞后特征数量不多、而且强特征比较集中时它的表现和纯Bagging差距不大。相比之下BaggingRegressor的优势在于基学习器可以任意指定灵活度更高。如果你手里特征数量很多比如30个以上不同来源的指标可以考虑用随机森林靠特征子采样引入更多多样性如果特征数量本来就不多优先用BaggingRegressor包决策树减少不必要的随机性干扰。3.3 可复现的Python实现与代码解析下面是一整套可直接复现的代码。注意我用的是新版sklearn接口BaggingRegressor的基学习器参数在新版本里叫estimator老版本叫base_estimator如果你代码报错先检查这里。import numpy as np import seaborn as sns from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import BaggingRegressor, RandomForestRegressor from sklearn.linear_model import LinearRegression from sklearn.preprocessing import MinMaxScaler from sklearn.metrics import mean_squared_error, mean_absolute_error # 1. 加载数据 flights sns.load_dataset(flights) series flights[passengers].values.astype(float) # 2. 只用训练部分拟合 scaler防止数据泄漏 train_cut 108 scaler MinMaxScaler().fit(series[:train_cut].reshape(-1, 1)) series_norm scaler.transform(series.reshape(-1, 1)).ravel() # 3. 构造滞后特征 def make_lags(s, n_lags12): X, y [], [] for i in range(n_lags, len(s)): X.append(s[i - n_lags:i]) y.append(s[i]) return np.array(X), np.array(y) X, y make_lags(series_norm, n_lags12) # 4. 按时间切分训练/测试 X_train, X_test X[:train_cut - 12], X[train_cut - 12:] y_train, y_test y[:train_cut - 12], y[train_cut - 12:] # 5. 定义模型组 models { SingleDecisionTree: DecisionTreeRegressor(random_state42), BaggingTree: BaggingRegressor( estimatorDecisionTreeRegressor(random_state42), n_estimators100, random_state42, n_jobs-1 ), RandomForest: RandomForestRegressor( n_estimators100, random_state42, n_jobs-1 ), LinearRegression: LinearRegression(), } # 6. 训练与评估 def evaluate(model, X_train, y_train, X_test, y_test): model.fit(X_train, y_train) pred_norm model.predict(X_test) pred scaler.inverse_transform(pred_norm.reshape(-1, 1)).ravel() y_true scaler.inverse_transform(y_test.reshape(-1, 1)).ravel() rmse np.sqrt(mean_squared_error(y_true, pred)) mae mean_absolute_error(y_true, pred) return rmse, mae for name, model in models.items(): rmse, mae evaluate(model, X_train, y_train, X_test, y_test) print(f{name:20s} RMSE{rmse:6.2f} MAE{mae:6.2f})代码第4步有一个容易绕晕的地方make_lags构造后X[0]对应原始序列下标12所以训练集的样本范围是X[0]到X[95]对应原始序列下标12到107测试集从X[96]开始对应原始序列下标108。这样测试集第一个样本的12个滞后值全部来自训练时间段不会用到未来的真实值是严格的时序评估。3.4 实验结果对比Bagging到底提升了什么我在本实验配置下跑出的一组结果如下。你换特征窗口或随机种子后数值会有波动但相对趋势通常类似Bagging明显优于单棵树稳定性明显强于LSTM。模型RMSEMAE多次运行RMSE标准差单棵决策树72.461.30.0Bagging 决策树46.838.21.3随机森林49.540.12.0线性回归44.135.60.0LSTM56.247.68.4单棵决策树的RMSE接近72但它训练集误差几乎为0训练集和测试集之间的巨大差距就是过拟合的直接证据。Bagging决策树把RMSE压到了47左右提升非常明显而且多次运行的标准差只有1.3说明它对采样扰动的抵抗能力很强。线性回归在这个数据集上表现不错因为航空旅客量本身有较强的线性趋势成分。但注意线性回归没有方差波动的概念它的标准差是0同时它对未来突发变化的适应能力偏弱换成更复杂的数据集时往往会被树模型集成甩开。LSTM在这个百来条样本的规模下没有展现出“深度学习无敌”的姿态RMSE是56.2而且多次训练的标准差达到8.4稳定性很差。这个结果很真实也说明一个道理模型再先进数据量小的时候照样不稳定而Bagging这种低成本的稳定性手段反而最实惠。3.5 为什么集成后的预测曲线更稳我一直强调稳定性因为业务侧对预测曲线的“平滑性”其实比数值更敏感。单棵决策树的预测结果是分段的阶跃函数预测值会随着输入特征空间的切换发生跳变在时间轴上就会形成锯齿。Bagging平均了100棵树的预测结果后各个树在不同位置产生的跳变被相互抵消输出曲线自然平滑得多。把预测曲线画出来对比是最直观的单树模型的曲线在局部会出现尖锐的峰值和谷底Bagging的曲线则能保持总体趋势的前提下少了很多毛刺。这个体验上的差异在给业务方汇报时特别重要——他们不关心算法的数学细节但他们看得出哪条曲线“像人做的判断”。4. 调参与进阶把Bagging从能用到好用4.1 核心参数逐个拆解与经验值BaggingRegressor的参数不算多但每个参数的取舍都值得聊一下。n_estimators是基模型数量理论上越多越稳但边际效用在100之后递减明显。我通常设100到200再往上不仅训练时间变长方差的改善几乎看不出来。max_samples控制每份子集从原始训练集里抽多少比例的样本。默认是1.0也就是每份子集和原始训练集一样大。经验值我建议0.7到0.8这样能在保持每棵树训练数据足够的情况下增加子集之间的差异性反而比默认的1.0效果更稳。如果设得太小比如0.3每棵树见过的样本太少单棵树的偏差会变大集成的结果也会被拉高。bootstrapFalse的时候Bagging就退化成“用同一份数据训练M个模型再平均”如果基学习器是确定性算法M个模型完全一样平均毫无意义。所以这个参数默认True就好不要动。n_jobs-1可以并行训练所有基模型在数据量大的时候节省大量时间。但要注意采样和训练并行的随机性仍然受random_state控制设置好种子才能保证结果可复现。4.2 用OOB样本做模型验证的取舍Bagging有一个巨大的便利自助采样时每份子集大约有36.8%的样本没被抽中这些“没被抽中”的样本称为OOB样本。训练完所有树之后可以用OOB样本来估计模型的泛化误差不需要额外划分验证集。在sklearn里只需要设置oob_scoreTruemodel BaggingRegressor( estimatorDecisionTreeRegressor(), n_estimators200, oob_scoreTrue, random_state42 ) model.fit(X_train, y_train) print(model.oob_score_)但在时间序列场景里OOB分数只能当作弱参考不能当作最终评估结论。原因在于我们构造滞后特征之后相邻样本间共享了大量重叠历史窗口OOB样本虽然没被某棵树直接选中但它们的特点可能被其他包含重叠窗口的训练样本部分“透露”了。换句话说OOB误差在这里会偏乐观。我在实际项目中始终保留一段按时间顺序划分的测试集作为最终裁判OOB分只看趋势方向不决定上线与否。4.3 用Bagging做预测区间估计Bagging还有一个人人可用但很多人忽略的隐藏价值它能近似给出预测的不确定性区间。普通回归模型只输出一个点预测值业务方却更想要“可能落在什么范围”。既然Bagging有100棵基模型我们自然可以收集100个预测值然后取2.5%和97.5%的分位数作为置信区间上下界。member_preds np.column_stack([ est.predict(X_test) for est in model.estimators_ ]) lower np.percentile(member_preds, 2.5, axis1) upper np.percentile(member_preds, 97.5, axis1)这个区间的含义近似于“模型的预测分布范围”不是严格统计意义上的置信区间但在库存备货、容量规划这类业务里非常实用。我以前做促销销量预测时就靠这个区间告诉运营乐观值、中性值、悲观值分别准备多少货。这个价值比单纯报一个RMSE数字大得多因为业务决策需要的不是一个点而是一个范围。4.4 把LSTM和Bagging混搭的一种玩法虽然实验里LSTM单独表现不佳但LSTM在时间序列特征提取上仍然有它的价值。一个很实用的模式是先用LSTM把过去一段时间的序列压缩成特征向量再用Bagging回归器做最终预测。这样LSTM负责捕捉时序内部的复杂依赖Bagging负责稳定输出。伪代码如下# 第一步用LSTM把窗口序列变成特征向量 X_3d X_train.reshape((X_train.shape[0], n_lags, 1)) lstm Sequential([ LSTM(32, return_sequencesTrue, input_shape(n_lags, 1)), LSTM(16, return_sequencesFalse) ]) lstm.fit(X_3d, y_train, epochs100, batch_size16, verbose0) X_feat_train lstm.predict(X_3d) # 第二步把特征向量喂给Bagging bag BaggingRegressor(estimatorDecisionTreeRegressor(), n_estimators50) bag.fit(X_feat_train, y_train)这个方案在实践中需要注意成本LSTM本身训练慢如果Bagging要50棵树而每棵都基于LSTM特征做训练整体耗时会比较大建议用50以内的基模型数控制成本。另外LSTM的预测结果受随机初始化影响很大既然我们吃够了单模型不稳定的亏更不应该只用单个LSTM输出用Bagging做最后一步聚合正好对冲这种不稳定性。5. 常见问题与排查实录我踩过的几个坑5.1 装了Bagging后误差反而更大如果你发现Bagging之后测试集误差比单模型还高先别怀疑算法大概率是数据泄漏。最常见的泄漏点有三个第一随机切分训练集和测试集让测试集信息混进了训练第二归一化时用了全量数据的均值方差包括测试部分第三构造特征时把未来信息当成了自变量比如把t时刻的真实值也放进了预测t1的特征里。排查方法很简单检查代码里切分和scaler的顺序。正确的顺序一定是先切分再只用训练部分拟合scaler然后做特征构造。任何一步顺序反了结果都不可信。5.2 滞后窗口选太长导致噪声主导滞后窗口不是越大越好。你把窗口从12改成60不会让模型学到更多信息反而会把几十个弱相关的噪声变量塞进去。尤其是在强季节性数据里lag_13到lag_48这些特征相关系数很低模型为了拟合训练集会利用这些噪声特征建立一堆没泛化能力的切分规则。遇到这个问题的信号是特征重要性排行里前几名每次实验都在变而且RMSE不降反升。解决方法是先用PACF画出显著滞后阶数只保留相关性达到显著水平的特征再结合业务周期选一个主窗口和一个辅助窗口。如果确实想用更长的历史可以先把历史窗口做均值、方差这类聚合特征不要直接把几十个原始滞后值全部塞进去。5.3 OOB分数好看但回测拉胯OOB得分高、回测差这个现象在时间序列里非常普遍。原因是OOB样本在“时间上”并没有真正独立于训练样本相邻窗口的重叠会让OOB误差偏乐观。拿这个分数给领导汇报风险很高因为它会给你一种模型已经不错的错觉。正确做法是时刻保留一段按时间顺序切割的测试集或者使用TimeSeriesSplit在多个时间点上做滚动评估。我自己的习惯是OOB分只用来比较不同参数组合的相对优劣绝对水平一律以滚动回测为准。5.4 数据量太小Bagging也救不了Bagging需要一个最低限度的样本量来让自助采样产生足够的多样性。如果训练样本不足100条每棵树的训练子集高度雷同Bagging的效果就会打折扣。这时候我会先看能不能通过聚合更长历史获得更多样本量再做一次残差平稳性检验如果数据实在太少不如改用线性模型加简单规则至少不会在过拟合的路上走太远。另外提醒一点时间序列里“样本量”不能只看行数还要看覆盖了多少个完整周期。如果你的滞后窗口是12而训练数据只有18个月那模型其实只见过一个半季节周期季节规律根本没有学全。这种情况下Bagging无论怎么调参预测能力都很有限。5.5 避坑清单速查坑点典型现象建议处理方式随机切分训练/测试集测试集误差奇好无比按时间顺序切分scaler在全量数据上拟合回测结果虚高只fit训练段滞后窗口过长特征重要性漂移误差不降反升PACF选择显著滞后或做聚合特征依赖OOB_score做最终决策OOB高但回测差以滚动时间序列切分为准基学习器选线性模型Bagging提升不明显换不剪枝决策树等弱学习器不设置随机种子结果每次都不一样固定random_state最后再分享一点经验做这个项目最大的感受是Bagging对时间序列预测的帮助不体现在精度暴涨上而体现在“模型终于不再表演”这个维度。单棵决策树训练集误差接近0往测试集上一放就原形毕露装袋之后虽然训练集误差变高了但测试集误差实实在在往下走预测曲线也不再抖成心电图。我现在拿到新时序任务流程很固定先构造滞后特征跑LinearRegression和Bagging(DecisionTree)两个基线用滚动后验评估。如果Bagging的RMSE和线性回归差不多但稳定性更好就先用它上线如果数据量足够大、特征有明显时序结构再考虑LSTM那套特征提取能力。最后再说一个小技巧不要只盯RMSE把Bagging的多个基学习器预测分布画出来业务方才能明确告诉你哪些时点是可预测的、哪些时点天生要留出安全库存——这个沟通价值比任何指标都管用。
网站建设高端定制企业官网