观文听傑

返回

上一篇把梯度提升树写成逐步加法模型:每一轮计算损失对当前预测的负梯度,再训练一棵浅树去逼近它。这样回答了“下一步往哪里修正”,却留下三个工程问题:

  1. 同样大小的一阶梯度,处在平坦区域和陡峭区域时,修正幅度应该一样吗?
  2. 一片叶子最终该输出多大,能否由同一个目标直接算出?
  3. 候选分裂虽然降低训练损失,但收益很小、叶子证据很少时,怎样让它不要发生?

XGBoost(Extreme Gradient Boosting,极端梯度提升)用损失的二阶近似和显式树复杂度惩罚,把“叶子输出多少”和“这刀值不值得切”统一进一个可计算目标。本文只讲透这条推导,再把公式逐项映射到当前 Python API。

01 一阶方向为什么还不够?#

设当前模型输出为 Ft1(xi)F_{t-1}(x_i),第 tt 棵树的输出为 ft(xi)f_t(x_i)

Ft(xi)=Ft1(xi)+ft(xi)F_t(x_i)=F_{t-1}(x_i)+f_t(x_i)

普通梯度下降只看当前斜率。用地形类比:

损失
 ▲              陡峭窄谷:斜率变化快
 │       \     /
 │        \___/

 │  \____________  平缓长坡:斜率变化慢
 └────────────────────────────► 模型输出 F
text

二阶导数描述斜率本身变化多快,也就是局部曲率(Curvature)。若曲率大,相同的一阶梯度通常对应更保守的步长;若曲率小,则可以允许更大修正。XGBoost 用牛顿法(Newton Method)式的二阶信息决定叶值。

这并不意味着它为每个神经网络参数构造巨大 Hessian 矩阵。对常见逐样本可加损失,只需为每个样本保存两个标量:

gi=L(yi,F)Fg_i=\frac{\partial L(y_i,F)}{\partial F} hi=2L(yi,F)F2h_i=\frac{\partial^2 L(y_i,F)}{\partial F^2}

一批 NN 个样本中,gradienthessian 都是形状 [N] 的向量。

02 用二阶泰勒展开近似新一轮目标#

tt 轮希望选择一棵树 ftf_t,最小化:

L(t)=i=1NL(yi,Ft1(xi)+ft(xi))+Ω(ft)\mathcal{L}^{(t)} =\sum_{i=1}^{N}L\left(y_i,F_{t-1}(x_i)+f_t(x_i)\right) +\Omega(f_t)

在当前预测 Ft1(xi)F_{t-1}(x_i) 附近,对新树输出做二阶泰勒展开:

L(yi,Ft1+ft)L(yi,Ft1)+gift(xi)+12hift2(xi)L(y_i,F_{t-1}+f_t) \approx L(y_i,F_{t-1})+g_i f_t(x_i)+\frac12h_i f_t^2(x_i)

第一项在本轮固定,去掉后得到需要优化的近似目标:

L~(t)=i=1N[gift(xi)+12hift2(xi)]+Ω(ft)\widetilde{\mathcal{L}}^{(t)} =\sum_{i=1}^{N} \left[g_i f_t(x_i)+\frac12h_i f_t^2(x_i)\right] +\Omega(f_t)
  • gig_i:样本 ii 希望预测向哪个方向变化;
  • hih_i:该位置损失曲率,对修正大小提供尺度;
  • ft(xi)f_t(x_i):新树对样本 ii 的叶输出;
  • Ω(ft)\Omega(f_t):树结构与叶权重的复杂度成本。

03 树结构怎样进入公式?#

假设第 tt 棵树有 TT 个叶子,q(xi)=jq(x_i)=j 表示样本 ii 被路由到叶子 jj,该叶输出常数 wjw_j

ft(xi)=wq(xi)f_t(x_i)=w_{q(x_i)}

XGBoost 的经典复杂度项写成:

