目录
  1. 1. 1. 什么是提升树算法(Boosting Tree)
    1. 1.1. 1.1 加法模型与前向分步算法
    2. 1.2. 1.2 梯度提升 vs 传统提升
  2. 2. 2. 可扩展端到端提升树系统
    1. 2.1. 2.1 正则化目标函数的二阶泰勒展开
    2. 2.2. 2.2 叶子权重的闭式解
    3. 2.3. 2.3 节点分裂增益
    4. 2.4. 2.4 几种常见损失函数的梯度推导
  3. 3. 3. 分裂查找算法(Split Finding)
    1. 3.1. 3.1 精确贪心算法(Exact Greedy Algorithm)
    2. 3.2. 3.2 近似算法(Approximate Algorithm)
    3. 3.3. 3.3 加权分位点草图(Weighted Quantile Sketch)
    4. 3.4. 3.4 稀疏感知(Sparsity-aware)分裂
  4. 4. 4. 系统设计:列块并行化
    1. 4.1. 4.1 列块(Column Block)结构
    2. 4.2. 4.2 缓��感知的预取(Cache-aware Prefetch)
    3. 4.3. 4.3 核外计算(Out-of-Core Computation)
    4. 4.4. 4.4 并行化策略总结
  5. 5. 5. 超参数指南
    1. 5.1. 5.1 核心参数详解
    2. 5.2. 5.2 调参流程建议
    3. 5.3. 5.3 常见的参数组合(Cheat Sheet)
  6. 6. 6. XGBoost 在 Kaggle 中的主导地位
    1. 6.1. 6.1 Higgs Boson(HiggsML 挑战赛)
    2. 6.2. 6.2 Otto Group Product Classification
    3. 6.3. 6.3 为什么 XGBoost 在表格数据上难以被超越?
  7. 7. 7. 与其他 Boosting 库的对比
    1. 7.1. 7.1 LightGBM 的 Leaf-wise 生长策略
    2. 7.2. 7.2 CatBoost 的 Ordered Boosting
  8. 8. 8. 实现细节与伪代码
    1. 8.1. 8.1 XGBoost 训练主循环
    2. 8.2. 8.2 BuildTree 的递归伪代码
  9. 9. 9. 数学补充与推导
    1. 9.1. 9.1 结构分数的另一种推导视角
    2. 9.2. 9.2 为什么 $h_i$ 对平方损失恒为 1?
    3. 9.3. 9.3 DART (Dropouts meet Multiple Additive Regression Trees)
  10. 10. 10. 面试高频问答
【论文笔记】XGBoost: A Scalable Tree Boosting System

1. 什么是提升树算法(Boosting Tree)

提升树(Boosting Tree)是一类集成学习方法,其核心思想是:通过迭代地训练一系列弱学习器(通常是浅层决策树),每一棵树都试图纠正前一棵树的错误,最终将所有树的预测结果加权求和得到最终预测。

1.1 加法模型与前向分步算法

提升树模型可以表示为加法模型(Additive Model):

$$ \hat{y}_i = \sum_{k=1}^{K} f_k(x_i), \quad f_k \in \mathcal{F} $$

其中,$\mathcal{F}$ 是回归树空间(CART),$K$ 是树的数量。

训练目标是最小化正则化目标函数:

$$ \mathcal{L}(\phi) = \sum_{i=1}^{n} l(\hat{y}_i, y_i) + \sum_{k=1}^{K} \Omega(f_k) $$

这里的 $\Omega(f) = \gamma T + \frac{1}{2}\lambda ||w||^2$,其中 $T$ 是叶子节点数量,$w$ 是叶子权重向量。

前向分步算法(Forward Stagewise Additive Modeling)的核心在于:在第 $t$ 轮迭代时,保持前 $t-1$ 棵树不变,仅优化第 $t$ 棵树。此时的预测值为:

$$ \hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + f_t(x_i) $$

对应的目标函数变为:

