我们在之前的章节中已经介绍了许多不同的单体模型,例如Logistic Regression, SVM, 决策树等等。然而在真正的实践中,单体模型由于各种因素,例如过拟合,欠拟合,数据噪声等等,往往无法达到理想的性能。

集成学习(Ensemble Learning) 的核心逻辑是,通过将不同的分类器进行组合来形成一个更强大的分类器(元分类器, Meta-Classifier),从而获得比任何单个分类器都更好的泛化性能。

简单来说,这就是一种集体智慧的集中体现。即使某个单体模型在特定数据分布上表现欠佳,但通过集成学习,我们将这些模型集思广益,战略性地结合这些模型的预测结果,我们最终能够得到一个更加准确且更稳健地预测结果。

在本章中,我们会介绍三种核心技术:

  • Majority Voting:多数投票是最简单最直观的集成方案,它通过统计多个分类器的预测结果来决定最终的分类结果

  • Bagging:Bagging(Bootstrap Aggregating)通过从原始数据集中随机采样生成多个子集,然后在每个子集上训练一个分类器,最后通过平均或投票的方式结合这些分类器的预测结果。

  • Boosting:Boosting通过迭代地训练一系列弱分类器,每个分类器都试图纠正前一个分类器的错误。最终的预测结果是这些弱分类器的加权组合。


Voting

我们首先要理解不同投票形式的区别:

pasted-image-1776603663206.webp

首先假设我们有这样十个分类器,每一个分类器都会输出二元分类的结果(0或1):

最理想的情况下,所有的分类器都能得出完全相同的结论,这种情况叫做 Unanimous Voting。这意味着所有的分类器都一致达成了强烈共识,而我们一般会认为这个结论是非常可靠的。但是这种情况在实际应用中非常罕见,因为不同的分类器可能会有不同的偏好和错误倾向。

因此我们更多地以来赖于 Majority Voting,即多数投票。这种二分分类的情况下,最终的分类结果是由大多数分类器的预测结果决定的,也就是说选出来的类别必须超过50%的选票。比如在上图中,深色圆圈的预测结果大于50%,因此最终的分类结果是1。

但是如果我们的任务是多分类问题,那么想要让某个决策大于50%就变得非常困难了。在这种情况下,我们通常会采用 Plurality Voting,即相对多数投票。也就是说我们不要求某个类别必须超过50%的选票,而是只要它的选票数比其他类别多就可以了。比如在上图最后一行,虽然没有任何一个类别的选票超过50%,但是类别1的选票数最多,因此最终的分类结果是1。


Voting集成管线

那么理论上听起来很棒,我们该如何在实践中实现Voting集成呢?

pasted-image-1776604015289.webp

上图展示了一个典型的Voting集成管线。我们会从训练集出发,并行训练出多个不同的分类器,我们叫它们为 Base Classifiers,也就是基础分类器,也就是图中的 ${C_1, C_2, \ldots, C_m}$。

当一个新的样本进入时,每一个基础分类器都会对这个样本进行预测,输出一个类别标签。随后,这些预测结果就会被送入一个集中的Voting机制进行汇总,最终集思广益产生一个更加稳健的最终预测。

还记得在之前的章节中我们提过随机森林吗?随机森林就是一个典型的Voting集成模型,我们构建多个决策树作为基础分类器,然后通过多数投票的方式来决定最终的分类结果。


Voting的正确性

那么我们为什么一定说Voting能够提升模型的性能?为什么这种方式一定能够保证比单体模型更好呢?我们引入一个概念,叫做 集成误差(Ensemble Error),它是指集成模型的预测结果与真实标签之间的误差率。

假设我们在某个集成模型内雇佣了 $n$ 个基础分类器,每个分类器都会有几率犯错,我们记作 $\epsilon$。如果这些分类器是相互独立的(实际上确实是,它们相互各不商量,各凭本事),那么只有当超过绝大多数的分类器都犯错时,集成模型才会犯错。

通过二项分布的推导,我们发现,只要每个分类器的表现能够比随机猜测更好(即 $\epsilon < 0.5$),那么随着基础分类器数量的增加,集成模型的误差率会迅速下降,趋近于0。这就是为什么我们说Voting能够提升模型性能的原因。

所以最后说回来,这个结论的前提是基础分类器之间必须是相互独立的,而且它们的错误不能高度相关。如果所有这些模型都犯了一样的错误,那么人多实际上并不会带来任何好处,反而可能会加剧错误的影响。


!?软硬投票?!

