GBDT、XGBoost是校招算法面试中的重点难点,这是一份精心总结的关于集成学习面试中常见问题,如果对您有帮助的话,不妨点赞、收藏、关注!!!
集成学习方法可以分为Bagging和Boosting。
Bagging方法在训练过程中,各基分类器之间无强依赖,可以进行并行训练。Bagging的基本思路是:每一次从原始数据中自助采样(有放回),样本点可能出现重复,不同的训练集随机子集在基模型上进行训练,再对这些基模型进行组合,通过投票的方式作出最后的集体决策。Bagging方法之所以有效,是因为每个模型都是在略微不同的训练数据集上拟合完成的,这又使得每个基模型之间存在略微的差异,使每个基模型拥有略微不同的训练能力。
Boosting方法在训练过程中,基分类器采用串行方式,各个基分类器之间有依赖。Boosting的基本思路是将基分类器层层叠加,后一个基分类器在训练时,对前一个基分类器分错的样本给予更高的权重,使模型在之后的训练中更加注意难以分类的样本,这是一个不断学习的过程。
除了决策树外,神经网络模型也适合作为基分类器,主要由于神经网络模型也比较“不稳定”,而且还可以通过调整神经元数量、连接方式、网络层数、初始权值等方式引入随机性。
偏差指的是由所有采样得到的大小为m的训练数据集训练出的模型的输出的平均值和真实模型输出之间的偏差。偏差主要是由于分类器的表达能力有限导致的系统性错误,表现在训练误差不收敛。通俗来讲,就是假设错了,导致分类器没选好
方差指的是由所有采样得到的大小为m的训练数据集训练出的模型的输出的方差。方差通常是由于模型的复杂度相对于训练样本数m过高导致的。方差是由于分类器对于样本分布过于敏感,导致在训练样本数较少时,产生过拟合。
Bagging方法则是采取分而治之的策略,通过对训练样本多次重采样,并分别训练出多个不同模型,然后做综合,来减小集成分类器的方差。假设模型是独立不相关的,对n个独立不相关的模型的预测结果取平均,方差是原来单个模型的1/n。
当然,模型之间不可能完全独立。为了追求模型的独立性,诸多Bagging 的方法做了不同的改进。比如在随机森林算法中,每次选取节点分裂属性时,会随机抽取一个属性子集,而不是从所有属性中选取最优属性,这就是为了避免弱分类器之间过强的相关性。通过训练集的重采样也能够带来弱分类器之间的一定独立性,从而降低 Bagging后模型的方差。
Boosting方法是通过逐步聚焦于基分类器分错的样本,不断较小损失函数,来使模型接近“靶心”,使得模型偏差不断降低。但是Boosting的过程并不会显著降低方差,这是因为Boosting的训练过程使得各弱分类器之间是强相关的,缺乏独立性,所以并不会降低方差。
首先,先弄清楚一个点:GBDT的核心在于累加所有树的结果作为最终结果,GBDT可用于分类,并不代表是累加所有分类树的结果。GBDT中的树都是回归树
梯度提升树:当损失函数是平方损失时,下一棵树拟合的是上一棵树的残差值(实际值减预测值)。当损失函数是非平方损失时,拟合的是损失函数的负梯度值,利用损失函数的负梯度在当前模型的值作为残差的一个近似值,进行拟合回归树。梯度提升算法实际采用加法模型(基函数的线性组合)与前向分布算法。
梯度提升算法的基本流程可以总结为:在每一次迭代中,首先计算出当前模型在所有样本上的负梯度,然后以该值为目标训练一个新的弱分类器进行拟合并计算出该弱分类器的权重,最后实现对模型的更新,其算法伪代码如下:
算法伪代码中, N N N表示数据的数量, M M M表示基分类器的数量, h ( x i : a m ) h(x_i:a_m) h(xi:am)表示基本分类器 ,4中 a m a_m am表示拟合负梯度能力最好的分类器参数,负梯度只是表示下降的方向,但是下降多少没有确定,5中 p m p_m pm可以认为是下降最快的步长(也可以认为是学习率),可以让 L o s s Loss Loss最小,可以用线性搜索的方式来估计 p m p_m pm的值。
一个包含 J J J个节点的回归树模型可以表示为
h ( x , { b j , R j } 1 J ) = ∑ j = 1 J b j I ( x ∈ R j ) h(x,\{b_j,R_j\}_{1}^{J})=\sum_{j=1}^{J}b_jI(x∈R_j) h(x,{ bj,Rj}1J)=j=1∑JbjI(x∈Rj)
其中 { R j } 1 J \{R_j\}_{1}^{J} { Rj}1J是不相交区域,可以认为是落在一颗决策树叶子节点上的 x x x值的集合; { b j } 1 J \{b_j\}_{1}^{J} { bj}1J是叶子节点的值。 b j b_j bj的计算方式:对于回归问题,计算方式可以取落在该叶子节点的样本点的平均值;对于分类问题,计算方式可以取落在该叶子节点中数量多的类别。
从算法中,我们可以看出先求出 b j m b_{jm} bjm,然后在求解 ρ m \rho_{m} ρm,那可不可以一起求解呢?
F m ( x ) = F m − 1 ( x ) + ρ m ∑ j = 1 J b j m I ( x ∈ R j m ) F_m(x)=F_{m-1}(x)+\rho_{m}\sum_{j=1}^{J}b_{jm}I(x∈R_{jm}) Fm(x)=Fm−1(x)+ρmj=1∑JbjmI(x∈Rjm)
令 γ j m = ρ m b j m \gamma_{jm}=\rho_{m}b_{jm} γjm=ρmbjm
F m ( x ) = F m − 1 ( x ) + ∑ j = 1 J γ j m I ( x ∈ R j m ) F_m(x)=F_{m-1}(x)+\sum_{j=1}^{J}\gamma_{jm}I(x∈R_{jm}) Fm(x)=Fm−1(x)+j=1∑JγjmI(x∈Rjm)
这样就把几个参数合并成一个来求解,并且由于叶子节点各个区域互不相加,样本最终都会属于某个节点因此可以求解如下公式来获取最优 γ j m \gamma_{jm} γjm
γ j m = a r g m i n γ ∑ x i ∈ R j m L ( y i , F m − 1 ( x i ) + γ ) \gamma_{jm}=argmin_{\gamma}\sum_{x_i∈R_{jm}}L(y_i,F_{m-1}(x_i)+\gamma) γjm=argminγxi∈Rjm∑L(yi,Fm−1(xi)+γ)
给定当前的 F m − 1 ( x i ) F_{m-1}(x_i) Fm−1(xi), γ j m \gamma_{jm} γjm可以作为叶子节点的值,该值可以看做是基于损失函数L的每一个叶子节点的理想更新值,也可以认为 γ j m \gamma_{jm} γjm既有下降方向又有下降步长。
其中4可能不太好理解,可以理解为求解一棵树,并且形成了不相交的叶子节点区域
因此,整体的算法可以如下表示:
现在我们只需要理解其初始值是如何设置的以及怎么使用牛顿法得出近似结果的,就可以实现二分类的GBDT算法。
想要了解huber损失函数的可以参考这个博客:最常用的5个回归损失函数
GBDT基本流程可以总结为:在每一次迭代中,首先计算出当前模型在所有样本上的负梯度,然后以该值为目标训练一个新的弱分类器进行拟合并计算出该弱分类器的权重,最后实现对模型的更新。
GBDT树的基分类器一般使用CART回归树。CART回归树采用平方误差最小化准则进行特征选择生成二叉树。
GBDT树可用于分类与回归任务,不同任务对应的损失函数不同。以原始论文中为例,二分类问题的GBDT中CART树将采用对数损失最小化准则进行特征选择生成二叉树。三分类问题的GBDT中CART树将采用交叉熵损失最小化准则进行特征选择生成二叉树。回归问题的GBDT中CART树将采用huber损失最小化准则进行特征选择生成二叉树,或者直接采用平方误差最小化准则进行特征选择生成二叉树。
参考二/三分类问题的GBDT算法来答
优点:
局限性:
相同点:
不同点:
Adaboost是通过提高错分样本的权重来定位模型的不足;GBDT是通过负梯度来定位模型的不足,因此GBDT可以使用更多种类的损失函数。
决策树可以用于特征构造,创建新特征。在回答GBDT是怎么构建特征的?
之前,我们先分析GBDT相对于单颗决策树与随机森林在特征构造方面的优势(即为什么选择GBDT算法):
那么,GBDT是怎么构造特征的呢?
gbdt本身是不能产生特征的,但是我们可以利用gbdt去产生特征的组合。Facebook在2014年发表的一篇论文就是利用gbdt去产生有效的特征组合,以便用于逻辑回归的训练,提升模型最终的效果。比如:我们使用GBDT生成了两棵树,两颗树一共有五个叶子节点。我们将样本×输入到两颗树当中去,样本X落在了第一棵树的第二个叶子节点,第二颗树的第一个叶子节点,于是我们便可以依次构建一个五纬的特征向量,每一个纬度代表了一个叶子节点,样本落在这个叶子节点上面的话那么值为1,没有落在该叶子节点的话,那么值为0。于是对于该样本,我们可以得到一个向量[0,1,0,1,0]作为该样本的组合特征,和原来的特征一起输入到逻辑回归当中进行训练。实验证明这样会得到比较显著的效果提升。且工业界现在已有实践,GBDT+LR、GBDT+FM都是值得尝试的思路。这里介绍一个GBDT+LR的实现过程。
import numpy as np
import random
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_curve, roc_auc_score
# 生成随机数据
np.random.seed(10)
X, Y = make_classification(n_samples=1000, n_features=30)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, random_state=233, test_size=0.4)
# LR模型
LR = LogisticRegression()
LR.fit(X_train, Y_train)
y_pred = LR.predict_proba(X_test)[:, 1]
fpr, tpr, _ = roc_curve(Y_test, y_pred)
auc = roc_auc_score(Y_test, y_pred)
print('LogisticRegression: ', auc)
# 训练GBDT模型
gbdt = GradientBoostingClassifier(n_estimators=10)
gbdt.fit(X_train, Y_train)
# 将训练好的树应用到X_train,返回叶索引,shape大小为(600,10,1)
print(gbdt.apply(X_train)[:, :, 0])
# print(gbdt.apply(X_train)[:, :, 0].shape)
# 对GBDT预测结果进行onehot编码
onehot = OneHotEncoder()
onehot.fit(gbdt.apply(X_train)[:, :, 0])
print(onehot.transform(gbdt.apply(X_train)[:, :, 0]))
# 训练LR模型
lr = LogisticRegression()
lr.fit(onehot.transform(gbdt.apply(X_train)[:, :, 0]), Y_train)
# 测试集预测
Y_pred = lr.predict_proba(onehot.transform(gbdt.apply(X_test)[:, :, 0]))[:, 1]
fpr, tpr, _ = roc_curve(Y_test, Y_pred)
auc = roc_auc_score(Y_test, Y_pred)
print('GradientBoosting + LogisticRegression: ', auc)
LogisticRegression: 0.9349156965074267
GradientBoosting + LogisticRegression: 0.9412133681252508
基于这三个博客对XGBoost算法中重要的点进行总结
机器学习—XGboost的原理、工程实现与优缺点
机器学习—XGBoost实战与调参
机器学习—XGBoost常见问题解析
Xgboost以CART决策树为子模型,通过Gradient Tree Boosting实现多棵CART树的集成学习,得到最终模型。XGBoost本质上还是一个GBDT,但是力争把速度和效率发挥到极致;XGBoost是一个优化的分布式梯度增强库,旨在实现高效,灵活和便携;XGBoost提供了并行树提升,可以快速准确地解决许多数据科学问题;在工业界大规模数据方面,XGBoost的分布式版本有广泛的可移植性,支持在Hadoop等分布式环境下运行。下面以陈天奇XGBoost
论文为框架来讲解,XGBoost论文链接提取码:1234
XGBoost
与GBDT
一样同样适用了加法模型与前向分布算法,其区别在于,GBDT
通过经验风险最小化来确定下一个决策树参数,XGBoost
通过结构风险极小化来确定下一个决策树参数 Θ m \Theta_{m} Θm,通俗理解来说,就是XGBoost
目标函数中加入了正则项
XGBoost
与GBDT
的一个重要区别,其目标函数可写为:GBDT
是泰勒一阶展开式,XGBoost
是泰勒二阶展开式XGBoost
与GBDT
对决策树进行了改写,两者类似,只不过改写的结果不同 首先先明白XGBoost
目标函数中的正则项
Ω ( h m ( x ⃗ ) ) \Omega(h_m(\vec{x})) Ω(hm(x))是如何计算的?
然后将正则化项公式代入目标函数(损失函数)中,得到
则损失函数可化简为:
此时假设 w j 和 T 、 G j 、 H j w_j和T、G_j、H_j wj和T、Gj、Hj 都无关的话,那么损失函数实质上是一个关于 w j w_j wj的一元二次方程。根据一元二次方程求极值公式可得:
将 w j ∗ w_j^* wj∗代入损失函数得,
这个就是所谓的结构分
上一步的化简,我们假设 w j 和 T 、 G j 、 H j w_j和T、G_j、H_j wj和T、Gj、Hj都无关, w j w_j wj表示树的叶子节点,所以实质是在假设已知树的结构,而事实上损失函数与 T T T是相关的,甚至和树的结构相关,所以定义 L ∗ L^* L∗为一种
scoring function
,来衡量已知树结构情况下目标函数的最小值。
这就把损失函数求极值问题转化成衡量已知树结构情况下损失函数的最小值的问题,那么,我们只要知道了树结构,我们就可以解决这个问题。下面举例:在已知树结构下,如何求目标函数的最小值?
现在的问题是:如何得到最佳的树的结构,来使得目标函数全局最小?我们可以采用贪心算法来寻找最佳树结构使得目标函数全局最小。贪心算法的思想:对现有的叶结点加入一个分裂,然后考虑分裂之后目标函数降低多少,如果目标函数下降,则说明可以分裂,如果目标函数不下降,则说明该叶结点不宜分裂。
贪心算法判断叶结点适不适宜分裂具体过程为:对于叶结点的某个特征,假如给定其分裂点,定义划分到左子结点的样本的集合为: I L = i ∣ q ( x ⃗ i ) = L \Iota_{L}={i|q(\vec{x}_i)=L} IL=i∣q(xi)=L;定义划分到右子结点的样本的集合为: I R = i ∣ q ( x ⃗ i ) = R \Iota_{R}={i|q(\vec{x}_i)=R} IR=i∣q(xi)=R,则右:
上面一段我们已经知道了如何判断某个叶结点适不适宜分裂,但是这是在假设给定其分裂点的前提下。对于每个叶结点来说,存在很多个分裂点,且可能很多分类点都能够带来收益。那么我们应该如何解决呢?解决的办法是:对于叶结点中的所有可能的分裂点排序后进行一次扫描,然后计算每个分裂点的增益,选取增益最大的分裂点作为本叶结点的最优分裂点。
贪心算法寻找分裂点伪代码如下:
贪心算法的缺点为:分裂点贪心算法尝试所有特征和所有分裂位置,从而求得最优分裂点。当样本太大且特征为连续值时,这种暴力做法的计算量太大,计算效率比较低。并且,贪心算法也不容易进行并行运算。不过,我们在平时用到的XGBoost的单线程版本与sklearn版本都是基于贪心算法来寻找分裂点的。接下来,介绍的近似算法正是对贪心算法这一缺点的改进。
近似算法针对贪心算法计算量大的缺点,用分桶的思想,使得循环的次数减少(贪心算法每一个点都要循环一次,近似算法每一个桶循环一次)。近似算法的思想为:
近似算法寻找分裂点伪代码如下:
近似算法伪代码中提到了两种分桶模式:全局分桶与局部分桶。
分桶时的桶区间间隔大小是个重要的参数。区间间隔越小,则桶越多,则划分的越精细,候选的拆分点就越多。
XGBoost算法中运用的是加权分桶算法。首先,先定义一个排序函数,
我们已经知道了什么是加权分桶?那么我们应该怎么理解加权分桶呢?
损失函数:
可以改写为:
原论文,这里写作减号,我们实际推导下来会发现会是加号,可以这么理解:这里不管是加号还是减号都不会影响函数的最值 4 a c − b 2 4 a \frac{4ac-b^2}{4a} 4a4ac−b2。改写以后,很像平方损失,可以发现对于第 t t t棵决策树而言,它等价于样本 x i x_i xi的真实label为 g i h i \frac{g_i}{h_i} higi,权重为 h i h_i hi,损失函数为平方损失。因此分桶时每个桶的权重为 h h h,所以分桶时使得 h h h更加均匀会使近似算法效果更好一点。
原论文中给出了贪心算法、近似算法全局分桶、近似算法局部分桶的方法比较:
这是一个面试中经常问的问题。
真实场景中,有很多可能导致产生稀疏。如:数据缺失、某个特征上出现很多0项、人工进行one-hot编码导致的大量的0。
我们看输出可以看到返回的是当前叶结点的最佳分裂点,前面也介绍了几个算法来寻找最佳分类点,所以缺失值处理算法并不是一个新的算法,只是在寻找分裂点的算法中加入了一些手段,使算法能够处理缺失值。XGBoost缺失值处理算法如下:
原论文中算法伪代码如下:
预排序—列块并行学习
在树的生成过程中,最耗时的一个步骤是在每次寻找最佳分裂点时都需要对特征的值进行排序。而xgboost在训练之前会根据特征对数据进行排序,然后保存到块结构中,并在每个块结构中都采用了稀疏矩阵存储格式(CSC)进行存储,后面的训练过程中会重复的使用块结构,可以大大减小工作量。
缓存访问
列块并行学习的设计可以减少节点分裂时的计算量,在顺序访问特征值时,访问的是一块连续的内存空间,但通过特征值持有的索引(样本索引)访问样本获取一阶、二阶导数时,这个访问操作访问的内存空间并不连续,这样可能造成cpu缓存命中率低,影响算法效率。
为了解决缓存命中率低的问题,XGBoost 提出了缓存访问算法:为每个线程分配一个连续的缓存区,将需要的梯度信息存放在缓冲区中,这样就实现了非连续空间到连续空间的转换,提高了算法效率。此外适当调整块大小,也可以有助于缓存优化。
“核外”块计算
当数据量非常大时,我们不能把所有的数据都加载到内存中。那么就必须将一部分需要加载进内存的数据先存放在硬盘中,当需要时再加载进内存。这样操作具有很明显的瓶颈,即硬盘的IO操作速度远远低于内存的处理速度,肯定会存在大量等待硬盘IO操作的情况。针对这个问题作者提出了“核外”计算的优化方法。具体操作为,将数据集分成多个块存放在硬盘中,使用一个独立的线程专门从硬盘读取数据,加载到内存中,这样算法在内存中处理数据就可以和从硬盘读取数据同时进行。此外,XGBoost 还用了两种方法来降低硬盘读写的开销:
块压缩(Block Compression)。数据按列进行压缩,并且在硬盘到内存的传输过程中自动解压缩。对于行索引,只保存第一个索引值,然后用16位的整数保存与该block第一个索引的差值。作者通过测试在block设置为 2 16 2^{16} 216个样本大小时,压缩比率几乎达到 26 % ∼ 29 % 26\% \sim 29\% 26%∼29%
块分区(Block Sharding )。块分区是将特征block分区存放在不同的硬盘上,每个硬盘对应一个预取线程,以此来增加硬盘IO的吞吐量。
XGB本质上仍然采用boosting 思想,每棵树训练前需要等前面的树训练完成才能开始训练。XGBoost 的并行,并不是tree
维度上的并行,而是特征维度上的并行。在训练之前,每个特征按特征值对样本进行预排序,并存储为Block结构,在后面查找特征分割点时可以重复使用,而且特征已经被存储为一个个block结构,那么在寻找每个特征的最佳分割点时,可以利用多线程对每个block 并行计算。
early stopping
:如果经过固定的迭代次数后,并没有在验证集上改善性能,停止训练过程shrinkage
: 可以叫做学习率或步长,调小学习率增加树的数量,为了给后面的训练留出更多的学习空间max_depth
,min_child_weight
,gamma
等参数。subsample
,colsample_bytree
。learning rate
,但需要同时增加estimator
参数。机器学习—XGBoost实战与调参
XGBoost在训练前预先将特征按照特征值进行了排序,并存储为block结构,以后在结点分裂时可以重复使用该结构。因此,可以采用特征并行的方法利用多个线程分别计算每个特征的最佳分割点,根据每次分裂后产生的增益,最终选择增益最大的那个特征的特征值作为最佳分裂点。如果在计算每个特征的最佳分割点时,对每个样本都进行遍历,计算复杂度会很大,这种全局扫描的方法并不适用大数据的场景。XGBoost还提供了一种直方图近似算法,对特征排序后仅选择常数个候选分裂位置作为候选分裂点,极大提升了结点分裂时的计算效率。
优点:
缺点:
不需要。首先,归一化是对连续特征来说的。那么连续特征的归一化,起到的主要作用是进行数值缩放。数值缩放的目的是解决梯度下降时,等高线是椭圆导致迭代次数增多的问题。而xgboost等树模型是不能进行梯度下降的,因为树模型是阶越的,不可导。树模型是通过寻找特征的最优分裂点来完成优化的。由于归一化不会改变分裂点的位置,因此xgboost不需要进行归一化。
xgboost支持离散类别特征进行onehot编码,因为xgboost只支持数值型的特征。但是不提倡对离散值特别多的特征通过one-hot的方式进行处理。因为one-hot进行特征打散的影响,其实是会增加树的深度。针对取值特别多的离散特征,我们可以通过embedding
的方式映射成低纬向量。与单热编码相比,实体嵌入不仅减少了内存使用并加速了神经网络,更重要的是通过在嵌入空间中映射彼此接近的相似值,它揭示了分类变量的内在属性。
XGBoost中有三个参数可以用于评估特征重要性:
参考:
如果对您有帮助,麻烦点赞关注,这真的对我很重要!!!如果需要互关,请评论留言!