Hexo

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

ML——XGBoost-vs-传统GBDT

本文主要介绍XGBoost和其他传统GBDT的比较的优劣
XGBoost又叫(Newton Boosting Tree)

  • GBDT推导: ML——GBDT-梯度提升树-推导过程
  • XGBoost推导: ML——XGBoost-推导过程

XGBoost的优点

参考博客: https://www.cnblogs.com/massquantity/p/9794480.html

  • XGBoost损失函数是二阶泰勒展开(与牛顿法对应),GBDT是一阶泰勒展开(与梯度下降法对应)

    • 传统 GBDT 在优化时只用到一阶导数信息, 所以传统GBDT也叫 (Gradient Boosting)
    • XGBoost 则对目标函数进行了二阶泰勒展开,同时用到了一阶和二阶导数, 所以XGBoost又叫(Newton Boosting Tree)
  • XGBoost加了正则项 ,普通的GBDT没有,所以XGBoost学出来的模型更简单,能防止过拟合,提高模型的泛化性能

  • XGBoost Shrinkage(缩减)

    • 每次进行完一次迭代后,将叶子节点的权重乘以该系数(一般叫做eta \(\eta\))
    • 可以理解为这里Shrinkage是将学习速率调小,从而需要的迭代次数增多
    • 减小学习率实际上是减弱了每棵树对整体的影响,从而让后面的树有更多的学习空间
    • 下面的表述还有待确定:
      • 进一步惩罚决策树叶节点的值(惩罚的意思是叶节点越大,惩罚越多,损失函数越大)
      • 对叶节点的惩罚本身可以理解为一个正则化
  • 结点分裂的增益计算公式不同

    • 传统 GBDT 一般采用的是最小二乘法作为内部分裂的增益计算指标(因为用的都是回归树)
      • 注意: 这里本文中描述的最小绝对偏差回归(LAD)是第一步损失函数的定义,不是这一步中的信息增益计算
      • 查看源码: sklearn.ensemble.GradientBoostingClassifier在分裂结点时可以选择三种方式:
        • friedman_mse(默认), mean squared error with improvement score by Friedman
        • mse: mean squared error
        • mae: mean absolute error
    • 而 XGBoost 使用的是经过优化推导后的式子
      $$
      \begin{align}
      Gain = \frac{G_L^2}{H_L+ \lambda} + \frac{G_R^2}{H_R+ \lambda} - \frac{(G_L + G_R)^2}{H_L+ H_R + \lambda} - \gamma
      \end{align}
      $$
      • 注意: XGBoost中的信息增益计算形式固定为上面的计算方式,但是具体的值与损失函数的定义相关(因为 \(g_i\) 和 \(h_i\) 的是损失函数的一阶和二阶梯度)
  • XGBoost支持自定义的损失函数 ,支持一阶和二阶可导就行

    • 注意,这里的损失函数指的是 \(l(y_i,\hat{y}_i)\),单个样本预测值与目标值的差异, 也就是单个样本的损失函数
    • 从ML——XGBoost-推导过程中可知:
      • \(g_i = l’(y_i,\hat{y}_i^{t-1})\) 为 \(l(y_i,\hat{y}_i)\) 对 \(\hat{y}_i\) 的一阶导数在 \(\hat{y}_i = \hat{y}_i^{t-1}\) 处的值
      • \(h_i = l’’(y_i,\hat{y}_i^{t-1})\) 是 \(l(y_i,\hat{y}_i)\) 对 \(\hat{y}_i\) 的二阶导数在 \(\hat{y}_i = \hat{y}_i^{t-1}\) 处的值
    • XGBoost中只要损失函数二次可微分即可得到 \(g_i\) 和 \(h_i\)
      • \(g_i\) 和 \(h_i\) 本身与损失函数的定义形式无关, 只要求损失函数二阶可微分即可
    • 只要有了 \(g_i\) 和 \(h_i\) 我们即可
      • 根据预先推导的叶子节点分数表达式 \(w_j^{\star} = -\frac{G_j}{H_j+\lambda}\) 求得叶子结点的分数
      • 根据预先推导的信息增益公式 \(Gain = \frac{G_L^2}{H_L+ \lambda} + \frac{G_R^2}{H_R+ \lambda} - \frac{(G_L + G_R)^2}{H_L+ H_R + \lambda} - \gamma\) 确定分裂特征和分裂点
    • GBDT 损失函数关系一般选择最小二乘回归或者最小绝对偏差回归
      • 最小方差回归 : (Least-Squares Regression, LSR)
        $$\begin{align} L(y,F(x)) = \frac{1}{2}(y-F(x))^{2} \end{align}$$
      • 最小绝对偏差回归 : (Least Absolute Deviation Regression, LAD)
        $$\begin{align} L(y,F(x)) = |y-F(x)| \end{align}$$
      • 查看源码: sklearn.ensemble.GradientBoostingClassifier的损失函数是定义好的, 不能自己定义, 详细如下源码
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        LOSS_FUNCTIONS = {'ls': LeastSquaresError,
        'lad': LeastAbsoluteError,
        'huber': HuberLossFunction,
        'quantile': QuantileLossFunction,
        'deviance': None, # for both, multinomial and binomial
        'exponential': ExponentialLoss,
        }


        _SUPPORTED_LOSS = ('deviance', 'exponential')
        ....

        if (self.loss not in self._SUPPORTED_LOSS
        or self.loss not in LOSS_FUNCTIONS):
        raise ValueError("Loss '{0:s}' not supported. ".format(self.loss))
  • XGBoost 借鉴了随机森林的做法,支持列采样 ,不仅能降低过拟合,还能减少计算量,这也是 XGBoost 异于传统 GBDT 的一个特性

    • 列采样: 这借鉴于随机森林中的做法,每棵树不使用所有特征,而是部分特征参与训练
    • 可以减少计算量,同时还能降低过拟合,简直优秀
  • XGBoost 有缺失值自动处理 , 在计算分裂增益时不会考虑带有缺失值的样本,这样就减少了时间开销,在分裂点确定了之后,将带有缺失值的样本分别放在左子树和右子树,比较两者分裂增益,选择增益较大的那一边作为默认分裂方向

    • 进一步理解稀疏数据的支持: [待更新]
  • 并行化处理 :由于 Boosting 本身的特性,传统 GBDT 无法像随机森林那样树与树之间的并行化

    • XGBoost 的并行主要体现在特征粒度上,在对结点进行分裂时,由于已预先对特征排序并保存为block 结构,每个特征的增益计算就可以开多线程进行,极大提升了训练速度
  • 剪枝策略不同

    • 传统 GBDT 在损失不再减少时会停止分裂,这是一种预剪枝的贪心策略,容易欠拟合
    • XGBoost采用的是后剪枝的策略,先分裂到指定的最大深度 (max_depth) 再进行剪枝
      • 而且和一般的后剪枝不同, XGBoost 的后剪枝是不需要验证集的[待更新:XGBoost剪枝的策略是怎样的?只依赖信息增益指标吗?]
      • 和博客作者指出的一样,我这里并不是”纯粹的”后剪枝,因为提前设定了最大深度
  • 基分类器的选择不同:

    • 传统GBDT中原始论文使用树回归 ,本文见Firedman 1999,后来作者提出可以使用逻辑回归 ,本文见Friedman 2000
    • XGBoost后面的各种损失计算等都包含着树模型的复杂度,叶子节点分类等,所以是只能用CART,不能使用逻辑回归的
    • (从函数空间定义和后面的公式推导来看)XGBoost中基函数只使用CART回归树,不能使用逻辑回归
    • 但是事实上XGBoost的实现中是支持线性分类器作为基分类器的, 参数booster[default='gbtree'],可选为booster=gblinear
      • 使用线性分类器作为基分类器时, XGBoost相当于带有L1正则化和L2正则化的:
        • Logistic回归(分类问题)
        • 线性回归(回归问题)
  • 分桶策略算法不同

    • 传统的GBDT分桶时每个样本的权重都是相同的
    • XGBoost中每个样本的权重为损失函数在该样本点的二阶导数(对不同的样本,计算得到的损失函数的二阶导数是不同的), 这里优点AdaBoost的思想,重点关注某些样本的感觉
    • 这里影响的是划分点的位置(我们划分划分点[桶]时都是均匀划分样本到桶里面,当不同样本的权重不同时,每个桶里面的样本数量可能会不同)
    • 下图是一个示例

