观文听傑

返回

上一篇构造了一棵决策树:每个节点贪心选择最能降低不纯度的特征与阈值。它能表达非线性规则、推理很快,但训练数据只改动几个样本,根节点就可能换掉,后续规则也随之重排。

这是一类典型的高方差(High Variance)学习器。把树剪得更浅可以降低方差,却也可能丢掉真实的细粒度规律。有没有办法保留许多较深的树,同时让最终预测不那么依赖某一份训练样本?

随机森林(Random Forest)的答案是:制造许多“各不相同但还算有用”的树,再平均它们的预测。 本文只聚焦三件紧密相连的事:自助采样怎样改变每棵树的数据、特征子采样怎样降低树之间的相关性,以及袋外样本怎样提供无需额外切分的诊断信号。

01 随机森林不是“把同一棵树复制很多次”#

如果 MM 棵树看到完全相同的数据和特征,并用相同算法训练,它们通常会得到相同规则;复制再多次,投票也没有新增信息。

随机森林给每棵树注入两类随机性:

  1. 自助采样(Bootstrap Sampling):每棵树从 NN 个训练样本中有放回地抽取 NN 次;有些样本重复,有些没有被抽中。
  2. 特征子采样(Feature Subsampling):每个节点只随机查看部分特征,再从其中选择最佳分裂。

完整数据流是:

X_train [N,D] + y_train [N]

       ├─ bootstrap #1 + 每节点随机特征 ─► Tree 1
       ├─ bootstrap #2 + 每节点随机特征 ─► Tree 2
       ├─ bootstrap #3 + 每节点随机特征 ─► Tree 3
       │                         ...
       └─ bootstrap #M + 每节点随机特征 ─► Tree M

X_query [Q,D]

       ├─► proba_1 [Q,C] ─┐
       ├─► proba_2 [Q,C] ─┼─► 按树平均 ─► forest_proba [Q,C]
       └─► proba_M [Q,C] ─┘                    │ argmax

                                         prediction [Q]
text

每棵树仍执行上一篇的不纯度下降搜索;森林改变的是训练样本与每个节点可见的候选特征,最后再聚合预测。

02 自助采样怎样制造不同训练集?#

假设训练集只有 4 个样本,编号为 [1,2,3,4]。每棵树有放回抽 4 次:

抽到的样本编号重复样本未抽中的样本
Tree 1[1,1,2,4]13
Tree 2[2,3,3,4]31
Tree 3[1,2,2,3]24

同一编号可以出现多次,因为每次抽取后都放回。每棵树看到的经验分布不同,于是容易在不同位置分裂。

对某个固定样本,一次抽取没有选中它的概率是 11/N1-1/N;连续抽 NN 次仍未选中的概率为:

(11N)NNe10.368\left(1-\frac1N\right)^N\xrightarrow[N\to\infty]{}e^{-1}\approx0.368

因此,大样本下每棵树平均约有 36.8% 的训练样本没有被抽中。这些样本叫袋外样本(Out-of-Bag Samples,OOB);被抽中的不同样本比例约为 63.2%,不是说树只执行了 0.632N0.632N 次抽取——重复项仍占据训练权重。

03 多棵树究竟怎样投票?#

分类森林通常平均成员树的类别概率,再取最大值。假设三个树对同一查询点给出正类概率:

Tree 1: P(y=1|x) = 1.0
Tree 2: P(y=1|x) = 0.0
Tree 3: P(y=1|x) = 1.0
text

森林结果为:

p^(y=1x)=1+0+13=23\hat p(y=1\mid x)=\frac{1+0+1}{3}=\frac23

阈值为 0.5 时预测正类。真实树的叶节点未必纯净,例如三个概率也可能是 [0.8,0.4,0.7],森林平均为约 0.633。

CC 类分类:

p^c(x)=1Mm=1Mp^m,c(x)\hat p_c(x)=\frac1M\sum_{m=1}^{M}\hat p_{m,c}(x) y^(x)=argmaxcp^c(x)\hat y(x)=\arg\max_c\hat p_c(x)
  • MM:树的数量;
  • p^m,c(x)\hat p_{m,c}(x):第 mm 棵树给类别 cc 的叶节点比例;
  • p^c(x)\hat p_c(x):森林平均后的类别概率。