$$ \mathcal{L}^{(t)} = \sum_{i=1}^{n} l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t) $$

1.2 梯度提升 vs 传统提升

传统的 AdaBoost 通过调整误分类样本的权重来引导后续学习器关注困难样本。而梯度提升(Gradient Boosting)则直接从函数空间梯度下降的角度出发:每一轮新加入的树 $f_t(x)$ 被训练去拟合当前模型在损失函数上的负梯度方向。

以平方损失 $l(y_i, \hat{y}_i) = \frac{1}{2}(y_i - \hat{y}_i)^2$ 为例,第 $t$ 轮的负梯度(伪残差)为:

$$ -\frac{\partial l(y_i, \hat{y}_i^{(t-1)})}{\partial \hat{y}_i^{(t-1)}} = y_i - \hat{y}_i^{(t-1)} $$

这正是上一轮的预测残差。新树 $f_t$ 的任务就是去拟合这些残差。

但对于一般损失函数(如 LogLoss),负梯度的表达式更为复杂,且添加新树后损失函数的变化无法直接由梯度完全刻画。这就引出了 XGBoost 的核心创新之一:二阶泰勒展开近似


2. 可扩展端到端提升树系统

陈天奇在 2016 年发表的 XGBoost 论文提出了一个端到端的可扩展提升树系统。相比传统的 GBM(Gradient Boosting Machine),XGBoost 在以下方面进行了深入优化:

2.1 正则化目标函数的二阶泰勒展开

XGBoost 使用损失函数的二阶泰勒展开来近似目标函数,这是其与 GBDT(Gradient Boosting Decision Tree)最本质的区别。

回顾第 $t$ 轮的目标函数:

$$ \mathcal{L}^{(t)} = \sum_{i=1}^{n} l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t) $$

在 $\hat{y}_i^{(t-1)}$ 处进行二阶泰勒展开:

$$ l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) \approx l(y_i, \hat{y}_i^{(t-1)}) + g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) $$

其中 $g_i = \partial_{\hat{y}^{(t-1)}} l(y_i, \hat{y}^{(t-1)})$ 是一阶梯度,$h_i = \partial^2_{\hat{y}^{(t-1)}} l(y_i, \hat{y}^{(t-1)})$ 是二阶梯度(Hessian)。

去掉常数项后,第 $t$ 轮的近似目标函数为:

$$ \tilde{\mathcal{L}}^{(t)} = \sum_{i=1}^{n} \left[ g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) \right] + \Omega(f_t) $$

为什么二阶展开优于仅用一阶梯度?

方面 一阶梯度(GBDT) 二阶梯度(XGBoost)
优化精度 仅用梯度方向 同时利用曲率信息
步长控制 需要单独学习率 Hessian 提供自适应步长
对 Hessian 的利用 可刻画样本”难度”
分裂增益计算 启发式 有闭式解

2.2 叶子权重的闭式解

将 $f_t(x)$ 展开为树结构表示:对于落在叶子 $j$ 上的样本集合 $I_j = {i \mid q(x_i) = j}$,有 $f_t(x_i) = w_{q(x_i)}$,其中 $q: \mathbb{R}^d \to {1, 2, …, T}$ 是树的结构函数。

带入 $\Omega(f_t) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2$,目标函数变为:

$$ \tilde{\mathcal{L}}^{(t)} = \sum_{j=1}^{T} \left[ \left(\sum_{i \in I_j} g_i\right) w_j + \frac{1}{2} \left(\sum_{i \in I_j} h_i + \lambda\right) w_j^2 \right] + \gamma T $$

这是一个关于 $w_j$ 的二次函数,可求得最优叶子权重 $w_j^*$:

$$ w_j^* = -\frac{\sum_{i \in I_j} g_i}{\sum_{i \in I_j} h_i + \lambda} $$

代入后的最小损失为:

