不是所有人都会告诉你,GBDT的每一棵树拟合的其实是上一轮预测的梯度方向,而不是什么玄乎的"残差"。我第一次看到"基于梯度的boosting方法推导"时,以为就是把梯度下降换了个名字。后来真去推了一遍,才发现它把"参数空间优化"搬到了"函数空间优化",这一步跨越直接决定了GBDT、XGBoost、LightGBM这一整条技术线的走向。这篇文章把我从零推导的过程完整写出来,包含手算数值算例、必要公式和实现代码,适合已经会用sklearn但还没啃过推导细节的读者,也适合准备面试时把"为什么GBDT要拟合负梯度"讲清楚的人。
1. 从"拟合残差"到"拟合梯度":大多数教程没讲透的那一步
1.1 "拟合残差"这句话的适用范围
初学者接触GBDT时,几乎都会看到一句话:每一轮新树拟合的是上一轮预测的残差。这句话在平方损失下完全正确,也特别好理解——上一轮预测高了,这轮就往低修;预测低了,就往高补。
但这句话藏着一个大坑:它把"残差"当成了Boosting的本质,把"平方损失"当成了默认损失。一旦你换一个损失函数,比如绝对损失、Huber损失、分位数损失,或者处理分类问题的对数损失,你继续用残差去拟合,模型性能就会出问题。
我最早踩过这个坑。当时想做一个分位数回归,于是直接把GBDT的平方损失换成了分位数损失,然后依然按照残差去拟合下一棵树。结果训练到一半损失根本不降。折腾了一晚上,最后回到原始论文看推导,才意识到问题出在哪:GBDT每一轮要拟合的目标不是残差,而是损失函数在当前位置的负梯度。残差只是当损失函数为平方损失时,负梯度的特殊形式。
1.2 当损失函数不再是平方损失
为了把这个问题看清楚,我们先写出加性模型的通用形式:
$$F_M(x) = \sum_{m=0}^{M} f_m(x)$$
其中 $F_M(x)$ 是最终模型,$f_0(x)$ 是初始常数模型,$f_m(x)$ 是第 $m$ 轮加入的基学习器。给定损失函数 $L(y, F(x))$,我们希望最小化:
$$\mathcal{L} = \sum_{i=1}^{N} L\left(y_i, F(x_i)\right)$$
这里 $F$ 是一个函数,不是一个向量参数。如果 $L$ 是平方损失 $\frac{1}{2}(y-F)^2$,对 $F$ 求导得到 $(F-y)$,负梯度正好是 $(y-F)$,也就是残差。
但如果 $L$ 是绝对损失 $|y-F|$,对 $F$ 求导(在 $F \neq y$ 处)得到 $-\text{sign}(y-F)$,负梯度是 $\text{sign}(y-F)$。这个结果和残差一点都不像。它只告诉模型:预测偏高就往下降,预测偏低就往上升,至于幅度多大,它不关心。这时候如果你还用残差去拟合,等于给模型传递了错误的监督信号,训练自然出问题。
1.3 负梯度作为通用伪残差
正确的做法是:每一轮计算每个样本在当前模型 $F_{m-1}(x_i)$ 处的负梯度:
$$\tilde{y}i = -\left. \frac{\partial L(y_i, F(x_i))}{\partial F(x_i)} \right|(x_i)}$$
这个 $\tilde{y}_i$ 在GBDT的语境里叫"伪残差"(pseudo-residual)。然后让第 $m$ 轮的基学习器去拟合这批伪残差。为什么可以这样?因为负梯度是损失函数下降最快的方向,我们想通过增加一个基学习器来最小化损失,最好的方向就是沿着负梯度走。
从这一刻开始,Boosting从"一种针对残差的启发式方法"变成了"一种在函数空间做梯度下降的通用框架"。任何可导的损失函数都可以套进去,只是不同损失函数对应不同的伪残差而已。理解了这一点,后面的推导就顺了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 函数空间里的最速下降:梯度提升为什么叫"梯度"提升
2.1 加性模型与函数空间优化
我们平时用的梯度下降,对象是一组参数 $\theta$,比如神经网络的权重。每次迭代更新的是参数向量:
$$\theta_{t+1} = \theta_t - \eta \nabla_\theta L(\theta_t)$$
GBDT做的事情在形式上完全一样,但对象换成了函数 $F$。我们不直接修改某个参数,而是往模型里"加一棵树"。这棵树可以看成是函数空间里的一个增量 $h_m(x)$:
$$F_m(x) = F_{m-1}(x) + \eta , h_m(x)$$
为什么可以这么做?因为损失函数是定义在样本点上的。对于 $N$ 个训练样本,我们可以把 $F(x_i)$ 看成 $N$ 个变量,那么损失函数 $\sum_i L(y_i, F(x_i))$ 对这些变量的梯度就是一个 $N$ 维向量:
$$g_i = \frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}$$
问题在于,我们不可能直接更新 $N$ 个"坐标值",因为测试时会遇到没见过的新样本。我们需要用一个可以从特征 $x$ 映射到梯度方向的函数 $h_m(x)$ 来近似这个 $N$ 维梯度向量。树模型就是干这个的。
2.2 一阶泰勒展开:为什么方向必须是负梯度
假设当前模型是 $F_{m-1}$,我们想找到一个增量 $h$,使得:
$$L(y, F_{m-1} + h) < L(y, F_{m-1})$$
对 $L$ 做一阶泰勒展开,在单个样本上近似为:
$$L(y, F_{m-1} + h) \approx L(y, F_{m-1}) + \left.\frac{\partial L}{\partial F}\right|{F{m-1}} \cdot h$$
对所有样本求和后,要让损失下降,就必须让第二项的求和为负。而 $\frac{\partial L}{\partial F}$ 是当前梯度方向,$h$ 如果和负梯度方向一致,内积就会是负数,损失就会下降。这就是"负梯度方向是局部最速下降方向"的由来。
这里有个细节值得注意:梯度下降要求每一步更新量必须和负梯度方向一致,但不要求方向和负梯度完全重合。GBDT也没有强求树完美拟合伪残差,它只希望树输出的方向尽量接近负梯度。所以基学习器用回归树而不是分类树,输出的是连续值,目的就是拟合伪残差这个连续目标。
2.3 树模型拟合负梯度的几何含义
把伪残差看成是 $N$ 维空间里的一个目标向量,树模型做的事情就是用分段的常数函数去逼近这个向量。一个叶节点里的样本共享同一个输出值,这个值通常取叶节点内伪残差的均值(平方损失下)或中位数(绝对损失下)。
几何上,每一轮就是在函数空间中沿着损失下降的方向走一小步。多棵树叠加起来,最终模型 $F_M$ 就是一个通过大量小步逼近损失函数极小点的函数。要注意的是,梯度下降本身不能保证收敛到全局最优,函数空间可能更复杂,所以实践中需要正则化手段来防止过拟合,这也解释了为什么GBDT需要控制树深度和学习率。
3. 三类损失函数的负梯度推导:回归、鲁棒回归、二分类
3.1 平方损失:负梯度恰好等于残差
这是最常见也最直观的情况。设:
$$L(y, F) = \frac{1}{2}(y - F)^2$$
对 $F$ 求导:
$$\frac{\partial L}{\partial F} = F - y$$
所以负梯度为:
$$-\frac{\partial L}{\partial F} = y - F$$
这个 $y - F$ 就是残差。注意这里我加了系数 $\frac12$,否则会多出一个常数因子 $2$,但那个因子会被后续的步长吸收,不影响模型表达能力。实际实现里为了简洁,很多人直接写成 $(y-F)^2$,对应负梯度是 $2(y-F)$,学出来的模型和 $\frac12$ 版本是等价的,因为树拟合的数值虽然相差一个倍数,但叶子节点在更新时还会通过线性搜索调整输出值。
3.2 绝对损失:符号函数带来的鲁棒性
设:
$$L(y, F) = |y - F|$$
在 $F \neq y$ 处求导:
$$\frac{\partial L}{\partial F} = -\text{sign}(y - F)$$
负梯度为:
$$-\frac{\partial L}{\partial F} = \text{sign}(y - F)$$
也就是说,每个样本给树的监督信号只有三个值:-1、0、1。它告诉树"方向错了"还是"方向对了",但不告诉错多远。这对异常值特别友好:一个离群点即使偏离很大,它对梯度的贡献也不过是 ±1,而平方损失下的残差可能是几十甚至几百,会把整棵树带偏。
但要注意,绝对损失下的叶子节点输出不再是均值,而是中位数。因为当我们拟合完树结构后,叶子节点的最优输出需要最小化这个叶子内所有样本的绝对损失之和,最优解是中位数。这就解释了为什么GBDT换成绝对损失后,模型的鲁棒性会显著提升。
3.3 对数损失:二分类GBDT的伪响应推导
二分类GBDT通常不直接预测类别,而是预测一个 logit 值 $F(x)$,然后用 sigmoid 函数转换成概率:
$$p = \sigma(F) = \frac{1}{1 + e^{-F}}$$
对数损失(也叫交叉熵):
$$L(y, p) = -\left[ y \log p + (1-y) \log(1-p) \right]$$
其中 $y \in {0, 1}$。我们要求的是 $\frac{\partial L}{\partial F}$。sigmoid 函数有一个很好的性质:
$$\sigma'(F) = \sigma(F)\left(1 - \sigma(F)\right) = p(1-p)$$
对损失函数关于 $F$ 求导:
$$\frac{\partial L}{\partial F} = -\left[ \frac{y}{p} \cdot p(1-p) - \frac{1-y}{1-p} \cdot p(1-p) \right]$$
整理一下:
$$\frac{\partial L}{\partial F} = -\left[ y(1-p) - (1-y)p \right] = -(y - p) = p - y$$
负梯度就是:
$$-\frac{\partial L}{\partial F} = y - p$$
所以二分类GBDT每一轮的伪残差就是"真实标签减去当前预测概率"。这是不是有点眼熟?它和逻辑回归里每个样本的梯度形式一模一样,只是逻辑回归用线性模型拟合,而GBDT用树拟合。理解了这一点,再看scikit-learn里 GradientBoostingClassifier 内部计算 pseudoresidual 的过程就一目了然了。
3.4 一个汇总表:不同损失下的负梯度与叶子输出
为了直观,我把常用的损失函数整理成了表格:
| 损失函数 | 表达式 | 负梯度/伪残差 | 叶子节点最优输出 |
|---|---|---|---|
| 平方损失 | $\frac12(y-F)^2$ | $y - F$ | 均值 |
| 绝对损失 | $|y - F|$ | $\text{sign}(y-F)$ | 中位数 |
| Huber损失 | 分段函数 | 小残差时约等于残差,大残差时截断 | 修正后的加权均值 |
| 对数损失(二分类) | $-y\log p-(1-y)\log(1-p)$ | $y - p$ | 对数值修正 |
我建议你把这张表记下来。面试的时候问"GBDT每一轮拟合什么",回答"拟合负梯度"只是及格,能把这张表推导出来才是真正的理解。
4. 完整的GBDT推导与一个手算数值算例
4.1 GBDT的通用算法流程(伪代码)
把上面的梯度下降思路整理成算法,GBDT的完整训练过程如下:
-
初始化常数模型:
$$F_0(x) = \arg\min_\gamma \sum_{i=1}^{N} L(y_i, \gamma)$$
平方损失下 $\gamma$ 是均值,绝对损失下是位数,对数损失下是正样本占比取logit。 -
对 $m = 1,2,\dots,M$:
- 计算伪残差:
$$\tilde{y}i = -\left.\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\right|{F=F_{m-1}}$$ - 用回归树拟合 $\tilde{y}i$,得到叶子区域 $R$。
- 对每个叶子节点 $j$,计算最优输出:
$$\gamma_{jm} = \arg\min_\gamma \sum_{x_i \in R_{jm}} L(y_i, F_{m-1}(x_i) + \gamma)$$ - 更新模型:
$$F_m(x) = F_{m-1}(x) + \eta \sum_{j} \gamma_{jm} I(x \in R_{jm})$$
- 计算伪残差:
-
输出 $F_M(x)$。
这个流程里最关键也最容易出错的一步,是在确定叶子节点输出时,不是简单取伪残差的均值,而是要重新在损失函数下求最优。只是在平方损失下,它恰好退化成均值。
4.2 叶子节点最优输出的推导
以平方损失为例,假设一个叶子节点 $R_j$ 里有 $n_j$ 个样本,我们在这个叶子内输出 $\gamma$。平方损失下,在这个叶子上的损失为:
$$\sum_{x_i \in R_j} \frac12\left(y_i - (F_{m-1}(x_i) + \gamma)\right)^2$$
对 $\gamma$ 求导并令其为零:
$$\sum_{x_i \in R_j} \left(F_{m-1}(x_i) + \gamma - y_i\right) = 0$$
得到:
$$\gamma = \frac{1}{n_j} \sum_{x_i \in R_j} (y_i - F_{m-1}(x_i)) = \frac{1}{n_j} \sum_{x_i \in R_j} \tilde{y}_i$$
所以平方损失下,叶子最优输出就是该叶子内伪残差的均值。绝对损失下则是中位数。这个推导也解释了为什么很多GBDT实现里叶子节点默认输出均值,因为默认损失是平方损失。
4.3 手算算例:4个样本、3轮迭代
空谈推导容易飘,我用手算一个小例子把整个过程走一遍。假设有4个训练样本:
| 样本 | x | y |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 3 |
| 3 | 3 | 7 |
| 4 | 4 | 8 |
使用平方损失,树深度限制为1,即每次只做一个分裂点,学习率先设为1,便于展示。初始模型:
$$F_0 = \frac{2+3+7+8}{4} = 5$$
第一轮伪残差:
$$[-3, -2, 2, 3]$$
用 x 拟合残差,深度为1的树需要选择一个分裂点。尝试候选分裂点 $x \le 1.5$、$x \le 2.5$、$x \le 3.5$,比较分裂后平方误差和:
- 分裂 $x \le 1.5$:左侧样本1预测-3,右侧样本2、3、4预测均值1,总误差 $0 + (-2-1)^2 + (2-1)^2 + (3-1)^2 = 14$
- 分裂 $x \le 2.5$:左侧样本1、2预测均值-2.5,右侧样本3、4预测均值2.5,总误差 $(-3+2.5)^2 + (-2+2.5)^2 + (2-2.5)^2 + (3-2.5)^2 = 1$
- 分裂 $x \le 3.5$:左侧样本1、2、3预测均值-1,右侧样本4预测3,总误差 $(-3+1)^2 + (-2+1)^2 + (2+1)^2 + 0 = 14$
选 $x \le 2.5$,叶子输出为 $-2.5$ 和 $2.5$。更新模型:
$$F_1 = [2, 2.5, 7.5, 7.5]$$
第一轮后样本2、3、4已经离目标比较近,样本1完全修正到2。
第二轮伪残差:
$$[0, 0.5, -0.5, 0.5]$$
注意样本2当前预测2.5,目标3,所以残差0.5;样本3当前预测7.5,目标7,残差-0.5;样本4当前预测7.5,目标8,残差0.5。重新拟合一棵树:
- 分裂 $x \le 1.5$:左侧样本1预测0,右侧样本2、3、4预测均值约0.167,总误差 $0 + (0.5-0.167)^2 + (-0.5-0.167)^2 + (0.5-0.167)^2 \approx 0.667$
- 分裂 $x \le 2.5$:左侧样本1、2预测均值0.25,右侧样本3、4预测均值0,总误差 $(0-0.25)^2 + (0.5-0.25)^2 + (-0.5-0)^2 + (0.5-0)^2 = 0.625$
- 分裂 $x \le 3.5$:左侧样本1、2、3预测均值0,右侧样本4预测0.5,总误差 $0 + 0.25 + 0.25 + 0 = 0.5$
选 $x \le 3.5$,叶子输出为0和0.5。更新模型:
$$F_2 = [2, 2.5, 7.5, 8.0]$$
样本4被修正,样本2、3还有一点误差。如果继续迭代,模型会继续修正。从这个手算过程你能看到GBDT其实很朴素:每轮都在用树去拟合"当前还差多少",但"差多少"由损失函数的梯度决定,而不是拍脑袋定下来的。
5. 学习率、子采样与梯度爆炸的错觉:调参前先弄清机制
5.1 学习率是步长,不是正则化系数
GBDT里学习率 $\eta$ 也叫收缩系数,更新公式写成:
$$F_m(x) = F_{m-1}(x) + \eta \cdot h_m(x)$$
它的作用类比梯度下降里的步长。$\eta$ 越大,每轮更新幅度越大,模型拟合训练数据越快,但也更容易过拟合。$\eta$ 越小,每步走得越谨慎,往往需要更多棵树才能达到同样的拟合效果,但泛化能力通常更好。
我在实际项目中习惯先固定 $\eta=0.05$ 或 $\eta=0.1$,再去调树的深度和叶子节点数量。不要一开始就把 $\eta$ 调到0.5以上,否则你会看到训练损失下降很快,验证集损失却不降甚至上升。
5.2 梯度爆炸在GBDT里并不对应反向传播的连乘
"梯度爆炸"这个词在深度学习里指的是反向传播时链式法则连乘,导致梯度值指数级增大。GBDT是加法模型,每一轮的树独立拟合伪残差,更新过程没有连乘,所以通常不存在神经网络意义上的梯度爆炸。
但是有一个现象和梯度爆炸在效果上很像:当树特别深、学习率又很高时,单棵树可能把伪残差拟合得过于激进,导致某个样本的预测值被推过头,最终模型出现剧烈震荡。我在调参时遇到过训练损失突然反弹的情况,排查原因是树深太大,叶子节点里样本太少,每个叶子输出都对单个样本过拟合。
处理方式比较简单:限制树深度(比如最大深度3到6)、限制叶子节点最小样本数、降低学习率、增加子采样比例。这样每一轮更新的方向仍然正确,但幅度不会失控。
5.3 子采样与早停:真正控制方差的常见手段
GBDT实际训练里常用的两个手段是行采样和列采样。行采样也叫随机梯度提升,每轮只随机抽一部分样本计算伪残差和训练树;列采样是在每个分裂点只考虑一部分特征。两者都能降低树之间的相关性,提高泛化能力。
早停则是直接盯住验证集损失,连续若干轮不下降就停止训练。我建议用早停而不是先拍脑袋定树的数量,因为最优树的数量高度依赖数据集大小、噪声程度和学习率,靠经验不如靠验证集。
5.4 梯度思想的通用性:多因素梯度回归与角点检测的侧面印证
梯度这个概念在很多领域都是同一套思想。多因素梯度回归里,我们关心的是多个因素同时对目标变量产生梯度方向上的影响,最终找到一组权重让目标函数下降最快;图像角点检测里,基于梯度的方法统计像素灰度在 x、y 方向上的变化率,角点往往是梯度方向和幅值发生剧烈变化的点,这与基于轮廓曲线的方法思路完全不同——一个研究像素级变化率,一个研究几何轮廓。
这些例子表面上和GBDT无关,但底层的逻辑一致:只要有一个可微的度量函数,就能用梯度告诉系统"往哪个方向调整会更好"。GBDT把这种思想扩展到函数空间,所以叫"基于梯度的boosting",一点也不夸张。
6. 从一阶梯度到二阶梯度:XGBoost到底改了什么
6.1 牛顿方向:为什么二阶梯度能加速
一阶梯度下降只利用了损失函数的局部斜率,相当于只知道"该往哪走",不知道"该走多快"。牛顿法引入二阶导数,相当于同时估计了曲率,曲率大的地方步长小一点,曲率小的地方可以大步走。
在GBDT的函数空间视角下,第 $m$ 轮要加入的树 $h_m$ 也可以看成是一个增量方向。如果我们把损失函数在 $F_{m-1}$ 处做二阶泰勒展开:
$$L(y, F_{m-1} + h) \approx L(y, F_{m-1}) + g h + \frac12 H h^2$$
其中 $g$ 是一阶导,$H$ 是二阶导。对 $h$ 求最优,可以得到牛顿步长:
$$h = -\frac{g}{H}$$
这就是XGBoost引入二阶导的动机:用 $g/H$ 而不是只用 $g$,每轮更新更准确,可以用更少的树达到同样的训练目标。
6.2 XGBoost的目标函数展开与分裂增益
XGBoost在训练第 $m$ 棵树时,把损失函数对每个样本在 $F_{m-1}(x_i)$ 处展开到二阶,同时加入树复杂度惩罚项 $\Omega(h)$:
$$\sum_{i=1}^{N}\left[ L(y_i, F_{m-1}(x_i)) + g_i h(x_i) + \frac12 H_i h^2(x_i) \right] + \Omega(h)$$
其中:
$$g_i = \frac{\partial L(y_i, F)}{\partial F} \bigg|{F=F(x_i)}, \quad H_i = \frac{\partial^2 L(y_i, F)}{\partial F^2} \bigg|{F=F(x_i)}$$
分裂时,某个特征分裂点的增益可以写成:
$$\text{Gain} = \frac{1}{2} \left[ \frac{(\sum_{i \in L} g_i)^2}{\sum_{i \in L} H_i + \lambda} + \frac{(\sum_{i \in R} g_i)^2}{\sum_{i \in R} H_i + \lambda} - \frac{(\sum_{i \in P} g_i)^2}{\sum_{i \in P} H_i + \lambda} \right] - \gamma$$
这个公式在GBDT里是没有的,因为GBDT分裂时只考虑平方误差的减小,而XGBoost把一阶导、二阶导和模型复杂度统一在一个目标函数里。这也是它效果更好、训练更快的重要原因之一。
6.3 梯度直方图:工程上如何高效计算分裂点
LightGBM进一步利用梯度信息做了两个工程优化。第一个是单边梯度采样,核心思想是保留梯度大的样本,因为它们对信息增益的贡献更大;对梯度小的样本随机采样,但乘以一个权重来修正分布偏差。第二个是互斥特征捆绑,把互斥的特征绑在一起减少特征维度。
这些都不改变梯度提升的理论框架,只在工程层面加速分裂点的搜索。所以你会发现,理解了基于梯度的boosting推导,再去看XGBoost、LightGBM的文档,很多原本感觉像黑科技的设计会突然变得顺理成章。
7. 简化版GBDT的代码实现与实测心得
7.1 一个最小可运行的GBDT实现
理论推导讲再多,不如自己写一遍。这里我用numpy实现一个只支持平方损失的简化版GBDT,核心逻辑只有50行左右。
python复制import numpy as np
from sklearn.tree import DecisionTreeRegressor
class SimpleGBDT:
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3, min_samples_leaf=1):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.min_samples_leaf = min_samples_leaf
self.trees = []
self.init_pred = None
def fit(self, X, y):
self.init_pred = np.mean(y)
F = np.full(y.shape[0], self.init_pred, dtype=float)
for _ in range(self.n_estimators):
residual = y - F
tree = DecisionTreeRegressor(
max_depth=self.max_depth,
min_samples_leaf=self.min_samples_leaf
)
tree.fit(X, residual)
self.trees.append(tree)
F += self.learning_rate * tree.predict(X)
def predict(self, X):
F = np.full(X.shape[0], self.init_pred, dtype=float)
for tree in self.trees:
F += self.learning_rate * tree.predict(X)
return F
这个实现里我没做线性搜索,直接用固定学习率替代,实际效果已经很接近。如果你想换成自定义损失函数,只需要把 fit 里的 residual = y - F 改成对应损失函数的负梯度,再把叶子输出改成对应损失下的最优值。这就是为什么我前面花那么多篇幅推导损失函数,因为写代码时真正需要改的只有这两处。
7.2 用手写实现验证上面的推导
我用一个简单的模拟数据验证一下。构造一组带噪声的二次函数数据:
python复制rng = np.random.RandomState(42)
X = np.linspace(0, 6, 300).reshape(-1, 1)
y = 0.5 * X[:, 0] ** 2 - 2 * X[:, 0] + 3 + rng.normal(0, 1, size=300)
训练并绘图观察,你会发现第一棵树拟合的近似是数据整体均值附近的残差,后面每棵树逐步修正局部区域。把训练过程中的树数量增加,训练误差稳步下降,验证误差在某个点开始上升,这就是典型的偏差方差权衡。
我之前用这个手写版本做过一个实验:把学习率从1改成0.1,树的数量不变,训练损失会差很多;但把树数量同步增加5倍,两者的训练损失几乎持平,验证损失却差很多。这说明学习率不是凭空加的,它要求你用更多的树来换取更平滑的决策边界。
7.3 实测调参顺序与常见误区
在实际项目中,我调GBDT参数的顺序一般是:
- 先固定学习率,比如0.05,设定一个较大的树数量配合早停。
- 调树深,从3开始,每次加1,观察验证集表现。
- 调最小叶子样本数,控制过拟合。
- 最后调行采样和列采样比例,通常在0.6到0.9之间。
- 如果训练时间长,再考虑降低树数量、增大学习率或引入梯度直方图优化。
有一个常见误区是把GBDT的树数量调到几千甚至上万,却不配合早停。树太多时,即使每棵树贡献很小,最终模型也可能记住训练集中的噪声。我见过一个案例,学习率0.01、树数量5000,验证集损失在第2000棵时已经稳定,后面3000棵树纯属浪费时间。
还有个细节,对分类问题,初始预测不能用均值,而应该用 log(p/(1-p));预测概率要用 sigmoid 变换。很多人直接拿回归代码改分类,初始值没改,结果模型很差。这正好呼应第3节里推导对数损失时强调的:每一轮拟合的对象、初始值、叶子输出都要跟着损失函数变。
