基础 · 08

决策树与集成学习

矩阵切空间用斜线,决策树只用横竖切。把弱树堆成森林、把残差喂给下一棵——表格数据冠军的几何直觉。

16 min read

另一种画法:轴对齐切分

前面的 SVM线性模型 在做同一件事:画一条方向任意的超平面,把空间切成两半。决策树走了一条完全不同的路:

它只用平行于坐标轴的刀。

每一刀要么水平、要么垂直。切完一刀,左右两块继续被切,直到每块里都是同一类——决策边界由此长成一片方框拼图

这听起来像在自找麻烦——为什么放弃斜线?回报是三个 SVM/线性模型给不了的好处:

  • 对特征尺度免疫。身高用米还是厘米对树没区别——它只关心「大于还是小于阈值」,不依赖任何距离度量。
  • 天生可解释。每个内部节点就是一句话:「年龄 > 30?」。整棵树画出来是流程图,业务人都能看懂。
  • 类别特征和缺失值不用预处理。「学历是博士?走左边」。「这一格是空的?走我指定的那边」。SVM/神经网络在这两件事上都得先做 one-hot 和插值。

代价也写在脸上:边界永远是方框。如果真实分界线是 y=xy = x 这种斜线,决策树得用很多小方块去逼近它——不是不能,而是不优雅。

一刀切在哪里

要把空间切好,每一刀都要回答两个问题:切哪个特征?阈值是多少?

目标是让两边尽量「纯」——一边几乎全是 A 类,另一边几乎全是 B 类。完全纯的话,这一刀就把不确定性彻底解决了;如果切完两边还是各占一半,这刀就白切。

度量「纯度」有两种主流指标:

基尼系数(Gini)

Gini(S)=1kpk2\mathrm{Gini}(S) = 1 - \sum_k p_k^2

它就是「从这个节点里随机抽两个样本,类别不同的概率」。全是一类时为 0,均匀混合时最大。

信息增益:基于 上一篇 讲过的熵,切完两边的加权熵相对切前熵的下降量:

IG=H(S)jSjSH(Sj)\mathrm{IG} = H(S) - \sum_j \tfrac{|S_j|}{|S|} H(S_j)

两种指标差别不大,但 Gini 不用算 log\log,CART 和 scikit-learn 都默认用它。

连续特征怎么切?把样本按这个特征排序,相邻两值之间取中点,每个中点都试一次 Gini,选下降最大的那个。排序后能增量更新统计量,实际效率比想象的快得多。

每一刀都贪心地选「让 Gini 下降最多」的切法,然后在两边递归——这就是整个建树算法。

24 片叶子准确率 93%
x₁x₂

拖动滑块调整树深度。注意数据中有少量「噪声点」(对面阵营的离群点)。深度加大时,树会为它们切出细碎的小方块——这就是过拟合的几何直觉。

交互 · 决策树深度 vs 决策边界复杂度

把深度从 1 推到 6,看决策边界从一刀粗暴二分,逐步细化到能为每个噪声点单独划一格。这个过程让人很爽——也直接通向下一节的问题。

深树为什么会过拟合

如果不加限制,决策树会一直切到每个叶子只剩一个样本。训练准确率 100%,但只要换一批新数据,方框拼图里那些为单个噪声点切出来的小格子立刻露馅。

回到泛化篇的偏差-方差权衡:深树是方差极高的模型。它对训练集里每一颗小石子都过度反应。上面那个交互拉到深度 5、6,你会看到红色的「敌方阵营」噪声点周围被切出独立小框——这就是过拟合的几何形态。

两种思路对抗它:

预剪枝在生长过程中提前停止。限制最大深度、限制节点最小样本数、限制叶子最小样本数。简单粗暴,但容易停得太早——可能错过后面的好分裂。

后剪枝先让树长满,再从下往上砍。最经典的是代价复杂度剪枝

Cα(T)=tleavesNtGini(t)+αTC_\alpha(T) = \sum_{t \in \text{leaves}} N_t \cdot \mathrm{Gini}(t) + \alpha\,|T|

T|T| 是叶子数,α\alpha 是「复杂度税」。α\alpha 越大,对叶子数惩罚越重,越偏好简单的树。用交叉验证扫不同 α\alpha,选验证集表现最好的子树。