$$ \tilde{\mathcal{L}}^{(t)*} = -\frac{1}{2} \sum_{j=1}^{T} \frac{(\sum_{i \in I_j} g_i)^2}{\sum_{i \in I_j} h_i + \lambda} + \gamma T $$

这个闭式解可以用来衡量一个树结构的”好坏”,也即 结构分数(Structure Score)

2.3 节点分裂增益

给定一个树结构,分裂一个叶子节点的增益定义为分裂前后的结构分数之差:

$$ \text{Gain} = \frac{1}{2} \left[ \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} \right] - \gamma $$

其中:

  • $G_L = \sum_{i \in I_L} g_i$, $H_L = \sum_{i \in I_L} h_i$(左子节点)
  • $G_R = \sum_{i \in I_R} g_i$, $H_R = \sum_{i \in I_R} h_i$(右子节点)
  • $\gamma$ 是分裂带来的复杂度惩罚

当 Gain 为正时分裂才有利,否则不分裂。$\gamma$ 可以看作分裂所需的最小损失减少量,起到了预剪枝的作用。

2.4 几种常见损失函数的梯度推导

平方损失(Squared Error)

$$ l(y, \hat{y}) = \frac{1}{2}(y - \hat{y})^2 $$

$$ g_i = \hat{y}_i - y_i, \quad h_i = 1 $$

逻辑损失(Logistic Loss)

$$ l(y, \hat{y}) = y \ln(1 + e^{-\hat{y}}) + (1-y) \ln(1 + e^{\hat{y}}) $$

令 $p_i = \frac{1}{1+e^{-\hat{y}_i}}$,则:

$$ g_i = p_i - y_i, \quad h_i = p_i (1 - p_i) $$


3. 分裂查找算法(Split Finding)

分裂查找是决策树训练中最耗时的步骤。XGBoost 提供了两种算法:精确贪心算法和近似算法。

3.1 精确贪心算法(Exact Greedy Algorithm)

对于连续特征,精确贪心算法按以下步骤进行:

Algorithm: Exact Greedy Split Finding
Input: I, instance set at current node
d, feature dimension

gain = 0
G = sum(g_i for i in I), H = sum(h_i for i in I)

for k = 1 to d: # 遍历每个特征
G_L = 0, H_L = 0
对特征 k 的所有样本值排序
for j in sorted(I, by x_{jk}): # 按特征值升序遍历
G_L += g_j, H_L += h_j
G_R = G - G_L, H_R = H - H_L
score = max(score, Gain(G_L, H_L, G_R, H_R))

该算法的时间复杂度为 $O(n \log n \times d)$(每个特征的排序开销),其中 $n$ 是样本数,$d$ 是特征数。当数据无法全部装入内存时,这种精确方法变得不可行。

3.2 近似算法(Approximate Algorithm)

为了处理大规模数据和分布式场景,XGBoost 提出了基于特征分位点(quantile)的近似算法。核心思想是:不检查所有可能的分裂点,而是根据特征值的分布提出一组候选分裂点(candidate splits),然后在这些候选点中搜索最佳分裂。

Algorithm: Approximate Split Finding
for k = 1 to d:
提出候选分裂点 S_k = {s_{k1}, s_{k2}, ..., s_{kl}} (通过分位点)
将样本按特征值映射到对应的桶(bucket)中

对所有特征的所有候选分裂点:
计算每个桶的 G 和 H 聚合值
在候选分裂点中搜索最佳分裂

近似算法可以在以下两种模式下工作:

  • Global:在树构建初期提出所有候选分裂点,整棵树使用同一组分位点
  • Local:每次分裂时重新提出候选分裂点

Global vs Local 对比

策略 候选点提出次数 内存开销 准确性
Global 1 次(构建开始) 需要更多候选点达到相同精度
Local 每次分裂时 中(但候选点少) 用较少候选点即可达到高精度

实验表明,Local 策略在候选点数量较少时更有优势,而 Global 策略在候选点足够多(eps 足够小)时可以达到与精确贪心算法相近的精度。

