0. 写在前面
看到 XGBoost 目标函数推导里凭空冒出的 $g_i$ 和 $h_i$, 很多教程只说”这是一阶导数和二阶导数”, 却不讲它们是怎么通过泰勒展开变出来的。本文把这个推导从头推一遍, 前提只有一个: 会加法和乘法。
参考来源: 一文入门: XGBoost与手推二阶导 - 博客园。工程建模与调参的内容不在本文范围内, 本站已有一篇《XGBoost梯度提升树建模教程》可以配合阅读。
1. XGBoost 是什么: 一群小朋友接力猜数字
想象我们要猜小明书包里有几颗糖:
- 一棵决策树 = 一个小朋友。他根据线索 (书包大小, 形状, 被压瘪的程度…) 问一连串”如果…那么…“的问题, 最后给出一个猜测。
- Boosting (提升) = 一群小朋友接力。第 1 个先粗略猜一个数; 第 2 个不重新猜, 专门纠正第 1 个的误差; 第 3 个再纠正前两个加起来之后剩下的误差……
- 最终答案 = 所有人猜测的总和。 每个人只求”把剩下的误差补掉一点”, 这种模型叫加法模型:
其中 $f_t$ 是第 $t$ 棵树, $T$ 是树的总数。XGBoost (eXtreme Gradient Boosting, 极限梯度提升) 就是这套接力玩法里策略最讲究的一版。
这里的树是 CART (Classification And Regression Tree, 分类与回归树): 样本沿着树的分支一路走到叶子, 树的输出就是那个叶子上的分数 $w$。
2. 好队伍的军规: Obj = Loss + Ω
XGBoost 给每棵树立了一条军规:
\[Obj = \underbrace{\sum_{i=1}^{n} Loss(y_i, \hat{y}_i)}_{损失: 答得准不准} + \underbrace{\sum_{j=1}^{t} \Omega(cart_j)}_{复杂度: 队伍臃不臃肿}\]翻译成人话: 答案要准, 话还不能多。
损失 Loss 衡量预测值和真实值的差距。回归问题常用平方误差:
\[Loss(y_i, \hat{y}_i) = \frac{1}{2}(y_i - \hat{y}_i)^2\]分类问题常用交叉熵:
\[Loss(y_i, \hat{y}_i) = -\left[y_i \log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i)\right]\]复杂度 Ω 是 XGBoost 的独门发明:
\[\Omega = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2\]| 符号 | 含义 | 人话 |
|---|---|---|
| $T$ | 叶子节点个数 | 提问结束后的”结论格子”数量, 每多一个格子罚 $\gamma$ 分, 防止树疯长 |
| $w_j$ | 第 $j$ 个叶子上的分数 | 每个格子的”自信程度”, 分数越大罚得越狠, 防止某个格子过于笃定 |
| $\gamma, \lambda$ | 罚分力度旋钮 | 人工调节的”严格程度” |
这条军规回答了 GBDT 的一个天生难题: “到底加到第几棵树才算好?” GBDT 不知道; XGBoost 每加一棵树都要交”复杂度税”, 当损失的下降抵不过税收时, 它就自动停了。
3. 先修课: 泰勒展开, 从零学起
手推二阶导只靠一个工具: 泰勒展开 (Taylor Expansion)。我们把它当成新课重新学一遍。
3.1 它解决什么问题
已知函数 $f$ 在 $x$ 点的值, 想快速知道它在”旁边一点点” $x + \Delta x$ 处的值, 但函数太复杂算不动, 怎么办?
生活例子: 你站在山腰, 想知道往东迈一小步 $\Delta x$ 之后海拔变成多少。
- 0 阶 (什么都不看): “海拔不会变” —— $f(x)$
- 1 阶 (看坡度): 脚下坡度是 $f’(x)$ (上坡为正), 那就是现在的海拔加上坡度乘以步长 —— $f(x) + f’(x)\Delta x$
- 2 阶 (发现坡度本身也在变): 越走越陡或者越走越缓, 再修一笔, 加上 $\frac{1}{2}f’‘(x)(\Delta x)^2$
写成公式:
\[f(x + \Delta x) \approx f(x) + f'(x)\Delta x + \frac{1}{2}f''(x)(\Delta x)^2\]一句话: 站在原地, 看坡度, 再看坡度怎么变, 就能推知迈出一小步后的世界。 步长 $\Delta x$ 越小, 这个近似越准, 因为我们没有让函数跑远。
3.2 用一个数字例子验证
取 $f(x) = x^2$, 已知 $x = 10$, 估计 $f(10.1)$, 也就是 $\Delta x = 0.1$:
- 真实值: $f(10.1) = 10.1^2 = 102.01$
- 0 阶: $f(10) = 100$, 误差 2.01, 很差
- 1 阶: $f’(x) = 2x$, $f’(10) = 20$, 所以 $100 + 20 \times 0.1 = 102$, 误差 0.01, 好多了
- 2 阶: $f’‘(x) = 2$, 所以 $102 + \frac{1}{2} \times 2 \times 0.1^2 = 102.01$, 分毫不差
2 阶展开能完全精确不是巧合: $x^2$ 本身就是二次函数, 泰勒展开到 2 阶就把它”写尽”了。记住这一点, 下一节会看到平方损失恰好也是二次的。
3.3 两个记忆要点
- 泰勒展开 = 用多项式临摹复杂曲线: 常数项定高度, 一次项定斜度, 二次项定弯曲度, 阶数越高临摹越像。
- 为什么有 $\frac{1}{2}$ 和 $(\Delta x)^2$: 二阶影响是”加速度式”的, 坡度的偏差会随步长累积, 累积量天然带平方; 而 $\frac{1}{2}$ 来自对 $x^2$ 求两次导产生的 2 被抵消掉一半, 参考上面 $f’‘(x) = 2$ 的例子。
4. 手推: 二阶导是怎么被”用进来”的
现在把泰勒这台机器接到 XGBoost 上, 一共五步。
4.1 写出总目标
第 $t$ 轮时, 模型的总目标是 (对所有 $n$ 个样本求和):
\[Obj = \sum_{i=1}^{n} Loss(y_i, \hat{y}_i^t) + \sum_{j=1}^{t} \Omega(cart_j)\]其中 $\hat{y}_i^t$ 是前 $t$ 棵树对样本 $i$ 的总预测。
4.2 新树只负责补差价
前 $t-1$ 棵树已经训练完, 全部视为定死的常数。第 $t$ 棵树 $f_t$ 的任务不是重新预测, 而是在旧预测上加一个修正量:
\[\hat{y}_i^t = \hat{y}_i^{t-1} + f_t(x_i)\]4.3 识别出”这是泰勒场景”
关键观察: 损失函数里的真实值 $y_i$ 是固定常数 (答案都印在卷子上, 不随模型变化), 所以损失其实只随预测值变化, 可以看成一元函数 $L(\cdot)$。代入补差价公式:
\[Loss(y_i, \hat{y}_i^t) = L\left(\hat{y}_i^{t-1} + f_t(x_i)\right)\]对照泰勒模板 $f(x + \Delta x) \approx f(x) + f’(x)\Delta x + \frac{1}{2}f’‘(x)(\Delta x)^2$:
- 这里的 $x$ 就是旧预测 $\hat{y}_i^{t-1}$
- 这里的 $\Delta x$ 就是新树的输出 $f_t(x_i)$
完全同构。
4.4 套用泰勒展开
\[L(\hat{y}_i^{t-1} + f_t(x_i)) \approx L(\hat{y}_i^{t-1}) + L'(\hat{y}_i^{t-1}) \cdot f_t(x_i) + \frac{1}{2} L''(\hat{y}_i^{t-1}) \cdot f_t^2(x_i)\]习惯记号: $g_i = L’(\hat{y}_i^{t-1})$, $h_i = L’’(\hat{y}_i^{t-1})$, 分别是损失对预测值的一阶导数和二阶导数。
注意一个容易忽略的点: 求导是对预测值做的, 而且在旧预测 $\hat{y}_i^{t-1}$ 这一点上”一次性算完”, 所以对第 $t$ 棵树来说, $g_i$ 和 $h_i$ 只是两个已知的数字, 每个样本各一对。
4.5 扔掉常数, 得到第 t 棵树的优化目标
在第 4.4 节的结果里, $L(\hat{y}_i^{t-1})$ 是旧预测的损失, 前 $t-1$ 棵树的复杂度也都和第 $t$ 棵树无关, 是常数, 直接删掉。剩下:
\[Obj^t \approx \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)\]这就是 XGBoost 每一轮真正求解的目标。看到泰勒展开带来的差别了吗? 如果只展开到 1 阶, 上式就没有 $\frac{1}{2}h_i f_t^2(x_i)$ 那一项 —— GBDT 正是这么干的 (只拟合负梯度 $-g_i$)。多出来的二阶项让目标函数变成关于新树输出的标准二次函数: 形状像一个碗, 有唯一最低点, 可以直接求导解析求解, 而不是像 GBDT 那样只能沿着梯度方向”盲走一小步”。
4.6 再往前一步: 叶子权重与分裂增益
(参考文章没有展开这一段, 这里补齐闭环。) 把样本按”落在哪个叶子”分组, 记第 $j$ 个叶子里样本的 $g$ 之和为 $G_j$, $h$ 之和为 $H_j$, 则目标可以改写成关于叶子分数的二次函数, 对每个叶子独立求导并令其为零, 解出最优叶子分数:
\[w_j^* = -\frac{G_j}{H_j + \lambda}\]把它代回去, 还能得到衡量”一次分裂赚了多少”的分裂增益, XGBoost 就是用它来决定树怎么长、什么时候停:
\[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\]现在回头看第 2 节的复杂度公式就完整了: $\lambda$ 出现在分母上抑制叶子分数, $\gamma$ 作为增益的门槛惩罚叶子个数 —— 正则项不是拍脑袋加的, 而是从这个推导里自然长出来的。
5. XGBoost vs GBDT: 四大区别
| # | 区别 | 人话 |
|---|---|---|
| 1 | 自带正则项 $\Omega$ | GBDT 不知道加几棵树该停; XGBoost 每棵树都交”复杂度税”, 损失降不过税就自动停 |
| 2 | 用到二阶导数 $h_i$ | GBDT 只看一阶梯度 (斜率); XGBoost 连坡度的变化 (弯曲度) 都用上, 近似更准, 步子迈得更聪明 |
| 3 | 列抽样 | 借鉴随机森林: 不仅对样本抽样 (行), 还对特征抽样 (列), 既防过拟合又省算力 |
| 4 | 自动处理缺失值 | 缺失样本会被学习到”该往哪边走”, 不需要提前填空 |
其中第 2 条正是第 3 节和第 4 节全部推导的意义所在。
6. 小结
XGBoost 是一群树接力补误差; 它给队伍立了”又准又简洁”的军规 $Obj = Loss + \Omega$; 而它比 GBDT 高明的核心, 是用泰勒展开在旧预测这个小点上把未来的损失算清楚:
\[\underbrace{f(x+\Delta x)}_{未来} \approx \underbrace{f(x)}_{现在} + \underbrace{f'(x)\Delta x}_{坡度} + \frac{1}{2}\underbrace{f''(x)(\Delta x)^2}_{坡度的变化}\]从而让每棵新树面对的是一个可以直接求解的二次优化问题, $g_i$ 和 $h_i$ 就是这台泰勒机器吐出来的两个数字。
勘误: 原参考文章有两处笔误。其一, 二分类交叉熵第二项应为 $\log(1-\hat{y}_i)$ 而非 $\log(\hat{y}_i)$, 否则 $y=0$ 时公式不成立; 其二, “叶子权重与叶子样本数量成正比”的说法不准确, 叶子权重实际由叶内样本的一阶/二阶导数之和解析求出 (见 4.6 节)。