集成与堆叠
在上一章中,我们介绍了几种机器学习算法,并使用它们对伦敦智能电表数据集进行了预测。现在,我们已经获得了数据集中所有家庭的多个预测结果,那么如何通过选择或组合这些不同的预测来得出一个单一的预测呢?最终,我们只能有一个预测结果,用于规划你进行预测的任何任务。这正是本章将要讨论的内容——我们将学习如何利用组合和数学优化来得出单一的预测。
在本章中,我们将涵盖以下主题:
- 组合预测的策略
- 堆叠或混合
技术要求
您需要按照本书前言中的说明设置Anaconda环境,以获得包含本书代码所需的所有库和数据集的运行环境。任何额外的库将在运行笔记本时安装。
在使用本章代码之前,你需要运行以下notebooks:
02-Preprocessing_London_Smart_Meter_Dataset.ipynb在Chapter0201-Setting_up_Experiment_Harness.ipynb在Chapter0402-Baseline_Forecasts_using_darts.ipynb在Chapter0401-Feature_Engineering.ipynb在Chapter0602-Dealing_with_Non-Stationarity.ipynb在Chapter0702a-Dealing_with_Non-Stationarity-Train+Val.ipynb在Chapter0700-Single_Step_Backtesting_Baselines.ipynb在Chapter0801-Forecasting_with_ML.ipynb在Chapter0801a-Forecasting_with_ML_for_Test_Dataset.ipynb在Chapter0802-Forecasting_with_Target_Transformation.ipynb在Chapter0802a-Forecasting_with_Target_Transformation(Test).ipynb在Chapter08
本章的代码可以在 https://github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python-/tree/main/notebooks/Chapter09 找到..
组合预测
我们已经使用多种技术生成了预测——有些是单变量方法,有些是机器学习方法,等等。但归根结底,我们需要一个单一的预测,这意味着要选择一个预测或组合多种预测。最直接的选择是在验证数据集上表现最好的算法,在我们的案例中,就是LightGBM。我们可以将这种选择视为另一个函数,该函数以我们生成的预测作为输入,并将它们组合成最终的预测。数学上,可以表示如下:
Y = F(Y1, Y2, …, YN)
这里,F 是组合 N 个预测的函数。我们可以使用 F 函数在验证数据集中选择表现最好的模型。然而,这个函数可以任意复杂,因此在平衡偏差和方差的同时选择正确的 F 函数是必须的。
笔记本提示:
若要跟随代码操作,请使用 01-Forecast_Combinations.ipynb 文件夹中的 Chapter09 笔记本。
我们将首先加载目前为止生成的所有预测(包括验证集和测试集的预测)及其对应的指标,并将它们合并到 pred_val_df 和 pred_test_df 中。现在,我们必须使用 pd.pivot 重塑 DataFrame 以获得所需的形状。到目前为止,我们一直在跟踪多个指标。但为了达到这个目标,我们需要选择一个指标。在本练习中,我们选择 MAE 作为指标。验证指标可以被合并并重塑为 metrics_combined_df:

图9.1:重塑后的预测 DataFrame
现在,让我们来看一些组合预测的不同策略。
最佳拟合
这种选择最佳预测的策略目前是最流行的,它很简单:基于验证指标为每个时间序列选择最佳预测。该策略因许多自动预测软件工具而流行,它们称之为“最佳拟合”预测。算法非常简单:
- 使用验证数据集为每个时间序列找到表现最佳的预测。
- 对于每个时间序列,从同一模型中选择测试数据集的预测。
我们可以很容易地做到这一点:
# Finding the lowest metric for each LCLid
best_alg = metrics_combined_df.idxmin(axis=1)
#Initialize two columns in the dataframe
pred_wide_test["best_fit"] = np.nan
pred_wide_test["best_fit_alg"] = ""
#For each LCL id
for lcl_id in tqdm(pred_wide_test.index.get_level_values(0).unique()):
# pick the best algorithm
alg = best_alg[lcl_id]
# and store the forecast in the best_fit column
pred_wide_test.loc[lcl_id, "best_fit"] = pred_wide_test.loc[lcl_id, alg].values
# also store which model was chosen for traceability
pred_wide_test.loc[lcl_id, "best_fit_alg"] = alg
这将创建一个名为 best_fit 的新列,其中包含根据我们讨论的策略选择的预测。现在,我们可以评估这个新预测并获取测试数据集的指标。下表展示了最佳个体模型(LightGBM)和新策略(best_fit):