3.3 加权分位点草图(Weighted Quantile Sketch)

近似算法的关键在于如何提出候选分裂点。XGBoost 的一个重要创新是:以 Hessian(二阶梯度)为权重来构造加权分位点

为什么用 $h_i$ 作为权重?回顾目标函数:

$$ \tilde{\mathcal{L}}^{(t)} = \sum_{i=1}^{n} \frac{1}{2} h_i (f_t(x_i) + \frac{g_i}{h_i})^2 + \Omega(f_t) + \text{constant} $$

这可以重写为以 $h_i$ 为权重的加权平方损失形式。因此,在构造候选分裂点时,应以样本的 Hessian 为权重,确保”重要”样本(Hessian 大,即预测不确定性高)附近有更密集的候选分裂点。

加权分位点草图(Weighted Quantile Sketch)数据结构

XGBoost 实现了一个支持 merge 和 prune 操作的数据结构,用于高效地维护带权重的分位点摘要。该结构能在分布式环境下将多个分区的分位点摘要合并,实现全局排序。

核心操作:

  • merge:将两个加权分位点摘要合并为一个
  • prune:在一定精度 $\epsilon$ 下截断以控制摘要大小

算法通过维护满足以下条件的候选点序列 ${s_{k1}, s_{k2}, …, s_{kl}}$ 来近似分位点:

$$ | r_k(s_{k,j}) - r_k(s_{k,j+1}) | < \epsilon $$

其中 $r_k(s) = \frac{1}{\sum h_i} \sum_{x_{ik} < s} h_i$ 是特征 k 在值 s 处的加权排序函数。

3.4 稀疏感知(Sparsity-aware)分裂

现实世界数据中经常存在缺失值、零值过多、One-Hot 编码等稀疏情况。XGBoost 为每个节点添加了一个默认方向(default direction)来处理缺失值。

Algorithm: Sparsity-aware Split Finding
对每个特征 k:
将缺失 x_{ik} 的样本暂时不分入任何子节点
处理非缺失样本,按正常流程计算
对每个候选分裂点:
枚举两种默认方向(将所有缺失样本全部划入左/右子节点)
选择 Gain 更大的方案

这样做有两个好处:

  1. 统一处理各种稀疏性(缺失值、零值、One-Hot)
  2. 计算效率高:只需要对非缺失值进行排序,缺失值样本只需一次聚合计算

4. 系统设计:列块并行化

4.1 列块(Column Block)结构

这是 XGBoost 系统设计中最关键的优化。传统 GBDT 实现中,每次分裂都需要对每个特征重新排序,排序开销占总时间的很大一部分。

XGBoost 的策略是:在训练开始前,将所有数据预先按列(特征)排序,并压缩存储为内存单元(Column Block)

Column Block 结构示意:

特征 1: [sorted sample_ids by feat1] [sorted feat1 values] [corresponding g, h]
特征 2: [sorted sample_ids by feat2] [sorted feat2 values] [corresponding g, h]
...
特征 d: [sorted sample_ids by featd] [sorted featd values] [corresponding g, h]

每个 Column Block 中的数据按特征值排序,并以 CSC(Compressed Sparse Column)格式存储。这样在训练过程中,可以并行地处理多个列块,每个列块独立完成分裂查找。

列块 vs 行块对比

维度 列块(Column Block) 行块(Row Block)
适用场景 特征数 < 样本数 特征数 > 样本数
分裂查找 O(列内样本数),无需重复排序 需要每次排序
内存布局 列连续,利于特征遍历 行连续,利于梯度更新
并行粒度 按列并行 按行并行
XGBoost 默认 Yes No

4.2 缓��感知的预取(Cache-aware Prefetch)

虽然列块并行化显著提高了计算效率,但它引入了非连续内存访问问题。当通过排序后的 sample_id 索引去获取对应的梯度和 Hessian 值时,访问模式是不规则的(indirect index lookup),容易导致 CPU 缓存未命中(cache miss)。