但是你就会想了,你说Voting有不同的机制,那么一共有哪些我们比较常用的Voting机制呢?

首先最简单的,也是我们之前提到的,最原始的 Majority Voting*:统计所有模型的预测结果,选出得票数最多的类别作为最终的预测结果。

本质上可以写成:

$$\hat{y} = mode {C_1(x), C_2(x), \ldots, C_m(x)}$$

但是在现实中,并非所有模型的意见权重都应该是一样的。如果我们知道 $C_3$ 比 $C_1$ 和 $C_2$ 更加可靠,那么我们就应该赋予 $C_3$ 更高的权重 $w_j$,使得它在投票过程中对最终结果的贡献更大。我们管这个叫做 Weighted Majority Voting,即加权多数投票:

$$\hat{y} = arg \max_{i} \sum_{j=1}^m w_j \chi_A(C_j(x) = i)$$

到这里,最后的预测结果 $\hat{y}$ 就不是简单的点名计票了,而是每个类别加权总票数的最大值。


但是这仍然属于硬投票的一种变体,而硬头票会带来一些问题。就比如即使我们用了加权多数投票,万一你加权的专家也在胡扯,那岂不是会错的更离谱了?由于我们投票是要保证能够尽可能贴近事实,因此当我们处理最后的Voting的时候,也可以考虑按照事实的概率来进行投票,这就是 Soft Voting,即软投票。

在软投票中,我们不仅仅看每个分类器的最终预测结果,而是看每个模型的输出的“置信程度”,也就是概率值。在scikit-learn中,许多分类器都提供了 predict_proba 方法,可以输出每个类别的预测概率。我们可以将这些概率进行加权平均,最终选择概率最高的类别作为最终的预测结果。

也就是说每个模型会说:“我有$x$的把握这玩意是类别A,$y$的把握是类别B...”,然后集成模型做的事,就是把各个类别的加权平均概率计算出来,最后选定平均概率最高的类别作为最终预测。

这种方式能够在保证专家意见的同时,在专家模型肯定的时候拥有更大的影响力,而在专家模型不确定的时候则会降低它的影响力,从而提高整体的预测性能。

$$\hat{y} = arg \max_{i} \sum_{j=1}^m w_j p_ij$$


调优

我们知道单模型的调优方法,但是对于集成模型来说,我们该如何调优?

对于集成模型调优的方法还要看你使用的具体ML框架,但是主要的方法思路大差不差。在scikit-learn中,我们可以使用之前提到过的网格搜索 GridSearchCV 来进行集成模型的调优,但是我们要在这个基础上多做一步:

由于scikit-learn中单模型方法都包含 get_params 方法,所以对于网格搜索来说,获取模型的超参数比较直接。但是对于集成模型来说,虽然没有直接的 get_params 方法,但是换个思路,你里面使用的每个基础分类器都是一个单模型,而单模型都有 get_params 方法,所以我们可以通过访问集成模型内部的基础分类器来获取它们的超参数。

当外部调优器询问集成模型:“有哪些参数可以用来调整”的时候,集成模型要做的事就是不仅报出自己的参数,还要递归地报出它手下每一个分类器的参数,并给它们贴上唯一标签。

举个例子,如果我们有一个集成模型叫做 VotingClassifier,它内部包含了三个基础分类器:LogisticRegressionSVCRandomForestClassifier。当我们调用 get_params 方法时,集成模型可以为每一个子模型按照双下划线的格式分配唯一标签。

例如随机森林的最大深度参数 max_depthrandomforestclassifier__max_depth,为SVC的核函数参数 kernel 贴上标签:svc__kernel,为Logistic Regression的正则化参数 C 贴上标签:logisticregression__C


Bagging

实际上Bagging的全称叫 Bootstrap Aggregating

如果你想训练一个专家团队,但是所有人读的都是一模一样的教科书,那么到最后培养出来的这些专家思维方式很可能会高度趋同,而且一旦遇到书中没讲透的偏难怪题,它们非常有可能犯同样的错误。而Bagging做的就是为每个专家提供不同的教材,让它们在不同的视角下学习,从而培养出一支多样化的专家团队。

Bagging的核心手段叫做 Bootstrap Sampling,叫做自助抽样法。如果原始数据集有 $n$ 个样本,那么与其说我们直接将它们分配给模型做训练,我们从这 $n$ 个样本中进行有放回的随机抽样,生成 $m$ 个新的训练子集,每个子集也包含 $n$ 个样本。由于是有放回的抽样,所以每个子集中可能会有重复的样本,而有些样本可能根本没有被抽到。