XGBoost的缺点

注意这里是

  • 空间消耗大
    • 因为要在训练之前先对每个特征进行预排序并将结果存储起来,所以空间消耗较大
    • GBDT无需预排序,但是每次重新排序很耗时间
  • 速度慢
    • 虽然XGBoost速度比传统 GBDT 快了不少, 但是不如 LightGBM 快, 且 LightGBM 占用内存更低

XGBoost为什么能够并行?

而GBDT是不能并行的,原因是:https://www.136.la/shida/show-187480.html

  • GBDT不能并行的原因是没有预排序(XGB的预排序结果会存储到block结构)等,在有了预排序结果后,同一个特征的切割方式可以并行尝试计算增益
  • 决策树最耗时间(包括GBDT)的步骤是对特征值的排序
    • 用于确定最佳分割点
  • XGBoost训练前,预先对数据进行了排序,称为预排序
    • 将预先排序的结果保存为block结构, 后面迭代的时候重复使用这个结构,从而实现一次排序,多次使用,大大减少计算量
  • 这个结构减少计算量的同时还为并行化提供了可能([待更新]实际上不用预排序也能并行的吧?只是每次需要先使用一个单一线程排序,然后再多个线程并行?只是不够并行)
    • 进行结点的分裂时,需要计算每个特征的增益,然后选择增益最大的那个特征分裂
    • 这里我们可以同时使用多个线程计算不同特征的增益, 从而实现并行化
  • 总结为三方面的并行, ([待更新]但是具体实现了哪些并行不确定)
    • 同一层级的结点间每个结点的分裂可以并行
    • 同一个结点内部不同特征增益的计算可以并行
    • 同一个结点同一个特征的不同分裂点的增益计算可以并行