XGBoost 的解决方案:

方案 1:缓存感知预取(Cache-aware Prefetch)

在每个线程内部维护一个缓冲区,将梯度统计量的累积操作延迟到缓冲满后进行。具体做法是:先按排序顺序预取对应的 g 和 h 到内部缓冲区(一次连续取出多个),然后在缓冲区内完成累加操作。

Pseudo-code for cache-aware prefetch:
for each feature block in parallel:
buffer_G = [], buffer_H = []
for each sample in sorted order (by feature value):
idx = sample_ids[j]
buffer_G.append(g[idx])
buffer_H.append(h[idx])
if len(buffer) == BUFFER_SIZE: # e.g., 256
# 批量累加,利用 CPU 向量化指令
accumulate(buffer_G, buffer_H)
buffer_G.clear(), buffer_H.clear()

方案 2:内存预取指令(Prefetch Intrinsics)

利用 CPU 的 _mm_prefetch 内在指令,在处理当前样本的同时提前将后续样本的 g, h 数据从主存加载到 CPU 缓存中。

4.3 核外计算(Out-of-Core Computation)

当数据量超过物理内存时,XGBoost 支持磁盘上的核外计算。核心策略包括:

块压缩(Block Compression):将列块按 2^16 行进行分组,每组独立压缩(使用通用压缩算法)。在读取时整组解压,实现了”部分加载、部分解压”的 IO 模式。

块分片(Block Sharding):当有多个磁盘时,将压缩数据分布到不同磁盘上,每个磁盘有一个独立的预取线程,将数据提前加载到内存缓冲区。

这两种技术配合,使得 XGBoost 可以在不牺牲太多速度的情况下处理远超物理内存的数据集。

4.4 并行化策略总结

XGBoost 的并行化在不同的层面展开:

层级 并行策略 说明
特征级 列块并行 多个特征同时进行分裂查找
数据级 数据分片 + AllReduce 分布式环境下,每台机器持有部分数据
树级 不支持 Boosting 的迭代依赖决定了树之间只能串行
节点级 同层节点间可并行 同一层的不同节点分裂可并行

5. 超参数指南

XGBoost 的超参数可以分为三类:通用参数、树参数和学习任务参数。

5.1 核心参数详解

通用参数(General Parameters)

参数 含义 典型值
booster 提升器类型 gbtree(默认), gblinear, dart
nthread 并行线程数 通常设为 CPU 核心数
verbosity 日志级别 0 (silent), 1 (warning), 2 (info), 3 (debug)

树参数(Tree Parameters)

参数 含义 建议范围 调参优先级
max_depth 树的最大深度 3-10,默认 6
min_child_weight 叶子节点最小 Hessian 和 1-10,默认 1 (控制过拟合)
gamma 分裂所需最小损失减少量 0-0.5,默认 0
subsample 行采样比例 0.5-1.0,默认 1.0
colsample_bytree 建树时的列采样比例 0.3-1.0,默认 1.0
colsample_bylevel 每层分裂时的列采样比例 0.3-1.0
colsample_bynode 每节点分裂时的列采样比例 0.3-1.0
lambda (reg_lambda) L2 正则化系数 0-10,默认 1
alpha (reg_alpha) L1 正则化系数 0-10,默认 0
eta (learning_rate) 学习率/收缩率 0.01-0.3,默认 0.3

学习任务参数

参数 含义 典型值
objective 损失函数 reg:squarederror, binary:logistic, multi:softmax
eval_metric 评估指标 rmse, logloss, auc, merror
seed 随机种子 任意整数

5.2 调参流程建议

# 调参的标准流程(由粗到细)

