观文听傑

返回

上一篇的随机森林让许多深树并行生长,再平均它们的预测。它擅长降低单棵树的高方差,但所有树若都漏掉同一种规律,平均只会稳定地保留这份共同偏差。

例如房价真实关系包含一个细小的“学区边界”,而每棵树都因叶子限制没有学到它。再增加 1,000 棵相似的树,遗漏不会自动消失。我们需要让新树看到旧模型还错在哪里,而不是从头回答同一个问题。

梯度提升(Gradient Boosting)采取顺序纠错:当前模型先预测,下一棵浅树拟合损失最希望修正的方向,再把这份修正加回模型。本文只讲透这条数据流,以及它为什么从“拟合残差”推广为“拟合负梯度”。

01 从并行平均转向顺序加法#

随机森林中的树可以独立训练:

同一训练集
  ├── 随机数据/特征 ─► Tree 1 ─┐
  ├── 随机数据/特征 ─► Tree 2 ─┼─► 平均
  └── 随机数据/特征 ─► Tree M ─┘
text

梯度提升树(Gradient Boosted Decision Trees,GBDT)则有严格的先后依赖:

X [N,D] ─► 当前模型 F₀ ─► prediction₀ [N]
                            │ 与 y [N] 比较

                      correction₁ [N]
                            │ 拟合

                         Tree 1
                            │ 加到 F₀

             F₁ = F₀ + η Tree 1

                     重复 M 轮

             F_M(x) = F₀(x) + ηΣ Tree_m(x)
text

mm 轮的模型写成逐步加法模型(Stage-wise Additive Model):

Fm(x)=Fm1(x)+ηhm(x)F_m(x)=F_{m-1}(x)+\eta h_m(x)
  • Fm1F_{m-1}:前 m1m-1 轮树的总和;
  • hmh_m:第 mm 棵回归树输出的修正值;
  • η(0,1]\eta\in(0,1]:学习率(Learning Rate),也叫收缩系数;
  • Fm(x)F_m(x):加入新树后的预测。

这里每棵树通常很浅。单棵树只负责一小块可解释的修正,复杂函数由许多小步累积出来。

02 为什么平方误差下会拟合残差?#

先考虑回归和半平方误差:

L(yi,F(xi))=12(yiF(xi))2L(y_i,F(x_i))=\frac12\left(y_i-F(x_i)\right)^2

定义当前残差:

rim=yiFm1(xi)r_{im}=y_i-F_{m-1}(x_i)

若下一棵树能精确输出 rimr_{im},且 η=1\eta=1,新预测就会变成真实值:

Fm(xi)=Fm1(xi)+rim=yiF_m(x_i)=F_{m-1}(x_i)+r_{im}=y_i

树无法逐点记住所有残差时,会把特征空间切成若干叶子,在每个叶子输出相近残差的平均值。因此它学到的是:“什么样的样本,目前被系统性低估或高估?”

当前预测太高:r < 0 ─► 新树输出负修正 ─► 预测下降
当前预测正确:r ≈ 0 ─► 新树输出接近 0 ─► 基本不动
当前预测太低:r > 0 ─► 新树输出正修正 ─► 预测上升
text

03 用四个样本手算两轮提升#

给定一个特征 xx 和四个回归目标:

样本 iixix_iyiy_i
113
225
334
448

第 0 轮:从常数预测开始#

平方误差下最好的常数是目标均值:

F0(x)=yˉ=3+5+4+84=5F_0(x)=\bar y=\frac{3+5+4+8}{4}=5

因此:

y:              [ 3, 5, 4, 8 ]
prediction_0:   [ 5, 5, 5, 5 ]
residual_1:     [-2, 0,-1, 3 ]
text

初始均方误差为:

MSE0=(2)2+02+(1)2+324=3.5\operatorname{MSE}_0=\frac{(-2)^2+0^2+(-1)^2+3^2}{4}=3.5

第 1 轮:用树桩拟合残差#

假设树桩选择 x2x\le2:左叶残差均值为 (2+0)/2=1(-2+0)/2=-1,右叶为 (1+3)/2=1(-1+3)/2=1

令学习率 η=0.5\eta=0.5

h_1(x):         [-1,-1, 1, 1]
η h_1(x):       [-0.5,-0.5, 0.5, 0.5]
prediction_1:   [ 4.5, 4.5, 5.5, 5.5]
residual_2:     [-1.5, 0.5,-1.5, 2.5]
text

新均方误差为 2.752.75。第一棵树没有直接预测房价,它只把低 xx 区域整体向下修正、高 xx 区域整体向上修正。

第 2 轮:只看剩下的错误#

第二棵树拟合 residual_2。假设它选择 x3x\le3

h2(x3)=1.5+0.51.530.833h_2(x\le3)=\frac{-1.5+0.5-1.5}{3}\approx-0.833 h2(x>3)=2.5h_2(x>3)=2.5

乘上 η=0.5\eta=0.5 后:

η h_2(x):       [-0.417,-0.417,-0.417, 1.250]
prediction_2:   [ 4.083, 4.083, 5.083, 6.750]
residual_3:     [-1.083, 0.917,-1.083, 1.250]
text

均方误差降到约 1.191.19。两棵简单树叠加后,已经能表达任何一棵树桩单独无法表达的三级预测。

交互手算:若第一轮 η=1,会怎样?

第一轮预测会变成 [4,4,6,6],MSE 为 2,比 η=0.5 的 2.75 更低。但单步下降更多不保证验证误差更好;较小学习率让后续树有机会用更细的步骤修正,通常需要更多轮数。

04 “负梯度”怎样统一不同损失?#

残差只在平方误差下恰好成立。若目标改成稳健回归的绝对误差、二分类的对数损失,临时目标该是什么?

把每个训练点当前的预测 Fm1(xi)F_{m-1}(x_i) 看成一个可调整变量。损失对它的导数是最陡上升方向,所以最陡下降方向为:

g_{im} =-left. \frac{\partial L(y_i,F(x_i))}{\partial F(x_i)} \right|_{F=F_{m-1}}

这叫伪残差(Pseudo-residual)或负梯度。第 mm 棵树拟合训练对:

{(xi,gim)}i=1N\{(x_i,g_{im})\}_{i=1}^{N}

对半平方误差:

F12(yF)2=yF-\frac{\partial}{\partial F}\frac12(y-F)^2=y-F

负梯度正好就是普通残差。于是“拟合残差”不是一条孤立技巧,而是函数空间梯度下降(Gradient Descent in Function Space)在平方误差下的具体形式。

损失当前模型最关注的方向直觉
平方误差yFy-F大残差得到更大修正
绝对误差残差符号(不可导点取次梯度)降低异常值支配
二元对数损失ypy-p(以 logit 为模型输出)修正预测概率与标签的差

05 训练与推理的完整伪代码#

推理时没有残差、标签或反向传播:

X_query [Q,D]
  ├── F_0                         [Q]
  ├── η h_1(X_query)              [Q]
  ├── η h_2(X_query)              [Q]
  └── ... + η h_M(X_query)        [Q]
               │ element-wise sum

          prediction [Q]
text

这也解释了为什么提升树难以像随机森林那样并行训练,却可以在树建好后批量并行计算部分节点和样本。

06 不依赖黑盒,写出最小平方误差提升#

下面只用 NumPy 写两轮“决策树桩 + 残差”,把关键状态全部暴露出来:

真实实现需要处理多特征、深树、任意损失、叶值优化、采样和高效直方图,但训练语义没有改变:先计算当前模型的方向,再拟合一棵树并缩小后相加。

07 用当前 scikit-learn API 落地并观察每一轮#

scikit-learn 1.9 的 GradientBoostingRegressor 是经典的精确梯度提升实现。下面显式留出测试集;估计器内部再从训练数据中留出早停验证集:

关键参数不是彼此独立的:

  • n_estimators:最大提升轮数;回归的每轮通常增加一棵树;
  • learning_rate:每棵树加入总模型前的缩放;更小通常需要更多树;
  • max_depth:单棵修正树的交互复杂度;深度 2 最多直接表达二阶特征交互;
  • min_samples_leaf:阻止极小叶对少数样本给出激进修正;
  • subsample < 1:每轮用部分训练样本,形成随机梯度提升并降低相关性;
  • n_iter_no_change:内部验证损失连续若干轮没有足够改善就停止。

使用 staged_predict 可以看到模型随树数增加的验证轨迹:

validation_rmse = []

for round_id, prediction in enumerate(model.staged_predict(X_test), start=1):
    validation_rmse.append(root_mean_squared_error(y_test, prediction))

best_round = int(np.argmin(validation_rmse)) + 1
print('best observed round:', best_round)
python

这段示例为了展示 API 使用 X_test 画轨迹;正式项目中应改用验证集,测试集只能在轮数和全部超参数冻结后评估一次。

08 数据变大时为什么常用直方图提升?#

精确枚举许多连续特征的候选阈值代价较高。直方图梯度提升(Histogram-based Gradient Boosting)先把连续值分箱,再在箱边界上累计梯度统计量。

原始特征值:0.13 0.18 0.19 0.44 0.47 0.91 ...
                 │ quantile / histogram bins

箱编号:      0    0    0    1    1    3  ...
                 │ 每箱聚合梯度

            只比较有限箱边界
text

scikit-learn 1.9 对中大型数据推荐 HistGradientBoostingRegressor;文档给出的经验起点是 N10,000N\ge10,000。它使用 max_iter 表示轮数,并原生处理数值缺失值:

原生接收 NaN 只表示算法能学习缺失值路由,并不证明缺失机制合理。训练与线上缺失率差异、把“未知”编码成 0、类别列类型漂移,仍会让模型失效。

09 学习率、树深与轮数如何一起控制容量?#

