Bagging & Random Forest —— 随机森林
前言
在上一篇文章中,我们深入探讨了决策树——这个可解释性最强、最贴近人类决策思维的机器学习模型。但决策树有一个致命的弱点:它太“脆弱”了——训练数据的微小变化可能导致一棵完全不同的树,这种高方差(High Variance) 特性让决策树在测试集上的表现常常不稳定。
那么,有没有办法既保留决策树的优点(可解释性、非线性建模能力),又克服它的缺点(过拟合、高方差)呢?
集成学习(Ensemble Learning) 给出了答案:“众人拾柴火焰高” ——与其相信一棵树,不如种一片森林。
Bagging(Bootstrap Aggregating) 和随机森林(Random Forest) 正是集成学习思想最经典的实践。Bagging由Leo Breiman于1996年提出,而随机森林则是Breiman在2001年将Bagging与Ho提出的随机子空间方法相结合的产物。
这篇文章,我们将从Bootstrap自助采样出发,一步步理解Bagging和随机森林的工作原理,深入分析偏差-方差分解如何解释随机森林的成功,最后系统讲解两种特征重要性计算方法及其数学原理。
一、Bootstrap自助采样:Bagging的基石
1.1 什么是Bootstrap?
Bootstrap(自助法) 是一种强大的统计方法,用于从有限的样本中估计总体统计量(如均值、方差等)。
假设我们有一个包含 n 个样本的数据集 D={(x1,y1),(x2,y2),...,(xn,yn)}。Bootstrap采样的过程是:
- 从数据集 D 中有放回地随机抽取一个样本
- 将抽取的样本放入Bootstrap样本集中
- 重复上述步骤 n 次(即抽取与原始数据集相同数量的样本)
这样我们就得到了一个Bootstrap样本集 D∗,它同样包含 n 个样本,但有些原始样本会出现多次,而有些则一次都不会出现。
1.2 为什么约63.2%的样本会被选中?
这是一个经典而优美的数学结果。对于原始数据集中的任意一个特定样本,在每次有放回抽样中不被选中的概率为:
P(不被选中)=1−n1经过 n 次独立抽样后,该样本从未被选中的概率为:
P(从未被选中)=(1−n1)n当 n 足够大时,利用极限 limn→∞(1−1/n)n=e−1:
P(从未被选中)≈e−1≈0.368因此,该样本至少被选中一次的概率为:
1−e−1≈0.632结论:每个Bootstrap样本集平均包含原始数据集约 63.2% 的独特样本,剩下的约 36.8% 从未出现在该Bootstrap样本中。
这些未被选中的样本被称为袋外样本(Out-of-Bag, OOB) 。
1import numpy as np2import matplotlib.pyplot as plt3
4def bootstrap_sampling_demo(n_samples=1000, n_bootstraps=1000):5 """演示Bootstrap采样中样本被选中的比例"""6 # 模拟:对每个Bootstrap样本,统计原始数据集中有多少个样本被选中7 selected_counts = []8 for _ in range(n_bootstraps):9 # 从0到n_samples-1中有放回地抽取n_samples次10 sampled_indices = np.random.choice(n_samples, size=n_samples, replace=True)11 unique_selected = len(np.unique(sampled_indices))12 selected_counts.append(unique_selected / n_samples)13
14 mean_ratio = np.mean(selected_counts)15 print(f"平均选中比例: {mean_ratio:.4f}")16 print(f"理论值 (1 - 1/e): {1 - np.exp(-1):.4f}")17
18 # 可视化19 plt.hist(selected_counts, bins=30, edgecolor='black', alpha=0.7)20 plt.axvline(1 - np.exp(-1), color='red', linestyle='--', label='理论值 ≈ 0.632')21 plt.xlabel('被选中的独特样本比例')22 plt.ylabel('频次')23 plt.legend()24 plt.title('Bootstrap采样中独特样本的比例分布')25 plt.show()26
27bootstrap_sampling_demo()28# 输出: 平均选中比例 ≈ 0.632, 与理论值高度一致二、Bagging:Bootstrap + 聚合
2.1 Bagging的核心思想
Bagging(Bootstrap Aggregating) 的核心思想非常简单:
- 从原始训练集中通过Bootstrap采样生成 m 个不同的训练子集
- 在每个子集上独立训练一个基学习器(通常是决策树)
- 对于新样本,将所有基学习器的预测结果进行聚合:
- 分类:多数投票(Majority Voting)
- 回归:简单平均(Averaging)
2.2 Bagging为什么有效?
Bagging的关键优势在于降低方差(Variance Reduction) 。让我们通过数学来理解这一点。
假设我们有 m 个独立同分布的基学习器 h1,h2,...,hm,每个学习器的预测方差为 σ2。它们的平均预测的方差为:
Var(m1i=1∑mhi)=m21i=1∑mVar(hi)=mσ2随着 m 增大,方差线性减小!
但在现实中,Bootstrap样本之间并非完全独立(因为它们是有放回地从同一数据集中采样的)。如果基学习器之间的相关性为 ρ,则平均预测的方差为:
Var(hˉ)=ρσ2+m1−ρσ2当 m→∞ 时,方差趋近于 ρσ2,而非零。这说明:基学习器之间的相关性越低,Bagging的方差降低效果越好。
这正是随机森林在Bagging基础上进一步引入列采样(特征随机子空间) 的原因——降低树之间的相关性。
对于回归问题的偏差-方差分解,期望预测误差可以分解为:
误差E[(hD(x)−y)2]=方差E[(hD(x)−hˉ(x))2]+偏差(hˉ(x)−yˉ(x))2+噪声E[(yˉ(x)−y(x))2]Bagging通过平均多个模型来降低方差项,而不增加偏差。
三、随机森林:Bagging + 列采样
3.1 随机森林的“双重随机性”
随机森林在Bagging的基础上增加了一个关键的改进:在每次分裂时,不是从所有特征中选择最佳分裂特征,而是从一个随机选择的特征子集中选择。
这形成了随机森林的双重随机性:
| 随机性来源 | 操作 | 目的 |
|---|---|---|
| 行采样(Bootstrap) | 每棵树使用不同的Bootstrap样本集 | 增加数据多样性 |
| 列采样(Feature Subspace) | 每次分裂时只考虑随机子集的特征 | 降低树间相关性 |
列采样也被称为随机子空间方法(Random Subspace Method) 或特征Bagging(Feature Bagging) 。
在scikit-learn中:
- 行采样由
max_samples和bootstrap控制 - 列采样由
max_features控制
对于分类任务,默认的 max_features = sqrt(n_features);对于回归任务,默认的 max_features = n_features。
3.2 列采样的数学动机
列采样通过降低树之间的相关性来进一步减小方差。
假设特征总数为 p,每次分裂时随机选择 k 个特征(k≪p)。如果两个特征高度相关,它们可能在不同的树中被选为分裂特征,从而产生相似的树结构。列采样强制不同树使用不同的特征子集,增加了树之间的多样性。
Breiman在原始随机森林论文中指出,森林的泛化误差取决于两个因素:
- 任意两棵树之间的相关性:相关性越低,误差越低
- 单棵树的强度(Strength) :每棵树本身的预测能力
列采样在降低相关性(好)的同时可能略微降低单棵树的强度(坏),但总体效果是降低泛化误差。
3.3 从数学上看:随机森林 vs Bagging
最近的研究表明,随机森林不仅降低方差,在某些情况下还能同时降低偏差:
当数据中存在某些模式时,随机森林能够捕捉到Bagging集成无法捕捉的模式,从而在降低方差的同时也降低偏差。
特别是当特征之间存在相关性时,随机森林的效果更为显著。
四、袋外误差(OOB Error):免费的验证集
4.1 什么是OOB误差?
由于每个Bootstrap样本只包含约63.2%的原始样本,剩下的36.8%是袋外样本(Out-of-Bag, OOB) 。
对于第 i 个样本,如果它在第 t 棵树的Bootstrap样本中从未出现,那么第 t 棵树就可以用来验证第 i 个样本——这相当于免费的交叉验证!
4.2 OOB误差的数学定义
对于样本 xn,定义:
Gn−(xn)=average(gi1(xn),gi2(xn),...,giT(xn))其中 i1,i2,...,iT 是那些没有使用样本 xn 进行训练的树的索引。
OOB误差为:
Eoob(G)=N1n=1∑Nerr(yn,Gn−(xn))其中 err 是损失函数(分类用0-1损失,回归用MSE)。
OOB误差是测试误差的无偏估计,而且不需要额外的验证集——这是随机森林的一个巨大优势。
1from sklearn.ensemble import RandomForestClassifier2from sklearn.datasets import make_classification3from sklearn.model_selection import train_test_split4
5X, y = make_classification(n_samples=1000, n_features=20, random_state=42)6X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)7
8rf = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)9rf.fit(X_train, y_train)10
11print(f"OOB Score: {rf.oob_score_:.4f}")12print(f"Test Accuracy: {rf.score(X_test, y_test):.4f}")13# OOB Score 与 Test Accuracy 通常非常接近五、特征重要性计算
5.1 基于基尼减少量的重要性(Gini Importance)
这是随机森林默认的特征重要性计算方法。
数学原理:
在决策树的每个节点,分裂会带来不纯度的减少。对于分类树,不纯度用基尼指数(Gini Index) 衡量:
Gini(D)=1−k=1∑Kpk2其中 pk 是节点 D 中第 k 类样本的比例。
对于一个节点 D 按特征 j 分裂为左子节点 DL 和右子节点 DR,基尼减少量为:
ΔGini=Gini(D)−∣D∣∣DL∣Gini(DL)−∣D∣∣DR∣Gini(DR)特征 j 的重要性就是:在所有树中,所有使用特征 j 进行分裂的节点的基尼减少量之和。
Importance(j)=t=1∑Tnode s splits on j∑ΔGini(s)最后将所有特征的重要性归一化到 [0,1] 区间。
1import numpy as np2import matplotlib.pyplot as plt3from sklearn.ensemble import RandomForestClassifier4from sklearn.datasets import make_classification5
6# 生成数据:只有3个特征是有信息的7X, y = make_classification(8 n_samples=1000, n_features=10, n_informative=3,9 n_redundant=0, n_repeated=0, random_state=4210)11
12rf = RandomForestClassifier(n_estimators=100, random_state=42)13rf.fit(X, y)14
15# 基尼重要性16importances = rf.feature_importances_17std = np.std([tree.feature_importances_ for tree in rf.estimators_], axis=0)18
19# 可视化20plt.figure(figsize=(10, 6))21indices = np.argsort(importances)[::-1]22plt.bar(range(X.shape[1]), importances[indices], yerr=std[indices], capsize=5)23plt.xticks(range(X.shape[1]), [f'Feature {i}' for i in indices])24plt.xlabel('特征')25plt.ylabel('基尼重要性')26plt.title('随机森林特征重要性(基于基尼减少量)')27plt.show()基尼重要性的局限性:
- 对高基数特征(取值多的特征)有偏好
- 可能高估相关特征的重要性
5.2 基于排列的重要性(Permutation Importance)
为了克服基尼重要性的偏差,另一种方法是排列重要性(Permutation Importance) 。
数学原理:
- 在训练好的模型上,计算原始数据集上的性能指标(如准确率或MSE)
- 对于特征 j,随机打乱该特征在所有样本中的取值(破坏特征与标签的关联)
- 重新计算打乱后的性能指标
- 重要性 = 原始性能 - 打乱后的性能
如果打乱某个特征后性能显著下降,说明该特征对模型很重要;如果性能几乎不变,说明该特征不重要。
排列重要性的优点:
- 模型无关:适用于任何模型
- 无偏:不依赖于特定的分裂准则
- 更可信:直接反映特征对预测的实际贡献
1from sklearn.inspection import permutation_importance2
3# 计算排列重要性4result = permutation_importance(5 rf, X, y,6 n_repeats=10, # 每个特征打乱10次取平均7 random_state=428)9
10perm_importances = result.importances_mean11perm_std = result.importances_std12
13# 对比两种重要性14plt.figure(figsize=(12, 5))15
16plt.subplot(1, 2, 1)17plt.bar(range(X.shape[1]), importances)18plt.title('基尼重要性')19
20plt.subplot(1, 2, 2)21plt.bar(range(X.shape[1]), perm_importances)22plt.title('排列重要性')23
24plt.tight_layout()25plt.show()5.3 两种方法的对比
| 对比维度 | 基尼重要性 | 排列重要性 |
|---|---|---|
| 计算速度 | 快(训练时顺便计算) | 慢(需要额外计算) |
| 偏差 | 对高基数特征有偏好 | 无偏 |
| 模型依赖 | 仅适用于树模型 | 适用于任何模型 |
| 可解释性 | 间接(基于不纯度减少) | 直接(基于性能下降) |
| scikit-learn实现 | feature_importances_ | permutation_importance |
六、偏差-方差分解:理解随机森林为何有效
6.1 偏差-方差权衡回顾
在机器学习中,泛化误差可以分解为三个部分:
Error=Bias2+Variance+Noise- 偏差(Bias) :模型预测的平均值与真实值之间的差异——衡量模型的表达能力
- 方差(Variance) :模型在不同训练集上的预测波动——衡量模型的稳定性
- 噪声(Noise) :数据本身的不可约误差
6.2 单棵决策树的问题
单棵完全生长的决策树:
- 低偏差:能够完美拟合训练数据
- 高方差:训练数据的微小变化会导致完全不同的树
这就是决策树容易过拟合的根本原因。
6.3 Bagging如何起作用
Bagging通过平均多棵树的预测来降低方差:
Var(Bagging)=ρσ2+m1−ρσ2其中 ρ 是树之间的相关性,σ2 是单棵树的方差。
- 当 m→∞ 时,方差趋近于 ρσ2
- 树之间的相关性越低(ρ 越小),方差降低越多
Bagging不改变偏差——如果单棵树有偏差,Bagging后的偏差基本不变。
6.4 随机森林:超越Bagging
随机森林通过列采样进一步降低树之间的相关性(ρ↓),从而比Bagging获得更低的方差。
更令人惊讶的是,近年来的研究表明,随机森林在某些情况下还能降低偏差:
随机森林能够捕捉到Bagging集成无法捕捉的数据模式,在降低方差的同时也降低偏差。
特别是在信噪比(SNR)较高或特征之间存在相关性时,随机森林的这种优势更为明显。
6.5 直观理解
| 模型 | 偏差 | 方差 | 总体 |
|---|---|---|---|
| 单棵决策树 | 低 | 极高 | 过拟合 |
| Bagging | 不变(低) | 降低 | 改善 |
| 随机森林 | 可能更低 | 进一步降低 | 最优 |
一句话总结:随机森林 = Bagging(降低方差)+ 列采样(进一步降低相关性,可能同时降低偏差)。
七、完整示例:从数据到森林
1import numpy as np2import pandas as pd3from sklearn.ensemble import RandomForestClassifier4from sklearn.datasets import load_breast_cancer5from sklearn.model_selection import train_test_split, cross_val_score6from sklearn.metrics import accuracy_score, classification_report7
8# 1. 加载数据9data = load_breast_cancer()10X, y = data.data, data.target11feature_names = data.feature_names12
13X_train, X_test, y_train, y_test = train_test_split(14 X, y, test_size=0.3, random_state=4215)16
17# 2. 训练随机森林(调参示例)18rf = RandomForestClassifier(19 n_estimators=100, # 树的数量20 max_features='sqrt', # 列采样:sqrt(n_features)21 max_depth=10, # 预剪枝:限制深度22 min_samples_split=10, # 预剪枝:最小分裂样本数23 oob_score=True, # 计算OOB误差24 random_state=42,25 n_jobs=-1 # 并行计算26)27rf.fit(X_train, y_train)28
29# 3. 评估30print(f"训练集准确率: {rf.score(X_train, y_train):.4f}")31print(f"测试集准确率: {rf.score(X_test, y_test):.4f}")32print(f"OOB Score: {rf.oob_score_:.4f}")33
34# 4. 特征重要性分析35importances = rf.feature_importances_36indices = np.argsort(importances)[::-1]37
38print("\nTop 10 重要特征:")39for i in range(10):40 print(f" {i+1}. {feature_names[indices[i]]}: {importances[indices[i]]:.4f}")41
42# 5. 交叉验证43cv_scores = cross_val_score(rf, X, y, cv=5)44print(f"\n5折交叉验证平均准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")45
46# 6. 排列重要性(可选,计算较慢)47from sklearn.inspection import permutation_importance48result = permutation_importance(rf, X_test, y_test, n_repeats=10, random_state=42)49print("\n排列重要性 Top 5:")50top5_perm = np.argsort(result.importances_mean)[::-1][:5]51for i in top5_perm:52 print(f" {feature_names[i]}: {result.importances_mean[i]:.4f} (+/- {result.importances_std[i]:.4f})")八、总结
| 概念 | 核心内容 |
|---|---|
| Bootstrap | 有放回抽样,每个样本被选中的概率 ≈ 63.2% |
| Bagging | Bootstrap + 聚合(投票/平均),降低方差 |
| 随机森林 | Bagging + 列采样(特征子空间),进一步降低相关性 |
| OOB误差 | 利用未被选中的样本做验证,免费的测试集 |
| 基尼重要性 | 累加所有树中特征分裂带来的基尼减少量,快速但有偏 |
| 排列重要性 | 打乱特征后观察性能下降,无偏但较慢 |
| 偏差-方差分解 | 随机森林降低方差,某些情况下也降低偏差 |
核心要点回顾
-
Bootstrap自助采样是Bagging的基石。每个Bootstrap样本包含约63.2%的独特样本,剩下的36.8%成为袋外样本(OOB) ,可用于免费验证。
-
Bagging通过对多个高方差模型(如决策树)的预测进行平均来降低方差,且不增加偏差。树之间的相关性越低,方差降低效果越好。
-
随机森林 = Bagging + 列采样(特征子空间) 。列采样进一步降低了树之间的相关性,使得方差降低效果更显著。在某些情况下,随机森林还能同时降低偏差。
-
OOB误差是随机森林的“免费午餐”——无需额外的验证集就能获得测试误差的无偏估计。
-
特征重要性有两种主流计算方法:
- 基尼重要性:累加特征在所有树中分裂带来的不纯度减少,计算快速但可能对高基数特征有偏好
- 排列重要性:打乱特征后观察性能下降,计算较慢但更可靠、模型无关
-
偏差-方差分解揭示了随机森林成功的根源:通过Bootstrap和列采样的双重随机性,随机森林在保持低偏差的同时大幅降低了方差。
Some information may be outdated