# Step 1: 确定学习率和估计器数量
# 使用较高的学习率 + 较多的树,配合早停
params = {
'max_depth': 6,
'eta': 0.1,
'subsample': 0.8,
'colsample_bytree': 0.8,
'eval_metric': 'logloss'
}
# 使用 xgb.cv() 确定最佳的 num_boost_round
cv_result = xgb.cv(params, dtrain, num_boost_round=1000,
nfold=5, early_stopping_rounds=50)

# Step 2: 调节 max_depth 和 min_child_weight
# 网格搜索 (max_depth, min_child_weight) 组合
for depth in [3, 5, 7, 9]:
for child_weight in [1, 3, 5, 7]:
# ... cross-validation

# Step 3: 调节 gamma(分裂阈值)
for gamma in [0, 0.1, 0.2, 0.3, 0.5]:
# ... cross-validation

# Step 4: 调节 subsample 和 colsample_bytree
for subsample in [0.6, 0.7, 0.8, 0.9, 1.0]:
for colsample in [0.6, 0.7, 0.8, 0.9, 1.0]:
# ... cross-validation

# Step 5: 调节正则化参数
for reg_lambda in [0, 0.1, 1, 5, 10]:
for reg_alpha in [0, 0.1, 1, 5, 10]:
# ... cross-validation

# Step 6: 降低学习率,增加树的数量
params['eta'] = 0.01
# num_boost_round 相应增大 5-10 倍

5.3 常见的参数组合(Cheat Sheet)

快速 benchmark(追求速度)

{'max_depth': 4, 'eta': 0.3, 'subsample': 0.8, 
'colsample_bytree': 0.5, 'nthread': -1}

高精度(追求精度)

{'max_depth': 8, 'eta': 0.01, 'subsample': 0.9,
'colsample_bytree': 0.8, 'min_child_weight': 3,
'gamma': 0.1, 'reg_lambda': 1}

防止过拟合

{'max_depth': 3, 'eta': 0.1, 'subsample': 0.6,
'colsample_bytree': 0.5, 'min_child_weight': 10,
'gamma': 0.3, 'reg_lambda': 5, 'reg_alpha': 1}

处理大规模稀疏数据

{'max_depth': 6, 'eta': 0.1, 'subsample': 0.8,
'colsample_bytree': 0.8, 'tree_method': 'hist', # 使用直方图加速
'grow_policy': 'lossguide', # 按损失引导生长
'max_leaves': 256}

6. XGBoost 在 Kaggle 中的主导地位

2015-2017 年间,Kaggle 竞赛获奖方案中 XGBoost 的使用率超过 50%。几个典型案例:

6.1 Higgs Boson(HiggsML 挑战赛)

这是一个二分类问题(信号/背景),数据量约 25 万行,30 个特征。XGBoost 拿到该竞赛的第一名,关键配置:

  • 使用 AMS(Approximate Median Significance)作为自定义评估指标
  • 多类参数组合平均(ensemble of XGBoost with different params)

6.2 Otto Group Product Classification

九分类问题,约 6.2 万样本,93 个特征。获胜方案:

  • 多通道 XGBoost 模型(不同深度、列采样)做 stacking
  • 二阶特征交互信息

6.3 为什么 XGBoost 在表格数据上难以被超越?

因素 XGBoost 深度学习(MLP/TabNet)
训练速度 快(分钟级) 慢(需 GPU)
不需要大量归一化 Yes No(需要标准化)
对缺失值鲁棒 内建支持 需要预处理
可解释性 高(特征重要性)
对小数据集 极好 通常过拟合
超参数调优成本 低-中
在大规模表格数据上 极好(GPU Hist 版本) 一般

7. 与其他 Boosting 库的对比

特性 XGBoost LightGBM CatBoost
树生长策略 Level-wise Leaf-wise Symmetric (Oblivious)
分裂算法 预排序 + Hist Hist (GOSS + EFB) Ordered Boosting
类别特征 需自行编码 内建支持 最优类别特征处理
GPU 支持 优秀 优秀 优秀
内存消耗 较高(预排序) 较低 中等
默认超参数 一般 较好 较好
速度(CPU) 更快(小数据) 慢(小数据)
速度(GPU) 非常快 非常快