Ω(ft)=γT+12λj=1Twj2\Omega(f_t)=\gamma T+\frac12\lambda\sum_{j=1}^{T}w_j^2
  • γ0\gamma\ge0:每增加一个叶子的结构成本;Python 参数常写成 gamma,别名是 min_split_loss
  • λ0\lambda\ge0:叶权重的 L2 正则化;Python 参数是 reg_lambda
  • TT:叶子数量;
  • wjw_j:第 jj 片叶子的输出,不是普通输入特征的线性系数。

把落入叶子 jj 的样本集合记为 IjI_j,聚合一阶和二阶统计量:

Gj=iIjgi,qquadHj=iIjhiG_j=\sum_{i\in I_j}g_i,qquad H_j=\sum_{i\in I_j}h_i

整棵树的近似目标可以按叶子拆开:

L~(t)=j=1T[Gjwj+12(Hj+λ)wj2]+γT\widetilde{\mathcal{L}}^{(t)} =\sum_{j=1}^{T} \left[G_jw_j+\frac12(H_j+\lambda)w_j^2\right] +\gamma T

到这里,树结构一旦固定,每个叶子的最优输出都能独立求解。

04 叶权重为什么是负梯度除以曲率?#

对某一叶子的 wjw_j 求导并令其为 0:

L~jwj=Gj+(Hj+λ)wj=0\frac{\partial\widetilde{\mathcal{L}}_j}{\partial w_j} =G_j+(H_j+\lambda)w_j=0

因此:

wj=GjHj+λw_j^*=-\frac{G_j}{H_j+\lambda}

这个式子同时编码方向、尺度和正则化:

  • Gj>0G_j>0 时,叶值为负,降低这一组样本的当前预测;
  • Gj<0G_j<0 时,叶值为正,提高预测;
  • HjH_j 大表示局部曲率或总证据更大,分母增大;
  • reg_lambda 增大时,叶值连续向 0 收缩。

若再乘学习率 η\eta,实际加入模型的是 ηwj\eta w_j^*

Ft(x)=Ft1(x)+ηwq(x)F_t(x)=F_{t-1}(x)+\eta w_{q(x)}^*

05 用四个样本手算叶值#

沿用上一篇的数据:

x:                   [1, 2, 3, 4]
y:                   [3, 5, 4, 8]
current prediction:  [5, 5, 5, 5]
text

使用半平方误差:

L(y,F)=12(yF)2L(y,F)=\frac12(y-F)^2

则:

gi=Fiyi,qquadhi=1g_i=F_i-y_i,qquad h_i=1

所以:

g: [ 2, 0, 1,-3]
h: [ 1, 1, 1, 1]
text

考虑候选分裂 x2x\le2,并设 λ=1\lambda=1

左叶 I_L = {1,2}:  G_L = 2,  H_L = 2
右叶 I_R = {3,4}:  G_R = -2, H_R = 2
text

最优叶值为:

wL=22+1=23w_L^*=-\frac{2}{2+1}=-\frac23 wR=22+1=23w_R^*=-\frac{-2}{2+1}=\frac23

若学习率 η=0.3\eta=0.3,本轮实际修正:

η f_t(X):       [-0.2,-0.2, 0.2, 0.2]
new prediction: [ 4.8, 4.8, 5.2, 5.2]
text

左叶整体预测偏高,所以向下修;右叶总体偏低,所以向上修。reg_lambda=0 时叶值会是 [-1,1],说明 L2 正则化把修正幅度从 1 收缩到了 2/32/3

交互手算:把 reg_lambda 从 1 改成 3

左右叶值变成 2/(2+3)=0.4-2/(2+3)=-0.42/(2+3)=0.42/(2+3)=0.4。若 η=0.3,实际只修正 ±0.12。训练更保守,但需要更多轮才能达到相近拟合程度。

06 一刀分裂的收益怎样计算?#

把最优叶值代回目标,固定树结构的最优分数为:

Score=12j=1TGj2Hj+λ+γT\operatorname{Score} =-\frac12\sum_{j=1}^{T}\frac{G_j^2}{H_j+\lambda}+\gamma T

把父叶分成左、右两片后,目标下降量——也就是分裂增益(Split Gain)——为:

Gain=12[GL2HL+λ+GR2HR+λ(GL+GR)2HL+HR+λ]γ\operatorname{Gain} =\frac12\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

继续计算 x2x\le2。父节点 G=0,H=4G=0,H=4,设 λ=1,γ=0.2\lambda=1,\gamma=0.2

Gain=12(222+1+(2)22+10)0.21.133\operatorname{Gain} =\frac12\left(\frac{2^2}{2+1}+\frac{(-2)^2}{2+1}-0\right)-0.2 \approx1.133

增益为正,这刀值得切。

再看 x3x\le3

左叶:G_L = 2 + 0 + 1 = 3, H_L = 3
右叶:G_R = -3,            H_R = 1
text
Gain=12(94+92)0.2=3.175\operatorname{Gain} =\frac12\left(\frac{9}{4}+\frac{9}{2}\right)-0.2 =3.175

仅看该公式,x<=3 的增益更大。但右叶只有 HR=1H_R=1。如果 min_child_weight=2,这刀会被拒绝,因为新子节点的 Hessian 总和不足。

候选阈值

   ├── 任一子叶 H < min_child_weight ─► 拒绝

   └── 两边证据足够

          ├── Gain <= gamma ─► 拒绝
          └── Gain > gamma  ─► 接受
text

gamma 问“收益是否足以支付新增叶子的结构成本”,min_child_weight 问“每个子叶是否有足够二阶证据”。二者不是同一个旋钮。

07 为什么 min_child_weight 不是固定样本数?#

平方误差中 hi=1h_i=1,所以:

Hj=iIj1=IjH_j=\sum_{i\in I_j}1=|I_j|

此时 min_child_weight 数值上等于最小叶样本数。但对二分类对数损失,若模型输出 logit FiF_i、概率 pi=σ(Fi)p_i=\sigma(F_i)

gi=piyig_i=p_i-y_i hi=pi(1pi)h_i=p_i(1-p_i)

pi=0.5p_i=0.5 时,hi=0.25h_i=0.25;当模型非常自信,如 pi=0.99p_i=0.99 时,hi=0.0099h_i=0.0099。因此 100 个样本的 Hessian 总和不一定是 100,也不一定比 20 个不确定样本更大。

当前概率 pp单样本 Hessian p(1p)p(1-p)局部含义
0.500.2500曲率最大,决策边界附近
0.900.0900已较自信
0.990.0099极度自信,曲率很小

所以在分类任务中把 min_child_weight=10 解读为“每叶至少 10 个样本”是错误的。应结合目标函数、样本权重和实际叶统计量理解。

08 从公式到一轮树生长伪代码#

输入矩阵仍是 [N,D],但建树时不再反复携带原始标签;核心统计变成每行的 (g_i,h_i),候选箱只需聚合 (G,H)

09 用 NumPy验证公式#

下面代码计算本文两个候选阈值的正则化增益:

预期结果约为:

2.5  1.133  -0.667   0.667
3.5  3.175  -0.750   1.500
text

若手算与代码不一致,最常见原因是把梯度写成 y - margin、遗漏公式中的 1/21/2,或在增益中多减/少减一次 gamma。只要梯度符号约定一致也能实现算法,但叶值公式必须同步改变;不要混用两套符号。

10 用当前 XGBoost Python API 落地#

当前 XGBoost 3.3 的 scikit-learn 接口把提升轮数写成 n_estimators,直方图建树用 tree_method='hist'。早停参数放在估计器构造中,验证数据通过 fit(..., eval_set=...) 提供:

当前 scikit-learn 包装器在启用早停后,默认 predict 会使用最佳迭代范围。若显式检查某段树,应使用半开区间:

best_prediction = model.predict(
    X_test,
    iteration_range=(0, model.best_iteration + 1),
)

history = model.evals_result()
validation_rmse = np.asarray(history['validation_0']['rmse'])
python

几个参数与公式的对应关系:

Python 参数公式或数据流作用
learning_rateη\eta缩小整棵新树的输出
reg_lambdaλ\lambdaL2 收缩叶值并降低分裂收益
reg_alpha叶值 L1 惩罚软阈值化聚合梯度,使叶值更稀疏
gammaγ\gamma要求分裂提供最低损失下降
min_child_weight子叶 HH 下限阻止二阶证据不足的叶子
subsample行采样降低方差,也减少每轮数据
colsample_bytree列采样降低树间相关性和计算量
max_depth树结构上限控制单轮交互复杂度与内存

tree_method='hist' 先对特征分箱并聚合梯度直方图,通常是当前通用起点;GPU 训练应显式评估 device='cuda' 与输入所在设备,避免 CPU 数据隐式转换成为瓶颈。

11 早停、测试集与模型保存不要混在一起#

训练集 ─► 学习树结构、叶值
验证集 ─► 监控 eval_metric、确定 best_iteration
测试集 ─► 全部选择冻结后的最终一次评估
text

eval_set 放入测试集,早停轮数就已经对测试数据发生了拟合。模型即使没有直接用测试标签计算树梯度,测试指标也参与了方案选择。

长期保存应使用 XGBoost 的稳定模型表示:

model.save_model('house-price-xgb.ubj')
# 或使用可读 JSON
model.save_model('house-price-xgb.json')
python

JSON/UBJSON 保存树与目标等模型状态;picklejoblib 更接近版本相关的内存快照,不适合作为跨版本长期归档。部署时还要单独版本化特征顺序、类别编码、缺失值语义和全部训练配置。

12 工程上为什么“极端”不只是一条公式?#

XGBoost 的目标推导解释了统计量怎样计算,实际可扩展性还来自系统设计:

  1. 直方图聚合。 把连续特征压到有限箱,在每箱累加 G,HG,H,减少候选阈值和内存访问。
  2. 稀疏与缺失路由。 每个分裂学习缺失值默认方向;推理必须复用同一规则。
  3. 行列采样。 subsamplecolsample_* 同时影响统计精度、相关性与速度。
  4. 缓存与并行。 同层候选统计可并行,但提升轮次仍存在顺序依赖。
  5. 外存与分布式接口。 数据大于单机内存时可以换数据结构和执行后端,但数据划分与泄漏原则不变。

这些优化让建树更快,不会自动修复错误标签、时间穿越、离线线上特征不一致或错误评价指标。

13 常见错误与最短调试路径#

  1. gig_i 当负梯度却仍使用 w=G/(H+λ)w=-G/(H+\lambda) 本文的 gig_i 定义为正的一阶导数;若改成负梯度,叶值符号也要同步调整。
  2. min_child_weight 当最小样本数。 只有平方误差且样本权重为 1 时二者数值相同;分类中它约束 Hessian 总和。
  3. 认为 gamma 会直接缩小已有叶值。 gamma 主要收取新增叶子的结构成本;reg_lambda 才进入叶值分母。
  4. 高学习率配深树。 二阶叶值也可能过拟合;牛顿步不是自动安全步,仍需收缩、结构限制和验证。
  5. 对时间数据随机早停。 eval_set 必须模拟未来部署方向,不能让未来样本影响最佳轮数。
  6. 类别标签或特征顺序漂移。 训练列 [age,income] 在服务中变成 [income,age] 仍能输出数字,却完全错误;保持特征名验证。
  7. 只看内置 importance。 weightgaincover 含义不同;相关特征和高基数仍会误导,应在验证集补充置换或局部分析。
  8. 交叉验证与模型内部都开满线程。 外层并行乘内部 n_jobs=-1 会导致 CPU 和内存争用。
  9. 直接 pickle 长期归档。 保存稳定模型格式,并记录库版本、参数、特征 schema 与最佳轮数。
  10. 错误理解不平衡参数。 scale_pos_weight 改变梯度统计和优化重点,不等同于调预测阈值;验证概率、排序和最终决策要分开。

