XGBoost 优化思路、公式推导及案例演示

XGBoost(Extreme Gradient Boosting)2014 年由陈天奇开发,旨在对传统梯度提升算法(GBDT)进行高效实现和优化。2015 年,XGBoost 在 Kaggle 竞赛中表现卓越,多个冠军队伍使用了 XGBoost,横扫众多榜单。凭借卓越的性能和广泛的适用性,XGBoost 很快成为竞赛和工业界的标配算法,被广泛应用于金融风控、推荐系统、广告点击率预测、科研建模等领域。

文档:https://xgboost.readthedocs.io/en/stable
论文:https://arxiv.org/pdf/1603.02754

1. 优化思路

在 GBDT 的训练过程中,模型通过不断拟合负梯度逐步优化预测结果,但仍存在两个明显的不足,这些不足会影响模型的训练稳定性和泛化能力。

1.1 结构复杂度

在传统 GBDT 中,每棵树的构建目标是尽可能降低训练损失,因此树会不断分裂以细化样本空间。随着分裂进行,叶子节点上的样本数量可能越来越少,极端情况下,某些叶子节点可能只覆盖一个样本,此时训练误差几乎为零。但这种过度拟合训练数据的做法容易捕捉到噪声,从而导致模型泛化能力下降,即过拟合问题。

有些同学可能会想:直接通过超参数限制叶子节点数量或树的深度,不就能防止模型过拟合了吗?是的,它有一定的作用,但我们要知道这是一种全局的硬限制,它不考虑树的实际生长情况。例如:

  • 一些本可以带来显著损失下降的分裂被提前阻断
  • 一些几乎无益的分裂,却因为没触发限制而被允许

因此,我们需要一种更灵活的方式,让模型在生长过程中,依据每次分裂对损失的实际贡献自动决策。这样既能避免无效分裂,也能保留真正有效的分裂。在此基础上,再辅以全局的硬约束,就能在灵活与稳健之间取得更好的平衡。

结论:我们需要在损失函数中引入控制树结构复杂度的正则化项,分裂时,既能考虑训练损失,也可以考虑到结构复杂带来的损失。

1.2 输出复杂度

在 GBDT 的树构建过程中,叶子节点的输出值是根据样本的一阶导和二阶导动态计算的,以保证每个叶子在拟合训练数据时步幅合适。

一阶导反映当前树要调整的方向和步长,是由真实值和预测值之间的差距决定的,会受到真实目标值的影响
二阶导反映的是预测输出的变化对损失函数的影响,是由损失函数的数学形状和当前预测值在该曲线的位置,它不受到真实值的影响。

如果某个样本是噪声(真实目标值偏离正常的值范围):

  1. 如果当前预测值和真实值差距很大,一阶导值就很大
  2. 二阶导因为不受到目标值的影响,所以其有可能目前的值很小

这就导致,叶子节点输出值较为激进,使得正常的样本被迫进行过度调整,损失增大。所以,如果训练样本中,存在噪声,我们需要对输出进行惩罚,并且噪声的偏离真实情况的程度越大,惩罚的力度也要适当增大。

结论:损失函数中需要引入控制决策树输出复杂度的正则化项,使得训练过程更加平稳

2. 公式推导

XGBoost 在 GBDT 的基础上,核心改进之一是为损失函数引入正则化项,通过约束树的结构复杂度与输出复杂度,让每棵基树的构建更稳健,有效降低过拟合风险。另一关键优化在于节点分裂增益的计算逻辑:它摒弃了传统决策树仅依赖平方误差的简单方式,创新性地融合损失函数的一阶、二阶梯度信息,同时将树的结构与输出复杂度纳入考量,最终形成更精准的分裂决策依据。因此,我们对 XGBoost 展开公式推导的核心目的的是:明确其分裂增益的具体计算公式及推导逻辑。

2.1 目标函数

XGBOOST 损失函数包含两部分:

  • 训练误差:衡量模型在训练数据上误差
  • 正则化项:用于控制树的复杂度(结构复杂度、输出复杂度)

\( \gamma T \):表示对叶子节点数量的惩罚,\( \gamma \) 越大说明越希望叶子节点少一些、树不那么高大,不要过于贴合训练数据。
\( \frac{1}{2}\lambda \|w\|^2 \) 表示对模型输出的惩罚。\(\lambda\) 越大说明越希望模型输出保守一些,不要过大。

这个目标函数包含了XGBOOST 完整的预测更新 + 残差拟合 + 正则化 + 损失优化联动逻辑,我们通过例子来深刻理解:

假设:目标值 100,初始预测 20,初始损失 6400,每棵新树聚焦拟合当前总预测与目标的差距(负梯度),目标是最大化总损失的下降幅度。

轮次拟合残差实际输出总预测更新当前损失=训练+正则损失下降说明
第 1 棵树8030502500+200-3700损失大幅下降
第 2 棵树5010601600+150-950损失持续下降
第 3 棵树403090100+100-1550损失进一步下降

后续树循环拟合剩余残差,总预测逐步逼近目标值,总损失持续向 0 收敛,但是注意过拟合风险。

总结:我们的优化目标是,构建一个新树\(f_{m}\) 进一步降低损失。由于并不是随意构建一个新树就能降低损失,所以,对于新树,我们得知道该树如何输(未知)以及如何构建才能降低损失(未知)。

由于 XGBOOST 构建树不仅要考虑训练损失,还要考虑树的复杂度,GBDT 使用的平方误差分裂增益计算方法并不适合,所以需要一种新的分裂增益计算方法。

2.2 泰勒展开

由于 \( l(y_i, \hat{y}_{i}) \) 可能是任意复杂的非线性函数,并且,我们要优化的 \( f_{m}(x) \) 是整个损失函数内部的一部分,直接对 \( f_{m}(x) \) 优化不现实。所以,我们需要使用泰勒二阶展开这个工具将损失函数近似成一个关于 \( f_{m}(x) \) 的二次函数,这样就能继续优化。

泰勒二阶展开的公式如下:

  • \(a\) 展开点,当前已知位置
  • \(x\) 自变量,要看的目标点
  • \(x-a\) 偏移量
  • \(f(a)\) 当前函数值
  • \(f^{\prime}(a)\) 当前变化趋势(斜率)
  • \(f^{\prime\prime}(a)\) 当前弯曲程度(曲率)

此处泰勒二阶展开的意思是,通过现在已有的模型损失,去近似表示出增加一棵新树后的损失。我们对损失函数在 \( t-1 \) 位置进行泰勒二阶展开:

  • \( g_{i} \) 表示样本的一阶导
  • \( h_{i} \) 表示样本的二阶导

\( l(y_i, \hat{y}^{t-1}) \) 表示还没有加入新树时的损失,这一项表示的模型是已知的,是常数项,可以去掉,我们就得到了一个关于 \( f_{t}(x_{i}) \) 的公式:

这个公式表示,如果新增第 \(t\) 棵 \(f_{t}\) 树时损失的下降幅度(新损失 – 旧损失),该值越小,下降幅度越大,从而让整体 XGB 损失下降最多。

再具体一点:我们希望通过新增树,让每个样本的预测值能够更加精准,损失下降幅度尽可能大,从而整体损失下降最多。


现在的公式,是从样本的角度来统计损失的下降幅度,需要将其转换为从叶子节点角度的损失表示:

注意:上述公式中,\( w \) 表示叶子节点的输出值。

这个公式表示,新增第\(m\)棵树时,所有叶子节点所覆盖样本的损失下降幅度之和。我们希望每个叶子节点贡献的下降幅度尽可能显著,使得总体损失尽可能下降。


为了使得每个叶子节点都能尽可能下降损失,我们需要知道叶子节点的输出 \( w_{j} \) 的最优值,对 \( \gamma \) 求导,并令导数等于 0。

  • 当构建好决策树后,每个叶子节点的输出值按照公式确定
  • 叶子节点的值是由节点覆盖所有样本的一阶导和二阶导来确定
  • 分子中的一阶导信息,它表示本轮对输出调整的步长
  • 分母中的二阶导信息,它用来动态调整本轮输出的步长

此时,将叶子节点输出值公式代回到原公式中,得到在最优输出的情况下,树的损失表示:

再简化下上面公式的表示:

\( G_{i} \) 表示叶子结点上一阶导之和,\( H_{i} \) 表示叶子结点上的二阶导之和。

2.3 最优输出

关于这个公式,有以下几点需要理解:

  • 当构建好决策树后,每个叶子节点的输出值按照该公式确定
  • 分子中的一阶导信息,它表示本轮对输出调整的步长
  • 分母中的二阶导信息,它用来动态调整本轮输出的步长
  • \( \lambda \) 超参数,控制对叶子节点输出值的惩罚

在 GBDT 中,如果叶子节点的输出值仅依据一阶导(负梯度)来计算,那么输出值的调整仅取决于叶子内所有样本一阶导的总和。一阶导越大,表示模型需要对预测值做大幅修正。但这种做法存在一个严重问题:模型只确定了调整方向和统一步长,却完全忽略了不同样本对调整幅度的敏感程度,本质上是一刀切的优化策略。