旋钮调大后的直接变化常见风险
learning_rate单棵树修正更强很快追逐噪声、验证损失反弹
树深 / 叶数单轮可表达更复杂交互小叶、不稳定、内存与延迟增加
提升轮数累积更多修正训练损失继续降而验证损失升
min_samples_leaf每个叶子证据更多过大时遗漏局部结构
subsample 降低每轮数据更随机太低时单树方向噪声过大

最稳妥的流程是:先用较浅的树和较小学习率,给足较大的最大轮数,再用独立验证信号早停。不要只按“100 棵树是默认值”决定训练长度。

training loss   ╲________________
validation loss ╲______╱

                  最佳轮数附近
text

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

  1. 把每轮目标固定成原始标签。 第二棵树开始必须依赖当前预测的负梯度;否则只是训练了很多重复树。
  2. 认为残差适用于任意损失。 非平方误差要按损失对当前输出求负梯度,并可能重新求每个叶子的最佳值。
  3. 用训练损失选择树数。 训练损失通常随轮数继续下降;树数应由验证曲线和业务指标决定。
  4. 同时使用深树、高学习率和很多轮。 三个容量旋钮会叠加,常导致快速过拟合。
  5. 早停验证集发生泄漏。 时间、患者或用户分组任务不能由内部随机切分代替结构化验证。
  6. 把特征重要性当因果解释。 分裂增益只描述模型使用了什么;相关特征、高基数和泄漏列都会误导。
  7. 线上漏加部分树。 推理模型必须保存初始常数、树顺序、学习率和实际最佳轮数,任一不一致都会改变输出。
  8. 只监控平均延迟。 提升树按顺序累加大量成员;同时测模型大小、P50/P99 延迟和峰值内存。

最小数值检查:

assert X_train.ndim == 2 and y_train.ndim == 1
assert X_train.shape[0] == y_train.shape[0]
assert np.isfinite(y_train).all()
assert model.n_estimators_ <= model.n_estimators
assert np.isfinite(model.predict(X_test)).all()
python

若第一棵树后训练损失完全不变,检查目标是否为常数、特征是否都缺失、min_samples_leaf 是否大到无法分裂;若训练损失下降而验证损失从第一轮就上升,优先检查数据划分、泄漏、分布偏移和目标定义,不要先堆更多树。

11 它与相近方法有什么区别?#

方法成员关系每个成员学什么主要改善
单棵决策树无集成直接拟合标签可追踪规则
随机森林并行、近似独立各自拟合标签通过平均降低方差
AdaBoost顺序提高错分样本权重聚焦困难样本
GBDT顺序拟合当前损失的负梯度逐轮降低一般可导损失
神经网络梯度下降同一网络内迭代更新参数张量在固定可微结构中优化

GBDT 在表格数据、非线性阈值和特征交互上通常很强,但它不会自然向训练范围外做平滑线性外推;面对超高维稀疏文本、图像原始像素、序列结构或需要端到端表示学习的任务,线性模型和神经网络往往有更合适的归纳偏置。

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

  1. 梯度提升不是平均许多独立树,而是让新树顺序修正当前模型仍犯的错误。
  2. 平方误差下,损失对预测的负梯度恰好等于残差,所以新树拟合残差。
  3. 一般损失下,每轮树拟合伪残差,即损失对当前函数输出的负梯度。
  4. 学习率缩小单轮修正,树深控制单轮交互,轮数控制累积容量;三者必须联合验证。
  5. 训练损失持续下降不代表应该继续加树,结构化验证和早停决定可用轮数。

13 思考题与小练习#

练习 1:完成第三轮手算

从本文第二轮残差 [-1.083, 0.917, -1.083, 1.250] 出发,分别尝试阈值 1.5、2.5、3.5,计算左右叶均值与叶内平方误差。选出最佳树桩,再用 η=0.5 更新预测。

练习 2:推导二分类伪残差

令模型输出 logit FF,概率 p=σ(F)p=\sigma(F),二元交叉熵为 [ylogp+(1y)log(1p)]-[y\log p+(1-y)\log(1-p)]。证明损失对 FF 的负梯度是 ypy-p,并解释为什么“自信地预测错”会得到较大修正。

练习 3:画出验证轨迹

固定 max_depth=2,比较 (learning_rate, n_estimators)(0.2,100)(0.05,400)(0.01,2000) 的训练与验证 RMSE。记录最佳轮数、模型大小和推理时间,而不只比较最低 RMSE。

相关工作#

14 下一篇预告#

经典 GBDT 用一阶负梯度告诉新树“往哪里改”,但没有直接利用损失曲率回答“应该改多大”,树的叶值与分裂也缺少统一的正则化评分。下一篇将从二阶泰勒展开推导 XGBoost 的叶权重和分裂增益,手算 reg_lambdagammamin_child_weight 究竟拦住了什么。

后一棵树怎样修正前一棵?从残差到梯度提升树
https://zwjcode.cn/blog/gradient-boosting-residual-functional-gradient
作者
发布于 2026年8月23日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。