2.3 树的结构打分与叶子权重的闭式解 给定树的形状,最优叶子权重有闭式解:权重 = −G / (H + λ),整棵树的结构分数 = −0.5·G²/(H+λ) 之和再减 γT。 其中 G、H 是落到该叶子的样本 g、h 之和。本节完成这两步推导,手算一个五样本例子,并解释 λ 为什么天然防止个别样本绑架叶子。 上一节把每个样本压缩成 (gi, hi)。本节回答两个连环问题:样本按树形分进叶子后,每个叶子该输出多少?以及,怎么给整棵树打一个"好坏分"? 一、从逐样本到逐叶子:归并 g 与 h 设某叶子落进了若干样本,把它们的 g 相加记为 G,h 相加记为 H。把上一节的二阶近似代入目标函数,逐样本求和可以重组为逐叶子求和:每个叶子贡献 G·w + 0.
给定树的形状,最优叶子权重有闭式解:权重 = −G / (H + λ),整棵树的结构分数 = −0.5·G²/(H+λ) 之和再减 γT。 其中 G、H 是落到该叶子的样本 g、h 之和。本节完成这两步推导,手算一个五样本例子,并解释 λ 为什么天然防止个别样本绑架叶子。
上一节把每个样本压缩成 (g_i, h_i)。本节回答两个连环问题:样本按树形分进叶子后,每个叶子该输出多少?以及,怎么给整棵树打一个"好坏分"?
设某叶子落进了若干样本,把它们的 g 相加记为 G,h 相加记为 H。把上一节的二阶近似代入目标函数,逐样本求和可以重组为逐叶子求和:每个叶子贡献 G·w + 0.5·(H+λ)·w²,其中 w 是该叶子权重(正则项的 λ/2·w² 已并入)。这是关于 w 的一元二次函数,配方可直接读出最小值:w = −G / (H + λ)*,代回得每叶子的最小贡献 −0.5·G²/(H+λ),再汇总减去 γT 就是结构分数 obj。obj 越小树越好。
这个结果的漂亮之处在于"最优权重不需要迭代搜索"——树形一定,权重一步到位。GBDT 时代叶子输出靠损失函数对应的均值或数值优化,XGBoost 把它变成了一个除法。
import numpy as np # 承接 2.1 的五样本任务:假设第 1 棵树按客流 15 分裂成两个叶子 g = np.array([-2.18, -1.48, -0.08, 1.62, 2.12]) # 平方损失下 g = 预测 - 真实 h = np.ones(5) # 平方损失下 h 恒为 1 lam, gamma = 1.0, 0.5 leaf1_idx = [0, 1, 2] # 客流 <= 15 leaf2_idx = [3, 4] # 客流 > 15 def leaf(G, H): w = -G / (H + lam) score = -0.5 * G**2 / (H + lam) return w, score G1, H1 = g[leaf1_idx].sum(), h[leaf1_idx].sum() G2, H2 = g[leaf2_idx].sum(), h[leaf2_idx].sum() w1, s1 = leaf(G1, H1) w2, s2 = leaf(G2, H2) print(f"叶子1: G={G1:+.2f} H={H1:.0f} -> 权重={w1:+.3f} 贡献={s1:+.3f}") print(f"叶子2: G={G2:+.2f} H={H2:.0f} -> 权重={w2:+.3f} 贡献={s2:+.3f}") obj = s1 + s2 - gamma * 2 print(f"整树结构分数 obj = {obj:.3f}")
运行输出:
叶子1: G=-3.74 H=3 -> 权重=+0.935 贡献=-1.167 叶子2: G=+3.74 H=3 -> 权重=-0.935 贡献=-1.167 整树结构分数 obj = -2.084
符号读出来就是业务:叶子 1 的样本 g 全负(预测普遍高于真实),最优权重为正去抬高预测;叶子 2 相反。两个叶子贡献对称,是因为这组数据残差恰好对称。obj = −2.08 是这棵树的"成绩单",任何另一棵树形与它比 obj,谁小谁好——树的搜索空间因此有了全序可比的标尺。

权重公式 w* = −G/(H+λ) 里,λ 加在分母上,效果是把所有叶子权重向零收缩。极端情形最能说明问题:某叶子只落进一个 g 很大的离群样本,若无 λ,权重 = −G/H 可能是一个巨大数值,整棵树被这个样本绑架;有了 λ,权重上限被压住,离群点的影响被稀释。λ 越大,模型输出越保守——这就是第 3 章 reg_lambda 参数的数学出身。
# 演示 lambda 对离群叶子权重的压制 G_out, H_out = 8.0, 1.0 # 一个 g=8 的离群样本独占叶子 for lam in [0.0, 0.5, 1.0, 3.0]: w = -G_out / (H_out + lam) print(f"lambda={lam:.1f} 离群叶子权重={w:+.3f}") # 运行输出: # lambda=0.0 离群叶子权重=-8.000 # lambda=0.5 离群叶子权重=-5.333 # lambda=1.0 离群叶子权重=-4.000 # lambda=3.0 离群叶子权重=-2.000
同一离群样本,λ 从 0 调到 3,叶子输出从 −8 收缩到 −2。噪声多的数据把 λ 调大,常常比限制树深更有效。
💡 关键直觉:结构分数公式把"树形搜索"变成"分数比较"。所有看似复杂的生长、剪枝、评估策略,底层都只是在问一句话——这个改动能让 obj 下降多少?
有了结构分数,树的生长就有裁判了。下一节推导分裂增益公式,看每个候选切分点如何被定价,γ 又如何充当剪枝闸门。
不是。结构分数 obj 是某一棵树(或某次候选改动)的目标函数值,用于树形之间的比较与生长决策;特征重要性是训练结束后对整个森林的统计汇总(第 4.4 节展开)。前者服务于训练过程,后者服务于人类理解,量纲与用途都不同。
当损失不是二次可导或二阶导数 h 可能为负(某些自定义损失)时,分母 H+λ 有变零或变负的风险,闭式解失去意义。XGBoost 对此有保护机制(对 h 做下限截断),但自定义损失时仍应保证 h 非负——这也是第 3.2 节自定义示例选用逐段二次损失的原因。
说明正负梯度互相抵消,这批样本已经被当前模型预测得大致均衡,叶子最优权重接近零、贡献分数也接近零。这正是 min_child_weight 之外另一种"叶子没有存在感"的表现,深度足够时算法会自然停止向这类节点投入分裂预算。