7.1 LightGBM 的 Leaf-wise 生长策略

与 XGBoost 的 Level-wise(按层生长)不同,LightGBM 采用 Leaf-wise(按叶生长):每轮选择当前所有叶子中分裂增益最大的那个进行分裂。这通常可以更快地降低损失,但也更容易导致过拟合(尤其在数据量小时)。

7.2 CatBoost 的 Ordered Boosting

CatBoost 的核心创新是 Ordered Boosting,它通过对每个样本使用”不含该样本的历史数据”来训练模型,以消除传统 boosting 中的预测偏移(prediction shift)问题。CatBoost 对类别特征的处理也是三者中最优的(使用 Ordered Target Statistics)。


8. 实现细节与伪代码

8.1 XGBoost 训练主循环

Algorithm: XGBoost Training
Input: D = {(x_i, y_i)}_{i=1}^n, 参数 param

Initialize: y_pred_i = base_score (usually 0.5 for classification)
for t = 1 to num_boost_round:
// Step 1: 计算梯度和 Hessian
for i = 1 to n:
g_i = gradient(loss_fn, y_pred_i, y_i)
h_i = hessian(loss_fn, y_pred_i, y_i)

// Step 2: 构建第 t 棵树
tree_t = BuildTree(D, g, h, param)

// Step 3: 更新预测
for i = 1 to n:
y_pred_i += eta * tree_t.predict(x_i)

// Step 4: 评估验证集(如果有)
if validation_set:
score = evaluate(validation_set, y_pred)
if early_stopping(score):
break

8.2 BuildTree 的递归伪代码

Function BuildTree(node, instances I, G_parent, H_parent):
// 检查终止条件
if depth >= max_depth or |I| < min_child_weight:
return Leaf(w = -G_parent / (H_parent + lambda))

// 寻找最佳分裂
best_gain = 0
best_split = None

for feature k in sampled_features:
G_L, H_L = 0, 0
for instance i in sorted(I, by x_{ik}):
G_L += g_i, H_L += h_i
G_R = G_parent - G_L, H_R = H_parent - H_L
gain = gain_formula(G_L, H_L, G_R, H_R) - gamma
if gain > best_gain:
best_gain = gain
best_split = (k, threshold)

if best_gain <= 0:
return Leaf(w = -G_parent / (H_parent + lambda))

// 递归构建子节点
I_L, I_R = split(I, best_split)
left = BuildTree(node.left, I_L, G_L, H_L)
right = BuildTree(node.right, I_R, G_R, H_R)
return InternalNode(best_split, left, right)

9. 数学补充与推导

9.1 结构分数的另一种推导视角

从信息论角度看,结构分数可以理解为”最大对数后验的近似”。对于每个叶子节点 $j$:

  1. 假设叶子内所有样本共享相同的预测值 $w_j$
  2. 损失函数是 $l(y, w_j)$ 的样本求和
  3. 在 $w_j$ 上加上 L2 正则化

在 $w_j$ 上的最大后验(MAP)估计等价于最小化:

$$ \min_{w_j} \sum_{i \in I_j} l(y_i, w_j) + \frac{1}{2}\lambda w_j^2 $$

用牛顿法做 1 步迭代(二阶展开 + 令导数为 0),恰好得到:

$$ w_j^* = -\frac{\sum g_i}{\sum h_i + \lambda} $$

9.2 为什么 $h_i$ 对平方损失恒为 1?

对于平方损失 $l(y, \hat{y}) = \frac{1}{2}(y - \hat{y})^2$:

一阶导数:$\frac{\partial l}{\partial \hat{y}} = \hat{y} - y \equiv g_i$
二阶导数:$\frac{\partial^2 l}{\partial \hat{y}^2} = 1 \equiv h_i$

