5.3 集成学习 - Boosting与AdaBoost
1 Boosting方法的基本思想
在集成学习的“弱分类器集成”领域,除了降低方差来降低整体泛化误差的装袋法Bagging,还有专注于降低整体偏差来降低泛化误差的提升法Boosting。相比起操作简单、大道至简的Bagging算法,Boosting算法在操作和原理上的难度都更大,但由于专注于偏差降低,Boosting算法们在模型效果方面的突出表现制霸整个弱分类器集成的领域。当代知名的Boosting算法当中,Xgboost,LightGBM与Catboost都是机器学习领域最强大的强学习器,Boosting毫无疑问是当代机器学习领域最具统治力的算法领域。
1.1 Boosting PK Bagging
装袋法 Bagging | 提升法 Boosting | |
---|---|---|
弱评估器 | 相互独立,并行构建 | 相互关联,按顺序依次构建 先建弱分类器的预测效果影响后续模型的建立 |
建树前的抽样方式 | 样本有放回抽样 特征无放回抽样 |
样本有放回抽样 特征无放回抽样 先建弱分类器的预测效果可能影响抽样细节 |
集成的结果 | 回归平均 分类众数 |
每个算法具有自己独特的规则,一般来说: (1) 表现为某种分数的加权平均 (2) 使用输出函数 |
目标 | 降低方差 提高模型整体的稳定性来提升泛化能力 本质是从“平均”这一数学行为中获利 |
降低偏差 提高模型整体的精确度来提升泛化能力 相信众多弱分类器叠加后可以等同于强学习器 |
单个评估器容易 过拟合的时候 |
具有一定的抗过拟合能力 | 具有一定的抗过拟合能力 |
单个评估器的效力 比较弱的时候 |
可能失效 | 大概率会提升模型表现 |
代表算法 | 随机森林 | 梯度提升树,Adaboost |
在以随机森林为代表的Bagging算法中,我们一次性建立多个平行独立的弱评估器,并让所有评估器并行运算。在Boosting集成算法当中,我们逐一建立多个弱评估器(基本是决策树),并且下一个弱评估器的建立方式依赖于上一个弱评估器的评估结果,最终综合多个弱评估器的结果进行输出,因此Boosting算法中的弱评估器之间不仅不是相互独立的、反而是强相关的,同时Boosting算法也不依赖于弱分类器之间的独立性来提升结果,这是Boosting与Bagging的一大差别。 如果说Bagging不同算法之间的核心区别在于靠以不同方式实现“独立性”(随机性),那Boosting的不同算法之间的核心区别就在于上一个弱评估器的评估结果具体如何影响下一个弱评估器的建立过程(就是后面学习Boosting算法最大的区别)。
与Bagging算法中统一的回归求平均、分类少数服从多数的输出不同,Boosting算法在结果输出方面表现得十分多样。早期的Boosting算法的输出一般是最后一个弱评估器的输出,当代Boosting算法的输出都会考虑整个集成模型中全部的弱评估器。一般来说,每个Boosting算法会其以独特的规则自定义集成输出的具体形式,但对大部分算法而言,集成算法的输出结果往往是关于弱评估器的某种结果的加权平均,其中权重的求解是boosting领域中非常关键的步骤。
1.2 Boosting算法的基本元素与基本流程
基于上面所明确的“降低偏差”、“逐一建树”、以及“以独特规则输出结果”的三大特色,我们可以确立任意boosting算法的三大基本元素以及boosting算法自适应建模的基本流程:
- 损失函数\(L(x,y)\) :用以衡量模型预测结果与真实结果的差异
- 弱评估器\(f(x)\) :(一般为)决策树,不同的boosting算法使用不同的建树过程
- 综合集成结果\(H(x)\):即集成算法具体如何输出集成结果
这三大元素将会贯穿所有我们即将学习的boosting算法,我们会发现几乎所有boosting算法的原理都围绕这三大元素构建。在此三大要素基础上,所有boosting算法都遵循以下流程进行建模:
依据上一个弱评估器\(f(x)_{t-1}\)的结果,计算损失函数\(L(x,y)\),
并使用\(L(x,y)\)自适应地影响下一个弱评估器\(f(x)_t\)的构建。
集成模型输出的结果,受到整体所有弱评估器\(f(x)_0\) ~ \(f(x)_T\)的影响。
并使用\(L(x,y)\)自适应地影响下一个弱评估器\(f(x)_t\)的构建。
集成模型输出的结果,受到整体所有弱评估器\(f(x)_0\) ~ \(f(x)_T\)的影响。
正如之前所言,Boosting算法之间的不同之处就在于使用不同的方式来影响后续评估器的构建。无论boosting算法表现出复杂或简单的流程,其核心思想一定是围绕上面这个流程不变的。
1.3 sklearn中的boosting算法
在sklearn当sklearn中的boosting算法中,我们可以接触到数个Boosting集成算法,包括Boosting入门算法AdaBoost,性能最稳定、奠定了整个Boosting效果基础的梯度提升树GBDT(Gradient Boosting Decision Tree),以及近几年才逐渐被验证有效的直方提升树(Hist Gradient Boosting Tree)。
在过去5年之间,除了sklearn,研究者们还创造了大量基于GBDT进行改造的提升类算法,这些算法大多需要从第三方库进行调用,例如极限提升树XGBoost(Extreme Gradient Boosting Tree),轻量梯度提升树LightGBM(Light Gradiant Boosting Machine),以及离散提升树CatBoost(Categorial Boosting Tree)。
Boosting算法 | 库 | 集成类 |
---|---|---|
ADB分类 | sklearn | AdaBoostClassifer |
ADB回归 | sklearn | AdaBoostRegressor |
梯度提升树分类 | sklearn | GradientBoostingClassifier |
梯度提升树回归 | sklearn | GradientBoostingRegressor |
直方提升树分类 | sklearn | HistGraidientBoostingClassifier |
直方提升树回归 | sklearn | HistGraidientBoostingRegressor |
极限提升树 | 第三方库xgboost | xgboost.train() |
轻量梯度提升树 | 第三方库lightgbm | lightgbm.train() |
离散提升树 | 第三方库catboost | catboost.train() |
2 AdaBoost
2.1 介绍
AdaBoost(Adaptive Boosting,自适应提升法)是当代boosting领域的开山鼻祖,它虽然不是首个实践boosting思想算法,却是首个成功将boosting思想发扬光大的算法。它的主要贡献在于实现了两个变化:
1、首次实现根据之前弱评估器的结果自适应地影响后续建模过程
2、在Boosting算法中,首次实现考虑全部弱评估器结果的输出方式,就是在之前人们认为树是一颗比一颗好,最好的应该是最后一棵树输出的结果
作为开山算法,AdaBoost的构筑过程非常简单:首先,在全样本上建立一棵决策树,根据该决策树预测的结果和损失函数值,增加被预测错误的样本在数据集中的样本权重,并让加权后的数据集被用于训练下一棵决策树。这个过程相当于有意地加重“难以被分类正确的样本”的权重,同时降低“容易被分类正确的样本”的权重,而将后续要建立的弱评估器的注意力引导到难以被分类正确的样本上。所以越往后的树越容易分类那些那分类的样本,越是会忽略那些容易分类的样本。
就是一个样本预测错误的次数越多,他被加权的次数就越多。
在该过程中,上一棵决策树的的结果通过影响样本权重、即影响数据分布来影响下一棵决策树的建立,整个过程是自适应的。当全部弱评估器都被建立后,集成算法的输出 𝐻(𝑥) 等于所有弱评估器输出值的加权平均,加权所用的权重也是在建树过程中被自适应地计算出来的。
需要注意的是,虽然最初的原理较为简单,但近年来AdaBoost在已经发展出多个升级的版本(比如,在建立每棵树之前,允许随机抽样特征,这使得Boosting中的决策树行为更加接近Bagging中的决策树),而sklearn中使用了这些升级后的版本进行实现。
如何理解改变样本的权值?
举例:
初始有3个样本,即其所占比例\(D_1\)
样本 | (\(x_1\),\(y_1\)) | (\(x_2\),\(y_2\)) | (\(x_3\),\(y_3\)) |
---|---|---|---|
所占比例 | $$\frac{1}{3}$$ | $$\frac{1}{3}$$ | $$\frac{1}{3}$$ |
经过一轮学习后,\(G_1\)分类器成功将\((x_2,y_2)(x_3,y_3)\)分类成功,所以我们需要将这两个样本的权重降低,提高第一个样本的权值,即下面表格:\(D_2\)
样本 | (\(x_1\),\(y_1\)) | (\(x_2\),\(y_2\)) | (\(x_3\),\(y_3\)) |
---|---|---|---|
所占比例 | $$\frac{2}{3}$$ | $$\frac{1}{6}$$ | $$\frac{1}{6}$$ |
所以增大权值相当于将初始样本中的错误分类的数量变多了,变成了这样:
2.加权多数表决
1.加大分类误差率小的弱分类器的权值\(\alpha_m\),使其在表决中起较大作用;
2.减少分类误差率大的弱分类器的权值\(\alpha_m\)使其在表决中起较小作用。
2.2 API
在sklearn中,AdaBoost既可以实现分类也可以实现回归,我们使用如下两个类来调用它们:
sklearn.ensemble.AdaBoostClassifier(base_estimator=None, *, n_estimators=50, learning_rate=1.0, algorithm='SAMME.R', random_state=None)
sklearn.ensemble.AdaBoostRegressor(base_estimator=None, *, n_estimators=50, learning_rate=1.0, loss='linear', random_state=None)
不难发现,AdaBoost的参数非常非常少,在调用AdaBoost时我们甚至无需理解AdaBoost的具体求解过程。同时,ADB分类器与ADB回归器的参数也高度一致。在课程当中,我们将重点Boosting算法独有的参数,以及ADB分类与ADB回归中表现不一致的参数。
参数 | 参数含义 |
---|---|
base_estimator | 弱评估器 |
n_estimators | 集成算法中弱评估器的数量 |
learning_rate | 迭代中所使用的学习率 |
algorithm(分类器专属) | 用于指定分类ADB中使用的具体实现方法 |
loss(回归器专属) | 用于指定回归ADB中使用的损失函数 |
random_state | 用于控制每次建树之前随机抽样过程的随机数种子 |
2.3 AdaBoost的基本参数与损失函数
这次有两个例子一个分类,一个回归。
分类的例子是手写数字的识别,然后这个输入是一个数据集的特征是一个(1796,64),这个64是8*8的一个图片经过扫描之后得到的一个64的像素值。我们最后预测的是一个10分类,最后的结果是0-9。
回归的例子就是那个房价预测
from sklearn.ensemble import AdaBoostClassifier as ABC
from sklearn.ensemble import AdaBoostRegressor as ABR
from sklearn.tree import DecisionTreeClassifier as DTC
from sklearn.tree import DecisionTreeRegressor as DTR
from sklearn.datasets import load_digits
导入数据
#用于分类的数据
data_c = load_digits()
X_c = data_c.data
y_c = data_c.target
X_c.shape
X_c
np.unique(y_c) #手写数字数据集,10分类
#用于回归的数据
data_r = pd.read_csv(r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\House Price\train_encode.csv",index_col=0)
X_g = data_r.iloc[:,:-1]
y_g = data_r.iloc[:,-1]
X_g.shape
X_g.head()
- 参数
base_estimator
属性base_estimator_
与estimators_
base_estimator
是规定AdaBoost中使用弱评估器的参数。与对弱评估器有严格要求的Bagging算法不同,boosting算法通过降低偏差来降低整体泛化误差,因此可以使用任意弱评估器,且这些弱评估器往往被假设成非常弱小的评估器。当然了,默认的弱评估器还是决策树。在sklearn中,ADB分类器的默认弱评估器是最大深度为1的“树桩”,ADB回归器的默认评估器是最大深度为3的“树苗”,弱评估器本身基本不具备判断能力。而回归器中树深更深是因为boosting算法中回归任务往往更加复杂。在传统ADB理论当中,一般认为AdaBoost中的弱分类器为最大深度为1的树桩,但现在我们也可以自定义某种弱评估器来进行输入。
当模型建好之后,我们可以使用属性base_estimator_
来查看当前弱评估器,同时也可以使用estimators_
来查看当前集成模型中所有弱评估器的情况:
#建立ADB回归器和分类器
clf = ABC(n_estimators=3).fit(X_c,y_c)
reg = ABR(n_estimators=3).fit(X_g,y_g)
clf.base_estimator_ #深度为1
reg.base_estimator_ #深度为3
reg.estimators_
当AdaBoost完成分类任务时,弱评估器是分类树,当AdaBoost完成回归任务时,弱评估器是回归树,这一点与之后的Boosting算法们有较大的区别。
- 自建弱评估器
在这里我们用决策树的分类器来作为这个弱评估器
base_estimator = DTC(max_depth=10,max_features=30)
clf = ABC(base_estimator = base_estimator, n_estimators=3).fit(X_c,y_c)
clf.base_estimator_
clf.estimators_
注意,为了保证集成算法中的树不一致,AdaBoost会默认消除我们填写在弱评估器中的random_state:
- 参数learning_rate(学习率)
在Boosting集成方法中,集成算法的输出\(H(x)\)往往都是多个弱评估器的输出结果的加权平均结果。但\(H(x)\)并不是在所有树建好之后才统一加权求解的,而是在算法逐渐建树的过程当中就随着迭代不断计算出来的。例如,对于样本\(x_i\),集成算法当中一共有\(T\)棵树(也就是参数n_estimators
的取值),现在正在建立第\(t\)个弱评估器,则第\(t\)个弱评估器上\(x_i\)的结果可以表示为\(f_t(x_i)\)。假设整个Boosting算法对样本\(x_i\)输出的结果为\(H(x_i)\),则该结果一般可以被表示为t=1~t=T过程当中,所有弱评估器结果的加权求和:
在这个一般过程中,每次将本轮建好的决策树加入之前的建树结果时,可以在权重\(\phi\)前面增加参数\(\color{red}\eta\),表示为第t棵树加入整体集成算法时的学习率,对标参数learning_rate
。
该学习率参数控制Boosting集成过程中\(H(x_i)\)的增长速度,是相当关键的参数。当学习率很大时,\(H(x_i)\)增长得更快,我们所需的n_estimators更少,当学习率较小时,\(H(x_i)\)增长较慢,我们所需的n_estimators就更多,因此boosting算法往往会需要在n_estimators与learning_rate当中做出权衡(以XGBoost算法为例)。
需要注意的是,以上式子为boosting算法中计算方式的一般规则,并不是具体到AdaBoost或任意Boosting集成算法的具体公式。
其实这一块就是这个:$$H_t(x_i) = H_{t-1}(x_i) + \boldsymbol{\color{red}\eta} \phi_tf_t(x_i)$$就是本次的$$H_t(x_i)$$要加上$$H_{t-1}(x_i)$$和这课数上学习的权值,但是我们可以设置一个\(\color{red}\eta\),来控制\(H(x_i)\)的增长速度。
2.4 参数algorithm与loss
参数algorithm
与loss
是boosting算法中非常常见的,分类器与回归器展示出不同参数的情况。正如之前提到的,虽然AdaBoost算法的原理简单,但是在近几年已经发展出了多种不同的算法实践手段,而参数algorithm与loss正是用来控制算法实践手段的关键参数,其中algorithm控制具体的实践算法,loss控制该实践算法中所使用的具体损失函数。
- algorithm
首先,参数algorithm
是针对分类器设置的参数,其中备选项有"SAMME"与"SAMME.R"两个字符串。这两个字符串分别代表了两种不同的、实现AdaBoost分类的手段:AdaBoost-SAMME与AdaBoost-SAMME.R。两者在数学流程上的区别并不大,只不过SAMME是基于算法输出的具体分类结果(例如-1,1,2)进行计算,而SAMME.R则是在SAMME基础上改进过后、基于弱分配器输出的概率值进行计算,两种方法都支持在AdaBoost上完成多分类任务,但SAMME.R往往能够得到更好的结果,因此sklearn中的默认值是SAMME.R,因此sklearn中默认可以输入的base_estimators也需要是能够输出预测概率的弱评估器。实际在预测时,AdaBoost输出的\(H(x)\)也针对于某一类别的概率。
需要注意的是,在分类器中,我们虽然被允许选择算法,却不被允许选择算法所使用的损失函数,这是因为SAMME与SAMME.R使用了相同的损失函数:二分类指数损失(Exponential Loss Function)与多分类指数损失(Multi-class Exponential loss function)。
二分类指数损失
其中y为真实分类,\(H^*(x)\)则是从集成算法输出的概率结果\(H(x)\)转换来的向量。转换规则如下:
在sklearn当中,由于\(H(x)\)是概率值,因此需要转换为\(H^*(x)\),如果在其他实现AdaBoost的算法库中,\(H(x)\)输出直接为预测类别,则可以不执行转换流程。
根据指数损失的特殊性质,二分类状况下的类别取值只能为-1或1,因此y yy的取值只能为-1或1。当算法预测正确时,\(yH^*(x)\)的符号为正,则在函数\(e^{-x}\)上损失很小。当算法预测错误时,\(yH^*(x)\)的符号为负,则在函数\(e^{-x}\)上损失较大。二分类指数损失是AdaBoost最经典的损失函数,它在数学推导上的有效性以及在实践过程中很强的指导性让其沿用至今。
多分类指数损失——
其中,\(K\)为总类别数,如四分类\([0,1,2,3]\)的情况时,\(K=4\),\(y^*\)与\(H^*(x)\)都是根据多分类具体情况、以及集成算法实际输出\(H(x)\)转化出的向量,其中\(y^{*1}\)与\(H^{*1}(x)\)的上标1都表示当前类别。在二分类算法中,算法会直接针对二分类中的其中一个类别输出概率,因为在二分类中\(P(Y=1)=1- P(Y=-1)\),所以只计算出一类的概率即可判断预测的标签。但在多分类算法中,算法必须针对所有可能的取值类型都输出概率,才能够从中找出最大概率所对应的预测标签。因此在集成算法中,我们对进行多分类预测时,会得到如下的表格:
每一行对应一个样本,每一列则对应该样本的预测标签为某一类别的概率,以上表格就是5个样本在10分类情况下得出的概率表格,而每一个样本的10个概率中,最大概率所对应的类别就是预测类别。而这一转换可以由函数argmax完成。argmax会取出最大值所对应的索引,刚好也就是最大概率所对应的预测标签。
np.argmax(pd.DataFrame(clf.predict_proba(X_c)).iloc[0,:])
#0
np.argmax(pd.DataFrame(clf.predict_proba(X_c)).iloc[1,:])
#7
np.argmax(pd.DataFrame(clf.predict_proba(X_c)).iloc[3,:])
#6
对一棵决策树我们会输出k个概率,对于boosting集成中的每一棵树,在任意样本上都会得到\(f^{c=0}(x)、f^{c=1}(x)、f^{c=2}(x)……\)数个不同的结果。在集成算法当中,每个样本在第t次建树过程中,都会生成针对于不同类别的结果:
因此,我们可以得到向量$[H0(x),H1(x),H2(x),...,Hk(x)],表示当前集成算法计算出的、针对多个类别的概率(也是对全部弱分类器输出的、针对多个类别的概率进行的加权求和)。针对该向量,一定可以得到向量中的一个最大值,该最大值所对应的标签类别就是多分类算法中的预测标签类别。根据该向量,以及指数损失的特性,我们规定:
其中,\(argmaxH(x)\)对应的是预测标签,\(k\)为所有预选标签类别。因此,假设在4分类情况下,集成算法针对样本\(i\)的各个分类输出的概率如下所示,则向量\(H^*(x)\)的取值如下所示:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
\(H^k_t(x_i)\) | 0.1 | 0.2 | 0.2 | 0.5 |
\(H^*(x)\) | -\(\frac{1}{3}\) | -\(\frac{1}{3}\) | -\(\frac{1}{3}\) | 1 |
其中3就是当前集成算法针对样本\(i\)预测的标签。
另外一方面,\({y^*}\)一般来说都是真实标签经过上述处理后的结果。同样是4分类器情况下,假设样本\(i\)的真实标签为2,则向量\(y^*\)的构成如下所示:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
\(y^*\) | -\(\frac{1}{3}\) | -\(\frac{1}{3}\) | 1 | -\(\frac{1}{3}\) |
用公式表示则有:
其中\(y_i\)为样本的真实标签,\(k\)为所有预选标签类别。不难发现,在此规则下,此时向量&y^*&以及向量\(H^*(x)\)的和永远是0,因为向量内部总是1与(K-1)个\(-\frac{1}{K-1}\)相加。
K = 4
y = [1,(-1/(K-1)),(-1/(K-1)),(-1/(K-1))]
int(sum(y))
#0
在多分类算法当中,我们常常求解类似于\(y^*\)或\(H^*(x)\)的向量,比如在softmax函数中,当预测值或真实值不等于k kk时,我们赋予的向量值为0,而不是\(-\frac{1}{K-1}\),softmax的一般规则:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
\(H^k_t(x_i)\) | 0.1 | 0.2 | 0.2 | 0.5 |
\(H^*(x)\) | 0 | 0 | 0 | 1 |
同时,当K=2时,多分类指数损失的值与二分类指数损失完全一致:
多分类指数损失:
假设K=2,
假设预测分类 = 真实分类 = 1,
1 | 2 | |
---|---|---|
\(H^k_t(x_i)\) | 0.7 | 0.3 |
\(H^*(x)\) | 1 | \(-\frac{1}{2-1}\) |
1 | 2 | |
---|---|---|
\(y^*\) | 1 | \(-\frac{1}{2-1}\) |
二分类指数损失,y=1,由于预测正确,所以\(H^*(x)\)
在实践中,无论是SAMME还是SAMME.R,我们都无法改变使用的损失函数,因此参数中没有为我们提供相应的选择。
loss(adaboost回归器才有)
看完参数algorithm,我们来看参数loss。与分类的情况完全相反,在AdaBoost回归当中,我们能够使用的算法是唯一的,即AdaBoost.R2,但是在R2算法下,我们却可以选择三种损失函数,分别是"linear"(线性),“square”(平方),“exponential”(指数)
在算法AdaBoost.R2当中,三种损失函数如下定义:
首先:
其中\(y_i\)为真实标签,\(H(x_i)\)为预测标签,sup表示“取最大值”,但它与直接写作max的函数的区别在于,max中的元素已是固定的数值,而sup中的元素可以是一个表达式、并让该表达式在i的备选值中循环。上述式子表示,取出1~N号样本中真实值与预测值差距最大的那一组差异来作为D的值。
R2算法线性损失——
R2算法平方损失——
R2算法指数损失——
不难发现,其实线性损失就是我们常说的MAE的变体,平方损失就是MSE的变体,而指数损失也与分类中的指数损失高度相似。在R2算法当中,这些损失函数特殊的地方在于分母D。由于D是所有样本中真实值与预测值差异最大的那一组差异,因此任意样本的\(L_i\)在上述线性与平方损失定义下,取值范围都只有\([0,1]\)(当真实值=预测值时,取值为0,当真实值-预测值=D时,取值为1)。
现在,我们已经了解了AdaBoost的全部参数了。不难发现,在AdaBoost的参数空间中,n_estimators与learning_rate是最为重要的两个参数。当我们在进行超参数调整时,注意对这两个参数的组合进行同时调整即可。
参数 | 参数含义 |
---|---|
base_estimator | 弱评估器 |
n_estimators | 集成算法中弱评估器的数量 |
learning_rate | 迭代中所使用的学习率 |
algorithm(分类器专属) | 用于指定分类ADB中使用的具体实现方法 |
loss(回归器专属) | 用于指定回归ADB中使用的损失函数 |
random_state | 用于控制每次建树之前随机抽样过程的随机数种子 |