随机森林
随机森林(Random Forest)是一种基于Bagging思想的集成学习方法,通过构建多棵决策树并综合其预测结果来提高分类和回归的准确性与稳定性。它在数学建模竞赛和实际工程中都有着广泛的应用。
基本概念
随机森林由Leo Breiman于2001年提出,其核心思想是“三个臭皮匠,顶个诸葛亮“——通过组合多个弱学习器(决策树)的预测结果,获得比单一学习器更优的泛化性能。
随机森林的两个关键“随机“机制:
- 样本随机:通过Bootstrap抽样为每棵树生成不同的训练集
- 特征随机:在每个节点分裂时,仅从随机选取的特征子集中选择最优分裂特征
Bagging原理
Bootstrap聚合
Bagging(Bootstrap Aggregating)是随机森林的理论基础。给定包含 \( n \) 个样本的训练集 \( D = {(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)} \),Bagging的步骤如下:
-
Bootstrap抽样:从 \( D \) 中有放回地随机抽取 \( n \) 个样本,生成新的训练子集 \( D_t \),重复 \( T \) 次得到 \( T \) 个训练子集
-
基学习器训练:在每个 \( D_t \) 上训练一个基学习器 \( h_t(x) \)
-
结果聚合:
- 分类任务:采用多数投票法 \[ H(x) = \arg\max_{y} \sum_{t=1}^{T} \mathbb{I}(h_t(x) = y) \]
- 回归任务:采用简单平均法 \[ H(x) = \frac{1}{T} \sum_{t=1}^{T} h_t(x) \]
Bootstrap抽样的统计性质
在一次Bootstrap抽样中,某个样本未被选中的概率为:
\[ P(\text{未被选中}) = \left(1 - \frac{1}{n}\right)^n \]
当 \( n \to \infty \) 时:
\[ \lim_{n \to \infty} \left(1 - \frac{1}{n}\right)^n = \frac{1}{e} \approx 0.368 \]
这意味着每次Bootstrap抽样大约有36.8%的样本不会被选中,这些样本被称为袋外(Out-of-Bag, OOB)样本。
Bagging降低方差的原理
假设每个基学习器的方差为 \( \sigma^2 \),基学习器之间的相关系数为 \( \rho \),则Bagging集成后的方差为:
\[ \text{Var}(H) = \rho \sigma^2 + \frac{1 - \rho}{T} \sigma^2 \]
当基学习器之间的相关性 \( \rho \) 较低时,集成后的方差能够显著降低。这正是随机森林引入特征随机选择的动机。
随机特征选择
特征子集的确定
在构建每棵决策树的每个内部节点时,随机森林不是从全部 \( p \) 个特征中选择最优分裂特征,而是先随机选取 \( m \) 个特征(\( m \leq p \)),然后从这 \( m \) 个特征中选择最优的进行分裂。
常用的 \( m \) 值选择策略:
- 分类问题:\( m = \lfloor \sqrt{p} \rfloor \)
- 回归问题:\( m = \lfloor p/3 \rfloor \)
- 其他经验值:\( m = \lfloor \log_2(p) + 1 \rfloor \)
随机特征选择的效果
引入特征随机性的效果:
- 降低树之间的相关性:不同的树使用不同的特征子集进行分裂,使得 \( \rho \) 更小
- 提高多样性:即使训练数据相同,不同的特征选择也会产生结构不同的树
- 计算效率:每次分裂只需评估 \( m \) 个特征,而非全部 \( p \) 个
随机森林算法流程
完整算法描述
输入:训练集 \( D = {(x_i, y_i)}_{i=1}^n \),树的数量 \( T \),特征子集大小 \( m \)
对 \( t = 1, 2, \ldots, T \):
- 从 \( D \) 中Bootstrap抽样得到 \( D_t \)
- 用 \( D_t \) 训练决策树 \( h_t \):
- 对每个节点,随机选择 \( m \) 个特征
- 从这 \( m \) 个特征中选择最优分裂点
- 递归生长树,直到满足停止条件
- 树不进行剪枝,充分生长
输出:集成分类器 \( H(x) = \arg\max_y \sum_{t=1}^T \mathbb{I}(h_t(x) = y) \)
节点分裂准则
常用的分裂准则包括:
基尼不纯度(Gini Impurity):
\[ \text{Gini}(D) = 1 - \sum_{k=1}^{K} p_k^2 \]
其中 \( p_k \) 是第 \( k \) 类样本在节点 \( D \) 中的比例。
信息增益(Information Gain):
\[ \text{Gain}(D, a) = \text{Ent}(D) - \sum_{v=1}^{V} \frac{|D^v|}{|D|} \text{Ent}(D^v) \]
其中 \( \text{Ent}(D) = -\sum_{k=1}^{K} p_k \log_2 p_k \) 为信息熵。
OOB误差估计
OOB误差的定义
对于随机森林中的第 \( t \) 棵树,未参与其训练的样本集合记为 \( D_t^{\text{oob}} \)。对于任意样本 \( (x_i, y_i) \),所有未使用该样本训练的树的集合为:
\[ \mathcal{T}_i^{\text{oob}} = {t : (x_i, y_i) \notin D_t} \]
该样本的OOB预测为:
\[ \hat{y}i^{\text{oob}} = \arg\max_y \sum{t \in \mathcal{T}_i^{\text{oob}}} \mathbb{I}(h_t(x_i) = y) \]
OOB误差率为:
\[ \text{OOB Error} = \frac{1}{n} \sum_{i=1}^{n} \mathbb{I}(\hat{y}_i^{\text{oob}} \neq y_i) \]
OOB误差的优势
- 无需额外验证集:OOB误差提供了对泛化误差的无偏估计,无需划分验证集
- 计算高效:在训练过程中即可得到,不增加额外计算成本
- 近似交叉验证:OOB误差估计与留一交叉验证的结果近似
超参数调优中的OOB
可以利用OOB误差来选择最优的超参数,如树的数量 \( T \) 和特征子集大小 \( m \):
\[ (T^, m^) = \arg\min_{T, m} \text{OOB Error}(T, m) \]
特征重要性
基于不纯度减少的重要性(MDI)
对于特征 \( j \),其重要性定义为所有树中该特征在所有节点上带来的不纯度减少的平均值:
\[ \text{Imp}(j) = \frac{1}{T} \sum_{t=1}^{T} \sum_{v \in \mathcal{V}_t^j} \frac{n_v}{n} \left[ \text{Gini}(D_v) - \frac{n_v^L}{n_v}\text{Gini}(D_v^L) - \frac{n_v^R}{n_v}\text{Gini}(D_v^R) \right] \]
其中 \( \mathcal{V}_t^j \) 是第 \( t \) 棵树中使用特征 \( j \) 进行分裂的节点集合。
基于排列的重要性(MDA)
排列重要性通过随机打乱特征 \( j \) 的值来衡量预测精度的下降:
\[ \text{Imp}{\text{perm}}(j) = \frac{1}{T} \sum{t=1}^{T} \left[ \text{OOB Error}_t^{(j)} - \text{OOB Error}_t \right] \]
其中 \( \text{OOB Error}_t^{(j)} \) 是对第 \( t \) 棵树的OOB样本中特征 \( j \) 的值随机排列后计算得到的误差率。
两种方法的比较
| 方法 | 优点 | 缺点 |
|---|---|---|
| MDI | 计算快速,内置于训练过程 | 对高基数特征有偏 |
| MDA | 更可靠,无偏 | 计算成本较高 |
实际案例分析
案例:信用卡欺诈检测
信用卡欺诈检测是一个典型的不平衡分类问题,随机森林在此类问题上表现优异。
问题描述:给定信用卡交易数据,判断每笔交易是否为欺诈行为。
数据特征:
- 交易金额、时间
- PCA变换后的匿名特征(V1-V28)
- 类别标签:0(正常)、1(欺诈)
- 欺诈比例通常不到0.2%
建模思路:
- 数据预处理:标准化交易金额,处理时间特征
- 处理不平衡:使用class_weight=’balanced’或SMOTE过采样
- 训练随机森林模型
- 使用OOB误差和交叉验证评估模型
- 分析特征重要性,理解欺诈模式
Python代码实现
基本实现
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import (
classification_report,
confusion_matrix,
roc_auc_score,
roc_curve,
)
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
# 生成模拟数据(以信用卡欺诈检测为例)
np.random.seed(42)
n_samples = 10000
n_fraud = 50 # 欺诈比例约0.5%
# 正常交易特征
normal_features = np.random.randn(n_samples - n_fraud, 10)
normal_labels = np.zeros(n_samples - n_fraud)
# 欺诈交易特征(均值偏移)
fraud_features = np.random.randn(n_fraud, 10) + 2
fraud_labels = np.ones(n_fraud)
# 合并数据
X = np.vstack([normal_features, fraud_features])
y = np.concatenate([normal_labels, fraud_labels])
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 特征标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 构建随机森林模型
rf_model = RandomForestClassifier(
n_estimators=500, # 树的数量
max_features='sqrt', # 每次分裂考虑的特征数
max_depth=None, # 不限制树的深度
min_samples_split=2, # 节点分裂最小样本数
min_samples_leaf=1, # 叶节点最小样本数
class_weight='balanced', # 处理不平衡数据
oob_score=True, # 计算OOB分数
random_state=42,
n_jobs=-1 # 使用所有CPU核心
)
# 训练模型
rf_model.fit(X_train_scaled, y_train)
# OOB评分
print(f"OOB准确率: {rf_model.oob_score_:.4f}")
# 测试集预测
y_pred = rf_model.predict(X_test_scaled)
y_pred_proba = rf_model.predict_proba(X_test_scaled)[:, 1]
# 评估结果
print("\n分类报告:")
print(classification_report(y_test, y_pred, target_names=['正常', '欺诈']))
print(f"\nAUC-ROC: {roc_auc_score(y_test, y_pred_proba):.4f}")
特征重要性分析
def plot_feature_importance(model, feature_names, top_n=10):
"""绘制特征重要性图"""
# 获取特征重要性(基于不纯度减少)
importances = model.feature_importances_
std = np.std(
[tree.feature_importances_ for tree in model.estimators_], axis=0
)
# 排序
indices = np.argsort(importances)[::-1][:top_n]
plt.figure(figsize=(10, 6))
plt.title("随机森林特征重要性")
plt.bar(
range(top_n),
importances[indices],
yerr=std[indices],
align="center",
alpha=0.8,
)
plt.xticks(range(top_n), [feature_names[i] for i in indices], rotation=45)
plt.xlabel("特征")
plt.ylabel("重要性(基尼不纯度减少)")
plt.tight_layout()
plt.savefig("feature_importance.png", dpi=150)
plt.show()
# 使用排列重要性(更可靠)
from sklearn.inspection import permutation_importance
perm_importance = permutation_importance(
rf_model, X_test_scaled, y_test, n_repeats=10, random_state=42, n_jobs=-1
)
# 输出排列重要性
feature_names = [f"Feature_{i}" for i in range(X.shape[1])]
for i in perm_importance.importances_mean.argsort()[::-1]:
if perm_importance.importances_mean[i] > 0.001:
print(
f"{feature_names[i]:<12}: "
f"{perm_importance.importances_mean[i]:.4f} "
f"+/- {perm_importance.importances_std[i]:.4f}"
)
超参数调优
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
# 定义参数搜索空间
param_grid = {
'n_estimators': [100, 300, 500, 800],
'max_features': ['sqrt', 'log2', 0.3, 0.5],
'max_depth': [None, 10, 20, 30],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4],
}
# 使用随机搜索(比网格搜索更高效)
rf_random = RandomizedSearchCV(
estimator=RandomForestClassifier(
class_weight='balanced', oob_score=True, random_state=42
),
param_distributions=param_grid,
n_iter=50,
cv=5,
scoring='roc_auc',
random_state=42,
n_jobs=-1,
verbose=1,
)
rf_random.fit(X_train_scaled, y_train)
print(f"最优参数: {rf_random.best_params_}")
print(f"最优AUC-ROC: {rf_random.best_score_:.4f}")
# 用最优参数重新评估
best_model = rf_random.best_estimator_
y_pred_best = best_model.predict(X_test_scaled)
print("\n最优模型分类报告:")
print(classification_report(y_test, y_pred_best, target_names=['正常', '欺诈']))
OOB误差曲线绘制
def plot_oob_error_curve(X_train, y_train, max_trees=500):
"""绘制OOB误差随树数量变化的曲线"""
oob_errors = []
tree_range = range(50, max_trees + 1, 50)
for n_trees in tree_range:
rf = RandomForestClassifier(
n_estimators=n_trees,
max_features='sqrt',
class_weight='balanced',
oob_score=True,
random_state=42,
n_jobs=-1,
)
rf.fit(X_train, y_train)
oob_errors.append(1 - rf.oob_score_)
plt.figure(figsize=(10, 6))
plt.plot(list(tree_range), oob_errors, 'b-o', linewidth=2)
plt.xlabel("决策树数量")
plt.ylabel("OOB误差率")
plt.title("OOB误差随决策树数量变化曲线")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("oob_error_curve.png", dpi=150)
plt.show()
return oob_errors
oob_errors = plot_oob_error_curve(X_train_scaled, y_train)
ROC曲线绘制
def plot_roc_curve(y_test, y_pred_proba):
"""绘制ROC曲线"""
fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
auc = roc_auc_score(y_test, y_pred_proba)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, 'b-', linewidth=2, label=f'Random Forest (AUC = {auc:.4f})')
plt.plot([0, 1], [0, 1], 'r--', linewidth=1, label='随机猜测')
plt.xlabel("假正率 (FPR)")
plt.ylabel("真正率 (TPR)")
plt.title("ROC曲线")
plt.legend(loc='lower right')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("roc_curve.png", dpi=150)
plt.show()
plot_roc_curve(y_test, y_pred_proba)
随机森林的理论分析
泛化误差上界
Breiman证明了随机森林泛化误差的上界为:
\[ PE^* \leq \frac{\bar{\rho}(1 - s^2)}{s^2} \]
其中:
- \( \bar{\rho} \) 是树之间的平均相关系数
- \( s \) 是森林中单棵树的分类强度(margin的期望)
这个上界表明:
- 降低树之间的相关性 \( \bar{\rho} \) 可以降低泛化误差
- 提高每棵树的强度 \( s \) 也可以降低泛化误差
- 两者之间存在权衡:增加特征随机性降低相关性但可能也降低强度
收敛性
随机森林的一个重要性质是:随着树的数量 \( T \to \infty \),随机森林不会过拟合。具体地,根据大数定律:
\[ \lim_{T \to \infty} PE_{\text{forest}} = P_{x,y}\left(\arg\max_j P_\Theta(h(x, \Theta) = j) \neq y\right) \]
即泛化误差收敛到一个确定的值,而非随着树的增加而持续增大。
应用注意事项与局限性
适用场景
- 高维数据:随机森林在高维数据上表现良好,能自动进行特征选择
- 非线性关系:能够捕捉复杂的非线性决策边界
- 不平衡数据:通过class_weight参数和采样策略处理不平衡
- 缺失值处理:可通过邻近度矩阵进行缺失值填补
- 异常值鲁棒:对异常值和噪声数据具有较强的鲁棒性
超参数选择建议
| 参数 | 建议范围 | 说明 |
|---|---|---|
| n_estimators | 100-1000 | 越多越好,但存在边际递减 |
| max_features | sqrt(p) 或 log2(p) | 分类用sqrt,回归用p/3 |
| max_depth | None 或 10-50 | 通常不限制 |
| min_samples_split | 2-10 | 较小值允许更深的树 |
| min_samples_leaf | 1-5 | 控制叶节点最小样本 |
局限性
- 可解释性较差:相比单棵决策树,随机森林难以直观解释
- 训练时间:大规模数据集上训练时间较长
- 存储空间:需要存储多棵完整决策树
- 线性关系表现一般:对于简单的线性关系,不如线性模型
- 外推能力弱:随机森林的预测值总是在训练数据的范围内,无法外推
- 高基数特征偏差:MDI特征重要性对高基数特征有偏好
与其他方法的比较
| 方法 | 优势 | 劣势 |
|---|---|---|
| 随机森林 | 鲁棒、不易过拟合、可并行 | 可解释性差、不能外推 |
| 单棵决策树 | 可解释性强 | 容易过拟合 |
| GBDT | 精度通常更高 | 容易过拟合、需仔细调参 |
| SVM | 高维空间效果好 | 大规模数据慢 |
| 神经网络 | 表达能力最强 | 需要大量数据和调参 |
实践建议
- 树的数量:从200-500棵树开始,观察OOB误差是否收敛
- 特征数量:先使用默认值(sqrt),再根据OOB误差调整
- 不平衡问题:优先使用class_weight=‘balanced’,严重不平衡时考虑过采样
- 特征工程:虽然随机森林对特征工程要求不高,但好的特征仍能提升性能
- 模型评估:同时使用OOB误差和交叉验证进行评估
- 特征重要性:优先使用排列重要性(MDA),而非默认的MDI
总结
随机森林是一种强大而实用的机器学习方法,其核心优势在于:
- 鲁棒性强:对噪声、异常值和不平衡数据都有较好的容忍度
- 调参简单:相比梯度提升方法,超参数更少且对默认值不敏感
- 不易过拟合:增加树的数量不会导致过拟合
- 可并行化:各棵树的训练相互独立,易于并行计算
- 特征重要性:内置的特征重要性评估有助于理解数据
在数学建模竞赛中,随机森林常作为基准模型使用,也常与其他方法结合形成更强的集成方案。