第40课:随机森林与 Bagging
学习目标
完成本课学习后,你将能够:
- 理解集成学习(Ensemble Learning)的核心思想,以及 Bagging 如何通过“民主投票”降低模型方差。
- 清晰地阐述随机森林(Random Forest)在 Bagging 基础上引入的“双重随机性”策略。
- 使用
scikit-learn构建、训练并评估一个随机森林分类模型。 - 解释随机森林的特征重要性,并了解其在模型解释中的作用。
- 掌握随机森林的关键超参数及其调整策略。
核心概念
1. 从单棵决策树到“森林”:集成学习的思路
上一课我们学习了决策树,它是一个强大但容易过拟合的“强学习器”。集成学习的基本思想是:“三个臭皮匠,顶个诸葛亮”。我们不依赖一棵可能犯错的树,而是构建多棵决策树,然后综合它们的预测结果。这通常能带来更稳定、更准确的模型。
Bagging (Bootstrap Aggregating) 是集成学习的一种经典策略:
- 自助采样 (Bootstrap Sampling):从原始训练集中,有放回地随机抽取多个子样本集。每个子样本集的大小与原始训练集相同,但由于是有放回抽样,每个子集约包含原始数据 63.2% 的独特样本。
- 并行建模:在每个自助采样得到的子样本集上,独立地训练一棵完整、未剪枝的决策树。
- 聚合预测:对于分类问题,最终的预测结果是所有树预测类别的投票(多数表决);对于回归问题,则是所有树预测值的平均。
Bagging 的核心作用是降低模型的方差。因为单棵决策树容易对训练数据中的噪声过于敏感(高方差),而对多个略有差异的树进行平均或投票,可以平滑掉这种波动,从而提升模型的泛化能力。
2. 随机森林:在 Bagging 上增加“随机性”
随机森林是 Bagging 的一个改进和特例,它在 Bagging 的框架上引入了一个关键改进:节点分裂时的随机特征选择。
具体来说:
- 第一重随机:训练每棵树时,使用自助采样得到的子样本(同 Bagging)。
- 第二重随机:在决策树的每个节点进行分裂时,不再考虑所有特征,而是随机选择一个特征子集(例如,总特征数的平方根个),然后只在这个子集中寻找最优分裂特征和分裂点。
这个“双重随机性”的引入有两个巨大好处:
- 进一步降低方差:强制每棵树使用不同的特征子集进行分裂,使得树与树之间的相关性更低。相关性低的树进行投票/平均,效果更好。
- 提升模型多样性:每棵树都专注于数据的不同方面,这有助于捕捉更复杂的模式。
通俗比喻:假设要评估一家公司的投资价值。
- 单棵决策树:像一个资深分析师独立做决定,可能受其个人偏好影响。
- Bagging:让100个分析师独立看同一份完整数据(但每人看的重点略有不同),然后投票。这减少了个人偏见。
- 随机森林:不仅让100个分析师独立看数据,而且在分析每个关键问题时,只允许他们查看随机抽取的部分文件。这迫使他们从不同角度思考,结论会更全面、更稳健。
代码示例
我们将使用经典的鸢尾花(Iris)数据集来演示随机森林的构建和评估。
# 导入必要的库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
import seaborn as sns
# 1. 加载并准备数据
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
target_names = iris.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]}")
print(f"特征名称: {feature_names}")
print(f"类别名称: {target_names}")
# 2. 创建并训练随机森林模型
# n_estimators: 森林中树的数量
# random_state: 保证结果可重现
# max_depth: 树的最大深度(这里未指定,树会生长到纯节点或达到min_samples_split)
# n_jobs=-1: 使用所有CPU核心并行训练
rf_clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
rf_clf.fit(X_train, y_train)
# 3. 进行预测与评估
y_pred = rf_clf.predict(X_test)
# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print(f"\n模型在测试集上的准确率: {accuracy:.4f}")
# 查看详细的分类报告
print("\n分类报告:")
print(classification_report(y_test, y_pred, target_names=target_names))
# 混淆矩阵可视化
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=target_names, yticklabels=target_names)
plt.xlabel('预测标签')
plt.ylabel('真实标签')
plt.title('随机森林混淆矩阵')
plt.show()
# 4. 特征重要性分析 - 随机森林的一大优势
importances = rf_clf.feature_importances_
indices = np.argsort(importances)[::-1]
print("\n特征重要性排序:")
for i in range(X.shape[1]):
print(f"{i+1}. {feature_names[indices[i]]}: {importances[indices[i]]:.4f}")
# 可视化特征重要性
plt.figure(figsize=(10, 6))
plt.title("特征重要性")
plt.bar(range(X.shape[1]), importances[indices], align="center")
plt.xticks(range(X.shape[1]), [feature_names[i] for i in indices], rotation=45)
plt.xlim([-1, X.shape[1]])
plt.ylabel("重要性分数")
plt.tight_layout()
plt.show()
# 5. 调整关键参数示例:n_estimators 的影响
n_estimators_range = [10, 50, 100, 200, 300, 500]
train_scores = []
test_scores = []
for n in n_estimators_range:
rf = RandomForestClassifier(n_estimators=n, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
train_scores.append(rf.score(X_train, y_train))
test_scores.append(rf.score(X_test, y_test))
plt.figure(figsize=(10, 6))
plt.plot(n_estimators_range, train_scores, 'o-', label='训练集准确率')
plt.plot(n_estimators_range, test_scores, 'o-', label='测试集准确率')
plt.xlabel('树的数量 (n_estimators)')
plt.ylabel('准确率')
plt.title('树的数量对模型性能的影响')
plt.legend()
plt.grid(True)
plt.show()
print("观察: 当树的数量增加到一定程度后,性能提升会趋于平缓。")
实践练习
练习1:基础建模与评估
使用 scikit-learn 的 make_classification 生成一个二分类数据集(1000个样本,20个特征,其中5个是信息性特征)。用 train_test_split 划分数据,然后构建一个随机森林分类器(n_estimators=150),计算并打印其在测试集上的准确率和 f1-score。
预期输出:
测试集准确率: 0.XXXX
测试集 F1-Score: 0.XXXX
(具体数值因数据随机生成而异,但应在一个合理范围内)
练习2:调参与交叉验证
在练习1的基础上,使用 sklearn.model_selection 中的 GridSearchCV 来寻找更优的参数。设定参数网格为:
n_estimators: [100, 200, 300]max_depth: [5, 10, 15, None]min_samples_split: [2, 5, 10]
进行5折交叉验证,找出最佳参数组合和对应的测试集准确率。
预期输出:
最佳参数: {'max_depth': ..., 'min_samples_split': ..., 'n_estimators': ...}
最佳交叉验证得分: 0.XXXX
测试集最终准确率: 0.XXXX
练习3:特征选择与重要性
使用 load_wine 数据集(来自 sklearn.datasets)。构建一个随机森林模型,输出其特征重要性。选择重要性排名前5的特征,仅使用这些特征重新训练一个新的随机森林模型。比较两个模型在测试集上的准确率,并讨论你的发现。
预期输出应包括:
- 完整模型的准确率。
- 特征重要性列表。
- 精简模型的准确率。
- 对两者性能差异的简要分析(1-2句话)。
常见错误
-
过度依赖默认参数:
scikit-learn的随机森林默认参数(如max_depth=None)可能导致单棵树非常复杂甚至过拟合。虽然 Bagging 和随机性有助于缓解,但对于噪声大的数据,适当限制树深度(max_depth)或提高叶子节点最小样本数(min_samples_leaf)通常效果更好。 -
忽略特征重要性的解释:特征重要性分数(基于基尼不纯度减少量)可以提供有价值的见解,但它不是因果关系。一个高重要性的特征可能只是与其它特征或目标变量有强相关性。不要草率下结论。
-
盲目增加树的数量:
n_estimators越大,模型性能通常越稳定,但计算开销和内存占用也线性增加。当性能提升出现“平台期”后,继续增加树的数量收益甚微。应通过学习曲线(如示例代码中所示)来选择一个合适的值。 -
混淆随机森林与 Bagging 决策树:
sklearn中的BaggingClassifier配合DecisionTreeClassifier可以实现 Bagging,但默认情况下它不会在节点分裂时引入随机特征选择。要得到随机森林,必须使用RandomForestClassifier或RandomForestRegressor。
小结
- 集成学习与 Bagging:通过组合多个“弱”或“强”但不同的模型,可以构建一个更强大、更稳健的集成模型。Bagging 通过对训练数据进行自助采样并行构建多个模型,然后进行投票或平均,核心作用是降低模型方差。
- 随机森林:是 Bagging 的一种高效变体,它在 Bagging 的“样本随机”基础上,增加了“节点分裂时的特征随机”。这种双重随机性极大地增强了模型的多样性,进一步降低了方差和过拟合风险。
- 优势:随机森林通常开箱即用,性能强大,对数据缩放不敏感,并能直接提供特征重要性评估。
- 关键参数:
n_estimators(树的数量)、max_depth(树的最大深度)、min_samples_split/min_samples_leaf(节点分裂的条件)是需要关注和调整的主要超参数。 - 实践要点:构建模型后,务必进行完整的评估(准确率、F1-score、混淆矩阵等),并利用特征重要性进行初步的模型解释。使用交叉验证和网格搜索来找到更优的参数组合。
下一课,我们将学习另一种强大而不同的分类算法——支持向量机(SVM),它试图找到将数据分开的“最优”边界。