序言
XGBoost是陈天奇等人开发的一个开源机器学习项目,高效地实现了GBDT算法并进行了算法和工程上的许多改进,被广泛应用在Kaggle竞赛及其他许多机器学习竞赛中,并取得了不错的成绩。
XGBoost本质上是一个GBDT(Gradient Boosting Decision Tree,梯度提升决策树),GBDT使用了Boosting的思想,其基本原理是所有弱分类器的结果相加等于预测值,然后下一个弱分类器去拟合上一个预测的残差(这个残差就是预测值与真实值之间的误差)1。
相比GBDT,XGBoost在工程实现上做了大量的优化,力争把速度和效率发挥到极致,所以叫X(Extreme)GBoost。
原理
例子引入
假设我们要预测一家人对电子游戏的喜好程度,已知喜好程度打分真实值:男孩2.9,女孩-0.8,爷爷-1.9,奶奶-1.9,妈妈-0.1。考虑到年龄因素,年轻人更可能喜欢电子游戏;考虑到性别因素,男性更喜欢电子游戏,故先根据年龄大小区分小孩和大人,然后再通过性别区分是男是女,逐一给各人在电子游戏喜好程度上打分,训练出第一颗树,如下图所示。
接着再根据电脑使用频率进行分类打分,训练出第二颗树,如下图所示。
而我们最终的预测分数是将两棵树的预测值相加。例如,小男孩的分数是2+0.9=2.9,爷爷的分数是-1-0.9=-1.9。
这与GBDT的原理异曲同工,第二颗树对男孩的预测值是男孩的真实值与第一颗树预测值的差值(0.9=2.9-2),即残差,这正好对应了上文提到的“下一个弱分类器去拟合上一个预测的残差”。最终的预测结果是所有分类器的结果相加。
总而言之,XGBoost的核心算法思想基本就是:
- 不断地添加树,不断地进行特征分裂来生长一棵树,每次添加一个树,其实是学习一个新函数f(x),去拟合上次预测的残差;
- 当我们训练完成得到k棵树,我们要预测一个样本的分数,其实就是根据这个样本的特征,在每棵树中会落到对应的一个叶子节点,每个叶子节点就对应一个分数;
- 最后只需要将每棵树对应的分数加起来就是该样本的预测值2。
目标函数
-
XGBoost与GBDT比较大的不同就是目标函数的定义。在第t轮,我们需要寻找一个f,来最小化下面的目标函数:
O b j ( t ) = ∑ i = 1 n l ( y i , y ^ i ( t − 1 ) + f t ( x i ) ) + Ω ( f t ) + c o n s t a n t \boldsymbol{Obj^{(t)}=\sum_{i=1}^n l\Big(y_i, \hat{y}_i^{(t-1)}+f_t(x_i)\Big)+\Omega(f_t)+constant} Obj(t)=i=1∑nl(yi,y^i(t−1)+ft(xi))+Ω(ft)+constant
其中第一项中的 l ( ) l(\quad) l()为损失函数;第二项为正则项,控制树的复杂度,防止过拟合;第三项为常数。在这个式子中, y ^ i ( t − 1 ) + f t ( x i ) = y ^ i ( t ) \hat{y}_i^{(t-1)}+f_t(x_i)=\hat{y}_i^{(t)} y^i(t−1)+ft(xi)=y^i(t),即第t轮的预测值,表示是由上一轮预测值加上本轮结果得到。 -
接下来,用泰勒展开三项来近似我们的目标函数:
O b j ( t ) ≅ ∑ i = 1 n [ l ( y i , y ^ i ( t − 1 ) ) + g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + Ω ( f t ) + c o n s t a n t \boldsymbol{Obj^{(t)}\cong\sum_{i=1}^n \Big[l(y_i, \hat{y}_i^{(t-1)})+g_if_t(x_i)+\frac{1}{2}h_if_t^2(x_i)\Big]+\Omega(f_t)+constant} Obj(t)≅i=1∑n[l(yi,y^i(t−1))+gift(xi)+21hift2(xi)]+Ω(ft)+constant
其中正则项 Ω ( f t ) = γ T + 1 2 λ ∑ j = 1 T w j 2 \Omega(f_t)=\gamma T+\frac{1}{2}\lambda\sum_{j=1}^T w_j^2 Ω(ft)=γT+21λ∑j=1Twj2, w w w是叶结点权重(分数), T T T是叶结点个数。 -
接下来求解这个目标函数,最终化为:
O b j ( t ) = ∑ j = 1 T [ ( ∑ i ∈ I j g i ) ⋅ w j + 1 2 ( ∑ i ∈ I j h i + λ ) ⋅ w j 2 ] + γ T \boldsymbol{Obj^{(t)}=\sum_{j=1}^T\Big[(\sum_{i\in I_j} g_i)\cdot w_j+\frac{1}{2}(\sum_{i\in I_j} h_i+\lambda)\cdot w_j^2\Big]+\gamma T} Obj(t)=j=1∑T[(i∈Ij∑gi)⋅wj+21(i∈Ij∑hi+λ)⋅wj2]+γT
其中 I j = { i ∣ q ( x i ) = j } I_j=\{i|q(x_i)=j\} Ij={i∣q(xi)=j}代表映射到叶结点 j j j的样本 x i x_i xi的所有下标, q q q表示从样本到叶结点的映射(代表了树结构)。(推导过程不作赘述,参考# XGBoost原理) -
最终我们将关于树模型的迭代转化为关于树的叶节点的迭代,并求出最优的叶节点分数,令 ∂ O b j ( t ) / ∂ w j = 0 \partial Obj^{(t)}/\partial w_j=0 ∂Obj(t)/∂wj=0,得 w j = − ∑ i ∈ I j g i ∑ i ∈ I j h i + λ w_j=-\frac{\sum_{i\in I_j} g_i}{\sum_{i\in I_j} h_i+\lambda} wj=−∑i∈Ijhi+λ∑i∈Ijgi。将叶节点的最优值带入目标函数,得到一个可看做树结构的打分函数:
L ( t ) ( q ) = − 1 2 ∑ j = 1 T ( ∑ i ∈ I j g i ) 2 ∑ i ∈ I j h i + λ + γ T \boldsymbol{L^{(t)}(q)=-\frac{1}{2}\sum_{j=1}^T \frac{(\sum_{i\in I_j} g_i)^2}{\sum_{i\in I_j} h_i+\lambda}+\gamma T} L(t)(q)=−21j=1∑T∑i∈Ijhi+λ(∑i∈Ijgi)2+γT
我们的任务就是找到一个最好的树结构 q q q使得这个函数最小,得分越小,说明结构越好。
树的生成
很显然,一棵树的生成是由一个节点一分为二,然后不断分裂最终形成为整棵树。对于一个叶子节点如何进行分裂,XGBoost做法是枚举所有不同树结构的贪心法。
不断地枚举不同树的结构,然后利用打分函数来寻找出一个最优结构的树,接着加入到模型中,不断重复这样的操作。这个寻找的过程使用的就是贪心算法。选择一个feature分裂,计算loss function最小值,然后再选一个feature分裂,又得到一个loss function最小值,枚举完,找一个效果最好的,把树给分裂,就得到了小树苗。
限于篇幅,更多的实现细节参考# XGBoost原理
代码实践
XGBoost算法的实现以鸢尾花数据集为例。Iris鸢尾花数据集是一个经典数据集,在统计学习和机器学习领域都经常被用作示例。数据集内包含 3 类共 150 条记录,每类各 50 个数据,每条记录都有 4 项特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度,可以通过这4个特征预测鸢尾花卉属于(iris-setosa,iris-versicolour,iris-virginica)中的哪一品种3。部分数据截图如下:
下面的示例代码4采用直接调用xgboost包的方式。
#导入sklearn和xgboost包
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import xgboost as xgb
#用于显示正确率
def show_accuracy(a, b, tip):
acc = a.ravel() == b.ravel()
print (acc)
print (tip + '正确率:\t', float(acc.sum()) / a.size)
if __name__ == "__main__":
#加载iris数据集
data=load_iris()
X=data.data
Y=data.target
#划分训练集和测试集
X_train,X_test,y_train,y_test=train_test_split(X,Y,test_size=0.25,random_state=1)
#数据转换为数据矩阵类DMatrix
data_train = xgb.DMatrix(X_train,label=y_train)
data_test = xgb.DMatrix(X_test,label=y_test)
#设置参数
param = {'max_depth': 3, 'eta': 1, 'silent': 1, 'objective': 'multi:softmax','num_class': 3}
watchlist = [(data_test, 'eval'), (data_train, 'train')]
#训练模型
bst = xgb.train(param, data_train, num_boost_round=4, evals=watchlist)
#测试
y_hat = bst.predict(data_test)
show_accuracy(y_hat, y_test, 'XGBoost ')
param各项参数的含义可参考4。
运行结果如下:
XGBoost的代码实现还可以使用xgboost包的sklearn接口xgboost.XGBClassifier(),利用函数参数设置模型参数,限于篇幅不再赘述,读者可以参考链接Github:XGBoost.ipynb。
总结
XGBoost是一种GBDT的实现,是一个优秀的分类算法,相比GBDT有许多优势,如:
- 显式地加入了正则项来控制模型的复杂度,有利于防止过拟合;
- 对代价函数进行二阶泰勒展开,可以同时使用一阶和二阶导数;
- 支持多种类型的基分类器;
- 能够自动学习出缺失值的处理策略。