跳到正文
本地阅读 · GBDT、XGBoost 与 LightGBM

GBDT、XGBoost 与 LightGBM

机器学习 · 机器学习 · P0
本章目录

学完后,你应能独立完成

  • 手写多轮平方损失树桩 boosting
  • 区分收益残差、分类伪残差与 logit
  • 推导二阶叶权重与分裂增益

前置建议:决策树与随机森林 · Logistic 与分类决策

改变一个条件,观察发生什么

浏览器内即时教学计算;Python 实验结果另列于代码区。图形与数值一起核对。

01

从一个常数开始逐步修正

平方损失下 F₀取训练目标均值。每轮不重新替换旧模型,而是增加一棵修正树。学习率 η控制步幅,轮数控制路径长度,树容量决定每轮能表达什么。三者共同决定复杂度。

\[F_m(x)=F_{m-1}(x)+\eta h_m(x),\quad r_{im}=-\left.\frac{\partial L(y_i,F_i)}{\partial F_i}\right|_{F_{m-1}}\]
  1. 先算旧预测 F,求风险对训练点预测的负梯度。
  2. 用允许容量的树拟合这些伪残差。
  3. 叶值可以对原损失做线搜索或二阶求解,不总是残差均值。
  4. 把树乘 η 后加回旧预测,保存模型与验证结果。
02

平方残差是一种特例

若 L=(y−F)²/2,负梯度为 y−F。y=[0,0,2,2]、F₀=1,树桩叶值−1/+1,η=.1后预测[.9,.9,1.1,1.1],MSE从1到.81。下一轮重新算残差,不能一直拟合第一轮残差。

二元交叉熵用 F作为 logit,p=σ(F),负梯度为 y−p,不是 y−F或错分标记。分类累加的是 logit,再经过 sigmoid得到概率;不能把一个叶输出当概率直接往旧概率上加。

\[L_{binary}=\log(1+e^F)-yF,\quad -\partial L/\partial F=y-\sigma(F)\]
03

二阶展开把叶值变成二次优化

XGBoost 的简化推导采用损失求和。令 g_i、h_i为旧预测处的一阶与二阶导数,新树叶值 w_j,G_j=Σg_i、H_j=Σh_i。合并每叶样本项,再加入 λw_j²/2 与 γJ。

\[\widetilde L=\sum_j\left[G_jw_j+\tfrac12(H_j+\lambda)w_j^2\right]+\gamma J,\quad w_j^*=-\frac{G_j}{H_j+\lambda}\]
  1. 二阶 Taylor 忽略与新树无关的常数。
  2. 在叶内把 g_i、h_i求和,因为同叶共享 w_j。
  3. 对 w_j求导,令 G_j+(H_j+λ)w_j=0。
  4. 要求 H_j+λ>0;额外 L1、单调约束或步幅约束会改变简式。
04

分裂增益来自优化后的目标差

把父叶分为左右叶,用父评分减子评分并扣掉新增一片叶子的惩罚。注意1/2、父项的负号和 γ。学习率在更新时另外乘,不能随意塞进这个简化的叶评分。

