本篇对应全书第八章,讲的是提升方法。提升(boosting)方法是一种常用的统计学习方法,应用广泛且有效。提升方法中最具代表性的是Adaboost算法,由Freund和Schapire于1995年提出。
1、提升方法Adaboost算法
1.1、提升方法的基本思路
提升方法基于这样一种思想:对于一个复杂任务来说,将多个专家的判断进行适当的综合所得出的判断,要比其中任何一个专家单独判断的好。
在学习中,如果已经发现了“弱学习算法”,那么能否将其提升(boost)为“强学习算法”。大家知道,发现弱学习算法通常要比发现强学习算法容易得多,那么如何具体实施提升,便成为开发提升方法时所要解决的问题。关于提升方法的研究很多,有很多算法被提出,最具代表性的是Adaboost算法。
对于分类问题而言,给定一个训练集,求比较粗糙的分类规则(弱分类器)要比求精确的分类规则(强分类器)容易得多。提升方法就是从弱学习算法出发,反复学习,得到一系列弱分类器(又称为基本分类器),然后组合这些弱分类器,构成一个强分类器。大多数的提升方法都是改变训练数据的概率分布(训练数据的权值分布),针对不同的训练数据分布调用弱学习算法学习一系列弱分类器。
这样,对提升方法来说,有两个问题需要回答:一是在每一轮如何改变训练数据的权值或概率分布;二是如何将弱分类器组合成为一个强分类器。关于第1个问题,Adaboost的做法是,提高那些被前一轮弱分类器错误分类样本的权值,而降低那些被正确分类样本的权值。这样一来,那些没有得到正确分类的数据,由于其权值的加大而受到后一轮的弱分类器的更大关注。至于第2个问题,即弱分类器的组合,Adaboost采用加权多数表决的方法。具体地,加大分类误差率小的弱分类器的权值,使其在表决中起较大的作用,减小分类误差率大的弱分类器的权值,使其在表决中起较小的作用。
Adaboost的巧妙之处就在于它将这些想法自然且有效地实现在一种算法里。
1.2、Adaboost算法
给定二分类的训练集
实例
输入:训练集,其中,;弱学习算法
输出:最终分类器
(1)初始化训练数据的权值分布
(2)对
(a)使用具有权值分布的训练集学习,得到基本分类器
(b)计算在训练数据集上的分类误差率
(c)计算的系数
这里的对数是自然对数。
(d)更新训练集的权值分布
这里,是规范化因子
它使成为一个概率分布。
(3)构建基本分类器的线性组合
得到最终分类器
对Adaboost算法做如下说明:
步骤(1)假设训练集具有均匀的权值分布,即每个训练样本在基本分类器的学习作用中相同,这一假设保证第1步能够在原始数据上学习基本分类器
步骤(2)Adaboost反复学习基本分类器,在每一轮
(a)使用当前分布
(b)计算基本分类器
这里,
(c)计算基本分类器
(d)更新训练数据的权值分布为下一轮作准备。式(8.4)可以写成:
由此可知,被基本分类器
步骤(3)线性组合
这里提一下Adaboost的迭代停止条件,既可以设定最低分类错误率,也可以设定最大迭代次数,达到阈值停止迭代即可。
2、Adaboost算法的训练误差分析
Adaboost最基本的性质是它能在学习过程中不断减少训练误差,即在训练集上的分类误差率。关于这个问题有下面的定理:
定理( Adaboost的训练误差界)Adaboost算法最终分类器的训练误差界为
这里,,和分别由式(8.7)、式(8.6)和式(8.5)给出。
这一定理说明,可以在每一轮选取适当的
对二分类问题,有如下结果:
定理( 二分类问题Adaboost的训练误差界)
这里,。
根据上述定理,可知,如果存在
这表明在此条件下Adaboost的训练误差是以指数速率下降的,这一性质当然是很有吸引力的。
3、Adaboost算法的解释
Adaboost算法还有另一个解释,即可以认为Adaboost算法是模型为加法模型、损失函数为指数函数、学习算法为前向分步算法时的二分类学习方法。
3.1、前向分步算法
考虑加法模型(additive model)
其中,
在给定训练数据及损失函数
通常这是一个复杂的优化问题。前向分步算法(forward stagewise algorithm)求解这一优化问题的想法是:因为学习的是加法模型,如果能够从前向后,每一步只学习一个基函数及其系数,逐步逼近优化目标函数式(8.14),那么就可以简化优化的复杂度。具体地,每步只需优化如下损失函数:
给定训练集
输入:训练集;损失函数;基函数集;
输出:加法模型。
(1)初始化
(2)对
(a)极小化损失函数
得到参数,
(b)更新
(3)得到加法模型
这样,前向分步算法将同时求解从
3.2、前向分步算法与Adaboost
由前向分步算法可以推导出Adaboost,用定理叙述这一关系。
定理 Adaboost算法是前向分布加法算法的特例,这时,模型是由基本分类器组成的加法模型,损失函数是指数函数。
具体证明过程这里不细述,读者可自行阅读书中相关内容。
4、Adaboost小结
4.1、Adaboost优缺点
优点:
a).作为分类器时,分类性能好;
b).可以选择不同的弱学习器,灵活;
c).不容易过拟合
缺点:
a).对异常样本敏感,异常样本在迭代中可能会获得较高的权重,影响最终的强学习器的预测准确性(这主要是由指数损失函数导致的)
b).训练耗时太长
4.2、其它
1. Adaboot能用于多分类吗?
可以。
事实上我们之前讲述的只是Adaboost的经典算法,它最初是针对二分类问题提出的,后来基于其核心思想发展了很多改进算法,这就包括针对多分类问题的Adaboost-SAMME、Adaboost-SAMME.R、AdaBoost.M1、AdaBoost.M2、AdaBoost.MH,针对回归问题的AdaBoost.R2,针对排序场景的AdaRank等,sklearn中关于Adaboost分类器的实现,采用的就是Adaboost-SAMME和Adaboost-SAMME.R。
2. Adaboost能用于回归吗?
可以。
上面我们已经说过了,AdaBoost.R2可用于解决回归问题,它也是sklearn中AdaBoostRegressor采用的算法。
3. Adaboost能否用来组合强分类器?
可以。
但是效果上可能未必如使用弱分类器那样,达到增强的目的,这是因为,强分类器学习得到的模型往往趋同,彼此之间的差异性不大,做不到所谓的“好而不同”,集成学习也就失去了意义。具体可参考这篇文章。
4. Adaboost能否使用其它损失函数?
可以。
事实上,“Adaboost算法是前向分步加法算法的特例,这时,模型是由基本分类器组成的加法模型,损失函数是指数函数”,这并不是Adaboost最初被设计的初衷,不过这提供了一个深入理解 AdaBoost,以及扩展 AdaBoost 的机会,通过使用其它损失函数,我们可以得到许多Adaboost的扩展,具体可参考这篇文章。
5. Adaboost为什么不容易过拟合?
这个问题目前尚没有很好的解释,可参看这篇文章。
6. Adaboost的正则化
关于这一点,笔者所能查阅到的资料不多,读者可参考这篇文章。
7. Adaboost的弱学习器
理论上Adaboost可选用任何弱学习器,但一般来说,使用最广泛的还是决策树和神经网络(MLP)。对于决策树,Adaboost分类用了CART分类树,而Adaboost回归用了CART回归树。
8. sklearn中的Adaboost
sklearn中Adaboost类库比较直接,就是AdaBoostClassifier和AdaBoostRegressor两个,从名字就可以看出AdaBoostClassifier用于分类,AdaBoostRegressor用于回归。
AdaBoostClassifier使用了两种Adaboost分类算法的实现,SAMME和SAMME.R。而AdaBoostRegressor则使用了Adaboost.R2。
具体调参细节可以参考这篇文章。
5、提升树
提升树是以分类树或回归树为基本分类器的提升方法。提升树被认为是统计学习中性能最好的方法之一。
5.1、提升树算法
以决策树(通常采用CART)为基函数的提升方法称为提升树(boosting tree),对分类问题决策树是二叉分类树,对回归问题决策树是二叉回归树。
针对不同问题的提升树学习算法,其主要区别在于使用的损失函数不同,包括用平方损失函数的回归问题,用指数损失函数的分类问题,以及用一般损失函数的一般决策问题。
对于二分类问题,提升树算法只需将Adaboost算法中的基本分类器限制为二叉分类树即可,可以说这时的提升树算法是Adaboost算法的特例。
对于回归问题,提升树模型可以表示为
对此,下面我们做一个详细的叙述。
已知一个训练集
其中,参数
回归问题提升树使用以下前向分步算法:
在前向分步算法的第
得到
当采用平方损失函数时,
其损失变为
其中,
是当前模型拟合数据的残差(residual),所以,对回归问题的提升树算法而言,只需简单地拟合当前模型的残差,这样,算法是相当简单的。
回归问题的提升树算法叙述如下:
输入:训练集,,;
输出:提升树
(1)初始化
(2)对
(a)计算残差
(b)拟合残差学习一个回归树,得到
(c)更新
(3)得到回归问题提升树
5.2、梯度提升
提升树利用加法模型与前向分步算法实现学习的优化过程,当损失函数是平方损失和指数损失函数时,每一步优化是很简单的,但对一般损失函数而言,往往每一步优化并不那么容易。针对这一问题,Freidman提出了梯度提升(gradient boosting)算法,这是利用最速下降法的近似方法,其关键是利用损失函数的负梯度在当前模型的值
作为回归问题提升树算法中的残差的近似值,拟合一个回归树。
梯度提升算法叙述如下:
输入:训练集,,;损失函数;
输出:回归树
(1)初始化
(2)对
(a)对,计算
(b)对拟合一个回归树,得到第颗树的叶结点区域
(c)对,计算
(d)更新
(3)得到回归树
5.3、梯度提升(续)
在李航老师的《统计学习方法》中,有关梯度提升的内容相对较少,又由于这一内容确实属于核心,因此我们在这一节接着讲一讲。
Part I.
Gradient Boosting,或者说是Gradient Boosting Machine(简称GBM),由Freidman提出,是一类以梯度提升为基本思想的算法。
和Adaboost一样,Gradient Boosting也是Boosting的一种。经典的AdaBoost算法只能处理采用指数损失函数的二分类学习任务(当然,现在已经有能够处理多分类或回归任务的AdaBoost算法变种),而Gradient Boosting通过设置不同的可微损失函数可以处理各类学习任务(分类、回归、排序等),应用范围大大扩展。另一方面,AdaBoost对异常点(outlier)比较敏感,而Gradient Boosting通过引入bagging思想、加入正则项等方法能够有效地抵御训练数据中的噪音,具有更好的健壮性。这也是为什么Gradient Boosting(尤其是采用决策树作为弱学习器的GBDT算法)如此流行的原因。
Gradient Boosting的基本思想是,将负梯度作为上一轮学习犯错的衡量指标,在下一轮学习中通过拟合负梯度来纠正上一轮犯的错误。这里的关键问题是:为什么通过拟合负梯度就能纠正上一轮的错误了?Freidman给出的答案是:函数空间的梯度下降。
首先回顾一下梯度下降(Gradient Descent)的参数更新公式:
Gradient Boosting采用和Adaboost同样的加法模型,在第
此时我们的目标是最小化经验风险
对比以上两个式子,可以发现,若用基学习器
所以,在GBDT中,与其说是用负梯度去拟合残差,倒不如说残差是负梯度的一个特例。
下面我们来详细描述一下Gradient Boosting的算法流程:
输入:训练集,,;损失函数;
输出:加法模型
(1)初始化(2)对
(a)对,计算(b)通过最小化均方差损失,用基学习器拟合
(c)使用line search确定步长,以使得经验风险最小:(d)更新
(3)得到回归树
Part II.
梯度提升树,常简称为GBDT(Gradient Boosting Decision Tree),或者是GBT(Gradient Boosting Tree),GTB(Gradient Tree Boosting ),GBRT(Gradient Boosting Regression Tree),MART(Multiple Additive Regression Tree)。GBDT在实际业务中得到了广泛应用,在机器学习算法中占有重要的一席之地。
这里我们首先要明确一点,GBDT采用的是CART中的回归树。
理论上,GBM可以选择各种不同的学习算法作为基学习器。现实中,用得最多的基学习器是决策树,这主要得益于决策树的“易于理解、可解释性强、预测速度快”等优点。同时,决策树算法相比于其他的算法需要更少的特征工程,比如可以不用做特征标准化,可以很好的处理字段缺失的数据,也可以不用关心特征间是否相互依赖等。决策树能够自动组合多个特征,它可以毫无压力地处理特征间的交互关系并且是非参数化的,因此你不必担心异常值或者数据是否线性可分。不过,单独使用决策树算法时,容易过拟合。所幸的是,通过各种方法,抑制决策树的复杂性,降低单颗决策树的拟合能力,再通过梯度提升的方法集成多个决策树,最终能够很好的解决过拟合的问题。由此可见,梯度提升方法和决策树学习算法可以互相取长补短,是一对完美的搭档。至于抑制单颗决策树的复杂度的方法有很多,比如限制树的最大深度、限制叶子节点的最少样本数量、限制节点分裂时的最少样本数量、吸收bagging的思想对训练样本采样(subsample),在学习单颗决策树时只使用一部分训练样本、借鉴随机森林的思路在学习单颗决策树时只采样一部分特征、在目标函数中添加正则项惩罚复杂的树结构等。现在主流的GBDT算法实现中这些方法基本上都有实现,因此GBDT算法的超参数还是比较多的,应用过程中需要精心调参,并用交叉验证的方法选择最佳参数。
前面我们已经讲过了GBDT如何解决回归问题,那么它能否用来解决分类问题呢?答案是肯定的,实际上无论是分类问题还是回归问题,GBDT所采用的算法思想都是一致的,唯一的区别在于所选取的损失函数不同。
在处理分类问题时,由于样本输出不是连续的值,而是离散的类别,因此我们可以选用《统计学习方法》系列(6)中讲过的负对数损失函数,将连续值转化为概率来拟合损失,具体可参考这篇文章,其中还区分了二分类和多分类,可仔细看看。
在解决分类问题时,GBDT常用的损失函数有:指数损失(退回到Adaboost)、负对数损失。指数损失容易受异常点影响,且只能用于二分类问题,因此sklearn中GradientBoostingClassifier的默认损失函数选得是后者。
在解决回归问题时,GBDT常用的损失函数有:均方差损失、绝对损失、Huber损失和分位数损失。对于Huber损失和分位数损失,常用于健壮回归,可以减少异常点对损失函数的影响,相反,均方差损失容易受到异常点的影响。
Part III.
XGBoost是对GBDT的扩展实现,不仅在速度上更加高效,而且在算法推导上也有所改进。二者的主要差别如下:
1. GBDT以CART回归树作为基学习器,XGBoost则支持gbtree、dart和gblinear三种基学习器。其中,前二者都是基于树的模型,dart还考虑了dropout思想,gblinear则是线性模型,这个时候XGBoost相当于带L1和L2正则化项的逻辑回归(分类问题)或者线性回归(回归问题)。
关于gblinear,读者可参考这篇文章。
2. GBDT在优化时只用到一阶导数信息,XGBoost则对代价函数进行了二阶泰勒展开,同时用到了一阶和二阶导数。顺便提一下,XGBoost工具支持自定义代价函数,只要函数一阶和二阶可导。
更本质一点来说,GBDT主要对loss
3. XGBoost在代价函数里加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、每个叶子节点上输出的score的L2模的平方和。从bias-variance tradeoff角度来讲,正则项降低了模型的variance,使学习出来的模型更加简单,防止过拟合。
4. XGBoost借鉴了随机森林的做法,支持列抽样(column subsampling),即随机选取部分特征进行学习,这不仅能降低过拟合,还能减少计算。
5. 对于特征的值有缺失的样本,XGBoost可以自动学习出它的分裂方向。
6. XGBoost工具支持并行。需要注意的是,这里的并行不是tree粒度的并行,XGBoost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值),XGBoost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),XGBoost在训练前,预先对数据进行排序,保存为block结构,后面的迭代中重复使用这个结构,大大减小计算量。这个block结构也使得并行成为可能:在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行,达到并行目的。
7. 树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以XGBoost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。
关于XGBoost,读者可参考陈天奇的PPT。
LightGBM是微软近年来开源的库,相较于XGBoost,它的速度更快并且内存占用更低。LightGBM的改进点主要在于以下几方面:
1. 采用直方图算法寻找分割点,减小内存占用、加速
2. 采用Leaf-wise的方式建树,提升准确率
3. 对并行学习的优化
4. 减少训练集(GOSS)、减少特征(EFB),可参考这篇文章
关于这些,读者可以参考这个PPT以及LightGBM的中文文档。
5.4、其它
1. GBDT的正则化
有好几种方式可以选择,读者可参考这篇文章。
2. 关于Adaboost和Gradient Boosting的历史发展渊源,读者可阅读这篇文章。
3. sklearn中的GBDT
读者可参考这篇文章。
4. XGBoost中的回归树和GBDT所使用的CART回归树并不一致,后者的叶结点输出值,是分到该叶结点的所有样本点的均值,前者的叶结点输出值是根据打分公式计算出来的。读者可参考这篇文章。
5. 有了XGBoost,其它简单线性模型是否还有存在的必要性
读者可参考这篇文章。
6. XGBoost常见面试题
读者可参考这篇文章。
7. 特征重要性
XGBoost可以输出特征重要性的score,其原理可参考这篇文章。
8. Boosting主要关注降低bias,因此Boosting能基于泛化性能相当弱的学习器构建出很强的集成;Bagging主要关注降低variance,因此它在不剪枝的决策树、神经网络等学习器上效用更为明显。具体可参考这篇文章。
6、代码实现
6.1、sklearn实现
Adaboost
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.ensemble import AdaBoostClassifier
from sklearn.model_selection import train_test_split
if __name__ == "__main__":
iris = load_iris()
X = iris.data[:100, :2]
y = iris.target[:100]
y[y == 0] = -1
xlabel = iris.feature_names[0]
ylabel = iris.feature_names[1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)
X_0 = X_train[y_train == -1]
X_1 = X_train[y_train == 1]
plt.figure("adaboost-sklearn")
plt.scatter(X_0[:, 0], X_0[:, 1], color = 'y', label = '-1')
plt.scatter(X_1[:, 0], X_1[:, 1], color = 'b', label = '1')
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.legend()
clf = AdaBoostClassifier()
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
print "score : %s" % score
y_pre = clf.predict(X_test)
X_test_pre_0 = X_test[y_pre == -1]
X_test_pre_1 = X_test[y_pre == 1]
plt.scatter(X_test_pre_0[:, 0], X_test_pre_0[:, 1], color = 'r', label = 'pre -1')
plt.scatter(X_test_pre_1[:, 0], X_test_pre_1[:, 1], color = 'k', label = 'pre 1')
plt.legend()
plt.show()
GBDT
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
if __name__ == "__main__":
iris = load_iris()
X = iris.data[:100, :2]
y = iris.target[:100]
y[y == 0] = -1
xlabel = iris.feature_names[0]
ylabel = iris.feature_names[1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)
X_0 = X_train[y_train == -1]
X_1 = X_train[y_train == 1]
plt.figure("gbdt-sklearn")
plt.scatter(X_0[:, 0], X_0[:, 1], color = 'y', label = '-1')
plt.scatter(X_1[:, 0], X_1[:, 1], color = 'b', label = '1')
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.legend()
clf = GradientBoostingClassifier()
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
print "score : %s" % score
y_pre = clf.predict(X_test)
X_test_pre_0 = X_test[y_pre == -1]
X_test_pre_1 = X_test[y_pre == 1]
plt.scatter(X_test_pre_0[:, 0], X_test_pre_0[:, 1], color = 'r', label = 'pre -1')
plt.scatter(X_test_pre_1[:, 0], X_test_pre_1[:, 1], color = 'k', label = 'pre 1')
plt.legend()
plt.show()
代码已上传至github:xiongzwfire/statistical-learning-method
参考文献
[1] adaboost的样本权值如何对弱分类器产生影响?
[2] 提升树算法 详解 - liuwu265 - 博客园
[3] https://blog.csdn.net/u014732537/article/details/79667703
[4] 1.11. Ensemble methods
[5] sklearn.ensemble.AdaBoostRegressor - scikit-learn 0.20.3 documentation
[6] sklearn.ensemble.AdaBoostClassifier - scikit-learn 0.20.3 documentation
[7] 集成学习之Adaboost算法原理小结 - 刘建平Pinard - 博客园
[8] scikit-learn Adaboost类库使用小结
[9] adaboost、adaboost.m1、adaboost.m2简介
[10] 机器学习 Adaboost | tool188
[11] 为什么集成学习要求弱分类器?
[12] Adaboost可不可以使用其他的损失函数?
[13] adaboost为什么不容易过拟合呢?
[14] 梯度提升树(GBDT)原理小结 - 刘建平Pinard - 博客园
[15] scikit-learn 梯度提升树(GBDT)调参小结
[16] 机器学习中的数学(3)-模型组合(Model Combining)之Boosting与Gradient Boosting
[17] wdmad:集成学习之Boosting —— Gradient Boosting原理
[18] 机器学习面试干货精讲 - TinyMind -专注人工智能的技术社区
[19] https://blog.csdn.net/yangxudong/article/details/53872141
[20] 为什么xgboost/gbdt在调参时为什么树的深度很少就能达到很高的精度?
[21] 余文毅:当我们在谈论GBDT:从 AdaBoost 到 Gradient Boosting
[22] RF,GBDT and XGboost
[23] 机器学习算法中 GBDT 和 XGBOOST 的区别有哪些?
[24] GBM之GBRT总结
[25] http://lightgbm.apachecn.org/#/
[26] LightGBM核心解析与调参 - 掘金
[27] 侯处然:说说XGBoost 算法中的CART模型树
[28] NIPS 2017 有什么值得关注的亮点?
[29] xgboost的gblinear是什么意思?-SofaSofa
[30] 既然xgboost那么好,线性回归还有存在的价值吗?-SofaSofa
[31] 机器学习时代的三大神器:GBDT,XGBOOST和LightGBM_慕课手记
[32] https://homes.cs.washington.edu/~tqchen/pdf/BoostedTree.pdf
[33] http://wepon.me/files/gbdt.pdf
[34] 当GridSearch遇上XGBoost 一段代码解决调参问题
[35] XGBoost
[36] 十分钟入门XGBoost(原理解析、优点介绍、参数详解) | VPrincekin的个人博客
[37] DART booster
[38] 渣渣辉:RF、GBDT、XGBoost常见面试题整理
[39] XGBoost特征重要性的实现原理?
以上为本文的全部参考文献,对原作者表示感谢。
我的足迹
- CSDN
- GitHub