在上一篇博客里,我们讨论了关于Bagging的内容,其原理是从现有数据中有放回抽取若干个样本构建分类器,重复若干次建立若干个分类器进行投票,今天我们来讨论另一种算法:提升(Boost)。
简单地来说,提升就是指每一步我都产生一个弱预测模型,然后加权累加到总模型中,然后每一步弱预测模型生成的的依据都是损失函数的负梯度方向,这样若干步以后就可以达到逼近损失函数局部最小值的目标。
下面开始要不说人话了,我们来详细讨论一下Boost算法。首先Boost肯定是一个加法模型,它是由若干个基函数及其权值乘积之和的累加,即
其中b是基函数,beta是基函数的系数,这就是我们最终分类器的样子,现在的目标就是想办法使损失函数的期望取最小值,也就是
一下子对这M个分类器同时实行优化,显然不太现实,这问题也太复杂了,所以人们想了一个略微折中的办法,因为是加法模型,所以我每一步只对其中一个基函数及其系数进行求解,这样逐步逼近损失函数的最小值,也就是说
那聪明的你一定想到了,要使损失函数最小,那就得使新加的这一项刚好等于损失函数的负梯度,这样不就一步一步使得损失函数最快下降了吗?没错,就是这样,那么就有了
Lambda是我随便写的一个参数,可以和beta合并表示步长,那么对于这个基函数而言,其实它就是关于x和这个函数梯度的一个拟合,然后步长的选择可以根据线性搜索法,即寻找在这个梯度上下降到最小值的那个步长,这样可以尽快逼近损失函数的最小值。
到这里,梯度提升的原理其实就讲完了,接下来我们就讲几个实际情况中的特例,包括梯度下降提升树(GDBT),自适应提升(AdaBoost),以及Kaggle竞赛的王者极限提升?翻译不知道对不对,就是(XGBoost)。
第一个,GDBT。
对于这个,一旦对上面梯度提升的想法理解了那就很容易解释了。首先既然是树,那么它的基函数肯定就是决策树啦,而损失函数则是根据我们具体的问题去分析,但方法都一样,最终都走上了梯度下降的老路,比如说进行到第m步的时候,首先计算残差
有了残差之后,我们再用(xi,rim)去拟合第m个基函数,假设这棵树把输入空间划分成j个空间R1m,R2m……,Rjm,假设它在每个空间上的输出为bjm,这样的话,第m棵树可以表示如下:
下一步,对树的每个区域分别用线性搜索的方式寻找最佳步长,这个步长可以和上面的区域预测值bjm进行合并,最后就得到了第m步的目标函数
当然了,对于GDBT比较容易出现过拟合的情况,所以有必要增加一点正则项,比如叶节点的数目或叶节点预测值的平方和,进而限制模型复杂度的过度提升,这里在下面的实践中的参数设置我们可以继续讨论。
第二个,AdaBoost。
首先要说的是是AdaBoost是用于分类的。然后套路想必你已经非常了解了,前面几步完全和上面的GDBT一样,区别在于AdaBoost给出了损失函数为指数损失函数,即
很好理解,预测正确了yf(x)为正值,损失函数值就小,预测错误yf(x)为正值,损失函数值较大,然后我们来看一下第m步的损失函数
现在就是分别求alpha和G(x)使得损失函数最小值,按照之前的想法,直接算伪残差然后用G(x)拟合,不过这边我们先不着急。指数项中,yi与fm-1的乘积是不依赖于alpha和G(x)的,所以可以提出来不用考虑,对于任意alpha>0,在exp(-yi*fm-1)权值分布下,要exp(-yi*alpha*G(x))取最小值,也就是要G(x)对加权y预测的正确率最高。接下来,求alpha很愉快,直接求导位0,懒癌发作,公式推导过程就不打了,最后的结果如下:
得到了参数之后就能愉快的迭代,使得训练数据上的正确率蹭蹭蹭地往上涨。
再回过头来看看AdaBoost的标准做法和我们推导的是否一致
1 第一步假设平均分布,权值都为1/N,训练数据得到分类器。
2 求第一步的分类器预测数据的错误率,计算G(x)的系数alpha。
3 更新权值分布,不过加了归一化因子,使权值满足概率分布。
4 基于新的权值分布建立新的分类器,累加在之前的模型中。
5 重复上述步骤,得到最终的分类器。
可以看出,除了在更新权值分布处加了一个归一化因子之外,其他的都和我们推导的一样,所以,所以什么呀……你不仅会用还会推导啦?O(∩_∩)O哈哈~
第三个,XGBoost。
其实说白了也很简单,之前用的梯度下降的方法我们都只考虑了一阶信息,根据泰勒展开,
我们可以把二阶信息也用上,假如目标函数如下
啊啊啊,这公式打得我真要吐血了。其中Ω为正则项,正如上面讲的,可表示如下
然后对于决策树而言,最重要的就是一共有多少个节点以及每个节点的权值,所以决策树可以表示为
这样就有了下一步的推导,鉴于它实在是太长了,我就直接截图了
第二步是因为不管fm如何取值第一项的值都不变,所以优化过程中可以不用考虑,第三步是因为对于每个样本而言其预测值就是对应输入空间对应的权值,第四步则是把样本按照划分区域重新组合,然后定义
带入对w求偏导使其为0,这样就求得了
再回代,就可以把J(fm)中的w给消去了,得到了
这样我们就把新一步函数的损失函数变成了只与上一步相关的一个新的损失函数,这样我们就可以遍历数据中所有的分割点,寻找新的损失函数下降最多的分割点,然后重复上述操作。
相比于梯度下降提升,XGBoost在划分新的树的时候还是用了二阶信息,因此能够更快地收敛,而且XGBoost包是用C/C++写的,所以速度更快,而且在寻找最佳分割点的时候,可以引入并行计算,因此速度进一步提高,广受各大竞赛参赛者的喜爱啊。
说到现在的理论推导,有耐心看到这里的少年我只能说你接近成功了,Boost算法要被你拿下了,接下来我们就来一把实战试试。
Sklearn中有GDBT和AdaBoost算法,它用的方法和前面的Bagging什么的一模一样,具体的参数设置大家可以参考帮助,这里给出一个简单的例子,没错,又是那个鸢尾花,它又来了,用它的前两个特征进行训练,我们来看看训练集上的正确率,按道理,Boost算法在训练集上的效果应该是十分卓越的,毕竟它有过拟合的趋势啊。
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import AdaBoostClassifier
import matplotlib.pyplot as plt
import matplotlib as mpl
from sklearn import datasets
iris=datasets.load_iris()
x=iris.data[:,:2]
y=iris.target
model1=DecisionTreeClassifier(max_depth=5)
model2=GradientBoostingClassifier(n_estimators=100)
model3=AdaBoostClassifier(model1,n_estimators=100)
model1.fit(x,y)
model2.fit(x,y)
model3.fit(x,y)
model1_pre=model1.predict(x)
model2_pre=model2.predict(x)
model3_pre=model3.predict(x)
res1=model1_pre==y
res2=model2_pre==y
res3=model3_pre==y
print '决策树训练集正确率%.2f%%'%np.mean(res1*100)
print 'GDBT训练集正确率%.2f%%'%np.mean(res2*100)
print 'AdaBoost训练集正确率%.2f%%'%np.mean(res3*100)
输出为
决策树训练集正确率84.67%
GDBT训练集正确率92.00%
AdaBoost训练集正确率92.67%
在训练集上表现全是很好啊,在测试集上就不好说了,但有一句话说的好,过拟合总比欠拟合好啊……没事儿,还有调参大法,根据自己的需要去试试就好了。
XGBoost在sklearn里没有,所以需要额外安装一下,我是按照这个网址的教程这里,亲测有效,只有一步,就是在安装MinGW-W64的时候这个博客里提供的地址下载下来安装总是失败,大家去官网下一个就行,其他的按照教程一步一步做就行了,没什么其他的幺蛾子。
所以,又是鸢尾花登场了
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn import datasets
iris=datasets.load_iris()
x=iris.data[:,:2]
y=iris.target
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.7, random_state=1)
data_train = xgb.DMatrix(x_train,label=y_train)
data_test=xgb.DMatrix(x_test,label=y_test)
param = {}
param['objective'] = 'multi:softmax'
param['eta'] = 0.1
param['max_depth'] = 6
param['silent'] = 1
param['nthread'] = 4
param['num_class'] = 3
watchlist = [ (data_train,'train'), (data_test, 'test') ]
num_round = 10
bst = xgb.train(param, data_train, num_round, watchlist );
pred = bst.predict( data_test );
print ('predicting, classification error=%f' % (sum( int(pred[i]) != y_test[i] for i in range(len(y_test))) / float(len(y_test)) ))
这里主要解释以下param的设置,objective设置的是你的分类的目标及方法,除了我们用的多分类的multi:softmax,还可以是binary:logistic,reg:logistic等等,根据你的目标需要去设置。eta设置的是衰减因子,就是在步长前面乘以一个系数,设置过小容易导致计算时间太长,太大又很容易过拟合,max_depth是所用的树的最大深度,silent是打印运行信息,没什么太大的意义,num_class应该是类的数目吧,nthread是调用的线程数,num_round是迭代计算次数。
看一下输出吧
[0] train-merror:0.133333 test-merror:0.266667
[1] train-merror:0.12381 test-merror:0.266667
[2] train-merror:0.114286 test-merror:0.266667
[3] train-merror:0.114286 test-merror:0.266667
[4] train-merror:0.114286 test-merror:0.266667
[5] train-merror:0.114286 test-merror:0.266667
[6] train-merror:0.114286 test-merror:0.266667
[7] train-merror:0.114286 test-merror:0.266667
[8] train-merror:0.104762 test-merror:0.288889
[9] train-merror:0.104762 test-merror:0.288889
predicting, classification error=0.288889
确实,在训练集上的错误率不断下降,但测试集并非如此,到了第八步第九步已经出现了过拟合的嫌疑,反正后面就是调参的工作吗,训练集上表现不好,衰减因子大一点,树的深度大一点,迭代次数多一点等等,反之,如果过拟合了,就反过来做好啦~
不行了,饿死了,先去吃饭了。
如果XGBoost的安装和使用有问题,大家可以讨论下啊,最近刚好想好好研究一下这个,后面有必要的话再专门写一篇吧,感觉这个调参还是有点意思。