但即便剪了枝,单棵树仍然是个高方差的模型——同样的数据集只要随机抽样 80% 重新训练,长出来的树都可能完全不一样。这个不稳定性反过来变成了集成方法的灵感。

Bagging 与随机森林

单棵树方差大,不稳定。但**「不稳定」恰好是集成方法最想要的东西**——多个不同的弱模型一平均,方差会下降,而偏差几乎不变。这就是数学上「相关性低的多个估计的平均,方差按 1/B 缩」的直接应用。

Bagging(Bootstrap Aggregating)做的事就一句话:

  1. 从训练集有放回抽样 BB 次,得到 BB 个子集——每个子集大约覆盖原始数据的 63%。
  2. 在每个子集上独立训一棵树。
  3. 预测时,BB 棵投票(分类)或取平均(回归)。

随机森林在 Bagging 之上又加了一层去相关:每次分裂时,不让树看所有特征,只随机给它 d\sqrt{d} 个候选。这强迫不同的树在不同的特征上下功夫,相关性进一步下降,集成效果进一步上升。

随机森林在工业界有个外号叫「baseline 之王」——超参数几乎不用调,对噪声鲁棒,几行代码就能跑出能交差的结果。

下面这个演示故意把每棵「树」退化成只切一刀的 stump——这样能更干净地看出「集成」本身的力量,而不是单棵树自己的力量:

训练准确率: 73%1 棵决策树桩
1100

XOR 数据——对角同类。单棵树只能画一刀,完全搞不定。 随着树的增多,边界从锯齿拼图逐渐逼近真实结构。这就是集成的力量。

颜色越深代表模型越确信,黑色的暗带代表模型不确定的决策边界(概率=0.5)。

交互 2 · Bagging stump 集成:从 1 棵到 100 棵的决策边界

数据是一片圆形分布——内圈是一类,外圈是另一类。一根 stump 永远只能切一条横线或竖线,画不出圆。但当你把 100 根 stump 的判决叠起来投票,每根 stump 都从自己的角度切下一刀,叠成一片多边形拼出来的圆——曲线竟然被一堆横竖切组合出来了。这就是 bagging 的魔法:单棵弱、整体强;单棵粗糙、整体平滑。

Boosting:每棵树修前一棵的错

Bagging 的哲学是「让一群独立的弱模型互相抵消噪声」。Boosting 完全相反:

串行训练。每一棵新树都专门去修前面所有树的错误。

最早的 AdaBoost 用样本权重实现这件事——上一棵分错的样本权重调大,下一棵会重点关照它们。但真正改变 ML 工业界的,是它的现代版:GBDT

GBDT 把梯度下降的思想从参数空间搬到了函数空间

Fm(x)=Fm1(x)+ηTm(x)F_m(x) = F_{m-1}(x) + \eta \cdot T_m(x)

每棵新树 TmT_m 拟合的是当前预测的负梯度。对最常见的平方损失,负梯度恰好等于残差 yFm1(x)y - F_{m-1}(x)——所以 GBDT 也常被描述成「每棵树拟合前面所有树的残差」。换成 logistic 损失、Huber 损失,「残差」自动换成对应的负梯度,框架一字不改。

XGBoost 是 GBDT 的工程化巅峰。它的三个关键改进,都能映射回我们在前面几篇见过的直觉:

  • 正则化的目标函数 L(yi,y^i)+m(γTm+12λwm2)\sum L(y_i, \hat y_i) + \sum_m \big(\gamma|T_m| + \tfrac12 \lambda \|w_m\|^2\big)。叶子数和叶子权重都被惩罚,对应 线性模型 里熟悉的 L1+L2。
  • 二阶泰勒展开。GBDT 只用一阶梯度,XGBoost 顺手用 Hessian 信息找更准的分裂——同样的思想在 微积分篇 的二阶方法里讲过。
  • 列采样 + 近似分裂。从随机森林借来的特征采样 + 用分位数粗略找分裂点,让算法能在十亿级样本上跑。

为什么 2026 年了,表格数据上 XGBoost 仍打败几乎所有深度模型?2022 年 Grinsztajn 等人的论文 Why do tree-based models still outperform deep learning on tabular data? 给出了系统的答案:神经网络对不相关特征过分敏感(它会努力去拟合噪声维度),而树模型每一步分裂都在做隐式的特征选择——根本不看那些没信息量的列。表格数据天然异质、稀疏、含大量无关特征——这正好是树模型主场。

这个想法在前沿里