系列 《LLM 时代的经典机器学习:只讲它在哪里重现》 第 6 / 11 篇
上一篇:SVM 与核方法——最大间隔、核技巧与 attention 的远亲下一篇:聚类——K-Means、DBSCAN 与「这批语料里有什么」
第四篇结尾留下一个问题:决策树方差极大、天生过拟合,怎么办?答案是很多棵树。两种组织方式:随机森林并行地训几百棵各不相同的树再投票——第一篇”集成只降方差”的直接应用;梯度提升则一棵接一棵地训,每棵树专门修正前面所有树加起来还没解释的部分(残差)。后者是至今表格数据上最强的模型(XGBoost、LightGBM、CatBoost 都是它),也是预训练数据工程里质量分类器的常用工具之一。最后算一笔账:给 15T token 打质量分,为什么用这些小模型而不是 LLM 自己。
全篇的核心问题是:
很多棵都过拟合的树平均起来为什么反而不过拟合?1 梯度提升”逐棵拟合残差”到底在做什么,为什么叫”梯度”?2 预训练数据过滤为什么用 fastText 与 GBDT 而不是 LLM?3
一、总览
1. 两种集成、一笔账
本文按”并行集成 → 顺序集成 → 它们在 LLM 工作里的位置”组织:
%% 两种集成方式:并行(bagging / 随机森林)与顺序(梯度提升)
flowchart LR
subgraph P["并行:随机森林"]
direction TB
D1[重采样 1] --> T1[树 1]
D2[重采样 2] --> T2[树 2]
D3[重采样 …] --> T3[树 …]
T1 & T2 & T3 --> V[投票 / 平均<br/>降方差]
end
subgraph S["顺序:梯度提升"]
direction LR
F0["F₀ = 均值"] -->|残差| G1[树 1]
G1 -->|残差| G2[树 2]
G2 -->|残差| G3[树 …]
G3 --> F["F = F₀ + η Σ 树<br/>降偏差"]
end
| 概念 | 一句话 | 本文里的数字 |
|---|---|---|
| bagging | 重采样训很多棵树,平均 | 一棵树测试 0.826 → 200 棵 0.905;预测方差 0.094 → 0.007 |
| 随机森林 | bagging + 每个节点只看部分特征 | 树间相关 0.59 → 0.53,测试 0.904 → 0.918 |
| 梯度提升 | 每棵树拟合前面的残差 | 3 棵深度 1 的树:MSE 0.211 → 0.070;手写 15 行与 scikit-learn 同量级 |
| 学习率 × 棵数 | 小步多棵更稳 | 学习率 1.0 第 24 棵到顶后下滑;0.1 到 0.915 |
| 特征重要性 | 每个特征贡献的不纯度下降 | 乳腺癌 30 个特征前 6 个占 91% |
| 算力账 | 给 15T token 打分 | 8B 模型 = 训练算力的 1/3;线性模型 0.004% |
2. 来龙去脉:从”一棵树不稳”到 Kaggle 的默认武器
| 年 | 谁 | 当时的问题 | 留下的东西 |
|---|---|---|---|
| 1984 | Breiman 等,CART | 单棵决策树能解释、但换几个训练点结构就全变(方差大) | 树成为后面一切集成的基本单元 |
| 1995–1997 | Freund & Schapire,AdaBoost | 能不能把很多”只比瞎猜好一点”的弱分类器合成一个强的 | boosting:顺序训练,每一轮加重上一轮分错的样本——第一个实用的”逐轮修正” |
| 1996 | Breiman,bagging | 树太不稳,怎么让它稳 | 重采样训很多棵取平均,方差降、偏差不变(第二章) |
| 2001 | Breiman,随机森林 | bagging 的树太像,平均效果有限 |
|
| 1999–2001 | Friedman,Gradient Boosting Machine | AdaBoost 为什么有效、能不能推到任意损失 | 把 boosting 解释成函数空间里的梯度下降:每棵树拟合负梯度(第四章);换 loss 只换梯度 |
| 2014–2016 | Chen & Guestrin,XGBoost | Higgs 玻色子等 Kaggle 比赛,GBM 太慢 | 二阶近似、正则、列采样、并行——2015 年 Kaggle 获胜方案里 17/29 用了它(第五章) |
| 2017–2018 | LightGBM(微软)、CatBoost(Yandex) | 亿级样本、高基数类别特征 | 直方图分桶、叶子优先生长;类别特征的有序目标编码 |
| 2022 | Grinsztajn 等,《Why do tree-based models still outperform deep learning on tabular data?》 | 深度学习横扫图像与文本,表格数据上呢 | 45 个数据集系统比较:中等规模表格数据上树的集成仍然领先(第六章) |
一条线看下来:bagging 和 boosting 解决的是两个不同的病。单棵树方差大——bagging 用”平均”治它;单个弱模型偏差大——boosting 用”逐轮修正”治它。第一篇的偏差-方差分解在这里不是理论装饰,它就是这两族方法的分界线。它们今天的位置:任何表格数据问题(风控、推荐排序、广告点击率、预训练数据质量过滤)的第一个强基线都是梯度提升树,第九章的案例是它最标准的一个考题。什么时候不该用:输入是图像、语音、原始文本这种要从像素 / 波形 / 字符里学特征的数据——树只会在给定的列上切,不会造特征,这是深度学习的地盘。
3. 本文的章节安排
| 章 | 主题 | 内容 |
|---|---|---|
| 二 | bagging |
|
| 三 | 随机森林 |
|
| 四 | 梯度提升 |
|
| 五 | 调参 |
|
| 六 | 特征重要性与表格数据 | 重要性怎么算;表格数据上 GBDT vs 神经网络 |
| 七 | 数据质量分类器的算力账 |
|
| 八 | 小分类器带回来的老问题 | 分布偏移、系统性误杀、阈值 |
| 九 | 案例:人口普查收入预测 |
|
| 十 | 本文小结 | |
| 十一 | 自测 | 六道题 |
二、bagging:很多棵树平均
1. 一棵树 vs 很多棵
第四篇的表格数据上一棵不限深度的树:训练 1.000、测试 0.826。bagging(bootstrap aggregating):从训练集里有放回地抽同样多的样本,训一棵树;重复 \(B\) 次;预测时 \(B\) 棵树投票。
“有放回地抽”(bootstrap 重采样)是什么意思:训练集有 5 个样本 {1, 2, 3, 4, 5},抽 5 次、每次抽完放回去再抽,可能得到 {3, 1, 3, 5, 2}——样本 3 被抽到两次、样本 4 一次都没抽到。每个样本每次被抽到的概率是 \(1/n\),\(n\) 次都没被抽到的概率是 \((1 - 1/n)^n\),\(n\) 大时趋于 \(1/e = 0.37\)——所以每棵树大约只见过 63% 的不同样本,另外 37% 对它来说是”没见过的”。这样得到的 \(B\) 份数据每份都略有不同,训出的 \(B\) 棵树也就略有不同——这正是我们要的。
| 棵数 | 1 | 2 | 5 | 10 | 20 | 50 | 100 | 200 |
|---|---|---|---|---|---|---|---|---|
| 测试准确率 | 0.829 | 0.841 | 0.870 | 0.886 | 0.899 | 0.901 | 0.905 | 0.905 |
每一棵树的训练准确率仍是 100%(各自背下了自己那份重采样数据),但 200 棵投票后测试从 0.83 升到 0.90。
2. 降的是方差
为什么?第一篇的分解:一棵树偏差小(容量够)、方差大(换几个训练点结构就变)。第一篇第六章那个骰子的比喻:一个骰子的结果在 1–6 乱跳,20 个骰子的平均稳稳落在 3.5 附近——平均 \(B\) 个方差为 \(\sigma^2\) 的独立估计,方差变成 \(\sigma^2 / B\)。树之间不完全独立(它们的重采样数据有大量重叠,像 20 个被胶水粘在一起的骰子),降不到 \(1/B\),但降得很多。直接量一下——换 20 批训练数据,看模型对测试样本预测概率的方差:
换 20 批训练数据:单棵树预测概率的方差 0.094;50 棵 bagging 的 0.007
方差降到 1/13。偏差没有变——每棵树仍是不限深度的树,平均之后能表达的边界形状与单棵树一样(右图边界仍是横竖线段拼的)。集成不改变模型的表达能力,只让它稳定。
三、随机森林
1. 再加一层随机
bagging 的树彼此还是太像:数据有 20 个特征,其中几个特别强,每棵树的根节点几乎都选它们切,树的上半部分长得差不多。随机森林(random forest)在每个节点只允许从随机抽的 \(m\) 个特征里选切分(默认 \(m = \sqrt{d}\)),逼树走不同的路:
| 每个节点可选的特征数 \(m\) | 测试准确率 | 树之间预测的平均相关 |
|---|---|---|
| 20(= bagging) | 0.904 | 0.593 |
| 10 | 0.917 | 0.581 |
| 4(默认 \(\sqrt{20}\)) | 0.918 | 0.530 |
| 2 | 0.909 | 0.477 |
| 1 | 0.895 | 0.374 |
\(m\) 越小,树彼此越不像(相关从 0.59 降到 0.37),平均后方差降得越多——但 \(m\) 太小每棵树本身太弱(连有用的特征都常抽不到),偏差升上来。\(\sqrt{d}\) 附近最好。相关低的估计平均起来方差降得多,这是随机森林比纯 bagging 好的全部理由。
2. 袋外估计
每棵树的重采样漏掉了约 37% 的样本(袋外,out-of-bag),这些样本对这棵树来说就是没见过的数据。用每个样本”没见过它的那些树”来预测它,得到一个不用切验证集的泛化估计:
300 棵、默认 max_features=√20≈4:测试 0.919;袋外(OOB)估计 0.913
随机森林几乎不需要调参(棵数越多越好、\(m\) 用默认),是”先跑一个看看”的默认模型。
四、梯度提升
1. 一步一步拟合残差
随机森林的树各自独立。梯度提升(gradient boosting)让树顺序地工作:第 1 棵树拟合数据,第 2 棵树拟合第 1 棵没拟合好的部分,第 3 棵拟合前两棵加起来还没拟合好的部分……用一个一维回归问题把每一步画出来(用深度 1 的树——只切一刀、预测是一个台阶——最容易看):
| 树的棵数 | 0 | 1 | 2 | 3 | 50 |
|---|---|---|---|---|---|
| 训练 MSE | 0.211 | 0.166 | 0.099 | 0.070 | 0.025 |
先用 4 个点手算两步,看清”拟合残差”是什么意思。四个样本的 \(y = 1, 2, 6, 7\)(按 \(x\) 从小到大排),学习率暂取 1:
| 步 | 当前预测 \(F\) | 残差 \(y - F\) | 这一步的树 \(h\)(深度 1:切一刀、两边各取残差均值) | 训练 MSE |
|---|---|---|---|---|
| 0 | 均值 4, 4, 4, 4 | −3, −2, 2, 3 | 6.5 | |
| 1 | 1.5, 1.5, 6.5, 6.5 | −0.5, 0.5, −0.5, 0.5 | 前两个 → −2.5,后两个 → +2.5 | 0.25 |
第 1 棵树不看 \(y\),只看残差 \((-3, -2, 2, 3)\):在中间切一刀,左边残差平均 −2.5、右边 +2.5,就是一个台阶;把台阶加到预测上,MSE 从 6.5 降到 0.25。第 2 棵树面对的是新残差 \((-0.5, 0.5, -0.5, 0.5)\),切在哪都只能再降一点——它拟合的是”前一棵树没做完的部分”。学习率 0.5 时第 1 步只加半个台阶(预测 2.75, 2.75, 5.25, 5.25),残差留得多一些,给后面的树慢慢修。
读下排:第 1 棵树面对的是”\(y\) 减去均值”,它切了一刀把左端那一撮低点分出来;第 2 棵面对的是剩下的残差,它在右边切一刀;每棵树只负责前面所有树加起来还没解释的部分。50 个台阶叠出一条曲线。
2. 为什么叫”梯度”
写成公式。模型是树的累加:
\[F_t(x) = F_{t-1}(x) + \eta\, h_t(x), \qquad F_0(x) = \bar y\]- \(F_t\):前 \(t\) 棵树加起来的预测;
- \(h_t\):第 \(t\) 棵树;
- \(\eta\):学习率——每棵树的贡献乘一个小系数。
第 \(t\) 棵树该拟合什么?我们想让 loss \(L = \sum_i \ell(y_i, F(x_i))\) 下降最快。第二篇的梯度下降是”对参数 \(w\) 求导、沿负梯度走”;这里换一个视角:暂时忘掉树,把模型在 \(n\) 个训练点上的 \(n\) 个预测值 \(F(x_1), \ldots, F(x_n)\) 本身当成 \(n\) 个可以自由调的参数。loss 对第 \(i\) 个预测值的导数 \(\partial \ell / \partial F(x_i)\) 告诉我们:把这个预测值调大一点,loss 会怎么变;沿负梯度 \(-\partial \ell / \partial F(x_i)\) 调,loss 下降最快。平方损失 \(\ell = \frac{1}{2}(y - F)^2\) 的负梯度是
\[-\frac{\partial \ell}{\partial F(x_i)} = y_i - F(x_i) = \text{残差}\]所以”拟合残差”就是”用一棵树去逼近负梯度”,再沿这个方向走一步(乘 \(\eta\))——这是第二篇的梯度下降,只不过参数不是一个向量 \(w\),而是函数 \(F\) 本身,每一步的”更新量”用一棵树表示。换 loss 只换负梯度:分类用第三篇的交叉熵,负梯度是 \(y - p\)(标签减预测概率——又是那个形式),树拟合它,累加的是 logit。
3. 十五行实现
def fit_gbdt(X, y, n_trees=100, lr=0.1, max_depth=2):
f0 = y.mean() # ① 初始预测:常数(均值)
pred = np.full(len(y), f0)
trees = []
for _ in range(n_trees):
resid = y - pred # ② 残差 = 平方损失的负梯度 −∂L/∂F
t = DecisionTreeRegressor(max_depth=max_depth, random_state=0).fit(X, resid) # ③ 用一棵浅树拟合残差
pred += lr * t.predict(X) # ④ 加进去,乘学习率
trees.append(t)
return f0, trees
def predict_gbdt(model, X, lr=0.1):
f0, trees = model
return f0 + lr * sum(t.predict(X) for t in trees) # ⑤ 预测 = 初值 + 所有树的累加
500 个样本、4 个特征、带交互项的非线性回归:
手写 GBDT(300 棵深度 3、lr 0.1)测试 MSE 0.4101;sklearn 0.4090
对比:一棵深度 8 的回归树 1.7368
手写版与 GradientBoostingRegressor 同量级(差别来自树切分平局时的处理),都比一棵深树好 4 倍。
五、调参
1. 学习率 × 棵数
| 学习率 | 最好测试准确率 | 在第几棵 | 600 棵时 |
|---|---|---|---|
| 1.0 | 0.898 | 24 | 0.891 |
| 0.3 | 0.914 | 497 | 0.911 |
| 0.1 | 0.915 | 137 | 0.907 |
| 0.03 | 0.911 | 274 | 0.911 |
学习率大:几十棵就到顶,然后开始过拟合下滑(每棵树的修正全额加进去,把噪声也学了)。学习率小:慢,但顶更高、更平。这与第二篇 SGD 的学习率是同一个权衡。实践:学习率 0.05–0.1,棵数用验证集早停(第一篇)——scikit-learn 的 staged_predict、XGBoost 的 early_stopping_rounds 都是为此。
2. 三个超参数与三个实现
梯度提升要调的核心是三个:棵数(早停定)、学习率(0.05–0.1)、树的深度(3–8,浅树;每棵树只需要捕捉一点点交互)。再加常用的正则:行采样(每棵树只用部分样本)、列采样(同随机森林)、叶子最少样本数。
| 实现 | 主要改动 | 什么时候用 |
|---|---|---|
| XGBoost(2014) | 二阶梯度(用 Hessian 定叶子值)、正则化的目标函数、缺失值自动处理、并行找切分 | 通用默认 |
| LightGBM(2017) | 直方图切分(特征分桶,快几倍)、按叶子生长(leaf-wise)、类别特征原生支持 | 大数据、特征多 |
| CatBoost(2018) | 有序目标编码处理类别特征(防泄漏)、对称树 | 类别特征多、不想调参 |
三者的模型是同一个——树的累加、拟合负梯度——差别在速度与工程细节。会用一个、看懂上面三个超参数即可。
六、特征重要性与表格数据
1. 特征重要性
树模型附带一个很有用的副产品:每个特征在所有分裂里贡献了多少不纯度下降(第四篇的 Gini),归一化后就是特征重要性。乳腺癌数据上:
测试准确率 0.942
worst perimeter 0.506
worst concave points 0.161
mean concave points 0.134
worst radius 0.050
worst texture 0.029
mean texture 0.026
前 6 个特征占重要性 91%
30 个特征里 6 个占了 91%。数据质量打分里这张表告诉你”困惑度、长度、符号比例、重复率”哪个在起作用——这是数据工程师调过滤器时最常看的东西。(它有偏向:高基数的特征、相关特征之间会分走重要性;更可靠的是 permutation importance——打乱一个特征看分数掉多少。)
2. 表格数据为什么是树的天下
在表格数据上——特征是数值与类别、样本几万到几千万、特征之间没有图像 / 文本那种局部结构——GBDT 至今是最强的默认选择。同一份表格数据、都不调参:
| 模型 | 测试准确率 | 训练耗时 |
|---|---|---|
| 梯度提升 300 棵 | 0.913 | 3.1 s |
| 随机森林 300 棵 | 0.919 | 0.3 s |
| MLP 两层 128 | 0.905 | 1.3 s |
| 逻辑回归 | 0.853 | 0.0 s |
树对特征的单调变换不敏感(不需要标准化)、天然处理缺失值与类别特征、对无关特征鲁棒、在小数据上不容易过拟合(相对深度网络);神经网络要调结构、学习率、正则、标准化才能追平,而树两三个参数就到位。LLM 工作里表格数据出现在:数据质量打分(特征是几十个统计量)、实验结果分析(哪些超参数组合好)、线上 A/B 的归因。会用 LightGBM 训一个模型、看特征重要性、调三个超参数即可。
七、数据质量分类器的算力账
1. 问题
预训练要从几十 T 原始文本里挑出高质量的部分。”高质量”由一个分类器判断——”这段像不像百科 / 教科书”。用什么模型?直觉是”用 LLM 自己判断最准”。算一笔账。
2. 账
给 15T token 打分。一个 token 过一个 \(N\) 参数的模型(前向一次)约 \(2N\) FLOP:8B 模型是 \(1.6 \times 10^{10}\),乘 15T 个 token 就是 \(2.4 \times 10^{23}\)。训练一个 8B 模型的算力约 \(6ND = 6 \times 8 \times 10^9 \times 15 \times 10^{12} = 7.2 \times 10^{23}\)(\(D\) 是训练 token 数):
| 打分模型 | 打分 FLOP | 占训练 8B 模型算力的 |
|---|---|---|
| 8B LLM 逐段打分 | \(2.4 \times 10^{23}\) | 33.3% ← 不可接受 |
| 1B 小 LLM | \(3.0 \times 10^{22}\) | 4.2% |
| BERT 级 1 亿参数 | \(3.0 \times 10^{21}\) | 0.42% |
| fastText / 线性模型 ~1M | \(3.0 \times 10^{19}\) | 0.004% |
用 8B 模型打分要花训练算力的三分之一。 换成 1 亿参数的模型是 0.4%,换成线性模型几乎为零。
3. 两级做法
所以实际做法是两级:用大模型给一小部分样本打标(几十万段),拿这些标签训一个小分类器,再用小分类器过全部语料。FineWeb-Edu 用 Llama-3-70B 给 45 万段打”教育价值”分(0–5),训一个小 embedding 模型加线性回归头,再过全部 15T;DCLM 用 fastText 做质量过滤。不是小模型效果更好,是只有它跑得起。
4. fastText 的形态
fastText 一类文本分类器就是词袋特征 + 线性分类器:把每个词(和相邻两个词组成的 bigram)映射成一个向量,整段文本的向量是它们的平均,上面接一个第三篇的 softmax 回归。scikit-learn 里等价的写法:
vec = TfidfVectorizer(sublinear_tf=True, min_df=2, ngram_range=(1, 2)) # ① 每段文本 → 几万维的词 / 词对计数(稀疏),TF-IDF 加权
Xtr = vec.fit_transform(train_texts)
clf = LogisticRegression(max_iter=3000, C=5).fit(Xtr, train_labels) # ② 第三篇的逻辑回归 / softmax 回归
TfidfVectorizer 做的是词袋计数再加权:一个词在这段里出现越多(TF,词频)权重越高,但在所有文本里都常见的词(”的”、”是”,IDF 低)权重被压低;ngram_range=(1, 2) 表示除了单个词还数相邻两个词组成的词对。几万个 n-gram 特征的线性模型,几千篇文本训练一秒钟。在”判断这段文本属于哪一类 / 像不像教科书”这件事上,它离 LLM 的差距远小于算力上的差距——第五篇说线性 SVM 活在 fastText 里,就是这里。
八、小分类器带回来的老问题
用小分类器过滤万亿 token,经典监督学习的每个问题也一并回来,只是规模大了一万倍:
- 分布偏移:训练标签是在几十万段样本上打的,这些样本是否代表全部语料?如果标注样本里没有代码、没有非英语,分类器对它们的判断是随机的(第一篇按组切的教训:验证集要像真实数据)。
- 系统性误杀(类别不平衡与偏见):分类器可能把某种语体——非英语、口语、诗歌、低资源领域——系统性判为低质量。每滤掉一类文本,模型就失去一种能力,而且这个损失在 benchmark 上不一定看得见。
- 阈值:分数切在哪里、滤掉多少——精确率与召回率的权衡(第十篇)。FineWeb-Edu 切在 3 分留下约 1.3T token,切在 2 分留下几倍多;这个决定没有标准答案,取决于你有多少数据、模型多大(Chinchilla 的 \(D / N \approx 20\) 告诉你至少要多少 token)。
数据工程师每天在做的,是经典监督学习在万亿 token 上的工程化——同样的模型、同样的失效方式、同样的评估工具。
九、案例:人口普查收入预测——表格数据的标准考题
问题与数据:UCI Adult(Census Income)——1994 年美国人口普查抽样 48,842 人,14 个特征,预测年收入是否超过 5 万美元(23.9% 是)。它是表格数据分类的标准考题,被几乎每篇集成方法的论文用过。它的”脏”是真实业务里的典型:5 个数值特征(年龄、教育年限、资本收益、资本损失、每周工时)+ 7 个类别特征(工作类别 8 类、婚姻 7 类、职业 14 类、家庭关系 6 类、种族 5 类、性别、国籍 41 类);工作类别和职业各缺约 2,800 个、国籍缺 857 个。
思路:按本篇的顺序——一棵树、一片森林、逐轮修正——在同一份数据上各跑一次,逻辑回归做对照。类别特征两种编码:逻辑回归要 one-hot(7 列变 100 列)、要标准化;树模型用序数编码(每个类别一个整数)直接吃,缺失当成一个类别。指标用 AUC 而不只是准确率——正类只有 24%,全判”≤50K”的准确率已有 76%(第十篇第四章)。
onehot = ColumnTransformer([("num", make_pipeline(SimpleImputer(), StandardScaler()), NUM),
("cat", OneHotEncoder(handle_unknown="ignore"), CAT)]) # 逻辑回归用
ordinal = ColumnTransformer([("num", "passthrough", NUM),
("cat", OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1), CAT)]) # 树用
models = [
("逻辑回归", make_pipeline(onehot, LogisticRegression(C=1.0, max_iter=2000))),
("一棵决策树(不限深度)", make_pipeline(ordinal, DecisionTreeClassifier(random_state=0))),
("随机森林(500 棵)", make_pipeline(ordinal, RandomForestClassifier(500, min_samples_leaf=2, n_jobs=-1))),
("梯度提升(500 轮,lr 0.05,早停)", make_pipeline(ordinal, HistGradientBoostingClassifier(
max_iter=500, learning_rate=0.05, early_stopping=True, validation_fraction=0.1))),
]
HistGradientBoostingClassifier 是 scikit-learn 里 LightGBM 式的实现(直方图分桶),第五章表里”XGBoost / LightGBM 各改了什么”的东西它都有一份;生产里换成 XGBoost / LightGBM 只是换一个 import。
效果(测试集 12,211 人):
| 模型 | 准确率 | AUC | 训练时间 |
|---|---|---|---|
| 基线:全部判 ≤50K | 0.761 | 0.500 | — |
| 逻辑回归(one-hot 后 100 列) | 0.854 | 0.905 | 0.1 s |
| 一棵决策树,不限深度 | 0.820 | 0.772 | 0.1 s |
一棵决策树,max_depth=6 |
0.855 | 0.890 | 0.1 s |
| 随机森林,500 棵 | 0.864 | 0.917 | 1.1 s |
| 梯度提升,默认 100 轮 | 0.874 | 0.929 | 0.7 s |
| 梯度提升,500 轮、lr 0.05、早停(停在 256 轮) | 0.875 | 0.930 | 2.2 s |
读这张表就是读本篇的前四章:
- 一棵不限深度的树是最差的(AUC 0.772,比逻辑回归低 0.13)——它把 36,000 个训练样本背了下来;限深到 6 层就回到 0.890。这是第二章开头那张”一棵树 vs 很多棵”的真实版。
- 500 棵这样的树平均,AUC 0.917——每棵都过拟合没关系,bagging 只降方差;随机森林还免费给了一个几乎不用调的模型(这里只设了
min_samples_leaf=2)。 - 梯度提升再高一截,0.930——它不是平均,是逐轮修正上一轮的错(第四章);256 轮早停,训练 2 秒。在表格数据上这个排序(GBDT > RF > 单树,线性在中间)几乎是定律,第六章的 Grinsztajn 等 2022 在 45 个数据集上验证过。
- 逻辑回归 0.905 不差——它输给树的 0.025 AUC 主要是特征交互(”已婚 × 高教育 × 高工时”这种组合),树天然会切、线性模型要手工造交互项;但它要 one-hot 出 100 列、要标准化、要补缺失,树模型直接吃原始列。
特征重要性怎么审计。第六章说的重要性这里用 permutation 版(把一列打乱后测试 AUC 掉多少,比不纯度版少一层偏向高基数特征的毛病):资本收益 −0.061、年龄 −0.057、教育年限 −0.031、婚姻状况 −0.026、家庭关系 −0.017 居前;sex −0.002、native-country 和 race −0.0007,几乎不起作用。这张表有两个用途:一是和常识对(收入靠资本、年龄、教育,说得通);二是审计——如果 sex 或 race 排在前面,模型就在学一种不该学的关系,上线前必须处理(去掉这列不够,因为别的列会替它——relationship 里的 Husband / Wife 就带着性别)。这是第八章”系统性误杀”的检查手段。
学习率 × 轮数的实物(第五章的调参,训练集内切 10% 做验证):
| 学习率 | 最佳轮数 | 最佳验证 AUC | 第 400 轮 |
|---|---|---|---|
| 0.3 | 80 | 0.9226 | 0.9123(掉头) |
| 0.1 | 222 | 0.9243 | 0.9224 |
| 0.03 | 400(还没到顶) | 0.9242 | 0.9242 |
学习率大:80 轮到顶,之后每加一棵树都在拟合噪声,AUC 掉头;学习率小:400 轮还在缓升,但更稳。三条线的顶差不到 0.002——这就是第五章的结论”学习率 0.05–0.1 + 早停”在真实数据上的样子:学习率决定你有多少轮的犯错余地,早停替你找到那一轮。
落地还差什么:这个数据是 1994 年的,所有数字只对 1994 年的美国有效——真正的收入模型每年要重训;race / sex 这类特征在信贷等受监管场景里不允许进模型,而且要检验模型对不同群体的错误率是否一致(第八章的”系统性误杀”在监管语境里叫公平性);AUC 0.93 之后要再往上,靠的不是换模型而是加特征(家庭收入、居住地、行业)——在表格数据上,梯度提升树已经把”给定这些列能学到的”学完了。
十、本文小结
- bagging:重采样训很多棵树取平均,一棵树 0.826 → 200 棵 0.905;预测方差 0.094 → 0.007(降 13 倍),偏差不变——集成不改变表达能力,只让它稳定。
- 随机森林 = bagging + 每个节点只在随机的 \(m\) 个特征里选切分:树越不像(相关 0.59 → 0.53)平均越有效,\(m = \sqrt{d}\) 最好 0.918;袋外估计免费给一个泛化数字;几乎不用调参。
- 梯度提升:\(F_t = F_{t-1} + \eta h_t\),每棵树拟合前面的残差——残差就是平方损失的负梯度,所以它是在函数空间里做梯度下降;换 loss 只换负梯度(分类:\(y - p\))。15 行手写与 scikit-learn 同量级,比一棵深树好 4 倍。
- 调参:学习率 0.05–0.1 + 棵数早停(学习率 1.0 第 24 棵到顶后下滑)+ 浅树 3–8;XGBoost / LightGBM / CatBoost 是同一个模型的三个工程实现。
- 特征重要性是调过滤器时最常看的表(乳腺癌 6/30 占 91%);表格数据上树的集成默认最强(RF 0.919 vs 不调参的 MLP 0.905),不用标准化、鲁棒、两三个参数到位。
- 算力账:给 15T token 打分,8B 模型 = 训练算力的 1/3、线性模型 0.004% → 两级做法:大模型标几十万段、小分类器过全部(FineWeb-Edu、DCLM)。fastText = 词袋 + 线性分类器。小分类器带回来的老问题:分布偏移、系统性误杀、阈值。
- 案例:Adult 48,842 人收入预测,AUC 一棵树 0.772 → 随机森林 0.917 → 梯度提升 0.930(逻辑回归 0.905);permutation 重要性做审计(
sex/race接近零);学习率 0.3 第 80 轮掉头、0.03 到 400 轮还在升,早停替你选轮数。
配套代码:本文全部数字与图由 classical-ml/06_ensembles_and_gradient_boosting.py(bagging / forest / boost_steps / boost_hand / lr / importance / tabular / budget 八个子实验)与 case_06_adult_income.py(第九章案例,首次运行下载 4 MB)产生,CPU 上两三分钟跑完。
十一、自测
-
决策树训练准确率 100%、测试 83%,随机森林训练也 100%、测试 92%。为什么训练准确率一样测试差这么多?
答案
两者都能背下训练集,但随机森林平均了 300 棵树,方差被压掉(本文实测 0.094 → 0.007),泛化更好;偏差没变。
-
随机森林为什么要在每个节点只看部分特征?\(m\) 太小会怎样?
答案
让树彼此更不相关,平均后方差降得更多;\(m\) 太小每棵树连有用特征都常抽不到,偏差升高(\(m = 1\) 时 0.895 < \(m = 4\) 的 0.918)。
-
梯度提升的第 \(t\) 棵树拟合的目标是什么?平方损失下它等于什么?log-loss 下呢?
答案
loss 对当前预测 \(F_{t-1}(x_i)\) 的负梯度;平方损失下是残差 \(y - F\),log-loss 下是 \(y - p\)。
-
学习率 1.0、600 棵树的梯度提升与学习率 0.1、600 棵,哪个更可能过拟合?为什么?
答案
1.0——每棵树的修正全额加进去,几十棵就把训练集连噪声学完,之后继续加树只会过拟合(本文 24 棵到顶后下滑);0.1 小步多棵,顶更高更平。
-
用 1B 模型给 30T token 打分,占训练一个 70B 模型(\(D = 15\text{T}\))算力的百分之几?
答案
\(2 \times 10^9 \times 30 \times 10^{12} = 6 \times 10^{22}\),训练 \(6 \times 70 \times 10^9 \times 15 \times 10^{12} = 6.3 \times 10^{24}\),约 1%。
-
质量分类器的训练标签来自 40 万段英文百科风格的样本。用它过滤一个含 30% 代码与 20% 中文的语料,会发生什么?
答案
分布偏移:代码与中文在训练标签里没有,分类器对它们的判断接近随机或系统性偏低,很可能把它们大量滤掉——模型失去这两种能力。
下一篇
前六篇的模型都需要标签。下一篇开始讲没有标签时能做什么:聚类——怎么知道一个几十 T 的语料里有哪些主题、各占多少。
-
一棵不限深度的树偏差小、方差大——换几个训练点结构就变。平均 \(B\) 棵各用重采样数据训的树,方差按近似 \(1/B\) 下降(实测 0.094 → 0.007),偏差不变;每棵树仍 100% 拟合自己的数据,但投票结果不再随某几个点乱跳——过拟合的表现(方差)被平均掉了。随机森林再让每个节点只看部分特征,树彼此更不相关,降得更多。详见第二章、第三章。 ↩
-
模型是树的累加 \(F_t = F_{t-1} + \eta h_t\)。把 \(n\) 个预测值 \(F(x_i)\) 看成参数,loss 下降最快的方向是负梯度 \(-\partial \ell / \partial F(x_i)\);平方损失下它恰好是残差 \(y_i - F(x_i)\)。所以”拟合残差”= 用一棵树逼近负梯度、再乘学习率走一步——是在函数空间里做梯度下降。换 loss 只换负梯度(分类:\(y - p\))。详见第四章。 ↩
-
不是效果更好,是只有它跑得起——给 15 T token 打分,用 8 B 模型的算力是预训练本身的 1/3,1 亿参数是 0.4%,线性模型几乎为零;所以是两级做法:大模型标几十万段,小分类器(fastText = 词袋 + 线性分类器,或 GBDT 吃几十个统计特征)过全部(FineWeb-Edu、DCLM 都这样)。代价是分布偏移与对少数语体的系统性误杀。详见第七章、第八章。 ↩
系列 《LLM 时代的经典机器学习:只讲它在哪里重现》 第 6 / 11 篇
上一篇:SVM 与核方法——最大间隔、核技巧与 attention 的远亲下一篇:聚类——K-Means、DBSCAN 与「这批语料里有什么」
本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/ensembles-random-forest-and-gradient-boosting.html)的前提下,欢迎各种形式的转载、翻译或商业引用。
COMMENTS
评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。