GBDT为什么不能自定义损失函数?

GBDT为什么不能像XGBoost一样自定义损失函数?

  • 查看sklearn.ensemble.GradientBoostingClassifier的源码发现, 确实不支持自定义的损失函数
  • [待更新],因为涉及到后面的参数更新方式?

XGBoost如何使用自定义的损失函数?

模型直接调用fit函数无法传入自定义的损失函数, 需要在模型开始定义的时候传入或者使用xgb.train函数调用

  • 使用方法1:

    1
    2
    3
    from xgboost import XGBClassifier

    clf = XGBClassifier(objective=MyLossFunction)
  • 使用方法2:

    1
    2
    3
    4
    5
    import xgboost as xgb
    from xgboost import XGBClassifier

    clf = XGBClassifier()
    xgb.train(xgb_model=clf, obj=MyLossFunction)

Algorithm——AVL树和红黑树等各种树结构总结


各种树的介绍

树

  • 一个根节点,每个结点可能有多个子节点

二叉树

  • 一个根节点,或者为空
  • 每个结点只有两个子节点

二叉搜索树

也叫二叉查找树

  • 首先是一棵二叉树
  • 左边孩子结点的值都小于当前结点
  • 右边孩子结点的值都大于当前结点
缺点
  • 极端情况下,树模型会退化成链表,查找变成了 O(n) 复杂度的

平衡二叉搜索树(AVL树)

也叫平衡二叉查找树

  • 首先是一棵二叉搜索树
  • 对每个结点而言, 左右孩子结点的深度差值不能超过1 , 从而保证查找是 O(log n) 的
  • 控制平衡方法: 参考链接AVL树详解
    • 左-左型: 右旋
    • 右-右型: 左旋
    • 左-右型: 左旋 + 右旋
      • 第一步左旋后面部分,变成 左-左 型
      • 第二步使用右旋修正 左-左 型
    • 右-左型: 右旋 + 左旋
      • 第一步右旋后面部分,变成 右-右 型
      • 第二步使用左旋修正 右-右 型
缺点
  • 每棵树的左右子树高度最多差1这个要求太严了
  • 几乎每次插入或者删除结点时都会造成规则破坏, 也就需要频繁的通过左旋或者右旋操作来修正
  • 插入和删除太频繁的场景中不太适合使用AVL树, 性能会因为左右子树高度最多差1这个规则频繁被打破而降低

红黑树

  • 首先是一棵二叉搜索树
  • 每个结点不是黑色就是红色
  • 根节点为黑色
  • 每个叶子结点都为黑色的空结点(NULL)
    • 注意: 叶子节点不存数据
  • 任何相邻结点不同时为红色
    • 注意,相邻结点可以为黑色,且可以某条路径上全是黑色
  • 对每个结点而言,当前结点到叶子结点的所有路径包含相同的黑色结点数
优点
  • 能保证查找时间复杂度是 O(log n) 的, 和AVL树差不多
    • 证明: [待更新]
  • 插入和删除操作中不会频繁破坏红黑树的规则
红黑树的应用
  • 容器集合 HashMap, TreeMap等
    • HashMap是 链表 + 红黑树的实现, 当冲突时就需要使用红黑树加快检索
    • HashMap中 链表长度太短时使用链表, 太长时使用红黑树, 这个阈值一般设置为8

二三树

  • 红黑树是二三树的一个变形,一般用红黑树就够了
    [待更新]

B树

  • B树在大量的数据库和文件系统场景中都有使用
    [待更新]

B+树

[待更新]


总结

  • 可以说红黑树是一种不严格的平衡树
1…297298299…352
San Ye

San Ye

Stay Hungry. Stay Foolish.

704 posts
53 tags
© 2026 San Ye
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4