集成学习(ensemble learning)通过构建并结合多个学习器来完成学习任务。根据个体学习器的生成方式,目前的集成学习方法大致可分为两大类,即个体学习器间存在强依赖关系、必须串行生成的序列化方法,代表为Boosting;以及个体学习器间不存在强依赖关系、可同时生成的并行化方法,代表为Bagging和随机森林(Random Forest)。
决策树有以下优点:
决策树有以下缺点:
分类与回归树CART是由Loe Breiman等人在1984年提出的,自提出后被广泛的应用。看这名字也知道CART既能用于分类也能用于回归,关于分类我们这里就不介绍了,因为和ID3、C4.5等常见的决策树相比较,CART除了把选择最优特征的方法从信息增益(率)换成了基尼指数,其他的没啥不同。这里介绍CART用户回归任务。主要有以下步骤:
- 遍历每个特征:
- 对于特征 f i f_i fi ,遍历每个取值 s s s :
- 用切分点s将数据分成两份,计算切分后的误差
求出误差最小的特征及其对应的切分点,此特征即被选中作为分裂点,切分点则形成左右分支。
递归重复以上步骤,下一次递归时已经分裂的特征就不用考虑了。
这里会产生个问题:我们上面讲了要计算切分后的误差,那么这个误差怎么计算?这个误差用的是均方误差来计算的,均方误差的话真实值是知道的,那么预测值是什么?在揭晓预测值之前先来形式化的定义下,不然不好描述。
假设特征 j j j 和切分点为 s s s ,把样本划分为两部分 R 1 R_1 R1 和 R 2 R_2 R2 ,每部分的预测值分别为 c 1 c_1 c1 和 c 2 c_2 c2 ,则均方误差(损失函数)为:
∑ x i ∈ R 1 ( y i − c 1 ) 2 + ∑ x i ∈ R 2 ( y i − c 2 ) 2 \sum_{x_i \in R_1}(y_i-c_1)^2 + \sum_{x_i \in R_2}(y_i-c_2)^2 xi∈R1∑(yi−c1)2+xi∈R2∑(yi−c2)2
我们的目标是要求 c 1 c_1 c1 和 c 2 c_2 c2 使得上式最小,也就是我们的优化目标为:
min j , s ( min c 1 ∑ x i ∈ R 1 ( y i − c 1 ) 2 + min c 2 ∑ x i ∈ R 2 ( y i − c 2 ) 2 ) \min_{j,s}(\min_{c_1}\sum_{x_i \in R_1}(y_i-c_1)^2 + \min_{c_2}\sum_{x_i \in R_2}(y_i-c_2)^2) j,smin(c1minxi∈R1∑(yi−c1)2+c2minxi∈R2∑(yi−c2)2)
对于里面的式子:
min c 1 ∑ x i ∈ R 1 ( y i − c 1 ) 2 + min c 2 ∑ x i ∈ R 2 ( y i − c 2 ) 2 \min_{c_1}\sum_{x_i \in R_1}(y_i-c_1)^2 + \min_{c_2}\sum_{x_i \in R_2}(y_i-c_2)^2 c1minxi∈R1∑(yi−c1)2+c2minxi∈R2∑(yi−c2)2
因为是个加法,只要分别求两项的最小值即可,直接求导即可:
c 1 = 1 N 1 ∑ x i ∈ R 1 ∑ y i c_1 = \frac{1}{N_1}\sum_{x_i \in R_1}\sum y_i c1=N11xi∈R1∑∑yi
c 2 = 1 N 2 ∑ x i ∈ R 2 ∑ y i c_2 = \frac{1}{N_2}\sum_{x_i \in R_2}\sum y_i c2=N21xi∈R2∑∑yi
里面的最小值知道了,外层的 min j , s \min_{j,s} minj,s 就回到了我们开头写的两个for循环遍历了。遍历每个切分点所对应的损失函数,找到其最小值的切分点即可。
最后这个回归模型的预测值为:
f ( x ) = ∑ m = 1 M c m I ( x ∈ R m ) f(x) = \sum_{m=1}^M c_m I(x \in R_m) f(x)=m=1∑McmI(x∈Rm)
关于这个公式解释下,就是最终构建完一颗完整的树(剪枝)后,会把样本集划分为M个区域(就是M个叶子节点),当预测新样本时,按照构造好的决策树走到最后的叶子节点,然后取叶子节点里所有样本的平均值作为预测值(即上面的公式)。
AdaBoosting算法是Boosting族算法中最著名、也是最经典的二分类算法。算法模型可表示为基学习器的线性组合:
H ( x ) = ∑ t = 1 T α t h t ( x ) H(x) = \sum_{t=1}^{T} \alpha_t h_t(x) H(x)=t=1∑Tαtht(x)
在AdaBoost算法中,第一个基分类器 h 1 h_1 h1 是通过直接将基学习算法用于初始数据而得;此后迭代地生成 h t h_t ht 和 α t \alpha _t αt ,当基分类器 h t h_t ht 基于分布 D t D_t Dt 产生后,该基分类器的权重 α t \alpha _t αt 应使得 α t h t \alpha _t h_t αtht 最小化指数损失函数(该损失函数在二分类中的有效性,这里不再证明)。
L ( α t h t ∣ D t ) = E x ∼ D t exp [ − y ∗ α t h t ( x ) ] = E x ∼ D t [ e − α t I ( y = h t ( x ) ) + e − α t I ( y ≠ h t ( x ) ) ] = e − α t P x ∼ D t ( y = h t ( x ) ) + e α t P x ∼ D t ( y ≠ h t ( x ) ) = e − α t ( 1 − ϵ t ) + e α t ( ϵ t ) \begin{aligned} L(\alpha _t h_t|D_t) &= E_{x \sim D_t}\exp \left [ -y * \alpha _t h_t(x) \right ]\\ &= E_{x \sim D_t} \left [ e^{-\alpha _t} I(y=h_t(x)) + e^{-\alpha _t} I(y \neq h_t(x)) \right ] \\ &= e^{-\alpha _t}P_{x \sim D_t}(y=h_t(x)) + e^{\alpha _t}P_{x \sim D_t}(y \neq h_t(x)) \\ &= e^{-\alpha _t}(1-\epsilon _t) + e^{\alpha _t}(\epsilon _t) \end{aligned} L(αtht∣Dt)=Ex∼Dtexp[−y∗αtht(x)]=Ex∼Dt[e−αtI(y=ht(x))+e−αtI(y=ht(x))]=e−αtPx∼Dt(y=ht(x))+eαtPx∼Dt(y=ht(x))=e−αt(1−ϵt)+eαt(ϵt)
令指数损失函数对 α t \alpha _t αt 求导,可得:
α t = 1 2 ln 1 − ϵ t ϵ t \alpha _t = \frac{1}{2} \ln \frac{1-\epsilon _t}{\epsilon _t} αt=21lnϵt1−ϵt
这就是每个新分类器的权重更新公式。
Adaboost算法在获得 H t − 1 H_{t-1} Ht−1 之后样本分布将进行调整,使下一轮的基学习器 h t h_t ht 能纠正 H t − 1 H_{t-1} Ht−1 的一些错误。希望理想的 h t h_t ht 能纠正 H t − 1 H_{t-1} Ht−1 的全部错误,即最小化:
L ( H t − 1 + h t ∣ D ) = E x ∼ D t exp [ − y ∗ ( H t − 1 ( x ) + h t ( x ) ) ] = E x ∼ D t [ e − y H t − 1 ( x ) e − y h t ( x ) ) ] = E x ∼ D t [ e − y H t − 1 ( x ) ( 1 − y h t ( x ) + 1 2 y 2 h t 2 ( x ) ] = E x ∼ D t [ e − y H t − 1 ( x ) ( 1 − y h t ( x ) + 1 2 ] \begin{aligned} L(H_{t-1} + h_t|D) &= E_{x \sim D_t}\exp \left [ -y *(H_{t-1}(x) + h_t(x)) \right ] \\ &= E_{x \sim D_t}\left [ e^{-y H_{t-1}(x)} e^{-y h_t(x)) } \right ] \\ &= E_{x \sim D_t}\left [ e^{-y H_{t-1}(x)} (1-yh_t(x)+ \frac{1}{2}y^2h_t^2(x) \right ] \\ &= E_{x \sim D_t}\left [ e^{-y H_{t-1}(x)} (1-yh_t(x)+ \frac{1}{2} \right ] \end{aligned} L(Ht−1+ht∣D)=Ex∼Dtexp[−y∗(Ht−1(x)+ht(x))]=Ex∼Dt[e−yHt−1(x)e−yht(x))]=Ex∼Dt[e−yHt−1(x)(1−yht(x)+21y2ht2(x)]=Ex∼Dt[e−yHt−1(x)(1−yht(x)+21]
其中, y ∈ { − 1 , 1 } y \in \{-1,1\} y∈{−1,1} 是真实值, h t ( x ) ∈ { − 1 , 1 } h_t(x) \in \{-1,1\} ht(x)∈{−1,1} 是预测值。于是理想的基学习器:
h t ( x ) = arg min h L ( H t − 1 + h t ∣ D ) = arg max h E x ∼ D t [ e − y H t − 1 ( x ) y h ( x ) ] \begin{aligned} h_t(x) &= \underset{h}{\arg \min}L(H_{t-1} + h_t|D) \\ &= \underset{h}{\arg \max} E_{x \sim D_t} \left [ e^{-y H_{t-1}(x)}yh(x) \right ] \\ \end{aligned} ht(x)=hargminL(Ht−1+ht∣D)=hargmaxEx∼Dt[e−yHt−1(x)yh(x)]
注意到, E x ∼ D t exp [ − y ∗ α t H t − 1 ( x ) ] E_{x \sim D_t}\exp \left [ -y * \alpha _t H_{t-1}(x) \right ] Ex∼Dtexp[−y∗αtHt−1(x)] 是一个常数。令 D t D_t Dt 表示一个分布:
D t ( x ) = D ( x ) e − y H t − 1 ( x ) E x ∼ D [ e − y H t − 1 ( x ) ] D_t(x) = \frac{D(x) e^{-y H_{t-1}(x)}}{ E_{x \sim D} \left [ e^{-y H_{t-1}(x)} \right ] } Dt(x)=Ex∼D[e−yHt−1(x)]D(x)e−yHt−1(x)
根据数学期望的定义,这等价于令
h t ( x ) = arg max h E x ∼ D [ e − y H t − 1 ( x ) E x ∼ D t [ e − y H t − 1 ( x ) ] y h ( x ) ] = arg max h E x ∼ D t [ y h ( x ) ] \begin{aligned} h_t(x) &= \underset{h}{\arg \max} E_{x \sim D} \left [ \frac{e^{-y H_{t-1}(x)}}{E_{x \sim D_t} \left [ e^{-y H_{t-1}(x)} \right ]} yh(x) \right ] \\ &= \underset{h}{\arg \max} E_{x \sim D_t} \left [yh(x) \right ] \\ \end{aligned} ht(x)=hargmaxEx∼D[Ex∼Dt[e−yHt−1(x)]e−yHt−1(x)yh(x)]=hargmaxEx∼Dt[yh(x)]
又有 y , h ( x ) ∈ { − 1 , 1 } y,h(x) \in \{ -1, 1\} y,h(x)∈{−1,1} ,有:
y h ( x ) = 1 − 2 I ( y ≠ h ( x ) ) yh(x) = 1 - 2I(y \neq h(x)) yh(x)=1−2I(y=h(x))
则理想的学习器 h t ( x ) = arg min h E x ∼ D t [ I ( y ≠ h ( x ) ) ] h_t(x) = \underset{h}{\arg \min}E_{x \sim D_t} \left [ I(y \neq h(x)) \right ] ht(x)=hargminEx∼Dt[I(y=h(x))]
由此可见,理想的学习器 h t h_t ht 将在分布 D t D_t Dt 下最小化分类误差。因此,弱分类器将基于分布 D t D_t Dt 来训练,且针对 D t D_t Dt 的分类误差应小于 0.5 。这在一定程度上类似“残差逼近”思想。考虑到 D t D_t Dt 和 D t + 1 D_{t+1} Dt+1 的关系,有样本分布更新公式:
D t + 1 ( x ) = D ( x ) e − y H t ( x ) E x ∼ D [ e − y H t ( x ) ] = D ( x ) e − y H t − 1 ( x ) e − y α t h t ( x ) E x ∼ D [ e − y H t ( x ) ] = D t ( x ) e − y α t h t ( x ) E x ∼ D [ e − y H t − 1 ( x ) ] E x ∼ D [ e − y H t ( x ) ] \begin{aligned} D_{t+1}(x) &= \frac{D(x) e^{-y H_{t}(x)}}{ E_{x \sim D} \left [ e^{-y H_{t}(x)} \right ] } \\ &= \frac{D(x) e^{-y H_{t-1}(x)} e^{-y \alpha _{t} h_t(x)} }{ E_{x \sim D} \left [ e^{-y H_{t}(x)} \right ] } \\ &= D_{t}(x)e^{-y \alpha _{t} h_t(x)} \frac{E_{x \sim D} \left [ e^{-y H_{t-1}(x)} \right ]}{E_{x \sim D} \left [ e^{-y H_{t}(x)} \right ]} \end{aligned} Dt+1(x)=Ex∼D[e−yHt(x)]D(x)e−yHt(x)=Ex∼D[e−yHt(x)]D(x)e−yHt−1(x)e−yαtht(x)=Dt(x)e−yαtht(x)Ex∼D[e−yHt(x)]Ex∼D[e−yHt−1(x)]
以上就是我们从基于加性模型迭代式优化指数损失函数的角度推导出的AdaBoost算法。每次迭代时,根据新的样本分布训练基学习器,然后根据训练结果更新权重,获得新一轮的算法模型。
经典的AdaBoost算法只能处理采用指数损失函数的二分类学习任务,而梯度提升方法通过设置不同的可微损失函数可以处理各类学习任务(多分类、回归、Ranking等),应用范围大大扩展。基于梯度提升算法的学习器叫做GBM(Gradient Boosting Machine)。理论上,GBM可以选择各种不同的学习算法作为基学习器。现实中,用得最多的基学习器是决策树。梯度提升算法利用损失函数的负梯度作为残差拟合的方式,如果其中的基函数采用决策树的话,就得到了梯度提升决策树 (Gradient Boosting Decision Tree, GBDT)。
GBDT算法可以看成是M棵树组成的加法模型,其对应的公式如下:
F ( x , w ) = ∑ m = 0 M α m h m ( x , w m ) = ∑ m = 0 M f m ( x , w m ) F(x,w)= \sum_{m=0}^{M}{\alpha_mh_m(x,w_m)}= \sum_{m=0}^{M}{f_m(x,w_m)} F(x,w)=m=0∑Mαmhm(x,wm)=m=0∑Mfm(x,wm)
其中, x x x 为输入样本; w w w 为模型参数; h h h 为分类回归树; α \alpha α 为每棵树的权重。给定训练数据集: T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x N , y N ) } T= \left\{ (x_{1}, y_{1}),(x_{2}, y_{2}),...,(x_{N},y_{N}) \right\} T={(x1,y1),(x2,y2),...,(xN,yN)} 。对于回归问题,损失函数 L ( y , f ( x ) ) L(y,f(x)) L(y,f(x)) 通常为平方差损失,即 L ( y , F ( x ) ) = 1 2 ( y − F ( x ) ) 2 L(y,F(x)) = \frac{1}{2} (y-F(x))^2 L(y,F(x))=21(y−F(x))2,我们的目标是基于损失函数的负梯度方向,逐步在模型中加入新的决策树 f m ( x , w m ) f_m(x,w_m) fm(x,wm) ,最终得到回归树模型 F M F_{M} FM。GBDT算法的实现过程如下:
(1)初始化第一个弱学习器 F 0 ( x ) F_{0}(x) F0(x) :
F 0 ( x ) = arg min c ∑ i = 1 N L ( y i , c ) F_{0}(x) = \arg\min_{c} {\sum_{i=1}^{N}{L(y_i,c)}} F0(x)=argcmini=1∑NL(yi,c)
(2)建立M棵分类回归树 m = 1 , 2 , … , M m = 1,2 , \dots , M m=1,2,…,M 。对 i = 1 , 2 , … , N i = 1,2 , \dots , N i=1,2,…,N ,计算第 m m m 棵树对应的响应值(损失函数的负梯度,即伪残差):
r m , i = − [ ∂ L ( y i , F ( x i ) ) ∂ F ( x ) ] F = F m − 1 r_{m,i} = -\left[ \frac{\partial L(y_i,F(x_i))}{\partial F(x)} \right]_{F=F_{m-1}} rm,i=−[∂F(x)∂L(yi,F(xi))]F=Fm−1
对 i = 1 , 2 , … , N i = 1,2 , \dots , N i=1,2,…,N ,利用CART回归树拟合数据 ( x i , r m , i ) \left(x_{i}, r_{m, i}\right) (xi,rm,i) ,得到第 m m m 棵回归树。
(3)第 m m m 棵回归树,其对应的叶子节点区域为 R m , j R_{m,j} Rm,j 。其中 j = 1 , 2 , . . . , J m j=1,2,...,J_m j=1,2,...,Jm,且 J m J_m Jm 为第 m m m 课回归树的叶子节点的个数。基于损失函数计算出最佳拟合值:
c m , j = arg min c ∑ x i ∈ R m L ( y i , F m − 1 ( x i ) + c ) c_{m,j}= \arg\min_{c} \sum_{x_i \in R_{m}}{L(y_i,F_{m-1}(x_i)+c)} cm,j=argcminxi∈Rm∑L(yi,Fm−1(xi)+c)
(4)更新强学习器 F m ( x ) F_m(x) Fm(x) :
F m ( x ) = F m − 1 ( x ) + ∑ j = 1 J m c m , j I ( x ∈ R m , j ) F_m(x) = F_{m-1}(x) + \sum_{j=1}^{J_{m}}c_{m,j}I(x \in R_{m,j}) Fm(x)=Fm−1(x)+j=1∑Jmcm,jI(x∈Rm,j)
(5)得到强学习器 F M ( x ) F_M(x) FM(x) 的最终表达式:
F M ( x ) = F 0 ( x ) + ∑ m = 1 M ∑ j = 1 J m c m , j I ( x ∈ R m , j ) F_M(x) = F_0(x) + \sum_{m=1}^M\sum_{j=1}^{J_{m}}c_{m,j}I(x \in R_{m,j}) FM(x)=F0(x)+m=1∑Mj=1∑Jmcm,jI(x∈Rm,j)
损失函数
对于GBDT回归模型,sklearn中实现了四种损失函数,有均方差’ls’, 绝对损失’lad’, Huber损失’huber’和分位数损失’quantile’。默认是均方差’ls’。一般来说,如果数据的噪音点不多,用默认的均方差’ls’比较好。如果是噪音点较多,则推荐用抗噪音的损失函数’huber’。而如果我们需要对训练集进行分段预测的时候,则采用’quantile’。下面我们具体来了解一下这四种损失函数。
正则化
为了防止过拟合,GBDT主要有五种正则化的方式:
“Shrinkage”: 这是一种正则化(regularization)方法,为了防止过拟合,在每次对残差估计进行迭代时,不直接加上当前步所拟合的残差,而是乘以一个系数 α \alpha α 。系数 α \alpha α 也被称为学习率(learning rate),因为它可以对梯度提升的步长进行调整,也就是它可以影响我们设置的回归树个数。对于前面的弱学习器的迭代:
F ( x , w ) = ∑ m = 0 M h m ( x , w m ) F(x,w)= \sum_{m=0}^{M}{h_m(x,w_m)} F(x,w)=m=0∑Mhm(x,wm)
如果我们加上了正则化项,则有:
α \alpha α 的取值范围为 α ≤ 1 \alpha \leq 1 α≤1 。对于同样的训练集学习效果,较小的 α \alpha α 意味着我们需要更多的弱学习器的迭代次数。通常我们用学习率和迭代最大次数一起来决定算法的拟合效果。即参数learning_rate会强烈影响到参数n_estimators(即弱学习器个数)。learning_rate的值越小,就需要越多的弱学习器数来维持一个恒定的训练误差(training error)常量。经验上,推荐小一点的learning_rate会对测试误差(test error)更好。在实际调参中推荐将learning_rate设置为一个小的常数(e.g. learning_rate <= 0.1),并通过early stopping机制来选n_estimators。
“Subsample”: 第二种正则化的方式是通过子采样比例(subsample),取值为 (0,1]。注意这里的子采样和随机森林不一样,随机森林使用的是放回抽样,而这里是不放回抽样。如果取值为1,则全部样本都使用,等于没有使用子采样。如果取值小于1,则只有一部分样本会去做GBDT的决策树拟合。选择小于1的比例可以减少方差,即防止过拟合,但会增加样本拟合的偏差,因此取值不能太低。推荐在 [0.5, 0.8]之间。
使用了子采样的GBDT有时也称作随机梯度提升树 (Stochastic Gradient Boosting Tree, SGBT)。由于使用了子采样,程序可以通过采样分发到不同的任务去做Boosting的迭代过程,最后形成新树,从而减少弱学习器难以并行学习的弱点。
对于弱学习器即CART回归树进行正则化剪枝。 这一部分在学习决策树原理时应该掌握的,这里就不重复了。
“Early Stopping”: Early Stopping是机器学习迭代式训练模型中很常见的防止过拟合技巧,具体的做法是选择一部分样本作为验证集,在迭代拟合训练集的过程中,如果模型在验证集里错误率不再下降,就停止训练,也就是说控制迭代的轮数(树的个数)。在sklearn的GBDT中可以设置参数n_iter_no_change实现early stopping。
“Dropout”: Dropout是deep learning里很常用的正则化技巧,很自然的我们会想能不能把Dropout用到GBDT模型上呢?AISTATS2015有篇文章《DART: Dropouts meet Multiple Additive Regression Trees》进行了一些尝试。文中提到GBDT里会出现over-specialization的问题:前面迭代的树对预测值的贡献比较大,后面的树会集中预测一小部分样本的偏差。Shrinkage可以减轻over-specialization的问题,但不是很好。作者想通过Dropout来平衡所有树对预测的贡献。具体的做法是:每次新加一棵树,这棵树要拟合的并不是之前全部树ensemble后的残差,而是随机抽取的一些树ensemble;同时新加的树结果要规范化一下。
对于分类问题,类别之间的差别如何用损失函数表示是GBDT回归算法需要改进的地方。若采用指数损失函数,这样GBDT就退化成了Adaboost,能够解决分类的问题。因此,对于GBDT二分类算法使用类似于逻辑回归的对数似然损失函数,如此可以通过结果的概率值与真实概率值的差距当做残差来拟合。
逻辑回归的预测函数为:
h θ ( x ) = 1 1 + e − F θ ( x ) h_\theta(x)=\frac{1}{1+e^{-F_{\theta}(x)}} hθ(x)=1+e−Fθ(x)1
将原函数 F θ ( x ) F_{\theta}(x) Fθ(x) 的结果映射到函数 h θ ( x ) h_{\theta}(x) hθ(x) ,它表示结果取1的概率。因此对于输入 x x x 分类结果为类别1和类别0的概率分别为:
P ( Y = 1 ∣ X ; θ ) = h θ ( x ) P(Y=1|X;\theta) = h_\theta(x) P(Y=1∣X;θ)=hθ(x)
P ( Y = 0 ∣ X ; θ ) = 1 − h θ ( x ) P(Y=0|X;\theta) = 1-h_\theta(x) P(Y=0∣X;θ)=1−hθ(x)
上式综合起来可以写成:
P ( Y = y ∣ X ; θ ) = ( h θ ( x ) ) y ( 1 − h θ ( x ) ) 1 − y , y ∈ { 0 , 1 } P(Y=y|X;\theta) = (h_\theta(x))^y (1-h_\theta(x))^{1-y},y \in \{0,1\} P(Y=y∣X;θ)=(hθ(x))y(1−hθ(x))1−y,y∈{0,1}
然后取对数似然函数为:
L ( θ ) = ∑ i = 1 N [ y i ln h θ ( x ) + ( 1 − y i ) ln ( 1 − h θ ( x ) ) L(\theta ) = \sum_{i=1}^N[y_i \ln h_\theta(x) + (1-y_i) \ln (1-h_\theta(x)) L(θ)=i=1∑N[yilnhθ(x)+(1−yi)ln(1−hθ(x))
最大似然估计就是求使 L ( θ ) L(\theta) L(θ) 取最大值(即在更定参数 θ \theta θ 和输入数据集 X X X 的情况下,预测正确的概率最大)时的 θ \theta θ 。这里对 L ( θ ) L(\theta) L(θ) 取相反数,可以使用梯度下降法求解,求得的 θ \theta θ 就是要求的最佳参数:
J ( θ ) = − 1 N L ( θ ) = − 1 N ∑ i = 1 N [ y i ln h θ ( x ) + ( 1 − y i ) ln ( 1 − h θ ( x ) ) J(\theta) = -\frac{1}{N}L(\theta ) = -\frac{1}{N}\sum_{i=1}^N[y_i \ln h_\theta(x) + (1-y_i) \ln (1-h_\theta(x)) J(θ)=−N1L(θ)=−N1i=1∑N[yilnhθ(x)+(1−yi)ln(1−hθ(x))
逻辑回归单个样本 ( x i , y i ) (x_{i},y_{i}) (xi,yi) 的损失函数可以表达为:
L ( θ ) = − y i ln h θ ( x ) − ( 1 − y i ) ln ( 1 − h θ ( x ) ) L(\theta) = -y_i\ln h_\theta(x) - (1-y_i) \ln (1-h_\theta(x)) L(θ)=−yilnhθ(x)−(1−yi)ln(1−hθ(x))
假设GBDT第 M M M 步迭代之后当前学习器为 F ( x ) = ∑ m = 0 M h m ( x ) F(x) = \sum_{m=0}^M h_m(x) F(x)=∑m=0Mhm(x) ,其预测结果 H ( x ) = 1 1 + e − F ( x ) H(x) = \frac{1}{1+e^{-F(x)}} H(x)=1+e−F(x)1,将 h θ ( x ) h_\theta(x) hθ(x)替换为 H ( x ) H(x) H(x) 带入上式之后,可将损失函数写为:
L ( y i , F ( x i ) ) = − y i ln 1 1 + e − F ( x i ) − ( 1 − y i ) ln ( 1 − 1 1 + e − F ( x i ) ) L(y_i,F(x_i)) = -y_i\ln \frac{1}{1+e^{-F(x_i)}}-(1-y_i) \ln (1- \frac{1}{1+e^{-F(x_i)}}) L(yi,F(xi))=−yiln1+e−F(xi)1−(1−yi)ln(1−1+e−F(xi)1)
后面的步骤就和GBDT回归算法一样。需要注意的是,这里对于生成的决策树,计算各个叶子节点的最佳残差拟合值时,由于无法求出 c m , j c_{m,j} cm,j 的解析解,需要使用损失函数的二阶泰勒展开来求得近似解。
使用基于Softmax回归的对数损失函数。是二分类算法的一般形式,本质上没有太大改变。
XGBoost的全称是eXtreme Gradient Boosting,它是经过优化的分布式梯度提升库,旨在高效、灵活且可移植。XGBoost是大规模并行boosting tree的工具,它是目前最快最好的开源 boosting tree工具包,比常见的工具包快10倍以上。在数据科学方面,有大量的Kaggle选手选用XGBoost进行数据挖掘比赛,是各大数据科学比赛的必杀武器;在工业界大规模数据方面,XGBoost的分布式版本有广泛的可移植性,支持在Kubernetes、Hadoop、SGE、MPI、 Dask等各个分布式环境上运行,使得它可以很好地解决工业界大规模数据的问题。
XGBoost和GBDT两者都是boosting方法,除了工程实现、解决问题上的一些差异外,最大的不同就是目标函数的定义。因此,我们从目标函数开始探究XGBoost的基本原理。
XGBoost是由 k k k 个基模型组成的一个加法模型,假设我们第 t t t 次迭代要训练的树模型是 f t ( x ) f_{t}(x) ft(x) ,则有:
y ^ i t = ∑ k = 1 t f k ( x i ) = y ^ i t − 1 + f t ( x i ) \hat{y}_i^t = \sum_{k=1}^{t}f_k(x_i)= \hat{y}_i^{t-1} + f_t(x_i) y^it=k=1∑tfk(xi)=y^it−1+ft(xi)
损失函数可由预测值 y ^ i \hat{y}_{i} y^i 与真实值 y i y_{i} yi 进行表示:
L = ∑ i = 1 n l ( y i , y ^ i ) L = \sum_{i=1}^{n}l(y_i,\hat{y}_i) L=i=1∑nl(yi,y^i)
其中, n n n 为样本的数量。我们知道模型的预测精度由模型的偏差和方差共同决定,损失函数代表了模型的偏差,想要方差小则需要在目标函数中添加正则项,用于防止过拟合。所以目标函数由模型的损失函数 L L L 与抑制模型复杂度的正则项 Ω \Omega Ω 组成,目标函数的定义如下:
O b j = ∑ i = 1 n l ( y i , y ^ i ) + ∑ j = 1 t Ω ( f j ) Obj = \sum_{i=1}^n l(y_i,\hat{y}_i) + \sum_{j=1}^t \Omega (f_j) Obj=i=1∑nl(yi,y^i)+j=1∑tΩ(fj)
其中, ∑ j = 1 t Ω ( f j ) \sum_{j=1}^t \Omega (f_j) ∑j=1tΩ(fj)是将全部 t t t 棵树的复杂度进行求和,添加到目标函数中作为正则化项,用于防止模型过度拟合。
由于XGBoost是boosting族中的算法,所以遵从前向分步加法,以第 t t t 步的模型为例,模型对第 i i i 个样本 x i x_{i} xi 的预测值为:
y ^ i ( t ) = y ^ i ( t − 1 ) + f t ( x i ) \hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + f_t(x_i) y^i(t)=y^i(t−1)+ft(xi)
由于与第 t − 1 t-1 t−1 步的模型相关的值都已知,是已知常数,因此:
O b j ( t ) = ∑ i = 1 n l ( y i , y ^ ( t ) ) + ∑ j = 1 t Ω ( f j ) = ∑ i = 1 n l ( y i , y ^ ( t − 1 ) + f t ( x i ) ) + Ω ( f t ) + c o n s t a n t \begin{aligned} Obj^{(t)} &= \sum_{i=1}^n l(y_i,\hat{y}^{(t)}) + \sum_{j=1}^t \Omega (f_j) \\ &= \sum_{i=1}^n l(y_i,\hat{y}^{(t-1)}+f_t(x_i)) + \Omega (f_t) + constant \end{aligned} Obj(t)=i=1∑nl(yi,y^(t))+j=1∑tΩ(fj)=i=1∑nl(yi,y^(t−1)+ft(xi))+Ω(ft)+constant
与GBDT二分类算法相同,需要对目标函数做二阶泰勒展开,以对于生成的决策树,计算各个叶子节点的最佳残差拟合值。
定义一棵决策树,其包括两个部分:
f t ( x ) = w q ( x ) , w ∈ R T , q : R d → { 1 , 2 , . . . , T } f_t(x) = w_q(x), w \in R^T, q: R^d \rightarrow \{1,2,...,T\} ft(x)=wq(x),w∈RT,q:Rd→{1,2,...,T}
w是长度为T的一维向量,代表树 q q q 各个叶子节点的权重。 q q q 代表一棵树的结构,作用是将输入 x i ∈ R d x_i \in R^d xi∈Rd 映射到某个叶子节点,且假设这棵树有T个叶子节点。
树的复杂度可由叶子数和叶子节点的权重表示。叶子节点越少模型越简单、每个叶子节点也不应该含有过高的权重。因此:
Ω ( 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=1∑Twj2
在实际训练过程中,当建立第 t t t 棵树时,一个非常关键的问题是如何找到叶子节点的最优切分点,XGBoost支持两种分裂节点的方法——贪心算法和近似算法。
(1)贪心算法
从树的深度为0开始:
所谓的贪心算法就是把所有特征的所有可能的分裂点都列举出来,然后计算每个分裂方案的收益,选择收益最大的分裂方案来进行分裂。
(2)近似算法
贪心算法可以得到最优解,但当数据量太大时则无法读入内存进行计算,近似算法主要针对贪心算法这一缺点给出了近似最优解。对于每个特征,只考察分位点可以减少计算复杂度。该算法首先根据特征分布的分位数提出候选划分点,然后将连续型特征映射到由这些候选点划分的桶中,然后聚合统计信息找到所有区间的最佳分裂点。
但实际上,XGBoost不是简单地按照样本个数进行分位,而是以二阶导数值 h i h_i hi 作为样本的权重进行划分。为了处理带权重的候选切分点的选取,作者提出了Weighted Quantile Sketch算法。
实际工程中一般会出现输入值稀疏的情况。比如数据的缺失、one-hot编码都会造成输入数据稀疏。XGBoost在构建树的节点过程中只考虑非缺失值的数据遍历,而为每个节点增加了一个缺省方向,当样本相应的特征值缺失时,可以被归类到缺省方向上,最优的缺省方向可以从数据中学到。至于如何学到缺省值的分支,其实很简单,分别枚举特征缺省的样本归为左右分支后的增益,选择增益最大的枚举项即为最优缺省方向。
在构建树的过程中需要枚举特征缺失的样本,乍一看这个算法会多出相当于一倍的计算量,但其实不是的。因为在算法的迭代中只考虑了非缺失值数据的遍历,缺失值数据直接被分配到左右节点,所需要遍历的样本量大大减小。作者通过在Allstate-10K数据集上进行了实验,从结果可以看到稀疏算法比普通算法在处理数据上快了超过50倍。
在树生成过程中,最耗时的一个步骤就是在每次寻找最佳分裂点时都需要对特征的值进行排序。而 XGBoost 在训练之前会根据特征对数据进行排序,然后保存到块结构中,并在每个块结构中都采用了稀疏矩阵存储格式(Compressed Sparse Columns Format,CSC)进行存储,后面的训练过程中会重复地使用块结构,可以大大减小计算量。
作者提出通过按特征进行分块并排序,在块里面保存排序后的特征值及对应样本的引用,以便于获取样本的一阶、二阶导数值。通过顺序访问排序后的块遍历样本特征的特征值,方便进行切分点的查找。此外分块存储后多个特征之间互不干涉,可以使用多线程同时对不同的特征进行切分点查找,即特征的并行化处理。在对节点进行分裂时需要选择增益最大的特征作为分裂,这时各个特征的增益计算可以同时进行,这也是 XGBoost 能够实现分布式或者多线程计算的原因。
列块并行学习的设计可以减少节点分裂时的计算量,在顺序访问特征值时,访问的是一块连续的内存空间,但通过特征值持有的索引(样本索引)访问样本获取一阶、二阶导数时,这个访问操作访问的内存空间并不连续,这样可能造成cpu缓存命中率低,影响算法效率。
为了解决缓存命中率低的问题,XGBoost 提出了缓存访问算法:为每个线程分配一个连续的缓存区,将需要的梯度信息存放在缓冲区中,这样就实现了非连续空间到连续空间的转换,提高了算法效率。此外适当调整块大小,也可以有助于缓存优化。
当数据量非常大时,我们不能把所有的数据都加载到内存中。那么就必须将一部分需要加载进内存的数据先存放在硬盘中,当需要时再加载进内存。这样操作具有很明显的瓶颈,即硬盘的IO操作速度远远低于内存的处理速度,肯定会存在大量等待硬盘IO操作的情况。针对这个问题作者提出了“核外”计算的优化方法。具体操作为,将数据集分成多个块存放在硬盘中,使用一个独立的线程专门从硬盘读取数据,加载到内存中,这样算法在内存中处理数据就可以和从硬盘读取数据同时进行。此外,XGBoost 还用了两种方法来降低硬盘读写的开销:
精度更高: GBDT(回归算法)只用到一阶泰勒展开,而 XGBoost 对损失函数进行了二阶泰勒展开。XGBoost 引入二阶导一方面是为了增加精度,另一方面也是为了能够自定义损失函数,二阶泰勒展开可以近似大量损失函数;
灵活性更强: GBDT 以 CART 作为基分类器,XGBoost 不仅支持 CART 还支持线性分类器,使用线性分类器的 XGBoost 相当于带 L 1 L1 L1 和 L 2 L2 L2 正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。此外,XGBoost 工具支持自定义损失函数,只需函数支持一阶和二阶求导;
正则化: XGBoost 在目标函数中加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、叶子节点权重的 L 2 L2 L2 范式。正则项降低了模型的方差,使学习出来的模型更加简单,有助于防止过拟合,这也是XGBoost优于传统GBDT的一个特性。
Shrinkage(缩减): 相当于学习速率。XGBoost 在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。传统GBDT的实现也有学习速率;
列抽样: XGBoost 借鉴了随机森林的做法,支持列抽样,不仅能降低过拟合,还能减少计算。这也是XGBoost异于传统GBDT的一个特性;
缺失值处理: 对于特征的值有缺失的样本,XGBoost 采用的稀疏感知算法可以自动学习出它的分裂方向;
XGBoost工具支持并行: boosting不是一种串行的结构吗?怎么并行的?注意XGBoost的并行不是tree粒度的并行,XGBoost也是一次迭代完才能进行下一次迭代的(第 t t t 次迭代的代价函数里包含了前面 t − 1 t-1 t−1 次迭代的预测值)。XGBoost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),XGBoost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
可并行的近似算法: 树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以XGBoost还提出了一种可并行的近似算法,用于高效地生成候选的分割点。
虽然利用预排序和近似算法可以降低寻找最佳分裂点的计算量,但在节点分裂过程中仍需要遍历数据集;
预排序过程的空间复杂度过高,不仅需要存储特征值,还需要存储特征对应样本的梯度统计值的索引,相当于消耗了两倍的内存。
# 基于XGboost的分类
from sklearn.datasets import load_iris
import xgboost as xgb
from xgboost import plot_importance
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split
# read in the iris data
iris = load_iris()
X = iris.data
y = iris.target
print(X[:5],y[:5])
# split train data and test data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1234565)
# set XGBoost's parameters
params = {
'booster': 'gbtree',
'objective': 'multi:softmax', # 回归任务设置为:'objective': 'reg:gamma',
'num_class': 3, # 回归任务没有这个参数
'gamma': 0.1,
'max_depth': 6,
'lambda': 2,
'subsample': 0.7,
'colsample_bytree': 0.7,
'min_child_weight': 3,
'silent': 1,
'eta': 0.1,
'seed': 1000,
'nthread': 4,
}
plst = list(params.items())
dtrain = xgb.DMatrix(X_train, y_train)
num_rounds = 500
model = xgb.train(plst, dtrain, num_rounds)
# 对测试集进行预测
dtest = xgb.DMatrix(X_test)
ans = model.predict(dtest)
# 计算准确率
cnt1 = 0
cnt2 = 0
for i in range(len(y_test)):
if ans[i] == y_test[i]:
cnt1 += 1
else:
cnt2 += 1
print("Accuracy: %.2f %% " % (100 * cnt1 / (cnt1 + cnt2)))
# 显示重要特征
plot_importance(model)
plt.show()
结果如下:
[[5.1 3.5 1.4 0.2]
[4.9 3. 1.4 0.2]
[4.7 3.2 1.3 0.2]
[4.6 3.1 1.5 0.2]
[5. 3.6 1.4 0.2]] [0 0 0 0 0]
LightGBM(Light Gradient Boosting Machine)是一个实现GBDT算法的框架,支持高效率的并行训练,并且具有更快的训练速度、更低的内存消耗、更好的准确率、支持分布式可以快速处理海量数据等优点。
常用的机器学习算法,例如神经网络等算法,都可以以mini-batch的方式训练,训练数据的大小不会受到内存限制。而GBDT在每一次迭代的时候,都需要遍历整个训练数据多次。如果把整个训练数据装进内存则会限制训练数据的大小;如果不装进内存,反复地读写训练数据又会消耗非常大的时间。尤其面对工业级海量的数据,普通的GBDT算法是不能满足其需求的。
关于mini-batch
深度学习的优化算法,说白了就是梯度下降。每次的参数更新有两种方式。
LightGBM提出的主要原因就是为了解决GBDT在海量数据遇到的问题,让GBDT可以更好更快地用于工业实践。
Histogram algorithm应该翻译为直方图算法,直方图算法的基本思想是:先把连续的浮点特征值离散化成 k k k 个整数,同时构造一个宽度为 k k k 的直方图。在遍历数据的时候,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点。
直方图算法简单理解为:首先确定对于每一个特征需要多少个箱子(bin)并为每一个箱子分配一个整数;然后将浮点数的范围均分成若干区间,区间个数与箱子个数相等,将属于该箱子的样本数据更新为箱子的值;最后用直方图(#bins)表示。看起来很高大上,其实就是直方图统计,将大规模的数据放在了直方图中。
我们知道特征离散化具有很多优点,如存储方便、运算更快、鲁棒性强、模型更加稳定等。对于直方图算法来说最直接的有以下两个优点:
内存占用更小: 直方图算法不仅不需要额外存储预排序的结果,而且可以只保存特征离散化后的值,而这个值一般用 8 位整型存储就足够了,内存消耗可以降低为原来的 1 8 \frac{1}{8} 81 。也就是说XGBoost需要用 32 位的浮点数去存储特征值,并用 32 位的整形去存储索引,而 LightGBM只需要用 8 位去存储直方图,内存相当于减少为 1 8 \frac{1}{8} 81 ;
计算代价更小: 预排序算法XGBoost每遍历一个特征值就需要计算一次分裂的增益,而直方图算法LightGBM只需要计算 k 次( k 可以认为是常数),直接将时间复杂度从 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 。
当然,Histogram算法并不是完美的。由于特征被离散化后,找到的并不是很精确的分割点,所以会对结果产生影响。但在不同的数据集上的结果表明,离散化的分割点对最终的精度影响并不是很大,甚至有时候会更好一点。原因是决策树本来就是弱模型,分割点是不是精确并不是太重要;较粗的分割点也有正则化的效果,可以有效地防止过拟合;即使单棵树的训练误差比精确分割的算法稍大,但在梯度提升(Gradient Boosting)的框架下没有太大的影响。
LightGBM另一个优化是Histogram(直方图)做差加速。一个叶子的直方图可以由它的父亲节点的直方图与它兄弟的直方图做差得到,在速度上可以提升一倍。通常构造直方图时,需要遍历该叶子上的所有数据,但直方图做差仅需遍历直方图的k个桶。在实际构建树的过程中,LightGBM还可以先计算直方图小的叶子节点,然后利用直方图做差来获得直方图大的叶子节点,这样就可以用非常微小的代价得到它兄弟叶子的直方图。
在Histogram算法之上,LightGBM进行进一步的优化。首先它抛弃了大多数GBDT工具使用的按层生长 (level-wise) 的决策树生长策略,而使用了带有深度限制的按叶子生长 (leaf-wise) 算法。
XGBoost 采用 Level-wise 的增长策略,该策略遍历一次数据可以同时分裂同一层的叶子,容易进行多线程优化,也好控制模型复杂度,不容易过拟合。但实际上Level-wise是一种低效的算法,因为它不加区分的对待同一层的叶子,实际上很多叶子的分裂增益较低,没必要进行搜索和分裂,因此带来了很多没必要的计算开销。
LightGBM采用Leaf-wise的增长策略,该策略每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。因此同Level-wise相比,Leaf-wise的优点是:在分裂次数相同的情况下,Leaf-wise可以降低更多的误差,得到更好的精度;Leaf-wise的缺点是:可能会长出比较深的决策树,产生过拟合。因此LightGBM会在Leaf-wise之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。
Gradient-based One-Side Sampling 应该被翻译为单边梯度采样(GOSS)。GOSS算法从减少样本的角度出发,排除大部分小梯度的样本,仅用剩下的样本计算信息增益,它是一种在减少数据量和保证精度上平衡的算法。
AdaBoost中,样本权重是数据重要性的指标。然而在GBDT中没有原始样本权重,不能应用权重采样。幸运的是,我们观察到GBDT中每个数据都有不同的梯度值,对采样十分有用。即梯度小的样本,训练误差也比较小,说明数据已经被模型学习得很好了,直接想法就是丢掉这部分梯度小的数据。然而这样做会改变数据的分布,将会影响训练模型的精确度,为了避免此问题,提出了GOSS算法。
GOSS是一个样本的采样算法,目的是丢弃一些对计算信息增益没有帮助的样本留下有帮助的。根据计算信息增益的定义,梯度大的样本对信息增益有更大的影响。因此,GOSS在进行数据采样的时候只保留了梯度较大的数据,但是如果直接将所有梯度较小的数据都丢弃掉势必会影响数据的总体分布。所以,GOSS首先将要进行分裂的特征的所有取值按照绝对值大小降序排序(XGBoost一样也进行了排序,但是LightGBM不用保存排序后的结果),选取绝对值最大的 a ∗ 100 % a*100\% a∗100% 个数据。然后在剩下的较小梯度数据中随机选择 b ∗ 100 % b*100\% b∗100% 个数据。接着将这 b ∗ 100 % b*100\% b∗100% 个数据乘以一个常数$\frac{1-a}{b} $ ,这样算法就会更关注训练不足的样本,而不会过多改变原数据集的分布。最后使用这 ( a + b ) ∗ 100 % (a+b)*100\% (a+b)∗100% 个数据来计算信息增益。 (相当于,把中间梯度那部分数据全部变成了小梯度数据,这样就更加关注大梯度数据)
高维度的数据往往是稀疏的,这种稀疏性启发我们设计一种无损的方法来减少特征的维度。通常被捆绑的特征都是互斥的(表现为某个特征时不会同时表现为另一个特征,即特征不会同时为非零值,像经过one-hot编码后的某种特征集),这样两个特征捆绑起来才不会丢失信息。如果两个特征并不是完全互斥(部分情况下两个特征都是非零值),可以用一个指标对特征不互斥程度进行衡量,称之为冲突比率,当这个值较小时,我们可以选择把不完全互斥的两个特征捆绑,而不影响最后的精度。互斥特征捆绑算法(Exclusive Feature Bundling, EFB)指出如果将一些特征进行融合绑定,则可以降低特征数量。这样在构建直方图时的时间复杂度从 O ( # d a t a ∗ # f e a t u r e ) O(\#data * \#feature) O(#data∗#feature) 变为 O ( # d a t a ∗ # b u n d l e ) O(\#data * \#bundle) O(#data∗#bundle) ,这里 # b u n d l e \#bundle #bundle 指特征融合绑定后特征包的个数,且 # b u n d l e \#bundle #bundle 远小于 # f e a t u r e \#feature #feature 。
针对这种想法,我们会遇到两个问题:
(1)解决哪些特征应该绑在一起
将相互独立的特征进行绑定是一个 NP-Hard 问题,LightGBM的EFB算法将这个问题转化为图着色的问题来求解,将所有的特征视为图的各个顶点,将不是相互独立的特征用一条边连接起来,边的权重就是两个相连接的特征的总冲突值,这样需要绑定的特征就是在图着色问题中要涂上同一种颜色的那些点(特征)。此外,我们注意到通常有很多特征,尽管不是100%相互排斥,但也很少同时取非零值。 如果我们的算法可以允许一小部分的冲突,我们可以得到更少的特征包,进一步提高计算效率。经过简单的计算,随机污染小部分特征值将影响精度最多 O ( [ ( 1 − γ ) n ] − 2 / 3 ) O([(1-\gamma)n]^{-2/3}) O([(1−γ)n]−2/3), γ \gamma γ 是每个绑定中的最大冲突比率,当其相对较小时,能够完成精度和效率之间的平衡。具体步骤可以总结如下:
算法允许两两特征并不完全互斥来增加特征捆绑的数量,通过设置最大冲突比率 γ \gamma γ 来平衡算法的精度和效率。
这样的算法叫贪心绑定算法(Greedy Bundling),时间复杂度是 O ( # f e a t u r e 2 ) O(\#feature^2) O(#feature2) ,训练之前只处理一次,其时间复杂度在特征不是特别多的情况下是可以接受的,但难以应对百万维度的特征。为了继续提高效率,LightGBM提出了一种更加高效的无图的排序策略:将特征按照非零值个数排序,这和使用图节点的度排序相似,因为更多的非零值通常会导致冲突,新算法在贪心绑定算法基础上改变了排序策略。
(2)解决怎么把特征绑为一捆
特征合并算法,其关键在于原始特征能从合并的特征中分离出来。绑定几个特征在同一个bundle里需要保证绑定前的原始特征的值可以在bundle中识别,考虑到histogram-based算法将连续的值保存为离散的bins,我们可以使得不同特征的值分到bundle中的不同bin(箱子)中,这可以通过在特征值中加一个偏置常量来解决。比如,我们在bundle中绑定了两个特征A和B,A特征的原始取值为区间 [ 0 , 10 ) [0,10) [0,10) ,B特征的原始取值为区间 [ 0 , 20 ) [0,20) [0,20) ,我们可以在B特征的取值上加一个偏置常量 10,将其取值范围变为 [ 10 , 30 ) [10,30) [10,30),绑定后的特征取值范围为 [ 0 , 30 ) [0, 30) [0,30) ,这样就可以放心的融合特征A和B了。
我们将论文《Lightgbm: A highly efficient gradient boosting decision tree》中没有提到的优化方案,而在其相关论文《A communication-efficient parallel algorithm for decision tree》中提到的优化方案,放到本节作为LightGBM的工程优化来向大家介绍。
实际上大多数机器学习工具都无法直接支持类别特征,一般需要把类别特征,通过 one-hot 编码,转化到多维的 0/1 特征,降低了空间和时间的效率。但我们知道对于决策树来说并不推荐使用 one-hot 编码,尤其当类别特征中类别个数很多的情况下,会存在以下问题:
会产生样本切分不平衡问题,导致切分增益非常小(即浪费了这个特征)。使用 one-hot编码,意味着在每一个决策节点上只能使用one vs rest(例如是不是狗,是不是猫等)的切分方式。例如,动物类别切分后,会产生是否狗,是否猫等一系列特征,这一系列特征上只有少量样本为 1,大量样本为 0,这时候切分样本会产生不平衡,这意味着切分增益也会很小。较小的那个切分样本集,它占总样本的比例太小,无论增益多大,乘以该比例之后几乎可以忽略;较大的那个拆分样本集,它几乎就是原始的样本集,增益几乎为零。比较直观的理解就是不平衡的切分和不切分没有区别。
会影响决策树的学习。因为就算可以对这个类别特征进行切分,独热编码也会把数据切分到很多零散的小空间上。而决策树学习时利用的是统计信息,在这些数据量小的空间上,统计信息不准确,学习效果会变差。
类别特征的使用在实践中是很常见的。且为了解决one-hot编码处理类别特征的不足,LightGBM优化了对类别特征的支持,可以直接输入类别特征,不需要额外的 0/1 展开。LightGBM采用 many-vs-many 的切分方式将类别特征分为两个子集,实现类别特征的最优切分。假设某维特征有 k k k 个类别,则有 2 ( k − 1 ) − 1 2^{(k-1)} - 1 2(k−1)−1 种可能,时间复杂度为 O ( 2 k ) O(2^k) O(2k) ,LightGBM 基于 Fisher的《On Grouping For Maximum Homogeneity》论文实现了 O ( k l o g k ) O(klogk) O(klogk) 的时间复杂度。
在Expo数据集上的实验结果表明,相比 0/1 展开的方法,使用LightGBM支持的类别特征可以使训练速度加速8倍,并且精度一致。更重要的是,LightGBM是第一个直接支持类别特征的GBDT工具。
这部分主要总结下 LightGBM 相对于 XGBoost 的优点,从内存和速度两方面进行介绍。
(1)速度更快
(2)内存更小
import lightgbm as lgb
from sklearn import datasets
from sklearn.model_selection import train_test_split
import numpy as np
from sklearn.metrics import roc_auc_score, accuracy_score
# 加载数据
iris = datasets.load_iris()
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.3)
# 转换为Dataset数据格式
train_data = lgb.Dataset(X_train, label=y_train)
validation_data = lgb.Dataset(X_test, label=y_test)
# 参数
params = {
'learning_rate': 0.1,
'lambda_l1': 0.1,
'lambda_l2': 0.2,
'max_depth': 4,
'objective': 'multiclass', # 目标函数
'num_class': 3,
}
# 模型训练 #可以接着建立模型进行训练,也可以先建立模型,再对模型进行fit训练
gbm = lgb.train(params, train_data, valid_sets=[validation_data])
# 模型预测
y_pred = gbm.predict(X_test)
y_pred = [list(x).index(max(x)) for x in y_pred]
print(y_pred)
# 模型评估
print(accuracy_score(y_test, y_pred))
结果如下:
[1, 2, 0, 0, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 2, 0, 0, 1, 0, 0, 0, 1, 0, 1, 2, 2, 2, 2, 1, 1, 2, 1, 0, 0, 0, 0, 2, 0, 1, 1, 0, 1, 0, 1]
0.9555555555555556
CatBoost全称CategoricalBoosting是俄罗斯的搜索巨头Y andex在2017年开源的机器学习库,也是Boosting族算法的一种,知名度较高的XGBoost和LightGBM类似,依然是在GBDT算法框架下的一种改进实现,是一种基于对称决策树(oblivious trees)算法的参数少、支持类别型变量和高准确性的GBDT框架。解决的主要痛点是高效合理地处理类别型特征,另外是处理梯度偏差(Gradient bias)以及预测偏移(Prediction shift)问题,提高算法的准确性和泛化能力。
类别型特征是指其值是离散的集合且相互比较并无意义的变量,比如用户的ID、产品ID、颜色等。因此,这些变量无法在二叉决策树当中直接使用。常规的做法是将这些类别变量通过预处理的方式转化成数值型变量再喂给模型,比如用一个或者若干个数值来代表一个类别型特征。目前广泛用于低势(每个特征的类别种类较少)类别特征的处理方法是独热编码One-hot encoding:将原来的特征内的每一个类别都独立为一个新的独立的特征,然后用0/1的用来指示是否含有该类别的数值型特征。One-hot encoding可以在数据预处理时完成,也可以在模型训练的时候完成,从训练时间的角度,后一种方法的实现更为高效,CatBoost对于低势类别特征也是采用后一种实现。
但是,在高势特征当中,比如 user ID,这种编码方式会产生大量新的特征,造成维度灾难。一种折中的办法是可以将类别分组成有限个的群体再进行 One-hot encoding。一种常被使用的方法是根据目标变量统计(Target Statistics,以下简称TS)进行分组,目标变量统计用于估算每个类别的目标变量期望值。甚至有人直接用TS作为一个新的数值型变量来代替原来的类别型变量。重要的是,可以通过对TS数值型特征的阈值设置,基于对数损失、基尼系数或者均方差,得到一个对于训练集而言将类别一分为二的所有可能划分当中最优的那个。
在LightGBM当中,类别型特征用每一步梯度提升时的梯度统计(Gradient Statistics,以下简称GS)来表示。虽然为建树提供了重要的信息,但是这种方法有以下两个缺点:
- 增加计算时间,因为需要对每一个类别型特征,在迭代的每一步,都需要对GS进行计算;
- 增加存储需求,对于一个类别型变量,需要存储每一次分离每个节点的类别。
为了克服这些缺点,LightGBM以损失部分信息为代价将所有的长尾类别归位一类,作者声称这样处理高势特征时比起 One-hot encoding还是好不少。如果采用TS特征,那么对于每个类别只需要计算和存储一个数字。
因此,本文作者认为采用TS作为一个新的数值型特征是最有效、信息损失最小的处理类别型特征的方法,且提出的排序原则对梯度统计也是有效的。
一个有效的处理类别型特征 i i i 的方法:将第 k k k 个样本的类别 x k i x_{k}^{i} xki 转化成与其相当的数值型特征 x ^ k i \hat{x}_{k}^{i} x^ki 。通常用基于类别的目标变量 y y y 的期望来进行估算TS: x ^ k i ≈ E ( y ∣ x i = x k i ) \hat{x}_{k}^{i} \approx E(y | x^{i} = x_{k}^{i}) x^ki≈E(y∣xi=xki) 。以下为几种计算TS的方法(也是其逐步改进的过程):
估算 E ( y ∣ x i = x k i ) E(y | x^{i} = x_{k}^{i}) E(y∣xi=xki) 最直接的方式就是用训练样本当中相同类别 x k i x_{k}^{i} xki 的目标变量 y y y 的平均值:
x ^ k i = ∑ j = 1 n I { x j i = x k i } ⋅ y j ∑ j = 1 n I { x j i = x k i } \hat{x}_{k}^{i} = \frac{\sum_{j=1}^{n}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}}\cdot y_j}{\sum_{j=1}^{n}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}}} x^ki=∑j=1nI{xji=xki}∑j=1nI{xji=xki}⋅yj
其中, I \mathbb{I} I 为指示函数。显然,这样的处理方式很容易引起过拟合。举个例子,假如在整个训练集当中所有样本的类别 x k i x_{k}^{i} xki 都互不相同,即 k k k 个样本有 k k k 个类别,那么新产生的数值型特征的值将与目标变量的值相同。某种程度上,这是一种目标穿越(target leakage),非常容易引起过拟合。比较好的一种做法是采用一个先验概率 p p p 进行平滑处理:
x ^ k i = ∑ j = 1 n I { x j i = x k i } ⋅ y j + a p ∑ j = 1 n I { x j i = x k i } + a \hat{x}_{k}^{i} = \frac{\sum_{j=1}^{n}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}}\cdot y_j+ ap}{\sum_{j=1}^{n}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}}+a} x^ki=∑j=1nI{xji=xki}+a∑j=1nI{xji=xki}⋅yj+ap
其中 a > 0 a>0 a>0 是先验概率的权重,而对于先验概率 p p p,通常的做法是设置为数据集当中目标变量的平均值。不过这样的平滑处理依然无法完全避免目标穿越:特征 x ^ k i \hat{x}_{k}^{i} x^ki 是通过自变量 x k x_{k} xk 的目标 y k y_{k} yk 计算所得。这将会导致条件偏移,即对于训练集和测试集, x ^ k i ∣ y \hat{x}_{k}^{i}|y x^ki∣y 的分布会有所不同。再举个比较极端的例子,假设第 i i i 个特征为类别型特征,并且特征所有取值为无重复的集合,然后对于每一个类别 A A A ,我们有 P ( y = 1 ∣ x i = A ) = 0.5 ) P(y=1|x^i=A)=0.5) P(y=1∣xi=A)=0.5),其中y为二分类目标变量,概率0.5为假设值。在训练集中,我们有 x ^ k i = y k + a p 1 + a \hat{x}_{k}^{i} = \frac{y_k+ap}{1+a} x^ki=1+ayk+ap ,因此,只需使用阈值 t = 0.5 + a p 1 + a t = \frac{0.5+ap}{1+a} t=1+a0.5+ap 就可以完美地对所有训练数据进行分类。然而,对于测试集,由于所有的特征取值无重复, 故 ∑ j = 1 n I { x j i = x k i } = 0 \sum_{j=1}^{n}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}} = 0 ∑j=1nI{xji=xki}=0 ,因此, x ^ k i = p \hat{x}_{k}^{i} = p x^ki=p。若 p < t p
为避免这种条件偏移,有几种类似的改进方法。他们的主体思想是,对于样本 x k x_k xk ,其TS的计算是在不包括 x k x_k xk 的子集 D k ⊂ D ∖ { x k } D_k\subset D\setminus \{x_k\} Dk⊂D∖{xk} 上完成:
x ^ k i = ∑ x j ⊂ D k I { x j i = x k i } ⋅ y j + a p ∑ x j ⊂ D k I { x j i = x k i } + a \hat{x}_{k}^{i} = \frac{\sum_{x_{j}\subset D_k}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}}\cdot y_j+ ap}{\sum_{x_j\subset D_k}\mathbb{I}_{{\{{x}_{j}^{i}={x}_{k}^{i}\}}}+a} x^ki=∑xj⊂DkI{xji=xki}+a∑xj⊂DkI{xji=xki}⋅yj+ap
(ps:这样做的目的就可以把上面所举的极端例子给过滤掉,其实对于无重复取值的特征来说,这个特征是没有意义的。直接把 x k x_k xk 剔除,则所有的TS取值都为 p p p ,此时该特征的所有数据的取值都相等,从而使该特征变得无意义,防止了出现上例中条件偏移的过拟合现象。)
Holdout TS 将 D D D 的划分为两个集合,一个用于训练一个用于测试,但是这样就减少了训练样本的数量。
Leave-one-out TS 仅将 x k x_k xk 剔出 D D D 作为训练集,整个 D D D 为测试集。乍一看可以很好地工作,然而这并不能防止target leakage。
CatBoost所使用的一个更为有效的方法,依赖于排序原则,根据在线学习算法(Online Learning Algorithm)获得时间序列数据得到的启示。步骤如下:
注意,如果只使用随机排序,那么前面的样本中的TS的方差要比后面的样本高得多。为此,CatBoost对不同的梯度增强步骤使用不同的排序。
值得注意的是几个类别型特征的任意组合都可视为新的特征。例如,在音乐推荐应用中,我们有两个类别型特征:用户ID和音乐流派。如果有些用户更喜欢摇滚乐,将用户ID和音乐流派转换为数字特征时,根据上述这些信息就会丢失。结合这两个特征就可以解决这个问题,并且可以得到一个新的强大的特征。然而,组合的数量会随着数据集中类别型特征的数量成指数增长,因此不可能在算法中考虑所有组合。为当前树构造新的分割点时,CatBoost会采用贪婪的策略考虑组合。对于树的第一次分割,不考虑任何组合。对于下一个分割,CatBoost将当前树的所有组合、类别型特征与数据集中的所有类别型特征相结合,并将新的组合类别型特征动态地转换为数值型特征。CatBoost还通过以下方式生成数值型特征和类别型特征的组合:树中选定的所有分割点都被视为具有两个值的类别型特征,并像类别型特征一样被进行组合考虑。
对于学习CatBoost克服梯度偏差的内容,我提出了三个问题:
CatBoost,和所有标准梯度提升算法一样,都是通过构建新树来拟合当前模型的梯度。然而,所有经典的提升算法都存在由有偏的点态梯度估计引起的过拟合问题。在每个步骤中使用的梯度都使用当前模型中的相同的数据点来估计,这导致估计梯度在特征空间的任何域中的分布与该域中梯度的真实分布相比发生了偏移,从而导致过拟合。为了解决这个问题,CatBoost对经典的梯度提升算法进行了一些改进,简要介绍如下:
在许多利用GBDT框架的算法(例如,XGBoost、LightGBM)中,构建下一棵树分为两个阶段:选择树结构和在树结构固定后计算叶子节点的值。为了选择最佳的树结构,算法通过枚举不同的分割,用这些分割构建树,对得到的叶子节点中计算值,然后对得到的树计算评分,最后选择最佳的分割。两个阶段叶子节点的值都是被当做梯度或牛顿步长的近似值来计算。在CatBoost中,第一阶段采用梯度步长的无偏估计,第二阶段使用传统的GBDT方案执行。既然原来的梯度估计是有偏的,那么怎么能改成无偏估计呢?
对于学习预测偏移的内容,我提出了两个问题:
预测偏移(Prediction shift)是由梯度偏差造成的。在GDBT的每一步迭代中, 损失函数使用相同的数据集求得当前模型的梯度, 然后训练得到基学习器, 但这会导致梯度估计偏差, 进而导致模型产生过拟合的问题。 CatBoost通过采用排序提升 (Ordered boosting) 的方式替换传统算法中梯度估计方法,进而减轻梯度估计的偏差,提高模型的泛化能力。下面我们对预测偏移进行详细的描述和分析。
CatBoost使用对称树(oblivious trees)作为基预测器。在这类树中,相同的分割准则在树的整个一层上使用。这种树是平衡的,不太容易过拟合。梯度提升对称树被成功地用于各种学习任务中。在对称树中,每个叶子节点的索引可以被编码为长度等于树深度的二进制向量。这在CatBoost模型评估器中得到了广泛的应用:我们首先将所有浮点特征、统计信息和独热编码特征进行二值化,然后使用二进制特征来计算模型预测值。
性能卓越: 在性能方面可以匹敌任何先进的机器学习算法;
鲁棒性/强健性: 它减少了对很多超参数调优的需求,并降低了过度拟合的机会,这也使得模型变得更加具有通用性;
易于使用: 提供与scikit集成的Python接口,以及R和命令行界面;
实用: 可以处理类别型、数值型特征;
可扩展: 支持自定义损失函数;
对于类别型特征的处理需要大量的内存和时间;
不同随机数的设定对于模型预测结果有一定的影响;