图9.2:最佳拟合策略的聚合指标
在这里,我们可以看到最佳拟合策略的整体表现优于最佳个体模型。然而,该策略的一个缺点是它的基本假设——在验证期内表现最好的模型在测试期内也表现最好。没有对其他预测模型的避险等。鉴于时间序列的动态性质,这并不总是最佳策略。这种方法的另一个缺点是最终预测的不稳定性。
当我们在实时环境中使用这样的规则时,每周重新训练并重新运行最佳拟合,任何时间序列的预测可能会在不同预测模型之间来回跳跃,从而产生截然不同的预测。因此,最终预测显示出很大的周环比不稳定性,这妨碍了我们使用这些预测的下游行动。我们可以研究其他一些没有这种不稳定性的技术。
集中趋势度量
另一个重要的策略是使用平均值或中位数来组合预测。这是一个独立于验证指标的函数 F。这既是这种方法的吸引力所在,也是其令人焦虑之处。由于我们根本不使用验证指标,所以不可能过拟合它们。但另一方面,如果没有验证指标的任何信息,我们可能会包含一些非常差的模型,这会拉低集成效果。然而,经验上,这种简单的平均或取中位数已被证明是一种非常强大的预测组合方法,很难被超越。让我们看看如何实现:
# ensemble_forecasts is a list of column names(forecast) we want to combine
pred_wide_test["average_ensemble"] = pred_wide_test[ensemble_forecasts].mean(axis=1)
pred_wide_test["median_ensemble"] = pred_wide_test[ensemble_forecasts].median(axis=1)
上述代码将创建两个新列,分别名为 average_ensemble 和 median_ensemble,其中包含组合后的预测。现在,我们可以评估这个新预测并获取测试数据集的指标。下表展示了最佳个体模型(LightGBM)和新策略:

图9.3:均值和中位数策略的聚合指标
在这里,我们可以看到均值策略和中位数策略都没有比最佳单个模型整体表现更好。这可能是因为我们包含了Theta和FFT等方法,它们的表现远逊于其他机器学习方法。但由于我们没有从验证数据集中获取任何信息,我们对此并不知情。我们可以破例,决定使用验证指标来选择哪些模型纳入均值或中位数计算。但我们必须谨慎,因为这样我们就更接近于假设在验证期内有效的模型在测试期内也会有效。
这里我们可以使用一些手动技术,例如修剪(丢弃集成中表现最差的模型)和筛选(只选择集成中最好的几个模型)。虽然有效,但这些方法有些主观,并且通常难以使用,尤其是当我们有大量模型可供选择时。
如果我们考虑这个问题,它本质上是一个组合优化问题,我们必须选择能够优化指标的最佳模型组合。如果考虑使用均值来组合不同的预测,从数学上可以这样理解:
$$\hat{Y} = argmin \, L\left(\frac{1}{\sum_{i=1}^{N} w_i} \sum_{i=1}^{N} w_i \times \hat{Y}_i, Y\right)$$
这里,L 是我们试图最小化的损失或指标。在我们的案例中,我们选择MAE作为该指标。$w_N \in [0, 1]$ 是每个基础预测的二进制权重。最后,$\hat{y}_N$ 是 N 个基础预测的集合,而 Y 是时间序列的真实观测值。
但与纯优化不同,纯优化中没有偏差和方差的概念,我们需要一个能够泛化的最优解。因此,在训练数据中选择全局最小值并不可取,因为那样我们可能会进一步过拟合训练数据集,增加结果模型的方差。对于这个最小化问题,我们通常使用样本外预测,在本例中可以是验证期间的预测。
最直接的解决方案是找到在验证数据上最小化该函数的 w。但这种方法有两个问题:
- 随着基础预测数量 N 的增加,可能的候选方案(基础预测的不同组合)呈指数级增长。这很快就会变得计算上不可行。
- 由于过拟合验证期,选择验证期内的全局最小值可能不是最佳策略。
现在,让我们看看这个组合优化问题的几个基于启发式的解决方案。
启发式问题求解是一种使用经验法则或捷径快速找到解决方案的策略,即使它们可能不是最优的。当精确解的计算成本高昂或耗时费力时,启发式方法可能很有用。然而,在某些情况下,它们可能导致次优甚至错误的解。
启发式方法通常与其他问题求解方法(如元启发式)结合使用,以提高搜索过程的效率和效果。元启发式是用于解决优化问题的高层次、问题无关的策略。它们提供了一个开发启发式算法的框架,可以高效地探索复杂的搜索空间并找到接近最优的解。与传统的优化方法不同,元启发式通常从自然现象或生物过程中汲取灵感。
元启发式方法的常见例子包括遗传算法(受自然选择启发)、模拟退火(受冶金学启发)、粒子群优化(受鸟类群集启发)和蚁群优化(受蚂蚁觅食启发)。这些方法采用概率或随机方法平衡探索与利用,从而避免陷入局部最优,并发现潜在更优的解。
简单爬山算法
我们在讨论决策树和梯度提升树时简要介绍了贪心算法。贪心优化是一种启发式方法,逐阶段构建解,在每个阶段选择局部最优。在这两种机器学习模型中,我们采用贪心、逐阶段的方法来寻找计算上不可行的优化问题的解。为了选择能够给出最佳预测组合的最优子集,我们可以使用一种称为爬山法的简单贪心算法。如果我们将目标函数表面视为一座山,要找到最大值,就需要爬山。顾名思义,爬山算法一步一阶地向上攀登,在每一步中都会选择最佳路径,从而增加目标函数值。下面的图示可以让这一点更清晰。

