摘要
随机森林(Random Forest)是一种基于Bagging集成学习思想的 ensemble method,通过构建多棵决策树并综合其预测结果来实现分类和回归任务。本文详细介绍了随机森林的核心原理、关键超参数、OOB误差估计机制,以及其在特征重要性分析、异常检测等场景中的应用。文中提供了完整的Python代码示例,涵盖鸢尾花分类、OOB误差估计、特征重要性可视化和超参数调优等实战内容。所有代码均基于scikit-learn库,可直接运行。
关键词:随机森林;Bagging;集成学习;决策树;OOB误差;特征重要性;scikit-learn
1. 引言
在机器学习领域,没有一种算法能够适用于所有场景。每种模型都有其优势和局限性,而集成学习(Ensemble Learning)正是通过组合多个模型来弥补单一模型的不足。随机森林作为集成学习中最具代表性的算法之一,凭借其优异的性能、较强的抗过拟合能力和易用性,在学术界和工业界都得到了广泛应用。
从Kaggle竞赛的历史来看,随机森林及其变体在众多表格数据分类和回归任务中表现出色,常常作为强有力的基准模型(Baseline)。本文将系统性地介绍随机森林的原理、实现细节和使用技巧,帮助读者全面掌握这一重要算法。
2. 集成学习基础
2.1 为什么需要集成学习?
在讨论随机森林之前,我们需要理解集成学习背后的核心思想。想象一下这样一个场景:你要决定是否投资某只股票,你会怎么做?你可能会咨询多位朋友的意见,然后综合他们的判断做出最终决策。如果其中一位朋友给出了过于极端的建议,而其他几位都给出了相反的建议,你自然会倾向于相信多数人的判断。这就是集成学习的基本哲学------群体的智慧往往优于个体。
在机器学习中,单个模型(无论是决策树、SVM还是神经网络)都可能存在偏差或方差问题。通过构建多个模型并综合它们的预测结果,我们可以获得更稳定、更准确的预测。
2.2 Bagging(Bootstrap Aggregating)原理
Bagging是随机森林的基石,其核心思想可以通过以下步骤理解:
步骤一:有放回随机采样(Bootstrap Sampling)
假设我们有一个包含N个样本的训练数据集。从中随机抽取一个样本,复制到新的采样集中,然后将原样本放回数据集。重复这个过程N次,我们就得到了一个与原数据集大小相同的Bootstrap样本。由于是有放回采样,Bootstrap样本中大约会包含原数据集63.2%的不同样本(这是统计学上的经典结论)。
步骤二:训练基学习器
使用每个Bootstrap样本分别训练一个基学习器(如决策树)。
步骤三:聚合预测结果
对于分类任务,采用多数投票(Majority Voting)------让所有基学习器分别预测,然后选择获得票数最多的类别作为最终预测。对于回归任务,则采用简单平均(Simple Averaging)------将所有基学习器的预测值取平均。
下面我们用代码演示Bagging的基本流程:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 生成非线性分类数据集(双月牙形)
X, y = make_moons(n_samples=500, noise=0.3, random_state=42)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 对比单棵决策树与Bagging(10棵树)
tree_clf = DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train, y_train)
tree_pred = tree_clf.predict(X_test)
print(f"单棵决策树准确率: {accuracy_score(y_test, tree_pred):.4f}")
bagging_clf = BaggingClassifier(
estimator=DecisionTreeClassifier(),
n_estimators=10,
bootstrap=True, # 使用Bootstrap采样
oob_score=True, # 计算OOB分数
random_state=42
)
bagging_clf.fit(X_train, y_train)
bagging_pred = bagging_clf.predict(X_test)
print(f"Bagging(10棵树)准确率: {accuracy_score(y_test, bagging_pred):.4f}")
print(f"Bagging OOB分数: {bagging_clf.oob_score_:.4f}")
# 可视化对比
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 绘制决策边界
def plot_decision_boundary(clf, X, y, ax, title):
h = 0.02
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.4)
ax.scatter(X[:, 0], X[:, 1], c=y, alpha=0.8, edgecolors='k')
ax.set_title(title)
ax.set_xlabel('Feature 1')
ax.set_ylabel('Feature 2')
plot_decision_boundary(tree_clf, X_test, y_test, axes[0], '单棵决策树')
plot_decision_boundary(bagging_clf, X_test, y_test, axes[1], 'Bagging(10棵树)')
plt.tight_layout()
plt.savefig('bagging_comparison.png', dpi=150)
plt.show()
运行结果:
单棵决策树准确率: 0.7900
Bagging(10棵树)准确率: 0.9000
Bagging OOB分数: 0.8875
从结果可以看到,Bagging通过集成多棵树显著提升了分类准确率。
2.3 偏差与方差的权衡
理解偏差-方差分解对于深入理解随机森林至关重要。模型的泛化误差可以分解为:
泛化误差 = 偏差² + 方差 + 不可约误差
-
偏差(Bias):模型预测值的期望与真实值之间的差异。高偏差意味着模型欠拟合,无法捕捉数据的基本模式。
-
方差(Variance):同一模型在不同训练集上的预测变化程度。高方差意味着模型过拟合,对训练数据的微小变化过于敏感。
单棵决策树通常具有低偏差但高方差的特性------它们可以很好地拟合训练数据,但容易过拟合,导致在不同数据集上表现差异很大。
Bagging通过以下方式降低方差:
-
每个Bootstrap样本训练一棵树,产生N棵略有不同的树
-
预测时取平均/投票,这些不同的预测值会相互"抵消"极端错误
-
最终结果是:偏差保持较低(因为每棵树都足够深),但方差大幅降低
这就是Bagging的核心优势------在不显著增加偏差的情况下大幅降低方差。
3. 随机森林原理详解
3.1 随机森林 = 决策树 + Bagging + 随机特征选择
随机森林在Bagging的基础上更进一步,引入了随机特征选择机制。标准Bagging中,每棵树都使用全部特征来寻找最优分裂点。而随机森林在每个节点分裂时,只考虑特征的一个随机子集。
这个看似简单的改动带来了巨大的好处:
-
进一步增加树之间的多样性:如果每棵树都使用相同的特征,即使使用不同的Bootstrap样本,树的结构可能仍然相似。随机特征选择确保每棵树都有独特的视角。
-
更强的抗过拟合能力:限制每个节点可用的特征数量,防止树变得过于复杂。
-
更好的泛化性能:多样性增加使整体预测更加稳健。
3.2 随机森林算法流程
以下是随机森林的完整训练流程:
输入:训练数据集 D,包含 N 个样本和 M 个特征
树的数量 T
特征子集大小 m(通常 m = √M 或 log₂(M))
输出:T 棵决策树的集合 {tree₁, tree₂, ..., treeₜ}
对于 t = 1 到 T:
1. Bootstrap采样:从 D 中有放回地抽取 N 个样本,得到 Bootstrap 数据集 Dₜ
2. 训练决策树 treeₜ:
- 从根节点开始
- 对每个节点:
a. 随机选择 m 个特征(不放回)
b. 在这 m 个特征中找到最优分裂
c. 按照最优分裂将节点分为两个子节点
- 持续分裂直到满足停止条件(如最大深度、最小样本数等)
- 不进行剪枝
3. 返回 treeₜ
预测:
- 分类:T 棵树投票,取票数最多的类别
- 回归:T 棵树预测值取平均
3.3 随机森林的Python实现
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.datasets import load_breast_cancer, fetch_california_housing
from sklearn.model_selection import cross_val_score
from sklearn.metrics import classification_report, mean_squared_error
# ============ 分类示例:乳腺癌数据集 ============
print("=" * 50)
print("乳腺癌数据集分类示例")
print("=" * 50)
# 加载数据
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
# 训练随机森林分类器
rf_clf = RandomForestClassifier(
n_estimators=100,
max_depth=None, # 不限制深度,让树完全生长
min_samples_split=2, # 节点分裂所需最小样本数
min_samples_leaf=1, # 叶节点最小样本数
max_features='sqrt', # 每次分裂考虑的特征数(sqrt为开平方)
bootstrap=True, # 使用Bootstrap采样
oob_score=True, # 计算OOB分数
random_state=42,
n_jobs=-1 # 使用所有CPU核心
)
# 交叉验证评估
cv_scores = cross_val_score(rf_clf, X, y, cv=5, scoring='accuracy')
print(f"5折交叉验证准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std() * 2:.4f})")
# 训练模型
rf_clf.fit(X, y)
print(f"OOB分数: {rf_clf.oob_score_:.4f}")
print(f"测试集准确率: {rf_clf.score(X, y):.4f}")
# 打印特征重要性(Top 10)
feature_importance = rf_clf.feature_importances_
feature_names = cancer.feature_names
indices = np.argsort(feature_importance)[::-1]
print("\n特征重要性排名(Top 10):")
for i in range(min(10, len(feature_names))):
print(f" {i+1}. {feature_names[indices[i]]}: {feature_importance[indices[i]]:.4f}")
# ============ 回归示例:加州房价数据集 ============
print("\n" + "=" * 50)
print("加州房价数据集回归示例")
print("=" * 50)
# 加载数据
housing = fetch_california_housing()
X_h, y_h = housing.data, housing.target
# 训练随机森林回归器
rf_reg = RandomForestRegressor(
n_estimators=100,
max_depth=15, # 限制最大深度
min_samples_split=5, # 内部节点分裂所需最小样本数
min_samples_leaf=2, # 叶节点最小样本数
random_state=42,
n_jobs=-1
)
# 交叉验证评估
cv_mse = -cross_val_score(rf_reg, X_h, y_h, cv=5, scoring='neg_mean_squared_error')
print(f"5折交叉验证 MSE: {cv_mse.mean():.4f} (+/- {cv_mse.std() * 2:.4f})")
print(f"5折交叉验证 RMSE: {np.sqrt(cv_mse.mean()):.4f}")
# 训练和预测
rf_reg.fit(X_h, y_h)
predictions = rf_reg.predict(X_h[:5])
print(f"\n前5个样本的预测值: {predictions}")
print(f"对应真实值: {y_h[:5]}")
运行结果:
==================================================
乳腺癌数据集分类示例
==================================================
5折交叉验证准确率: 0.9632 (+/- 0.0249)
OOB分数: 0.9596
测试集准确率: 1.0000
特征重要性排名(Top 10):
1. worst radius: 0.0715
2. worst perimeter: 0.0698
3. mean concave points: 0.0584
4. mean radius: 0.0553
5. worst area: 0.0508
...
==================================================
加州房价数据集回归示例
==================================================
5折交叉验证 MSE: 0.2639 (+/- 0.0192)
5折交叉验证 RMSE: 0.5137
4. 关键超参数详解
随机森林有多个重要超参数,理解它们的作用对于调优模型至关重要。
4.1 n_estimators - 树的数量
n_estimators控制随机森林中树的数量。理论上,树越多,模型越稳定,泛化能力越强。但边际效益会递减------增加到一定程度后,增加更多的树只会增加计算成本,而性能提升很小。
经验法则:
-
分类任务:100-500棵树通常足够
-
回归任务:可能需要更多(200-1000棵)
-
使用OOB分数或验证集监控,找到最优数量
# 探究树的数量与性能的关系
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# 测试不同数量的树
n_trees_list = [10, 50, 100, 200, 500]
train_scores = []
oob_scores = []
for n_trees in n_trees_list:
rf = RandomForestClassifier(
n_estimators=n_trees,
bootstrap=True,
oob_score=True,
random_state=42
)
rf.fit(X, y)
train_scores.append(rf.score(X, y))
oob_scores.append(rf.oob_score_)
print(f"树数量: {n_trees:3d} | 训练准确率: {rf.score(X, y):.4f} | OOB准确率: {rf.oob_score_:.4f}")
# 可视化
plt.figure(figsize=(10, 5))
plt.plot(n_trees_list, train_scores, 'b-o', label='训练准确率')
plt.plot(n_trees_list, oob_scores, 'r-o', label='OOB准确率')
plt.xlabel('树的数量 (n_estimators)')
plt.ylabel('准确率')
plt.title('随机森林:树的数量 vs 准确率')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('n_estimators_analysis.png', dpi=150)
plt.show()
4.2 max_depth - 最大深度
max_depth限制每棵树的最大深度。深度越大,树越复杂,越容易过拟合;深度越小,树越简单,可能欠拟合。
关键点:
-
max_depth=None:树完全生长,直到所有叶节点纯或样本数少于min_samples_split -
限制深度可以显著减少训练时间
-
结合
min_samples_split和min_samples_leaf使用效果更好
# 对比不同max_depth的效果
max_depths = [3, 5, 10, 15, None]
depth_train_scores = []
depth_test_scores = []
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.3, random_state=42)
for depth in max_depths:
rf = RandomForestClassifier(n_estimators=100, max_depth=depth, random_state=42)
rf.fit(X_train, y_train)
depth_train_scores.append(rf.score(X_train, y_train))
depth_test_scores.append(rf.score(X_test, y_test))
print(f"max_depth: {str(depth):5s} | 训练准确率: {rf.score(X_train, y_train):.4f} | 测试准确率: {rf.score(X_test, y_test):.4f}")
4.3 min_samples_split 和 min_samples_leaf
-
min_samples_split:节点分裂所需的最小样本数。如果节点样本数少于这个值,则不再分裂。 -
min_samples_leaf:叶节点所需最小样本数。如果分裂会导致叶节点样本数少于这个值,则不进行分裂。
这两个参数共同作用,防止树变得过于复杂:
# 演示min_samples_split和min_samples_leaf的效果
params_combos = [
(2, 1), # 默认值,不过滤
(10, 5), # 较严格
(20, 10), # 非常严格
(50, 20) # 极度严格
]
print("min_samples_split | min_samples_leaf | 训练准确率 | 测试准确率 | 叶节点数")
print("-" * 75)
for min_split, min_leaf in params_combos:
rf = RandomForestClassifier(
n_estimators=100,
min_samples_split=min_split,
min_samples_leaf=min_leaf,
random_state=42
)
rf.fit(X_train, y_train)
# 计算总叶节点数
total_leaves = sum(tree.get_n_leaves() for tree in rf.estimators_)
print(f"{min_split:17d} | {min_leaf:16d} | {rf.score(X_train, y_train):.4f} | {rf.score(X_test, y_test):.4f} | {total_leaves:5d}")
4.4 max_features - 特征采样比例
max_features控制每个节点分裂时考虑的特征数量。这是随机森林引入的关键随机性来源。
常用设置:
-
'sqrt':分类时推荐,等于√M(M为特征总数) -
'log2':分类时推荐,等于log₂(M) -
0.3-0.7:可以尝试的范围 -
None:使用所有特征(等同于Bagging)
# 对比不同max_features设置
max_features_options = ['sqrt', 'log2', None, 0.5, 0.7]
print("max_features | 训练准确率 | 测试准确率")
print("-" * 45)
for mf in max_features_options:
rf = RandomForestClassifier(n_estimators=100, max_features=mf, random_state=42)
rf.fit(X_train, y_train)
print(f"{str(mf):12s} | {rf.score(X_train, y_train):.4f} | {rf.score(X_test, y_test):.4f}")
5. OOB(Out-of-Bag)误差详解
5.1 OOB误差的概念
在Bagging过程中,每个Bootstrap样本大约包含原数据集63.2%的不同样本。这意味着对于每棵树,大约有36.8%的样本从未被用于该树的训练。这些未被使用的样本被称为Out-of-Bag(OOB)样本。
OOB误差的巧妙之处在于:我们可以利用这些"未见过"的样本来评估模型性能,而无需单独的验证集或交叉验证!
5.2 OOB误差计算方法
对于每个样本xᵢ,我们可以找到所有未使用xᵢ的树(即xᵢ在那些树的OOB集中),让这些树对xᵢ进行预测,然后综合这些预测得到该样本的OOB预测。最后,比较所有样本的OOB预测与真实标签,计算整体OOB误差。
OOB误差的优势:
-
无需交叉验证:可以快速获得无偏的性能估计
-
节省数据:所有数据都可用于训练
-
与验证集误差高度相关:可以作为模型选择和超参数调优的可靠指标
5.3 OOB误差的Python实现
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import numpy as np
# 加载数据
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
# 演示OOB误差估计
print("随机森林 OOB 误差估计演示")
print("=" * 50)
# 不使用OOB
rf_no_oob = RandomForestClassifier(n_estimators=100, oob_score=False, random_state=42)
rf_no_oob.fit(X, y)
print(f"无OOB - 训练准确率: {rf_no_oob.score(X, y):.4f}")
# 使用OOB
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X, y)
print(f"有OOB - 训练准确率: {rf_oob.score(X, y):.4f}")
print(f"有OOB - OOB分数: {rf_oob.oob_score_:.4f}")
# OOB分数的解读
print(f"\nOOB分数与训练准确率的差异: {rf_oob.score(X, y) - rf_oob.oob_score_:.4f}")
print("差异越大,说明模型过拟合越严重")
# 验证OOB与交叉验证的一致性
from sklearn.model_selection import cross_val_score
cv_scores = cross_val_score(rf_oob, X, y, cv=5)
print(f"\n5折交叉验证准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std() * 2:.4f})")
print(f"OOB分数: {rf_oob.oob_score_:.4f}")
print("可以看到OOB分数与CV分数非常接近,说明OOB估计是可靠的")
6. 随机森林使用场景
6.1 表格数据分类与回归
随机森林在结构化表格数据上的表现通常非常出色,是Kaggle竞赛中的常胜模型。它特别适合:
-
特征类型混合(数值型+类别型)
-
特征维度不是特别高(几百个特征以内)
-
数据量中等(几千到几百万样本)
6.2 特征重要性分析
随机森林提供了天然的**特征重要性(Feature Importance)**度量,这是其最受欢迎的应用之一。通过分析每个特征在所有树中对分裂和信息增益的贡献,我们可以识别最关键的特征。
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
import numpy as np
# 加载鸢尾花数据集
iris = load_iris()
X, y = iris.data, iris.target
# 训练随机森林
rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X, y)
# 获取特征重要性
feature_importance = rf.feature_importances_
feature_names = iris.feature_names
indices = np.argsort(feature_importance)[::-1]
# 可视化特征重要性
plt.figure(figsize=(10, 6))
colors = plt.cm.viridis(np.linspace(0, 0.8, len(feature_names)))
plt.bar(range(len(feature_names)), feature_importance[indices], color=colors)
plt.xticks(range(len(feature_names)), [feature_names[i] for i in indices], rotation=45, ha='right')
plt.xlabel('特征')
plt.ylabel('重要性')
plt.title('随机森林特征重要性分析 - 鸢尾花数据集')
plt.tight_layout()
plt.savefig('feature_importance.png', dpi=150)
plt.show()
# 打印排名
print("特征重要性排名:")
for i, idx in enumerate(indices):
print(f" {i+1}. {feature_names[idx]}: {feature_importance[idx]:.4f}")
6.3 异常检测(Anomaly Detection)
随机森林可用于异常检测,方法是:对于每个样本,计算它穿过所有树的平均深度或平均非叶节点数。异常点通常位于树的较浅层(因为它们很快就被分离出去了),或者在特征空间中远离训练数据。
from sklearn.ensemble import IsolationForest
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
# 生成包含异常点的数据
X, y = make_blobs(n_samples=300, centers=1, cluster_std=0.5, random_state=42)
# 添加异常点
outliers = np.random.uniform(-6, 6, (20, 2))
X_outliers = np.vstack([X, outliers])
y_outliers = np.hstack([y, [-1]*20]) # -1表示异常
# 使用Isolation Forest进行异常检测
iso_forest = IsolationForest(n_estimators=100, contamination=0.05, random_state=42)
predictions = iso_forest.fit_predict(X_outliers)
# 可视化
plt.figure(figsize=(10, 6))
inliers = predictions == 1
outliers_mask = predictions == -1
plt.scatter(X_outliers[inliers, 0], X_outliers[inliers, 1], c='blue', label='正常点', alpha=0.6)
plt.scatter(X_outliers[outliers_mask, 0], X_outliers[outliers_mask, 1], c='red', label='异常点', alpha=0.8, edgecolors='k')
plt.title('Isolation Forest 异常检测结果')
plt.legend()
plt.savefig('anomaly_detection.png', dpi=150)
plt.show()
# 输出检测统计
print(f"检测出的异常点数量: {(predictions == -1).sum()}")
print(f"异常点比例: {(predictions == -1).sum() / len(predictions):.2%}")
6.4 缺失值填补
随机森林可以用于迭代式填补缺失值,方法如下:
-
用均值/中位数初步填补
-
将填补后的数据训练随机森林
-
用训练好的随机森林重新预测缺失值
-
重复步骤2-3直到收敛或达到最大迭代次数
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestRegressor
import numpy as np
import pandas as pd
# 加载数据并人为添加缺失值
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names
# 创建DataFrame方便操作
df = pd.DataFrame(X, columns=feature_names)
df['target'] = y
# 人为添加缺失值(约10%的数据)
np.random.seed(42)
missing_mask = np.random.random(df.shape) < 0.1
df_with_missing = df.copy()
df_with_missing[missing_mask] = np.nan
print(f"缺失值数量: {df_with_missing.isna().sum().sum()}")
print(f"缺失值比例: {df_with_missing.isna().mean().mean():.2%}")
# 使用随机森林迭代填补缺失值
def rf_impute(df, target_col, n_iterations=10, n_estimators=100):
"""使用随机森林迭代填补缺失值"""
df_imputed = df.copy()
for iteration in range(n_iterations):
df_temp = df_imputed.copy()
# 对每一列进行填补
for col in df_temp.columns:
if col == target_col:
continue
# 找出缺失值的位置
missing_mask = df_temp[col].isna()
if missing_mask.sum() == 0:
continue
# 准备训练数据
train_data = df_temp[~missing_mask]
predict_data = df_temp[missing_mask]
# 特征是除了目标列和当前列外的所有列
feature_cols = [c for c in df_temp.columns if c != col and c != target_col]
X_train = train_data[feature_cols]
y_train = train_data[col]
X_predict = predict_data[feature_cols]
# 训练随机森林
rf = RandomForestRegressor(n_estimators=n_estimators, random_state=42)
rf.fit(X_train, y_train)
# 预测并填补
if len(X_predict) > 0:
predicted_values = rf.predict(X_predict)
df_imputed.loc[missing_mask, col] = predicted_values
if iteration == 0 or iteration == n_iterations - 1:
print(f"迭代 {iteration + 1}/{n_iterations} 完成")
return df_imputed
# 执行填补
df_imputed = rf_impute(df_with_missing, 'target', n_iterations=5)
# 评估填补效果
print("\n填补后的数据统计:")
print(df_imputed.describe())
7. 实战:鸢尾花分类完整示例
下面我们将完成一个完整的鸢尾花分类实战,对比单棵决策树和随机森林的性能差异,并展示OOB误差估计和特征重要性分析。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
accuracy_score,
classification_report,
confusion_matrix,
ConfusionMatrixDisplay
)
# ============ 1. 数据加载与探索 ============
print("=" * 60)
print("鸢尾花分类实战 - 单棵决策树 vs 随机森林")
print("=" * 60)
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names
target_names = iris.target_names
print(f"\n数据集信息:")
print(f" 样本数: {X.shape[0]}")
print(f" 特征数: {X.shape[1]}")
print(f" 类别: {list(target_names)}")
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f" 训练集: {X_train.shape[0]} 样本")
print(f" 测试集: {X_test.shape[0]} 样本")
# ============ 2. 训练单棵决策树 ============
print("\n" + "-" * 40)
print("训练单棵决策树")
print("-" * 40)
tree_clf = DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train, y_train)
tree_train_pred = tree_clf.predict(X_train)
tree_test_pred = tree_clf.predict(X_test)
print(f"\n单棵决策树结果:")
print(f" 训练集准确率: {accuracy_score(y_train, tree_train_pred):.4f}")
print(f" 测试集准确率: {accuracy_score(y_test, tree_test_pred):.4f}")
print("\n分类报告:")
print(classification_report(y_test, tree_test_pred, target_names=target_names))
# ============ 3. 训练随机森林 ============
print("-" * 40)
print("训练随机森林(100棵树)")
print("-" * 40)
rf_clf = RandomForestClassifier(
n_estimators=100,
bootstrap=True,
oob_score=True,
random_state=42
)
rf_clf.fit(X_train, y_train)
rf_train_pred = rf_clf.predict(X_train)
rf_test_pred = rf_clf.predict(X_test)
print(f"\n随机森林结果:")
print(f" 训练集准确率: {accuracy_score(y_train, rf_train_pred):.4f}")
print(f" 测试集准确率: {accuracy_score(y_test, rf_test_pred):.4f}")
print(f" OOB分数: {rf_clf.oob_score_:.4f}")
print("\n分类报告:")
print(classification_report(y_test, rf_test_pred, target_names=target_names))
# ============ 4. 混淆矩阵可视化 ============
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 决策树混淆矩阵
ConfusionMatrixDisplay.from_estimator(
tree_clf, X_test, y_test,
display_labels=target_names,
cmap='Blues',
ax=axes[0]
)
axes[0].set_title('单棵决策树 - 混淆矩阵')
# 随机森林混淆矩阵
ConfusionMatrixDisplay.from_estimator(
rf_clf, X_test, y_test,
display_labels=target_names,
cmap='Greens',
ax=axes[1]
)
axes[1].set_title('随机森林 - 混淆矩阵')
plt.tight_layout()
plt.savefig('confusion_matrices.png', dpi=150)
plt.show()
# ============ 5. 特征重要性分析 ============
print("\n" + "-" * 40)
print("特征重要性分析")
print("-" * 40)
importances = rf_clf.feature_importances_
indices = np.argsort(importances)[::-1]
print("\n随机森林特征重要性:")
for i, idx in enumerate(indices):
print(f" {i+1}. {feature_names[idx]}: {importances[idx]:.4f}")
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 条形图
colors = plt.cm.RdYlGn(np.linspace(0.2, 0.8, len(feature_names)))
axes[0].barh(range(len(feature_names)), importances[indices], color=colors)
axes[0].set_yticks(range(len(feature_names)))
axes[0].set_yticklabels([feature_names[i] for i in indices])
axes[0].set_xlabel('重要性')
axes[0].set_title('随机森林特征重要性')
axes[0].invert_yaxis()
# 箱线图:每棵树的特征重要性分布
tree_importances = np.array([tree.feature_importances_ for tree in rf_clf.estimators_])
axes[1].boxplot(tree_importances, labels=feature_names, vert=False)
axes[1].set_xlabel('重要性')
axes[1].set_title('各特征重要性分布(100棵树的箱线图)')
plt.tight_layout()
plt.savefig('feature_importance_detailed.png', dpi=150)
plt.show()
# ============ 6. 超参数调优 ============
print("\n" + "-" * 40)
print("超参数调优(使用GridSearchCV)")
print("-" * 40)
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 5, 10, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
rf_grid = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1,
verbose=1
)
rf_grid.fit(X_train, y_train)
print(f"\n最优参数: {rf_grid.best_params_}")
print(f"最优交叉验证分数: {rf_grid.best_score_:.4f}")
# 使用最优模型在测试集上评估
best_rf = rf_grid.best_estimator_
best_pred = best_rf.predict(X_test)
print(f"最优模型测试集准确率: {accuracy_score(y_test, best_pred):.4f}")
运行结果:
============================================================
鸢尾花分类实战 - 单棵决策树 vs 随机森林
============================================================
数据集信息:
样本数: 150
特征数: 4
类别: ['setosa', 'versicolor', 'virginica']
训练集: 105 样本
测试集: 45 样本
--------------------------------------------------
单棵决策树结果:
训练集准确率: 1.0000
测试集准确率: 0.9778
随机森林结果:
训练集准确率: 1.0000
测试集准确率: 1.0000
OOB分数: 0.9429
--------------------------------------------------
特征重要性分析
随机森林特征重要性:
1. petal width (cm): 0.4441
2. petal length (cm): 0.4169
3. sepal width (cm): 0.0928
4. sepal length (cm): 0.0462
--------------------------------------------------
超参数调优(使用GridSearchCV)
最优参数: {'max_depth': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 50}
最优交叉验证分数: 0.9619
最优模型测试集准确率: 1.0000
8. 随机森林的优缺点总结
8.1 优点
-
性能优异:在大多数分类和回归任务中表现出色
-
抗过拟合:通过集成和随机特征选择降低过拟合风险
-
并行化友好:每棵树相互独立,可并行训练
-
特征重要性:内置特征重要性分析
-
OOB估计:无需交叉验证即可评估模型
-
处理缺失值:可以处理缺失值和类别型特征
-
鲁棒性:对异常值和噪声相对不敏感
-
易用性:超参数通常使用默认值就能获得不错的效果
8.2 缺点
-
解释性差:数百棵树组成的模型难以解释(相比单棵决策树)
-
计算成本:需要训练多棵树,比单棵树慢
-
内存消耗:需要存储所有树,内存占用较大
-
边缘化优势:在某些任务中可能不如梯度提升(如XGBoost、LightGBM)表现好
-
时间复杂度:随着树的数量增加,预测时间线性增长
8.3 适用场景总结
| 场景 | 推荐程度 | 说明 |
|---|---|---|
| 表格数据分类/回归 | ⭐⭐⭐⭐⭐ | 随机森林的最佳应用场景 |
| 特征重要性分析 | ⭐⭐⭐⭐⭐ | 内置支持,非常方便 |
| 异常检测 | ⭐⭐⭐⭐ | Isolation Forest是专门为此设计的变体 |
| 高维稀疏数据(如文本) | ⭐⭐ | 效果通常不如线性模型或深度学习 |
| 实时预测 | ⭐⭐⭐ | 需要预训练好模型,单次预测速度可以接受 |
9. 结语
随机森林是机器学习工具箱中不可或缺的利器。它完美地体现了"群体智慧"的思想------通过集成多棵决策树并引入随机性,既保留了决策树强大的表达能力,又大幅提升了泛化能力。
在实际应用中,随机森林通常可以作为首选模型来快速建立基准性能,然后再尝试更复杂的算法(如XGBoost、LightGBM等梯度提升方法)。特别是在特征重要性分析、缺失值处理等场景中,随机森林提供了开箱即用的解决方案。
掌握随机森林的原理和使用技巧,对于每一位机器学习从业者都是必备的基本功。希望本文能够帮助读者全面理解随机森林,并在实际项目中灵活运用。