平均能削弱某一棵树因噪声产生的极端预测,但不能自动保证概率校准。所有树共享同一训练来源且叶节点很小时,0.9 仍未必对应真实事件 90% 的发生率。

04 为什么平均能降低方差?#

先把每棵树对某个目标的预测看成方差为 σ2\sigma^2 的随机变量。若树之间彼此独立,MM 棵树均值的方差是:

Var(fˉ)=σ2M\operatorname{Var}(\bar f)=\frac{\sigma^2}{M}

但真实森林中的树共享数据,预测相关。若任意两棵树的相关系数近似为 ρ\rho

Var(fˉ)=ρσ2+1ρMσ2\operatorname{Var}(\bar f) =\rho\sigma^2+\frac{1-\rho}{M}\sigma^2

这个式子揭示两个不同旋钮:

  • 增大 MM 会缩小第二项,但无法消除由相关性留下的 ρσ2\rho\sigma^2
  • 降低树之间的相关性 ρ\rho,才能继续压低共同波动。

自助采样改变每棵树的数据;特征子采样进一步迫使树寻找不同规则。它们不是为了让单棵树更准确,而是在“成员树足够强”和“成员之间不要太相似”之间做折中。

05 特征子采样为何必须发生在每个节点?#

假设有 100 个特征,其中一个强特征几乎总能带来最大不纯度下降。若每棵树的每个节点都查看全部特征,许多树会在根节点选择同一特征,随后结构高度相似;Bootstrap 带来的差异可能不够。

分类森林常在每个节点随机查看约 D\sqrt D 个特征:

Tree 1 根节点候选:{x₂, x₇, x₉}  ─► 选 x₇
Tree 2 根节点候选:{x₁, x₄, x₈}  ─► 选 x₁
Tree 3 根节点候选:{x₃, x₇, x₁₀} ─► 选 x₃
text

关键是“每个节点重新抽候选特征”,不是每棵树只抽一次固定列子集。子树的不同节点仍可能使用全部 DD 个特征中的不同部分。

max_features单节点候选数常见影响
NoneDD单树较强,但树更相似
'sqrt'D\sqrt D分类森林常用折中
很小的整数或比例更少相关性下降,但节点可能错过有效特征
交互判断:max_features 越小,森林一定越好吗?

不一定。它可能降低树间相关性,却也让每棵树更弱。如果大多数节点看不到任何有用特征,偏差会明显上升。应在开发数据上结合准确性、方差和成本验证。

06 用四个样本手算 OOB 预测#

回到前三棵树的抽样结果:

Tree 1 没见过样本 3
Tree 2 没见过样本 1
Tree 3 没见过样本 4
text

袋外估计(OOB Estimate)只让“训练时没见过该样本”的树为它预测:

样本可参与 OOB 预测的树正类概率
1Tree 20.2
3Tree 10.8
4Tree 30.6
2本例没有NaN / 不可估计

若样本 1 的真实标签是 0,OOB 概率 0.2 对应预测正确;样本 3 的真实标签是 1,概率 0.8 也正确。样本 2 在这个极小森林中被每棵树抽到,无法得到 OOB 预测。

树数量足够多时,大部分样本都会被若干棵树留在袋外。OOB 分数可以作为快速诊断,尤其在数据宝贵、不想额外划出验证集时有用。

但 OOB 不是测试集:你若反复根据 OOB 结果选择特征、超参数和数据清洗规则,它就参与了模型开发。最终泛化结论仍应由封存测试集给出;时间序列、分组数据等不满足普通 Bootstrap 结构的任务也不能机械依赖 OOB。

07 Bagging 训练的伪代码#

训练阶段每棵树可以相对独立地构建,因此易于并行;推理时也可以分树并行,但最终必须归并所有成员输出。

08 用当前 scikit-learn API 落地#

截至本文写作时,scikit-learn 1.9 的 RandomForestClassifier 默认 n_estimators=100max_features='sqrt'bootstrap=True。以下代码显式设置关键参数,避免默认值或实验意图含糊:

重要 API 与属性:

  • n_estimators:森林中的树数量;增加它通常让结果更稳定,也线性增加模型大小和计算;
  • bootstrap=True:每棵树使用有放回样本;oob_score 仅在此时可用;
  • max_samples:控制每棵树抽取多少次,默认为与训练集样本数相同;
  • max_features='sqrt':每个节点随机候选特征数约为 D\sqrt D
  • oob_score_:默认以准确率计算的袋外总体分数;当前 API 也允许给 oob_score 传入指标函数;
  • oob_decision_function_:每个训练样本的 OOB 类别概率,形状 [N,C];树太少时某些行可能含 NaN
  • estimators_:已训练成员树列表;estimators_samples_ 可查看各树抽中的训练索引;
  • n_jobs=-1:让 fitpredictdecision_pathapply 在树之间使用可用处理器并行。

scikit-learn 1.9 的随机森林还原生支持数值特征中的 NaN 路由:训练时学习缺失值应走左右哪一侧。不过这不等于缺失机制已被正确建模;线上缺失率、缺失含义和数据管道仍必须单独监控。

09 OOB、交叉验证与测试集如何分工?#

评估来源每个样本由谁预测适合用途主要限制
OOB没抽到该样本的树快速监控、减少额外切分只适用于 Bootstrap 语义;会参与调参
交叉验证当前折未用该样本训练的整个森林比较超参数、估计波动计算成本约乘折数
独立测试集完全冻结方案后训练的最终模型一次最终验收不能反复查看后继续修改

OOB 与交叉验证结果不一致时,先检查:样本是否有时间或群组结构、类别是否极不平衡、树数量是否足够、两者使用的评分指标是否相同。不要只选择数值更好看的那一个。

10 怎样判断树的数量够不够?#

增加树数量一般不会像增加单树深度那样直接加剧过拟合,但收益会递减。可以观察 OOB 指标或固定验证集指标随树数量是否稳定:

tree_counts = [50, 100, 200, 500, 1000]

for count in tree_counts:
    model = RandomForestClassifier(
        n_estimators=count,
        max_features='sqrt',
        min_samples_leaf=2,
        bootstrap=True,
        oob_score=True,
        n_jobs=-1,
        random_state=42,
    )
    model.fit(X_train, y_train)
    print(count, model.oob_score_)
python

记录的不应只有分数,还要包括模型文件大小、训练时长、P50/P99 推理延迟和峰值内存。当曲线已稳定而成本继续线性增长时,再增加树没有工程收益。

warm_start=True 可以在后续 fit 中保留旧树并增加 n_estimators,适合逐步观察树数;它不会让旧树用新超参数重训,也不是任意增量学习接口。正式交叉验证中更清晰的做法通常是独立拟合各候选模型。

11 特征重要性为何容易被误读?#

feature_importances_ 汇总所有树中某特征带来的加权不纯度下降。它计算快,但有两个常见陷阱:

  1. 可选切分点多的连续或高基数特征更容易偶然获得大下降;
  2. 两个高度相关特征可以互相替代,重要性会被分散或随机偏向其中一个。

更可靠的补充是只在验证集上做置换重要性(Permutation Importance):随机打乱一列,观察指标下降多少。

即使置换重要性很高,也只能说明模型在当前验证分布下依赖该特征,不代表特征对真实结果具有因果作用。

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

  1. 以为每棵树只随机一次特征。 max_features 在每个节点生效;检查成员树的 tree_.feature 可看到不同路径使用不同列。
  2. 关闭 Bootstrap 却打开 OOB。 oob_score 需要 bootstrap=True,否则没有“未被抽中”的定义。
  3. 树太少就相信 OOB 概率。 先检查 oob_decision_function_ 是否含 NaN 以及每个样本有多少 OOB 成员。
  4. 只增加树,不控制单树叶子。 平均能降方差,却不能修复泄漏、无意义 ID、错误标签或极端小叶产生的不可靠概率。
  5. 嵌套并行耗尽资源。 外层交叉验证和森林都设 n_jobs=-1 可能造成 CPU 与内存争用;只让一层负责并行。
  6. 把 OOB 当最终测试。 OOB 被反复用于决策后就是开发信号,仍需封存测试集。
  7. 部署时只测平均延迟。 500 棵深树会带来模型体积、缓存未命中和尾延迟;同时测 P99 与并发吞吐。
  8. 类别失衡只看默认 OOB 准确率。 改用适当指标、class_weight 或独立验证,并检查每类召回率。