从图9.4中可以看出,目标函数(需要优化的函数)有多个峰值(山丘),在爬山算法中,我们逐步“攀登”山峰以到达峰值。还需要注意的是,根据我们开始攀登的位置,我们可能会达到目标函数的不同点。在图9.4中,如果从点A开始,我们会达到局部最优而错过全局最优。现在,让我们更严格地看一下算法是如何工作的。
这里,C是一个候选集(基础预测),O是我们要最小化的目标函数。简单爬山算法的步骤如下:
- 初始化一个起始解 Cbest,作为在O中给出最小值的候选,以及 Obest,并将 Cbest 从 C 中移除。
- 当 C 的长度 > 0 时,执行以下步骤:
- 评估 C 中的所有成员,通过将 Cbest 中的基础预测与 C 中的每个元素平均,并选择最佳成员(Cstage best),将其添加到 Cbest 中以最小化目标函数 O(Ostage best)。
- 如果 Ostage best > Obest,则执行以下操作:
- Cbest = Cbest U Cstage best。
- Obest = Ostage best。
- 从 C 中移除 Cstage best。
- 否则,退出。
运行结束时,我们得到 Cbest,这是通过贪心优化得到的最佳预测组合。我们已经在 src.forecasting.ensembling.py 中为此提供了一个实现,位于 greedy_optimization 函数下。该函数的参数如下:
objective:这是一个可调用对象,它接受一个字符串列表作为候选者,并返回一个float目标值。candidates:这是要纳入优化的候选者列表。verbose:一个标志,指定是否打印进度。
该函数返回一个元组,包含最佳解(字符串列表)和通过优化获得的最佳分数。
让我们看看如何在示例中使用它:
- 导入所有必需的库/函数:
# Used to partially construct a function call from functools import partial # calculate_performance is a custom method we defined to calculate the MAE provided a list of candidates and prediction dataframe from src.forecasting.ensembling import calculate_performance, greedy_optimization - 定义目标函数并运行贪心优化:
# We partially construct the function call by passing the necessary parameters objective = partial( calculate_performance, pred_wide=pred_wide_val, target="energy_consumption" ) # ensemble forecasts is the list of candidates solution, best_score = greedy_optimization(objective, ensemble_forecasts) - 得到最优解后,我们可以在测试DataFrame中创建组合预测:
pred_wide_test["greedy_ensemble"] = pred_wide_test[solution].mean(axis=1)
运行这段代码后,我们将在预测DataFrame中得到名为code的组合预测greedy_ensemble的组合预测。属于最优解的候选模型是LightGBM、Lasso Regression和