$h_i = 1$ 恒成立意味着:当使用平方损失时,所有样本的权重相同,XGBoost 退化为仅用一阶梯度的 GBDT。这也解释了为什么 XGBoost 的分类问题(使用 LogLoss)比回归问题(使用平方损失)更具优势——因为分类问题的 Hessian $p(1-p)$ 携带了额外的曲率信息。

9.3 DART (Dropouts meet Multiple Additive Regression Trees)

除了标准的 GBDT 提升器,XGBoost 还实现了 DART 提升器。DART 借鉴了深度学习中 Dropout 的思想:在每轮迭代中,随机丢弃一部分已有的树,然后训练新树来弥补被丢弃树的贡献。

DART 的更新规则:

$$ \hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} - \eta \cdot k \cdot \sum_{m \in \text{dropped}} f_m(x_i) + \eta \cdot f_t(x_i) $$

其中 k 是一个归一化因子,用于补偿被丢弃树的影响。

DART 的优势:

  • 缓解了 GBDT 中先期树对最终预测贡献过大的问题(即树的”重要性”过度集中在早期迭代中)
  • 在部分任务上能得到更好的泛化能力

10. 面试高频问答

Q1: XGBoost 与 GBDT 的核心区别是什么?

XGBoost 使用损失函数的二阶泰勒展开,引入了 Hessian 信息,使得每一步的目标函数可以写成关于叶子权重的显式二次形式,从而求出叶子权重的闭式解。而 GBDT 仅使用一阶梯度(负梯度)来拟合残差,每棵树的学习率(shrinkage)是手动指定的。此外,XGBoost 在目标函数中显式加入了正则化项 $\gamma T + \frac{1}{2}\lambda ||w||^2$,而 GBDT 通常没有。

Q2: 为什么 XGBoost 使用 Hessian 作为加权分位点的权重?

通过重写目标函数可以发现,XGBoost 的优化目标实际上等价于一个以 $h_i$ 为权重的加权平方损失问题。以 $h_i$ 为权重意味着那些预测不确定性高的样本(Hessian 大)在构造候选分裂点时被赋予更大的关注,确保这些”困难样本”附近有更密集的分裂候选点。从优化角度看,这近似于在 Hessian 加权空间中做等频分桶。

Q3: 列块(Column Block)并行化的原理和局限性?

在训练开始前,XGBoost 将每个特征的数据预先排序并存储为独立的列块。在分裂查找时,多个线程可以同时处理不同的列块,因为每个列块内部的排序顺序是独立的。局限性在于:通过排序后的 sample_id 索引获取梯度和 Hessian 值时,会产生不连续的内存访问(scatter-gather),在缓存不友好时性能下降。这使得 XGBoost 在特征数量较少但样本量极大的场景下优于 LightGBM,而在高维特征场景下可能不如 LightGBM。

Q4: XGBoost 如何处理缺失值?

XGBoost 为每个树节点维护一个默认方向(default direction)。当一个样本在某个特征上缺失时,它会被分到默认方向。默认方向是通过数据驱动的:在训练时对于每个分裂点,尝试将缺失值分配到左子节点和右子节点,选择增益更大的方案作为该节点的默认方向。这种方法在一个统一的框架下处理了所有类型的稀疏性。

Q5: 解释 XGBoost 的 Gain 公式中每一项的含义。

Gain 公式为:

$$ \text{Gain} = \frac{1}{2}\left[ \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} \right] - \gamma $$

  • 第一项 $\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$:分裂的复杂度惩罚,只有净收益超过 $\gamma$ 时才分裂
  • 因子 $\frac{1}{2}$:来自目标函数二阶展开中的系数
文章作者: Leo·Cheung
文章链接: http://tufusi.com/2021/10/15/%E3%80%90%E8%AE%BA%E6%96%87%E7%AC%94%E8%AE%B0%E3%80%91XGBoost-A-Scalable-Tree-Boosting-System/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ONE·PIECE
打赏
  • 微信
  • 支付宝

评论