最小诊断代码:

assert X_train.ndim == 2 and y_train.ndim == 1
assert X_train.shape[0] == y_train.shape[0]
assert model.n_features_in_ == X_train.shape[1]
assert model.best_iteration < model.n_estimators
assert np.isfinite(prediction).all()

booster = model.get_booster()
print('boosted rounds:', booster.num_boosted_rounds())
print('feature names:', booster.feature_names)
python

若训练和验证指标都不动,检查目标/指标是否匹配、标签是否近似常数、min_child_weightgamma 是否过大;若训练迅速变好而验证恶化,依次减小深度、降低学习率、增大叶子约束并检查泄漏,不要只增加 reg_lambda 期待包治百病。

14 失败场景与相近方法边界#

方法使用的局部信息树的评分与约束典型定位
经典 GBDT一阶负梯度依实现的叶值与树约束教学清晰、小中型数据
XGBoost一阶 + 二阶统计叶权重、结构成本、采样统一强表格基线与可扩展训练
sklearn HistGBDT一阶/二阶损失实现直方图、L2、早停与约束sklearn 原生中大型表格
随机森林不显式使用损失梯度Bootstrap + 特征子采样稳健、少调参、易并行
线性/广义线性模型参数梯度或闭式结构参数正则化高维稀疏、解释与外推

XGBoost 仍有明确局限:树模型对训练范围外的连续趋势不擅长平滑外推;大量高基数类别若编码不当会制造碎片;图像、文本和语音的原始表示通常需要专门表征学习;高度漂移的数据会让任何离线分裂增益迅速过期;极低延迟设备也可能无法承受数千棵树的模型体积与分支访问。

15 今天真正需要记住什么?#

  1. XGBoost 用每个样本的一阶梯度 gig_i 表示修正方向,用二阶导数 hih_i 表示局部曲率。
  2. 固定树结构后,叶子的最优输出是 G/(H+λ)-G/(H+\lambda)reg_lambda 通过分母收缩叶值。
  3. 分裂增益比较左右叶与父叶的正则化得分,再减去 gamma 的结构成本。
  4. min_child_weight 限制子叶 Hessian 总和,不普遍等于最小样本数。
  5. 二阶优化不会替代数据划分、早停和部署验证;学习率、深度、采样、正则化仍需联合选择。

16 思考题与小练习#

练习 1:比较两个正则化强度

对候选分裂 x<=2,分别令 reg_lambda=0reg_lambda=4,计算左右叶值和增益(保持 gamma=0.2)。解释为什么 reg_lambda 同时缩小叶输出和候选分裂收益。

练习 2:计算分类 Hessian 覆盖

一片候选叶有 20 个样本,其中 10 个的预测概率为 0.5,另 10 个为 0.99。忽略样本权重,计算 H=pi(1pi)H=\sum p_i(1-p_i)。若 min_child_weight=3,该叶是否满足约束?

练习 3:做一次参数消融

固定数据划分和随机种子,单独改变 max_depthmin_child_weightgammareg_lambda。同时记录最佳轮数、训练/验证差距、叶数、模型大小与 P99 延迟,说明哪些参数主要改变结构,哪些主要改变叶值。

相关工作#

17 下一篇预告#

树模型通过不断切分坐标轴构造非线性边界。下一篇将回到另一类经典几何方法:支持向量机为什么不只找一条能分开的直线,而要最大化离最近样本的间隔,并如何用核技巧得到弯曲边界。

XGBoost 为何要看二阶梯度?从叶权重到正则化分裂增益
https://zwjcode.cn/blog/xgboost-second-order-regularized-split
作者
发布于 2026年8月23日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。