随机森林算法教程
1. 背景溯源
随机森林(Random Forest, RF)是集成学习 (Ensemble Learning)领域的里程碑算法,其诞生源于对单决策树局限性的系统性突破,以及对**"多样性-准确性"核心权衡**的深刻实践。
1.1 决策树的固有局限
经典决策树(如ID3、C4.5、CART)通过递归分裂节点构建树结构,具有可解释性强、天然处理非线性数据的优势,但存在致命缺陷:
- 过拟合风险高:当树深度无限制时,会拟合训练数据中的噪声(如异常值、随机波动),导致对新数据的泛化能力急剧下降;
- 稳定性差:训练数据或特征的微小变化,可能导致树结构发生巨大改变("蝴蝶效应"),预测结果波动大。
1.2 集成学习的破局思路
集成学习的核心逻辑是:"三个臭皮匠,顶个诸葛亮" ------通过组合多个弱学习器 (Weak Learner,性能略优于随机猜测的模型),抵消单个模型的偏差或方差,最终形成强学习器(Strong Learner)。
弱学习器的"多样性"是集成效果的关键:若弱学习器之间预测误差不相关 ,集成后的误差会显著降低。例如,100个独立弱学习器(错误率5%)集成后,错误率可降至10−710^{-7}10−7量级(二项分布推导)。
1.3 随机森林的诞生:Bagging的"升级版"
2001年,Leo Breiman在《Machine Learning》期刊发表《Random Forests》一文,提出随机森林算法。它是**Bagging(Bootstrap Aggregating)**的扩展:
- Bagging仅通过"样本随机"(Bootstrap抽样)保证弱学习器多样性;
- 随机森林在"样本随机"基础上,增加**"特征随机"**(每个节点分裂时随机选特征子集),进一步提升多样性,最终成为"工业界最稳健的算法之一"。
2. 核心思想:"两随机+一集成"
随机森林的本质是**"多棵随机化CART树的Bagging集成",核心可概括为"两随机+一集成"**:
2.1 样本随机:Bootstrap抽样(样本多样性来源)
为每棵树生成独立的训练集 :对原始训练集DDD(含nnn个样本),有放回地抽取nnn个样本,得到Bootstrap样本集DtD_tDt(第ttt棵树的训练集)。
- 关键性质 :当n→∞n \to \inftyn→∞时,每个样本被抽中的概率为1−(1−1n)n→1−1e≈63.2%1 - (1-\frac{1}{n})^n \to 1 - \frac{1}{e} \approx 63.2\%1−(1−n1)n→1−e1≈63.2%(1e\frac{1}{e}e1是自然常数的倒数)。因此,DtD_tDt约包含原始样本的63.2%(独特样本),剩余36.8%未被抽取的样本称为OOB样本(Out-of-Bag,袋外样本)。
- 作用:通过样本的随机性,降低树之间的相关性(不同树的训练数据重叠率约63.2%,但不完全相同)。
2.2 特征随机:随机子空间法(特征多样性来源)
每棵树的每个节点分裂时 ,仅从所有特征中随机选择mmm个特征(m≪dm \ll dm≪d,ddd为总特征数),作为该节点分裂的候选特征集。
- mmm的经验选择 :
- 分类问题:m=dm = \sqrt{d}m=d (如d=100d=100d=100,m=10m=10m=10);
- 回归问题:m=d3m = \frac{d}{3}m=3d(如d=100d=100d=100,m=33m=33m=33)。
- 作用:限制树的特征选择范围,避免单棵树依赖"强特征"(如高度相关的特征),进一步提升树的多样性(不同树的特征选择路径差异大)。
2.3 森林集成:多棵CART树的"集体决策"
随机森林由多棵未剪枝的CART树 组成(CART是二叉树,支持分类与回归,且适合并行训练)。单棵树充分生长(允许过拟合),但通过**多数投票(分类)或平均(回归)**的集成策略,抵消单棵树的过拟合误差,最终输出稳定、泛化能力强的结果。
2. 核心理论与公式推导
2.1 Bootstrap抽样的数学性质
Bootstrap抽样是有放回简单随机抽样(Simple Random Sampling with Replacement, SRSWR),其关键性质可通过概率推导验证:
性质1:样本被抽中的概率
对原始样本xix_ixi,单次抽样未被选中的概率为1−1n1 - \frac{1}{n}1−n1。nnn次抽样均未被选中的概率为:P(未被选中)=(1−1n)n P(\text{未被选中}) = \left(1 - \frac{1}{n}\right)^n P(未被选中)=(1−n1)n
当n→∞n \to \inftyn→∞时,由重要极限limn→∞(1+an)n=ea\lim_{n \to \infty} (1 + \frac{a}{n})^n = e^alimn→∞(1+na)n=ea,可得:P(未被选中)→e−1≈0.368 P(\text{未被选中}) \to e^{-1} \approx 0.368 P(未被选中)→e−1≈0.368
因此,样本被抽中的概率为:P(被选中)=1−P(未被选中)→1−e−1≈0.632 P(\text{被选中}) = 1 - P(\text{未被选中}) \to 1 - e^{-1} \approx 0.632 P(被选中)=1−P(未被选中)→1−e−1≈0.632
性质2:OOB样本的无偏性
OOB样本是未参与第ttt棵树训练的样本(约占36.8%)。对任意样本xix_ixi,其OOB预测由所有未使用xix_ixi训练的树共同给出。
由于OOB样本未参与树的训练,其预测结果与真实标签的误差是泛化误差的无偏估计(无需额外验证集)。
2.2 决策树分裂准则的数学推导
随机森林的基学习器是未剪枝的CART树(二叉分类/回归树),其节点分裂准则是树"强度"的核心来源。以下对分裂准则进行严格推导:
2.2.1 分类问题:Gini指数
Gini指数衡量节点的"不纯度"(节点中样本类别的混杂程度),定义为:
G(S)=1−∑k=1Kpk2 G(S) = 1 - \sum_{k=1}^K p_k^2 G(S)=1−k=1∑Kpk2
其中:
- SSS为节点样本集;
- KKK为类别总数;
- pk=∣Sk∣∣S∣p_k = \frac{|S_k|}{|S|}pk=∣S∣∣Sk∣为节点SSS中类别kkk的样本占比(SkS_kSk是SSS中类别kkk的子集)。
物理意义 :随机从节点SSS中选两个样本,它们属于不同类别的概率(不纯度越高,概率越大)。
当用特征AAA分裂节点SSS为左子节点SLS_LSL和右子节点SRS_RSR时,分裂后的Gini指数为加权平均(权重为子节点样本占比):
G(A,S)=∣SL∣∣S∣G(SL)+∣SR∣∣S∣G(SR) G(A, S) = \frac{|S_L|}{|S|} G(S_L) + \frac{|S_R|}{|S|} G(S_R) G(A,S)=∣S∣∣SL∣G(SL)+∣S∣∣SR∣G(SR)
分裂规则 :选择使G(A,S)G(A, S)G(A,S)最小的特征AAA(分裂后节点最纯)。
2.2.2 分类问题:信息增益
信息增益基于熵(Entropy)(衡量节点的"不确定性"),熵的定义为:
H(S)=−∑k=1Kpklog2pk H(S) = -\sum_{k=1}^K p_k \log_2 p_k H(S)=−k=1∑Kpklog2pk
其中,pkp_kpk的定义与Gini指数一致。
物理意义 :节点SSS中样本类别的平均不确定性(熵越大,不确定性越高;若所有样本属于同一类,熵为0)。
信息增益是分裂前熵与分裂后加权熵的差值(衡量分裂对"不确定性减少"的贡献):
IG(A,S)=H(S)−(∣SL∣∣S∣H(SL)+∣SR∣∣S∣H(SR)) IG(A, S) = H(S) - \left( \frac{|S_L|}{|S|} H(S_L) + \frac{|S_R|}{|S|} H(S_R) \right) IG(A,S)=H(S)−(∣S∣∣SL∣H(SL)+∣S∣∣SR∣H(SR))
分裂规则 :选择使IG(A,S)IG(A, S)IG(A,S)最大的特征AAA(分裂后不确定性降低最多)。
2.2.3 回归问题:平方误差
回归问题中,节点的"纯度"用**平方误差(MSE)**衡量(反映样本标签的离散程度),定义为:
MSE(S)=1∣S∣∑i∈S(yi−yˉS)2 MSE(S) = \frac{1}{|S|} \sum_{i \in S} (y_i - \bar{y}_S)^2 MSE(S)=∣S∣1i∈S∑(yi−yˉS)2
其中:
- yiy_iyi为样本iii的真实标签;
- yˉ∗S=1∣S∣∑∗i∈Syi\bar{y}*S = \frac{1}{|S|} \sum*{i \in S} y_iyˉ∗S=∣S∣1∑∗i∈Syi为节点SSS的平均标签。
物理意义 :节点SSS中样本标签与平均标签的偏差平方和(MSE越小,标签越集中)。
当用特征AAA分裂节点SSS为SLS_LSL和SRS_RSR时,分裂后的MSE为:
MSE(A,S)=∣SL∣∣S∣MSE(SL)+∣SR∣∣S∣MSE(SR) MSE(A, S) = \frac{|S_L|}{|S|} MSE(S_L) + \frac{|S_R|}{|S|} MSE(S_R) MSE(A,S)=∣S∣∣SL∣MSE(SL)+∣S∣∣SR∣MSE(SR)
分裂规则 :选择使MSE(A,S)MSE(A, S)MSE(A,S)最小的特征AAA(分裂后标签离散程度最低)。
2.3 随机森林的泛化误差边界
Breiman在2001年的原论文中,对随机森林的泛化误差进行了严格证明,核心结论如下:
前提假设
- 单棵树的预测结果为ht(x)∈{−1,1}h_t(x) \in \{-1, 1\}ht(x)∈{−1,1}(二分类问题,标签y∈{−1,1}y \in \{-1, 1\}y∈{−1,1});
- 单棵树的强度 (Strength)定义为s=E[ht(x)y]s = \mathbb{E}[h_t(x) y]s=E[ht(x)y](s>0s > 0s>0表示单棵树的预测优于随机猜测);
- 两棵树的相关性 (Correlation)定义为ρ=corr(ht(x)y,hs(x)y)\rho = \text{corr}(h_t(x) y, h_s(x) y)ρ=corr(ht(x)y,hs(x)y)(ρ∈[0,1]\rho \in [0, 1]ρ∈[0,1],ρ\rhoρ越小,树的多样性越高)。
泛化误差边界
随机森林的泛化误差ϵ\epsilonϵ满足:
ϵ≤σ2+(1−s2)T+o(1T) \epsilon \leq \frac{\sigma^2 + (1 - s^2)}{T} + o\left(\frac{1}{T}\right) ϵ≤Tσ2+(1−s2)+o(T1)
其中:
- σ2=V[ht(x)y]\sigma^2 = \mathbb{V}[h_t(x) y]σ2=V[ht(x)y]为单棵树预测结果的方差;
- TTT为树的数量;
- o(1T)o\left(\frac{1}{T}\right)o(T1)为高阶无穷小(当TTT足够大时可忽略)。
结论解读
- 树数量的影响 :泛化误差随TTT的增加而降低(但当T>100T > 100T>100时,下降速度显著放缓,趋于收敛);
- 多样性的影响 :相关性ρ\rhoρ越小(树的多样性越高),泛化误差越低;
- 单棵树强度的影响 :强度sss越大(单棵树的准确性越高),泛化误差越低。
3. 模型求解完整步骤
随机森林的求解分为训练阶段 和预测阶段,每个阶段的细节需严格遵循"两随机+一集成"的核心逻辑。
3.1 训练阶段("森林的生长过程")
输入
- 训练集:D={(x1,y1),(x2,y2),...,(xn,yn)}D = \{(x_1, y_1), (x_2, y_2), ..., (x_n, y_n)\}D={(x1,y1),(x2,y2),...,(xn,yn)},其中xi∈Rdx_i \in \mathbb{R}^dxi∈Rd(ddd为特征数),yiy_iyi为标签(分类:yi∈{c1,c2,...,cK}y_i \in \{c_1, c_2, ..., c_K\}yi∈{c1,c2,...,cK};回归:yi∈Ry_i \in \mathbb{R}yi∈R);
- 超参数:树数量TTT、特征子集大小mmm(分类:m=dm = \sqrt{d}m=d ;回归:m=d3m = \frac{d}{3}m=3d)、节点分裂的最小样本数minsample\min_{\text{sample}}minsample(如5)。
输出
- 随机森林模型:H(x)=集成(h1(x),h2(x),...,hT(x))H(x) = \text{集成}(h_1(x), h_2(x), ..., h_T(x))H(x)=集成(h1(x),h2(x),...,hT(x)),其中ht(x)h_t(x)ht(x)为第ttt棵CART树。
训练流程
-
初始化树集合 :H=∅\mathcal{H} = \emptysetH=∅(空森林)。
-
循环生成每棵树 :对t=1t = 1t=1到TTT:
a. 生成Bootstrap样本集 :对原始训练集DDD进行有放回抽样,得到DtD_tDt(样本数与DDD相同,约63.2%的独特样本);
b. 构建未剪枝的CART树 :用DtD_tDt训练树ht(x)h_t(x)ht(x),过程如下:
i. 初始化根节点 :根节点N0N_0N0包含DtD_tDt的所有样本;
ii. 递归分裂节点 :对每个节点NNN:
- 停止条件判断 :若满足以下任一条件,标记为叶节点:
- 节点样本数≤minsample\leq \min_{\text{sample}}≤minsample;
- 分类问题:节点内所有样本的标签相同;
- 回归问题:节点内样本标签的方差≤ϵ\leq \epsilon≤ϵ(ϵ\epsilonϵ为极小值,如10−610^{-6}10−6);
- 叶节点预测值 :
- 分类:叶节点的预测为该节点中样本数最多的类别(多数投票);
- 回归:叶节点的预测为该节点中样本标签的平均值;
- 特征随机选择 :从ddd个特征中随机选择mmm个特征,记为FmF_mFm;
- 最优分裂特征与阈值选择 :
- 对FmF_mFm中的每个特征aaa,遍历所有可能的分裂阈值vvv(分类:特征aaa的不同取值;回归:特征aaa的分位数);
- 计算分裂后的准则值(分类:Gini指数或信息增益;回归:MSE);
- 选择使准则最优的(a∗,v∗)(a*, v*)(a∗,v∗)(分类:Gini最小或信息增益最大;回归:MSE最小);
- 分裂节点 :用(a∗,v∗)(a*, v*)(a∗,v∗)将节点NNN分裂为左子节点NLN_LNL(满足xa≤v∗x_a \leq v*xa≤v∗的样本)和右子节点NRN_RNR(满足xa>v∗x_a > v*xa>v∗的样本);
- 递归处理子节点 :对NLN_LNL和NRN_RNR重复步骤ii;
c. 将树加入森林 :H=H∪{ht(x)}\mathcal{H} = \mathcal{H} \cup \{h_t(x)\}H=H∪{ht(x)}。
- 停止条件判断 :若满足以下任一条件,标记为叶节点:
-
输出森林 :返回H\mathcal{H}H。
3.2 预测阶段("森林的集体决策")
输入
- 新样本:x∈Rdx \in \mathbb{R}^dx∈Rd;
- 随机森林模型:H={h1(x),h2(x),...,hT(x)}\mathcal{H} = \{h_1(x), h_2(x), ..., h_T(x)\}H={h1(x),h2(x),...,hT(x)}。
输出
- 预测结果:y^=H(x)\hat{y} = H(x)y^=H(x)。
预测流程
- 单棵树预测 :对每棵树ht(x)h_t(x)ht(x),输入xxx得到预测值y^t=ht(x)\hat{y}_t = h_t(x)y^t=ht(x);
- 集成策略 :
- 分类问题 :多数投票(Majority Vote)------统计所有y^∗t\hat{y}*ty^∗t的类别,选择出现次数最多的类别:y^=argmaxc∈{c1,...,cK}∑t=1TI(y^t=c) \hat{y} = \arg\max_{c \in \{c_1, \dots, c_K\}} \sum_{t=1}^T \mathbf{I}(\hat{y}_t = c) y^=argc∈{c1,...,cK}maxt=1∑TI(y^t=c)其中I(⋅)\mathbf{I}(\cdot)I(⋅)为指示函数(条件满足时为1,否则为0);
- 回归问题 :平均值(Mean Average)------计算所有y^∗t\hat{y}*ty^∗t的算术平均:y^=1T∑∗t=1Ty^t \hat{y} = \frac{1}{T} \sum*{t=1}^T \hat{y}_t y^=T1∑∗t=1Ty^t
4. 适用边界与局限性
4.1 适用场景
- 高维数据:特征随机选择可有效降低维度灾难的影响(无需预处理降维);
- 非线性问题:CART树的非线性结构可捕捉特征与标签的复杂关系;
- 鲁棒性要求高的场景:对异常值、缺失值(需预处理)、特征尺度不敏感(无需归一化);
- 特征选择任务:可通过"特征重要性"(如Gini重要性、Permutation Importance)识别关键特征;
- 并行计算场景:每棵树的训练独立,可通过并行化加速(如Spark MLlib的随机森林实现)。
4.2 局限性
- 可解释性差:"森林"是黑箱模型,无法像单决策树那样展示"特征-标签"的逻辑路径;
- 计算成本高 :训练TTT棵树的时间复杂度为O(T⋅n⋅d⋅logn)O(T \cdot n \cdot d \cdot \log n)O(T⋅n⋅d⋅logn)(nnn为样本数),当TTT或nnn很大时,训练时间较长;
- 实时预测慢:预测时需遍历所有树,延迟高于单棵决策树或线性模型;
- 线性关系的劣势:若特征与标签是严格线性关系,随机森林的性能可能不如线性回归/逻辑回归(线性模型更简洁,泛化能力更强);
- 噪声敏感:Bootstrap抽样会放大噪声样本的影响(噪声样本可能被多次抽中,导致多棵树学习到错误模式)。
5. 总结
随机森林是**"多样性与准确性权衡"**的完美实践:
- 通过样本随机+特征随机 最大化弱学习器的多样性(降低相关性ρ\rhoρ);
- 通过未剪枝的CART树 保证单棵树的强度(提升sss);
- 通过集成策略抵消单棵树的过拟合误差,最终输出稳定、泛化能力强的结果。
它是工业界最常用的"瑞士军刀"算法之一,广泛应用于分类、回归、特征选择、异常检测等任务,是机器学习工程师必须掌握的核心算法。
案例介绍
本案例模拟了一个银行客户贷款违约预测场景:已知2000个客户的4个特征(年龄、收入、贷款金额、贷款期限)和1个标签(违约=1/未违约=0),使用随机森林算法预测客户是否会违约。
python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix, roc_curve, auc, precision_recall_curve, average_precision_score
# ------------------------------
# 随机森林模型实现
# ------------------------------
def bootstrap_sample(X, y):
"""
生成Bootstrap抽样数据集
Parameters:
X: 特征矩阵 (n_samples, n_features)
y: 标签向量 (n_samples,)
Returns:
X_sample: 抽样后的特征矩阵 (n_samples, n_features)
y_sample: 抽样后的标签向量 (n_samples,)
"""
n_samples = X.shape[0]
# 随机生成有放回的索引
idx = np.random.choice(n_samples, n_samples, replace=True)
return X[idx], y[idx]
def calculate_gini(y):
"""
计算Gini指数(分类问题分裂准则)
Parameters:
y: 节点样本的标签向量
Returns:
gini: Gini指数值
"""
# 统计每个类别的样本数
unique_classes, counts = np.unique(y, return_counts=True)
prob = counts / len(y)
gini = 1 - np.sum(prob ** 2)
return gini
def find_best_split(X, y, n_features):
"""
寻找最优分裂特征和阈值
Parameters:
X: 特征矩阵 (n_samples, n_features)
y: 标签向量 (n_samples,)
n_features: 随机选择的特征数
Returns:
best_split: 最优分裂信息,包含特征索引和阈值
left_idx: 左子节点样本索引
right_idx: 右子节点样本索引
"""
n_samples, n_total_features = X.shape
best_gini = float('inf') # 最小化Gini指数
best_split = {'feature_idx': None, 'threshold': None}
left_idx, right_idx = None, None
# 随机选择n_features个特征
feature_idx = np.random.choice(n_total_features, n_features, replace=False)
for idx in feature_idx:
feature_values = X[:, idx]
# 获取特征的唯一值作为候选阈值
thresholds = np.unique(feature_values)
for thresh in thresholds:
# 分裂样本:左子节点<=阈值,右子节点>阈值
left_mask = feature_values <= thresh
right_mask = feature_values > thresh
# 跳过样本数为0的情况
if len(y[left_mask]) == 0 or len(y[right_mask]) == 0:
continue
# 计算分裂后的Gini指数
gini_left = calculate_gini(y[left_mask])
gini_right = calculate_gini(y[right_mask])
gini_split = (len(left_mask)/n_samples)*gini_left + (len(right_mask)/n_samples)*gini_right
# 更新最优分裂
if gini_split < best_gini:
best_gini = gini_split
best_split['feature_idx'] = idx
best_split['threshold'] = thresh
left_idx = left_mask
right_idx = right_mask
return best_split, left_idx, right_idx
class Node:
"""决策树节点类"""
def __init__(self):
self.is_leaf = False # 是否为叶节点
self.label = None # 叶节点的预测标签
self.left = None # 左子节点
self.right = None # 右子节点
self.feature_idx = None # 分裂特征索引
self.threshold = None # 分裂阈值
def build_cart_tree(X, y, n_features, min_samples=5):
"""
递归构建未剪枝的CART分类树
Parameters:
X: 特征矩阵 (n_samples, n_features)
y: 标签向量 (n_samples,)
n_features: 随机选择的特征数
min_samples: 节点分裂的最小样本数
Returns:
root: 构建完成的树的根节点
"""
n_samples = X.shape[0]
# 停止条件1: 样本数小于等于min_samples
# 停止条件2: 所有样本标签相同
if n_samples <= min_samples or len(np.unique(y)) == 1:
node = Node()
node.is_leaf = True
# 叶节点预测为出现次数最多的标签
node.label = np.bincount(y).argmax()
return node
# 寻找最优分裂
best_split, left_mask, right_mask = find_best_split(X, y, n_features)
# 若无法找到有效分裂(如所有特征值相同),则返回叶节点
if best_split['feature_idx'] is None:
node = Node()
node.is_leaf = True
node.label = np.bincount(y).argmax()
return node
# 构建当前节点
node = Node()
node.feature_idx = best_split['feature_idx']
node.threshold = best_split['threshold']
# 递归构建左子树和右子树
node.left = build_cart_tree(X[left_mask], y[left_mask], n_features, min_samples)
node.right = build_cart_tree(X[right_mask], y[right_mask], n_features, min_samples)
return node
def predict_single_tree(tree, x):
"""
使用单棵决策树预测样本
Parameters:
tree: 决策树的根节点
x: 单个样本的特征向量 (n_features,)
Returns:
pred: 预测标签
"""
current_node = tree
while not current_node.is_leaf:
# 根据特征值和阈值决定进入左/右子节点
if x[current_node.feature_idx] <= current_node.threshold:
current_node = current_node.left
else:
current_node = current_node.right
return current_node.label
class RandomForest:
"""随机森林类"""
def __init__(self, n_trees=100, n_features='sqrt', min_samples=5):
"""
初始化随机森林
Parameters:
n_trees: 树的数量
n_features: 每次分裂随机选择的特征数,可选'sqrt'(分类默认)或int
min_samples: 节点分裂的最小样本数
"""
self.n_trees = n_trees
self.n_features = n_features
self.min_samples = min_samples
self.trees = [] # 存储森林中的树
def fit(self, X, y):
"""
训练随机森林
Parameters:
X: 特征矩阵 (n_samples, n_features)
y: 标签向量 (n_samples,)
"""
n_total_features = X.shape[1]
# 确定每次分裂随机选择的特征数
if self.n_features == 'sqrt':
self.n_features = int(np.sqrt(n_total_features))
elif not isinstance(self.n_features, int):
raise ValueError("n_features must be 'sqrt' or int")
# 生成n_trees棵树
self.trees = []
for _ in range(self.n_trees):
# Bootstrap抽样
X_sample, y_sample = bootstrap_sample(X, y)
# 构建CART树
tree = build_cart_tree(X_sample, y_sample, self.n_features, self.min_samples)
self.trees.append(tree)
def predict(self, X):
"""
预测样本
Parameters:
X: 特征矩阵 (n_samples, n_features)
Returns:
predictions: 预测标签向量 (n_samples,)
"""
n_samples = X.shape[0]
# 获取所有树的预测结果
tree_predictions = np.array([[predict_single_tree(tree, X[i]) for tree in self.trees] for i in range(n_samples)])
# 多数投票
predictions = [np.bincount(tree_pred).argmax() for tree_pred in tree_predictions]
return np.array(predictions)
def predict_proba(self, X):
"""
预测样本属于各个类别的概率
Parameters:
X: 特征矩阵 (n_samples, n_features)
Returns:
probas: 概率矩阵 (n_samples, n_classes)
"""
n_samples = X.shape[0]
# 获取所有树的预测结果
tree_predictions = np.array([[predict_single_tree(tree, X[i]) for tree in self.trees] for i in range(n_samples)])
# 计算每个样本属于类别1的概率(假设二分类,标签为0和1)
# 统计每行中1出现的次数,除以树的总数
proba_1 = np.mean(tree_predictions == 1, axis=1)
proba_0 = 1 - proba_1
return np.column_stack((proba_0, proba_1))
# ------------------------------
# 案例数据与主程序
# ------------------------------
def main():
# 1. 生成模拟贷款违约数据
n_samples = 100 # 样本数
# 特征生成
age = np.random.randint(18, 70, n_samples) # 年龄:18-69岁
income = np.random.randint(2000, 50000, n_samples) # 收入:2000-49999元
loan_amount = np.random.randint(5000, 200000, n_samples) # 贷款金额:5000-199999元
loan_term = np.random.randint(12, 120, n_samples) # 贷款期限:12-119个月
X = np.column_stack((age, income, loan_amount, loan_term))
# 生成标签:违约=1,未违约=0(基于简单逻辑)
# 违约概率与年龄负相关、与贷款金额/期限正相关、与收入负相关
risk_score = (loan_amount / income) * (loan_term / 12) - (age / 10)
y = np.where(risk_score > 2.5, 1, 0) # 风险分数>2.5则违约
# 2. 划分训练集和测试集(8:2)
split_idx = int(n_samples * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
# 3. 训练随机森林模型
rf = RandomForest(n_trees=100, n_features='sqrt', min_samples=5)
rf.fit(X_train, y_train)
# 4. 预测与评估
y_pred = rf.predict(X_test)
accuracy = np.mean(y_pred == y_test)
# 5. 输出结果
print("随机森林模型评估结果:")
print(f"测试集准确率:{accuracy * 100:.2f}%")
# 6. 可视化混淆矩阵
# 计算混淆矩阵
cm = confusion_matrix(y_test, y_pred)
# 获取预测概率用于绘制ROC和PR曲线
y_prob = rf.predict_proba(X_test)[:, 1]
# 创建包含三个子图的画布
plt.figure(figsize=(18, 5))
# --- 子图1:混淆矩阵 ---
plt.subplot(1, 3, 1)
sns.set(font_scale=1.1)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Not Default (0)', 'Default (1)'],
yticklabels=['Not Default (0)', 'Default (1)'])
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix')
# --- 子图2:ROC曲线 ---
plt.subplot(1, 3, 2)
fpr, tpr, _ = roc_curve(y_test, y_prob)
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC)')
plt.legend(loc="lower right")
# --- 子图3:Precision-Recall曲线 ---
plt.subplot(1, 3, 3)
precision, recall, _ = precision_recall_curve(y_test, y_prob)
average_precision = average_precision_score(y_test, y_prob)
plt.plot(recall, precision, color='green', lw=2, label=f'PR curve (AP = {average_precision:.2f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve')
plt.legend(loc="lower left")
plt.grid(True, linestyle='--', alpha=0.7)
# 显示图表
plt.tight_layout()
plt.show()
if __name__ == "__main__":
main()
代码深度解析:基于随机森林的银行贷款违约预测模型
1. 导入库与模块说明
python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix, roc_curve, auc, precision_recall_curve, average_precision_score
- numpy:数值计算核心库,用于数组操作与随机数生成。
- matplotlib/seaborn:可视化库,用于绘制混淆矩阵、ROC曲线、PR曲线。
- sklearn.metrics:模型评估工具,提供分类任务常用的性能指标函数。
2. 随机森林核心组件实现
随机森林是Bagging集成算法 与CART分类树 的结合,核心特点是bootstrap抽样 和随机特征选择。
2.1 Bootstrap抽样(样本多样性)
python
def bootstrap_sample(X, y):
n_samples = X.shape[0]
idx = np.random.choice(n_samples, n_samples, replace=True) # 有放回抽样
return X[idx], y[idx]
- 原理 :从原始数据集(
n_samples个样本)中有放回地随机抽取n_samples个样本,生成新的训练集。 - 作用:保证每棵树的训练数据存在差异,降低树之间的相关性,避免过拟合。
- 代码细节 :
np.random.choice(..., replace=True)实现有放回抽样,idx为抽样索引,X[idx]和y[idx]为抽样后的特征与标签。
2.2 基尼指数计算(节点不纯度度量)
python
def calculate_gini(y):
unique_classes, counts = np.unique(y, return_counts=True)
prob = counts / len(y) # 各类样本占比
gini = 1 - np.sum(prob ** 2)
return gini
- 数学原理 :基尼指数用于衡量分类节点的不纯度 ,公式为:
\\text{Gini}(D) = 1 - \\sum_{k=1}\^{K} \\left( \\frac{\|D_k\|}{\|D\|} \\right)\^2
其中,DDD为节点样本集,DkD_kDk为节点中第kkk类样本子集,∣Dk∣/∣D∣|D_k|/|D|∣Dk∣/∣D∣为第kkk类样本的比例。 - 性质:Gini值越小,节点纯度越高(样本越集中于某一类);Gini=0时节点完全纯。
- 代码细节 :
np.unique(y, return_counts=True)统计各类样本数量,prob ** 2计算比例平方和,1 - 平方和得到基尼指数。
2.3 最优分裂寻找(特征与阈值选择)
python
def find_best_split(X, y, n_features):
n_samples, n_total_features = X.shape
best_gini = float('inf') # 目标:最小化基尼指数
best_split = {'feature_idx': None, 'threshold': None}
# 随机选择n_features个特征(随机森林的"随机"特性)
feature_idx = np.random.choice(n_total_features, n_features, replace=False)
for idx in feature_idx:
thresholds = np.unique(X[:, idx]) # 候选阈值:特征的唯一值
for thresh in thresholds:
left_mask = X[:, idx] <= thresh # 左子节点:<=阈值
right_mask = X[:, idx] > thresh # 右子节点:>阈值
if len(y[left_mask]) == 0 or len(y[right_mask]) == 0:
continue # 跳过空子集
# 分裂后基尼指数:加权平均左右子节点基尼
gini_split = (len(left_mask)/n_samples)*calculate_gini(y[left_mask]) + \
(len(right_mask)/n_samples)*calculate_gini(y[right_mask])
if gini_split < best_gini:
best_gini = gini_split
best_split['feature_idx'] = idx
best_split['threshold'] = thresh
left_idx = left_mask
right_idx = right_mask
return best_split, left_idx, right_idx
- 核心逻辑 :在随机选择的
n_features个特征 中,遍历每个特征的所有候选阈值,找到使分裂后基尼指数最小的特征与阈值组合。 - 随机特征选择:避免强特征主导所有树,增加树的多样性(随机森林的第二个"随机"特性)。
- 候选阈值:取特征的唯一值,避免重复计算无效阈值。
- 分裂规则:左子节点为<=阈值的样本,右子节点为>阈值的样本。
- 代码细节 :
left_mask和right_mask为布尔索引,用于划分左右子集;gini_split为加权平均基尼指数,权重为子集样本数占比。
2.4 决策树节点类(数据结构)
python
class Node:
def __init__(self):
self.is_leaf = False # 是否为叶节点
self.label = None # 叶节点预测标签
self.left = None # 左子节点
self.right = None # 右子节点
self.feature_idx = None# 分裂特征索引
self.threshold = None # 分裂阈值
- 结构 :树节点包含两类信息:
- 叶节点:
is_leaf=True,存储预测标签label; - 内部节点:
is_leaf=False,存储分裂特征feature_idx和阈值threshold,以及左右子节点指针。
- 叶节点:
2.5 CART分类树构建(递归生长)
python
def build_cart_tree(X, y, n_features, min_samples=5):
n_samples = X.shape[0]
# 停止条件1:样本数<=min_samples(预剪枝,防止过拟合)
# 停止条件2:所有样本标签相同(节点已纯)
if n_samples <= min_samples or len(np.unique(y)) == 1:
node = Node()
node.is_leaf = True
node.label = np.bincount(y).argmax() # 叶节点预测:多数投票
return node
# 寻找最优分裂
best_split, left_mask, right_mask = find_best_split(X, y, n_features)
# 无法分裂时返回叶节点(如所有特征值相同)
if best_split['feature_idx'] is None:
node = Node()
node.is_leaf = True
node.label = np.bincount(y).argmax()
return node
# 构建当前内部节点
node = Node()
node.feature_idx = best_split['feature_idx']
node.threshold = best_split['threshold']
# 递归构建左右子树
node.left = build_cart_tree(X[left_mask], y[left_mask], n_features, min_samples)
node.right = build_cart_tree(X[right_mask], y[right_mask], n_features, min_samples)
return node
- CART树:分类与回归树(Classification and Regression Tree)的简称,这里用于二分类任务。
- 停止条件 :
- 样本数<=
min_samples:预剪枝策略,避免节点样本过少导致过拟合; - 所有样本标签相同:节点已经完全纯,无需继续分裂。
- 样本数<=
- 叶节点预测 :
np.bincount(y).argmax()返回出现次数最多的标签(多数投票)。 - 递归生长:内部节点分裂后,递归处理左右子集,直到满足停止条件。
2.6 单棵树预测
python
def predict_single_tree(tree, x):
current_node = tree
while not current_node.is_leaf:
# 根据特征值与阈值决定子节点方向
if x[current_node.feature_idx] <= current_node.threshold:
current_node = current_node.left
else:
current_node = current_node.right
return current_node.label
- 预测逻辑:从根节点开始,根据当前节点的分裂特征和阈值,逐层向下遍历,直到到达叶节点,返回叶节点的预测标签。
3. 随机森林类封装
python
class RandomForest:
def __init__(self, n_trees=100, n_features='sqrt', min_samples=5):
self.n_trees = n_trees # 树的数量
self.n_features = n_features # 每次分裂随机选择的特征数
self.min_samples = min_samples # 节点分裂最小样本数
self.trees = [] # 森林中的树
def fit(self, X, y):
n_total_features = X.shape[1]
# 确定随机特征数:分类问题默认取总特征数的平方根
if self.n_features == 'sqrt':
self.n_features = int(np.sqrt(n_total_features))
# 训练n_trees棵树
for _ in range(self.n_trees):
X_sample, y_sample = bootstrap_sample(X, y) # Bootstrap抽样
tree = build_cart_tree(X_sample, y_sample, self.n_features, self.min_samples) # 构建树
self.trees.append(tree)
def predict(self, X):
# 所有树的预测结果:n_samples行 × n_trees列
tree_predictions = np.array([[predict_single_tree(tree, X[i]) for tree in self.trees] for i in range(X.shape[0])])
# 多数投票:每行取出现次数最多的标签
predictions = [np.bincount(tree_pred).argmax() for tree_pred in tree_predictions]
return np.array(predictions)
def predict_proba(self, X):
# 二分类:计算每个样本被预测为1的概率
tree_predictions = np.array([[predict_single_tree(tree, X[i]) for tree in self.trees] for i in range(X.shape[0])])
proba_1 = np.mean(tree_predictions == 1, axis=1) # 1的比例
proba_0 = 1 - proba_1 # 0的比例
return np.column_stack((proba_0, proba_1)) # 返回概率矩阵:n_samples行 × 2列
- 初始化参数 :
n_trees:森林中树的数量(默认100,数量越多模型越稳定,但计算量越大);n_features:每次分裂随机选择的特征数(分类问题默认取总特征数的平方根,回归问题默认取总特征数);min_samples:节点分裂的最小样本数(预剪枝参数)。
- fit方法 :
- 确定随机特征数(若为'sqrt'则计算总特征数的平方根);
- 循环生成
n_trees棵树:每棵树通过bootstrap抽样获取训练数据,调用build_cart_tree构建CART树。
- predict方法 :
- 收集所有树对每个样本的预测结果(
tree_predictions为二维数组); - 对每个样本的预测结果进行多数投票 (
np.bincount(...).argmax()),得到最终预测。
- 收集所有树对每个样本的预测结果(
- predict_proba方法 :
- 仅支持二分类:计算每个样本被
n_trees棵树预测为1的比例,作为类别1的概率;类别0的概率为1减去类别1的概率; - 返回概率矩阵(行对应样本,列对应类别)。
- 仅支持二分类:计算每个样本被
4. 案例数据与主程序流程
python
def main():
# 1. 生成模拟贷款违约数据
n_samples = 100 # 样本数
# 特征:年龄(18-69)、收入(2000-49999)、贷款金额(5000-199999)、贷款期限(12-119)
age = np.random.randint(18, 70, n_samples)
income = np.random.randint(2000, 50000, n_samples)
loan_amount = np.random.randint(5000, 200000, n_samples)
loan_term = np.random.randint(12, 120, n_samples)
X = np.column_stack((age, income, loan_amount, loan_term)) # 特征矩阵
# 标签生成:违约(1)/未违约(0),基于风险分数
risk_score = (loan_amount / income) * (loan_term / 12) - (age / 10) # 违约风险逻辑
y = np.where(risk_score > 2.5, 1, 0) # 风险分数>2.5则违约
# 2. 划分训练集/测试集(8:2)
split_idx = int(n_samples * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
# 3. 训练随机森林
rf = RandomForest(n_trees=100, n_features='sqrt', min_samples=5)
rf.fit(X_train, y_train)
# 4. 预测与评估
y_pred = rf.predict(X_test)
accuracy = np.mean(y_pred == y_test) # 准确率
print(f"测试集准确率:{accuracy * 100:.2f}%")
# 5. 可视化评估指标
cm = confusion_matrix(y_test, y_pred) # 混淆矩阵
y_prob = rf.predict_proba(X_test)[:, 1] # 类别1的预测概率
# 绘制3张子图:混淆矩阵、ROC曲线、PR曲线
plt.figure(figsize=(18, 5))
# 子图1:混淆矩阵
plt.subplot(1, 3, 1)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Not Default (0)', 'Default (1)'],
yticklabels=['Not Default (0)', 'Default (1)'])
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix')
# 子图2:ROC曲线(AUC)
plt.subplot(1, 3, 2)
fpr, tpr, _ = roc_curve(y_test, y_prob)
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--') # 随机猜测线
plt.title('Receiver Operating Characteristic (ROC)')
# 子图3:PR曲线(AP)
plt.subplot(1, 3, 3)
precision, recall, _ = precision_recall_curve(y_test, y_prob)
average_precision = average_precision_score(y_test, y_prob)
plt.plot(recall, precision, color='green', lw=2, label=f'PR curve (AP = {average_precision:.2f})')
plt.title('Precision-Recall Curve')
plt.tight_layout()
plt.show()
4.1 模拟数据生成
- 特征 :随机生成年龄、收入、贷款金额、贷款期限4个特征,构建
(n_samples, 4)的特征矩阵。 - 标签逻辑 :基于风险分数
risk_score生成违约标签。风险分数与"贷款金额/收入"(债务负担)、"贷款期限/12"(贷款年限)正相关,与"年龄/10"(年龄越大违约概率越低)负相关;当风险分数>2.5时标记为违约(y=1)。
4.2 数据集划分
- 按8:2比例将数据集分为训练集(前80%样本)和测试集(后20%样本),用于模型训练和验证。
4.3 模型训练与预测
- 初始化
RandomForest实例,调用fit方法训练模型,调用predict方法预测测试集,计算准确率(正确预测样本数占比)。
4.4 可视化评估
- 混淆矩阵:展示真阳性(TP)、真阴性(TN)、假阳性(FP)、假阴性(FN),直观反映模型分类结果。
- ROC曲线与AUC :
- 横轴为假阳性率(FPR),纵轴为真阳性率(TPR);
- AUC(曲线下面积)反映模型的整体区分能力,AUC越接近1,模型性能越好。
- PR曲线与AP :
- 横轴为召回率(Recall=TP/(TP+FN)),纵轴为精确率(Precision=TP/(TP+FP));
- AP(平均精确率)在样本不平衡场景下更能反映模型对少数类(违约类)的识别能力,AP越接近1,模型性能越好。
5. 关键技术点总结
- 随机森林原理 :通过Bootstrap抽样和随机特征选择构建多棵CART树,利用多数投票或概率平均得到最终预测,属于Bagging集成算法。
- CART树构建 :基于基尼指数寻找最优分裂,递归生长并通过预剪枝(
min_samples)防止过拟合。 - 评估指标选择 :
- 准确率:适合样本均衡场景;
- ROC-AUC:反映模型整体区分能力,适合正负样本比例接近的场景;
- PR-AUC/AP:适合样本不平衡场景(如违约样本为少数类)。
- 代码扩展性 :可通过调整
n_trees、n_features、min_samples等参数优化模型性能,也可扩展为回归任务(修改叶节点预测方式和分裂准则)。