图9.5:基于简单爬山法的集成模型的聚合指标
正如我们所见,简单的爬山法比我们迄今为止看到的任何单个模型或集成技术表现更好。这种贪心方法在这种情况下似乎效果很好。现在,让我们了解一下爬山法的一些局限性,如下所示:
- 运行时考虑:由于简单的爬山法要求我们在每一步评估所有候选模型,这可能会导致运行时的瓶颈。如果候选模型数量很大,这种方法可能需要更长时间才能完成
- 短视性:爬山优化是短视的。在优化过程中,它总是在每一步选择最好的。有时,通过在一步中选择稍差的解,我们可能会得到更好的全局解
- 只进不退:爬山算法是一种只进不退的算法。一旦候选模型被纳入解中,我们就不能回去将其移除
贪心方法可能并不总能得到最优解,尤其是当有大量模型需要组合时。因此,让我们看看爬山法的一个小变种,它试图克服贪心方法的一些局限性。
随机爬山法
随机爬山法与简单爬山法的关键区别在于候选解的评估方式。在简单爬山法中,我们评估所有可能的选项并从中选出最佳者。然而,在随机爬山法中,我们随机选取一个候选,如果它比当前解更好,则将其加入解中。换言之,在爬山法中我们总是逐步向上攀登,而在随机爬山法中,我们神奇地瞬移到目标函数的不同点,检查我们是否比之前更高。这种随机性的引入有助于优化过程避免陷入局部最大值/最小值,但也给达到任何最优值带来了相当大的不确定性。让我们来看一下算法。
这里,C 是一个候选解(基预测)集合,O 是我们想要最小化的目标函数,N 是我们要运行优化的最大迭代次数。随机爬山法的算法如下:
- 初始化一个起始解 Cbest 作为候选。可以通过随机选取一个候选或选择性能最佳的模型来实现。
- 将Cbest的目标函数值O设为Obest,并将Cbest从C中移除。
- 重复以下步骤 N 次:
- 从 C 中随机抽取一个样本,将其加入 Cbest,并存储为 Cstage。
- 在目标函数 O 上评估 Cstage,并存储为 Ostage。
- 如果 Ostage > Obest,则执行以下操作:
- Cbest = Cbest U Cstage
- Obest= Ostage.
- 从 C 中移除 Cbest。
运行结束时,我们得到 Cbest,即通过随机爬山法得到的最佳预测组合。我们已在 src.forecasting.ensembling.py 的 stochastic_hillclimbing 函数中提供了该实现。该函数的参数如下:
objective:这是一个可调用对象,它接受一个字符串列表作为候选者,并返回一个float目标值。candidates:这是要纳入优化的候选者列表。n_iterations: 运行爬山法的迭代次数。如果未指定,则使用启发式(候选数的两倍)来设置此值。init: 这决定了用于初始解的策略。可以是random或best。verbose:一个标志,指定是否打印进度。random_state:一个用于获取可重复结果的种子。
该函数返回一个包含最佳解决方案(以字符串列表形式)和通过优化获得的最佳得分的元组。
它的使用方式与greedy_optimization非常相似。此处仅展示不同部分。完整代码可在notebook中找到。
from src.forecasting.ensembling import stochastic_hillclimbing
# ensemble forecasts is the list of candidates
solution, best_score = stochastic_hillclimbing(
objective, ensemble_forecasts, n_iterations=10, init="best", random_state=9
)
运行此代码后,我们的预测DataFrame中将得到名为stochastic_hillclimb__ensemble的集成预测。构成最优解的候选模型包括LightGBM、Lasso Regression_auto_stat、LightGBM_auto_stat和Lasso Regression。现在,让我们评估结果并查看聚合指标。

