随机森林算法-数学建模优秀论文算法

随机森林算法教程

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→∞时,由重要极限lim⁡n→∞(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=1Kpklog⁡2pk 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足够大时可忽略)。
结论解读
  1. 树数量的影响 :泛化误差随TTT的增加而降低(但当T>100T > 100T>100时,下降速度显著放缓,趋于收敛);
  2. 多样性的影响 :相关性ρ\rhoρ越小(树的多样性越高),泛化误差越低;
  3. 单棵树强度的影响 :强度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)、节点分裂的最小样本数min⁡sample\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树。
训练流程
  1. 初始化树集合 :H=∅\mathcal{H} = \emptysetH=∅(空森林)。

  2. 循环生成每棵树 :对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:

    • 停止条件判断 :若满足以下任一条件,标记为叶节点:
      • 节点样本数≤min⁡sample\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)}。
  3. 输出森林 :返回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)。
预测流程
  1. 单棵树预测 :对每棵树ht(x)h_t(x)ht(x),输入xxx得到预测值y^t=ht(x)\hat{y}_t = h_t(x)y^t=ht(x);
  2. 集成策略
    • 分类问题 :多数投票(Majority Vote)------统计所有y^∗t\hat{y}*ty^∗t的类别,选择出现次数最多的类别:y^=arg⁡max⁡c∈{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⋅log⁡n)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_maskright_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. 关键技术点总结
  1. 随机森林原理 :通过Bootstrap抽样和随机特征选择构建多棵CART树,利用多数投票或概率平均得到最终预测,属于Bagging集成算法
  2. CART树构建 :基于基尼指数寻找最优分裂,递归生长并通过预剪枝(min_samples)防止过拟合。
  3. 评估指标选择
    • 准确率:适合样本均衡场景;
    • ROC-AUC:反映模型整体区分能力,适合正负样本比例接近的场景;
    • PR-AUC/AP:适合样本不平衡场景(如违约样本为少数类)。
  4. 代码扩展性 :可通过调整n_treesn_featuresmin_samples等参数优化模型性能,也可扩展为回归任务(修改叶节点预测方式和分裂准则)。
相关推荐
wa的一声哭了6 小时前
赋范空间 赋范空间的完备性
python·线性代数·算法·机器学习·数学建模·矩阵·django
您好啊数模君21 小时前
决策树模型-数学建模优秀论文算法
决策树·数学建模·决策树模型
算法如诗21 小时前
豆渣发酵工艺优化 - 基于响应面法结合遗传算法
数学建模
88号技师1 天前
2026年1月一区SCI-波动光学优化算法Wave Optics Optimizer-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
AI科技星1 天前
张祥前统一场论电荷定义方程分析报告
开发语言·经验分享·线性代数·算法·数学建模
老歌老听老掉牙1 天前
基于参数化模型的砂轮轮廓建模与可视化
python·数学建模
郝学胜-神的一滴1 天前
李航《机器学习方法》全面解析与高效学习指南
人工智能·python·算法·机器学习·数学建模·scikit-learn
光羽隹衡2 天前
集成学习之随机森林
随机森林·机器学习·集成学习
Sunsets_Red2 天前
待修改莫队与普通莫队优化
java·c++·python·学习·算法·数学建模·c#