最小诊断代码:

assert X_train.ndim == 2 and y_train.ndim == 1
assert len(forest.estimators_) == forest.n_estimators
assert forest.oob_decision_function_.shape == (
    X_train.shape[0], len(forest.classes_)
)

depths = np.array([tree.get_depth() for tree in forest.estimators_])
leaves = np.array([tree.get_n_leaves() for tree in forest.estimators_])
print('depth median/max:', np.median(depths), depths.max())
print('leaves median/max:', np.median(leaves), leaves.max())
python

若训练分数接近 1、OOB 与验证都明显较低,优先调大 min_samples_leaf、限制深度或改善数据;若 OOB 很好而时间切分验证很差,优先怀疑分布漂移或划分结构,而不是继续增加树。

13 它与相近集成方法有什么区别?#

方法样本随机性特征随机性树之间关系主要目标
单棵决策树通常看全部特征只有一棵得到可追踪规则
Bagging TreesBootstrap通常看全部特征并行、独立训练通过平均降低方差
随机森林Bootstrap每节点随机子集并行、独立训练降低方差并降低树间相关性
Extra Trees可选 Bootstrap每节点随机子集且阈值更随机并行进一步随机化、常更快
Gradient Boosting通常非 Bootstrap 核心依实现而定后一棵拟合前一轮残差/梯度顺序降低偏差

随机森林的成员树可以并行训练;Boosting 的树有顺序依赖,不能把它理解成“另一种投票森林”。随机森林通常是表格数据的强基线,但面对高维稀疏线性问题、需要平滑外推的回归、极严格模型体积或延迟约束时,线性模型、专门梯度提升实现或更紧凑模型可能更合适。

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

  1. 随机森林让每棵树看到不同的 Bootstrap 样本,并在每个节点随机限制候选特征。
  2. 平均多棵高方差树能降低独立波动;树间相关性越高,继续增加树的收益越有限。
  3. 特征子采样的作用是降低成员相关性,不保证单棵树更准确;max_features 过小也会增加偏差。
  4. 每棵树约有 36.8% 的样本位于袋外,可用于 OOB 诊断,但 OOB 参与调参后不能替代最终测试集。
  5. 树数量要结合指标稳定性、模型大小、训练成本和尾延迟选择;特征重要性只能说明模型依赖,不能证明因果。

15 思考题与小练习#

练习 1:计算一次 Bootstrap 的 OOB 集合

训练编号为 [1,2,3,4,5],某棵树抽到 [2,2,5,1,2]。不同袋内样本是 {1,2,5},袋外集合是 {3,4};虽然只出现 3 个不同样本,训练序列仍有 5 个位置。

练习 2:相关性为何决定平均上限?

σ2=1,ρ=0.2,M=100\sigma^2=1,\rho=0.2,M=100,均值方差为 0.2+0.8/100=0.2080.2+0.8/100=0.208。把树从 100 增到 1000 只能降到约 0.201;降低相关性比继续堆树更有潜力。

练习 3:比较 OOB 与时间验证

在带日期的数据上同时计算随机森林 OOB 分数和“过去训练、未来验证”的分数。若 OOB 明显更高,解释随机 Bootstrap 如何混合时间分布,以及为何线上预期应更相信时间验证。

相关工作#

16 下一篇预告#

随机森林让许多树并行、独立地生长,再通过平均降低方差。下一篇将转向另一条集成路线:让后一棵浅树专门修正前一轮仍然犯错的方向,建立梯度提升树的逐步加法模型。

多棵不稳定的树为何能更可靠?随机森林的 Bagging、特征子采样与 OOB 估计
https://zwjcode.cn/blog/random-forest-bagging-oob-feature-subsampling
作者
发布于 2026年8月22日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。