图9.6:基于随机爬山法的集成模型的聚合指标
随机爬山法的表现不如贪心法,但优于均值、中位数和最佳拟合集成模型。我们之前讨论了简单爬山法的三个缺点:运行时间考量、短视和单向性。随机爬山法解决了运行时间考量问题,因为我们不需要评估所有组合并选择最佳。相反,我们随机评估组合,一旦发现表现更好的解,就立即将其加入集成模型。它部分解决了短视问题,纯粹是因为算法中的随机性可能导致每个阶段选择了次优解。但它仍然只选择比当前解更好的解。
现在,我们来看另一种同样处理该问题的爬山法变体。
模拟退火
模拟退火是爬山法的一种变体,其灵感来源于物理现象——固体退火。退火是将固体加热到预定温度(通常高于再结晶点但低于熔点),保温一段时间,然后冷却(缓慢冷却或通过水淬快速冷却)的过程。
这样做是为了确保原子达到新的全局最小能量状态,从而在一些金属(如铁)中产生期望的性质。
1952年,Metropolis提出了将模拟退火作为一种优化技术。退火类比同样适用于优化背景。当我们说给系统加热时,意思是鼓励随机扰动。因此,当高温启动优化时,算法会探索空间并形成问题的初始结构。随着温度降低,结构被逐渐优化,最终得到最终解。该技术有助于避免陷入任何局部最优。局部最优是目标函数表面上的极值点,比附近其他值好,但不一定是绝对最佳解。延伸阅读部分包含用简洁语言解释局部和全局最优的资源。
现在,让我们来看看这个算法。
这里,C是一组候选(基础预测),O是我们希望最小化的目标,N是我们希望运行优化的最大迭代次数,Tmax是最大温度,而$\alpha$是温度衰减。模拟退火算法如下:
- 初始化一个起始解Cbest作为候选。这可以通过随机选取一个候选或选择表现最佳的模型来完成。
- 将Cbest的目标函数值O设为Obest,并将Cbest从C中移除。
- 将当前温度t设置为Tmax。
- 重复以下过程N次:
- 从C中随机抽取一个样本,将其加入Cbest,并存储为Cstage。
- 在目标函数O上评估Cstage,并将结果存储为Ostage。
- 如果Ostage > Obest,则执行以下操作:
- Cbest = Cbest ∪ Cstage
- Obest = Ostage
- 将Cbest从C中移除。
- 否则,执行以下操作:
- 计算接受概率$s = e^{\frac{o_{best} - o_{stage}}{t}}$。
- 在0和1之间随机抽取一个样本,记为p。
- 如果p < s,则执行以下操作:
- Cbest = Cbest ∪ Cstage
- Obest = Ostage
- 将Cbest从C中移除。
- t = t - $\alpha$(线性衰减)或t = t/$\alpha$(几何衰减)。
- 当C为空时退出。
运行结束时,我们得到Cbest,即通过模拟退火得到的最佳预测组合。我们在src.forecasting.ensembling.py中的simulated_annealing函数下提供了这一实现。将温度设置为正确的值对于算法正常工作至关重要,而且通常是最难设置的超参数。更直观地说,我们可以将温度理解为一开始接受较差解的概率。在实现中,我们也允许输入接受较差解的起始概率和终止概率。
1989年,D.S. Johnson等人提出了一种从给定概率范围估计温度范围的方法。这在initialize_temperature_range中已经实现。
总结来说,该算法从随机解开始,评估每个解的好坏。然后不断尝试新解,有时接受较差的解以避免陷入局部最优,但随着时间推移,它会“冷却”(就像金属冷却变硬),逐渐不再接受劣解。算法重复这个过程,直到没有候选可试或温度过低,最终留下找到的最佳解。
参考文献检查:
D.S. Johnson的研究论文,标题为Optimization by Simulated Annealing: An Experimental Evaluation; Part I, Graph Partitioning,在参考文献部分被列为参考文献1。
simulated_annealing函数的参数如下:
objective:这是一个可调用对象,它接受一个字符串列表作为候选者,并返回一个float目标值。candidates:这是要纳入优化的候选者列表。n_iterations:要运行模拟退火的迭代次数。这是一个强制参数。p_range:起始和结束概率,以元组形式给出。这是模拟退火中接受更差解的概率。温度范围(t_range)在优化过程中从p_range推断得出。t_range:如果我们想直接将温度范围设置为元组(起始, 结束),可以使用此参数。如果设置了此参数,p_range将被忽略。init:这决定了用于初始解的策略。可以是random或best。temperature_decay:这指定了如何衰减温度。可以是linear或geometric。verbose:一个标志,指定是否打印进度。random_state:用于获得可重现结果的种子。
该函数返回一个元组,包含最佳解(字符串列表)和通过优化获得的最佳分数。
这可以以与其它组合预测方式非常相似的方式使用。此处我们仅展示不同的部分。完整代码可在笔记本中获得:
from src.forecasting.ensembling import simulated_annealing
# ensemble forecasts is the list of candidates
solution, best_score = simulated_annealing(
objective,
ensemble_forecasts,
p_range=(0.5, 0.0001),
n_iterations=50,
init="best",
temperature_decay="geometric",
random_state=42,
)
运行此代码后,我们将在预测DataFrame中得到一个名为simulated_annealing_ensemble的组合预测。属于最优解一部分的候选者是LightGBM、Lasso Regression_auto_stat、LightGBM_auto_stat和XGB Random Forest。让我们评估结果并查看聚合指标:

图9.7:基于模拟退火的集成模型的聚合指标
模拟退火似乎比随机爬山法表现更好。我们之前讨论了简单爬山法的三个缺点——运行时间考量、短视以及只向前。模拟退火解决了运行时间考量问题,因为我们并非评估所有组合并选择最佳,而是随机评估组合,一旦发现表现更好的解就将其加入集成。它也解决了短视问题,因为通过使用温度,我们在优化初期也接受稍差的解。然而,它仍然是一个只向前的过程。
到目前为止,我们一直关注组合优化,因为我们规定了 $W_N \in [0, 1]$ 为整数。但如果我们可以放宽这一约束,使 $W_N \in \mathbb{R}$(实数),那么组合优化问题可以放松为一般的数学优化问题。让我们看看如何做到这一点。
最优加权集成
之前,我们将要解决的优化问题定义如下:
$$\hat{Y} = \underset{w}{\operatorname{argmin}} \; L\left( \frac{1}{\sum_{i=1}^{N} w_i} \sum_{i=1}^{N} w_i \times \hat{Y}_i, Y \right)$$
这里,L 是我们试图最小化的损失或指标。在我们的案例中,我们选择 MAE。$\hat{Y}_N$ 是 N 个基础预测的集合,而 Y 是时间序列的真实观测值。我们不定义 $W_N \in [0, 1]$ 为整数,而是让 $W_N \in \mathbb{R}$ 作为每个基础预测的连续权重。通过这种新的放松,组合变成了不同基础预测之间的加权平均。现在,我们考虑的是预测的软混合,而不是基于硬选择的组合优化(这是到目前为止我们一直在使用的)。
这是一个可以使用 scipy 中的现成算法求解的优化问题。让我们看看如何使用 scipy.optimize 来解决这个问题。
首先,我们需要定义一个损失函数,它接受一组权重(以列表形式)并返回我们需要优化的指标:
def loss_function(weights):
# Calculating the weighted average
fc = np.sum(pred_wide[candidates].values * np.array(weights), axis=1)
# Using any metric function to calculate the metric
return metric_fn(pred_wide[target].values, fc)
现在,我们只需使用必要的参数调用 scipy.optimize 即可。让我们学习如何做到这一点:
from scipy import optimize
opt_weights = optimize.minimize(
loss_function,
# set x0 as initial values, which is a uniform distribution over all the candidates
x0=[1 / len(candidates)] * len(candidates),
# Set the constraint so that the weights sum to one
constraints=({"type": "eq", "fun": lambda w: 1 - sum(w)}),
# Choose the optimization technique. Should be gradient-free and bounded.
method="SLSQP",
# Set the lower and upper bound as a tuple for each element in the candidate list.
# We set the maximum values between 1 and 0
bounds=[(0.0, 1.0)] * len(candidates),
# Set the tolerance for termination
options={"ftol": 1e-10},
)["x"]
优化通常很快,我们将得到一组浮点数形式的权重。我们在 src.forecasting.ensembling.py 中将其封装在一个名为 find_optimal_combination 的函数中。该函数的参数如下:
candidates:这是一个候选列表,将被包含在优化中。它们以与返回的权重相同的顺序返回。pred_wide:这是我们需要学习权重的预测DataFrame。target:这是目标的列名。metric_fn:这是一个具有metric(actuals, pred)签名的任意可调用对象。
该函数将最优权重作为浮点数列表返回。让我们看看通过验证预测学习到的最优权重是什么:

图9.8:通过优化学习到的最优权重
这里,我们可以看到优化自动学会了忽略FFT、Theta、XGB Random Forest和XGB Random Forest_auto_stat,因为它们没有为集成增加太多价值。它还为每个预测学习了一些非零权重。这些权重已经类似于我们使用之前讨论的技术所做的选择。现在,我们可以使用这些权重来计算加权平均,并将其称为optimal_combination_ensemble。
聚合结果应如下所示:

图9.9:基于最优组合的集成的聚合指标
这里,我们可以看到这种软混合预测在所有三个指标上都比所有基于硬选择的集成要好得多。
在我们讨论的所有技术中,都将MAE作为目标函数。但我们可以使用任何度量、多个度量的组合,甚至带有正则化的度量作为目标函数。在讨论随机森林时,我们提到去相关树对于获得更好性能至关重要。一个非常相似的原则也适用于选择集成。去相关的基础预测为集成增加了价值。因此,我们可以使用任何多样性度量来正则化我们的度量。例如,我们可以使用相关性作为一个度量,并创建一个正则化的度量用于这些技术。01-Forecast_Combinations.ipynb文件夹中的Chapter09笔记本包含一个展示如何操作的额外部分。
我们首先讨论用数学模型组合预测:
Y = F(Y1, Y2, …, YN)
这里,F是组合N个预测的函数。
我们在寻找将这个函数作为优化问题的方法时,使用了诸如均值或中位数来组合度量。但我们也看到了另一种学习这个函数F的方法,即从数据中学习,不是吗?让我们看看如何做到这一点。
堆叠与混合法
本章开始时我们讨论了机器学习算法,这些算法从一组输入和输出中学习一个函数。在使用这些机器学习算法时,我们学习了预测时间序列的函数,现在称之为基础预测。
为什么不使用相同的机器学习范式来学习这个新函数F呢?
这正是我们在堆叠(通常称为堆叠泛化)中所做的,我们在一些基础学习器的预测上训练另一个学习算法来组合这些预测。这个第二层模型通常被称为堆叠模型或元模型。而且,通常这个元模型的表现等于或优于基础学习器。这与混合法非常相似,唯一的区别在于数据划分的方式。
尽管这个想法源于1992年的Wolpert,但Leo Breiman在1996年发表的论文《堆叠回归》(Stacked Regressions)中将其形式化为当前使用的形式。2007年,M. J. Van der Laan等人建立了该技术的理论基础,并证明了这个元模型至少与基础学习器一样好,甚至更好。
参考文献检查:
Leo Breiman(1996年)和Mark J. Van der Laan等人(2007年)的研究论文分别被引用在参考文献部分的2和3中。
这是一种在Kaggle等机器学习竞赛中非常流行的技术,被机器学习从业者视为一门秘术。我们还讨论了一些其他技术,如bagging和boosting,它们将基学习器组合成更强大的模型。但这些技术要求基学习器是弱学习器。而stacking的不同之处在于,它试图组合一组多样化的强学习器。
stacking背后的直觉是,不同的模型或函数族对输出函数的学习方式略有不同,从而捕捉到问题的不同性质。例如,一个模型可能很好地捕捉了季节性,而另一个模型可能更好地捕捉了与某个外生变量的特定交互。stacking模型能够将这些基模型组合起来,学习到针对季节性问题参考前者模型、针对交互问题参考后者模型。这是通过让元模型学习基模型的预测来实现的。但为了防止数据泄露从而避免过拟合,元模型应该基于样本外预测进行训练。目前这种技术有两个小变体——stacking和blending。
Stacking 是指元模型在整个训练数据集上训练,但使用样本外预测。涉及stacking的步骤如下:
- 将训练数据集分成k份。
- 迭代地在k-1份上训练基模型,在第kth份上进行预测,并保存预测值。一旦完成这一步,我们就得到了所有基模型对训练数据集的样本外预测。
- 在这些预测上训练一个元模型。
Blending 与此类似,但在生成样本外预测的方式上略有不同。blending涉及的步骤如下:
- 将训练数据集分成两部分——训练集和保留集。
- 在训练集上训练基模型,并在保留集上进行预测。
- 在验证集上训练元模型,以基模型的预测作为特征。
直观上看,我们可以发现stacking可能效果更好,因为它使用了更大的数据集(通常是全部训练数据)作为样本外预测,因此元模型可能更具泛化能力。但有一个注意事项:我们假设整个训练数据是独立同分布(iid)。这在时间序列中通常很难满足,因为数据生成过程随时可能变化(无论是渐变的还是剧烈的)。如果我们知道数据分布随时间发生了显著变化,那么使用保留期(通常是数据集最近的部分)进行blending会更好,因为元模型只在最新数据上学习,从而尊重了数据分布的时间变化。
我们可以包含的基模型数量没有限制,但通常存在一个平台期,超过这个点后新增模型对stacked组合的贡献就不大了。我们还可以添加多层stacking。例如,假设有四个基学习器:B1、B2、B3和B4。我们还在基模型上训练了两个元模型M1和M2。现在,我们可以训练第二层元模型M,它在M1和M2的输出上训练,并将其作为最终预测。我们可以使用pystacknet Python库(https://github.com/h2oai/pystacknet),它是旧库stacknet的Python实现,可以轻松创建多层(或单层)stacked组合。
另一个需要牢记的关键点是通常用作元模型的模型类型。假设大部分学习已由基础模型完成,这些基础模型是用于预测模式的多维数据。因此,元模型通常是简单的模型,如线性回归、决策树,甚至是深度比基础模型低得多的随机森林。另一种思考方式是从偏差和方差的角度。堆叠可能会过拟合训练集或保留集,而通过包含更大灵活性或表达能力的模型族,我们正在助长这种过拟合。进一步阅读部分包含了一些链接,从一般机器学习的角度解释了不同的堆叠技术。
现在,让我们快速看看如何在数据集中使用它:
from sklearn.linear_model import LinearRegression
stacking_model = LinearRegression()
# ensemble_forecasts is the list of candidates
stacking_model.fit(
pred_wide_val[ensemble_forecasts], pred_wide_val["energy_consumption"]
)
pred_wide_test["linear_reg_blending"] = stacking_model.predict(
pred_wide_test[ensemble_forecasts]
)
这将把线性回归的混合预测保存为linear_reg_blending。我们可以使用相同的代码,但交换模型来尝试其他模型。
最佳实践:
当有许多基础模型并且我们也想进行隐式基础模型选择时,我们可以选择一种正则化线性模型,如Ridge或Lasso回归。Breiman在他的原始论文stacked regressions中提出使用具有正系数且无截距的线性回归作为元模型。他认为这提供了理论保证,即堆叠模型至少与任何最佳单个模型一样好。但在实践中,我们在实验时可以放宽这些假设。无截距的非负回归与我们之前讨论的最优加权集成非常接近。最后,如果我们评估多个堆叠模型以选择哪个效果好,我们应该要么使用单独的验证数据集(而不是一个,train-validation-test分割,我们可以使用一个train-validation-validation_meta-test分割)或使用交叉验证估计。如果我们只选择在测试数据集上表现最好的堆叠模型,我们就是在过拟合测试数据集。
现在,让我们看看混合模型在测试数据上的表现:

图9.10:混合模型的聚合指标
在这里,我们可以看到简单的线性回归学习到了一个元模型,其性能远远优于我们任何平均集成方法。而Huber回归(一种直接优化MAE的方法)在MAE基准上的表现要好得多。但请记住,这并非普遍适用,必须针对你遇到的每个问题进行评估。选择优化目标和用于组合的模型会带来很大的差异。而且,简单的平均集成通常是组合模型的一个非常强大的基准。
Huber回归是线性回归的另一个版本(如Ridge和Lasso),其损失函数是平方损失(用于普通线性回归)和绝对损失(用于L1方法)的组合。对于小残差,它表现为平方损失;对于大残差,它表现为绝对损失。这使得它对异常值不那么敏感。Scikit-Learn有HuberRegressor(https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.HuberRegressor.html),实现了这一点。
额外阅读:
还有其他更具创新性的方法来组合基础预测。这是一个活跃的研究领域。进一步阅读部分包含两个非常相似的想法。基于特征的预测模型平均(FFORMA)从时间序列中提取一组统计特征,并用它来训练一个机器学习模型,该模型预测基础预测应如何组合的权重。另一种技术(用于快速和可扩展时间序列超参数调优的自监督学习),来自Facebook(Meta)研究院,训练了一个分类器,根据从时间序列中提取的一组统计特征来预测哪个基础学习器表现最佳。
小结
延续上一章一系列实践课程的势头,我们又完成了一堂动手实践课。在本章中,我们利用上一章的不同机器学习模型生成了预测。我们学习了如何将这些不同的预测组合成一个比任何单一模型表现更优的预测。然后,我们探索了组合优化和堆叠/混合等概念,以取得最先进的成果。
在下一章中,我们将开始讨论全局预测模型,并探索策略、特征工程等,以实现此类建模。
参考文献
本章提供了以下参考文献:
- David S. Johnson, Cecilia R. Aragon, Lyle A. McGeoch, and Catherine Schevon (1989), 通过模拟退火进行优化:实验评估;第一部分,图划分。《运筹学》,1989,第37卷,第6期,865-892:http://dx.doi.org/10.1287/opre.37.6.865
- L. Breiman (1996),堆叠回归。《机器学习》24,49–64:https://doi.org/10.1007/BF00117832
- Mark J. van der Laan; Eric C.Polley; and Alan E.Hubbard (2007), Super Learner。U.C. Berkeley Division of Biostatistics Working Paper Series. Working Paper 222:https://biostats.bepress.com/ucbbiostat/paper222
延伸阅读
要了解更多关于本章涉及的主题,请参阅以下资源:
- Kaggle用户实践模型堆叠指南,作者Ha Nguyen:https://datasciblog.github.io/2016/12/27/a-kagglers-guide-to-model-stacking-in-practice/
- Kai Ming Ting and Ian H. Witten (1997), 堆栈泛化:何时有效?:https://www.ijcai.org/Proceedings/97-2/Papers/011.pdf
- Pablo Montero-Manso、George Athanasopoulos、Rob J. Hyndman、Thiyanga S. Talagala(2020),FFORMA:基于特征的预测模型平均。《国际预测杂志》,第36卷第1期:https://robjhyndman.com/papers/fforma.pdf
- Peiyi Zhang等(2021),用于快速且可扩展的时间序列超参数调优的自监督学习:https://www.ijcai.org/Proceedings/97-2/Papers/011.pdf
- 局部最优与全局最优:https://www.mathworks.com/help/optim/ug/local-vs-global-optima.html