这样的做法的目的是什么呢?虽然每个模型使用到的样本总量是一样的,但由于训练数据构成的微小差异,每一个模型最终学到的决策逻辑都可能略有不同。就好比我们每个人生长环境中的所有变量都不完全一样,因此我们长大后也成为了个性迥异的人。

但是不同人格的人往往能够在不同的情境下发挥不同的优势,这样的团队就能够在面对各种各样的挑战时表现得更加出色。在机器学习中也是一样:当我们把不同的模型使用Voting聚合起来的之后,某些模型的过拟合就会被其他模型的正确预测抵消掉。

因此,Bagging能够显著提升那些“不稳定模型”的准确性,例如没有剪枝的决策树。说到底Bagging的核心威力在于能够降低模型的方差(Variance),能够有效一直那些对数据扰动非常敏感的“不稳定模型”。

但是Bagging并不适合所有模型,因为Bagging无法有效降低模型的偏差(Bias)。如果一个模型本身的假设空间过于有限,逻辑过于简单,大脑褶皱过于平坦,那么即便使用的Bagging组合再多,也没法捕捉到复杂的非线性数据规律。这就是为什么我们在实践中通常将Bagging和那些高方差,低偏差模型组合使用的原因,例如决策树。


OOB评估

有趣的是,如果我们动用一些数学逻辑:

假设原始数据集 $D$ 的大小为 $n$ 个样本。在每一轮的Bagging中,我们通过有放回随机抽样来构建一个新的训练子集 $D_i$,这个子集的大小也是 $n$ 个样本。那么我们可以说,对于任何一个特定样本,它在一次抽样中不被选中的概率就是 $(1 - 1/n)$。当 $n$ 趋于无穷大时,一个样本在构建 $D_i$时从未被选中的概率会收敛于:

$$\lim_{n \to \infty} (1 - 1/n)^n = e^{-1} \approx 0.368$$

也就是说,大约有36.8%的样本在每次抽样中都没有被选中,这些数据一定是某次训练中模型从来没见过的。我们叫这些数据为 Out-of-Bag (OOB) Samples,也就是袋外样本。

这些样本简直就是天然的测试集啊!我们可以利用这些OOB样本来评估模型的性能,而不需要额外划分出一个独立的测试集。对于每个样本,我们可以统计它在多少次抽样中被选中,并且在这些抽样中对应的模型对它的预测结果是什么。通过比较这些预测结果与真实标签,我们就可以计算出模型的OOB误差率。


Boosting

你可能联想到了Gradient Boosting,XGBoost,LightGBM等一众Boosting算法,但是在我们正式介绍Boosting之前,我们先来看看Boosting的核心思想是什么。

在Boosting这里,我们不再并行训练一堆模型,而是采用串联的思路训练。

在这个串联链路上,第一个“弱模型”会先对数据集进行学习,尝试解决问题,但是犯错肯定是必然的。但与其我们推倒重来,我们会引入第二个模型来盯着第一个模型的错题集去加强训练,以此类推。这意味着后续的每一个模型都在尽力弥补前面模型的短板,而这种接力的过程就像进化一样,能够从一开始的弱模型逐步演化成一个强大的模型。


Weighting / AdaBoost

由于我们需要来找出那些前面模型的错题集,Boosting的核心是通过训练集的权重分配来实现精准挑错。

在最原始的Boosting算法中,我们会首先抽取一部分数据训练出第一个弱学习器 $C_1$,然后为了训练第二个模型 $C_2$,我们不只是简单的随机抽样,而是可以从训练集中挑选出50%之前被 $C_1$ 预测错了的样本,让 $C_2$ 重点关注这些样本进行训练。接着我们再训练第三个模型 $C_3$,这次我们会挑选那些让 $C_1$ $C_2$ 产生分歧的样本交给 $C_3$ 去专项训练。到了最后,通过这三位专家进行 Majority Voting,我们就能够得到一个非常强大的集成模型。


上面的早期Boosting算法虽然能够提升模型性能,但它的效率并不高,因为每一轮都需要重新抽样和训练一个新的模型。后来,AdaBoost(Adaptive Boosting)算法的出现极大地优化了这个过程。

在AdaBoost的每一轮迭代中,训练集中的每一个样本实际上并非平等,而是被赋予了一个权重,这个权重反映了这个样本的重要程度。初始时,所有样本的权重都是相等的,然而当第一个模型预测错了某个样本时,我们会增加这个样本的权重,这样在下一轮训练中,模型就会更加关注这个样本,从而有更大的机会纠正之前的错误。