\[\mathrm{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\]
  1. G_L=−2,H_L=2,G_R=2,H_R=2,λ=1。
  2. 左右叶权重为 +2/3、−2/3;父 G=0。
  3. γ=.1时 Gain=4/3−.1≈1.2333。
  4. min_child_weight约束 Hessian和,不无条件等于最少行数。
05

库优化与算法原理分开学

直方图把连续特征映射到 bins,累计梯度与曲率,减少候选扫描。Leaf-wise优先分裂全树当前收益最大的叶,可能形成不平衡深路径。num_leaves、max_depth与叶最小样本分别控制不同方面。

XGBoost也可以使用直方图和不同生长策略,不能把品牌名当作唯一算法配置。GOSS对梯度采样,EFB针对稀疏互斥特征;CatBoost的ordered机制也不能替代金融时间边界。这些作为扩展理解,先掌握负梯度与合法验证。

06

怎样比较 RF、Ridge 与 boosting

RF基树可独立训练,boosting后轮依赖前轮;两者表达和随机化不同,但不能据此宣布任何市场中的固定赢家。使用同标签、同信息集、同开发折、同评价集合,记录各自容量和搜索预算。

页面演示固定训练点和平方损失树桩,用于理解算法更新。训练 MSE下降不是未来增量证据。真实实验在综合案例里做日期选择;内部随机早停关闭,轮数由开发折选择。

从算法到一次真实运行

代码在本地 Python 中执行,HTML 内展示参考实现和本轮运行结果。请先独立完成练习,再展开完整参考文件。

boosting.py / 核心函数
def best_split(x, y, min_leaf=1):
    """One-feature squared-error stump. None means no positive valid gain."""
    x, y = np.asarray(x,float), np.asarray(y,float)
    if x.ndim != 1 or y.shape != x.shape or min_leaf < 1:
        raise ValueError("Expected matching 1D arrays and min_leaf>=1")
    if len(x)==0 or not np.isfinite(x).all() or not np.isfinite(y).all():
        raise ValueError("Expected nonempty finite data")
    order = np.argsort(x,kind="stable"); xs, ys = x[order], y[order]
    parent = np.sum((ys-ys.mean())**2)
    best = None; best_sse = parent
    for i in range(min_leaf, len(x)-min_leaf+1):
        if xs[i-1] == xs[i]:
            continue
        left, right = ys[:i].mean(), ys[i:].mean()
        sse = np.sum((ys[:i]-left)**2)+np.sum((ys[i:]-right)**2)
        if sse < best_sse-1e-12:
            best_sse = sse
            best = (float((xs[i-1]+xs[i])/2), float(left), float(right), float(sse))
    return best

def fit_boosting(x, y, rounds=10, rate=.2, min_leaf=1):
    """Square-loss additive stumps. Return initial value, trees, train path."""
    if rounds < 0 or rate < 0 or not np.isfinite(rate):
        raise ValueError("Invalid rounds or learning rate")
    x, y = np.asarray(x,float), np.asarray(y,float)
    initial = float(y.mean()); prediction = np.full(len(y),initial)
    trees = []; history = [prediction.copy()]
    for _ in range(rounds):
        residual = y-prediction
        split = best_split(x,residual,min_leaf)
        if split is None:
            tree = (None,float(residual.mean()),float(residual.mean()))
            update = np.full(len(y),residual.mean())
        else:
            threshold,left,right,_ = split
            tree = (threshold,left,right)
            update = np.where(x<=threshold,left,right)
        prediction += rate*update
        trees.append(tree); history.append(prediction.copy())
    return initial, trees, np.asarray(history)

def predict_boosting(x, initial, trees, rate):
    prediction = np.full(len(x),initial,dtype=float)
    for threshold,left,right in trees:
        prediction += rate*(left if threshold is None else np.where(x<=threshold,left,right))
    return prediction
下载本节 .pypython boosting.py依赖版本见首页,示例只使用合成数据。
查看本轮本地运行记录 · 2026-10-12
{
  "initial": 3.5,
  "first_tree": [
    4.5,
    -2.75,
    2.75
  ],
  "mse_by_round": [
    9.25,
    6.527500000000001,
    4.7851,
    3.6699640000000002,
    2.78081728,
    2.1451337727999995,
    1.6245676048383992,
    1.2552452499044056,
    0.9618954167732037,
    0.7553603517911276,
    0.580139255192635,
    0.45205912245523483,
    0.3523161641586665
  ],
  "final_prediction": [
    0.792553593565472,
    0.792553593565472,
    1.114787551909744,
    1.9844843857383154,
    4.87903177633569,
    4.87903177633569,
    5.768290121010318,
    7.789267201539297
  ],
  "constant_feature_prediction": [
    3.0,
    3.0,
    3.0
  ]
}

先回答,再看提示和答案

Q01

所有 GBDT 都拟合 y−F 吗?

一级提示

先对所用预测参数求导。

参考答案与条件

只有相应平方损失是这个普通残差。交叉熵对 logit 的伪残差是 y−p,其他损失按负导数求。

Q02

λ=1,G=−2,H=2 的叶权重多少?

一级提示

先写出目标、信息集和所需条件。

参考答案与条件

−G/(H+λ)=2/3;最终更新还要乘学习率。

Q03

把 learning_rate减半,不加轮数,一定泛化更好吗?

一级提示

先写出目标、信息集和所需条件。

参考答案与条件

不一定,可能总修正不足而欠拟合。需要共同评价轮数、容量与预算。

把知识迁移到一个新任务

不调用库,写多轮树桩 boosting,保存每轮预测并在新 x上重新累加。检查无合法分裂时常数叶、未排序输入和 η=0。再手推一组分类负梯度与二阶 Gain。

这里保存学习笔记,不在浏览器执行 Python。A/B/C 由你按独立完成程度填写。

参考与进一步阅读

在线文档可能比本地固定版本更新;API 行为以已验证代码和版本记录为准。

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。