具体来说,叶子节点覆盖的样本通常处于不同的优化状态:

  • 部分样本对预测值的微调极为敏感,即使输出值变化一点,损失就会剧烈波动
  • 另一些样本对预测值调整不敏感,适度加大输出值也不会导致损失显著变化

如果使用统一步长调整,就可能出现顾此失彼的情况:

  • 对敏感样本而言,步长过大,会使损失暴涨
  • 对不敏感样本而言,步长合理,可以使损失下降

最终可能出现敏感样本的损失增幅超过不敏感样本的损失下降,导致整体损失不降反升。

因此,优化的关键在于:不仅要明确调整方向,还要判断调整幅度是否安全

这时,损失函数的二阶导就发挥作用了,它表示输出值变化对损失的影响程度:

  • 二阶导越大,说明该样本对输出值的微小变化非常敏感,承受能力弱
  • 二阶导越小,说明输出值调整后损失变化平缓,承受能力强

基于二阶导信息,可以对叶子输出进行修正:

  • 如果叶子内样本的总二阶导较大(整体承受能力弱),就适度减小输出值,使调整更加保守,保证损失平稳下降
  • 如果总二阶导较小(整体承受能力强),就可以加大输出值,加快模型拟合进度

因此,引入二阶导信息,可以使训练过程中的损失变化更加稳定,训练更加稳健。

2.4 分裂增益

我们再回顾一个重要公式:\( f_{t} \) 下降幅度是由多个叶子节点构成。单个叶子节点下降的损失表示为:

当某个节点分裂为左右节点,分裂后的损失下降幅度就是左右节点的下降损失和。

那么,我们可以通过衡量分裂前后的损失下降幅度来构建一个新的分裂增益计算方法(既考虑训练损失,也考虑树的复杂度带来的损失):

  • 当 Gain > 0,分裂前能降 -500,分裂后能降 -800,分裂后损失能多降 300,该分裂需要被考虑
  • 当 Gain <= 0,分裂前能降 -500,分裂后能降 -300,分裂后损失能少降 200,该分裂不考虑

这个公式就是 XGBoot 算法的分裂增益计算公式,在 XGBoost 的树构建过程中,在所有候选切分点中选择分裂增益(Gain)最大的那个进行分裂

3. 计算案例

样本 ixy
112.1
224.0
336.2
448.1
5510.0
6612.2
7714.1
8816.0
9918.1
101020.2

损失函数:平方损失,λ = 1, γ = 0

损失函数一阶导和二阶导计算公式:

初始预测取均值:

计算每个样本的一阶导和二阶导值:

样本 ixy基础预测一阶导二阶导
112.111.19.01
224.011.17.11
336.211.14.91
448.111.13.01
5510.011.11.11
6612.211.1-1.11
7714.111.1-3.01
8816.011.1-4.91
9918.111.1-7.01
101020.211.1-9.11

我们以分裂点 x < 6 为例,计算下其分裂增益:

节点样本集合一阶导和 (G)二阶导和 (H)
左子集(1, 2, 3, 4, 5)25.15
右子集(6, 7, 8, 9, 10)-25.15
父节点所有样本010

带入到分裂增益计算公式中:

Gain = 0.5 ∗ (105.0 + 105.0 − 0)−0 = 0.5 ∗ 210.0 = 105.0

最后计算下叶子节点的输出值:

# pip install scikit-learn
# pip install xgboost
# pip install matplotlib

from xgboost import XGBRegressor, plot_tree
import numpy as np
import matplotlib.pyplot as plt

def demo():
    # 构造数据
    x = np.array([1,2,3,4,5,6,7,8,9,10]).reshape(-1,1)
    y = np.array([2.1,4.0,6.2,8.1,10.0,12.2,14.1,16.0,18.1,20.2])

    # 训练模型
    xgb = XGBRegressor(n_estimators=2, max_depth=2, learning_rate=0.1, objective='reg:squarederror')
    xgb.fit(x, y)

    # 绘制每棵树
    for idx in range(xgb.n_estimators):
        plt.figure(figsize=(12,8), dpi=150)  # 调整图像大小和分辨率
        plot_tree(xgb, tree_idx=idx, rankdir='LR', with_stats=True)  # 'LR' 横向展示
        plt.show()

    xgb.predict

if __name__ == '__main__':
    demo()

未经允许不得转载:一亩三分地 » XGBoost 优化思路、公式推导及案例演示
评论 (0)

9 + 3 =