下面这个图应该能比较清晰的展示这个权重调整的过程:

pasted-image-1776615903021.webp

第一个图展示了第一个模型的首次预测。你会发现左边下面两个蓝色圆球的位置错了,因此在第二个图中你会发现它们变大了。这代表着我们增加了它们的权重,让第二个模型在训练的时候更加关注它们。将这个过程以此类推,到最后我们能够将所有的错题都纠正掉。


AdaBoost的流程

我们不妨用伪代码来看看AdaBoost是如何运作的。首先:

  1. 我们将初始权重向量 $\mathbf{w}$ 初始化为均匀分布,即 $\sum_{i} w_i = 1$

  2. 对于每一轮迭代 $j = 1, 2, \ldots, M$,我们:

    a. 使用权重向量 $\mathbf{w}$ 来训练一个弱分类器 $C_j = train(\mathbf{X}, \mathbf{y}, \mathbf{w})$。

    其中, $\mathbf{X}$ 是训练数据,$\mathbf{y}$ 是对应的标签,$\mathbf{w}$ 是当前的权重分布。

    b. 使用这个模型来做一个预测看看:

    $$\hat{\mathbf{y}} = predict(C_j, \mathbf{X})$$

    c. 随后我们要知道模型错了哪些。我们可以通过计算加权错误率 $\epsilon_j$ 来衡量模型的表现:

    $$\epsilon = \mathbf{w} \cdot (\hat{y} \neq \mathbf{y})$$

    计算过程中,我们会将权重向量 $\mathbf{w}$ 与一个指示函数进行点积,这个指示函数在模型预测错误的样本位置上为1,在正确的位置上为0。这样我们就能够得到一个加权错误率 $\epsilon_j$了。

    d. 接下来我们要计算这个模型的权重 $\alpha_j$,它反映了这个模型在集成中的重要程度:

    $$\alpha_j = \frac{1}{2} \log \left( \frac{1 - \epsilon_j}{\epsilon_j} \right)$$

    如果模型的错误率 $\epsilon_j$ 越小,那么 $\alpha_j$ 就越大,说明这个模型在集成中的话语权更重。反之,如果模型的错误率接近0.5,那么 $\alpha_j$ 就会接近0,说明这个模型对集成的贡献非常有限。

    e. 知道模型话语权后,我们就可以根据模型的表现来更新样本的权重了。更新权重的规则是:

     1. 分类正确的样本
    
         对于分类正确的样本,我们定义权重更新公式为:
    
         $$w_i := w_i \cdot e^{-\alpha_j}$$
    
         由于指数项为负,权重会迅速变小。这意味着如果模型已经学会了这些点,那么下一轮就不需要花太多精力在上面了。
    
     2. 分类错误的样本
    
         对于分类错误的样本,我们定义权重更新公式为:
    
         $$w_i := w_i \cdot e^{\alpha_j}$$
    
         反之,权重会按照指数级增长,那么在酿成大祸之前这些样本必须要被纠正掉。
    

    f. 最后我们需要对权重向量进行归一化处理,确保它们的总和为1:

    $$\mathbf{w} := \frac{\mathbf{w}}{\sum_{i} w_i}$$

  3. 当我们完成了所有的迭代之后,我们就可以得到一个最终的集成模型了。对于一个新的输入样本 $\mathbf{x}$,我们可以通过加权投票的方式来得到最终的预测结果:

$$\hat{y} = (\sum_j (\alpha_j \times predict(C_j, \mathbf{x})) > 0)$$

或者换种写法也行:

$$\hat{y} = sign \left( \sum_{j=1}^M \alpha_j C_j(\mathbf{x}) \right)$$

p.s. 你可能发现有些地方用了点乘,有些地方是乘号。具体来说,我们使用乘号 $\times$ 来表示样本与样本之间的乘积关系,而使用点乘 $\cdot$ 来专门表示两个向量之间的乘积关系。

至此,你就已经学会如何更新你的AdaBoost模型了。你可以通过调整迭代次数 $M$,或者选择不同的弱分类器来进一步提升模型的性能。


值得一提,AdaBoost虽然是一个非常强大的集成算法,但它也有一些局限性。首先,AdaBoost对异常值非常敏感,因为它会不断增加那些被错误分类的样本的权重,如果数据中存在噪声或者异常值,这些样本可能会获得过高的权重,从而导致模型过拟合。

其次,AdaBoost通常适用于二分类问题,对于多分类问题的处理相对复杂,需要进行一些改进和调整。