哈喽,大家好~
今儿再来和大家详细的聊聊随机森林~
首先,基础大家已经非常清楚了~
随机森林是一种基于决策树的集成学习算法,它通常采用袋装法与随机子空间(随机特征选择)来降低模型方差,提高泛化能力,同时在多种问题(分类与回归)中表现稳定,且内置特征重要性评估与一定程度的鲁棒性。
直观上来看,随机森林通过训练多个弱学习器(通常是CART决策树),然后将这些基学习器的预测进行投票(分类)或平均(回归),以抵消个体模型由于数据抽样与特征选择所导致的不稳定性。每棵树使用一个自助样本(bootstrap sample)进行训练,同时在每个节点分裂时随机选择一个特征子集进行评估,这两种随机性共同减少树之间的相关性,从而降低总体方差。
理论表述与推理
我们可以设有训练数据集 ,其中 为特征向量, 为目标变量(分类问题中 ;回归问题中 )。
随机森林的目标是构造一个函数 ,在未知样本上获得尽可能低的泛化误差。
随机森林由 棵独立训练的决策树组成。记第 棵树为 ,其训练数据为 ,通过bootstrap从 中进行带放回抽样获得。
记特征子空间大小为 (分类常用 ,回归常用 左右,具体实现可能有所不同)。
决策树的分裂与叶子预测
CART决策树在分类问题中常用Gini不纯度或信息熵作为节点分裂的准则;在回归问题中常用方差或均方误差(MSE)作为分裂的准则。
分类的Gini不纯度:
其中 是节点 中类别 的比例。
分类的信息熵:
回归的MSE(在节点 上的经验风险):
其中 是节点 上目标的均值。
当考察某一分裂 (例如选择某个特征 与阈值 )时,分裂的“信息增益”可定义为:
对分类(以Gini为例):
其中 是分裂后左右子节点的样本集合。
对回归(以MSE为例):
在分类叶子节点中,若不做平滑,概率估计可简单为:
也可采用拉普拉斯平滑:
回归叶子节点的预测通常为样本均值:
袋装法与随机子空间
Bootstrap采样:每棵树 在训练时使用 ,其中:
即从 中进行 次带放回抽样。每个样本被选中的概率为 ,在 次抽样中未被选中的概率为:
因此每棵树约有 的样本未被采到,这些样本可用于袋外估计(Out-of-Bag, OOB)测试。
随机子空间:在每个节点进行分裂时,不是对所有 个特征进行评估,而是随机选择 个特征进行候选分裂,降低树与树之间的相关性。
随机森林的预测与投票机制
分类的硬投票(多数表决):
分类的软投票(概率平均):
回归的平均:
泛化误差、边际函数与强度-相关性理论
Breiman(2001)给出了随机森林的一个理论框架,将模型的泛化误差与树的“强度”(strength)和“相关性”(correlation)联系起来。
定义样本 的边际函数:
直观上,边际表示正确类别与最强竞争类别的投票差。
定义森林的强度 为边际的期望:
定义树之间的相关性 为不同树对同一输入的误差相关性。Breiman指出,当 时,随机森林的泛化误差 有上界与 和 相关(在理想化假设下):
该式表明提高树的强度(更好的弱学习器)和降低树之间的相关性(通过随机特征与bootstrap)均有助于降低整体误差。
此外,随机森林的泛化误差随树数量 增加通常会趋于稳定(即收敛),这是由于投票或平均的方差减小。对于回归,集成平均可显著降低方差,偏差则主要由基学习器的能力决定。
偏差-方差分解与相关性削减
对回归任务给出偏差-方差分解的一般形式:
随机森林通过引入随机性,使不同树之间的误差不高度相关,从而使平均后的方差下降更显著。设每棵树预测的方差为 ,两棵树的预测相关性为 ,则集成平均后方差近似为:
简化而言,降低 与增大 都能降低总体方差;当 小且 大时,方差显著降低。
袋外(OOB)估计
OOB估计利用每棵树未被采到的样本作为该树对应的测试集。对每个样本 ,考虑所有不包含它的树集合 ,则OOB预测为:
或概率平均形式。OOB误差是训练过程中无需额外验证集的自然估计,常被认为接近测试集误差,尤其当样本量较大时。
特征重要性:Gini与Permutation
基于Gini的重要性(也称为“Mean Decrease in Impurity”,MDI): 设第 个特征在某树 上参与分裂的节点集合为 ,每个节点分裂带来的不纯度减少为 (加权),则该特征在该树上的重要性:
森林整体重要性为:
基于置换的重要性:
在验证集或OOB样本上,将特征 随机打乱(破坏它与目标的关系),观察性能指标的下降。设原性能为 ,置换后性能为 ,则:
该方法倾向于更加稳健,可缓解MDI对高基数或连续特征的偏置。
部分依赖(PDP)与个体条件期望(ICE)
设模型为 ,特征集合记为 ,其补集为 。部分依赖函数定义为:
PDP的意义在于观察固定 时模型输出随 的变化趋势,平均掉其他特征 的影响。
ICE曲线针对单个样本 :
ICE可以展示不同个体的异质性,PDP则是ICE的平均。
在分类时,通常分析某一目标类别 的预测概率的PDP与ICE,即:
实战案例
本案例使用一个多类分类数据集,涵盖:
-
数据生成(含非线性特征) -
训练/验证/超参搜索 -
指标评估与特征重要性分析 -
可视化:决策边界、混淆矩阵、特征重要性(MDI与Permutation)、PDP+ICE
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score, RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
classification_report, confusion_matrix, accuracy_score,
roc_auc_score
)
from sklearn.inspection import permutation_importance, PartialDependenceDisplay
from matplotlib.colors import ListedColormap
# 设置随机种子与全局风格
RANDOM_STATE = 42
np.random.seed(RANDOM_STATE)
# 1. 生成多类数据集
n_samples = 2600
X, y = make_classification(
n_samples=n_samples,
n_features=10,
n_informative=5,
n_redundant=2,
n_repeated=0,
n_classes=3,
n_clusters_per_class=2,
weights=[0.45, 0.35, 0.20], # 类不平衡
flip_y=0.03, # 噪声标签
class_sep=1.2, # 类间可分性
random_state=RANDOM_STATE
)
# 增加两个非线性派生特征
# 选择前两个信息较强的维度进行构造
f1 = X[:, 0]
f2 = X[:, 1]
nonlin1 = np.sin(1.5 * f1) * np.cos(1.2 * f2) + 0.1 * np.random.randn(n_samples)
nonlin2 = (f1 * f2) / (1 + np.abs(f1)) + 0.05 * np.random.randn(n_samples)
# 合并成最终特征矩阵
X_full = np.hstack([X, nonlin1.reshape(-1, 1), nonlin2.reshape(-1, 1)])
feature_names = [f"feat_{i}" for i in range(X.shape[1])] + ["nonlin_sin_cos", "nonlin_interact"]
df = pd.DataFrame(X_full, columns=feature_names)
df["label"] = y
print("数据维度:", df.shape)
print("类别分布:", df["label"].value_counts().to_dict())
# 2. 训练/测试划分
X_train, X_test, y_train, y_test = train_test_split(
X_full, y, test_size=0.25, stratify=y, random_state=RANDOM_STATE
)
# 可选:标准化(树模型不敏感,但为流水线可兼容)
# 这里仅用于后续可能的管道拓展(比如与其他模型比较时)
scaler = StandardScaler(with_mean=False) # with_mean=False 适用于可能很大的稀疏矩阵,这里纯演示
# 3. 随机森林模型,带OOB估计与类权重平衡
rf = RandomForestClassifier(
n_estimators=300,
criterion="gini",
max_depth=None,
min_samples_split=2,
min_samples_leaf=2,
max_features="sqrt",
bootstrap=True,
oob_score=True,
class_weight="balanced",
random_state=RANDOM_STATE,
n_jobs=-1
)
# 4. 拟合
rf.fit(X_train, y_train)
print(f"OOB Score: {rf.oob_score_:.4f}")
# 5. 测试集评估
y_pred = rf.predict(X_test)
acc = accuracy_score(y_test, y_pred)
print(f"Test Accuracy: {acc:.4f}")
print("Classification Report:")
print(classification_report(y_test, y_pred, digits=4))
# 6. 交叉验证(宏平均 F1)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)
cv_scores = cross_val_score(rf, X_full, y, cv=cv, scoring="f1_macro", n_jobs=-1)
print(f"CV F1-macro: mean={cv_scores.mean():.4f}, std={cv_scores.std():.4f}")
# 7. 超参数随机搜索(为演示稳定性,迭代次数适中)
param_dist = {
"n_estimators": [200, 300, 400, 600],
"max_depth": [None, 10, 20, 30],
"min_samples_split": [2, 5, 10],
"min_samples_leaf": [1, 2, 4],
"max_features": ["sqrt", 4, 6, 8],
"bootstrap": [True]
}
rf_base = RandomForestClassifier(
oob_score=True, class_weight="balanced", random_state=RANDOM_STATE, n_jobs=-1
)
rand_search = RandomizedSearchCV(
rf_base, param_dist, n_iter=20, scoring="f1_macro", cv=cv, n_jobs=-1, random_state=RANDOM_STATE, verbose=1
)
rand_search.fit(X_train, y_train)
best_rf = rand_search.best_estimator_
print("Best Params:", rand_search.best_params_)
print(f"Best CV score (F1-macro): {rand_search.best_score_:.4f}")
# 用最优模型在测试集评估
y_pred_best = best_rf.predict(X_test)
acc_best = accuracy_score(y_test, y_pred_best)
print(f"Best RF Test Accuracy: {acc_best:.4f}")
print("Best RF Classification Report:")
print(classification_report(y_test, y_pred_best, digits=4))
# 8. 特征重要性(MDI)与Permutation
mdi_importances = best_rf.feature_importances_
mdi_df = pd.DataFrame({"feature": feature_names, "importance_mdi": mdi_importances}).sort_values(
"importance_mdi", ascending=False
)
perm_result = permutation_importance(best_rf, X_test, y_test, scoring="f1_macro", n_repeats=7, random_state=RANDOM_STATE, n_jobs=-1)
perm_imp = perm_result.importances_mean
perm_df = pd.DataFrame({"feature": feature_names, "importance_perm": perm_imp}).sort_values(
"importance_perm", ascending=False
)
# 合并两个重要性结果
imp_merged = mdi_df.merge(perm_df, on="feature", how="outer")
imp_merged.fillna(0, inplace=True)
imp_merged.sort_values(["importance_mdi", "importance_perm"], ascending=False, inplace=True)
# 选出最佳两个特征用于绘制决策边界
top2 = imp_merged.head(2)["feature"].tolist()
print("Top 2 features for decision boundary:", top2)
fidx = [feature_names.index(top2[0]), feature_names.index(top2[1])]
# 9. 预测概率用于PDP(选择一个目标类,比如类1)
target_class = 1
# 10. 四图合一可视化
fig = plt.figure(figsize=(18, 14))
gs = fig.add_gridspec(2, 2, hspace=0.25, wspace=0.25)
# 子图A:决策边界(使用Top2特征)
ax1 = fig.add_subplot(gs[0, 0])
x_min, x_max = X_full[:, fidx[0]].min() - 0.5, X_full[:, fidx[0]].max() + 0.5
y_min, y_max = X_full[:, fidx[1]].min() - 0.5, X_full[:, fidx[1]].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 300), np.linspace(y_min, y_max, 300))
grid = np.c_[xx.ravel(), yy.ravel()]
# 为了在二维上预测,用一个投影模型:复制其他特征均值或中位数
X_proxy = np.zeros((grid.shape[0], X_full.shape[1]))
X_proxy[:] = X_full.mean(axis=0)
X_proxy[:, fidx[0]] = grid[:, 0]
X_proxy[:, fidx[1]] = grid[:, 1]
Z = best_rf.predict(X_proxy)
Z = Z.reshape(xx.shape)
cmap_light = ListedColormap(["#FF6F61", "#6B5B95", "#88B04B"])
cmap_bold = ListedColormap(["#FF3000", "#0047AB", "#00A676"])
ax1.contourf(xx, yy, Z, alpha=0.35, cmap=cmap_light)
# 绘制训练/测试散点
for cls, color in zip(np.unique(y_train), ["#FF3000", "#0047AB", "#00A676"]):
idx = y_train == cls
ax1.scatter(X_train[idx, fidx[0]], X_train[idx, fidx[1]], c=color, label=f"Train class {cls}", s=18, edgecolor="k", alpha=0.85)
for cls, marker in zip(np.unique(y_test), ["^", "s", "D"]):
idx = y_test == cls
ax1.scatter(X_test[idx, fidx[0]], X_test[idx, fidx[1]], c="none", edgecolor="black", marker=marker, label=f"Test class {cls}", s=60, linewidths=1.2)
ax1.set_title(f"A. 决策边界(Top2特征:{top2[0]} vs {top2[1]})", fontsize=18)
ax1.set_xlabel(top2[0])
ax1.set_ylabel(top2[1])
ax1.legend(loc="upper right", frameon=True)
# 子图B:混淆矩阵
ax2 = fig.add_subplot(gs[0, 1])
cm = confusion_matrix(y_test, y_pred_best)
sns.heatmap(cm, annot=True, fmt="d", cmap="magma", cbar=True, ax=ax2, linewidths=1.5, linecolor="white")
ax2.set_title("B. 混淆矩阵(Test, Best RF)", fontsize=18)
ax2.set_xlabel("预测类别")
ax2.set_ylabel("真实类别")
# 子图C:特征重要性(MDI与Permutation并列柱状)
ax3 = fig.add_subplot(gs[1, 0])
top_k = 10
imp_plot = imp_merged.head(top_k).copy()
xpos = np.arange(top_k)
width = 0.38
ax3.bar(xpos - width/2, imp_plot["importance_mdi"], width=width, color="#FF6F61", edgecolor="black", label="MDI")
ax3.bar(xpos + width/2, imp_plot["importance_perm"], width=width, color="#88B04B", edgecolor="black", label="Permutation")
ax3.set_xticks(xpos)
ax3.set_xticklabels(imp_plot["feature"], rotation=30, ha="right")
ax3.set_title("C. 特征重要性对比(MDI vs Permutation)", fontsize=18)
ax3.set_ylabel("重要性分数")
ax3.legend(frameon=True)
# 子图D:PDP + ICE(针对某个目标类和Top1特征)
ax4 = fig.add_subplot(gs[1, 1])
# 使用sklearn的PartialDependenceDisplay
# kind="both" 会同时展示PDP与ICE曲线;classification下默认使用目标类别的概率
X_test_df = pd.DataFrame(X_test, columns=feature_names)
PartialDependenceDisplay.from_estimator(
best_rf, X_test_df, [top2[0]], target=target_class, kind="both", ax=ax4, line_kw={"color": "#0047AB"},
ice_lines_kw={"color": "#FF6F61", "alpha": 0.25}
)
ax4.set_title(f"D. PDP + ICE(目标类={target_class}, 特征={top2[0]})", fontsize=18)
plt.suptitle("随机森林分析", fontsize=22, y=0.98)
plt.tight_layout()
plt.show()
代码中,我们使用了三类数据,具有不平衡性与噪声标签,构造了两个非线性特征nonlin_sin_cos与nonlin_interact。
模型使用oob_score=True进行袋外估计,对特征重要性采用两种方法:MDI(Gini减小)与Permutation(置换重要性)。
可视化分析
决策边界:
展示在Top2特征空间中,随机森林的分类边界如何划分不同类别,背景颜色表示模型预测的类别,训练点与测试点分别以不同样式展现。
如果边界过于复杂(呈现高度碎裂),可能暗示模型在该特征空间中存在过拟合或非线性过强。若测试点聚集在边界附近且误差较多,可能需要增加样本或进行特征工程来提升可分性。
混淆矩阵:
直观呈现各类别的误判情况。例如某个类别常被误判为另一个类别。
问题说明:若对某个类别的误判显著偏高(例如第三类被大量误判为第一类),可能存在类间分布重叠、样本数不足或类不平衡,需考虑调整class_weight、采样策略或特征工程。
特征重要性:MDI vs Permutation:
左右柱展示两种重要性评估方法的对比,MDI可能偏向容易产生较多分裂的连续特征与高基数特征;Permutation更稳健,因为它直接衡量性能下降。
若两者差异明显,需谨慎使用MDI作解释,建议以Permutation为主,或综合两者结论。还可进一步采用交叉验证下的Permutation平均结果更稳健。
PDP + ICE:
PDP曲线展示目标类的预测概率随Top1特征变化的平均趋势,ICE曲线展示各个样本的个体变化趋势(透明红线)。
若ICE曲线分布高度分散,说明样本间存在强烈异质性,PDP的平均趋势可能掩盖重要个体差异;需要考虑交互特征或分段建模。如果PDP显示单调关系但误差高,可能存在某些条件依赖结构被平均掩盖。
优化项与注意点
数据质量与特征工程
处理异常值与缺失值:scikit-learn的树不原生支持缺失值分裂(相较于某些GBDT实现),应在前处理阶段填充或建模缺失模式。
类别特征:需要One-Hot或Ordinal编码;若类别层级很多(高基数),MDI可能高估该特征重要性,Permutation更可信。
派生特征:加入有意义的非线性与交互项,有助于树学习结构化分裂,提高强度 。
类不平衡
class_weight或重新采样(如SMOTE)可缓解少数类召回不足问题。
指标选择:准确率在不平衡下不可靠,建议使用宏平均F1、ROC-AUC(OVR)或PR-AUC。
超参数调优建议
n_estimators:足够大使误差收敛;太小会不稳定,太大增时耗但一般不会过拟合,可根据OOB曲线或验证集观察收敛趋势。
max_depth, min_samples_leaf:适度限制深度与叶子样本数以避免过拟合。
max_features:分类建议sqrt(d),回归建议d/3;可根据验证集调优。
bootstrap与oob_score:保留True以便OOB估计。
可解释性与稳定性
特征重要性需结合Permutation与MDI并参照多次重复结果,避免偶然性。
使用PDP与ICE时注意特征间相关性与条件分布问题。PDP假设条件独立,在强相关特征下可能产生偏差,可考虑累积局部效应(ALE)作为替代。
资源与并行
设置n_jobs=-1以利用多核并行,提升训练速度。
大数据集上可能内存紧张,注意批量训练与压缩存储。
数据泄露与评估偏差
超参数搜索应严格使用训练集与交叉验证,不要泄露测试集信息。
使用Permutation重要性时要在独立的验证集或OOB上进行,避免乐观偏差。
与深度学习的关系
在结构化表格数据(tabular)中,随机森林往往是强基线;对于图像、文本、语音等数据,深度学习表现更好。
树模型对缩放不敏感,但深度学习通常需要标准化与归一化,且训练成本更高。
总结
随机森林通过将多个决策树进行袋装与随机子空间构造,形成一个鲁棒、稳定且易用的集成模型。
咱们整个的从理论到实践全面展示了随机森林的数学基础,比如分裂准则、投票机制、OOB估计、特征重要性与PDP/ICE与性能分析,比如:偏差-方差分解、强度-相关性理论。
优化建议涉及数据质量、类不平衡、可解释性、资源并行与评估严格性等方面。随机森林常作为结构化数据上的强基线模型,具备稳健性与良好解释能力,在实践中应结合Permutation重要性、交叉验证与OOB来进行稳健评估。若追求极致精度或处理更为复杂的数据类型,可考虑与GBDT或深度学习方法配合使用~

