全称:eXtreme Gradient Boostinghtml
做者:陈天奇python
基础:GBDTgit
所属:boosting迭代型、树类算法github
适用范围:回归,分类,排序算法
xgboost工具包:sklearn xgboost连接 | xgboost工具包(中文)连接 | xgboost工具包(英文)连接chrome
优势:api
缺点:(与LightGBM相比)数组
xgboost 也是使用与提高树相同的前向分步算法。其区别在于:xgboost 经过结构风险极小化来肯定下一个决策树的参数 :缓存
最初损失函数:
$L_t=\sum\limits_{i=1}^mL(y_i, f_{t-1}(x_i)+ h_t(x_i)) + \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2$
在GBDT损失函数$L(y, f_{t-1}(x)+ h_t(x))$的基础上,加入正则项$\Omega(h_t) = \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2$其中,J是叶子节点的个数,$w_{tj}$是第j个叶子节点的最优值,这里的$w_{tj}$和GBDT中的$c_{tj}$是一个意思,Xgboost论文中用的是w表示叶子的值,这里和论文保持一致。
损失函数的二阶展开:
$\begin{align} L_t & = \sum\limits_{i=1}^mL(y_i, f_{t-1}(x_i)+ h_t(x_i)) + \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2 \\ & \approx \sum\limits_{i=1}^m( L(y_i, f_{t-1}(x_i)) + \frac{\partial L(y_i, f_{t-1}(x_i) }{\partial f_{t-1}(x_i)}h_t(x_i) + \frac{1}{2}\frac{\partial^2 L(y_i, f_{t-1}(x_i) }{\partial f_{t-1}^2(x_i)} h_t^2(x_i)) + \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2 \end{align}$
为了方便,记第i个样本在第t个弱学习器的一阶和二阶导数分别为:
$g_{ti} = \frac{\partial L(y_i, f_{t-1}(x_i) }{\partial f_{t-1}(x_i)}, \; h_{ti} = \frac{\partial^2 L(y_i, f_{t-1}(x_i) }{\partial f_{t-1}^2(x_i)}$
则损失函数能够表达为:
$L_t \approx \sum\limits_{i=1}^m( L(y_i, f_{t-1}(x_i)) + g_{ti}h_t(x_i) + \frac{1}{2} h_{ti} h_t^2(x_i)) + \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2$
第一项是常数,对最小化loss无影响,能够去掉,同时因为每一个决策树的第j个叶子节点的取值最终是同一个值$w_{tj}$,所以损失函数简化为:
$\begin{align} L_t & \approx \sum\limits_{i=1}^m g_{ti}h_t(x_i) + \frac{1}{2} h_{ti} h_t^2(x_i)) + \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2 \\ & = \sum\limits_{j=1}^J (\sum\limits_{x_i \in R_{tj}}g_{ti}w_{tj} + \frac{1}{2} \sum\limits_{x_i \in R_{tj}}h_{ti} w_{tj}^2) + \gamma J + \frac{\lambda}{2}\sum\limits_{j=1}^Jw_{tj}^2 \\ & = \sum\limits_{j=1}^J [(\sum\limits_{x_i \in R_{tj}}g_{ti})w_{tj} + \frac{1}{2}( \sum\limits_{x_i \in R_{tj}}h_{ti}+ \lambda) w_{tj}^2] + \gamma J \end{align}$
把每一个叶子节点区域样本的一阶和二阶导数的和单独表示以下:
$G_{tj} = \sum\limits_{x_i \in R_{tj}}g_{ti},\; H_{tj} = \sum\limits_{x_i \in R_{tj}}h_{ti}$
最终损失函数的形式能够表示为:
$L_t = \sum\limits_{j=1}^J [G_{tj}w_{tj} + \frac{1}{2}(H_{tj}+\lambda)w_{tj}^2] + \gamma J$
xgboost须要目标函数的二阶导数信息(或者hess矩阵),在回归问题中常常将MAE或MAPE做为目标函数,然而,这两个目标函数二阶导数不存在。
$MAE=\frac{1}{n}\sum_1^n|y_i-\hat y_i|$,$MAPE=\frac{1}{n}\sum _i^n \frac{|y_i-\hat y_i|}{y_i}$
其中,$y_i$是真实值,$\hat y_i$是预测值
方法(1):利用可导的函数逼近MAE或MAPE---MSE、Huber loss、Pseudo-Huber loss
利用MSE逼近是能够的,可是MSE在训练初偏差较大的时候,loss是其平方,会使得训练偏离MAE的目标函数,通常难以达到高精度的要求。
利用Huber loss进行逼近也能够,可是Huber loss是分段函数,不方便计算,其中$\delta$是可调节参数。
实际采用Huber loss的可导逼近形式:Pseudo-Huber loss function
一阶导数:
二阶导数:
方法(2):自定义二阶导数的值:$ln(cosh(x))$
用$ln(cosh(x))$以及$log(exp(-x) + exp(x))$进行逼近
$ln(cosh(x))$的一阶导数:$tanh(x)$
$ln(cosh(x))$的二阶导数:$1-tanh(x)*tanh(x)$
Xgboost框架用tree_method[默认为’auto’] 指定了构建树的算法,能够为下列的值(分布式,以及外存版本的算法只支持 ‘approx’,’hist’,’gpu_hist’ 等近似算法):
‘auto’: 使用启发式算法来选择一个更快的tree_method: 对于小的和中等的训练集,使用exact greedy 算法分裂节点 对于很是大的训练集,使用近似算法分裂节点 旧版本在单机上老是使用exact greedy 分裂节点 ‘exact’: 使用exact greedy 算法分裂节点 ‘approx’: 使用近似算法分裂节点 ‘hist’: 使用histogram 优化的近似算法分裂节点(好比使用了bin cacheing 优化) ‘gpu_exact’: 基于GPU 的exact greedy 算法分裂节点 ‘gpu_hist’: 基于GPU 的histogram 算法分裂节点
(1)第一种方法是对现有的叶节点加入一个分裂,而后考虑分裂以后目标函数下降多少。
(2)对于一个叶节点,加入给定其分裂点,定义划分到左子样本节点的集合为:$\mathbb{I_R}$,则有:
(3)定义叶节点的分裂增益为:
其中,
每次只有一个叶节点分裂,所以其余叶节点不会发生变化,所以:
(4)如今的问题是:不知道分裂点,对于每一个叶节点,存在多个分裂点,且可能不少分裂点都能带来增益。
解决办法:对于叶节点中的全部可能的分裂点进行一次扫描。而后计算每一个分裂点的增益,选取增益最大的分裂点做为本叶节点的最优分裂点。
(5)最优分裂点贪心算法
输入:$D={(X_1,y_1),(X_2,y_2), ...(X_m,y_m)}$,属于当前叶节点的样本集的下标集合$\mathbb{I}$
输出:当前叶节点最佳分裂点
算法:
step1:初始化 $score \leftarrow 0$,$G \leftarrow_{i\in \mathbb{I}}g_i$,$H \leftarrow_{i\in \mathbb{I}}h_i$
step2:遍历各维度 $k=1,2,...,m$:
a)初始化:$G_L \leftarrow 0,H_L \leftarrow 0$
b)若是第$k$维特征为连续值,则将当前叶节点中的样本从小到大排序。而后用$j$顺序遍历排序后的样本下标。
c)若是第$k$维特征为离散值${a_1,a_2,...,a_{n_k}},设当前叶节点中第$k$维取值$a_j$样本的下标集合为$\mathbb{I_j}$,则遍历$j=1,2,...,n_k$:
step3:选取最大的$score$对应的维度和拆分点做为最优拆分点。
分裂点贪心算法尝试全部特征和全部分裂位置,从而求得最优分裂点。当样本太大且特征为连续值时,这种暴力作法的计算量太大。
(1)近似算法寻找最优分裂点时不会枚举全部的特征值,而是对特征值进行聚合统计,而后造成若干个桶。而后仅仅将桶边界上的特征的值做为分裂点的候选,从而获取计算性能的提高。
(2)对第k个特征进行分桶,分桶的数量l就是全部样本在第k个特征上的取值的数量。
若是第k个特征为连续特征,则执行百分位分桶,获得分桶的区间为:$S_k={s_{k,1},s_{k,2},...,s_{k,l}}$,其中$s_{k,1}<s_{k,2}<...<s_{k,l}$,分桶的数量、分桶的区间都是超参数,须要仔细挑选
若是第k个特征为离散特征,则执行按离散值分桶,获得的分桶为:$S_k={s_{k,1},s_{k_2},...,s_{k,l}}$,其中,$s_{k,1}<s_{k,2}<...<s_{k,l}$ 为第k个特征的全部可能的离散值。
(3)最优分裂点近似算法
算法流程:
输入:数据集$D={(X_1,y_1),(X_2,y_2),...,(X_N,y_N)}$,属于当前叶结点的样本集的下标集合$\mathbb{I}$
输出:当前叶节点最佳分裂点
step1:对每一个特征进行分桶。假设对第k个特征上的值进行分桶为:$S_k={s_{k,1},s_{k,2},...,s_{k,l}}$,若是第k个特征为连续特征,则要求知足$s_{k,1}<s_{k,2}<...<s_{k,l}$
step2:初始化:$score \leftarrow 0,G\leftarrow \sum_{i\in \mathbb{I}} g_i ,H\leftarrow \sum_{i\in \mathbb{I}} h_i$
step3:遍历各维度:$k=1,...,n$
初始化:$G_L \leftarrow 0,H_L \leftarrow 0$
遍历各拆分点,即遍历$j=1,2,...,l$:
若是是连续特征,即设叶节点的样本中,第k个特征取值在区间$(s_{k,j},s_{k,j+1}]$的样本的下标集合为$\mathbb{I}_j$,则:
若是是离散特征,则设叶结点的样本中,第k个特征取值等于$s_{k,j}$的样本的下标集合为$\mathbb{I}$ ,则:
选取最大的score对应的维度和拆分点做为最优拆分点。
(4)分桶有两种模式:
全局模式:在算法开始时,对每一个维度分桶一次,后续的分裂都依赖于该分桶并再也不更新;
优势:只须要计算一次,不须要重复计算;
缺点:在通过屡次分裂以后,叶节点的样本有可能在不少全局桶中是空的。
局部模式:每次拆分以后再从新分桶;
优势:每次分桶都能保证各桶中的样本数量都是均匀的;
缺点:计算量较大。
全局模式会构造更多的候选拆分点,而局部模式会更适合构造构造更深的树。
(5)分桶时的桶区间间隔大小是个重要的参数。区间间隔越小,则桶越多,划分的越精细,候选的拆分点就越多。
Quantile就是ranking。若是有$N$个元素,那么$\phi$-quantile就是指rank在$⌊\phi × N⌋$的元素。例如$S=[11,21,24,61,81,39,89,56,12,51]$,首先排序为$[11,12,21,24,39,51,56,61,81,89]$,则$0.1-quantile=11, 0.5-quantile=39$. 上面的是exact quantile寻找方法,若是数据集很是大,难以排序,则须要引入$\epsilon-approximate \phi-quantiles$
该方法为离线算法(全部的数必需要排序,再找分位点),是不适用于数据流的。
$\phi$-quantile是在区间$[⌊(\phi−\epsilon)×N⌋,⌊(\phi+\epsilon)×N⌋]$
当$N$增长时,$φ$-quantile的“正确”答案($\epsilon$-近似)的集合增长。所以,您能够从输入流中删除一些元素,并仍保留ε近似分位数查询的正确答案(=询问$\epsilon$近似分位数的查询)
回到XGBoost的建树过程,在创建第i棵树的时候已经知道数据集在前面i−1棵树的偏差,所以采样的时候是须要考虑偏差,对于偏差大的特征值采样粒度要加大,偏差小的特征值采样粒度能够减少,也就是说采样的样本是须要权重的。
从新审视目标函数:
$$\begin{equation} \sum_{i=1}^n [g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i)] + \Omega(f_t) \end{equation}$$
经过配方能够获得
$$\begin{equation} \sum_{1}^n \left[ \frac {1}{2} h_i \left( f_t(x_i) - (-g_i/h_i)\right)^2 \right] + \Omega (f_t) + constant \end{equation}$$
所以能够将该目标看作是第$m$棵决策树,关于真实标签为$-\frac{g_i}{h_i}$和权重为$h_i$的、损失函数为平方损失的形式。
若是损失函数是Square loss,即$Loss(y, \widehat y) = (y - \widehat y)^2$,则$h=2$,那么其实是不带权;若是损失函数是Log Loss,则$h=pred * (1-pred)$。这是个开口朝下的一元二次函数,因此最大值在0.5。当$pred$在0.5附近,这个值是很是不稳定的,很容易误判,h做为权重则所以变大,那么直方图划分,这部分就会被切分的更细。
假设候选样本的第k维特征,及候选样本的损失函数的二阶偏导数为:$\begin{equation} D_k = \{(x_{1k}, h_1), (x_{2k}, h_2), \cdots (x_{nk}, h_n)\} \end{equation}$
定义排序函数:$x_{i,k}$表示样本$x_i$的第$k$个特征
它刻画的是:第$k$维特征小于$z$的样本的$h$之和,占总的$h$之和的比例,其中二阶导数$h$能够视为权重,在这个排序函数下,找到一组点$\{ s_{k1}, s_{k2}, ... ,s_{kl} \}$,知足:$% <![CDATA[ \begin{equation} | r_k (s_{k,j}) - r_k (s_{k, j+1}) | < \varepsilon \end{equation} %]]>$。
其中,${s_{k1}} = \mathop {\min }\limits_i {x_{ik}},{s_{kl}} = \mathop {\max }\limits_i {x_{ik}}$,$\epsilon$为采样率,直观上理解,最后会获得$1/{\epsilon}$个分界点。其中$x_{i,k}$表示样本$x_i$的第$k$个特征,即:
最小的拆分点:全部样本第$k$维的最小值;
最大的拆分点:全部样本第$k$维的最大值;
中间的拆分点:选取拆分点,使得相邻拆分点的排序函数值小于$\epsilon$(分桶的桶宽)。其意义为:第$k$维大于等于$s_{k,j}$,小于$s_{k,j+1}$的样本的$h$之和,占总的$h$之和的比例小于$\epsilon$;这种拆分点使得每一个桶内的以$h$为权重的样本数量比较均匀,而不是样本个数比较均匀。
举例:
要切分为3个,总和为1.8,所以第1个在0.6处,第2个在1.2处。
对于每一个样本都有相同权重的问题,有quantile sketch算法解决该问题,做者提出Weighted Quantile Sketch算法解决这种weighted datasets的状况
问题:To design an algorithm, you must first design an adequate data structure to maintain the information used by the algorithm
该数据结构须要每插入值进行大量操做。 虽然它颇有用,但效率不高
这个数据结构存在问题:它不包含足够的信息来删除没必要要的条目
定义:
$v_0$=目前为止遇到的最小的数
$v_{s-1}$=目前为止遇到的最大的数
三个性质:
举例:
命题1:summary达到的准确度,偏差$e=max_{all i}(g_i+\triangle_i)/2$
推论1:Greenwald和Khanna算法的不变性
GK算法框架:先判断是否要合并,再插入
插入算法:
删除算法:
how to use the quantile summary?
直方图聚合是树木生长中的主要计算瓶颈。咱们引入了一种新的树生长方法hist,其中只考虑了可能的分裂值的子集。与FastBDT和LightGBM同样,连续特征被分红不连续的区域。因为较少的索引操做,直方图累积变得更快
新方法与tree_method = approx有何不一样?
hist方法能够实现approx方法没法实现的额外优化,以下所示:
除了上述改进以外,还有一些亮点
如何使用?
(1)真实场景中,有不少可能致使产生稀疏。如:数据缺失、某个特征上出现不少 0 项、人工进行 one-hot 编码致使的大量的 0。
注意:每一个结点的默认分裂方向可能不一样。
(2)在xgboost 算法的实现中,容许对数值0进行不一样的处理。能够将数值0视做缺失值,也能够将其视做有效值。 若是数值0是有真实意义的,则建议将其视做有效值。
(3)缺失值处理算法
输入:数据集$D={(X_1,y_1),(X_2,y_2),...,(X_N,y_N)}$
属于当前叶结点的样本集的下标集合$\mathbb{I}$
属于当前叶节点,且第$k$维特征有效的样本的下标集合$\mathbb{I}_k = \{{i\in \mathbb{I}| x_{k,i}\neq missing}\}$
输出:当前叶节点最佳分裂点
step1:初始化:$score \leftarrow 0,G\leftarrow \sum_{i\in \mathbb{I}} g_i ,H\leftarrow \sum_{i\in \mathbb{I}} h_i$
step3:遍历各维度:$k=1,...,n$
先从左边开始遍历:
初始化:$G_L \leftarrow 0,H_L \leftarrow 0$
遍历各拆分点:沿着第$k$维,将当前有效的叶节点的样本从小到大排序。这至关于全部无效特征值的样本放在最右侧,所以能够保证无效的特征值都在右子树。而后用$j$顺序遍历排序后的样本下标:
再从右边开始遍历:
初始化:$G_R \leftarrow 0,H_R \leftarrow 0$
遍历各拆分点:沿着第$k$维,将当前有效的叶节点的样本从大到小排序。这至关于全部无效特征值的样本放在最左侧,所以能够保证无效的特征值都在左子树。而后用$j$逆序遍历排序后的样本下标:
选取最大的score对应的维度和拆分点做为最优拆分点。
缺失值处理算法中,经过两轮遍历能够确保稀疏值位于左子树和右子树的情形。
xgboost在学习过程当中使用了以下的正则化策略来缓解过拟合:
xgboost在如下方面提出改进来提高计算速度:
(1)xgboost提出column block数据结构来下降排序时间。
时间复杂度减小:
(2)block能够仅存放样本的索引,而不是样本自己,这样节省了大量的存储空间。
如:block_1表明全部样本在feature_1上的从小到大排序:sample_no1,sample_no2,...
其中样本编号出现的位置表明了该样本的排序。
能够看出,只需在建树前排序依次,后面节点分裂时能够直接根据索引获得梯度信息。
(1)因为在column block中,样本的顺序会被打乱,这会使得从导数数组中获取$g_i$时的缓存命中率较低。
所以,xgboost提出了cache-aware预取算法,对每一个线程分配一个连续的buffer,读取梯度信息并存入Buffer中(这样就实现了非连续到连续的转化),而后再统计梯度信息。该方式在训练样本数大的时候特别有用,用于提高缓存命中率。
(2)xgboost会以minibatch的方式累加数据,而后在后台开启一个线程来加载须要用到的导数$g_i$。
这里有个折中:minibatch太大,会引发cache miss;过小,则并行程度较低。
(1)xgboost利用硬盘来处理超过内存容量的大数据量,其中使用了下列技术:
参考文献:
【5】『我爱机器学习』集成学习(三)XGBoost - 细语呢喃
【6】gbdt.pdf
【7】Xgboost系统设计:分块并行、缓存优化和Blocks for Out-of-core Computation - anshuai_aw1的博客 - CSDN博客