文章目录
关系梳理
提升树:拟合残差的boosting树模型,是一类模型的思想概括;
GDBT:广义GDBT就是指提升树模型;狭义GDBT指最原始的提升树,使用一阶导数拟合;
XGBoost:使用二阶导数并加入正则项的提升树模型。
如果GDBT取广义,那么“XGBoost是GDBT的一种”;如果GDBT取狭义,那么“XGBoost是GDBT的改进”,两种说法都有道理。
LightGBM:XGBoost的改进版,包括:并行方案、基于梯度的单边检测、排他性特征捆绑等。
此外,GBDT是一种机器学习算法,而XGBoost和LightGBM是工程上实现的框架。
Xgboost
XGBoost的全称是eXtreme Gradient Boosting,是2014年3月陈天奇博士提出的,是基于CART树的一种boosting算法。
原理介绍
xgboost是集成了K棵树之后的集成模型,如下图所示。
- 每个样本需要经过K棵决策树
- 最终的结果是每棵树的结果(对应叶子节点分数)之和,如下图所示。
对应的有以下两个关键问题: - 针对每棵树,如何计算每个叶子的分数f(x)?
- 针对每棵树,如何构建整棵树的结构?
计算叶子阶段的分数
1. 定义目标函数
XGboost采用的是加法训练,首先优化第一棵树,之后再优化第二棵树,直至优化完k棵树。
在优化第t棵树时,前t-1课已定,
y
t
=
y
t
−
1
+
f
t
(
x
)
y^t = y^{t-1}+f_t(x)
yt=yt−1+ft(x)。
n为样本数量,Obj为预测值与真实值之间的损失
l
(
y
,
y
^
)
l(y,\hat{y})
l(y,y^)(如MSE)与惩罚项
Ω
\Omega
Ω 之和。
2. 二阶泰勒展开
XGboost使用二阶泰勒展开进行优化。泰勒公式(二阶):
此处将
y
^
i
t
−
1
=
f
1
,
.
.
.
,
t
−
1
(
x
i
)
\hat{y}_i^{t-1}=f_{1,...,t-1}(x_i)
y^it−1=f1,...,t−1(xi)看做x,
f
t
(
x
i
)
f_t(x_i)
ft(xi)看做△x,
l
(
y
i
,
y
^
i
t
−
1
)
l(y_i,\hat{y}_i^{t-1})
l(yi,y^it−1)看做泰勒公式中的
f
(
x
)
f(x)
f(x),得:
其中
l
(
y
i
,
y
^
i
t
−
1
)
l(y_i,\hat{y}_i^{t-1})
l(yi,y^it−1)为常数项(前t-1棵树已经优化完了),所以可以去掉,对函数没有影响;
g
i
g_i
gi和
h
i
h_i
hi分别表示对x(即
y
^
t
−
1
\hat{y}^{t-1}
y^t−1)的一阶、二阶导数(函数中没有
f
t
(
x
)
f_t(x)
ft(x),也是常数)。
3. 惩罚项量化
树的复杂度由叶子结点个数T和各个叶子结点的w(f(x))表示:
进一步将
f
t
(
x
)
f_t(x)
ft(x)替换为
w
w
w,其中
I
j
I_j
Ij表示被分到第j个叶子结点下的样本的下标集合,求导得最终w(f(x)):
最终的目标函数即为:
构建第k棵树的结构
1. 贪心算法
构建树有多种算法,这里我们以贪心算法为例,说明构建树的过程。
算法流程
- 对每个节点枚举所有的可用特征;
- 针对每个特征,把属于该节点的训练样本根据该特征的值进行升序排列,通过线性扫描的方式来决定该特征的最佳分裂点,并记录该特征的分裂收益;
- 选择收益最大的特征作为分裂特征,用该特征的最佳分裂点作为分裂位置,在该节点上分裂出左右两个新的叶节点,并为每个新节点关联对应的样本集;
- 回到第1步,递归执行直到满足特定条件为止。
因为我们想要得到最小值,所以判断当前节点是否继续划分的标准是:划分后的score应比划分前的score要更小;确定划分点的标准为:选择(划分前score-划分后score)最大的切分点作为这一次的划分点。
设
I
=
I
L
∪
I
R
I = I_L\cup I_R
I=IL∪IR,则有:
2. 近似算法:分桶
用贪心算法来寻找最佳划分点,准确度非常不错,但是时间复杂度和空间复杂度都太高了,作者提出一种近似法分位法,先对数据进行分桶(Bucket),然后桶内的数据相加起来,作为一个代表来进行计算。
分桶有两种方式:
- 全局分桶(Global Bucket),可以一开始就对全部的数据进行分桶。后面进行划分的时候只需要使用分桶数据就可以了。
- 局部分桶(Local Bucket),每次需要对当前leaf node 进行划分的时候,对当前节点里面的数据进行分桶。然后再划分。当然这个时间复杂度也会变的比较高。
作者测试对几种不同的切分算法的AUC结果比较。可以看得出,当eps=0.05,也就是将数据分成20个Bucket的时候,AUC的分数跟精准的贪心算法一样。
常见面试问题
- 如何实现并行?
Boosting不是一种串行的结构吗?怎么并行的?注意Xgboost的并行不是tree粒度的并行,Xgboost也是一次迭代完才能进行下一次迭代的(第t次迭代的损失函数里包含了前面t−1次迭代的预测值)。Xgboost的并行是在特征粒度上的。决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),Xgboost在训练之前,预先对数据进行了排序,然后保存为block(块)结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行
- 怎样防止过拟合
① 在损失函数中添加正则项。
② 添加类似梯度下降优化问题中的学习率 η \eta η,在累加时用得分乘以这个学习率。这可以收缩每棵树的权重,让每棵树的生长更加稳定。
③ 对样本的特征随机抽样。每次生成树的时候,只用其中一部分抽样的特征。这样子也能搞比较好的降低过拟合的风险。而且有研究怎么特征抽样效果比样本抽样效果更好。 - 学习率的作用?在公式的什么地方?
Shrinkage(缩减):相当于学习速率。XGBoost 在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。传统GBDT的实现也有学习速率;
公式=预测值+正则项,预测值和正则项两部分都包含学习率参数。 - 如何处理缺失值?
手动处理:使用均值;统一使用标记值(如-1)
自动处理:每次计算Gain的时候分别遍历将含有缺失值的结点分别加到左边或者右边,选择Gain最大的方向作为缺失值应该在的方向。
- 不足
xgboost本质上还是一个基于现有特征的分类器,并不能实现特征交叉的功能,需要手动手工的对特征进行交叉发现新的有效特征。 - XGboost相对于GDBT的改进?
① 传统的GBDT算法以CART作为基分类器,xgboost还可以支持线性分类器,相当于带L1和L2的逻辑回归或者线性回归。
② 传统的GBDT在优化的时候,使用的是一阶导数信息,xgboost则对代价函数进行了二阶泰勒展开,同时用到了一阶导数和二阶导数。顺便提一下,xgboost工具支持自定义代价函数,只要函数可一阶和二阶求导。
③ xgboost在代价函数中加入了正则项,用于控制模型的复杂度。正则项里面包括树的叶子节点的个数、每个叶子节点上输出的score的L2模的平方和。( 从Bias-variance tradeoff 的角度来说,正则化降低了模型的variance,使得到的模型更加简单,防止过拟合,这是xgboost优于传统的GBDT的一个特征)
④ 列抽样(column sampling),借鉴了随机森林的做法,支持列抽样可以降低过拟合,同时减少了计算量,这也是xgboost异于传统gbdt的一个特性,sklearn中已经实现行采样和列采样,同样xgboost也是可以实现的。 - XGBoost是否需要归一化?
不需要。连续特征的归一化,作用是解决梯度下降时,等高线是椭圆导致迭代次数增多的问题。而xgboost等树模型是阶跃(不可导)的,不能进行梯度下降,而是通过寻找特征的最优分裂点来完成优化的。由于归一化不会改变分裂点的位置,因此xgboost不需要进行归一化。
(此外,归一化也不会影响信息熵增益的值,因此在决策树模型中也不适用。) - 怎样计算特征重要性?
Python中直接调用xgboost库的feature_importance函数,原理是根据每个特征的增益进行排序。
由损失函数:
得到信息增益Gain的公式:
实际计算使用每棵子树的所有信息增益的平均值,得到最终该特征的Gain。
Python API: xgboost
- xgboost.XGBClassifier().fit()和xgboost.train()的区别
Python xgboost库中有两种训练模型的方法。
import xgboost as xgb
① xgboost.XGBClassifier().fit()
xgm = xgb.XGBClassifier()
xgm.fit(X_train, y_train)
y_pred = xgm.predict(X_test)
② xgboost.train()
param = {'max_depth':2, 'eta':1, 'silent':1, 'objective':'binary:logistic' }
dtrain = xgb.DMatrix(X_train, y_train)
dtest = xgb.DMatrix(X_test)
bst = xgb.train(list(param), dtrain)
preds = bst.predict(dtest)
- xgb.train()中参数的解释
参见这篇博客 - 官方文档
XGboost documentation - 类别型变量的处理
有序型:LabelEncoder;无序型:onehot/embedding
onehot:本质上被当做n个特征,每次分裂one vs rest,数据量较少时会导致切分数据不平衡;
embedding:可以避免onehot的问题。
LightGBM
LightGBM相对于XGBoost的改进
LightGBM相对于XGBoost的改进包括:并行方案、基于梯度的单边检测、排他性特征捆绑等。
LightGBM的并行方案
LightGBM改进了XGBoost的特征并行方案,并且提出了数据并行方案和投票并行方案。
-
特征并行方案
LightGBM并没有垂直切分(即按特征切分)数据集,而是每个worker都有全量的训练数据,避免了通讯开销过大的问题。
最优的特征分裂结果不需要传输到其他worker中,只需要将最优特征以及分裂点告诉其他worker,worker随后本地自己进行数据分裂,生成决策树。 -
数据并行方案
当数据量很大时,特征并行算法受限于特征分裂效率,此时推荐使用数据并行算法。
为避免过大通讯开销,使用Reduce Scatter机制归并不同worker的不同特征子集的直方图,找到最优局部分裂信息,最终同步找到全局最优。(没懂) -
基于投票的并行方案
在每个worker中选出top k个分裂特征,将所有top-k进行汇总,选出全局分裂特征进行分裂。
可以证明,该方法有很大的概率选出实际最优的特征。
基于梯度的单边检测(GOSS)
GOSS是一个样本的采样算法,目的是丢弃一些对计算信息增益没有帮助的样本。梯度大的样本对信息增益有更大的影响,但直接抛弃拥有小梯度的实例数据会使据的分布发生改变。
因此,GOSS方法保持有较大梯度的实例,而在小梯度数据上运行随机采样。既减少了时间和空间开销,又保证了计算精度。
排他性特征捆绑(EFB)
在实际应用中,高维度特征具有稀疏性,且许多特征具有互斥性,如one-hot。捆绑互斥的特征可以减小特征数量。
常见面试问题
- LightGBM和XGBoost对比
1)XGBoost使用基于预排序的决策树算法,每遍历一个特征就需要计算一次特征的增益,时间复杂度为O(datafeature)。
而LightGBM使用基于直方图的决策树算法,直方图的优化算法只需要计算K次,时间复杂度为O(Kfeature)
2)XGBoost使用按层生长(level-wise)的决策树生长策略,LightGBM则采用带有深度限制的按叶子节点(leaf-wise)算法。在分裂次数相同的情况下,leaf-wise可以降低更多的误差,得到更好的精度。leaf-wise的缺点在于会产生较深的决策树,产生过拟合。
3)支持类别特征,不需要进行独热编码处理
4)优化了特征并行和数据并行算法,除此之外还添加了投票并行方案
5)采用基于梯度的单边采样来保持数据分布,减少模型因数据分布发生变化而造成的模型精度下降
6)特征捆绑转化为图着色问题,减少特征数量 - LightGBM的直方图算法
直方图算法的思想:将每个特征的连续浮点数映射为k个整数,并根据其出现频率构造直方图。从特征处理的角度即数据离散化。
优点:
1)占用内存更小,不需要存储浮点数和索引,只需要存储整型;
2)计算开销更小,XGBoost需要遍历所有特征值,LightGBM的时间复杂度从 O ( # d a t a ∗ # f e a t u r e ) O(\#data*\#feature) O(#data∗#feature)降低到 O ( k ∗ # f e a t u r e ) O(k*\#feature) O(k∗#feature), # d a t a > > k \#data>>k #data>>k。
3)使用直方图作差加快训练速度。我们只需要对其中一个子节点进行数据传输,其兄弟节点可以通过父节点和该节点作差得到。