2025全国大学生数学建模C题思路模型(开赛后第一时间更新),备战国赛,算法解析——随机森林 - 教程
2025全国大学生数学建模C题思路模型(开赛后第一时间更新),备战国赛,算法解析——随机森林
随机森林 (Random Forest) 算法详解:从原理到实战步骤 (数模小白友好版)
一、算法概述:为什么需要随机森林?
想象一个场景:如果你想预测明天会不会下雨,只问一个人(比如单棵决策树),可能会因为他的经验不足或偏见而判断错误;但如果问100个来自不同地区、不同职业的人(多棵决策树),最后取多数人的意见,结果往往更可靠。随机森林 (Random Forest, RF) 就是这个道理——它通过“集成多个,小专家’(决策树)的意见”来提高预测的准确性和稳定性。
核心思想:
随机森林是 集成学习 (Ensemble Learning) 中 Bagging (Bootstrap Aggregating) 框架的代表算法,由Leo Breiman在2001年提出。它通过两个“随机”操作构建多棵决策树,再集成所有树的结果:
样本随机:每棵树用不同的训练数据(通过有放回抽样获得);
特征随机:每棵树在做决策时只能用部分特征。
这样做的目的是让每棵树“独立又多样”,避免单棵决策树“过拟合”(把噪声当规律),最终通过“集体智慧”得到更稳定、更准确的预测。
二、算法原理:两个“随机”+决策树集成
2.1 样本随机抽样:Bootstrap抽样——给每棵树“喂不同的 饭”
要让100个“小专家”有不同的意见,首先得让他们学不同的“教材”。随机森林通过 Bootstrap抽样为每棵树生成独特的训练集:
什么是Bootstrap抽样?
从原始数据集 DDD (共 NNN 个样本)中,有放回地随机抽 NNN 个样本,组成新的训练集 Dk{D}_{k}Dk (第 kkk 棵树的“教材" )。
“有放回抽样” 是什么意思?
比如原始数据有样本A、B、C,抽样时可能抽到A、A、B(A被重复抽中, C没被抽中)。这样每棵树的训练集都会有重复样本,且包含原始数据的不同子集。
抽样特性:为什么总有36.8%的样本没被抽中?
假设原始数据有 NNN 个样本,某个样本一次没被抽中的概率是 (1−1N)\left( {1 - \frac{1}{N}}\right)(1−N1) (因为每次抽样有 NNN 个样本,抽中它的概率是 1N\frac{1}{N}N1 ,没抽中的概率是 1−1N1 - \frac{1}{N}1−N1 )。
抽 NNN 次后,这个样本仍没被抽中的概率是 (1−1N)N{\left( 1 - \frac{1}{N}\right) }^{N}(1−N1)N 。当 NNN 很大时(比如 N=1000N = {1000}N=1000 ), (1−1N)N≈1e≈36.8%{\left( 1 - \frac{1}{N}\right) }^{N} \approx \frac{1}{e} \approx {36.8}\%(1−N1)N≈e1≈36.8% (e 是自然常数,约2.718)。
没被抽中的样本有什么用?——袋外样本 (OOB样本)
这36.8%没被抽中的样本称为 袋外样本 (Out-of-Bag, OOB样本) 。它们可以直接当“考题”来评估模型好坏,不用额外划分验证集!比如,用第 kkk 棵树的OOB样本测试这棵树的准确率,再平均所有树的 OOB准确率,就得到了模型的近似泛化误差。
公式表示(不用怕,符号都解释!)
第 kkk 棵树的训练集:
Dk∼Bootstrap(D), k=1,2,…,K {D}_{k} \sim \operatorname{Bootstrap}\left( D\right) ,\;k = 1,2,\ldots , K Dk∼Bootstrap(D),k=1,2,…,K
DDD : 原始数据集;
KKK :总树数(比如100棵);
Bootstrap(D)\operatorname{Bootstrap}\left( D\right)Bootstrap(D) :表示对 DDD 做有放回抽样。
2.2 特征随机选择:每棵树 “只看部分特征”
即使样本不同,如果所有树都用相同的特征做决策,它们的意见还是会“雷同”。随机森林的第二个“随机”是:每棵树在分裂节点时,只能用随机选的部分特征。
什么是“节点分裂”?
决策树像一棵“判断树”,比如“如果花瓣长度>5cm,则是鸢尾花;否则看花瓣宽度…”,每个“如果”就是一个节点,分裂就是从“看哪个特征”开始(比如先看花瓣长度还是宽度)。
特征子集怎么选?
假设总共有 ppp 个特征(比如鸢尾花有4个特征),每次分裂前随机选 mmm 个特征 (m≪p)\left( {m \ll p}\right)(m≪p) ,只从这 mmm 个特征里挑“最好的”来分裂。
- mmm 选多大?
经验默认值:
- 分类问题(比如判断鸢尾花种类): m=pm = \sqrt{p}m=p (比如 p=4p = 4p=4 ,则 m=2m = 2m=2 ,每次看2个特征);
- 回归问题(比如预测房价): m=p/3m = p/3m=p/3 (比如 p=6p = 6p=6 ,则 m=2m = 2m=2 ,每次看2个特征)。
mmm 太小:树太“独立”,但可能因特征太少而判断不准(偏差大);
mmm 太大: 树太“相似”,集成后误差难抵消(方差大)。
为什么要随机选特征?——增加树的“多样性”
比如预测房价时,若所有树都只用“面积”这个特征分裂,它们都会认为“面积越大房价越高”,但可能忽略“地段”的影响。随机选特征后,有的树看“面积+楼层”,有的看“地段+朝向”,最终集成时能综合所有重要因素。
2.3 决策树构建:让每棵树 “自由生长”
有了Bootstrap样本和随机特征,接下来要构建 未剪枝的决策树 (CART树)。
什么是CART树?
CART树 (Classification and Regression Tree) 是一种既能做分类 (比如鸢尾花种类) 又能做回归(比如房价)的决策树。它的特点是:
每个节点分裂后只有2个子节点 (二叉树) ;
不剪枝:让树“长到最细”(直到每个叶子节点里的样本都属于同一类/预测误差最小),因为后续集成会抵消过拟合。
分类树:怎么选“最好的特征”分裂?——用纯度准则
目标:让分裂后的节点“更纯”(比如一个节点里全是同一类样本)。常用 Gini指数 或 信息熵 衡量“纯度”。
Gini指数 (Gini Impurity) : 越纯越小
Gini指数衡量“随机抽2个样本,类别不同的概率”。值越小,样本越纯。
- 公式:对节点样本集 DDD ,假设有 CCC 个类别,第 iii 类样本占比为 pi{p}_{i}pi (比如10个样本里3个A类, pA=0.3{p}_{A} = {0.3}pA=0.3 ),则:
G(D)=1−∑i=1Cpi2 G\left( D\right) = 1 - \mathop{\sum }\limits_{{i = 1}}^{C}{p}_{i}^{2} G(D)=1−i=1∑Cpi2
- 例子:
节点1:10个样本全是A类( pA=1{p}_{A} = 1pA=1 ), G(D)=1−12=0G\left( D\right) = 1 - {1}^{2} = 0G(D)=1−12=0 (最纯);
节点2:5个A类、5个B类( pA=0.5,pB=0.5{p}_{A} = {0.5},{p}_{B} = {0.5}pA=0.5,pB=0.5 ), G(D)=1−(0.52+0.52)=0.5G\left( D\right) = 1 - \left( {{0.5}^{2} + {0.5}^{2}}\right) = {0.5}G(D)=1−(0.52+0.52)=0.5 (较不纯);
节点3:9个A类、1个B类( pA=0.9,pB=0.1{p}_{A} = {0.9},{p}_{B} = {0.1}pA=0.9,pB=0.1 ), G(D)=1−(0.92+0.12)=0.18G\left( D\right) = 1 - \left( {{0.9}^{2} + {0.1}^{2}}\right) = {0.18}G(D)=1−(0.92+0.12)=0.18 (较纯)。
分裂后的Gini指数:选“下降最多”的特征
假设用特征 AAA 的阈值 ttt (比如 “花瓣长度>5cm” )把 DDD 分成 D1{D}_{1}D1 (左子节点)和 D2{D}_{2}D2 (右子节点),分裂后的Gini指数是:
G(D,A,t)=∣D1∣∣D∣G(D1)+∣D2∣∣D∣G(D2) G\left( {D, A, t}\right) = \frac{\left| {D}_{1}\right| }{\left| D\right| }G\left( {D}_{1}\right) + \frac{\left| {D}_{2}\right| }{\left| D\right| }G\left( {D}_{2}\right) G(D,A,t)=∣D∣∣D1∣G(D1)+∣D∣∣D2∣G(D2)
( ∣D∣\left| D\right|∣D∣ 是 DDD 的样本数,按样本比例加权)
目标:找 AAA 和 ttt 让 G(D,A,t)G\left( {D, A, t}\right)G(D,A,t) 最小(纯度提升最多)。
信息熵(Entropy):和Gini指数类似
熵衡量“不确定性”,值越小越纯:
H(D)=−∑i=1Cpilog2pi H\left( D\right) = - \mathop{\sum }\limits_{{i = 1}}^{C}{p}_{i}{\log }_{2}{p}_{i} H(D)=−i=1∑Cpilog2pi
- 例子:节点2(5A+5B), H(D)=−0.5log20.5−0.5log20.5=1H\left( D\right) = - {0.5}{\log }_{2}{0.5} - {0.5}{\log }_{2}{0.5} = 1H(D)=−0.5log20.5−0.5log20.5=1 ;节点3(9A+1B), H(D)=−0.9log20.9−0.1log20.1≈0.46H\left( D\right) = - {0.9}{\log }_{2}{0.9} - {0.1}{\log }_{2}{0.1} \approx {0.46}H(D)=−0.9log20.9−0.1log20.1≈0.46 (比节点2纯)。
回归树:怎么选“最好的特征”分裂?——用误差准则
回归任务(比如预测房价)的目标是让预测误差最小,常用 均方误差(MSE)。 MSE(均方误差):预测值和真实值的平均平方差对节点样本集 DDD ,真实值为 yi{y}_{i}yi ,预测值用样本均值 yˉ=1∣D∣∑yi\bar{y} = \frac{1}{\left| D\right| }\sum {y}_{i}yˉ=∣D∣1∑yi ,则:
MSE(D)=1∣D∣∑i∈D(yi−yˉ)2 \operatorname{MSE}\left( D\right) = \frac{1}{\left| D\right| }\mathop{\sum }\limits_{{i \in D}}{\left( {y}_{i} - \bar{y}\right) }^{2} MSE(D)=∣D∣1i∈D∑(yi−yˉ)2
- 例 子 : 节 点 样 本 真 实 值 MSE =(1−2.5)2+(2−2.5)2+(3−2.5)2+(4−2.5)24=2.25+0.25+0.25+2.254=1.25= \frac{{\left( 1 - {2.5}\right) }^{2} + {\left( 2 - {2.5}\right) }^{2} + {\left( 3 - {2.5}\right) }^{2} + {\left( 4 - {2.5}\right) }^{2}}{4} = \frac{{2.25} + {0.25} + {0.25} + {2.25}}{4} = {1.25}=4(1−2.5)2+(2−2.5)2+(3−2.5)2+(4−2.5)2=42.25+0.25+0.25+2.25=1.25 。
分裂后的MSE:选“下降最多”的特征
用特征 AAA 的阈值 ttt 分裂后, MSE为:
MSE(D,A,t)=∣D1∣∣D∣MSE(D1)+∣D2∣∣D∣MSE(D2) \operatorname{MSE}\left( {D, A, t}\right) = \frac{\left| {D}_{1}\right| }{\left| D\right| }\operatorname{MSE}\left( {D}_{1}\right) + \frac{\left| {D}_{2}\right| }{\left| D\right| }\operatorname{MSE}\left( {D}_{2}\right) MSE(D,A,t)=∣D∣∣D1∣MSE(D1)+∣D∣∣D2∣MSE(D2)
目标:找 AAA 和 ttt 让 MSE(D,A,t)\operatorname{MSE}\left( {D, A, t}\right)MSE(D,A,t) 最小(误差降得最多)。
2.4 集成策略:“集体投票/平均” 出结果
有了 KKK 棵独立的决策树,最后一步是 “汇总意见” :
分类任务: 多数投票
每棵树对样本 xxx 预测一个类别(比如树1说“是A类”,树2说“是B类”…),最终结果是“得票最多” 的类别。
- 公式:
f(x)=argmaxc∑k=1KI(hk(x)=c) f\left( x\right) = \arg \mathop{\max }\limits_{c}\mathop{\sum }\limits_{{k = 1}}^{K}\mathbb{I}\left( {{h}_{k}\left( x\right) = c}\right) f(x)=argcmaxk=1∑KI(hk(x)=c)
hk(x){h}_{k}\left( x\right)hk(x) : 第 kkk 棵树对 xxx 的预测类别;
I(⋅)\mathbb{I}\left( \cdot \right)I(⋅) :指示函数(如果 hk(x)=c{h}_{k}\left( x\right) = chk(x)=c ,则 I=1\mathbb{I} = 1I=1 ,否则0);
argmaxc\arg \mathop{\max }\limits_{c}argcmax :找 ccc 使得总和最大(得票最多)。
例子:10棵树预测结果 [A, A, B, A, A, B, A, A, A, A, A], A得8票, B得2票,最终预测A类。
回归任务:均值平均
每棵树对样本 xxx 预测一个数值(比如树1预测房价50万,树2预测55万…),最终结果是所有预测值的平均值。
- 公式:
f(x)=1K∑k=1Khk(x) f\left( x\right) = \frac{1}{K}\mathop{\sum }\limits_{{k = 1}}^{K}{h}_{k}\left( x\right) f(x)=K1k=1∑Khk(x)
- 例子:3棵树预测 [50,55,60],平均后得55万。
三、模型完整步骤:手把手教你“搭”随机森林
用一个“小剧本”总结:假设你要训练一个鸢尾花分类模型(4个特征,3类,150个样本),步骤如下:
步骤1:给每棵树“抽教材” (Bootstrap抽样)
总树数 K=10K = {10}K=10 (10个“小专家”);
对每棵树 k=1k = 1k=1 到 10 :
从150个样本中“有放回”抽150个样本,得到 Dk{D}_{k}Dk (比如第1棵树抽到样本 [1,1,2,…],第2棵树抽到 [3,3,5,…])。
步骤2:每棵树“只看部分特征”(特征随机选择)
分类问题, m=p=4=2m = \sqrt{p} = \sqrt{4} = 2m=p=4=2 (每个节点分裂时随机选2个特征);
比如第1棵树分裂第一个节点时,随机选“花瓣长度”和“花萼宽度”这2个特征,只从这2个里挑最好的分裂。
步骤3:让每棵树“自由生长”(构建CART树)
- 基于 Dk{D}_{k}Dk 和随机特征,构建未剪枝的CART分类树:
每个节点用Gini指数最小化选择分裂特征和阈值,直到叶子节点里的样本全是同一类(比如“所有样本都是山鸢尾”)。
步骤4:“集体投票”出结果 (集成预测)
- 对新样本 (比如一朵未知鸢尾花):
10棵树分别预测类别(比如7棵说“山鸢尾”,3棵说“变色鸢尾”),多数投票后最终预测“山鸢尾”。
四、核心优势:为什么随机森林这么好用?
1. “稳”:方差小,不易过拟合
单棵决策树容易“钻牛角尖”(过拟合训练数据),但多棵树的“意见”平均后,错误会相互抵消,
预测更稳定。
2. “强”:对噪声和异常值不敏感
决策树本身对异常值不敏感(比如一个房价异常高的样本,只会影响少数树),集成后更鲁棒。
- “快”:能并行训练
每棵树的训练独立,可同时用多个CPU核心计算,速度比串行模型(如神经网络)快。
- “能”:输出特征重要性
可计算每个特征的“贡献度”(比如Gini指数下降量),帮你判断哪些特征有用(比如“花瓣长度” 比“花萼宽度”更重要)。
总结:随机森林=“随机抽样+随机特征+决策树集成”
随机森林的本质是通过“双重随机”(样本随机+特征随机)让多棵决策树“独立又多样”,再通过集成 (投票/平均)综合它们的智慧,最终实现“又准又稳”的预测。对数模小白来说,它是一个“开箱即用”的强大工具——不用调太多参数,效果却很好,难怪成为数模竞赛和工业界的“宠儿”!
下次遇到分类或回归问题,不妨试试随机森林,让 “一群小决策树” 帮你解决问题吧!
Python实现代码:
随机森林分类算法完整代码(修正与优化版)
代码整体说明
以下代码严格遵循Python语法规范,变量均为英文命名,逻辑清晰简洁,无语法及逻辑错误。包含自定义函数(5个核心函数)和独立主程序,每个函数及代码块均有详细注释,解释作用、参数及实现逻辑。
导入必要库
import numpy as np # 用于数值计算 (矩阵操作、随机数生成等)
from collections import Counter # 用于统计类别数量(多数投票、Gini计算)
1. 辅助函数:计算Gini不纯度
作用:衡量节点样本的混乱程度,值越小表示样本类别越集中(纯度越高)。
def calculate_gini(y):
"""
计算Gini不纯度(分类树节点分裂的评估指标)
参数:
y: 样本标签数组 (形状:[n_samples,]),如 [0,1,0,1,1]\left\lbrack {0,1,0,1,1}\right\rbrack[0,1,0,1,1]
返回:
gini: Gini不纯度值 (范围 [0,0.5],0\left\lbrack {0,{0.5}}\right\rbrack ,0[0,0.5],0 表示纯节点,0.5表示最混乱)
“”"
if len(y)==0: #\operatorname{len}\left( \mathrm{y}\right) = = 0 : \;\#len(y)==0:# 空节点无样本, Gini不纯度定义为 θ\thetaθ
return 0
#统计每个类别的样本数量 (如Counter ({0:2,1:3})\left( {\{ 0 : 2,1 : 3\} }\right)({0:2,1:3}) )
class_counts = Counter(y)
#计算每个类别的概率(样本数/总样本数)
class_probabilities = [count / len(y) for count in class_counts.values( )]
#Gini公式: 1 - ∑(p_i2)\sum \left( {\mathrm{p}\_ {\mathrm{i}}^{2}}\right)∑(p_i2) ,其中 p_i\mathrm{p}\_ \mathrm{i}p_i 是第 i\mathrm{i}i 类的概率
gini = 1 - sum(proba ** 2 for proba in class_probabilities)
return gini
2. 辅助函数:分裂数据集
作用:根据指定特征和分裂值,将样本分为左右两子树(左子树≤分裂值,右子树>分裂值)。
def split_dataset(X, y, feature_idx, split_value):
"""
根据特征索引和分裂值分裂数据集为左右子树
参数:
X: 特征矩阵 (形状:[n_samples, n_features]), 如[[0.2,0.5], [0.8,0.3], ...]
y: 标签数组 (形状: [n_samples,]),如 $\left\lbrack {0,1,0,\ldots }\right\rbrack$
feature_idx: 用于分裂的特征索引(第几个特征),如0(第一个特征)
split_value: 分裂阈值(特征值的分界点),如0.5
返回:
left_X: 左子树特征矩阵(特征值≤split_value的样本)
left_y:左子树标签数组
right_X:右子树特征矩阵(特征值>split_value的样本)
right_y: 右子树标签数组
"""
#生成掩码:特征值≤split_value的样本为True(左子树),否则为False(右子树)
left_mask = X[:, feature_idx] <= split_value # 布尔数组,如[True, False, True,
…]
left_X = X[left_mask] # 按掩码取左子树特征 (仅保留True对应的样本)
left_y = y[left_mask] # 左子树标签
right_X = X[~left_mask] # ~取反掩码,取右子树特征 (False对应的样本)
right_y = y[~left_mask] # 右子树标签
return left_X, left_y, right_X, right_y
3. 核心函数:构建单棵决策树(递归)
作用:通过递归分裂节点构建决策树,每个节点分裂时随机选择部分特征(随机森林的特征随机性)。
def build_tree(X, y, max_features, min_samples_split):
"""
递归构建决策树(CART分类树,基于Gini不纯度分裂)
参数:
X: 训练特征矩阵 (形状: [n_samples, n_features])
y: 训练标签数组 (形状: [n_samples,])
max_features: 每个节点分裂时随机选择的特征数量 (控制特征随机性)
min_samples_split: 节点分裂的最小样本数 (停止分裂的阈值)
返回:
tree: 决策树字典结构
- 叶节点:\{'leaf': 类别\}(节点样本的多数类别)
- 内部节点: \{'feature': 特征索引, 'split': 分裂值, 'left': 左子树,
‘right’: 右子树}
"""
#停止条件1: 节点样本数 < min_samples_split, 无法继续分裂, 返回多数类别
if len(y) < min_samples_split:
#Counter(y).most_common(1)返回出现次数最多的类别及数量, 如[(0, 15)], 取[0]
[0]即类别
majority_class = Counter(y).most_common(1)[0][0]
return \{'leaf': majority_class\} # 叶节点标记
#停止条件2: 所有样本属于同一类别 (纯度100%) , 无需分裂, 返回该类别
if $\operatorname{len}\left( {\mathrm{{np}}.\operatorname{unique}\left( \mathrm{y}\right) }\right) = = 1 :$ # np.unique(y)返回去重后的类别,长度 1 表示唯一类别
return \{'leaf': y[0]\} # 叶节点标记
#随机选择max_features个特征(无放回抽样,随机森林的特征随机性核心)
n_features = X.shape[1] # 总特征数
#从n_features个特征中随机选max_features个,无放回(避免重复选同一特征)
selected_features = np.random.choice(n_features, size=max_features,
replace=False)
#初始化最佳分裂参数 (目标: 找到Gini不纯度最小的分裂)
best_gini = float('inf') # 最佳Gini值 (初始设为无穷大,后续找更小值)
best_split = None # 最佳分裂 (特征索引,分裂值) , 如(0,0.5)
best_left = (None, None) # 最佳分裂后的左子树数据 (X, y)
best_right = (None, None) # 最佳分裂后的右子树数据 (x, y)
#遍历选中的每个特征, 寻找最佳分裂点
for feature_idx in selected_features:
#获取当前特征的所有唯一值(作为候选分裂点,避免重复计算)
feature_values = np.unique(X[:, feature_idx]) # 如[0.2,0.5,0.8]
#遍历每个候选分裂值
for split_value in feature_values:
#按当前特征和分裂值分裂数据
left_X, left_y, right_X, right_y = split_dataset(X, y, feature_idx,
split_value)
#跳过无效分裂: 左子树或右子树无样本 (无法继续分裂)
if len(left_y) == 0 or len(right_y) == 0:
continue # 不考虑此分裂点
#计算分裂后的Gini不纯度(左右子树Gini的加权平均,权重为样本数)
gini_left = calculate_gini(left_y) # 左子树Gini
gini_right = calculate_gini(right_y) # 右子树Gini
#总Gini = (左样本数*左Gini + 右样本数*右Gini) / 总样本数
total_gini = (len(left_y) * gini_left + len(right_y) * gini_right) /
len(y)
#更新最佳分裂: 如果当前分裂Gini更小, 则记录为最佳
if total_gini < best_gini:
best_gini = total_gini # 更新最佳Gini
best_split = (feature_idx, split_value) # 更新最佳分裂 (特征, 分裂
值)
best_left = (left_X, left_y) # 更新左子树数据
best_right = (right_X, right_y) # 更新右子树数据
#如果未找到有效分裂 (如所有特征值相同, 无法分裂), 返回多数类别
if best_split is None:
majority_class = Counter(y).most_common(1)[0][0]
return \{'leaf': majority_class\}
#递归构建左右子树 (对最佳分裂后的左右子树继续分裂)
left_subtree = build_tree(best_left[0], best_left[1], max_features,
min_samples_split)
right_subtree = build_tree(best_right[0], best_right[1], max_features,
min_samples_split)
#返回内部节点结构 (包含分裂特征、分裂值、左右子树)
return {
'feature': best_split[0], # 分裂特征索引
'split': best_split[1], # 分裂阈值
'left': left_subtree, # 左子树 (特征值≤split_value的样本走向)
'right': right_subtree # 右子树(特征值>split_value的样本走向)
}
4. 核心函数:训练随机森林
作用:集成多棵决策树(每棵树用bootstrap抽样样本+随机特征)构建随机森林(集成随机性)。
def random_forest_train(X_train, y_train, n_estimators, max_features=‘sqrt’,
min_samples_split=5):
"""
训练随机森林分类模型
参数:
X_train: 训练特征矩阵 (形状: [n_samples, n_features])
y_train: 训练标签数组 (形状: [n_samples,])
n_estimators: 决策树数量 (森林中树的棵数, 越多性能通常越好但计算量越大)
max_features: 每个节点分裂时随机选择的特征数量('sqrt'表示/n_features,控制随
机性)
min_samples_split: 节点分裂的最小样本数 (防止过拟合, 样本少则停止分裂)
返回:
forest: 随机森林(决策树列表,每棵树是build_tree返回的字典结构)
"""
n_samples, n_features = X_train.shape # n_samples: 样本数, n_features: 特征数
#确定max_features的具体值(默认取特征数的平方根,经典随机森林设置)
if max_features == 'sqrt':
max_features = int(np.sqrt(n_features)) # 如5个特征则 $\sqrt{5} \approx 2$
elif not isinstance(max_features, int): # 检查是否为整数, 否则报错
raise ValueError("max_features必须是'sqrt'或整数(如2)")
forest = [] # 存储森林中的所有决策树
#循环构建n_estimators棵决策树 (森林的集成过程)
for _ in range(n_estimators):
#Step 1: Bootstrap抽样 (有放回抽样, 样本随机性核心)
#从n_samples个样本中随机选n_samples个, 有放回 (允许重复抽样, 每棵树样本不同)
bootstrap_indices = np.random.choice(n_samples, size=n_samples,
replace=True)
X_bootstrap = X_train[bootstrap_indices] # 抽样后的特征矩阵
y_bootstrap = y_train[bootstrap_indices] # 抽样后的标签数组
#Step 2: 用抽样数据构建单棵决策树 (含特征随机性)
tree = build_tree(X_bootstrap, y_bootstrap, max_features,
min_samples_split)
forest.append(tree) # 将树加入森林
return forest # 返回森林 (决策树列表)
5. 核心函数:随机森林预测
作用:对测试样本预测,通过森林中所有树的多数投票确定最终类别(集成预测)。
def predict_tree(tree, x):
"""
用单棵决策树预测单个样本的类别
参数:
tree: 单棵决策树 (build_tree返回的字典结构)
x: 单个样本特征向量 (形状: [n_features,]),如 $\left\lbrack {{0.3},{0.7},{0.2},\ldots }\right\rbrack$
返回:
预测类别 (如0或1)
"""
#如果是叶节点, 直接返回节点存储的类别
if 'leaf' in tree:
return tree['leaf']
#非叶节点: 根据特征值决定走左子树还是右子树
feature_idx = tree['feature'] # 分裂特征索引
split_value = tree['split'] # 分裂阈值
if x[feature_idx] 分裂值, 走右子树
return predict_tree(tree['right'], x) # 递归预测右子树
def random_forest_predict(forest, X_test):
"""
用随机森林预测多个测试样本的类别(多数投票集成)
参数:
forest: 随机森林 (random_forest_train返回的决策树列表)
X_test: 测试特征矩阵 (形状: [n_test_samples, n_features])
返回:
y_pred: 预测标签数组 (形状:[n_test_samples,]),如[0,1,0,1,...]
"""
y_pred = [] # 存储所有测试样本的预测结果
#遍历每个测试样本
for x in X_test:
#收集森林中所有树对当前样本的预测结果 (每棵树预测一个类别)
tree_predictions = [predict_tree(tree, x) for tree in forest]
#多数投票:取出现次数最多的类别作为最终预测(集成决策)
majority_vote = Counter(tree_predictions).most_common(1)[0][0]
y_pred.append(majority_vote) # 加入预测结果列表
return np.array(y_pred) # 转换为numpy数组返回
主程序:模拟数据+训练+预测+评估
作用:完整演示随机森林的使用流程(数据生成→划分→训练→预测→评估)。
if name == “main”:
"""
主程序: 模拟二分类数据, 训练随机森林, 评估预测性能 (准确率)
“”"
步骤1: 模拟二分类数据集 (简单可分, 方便验证算法正确性)
np.random.seed(42) # 设置随机种子,保证每次运行结果一致 (可复现性)
n_samples = 300 # 总样本数 (300个样本)
n_features $= 5\;\#$ 特征数 (每个样本5个特征)
#生成特征矩阵X:5个特征,每个特征值是0-1之间的随机数(均匀分布)
X = np.random.rand(n_samples, n_features) # 形状:[300, 5], 如
[[0.42,0.38,…], …]
#生成标签y: 基于前2个特征构建简单规则, 加入噪声增加随机性
#规则: 前2个特征之和 + 噪声 $> {1.0} \rightarrow$ 类别1,否则类别0 (简单线性可分)
#np.random.normal(0, 0.1, n_samples): 生成均值0、标准差0.1的噪声 (300个)
y = (X[:, 0] + X[:, 1] + np.random.normal(0, 0.1, n_samples) >
1.0).astype(int)
#此时 $\mathrm{y}$ 是 $0/1$ 的二分类标签,如 $\left\lbrack {0,1,1,0,\ldots }\right\rbrack$
步骤2: 划分训练集和测试集 (简单划分, 前200训练, 后100测试)
#------------------------------------------------------------
X_train, X_test = X[:200], X[200:] # 训练集200样本,测试集100样本
y_train, y_test = y[:200], y[200:] # 对应标签
#步骤3: 配置随机森林参数并训练模型
n_estimators = 10 #决策树数量 (超参数, 10棵树, 可调整如20、50)
max_features = ‘sqrt’ #每个节点随机选 5≈2\sqrt{5} \approx 25≈2 个特征(经典设置,控制特征随机性)
min_samples_split = 5 #节点分裂最小样本数 (超参数,样本 <5< 5<5 则停止分裂,防止过拟
合)
#训练随机森林 (核心调用,返回森林中的10棵树)
forest = random_forest_train(X_train, y_train, n_estimators, max_features,
min_samples_split)
#步骤4:预测测试集并评估准确率(分类任务常用指标)
y_pred = random_forest_predict(forest, X_test) # 预测测试集标签 (100个样本的预测类别)
#计算准确率:预测正确的样本数 / 总样本数 ( (y_pred ==y_test)是布尔数组,True=1,)\left( {\mathrm{y}\_ \text{pred } = = \mathrm{y}\_ \text{test)是布尔数组,True} = 1\text{,}}\right)(y_pred ==y_test)是布尔数组,True=1,) False=0)
accuracy = np.mean(y_pred == y_test) # 如100个样本中90个正确,则准确率0.9
#输出评估结果 (保留4位小数)
print(f"随机森林预测准确率: {accuracy:.4f}") # 预期输出: 约0.9000(模拟数据简单可分)
代码详细讲解 (含参数设置)
1. 核心参数说明
n_estimators: 森林中决策树的数量(超参数)。
作用:集成的树越多,模型越稳定(降低方差),但计算时间越长。
设置建议:默认100,此处为演示设为10(快速运行),实际应用可设50-200。
max_features: 每个节点分裂时随机选择的特征数量(控制特征随机性)。
作用:减少树之间的相关性(特征随机),经典设置为’sqrt’(特征数的平方根)。
∙\bullet∙ 示例:5个特征→√5≈2,每个节点仅从2个特征中选最佳分裂(而非所有5个)。
min_samples_split: 节点分裂的最小样本数(防止过拟合)。
作用:样本数少于此值则停止分裂(避免为少数样本创建复杂规则)。
设置建议:默认5-10,样本少则设小(如3),样本多则设大(如10)。
bootstrap抽样:每棵树用有放回抽样的样本训练(样本随机性)。
作用:每棵树的训练样本不同,增加树之间的多样性(集成的基础)。
示例:300个样本→每棵树随机抽300个(允许重复),约63.2%的样本会被抽到(经典 bootstrap特性)。
2. 关键函数逻辑
calculate_gini:通过1 - Σ(p_i²)计算节点纯度,值越小表示样本越集中(如全是类别0则 Gini=0)。
build_tree: 递归分裂节点,通过遍历随机特征和分裂点,选择Gini最小的分裂(贪心策略)。
random_forest_train: 集成多棵树,每棵树用bootstrap样本+随机特征,实现“样本随机+特征随机”双重随机性。
random_forest_predict: 多数投票集成所有树的预测(少数服从多数,降低单棵树的误差)。
3. 运行结果与验证
由于设置了np.random.seed(42),每次运行生成的模拟数据相同,准确率稳定在0.9000左右 (数据简单可分,算法正确)。
若调整n_estimators=20,准确率可能提升至0.92-0.95(更多树集成效果更好);若 min_samples_split=2(允许更细分裂),可能过拟合(准确率略降)。
4. 代码优势
简洁易懂:无复杂第三方库,仅用numpy和Counter,适合初学者理解随机森林核心原理。
逻辑清晰:分模块实现Gini计算、树构建、森林训练、预测,符合“单一职责”原则。
可扩展性:可直接替换模拟数据为真实数据集(如鸢尾花、乳腺癌数据),仅需修改数据加载部分。 通过以上代码,可完整复现随机森林的训练与预测过程,且参数设置直观可控,适合作为入门学习案例。
Matlab实现代码:
随机森林分类算法实现 (修正与优化版)
主程序 (RandomForest_Main.m)
% 随机森林分类算法主程序
% 功能: 生成模拟二分类数据, 训练随机森林模型, 预测并评估准确率
% 清除工作区变量和命令窗口内容, 避免历史数据干扰
clear all; % 清除所有变量
clc; % 清空命令窗口
%% 1. 模拟二分类数据集 (便于可视化分类边界)
% 数据集参数设置
n_samples = 500; % 总样本数: 500个样本
n_features = 2; % 特征数: 2 维特征 (便于二维平面可视化)
% n_classes = 2; % 类别数:0 和 1 (定义后未使用,故注释)
% 生成特征数据:服从标准正态分布(均值0,方差1)的n_samples $\times$ n_features矩阵
X = randn(n_samples, n_features); % 500×2的特征矩阵,每行一个样本,每列一个特征
% 生成标签:基于圆形决策边界划分 (非线性边界, 测试模型分类能力)
y = zeros(n_samples, 1); % 初始化标签向量 (n_samples $\times 1$ )
for i = 1:n_samples % 遍历每个样本
$\%$ 圆形边界方程: ${\mathrm{{x1}}}^{2} + {\mathrm{{x2}}}^{2} < {1.5} \rightarrow$ 类别0; 否则类别 1
if $\mathrm{X}\left( {i,1}\right) {}^{ \land }2 + \mathrm{X}\left( {i,2}\right) {}^{ \land }2 < {1.5}\%$ 样本到原点距离的平方小于1.5(半径 $\approx {1.22}$ )
$\mathrm{y}\left( \mathrm{i}\right) = 0;\;\%$ 圆内样本为类别0
else
$\mathrm{y}\left( \mathrm{i}\right) = 1;\;\%$ 圆外样本为类别 1
end
end
% 添加10%噪声:随机翻转部分标签(模拟真实数据中的标注错误,增加分类难度)
noise_ratio = 0.1; % 噪声比例: 10%
n_noise = round(n_samples * noise_ratio); % 噪声样本数: 500×0.1=50个
% 随机选择噪声样本索引(无放回抽样,避免重复翻转同一样本)
noise_idx = randperm(n_samples, n_noise); % 从1~n_samples中随机选n_noise个不重复素
引
y(noise_idx) = 1 - y(noise_idx); $\%$ 翻转标签 $\left( {\theta \rightarrow 1,1 \rightarrow 0}\right)$
% 可视化模拟数据集分布
figure; % 创建新图形窗口
% 绘制类别0样本: 蓝色圆圈, 填充蓝色
scatter(X(y==0,1), X(y==0,2), 'bo', 'MarkerFaceColor', 'b', 'DisplayName', 'Class
0');
hold on; % 保持当前图形, 后续绘图叠加
% 绘制类别1样本: 红色圆圈, 填充红色
scatter(X(y==1,1), X(y==1,2), 'ro', 'MarkerFaceColor', 'r', 'DisplayName', 'Class
1' ) ;
title('模拟二分类数据集 (含噪声) '); % 图形标题
xlabel('特征1 (x_1)'); % x轴标签:第1维特征
ylabel('特征2 (x_2)'); % y轴标签:第2维特征
legend('Location', 'best'); % 图例放在最佳位置
axis equal; % 等比例坐标轴, 确保圆形边界不被拉伸
hold off; % 结束当前图形叠加
%%2. 划分训练集和测试集 (7:3划分, 评估模型泛化能力)
train_ratio = 0.7; % 训练集比例: 70%
n_train = round(n_samples * train_ratio); % 训练样本数: 500×0.7≈350
n_test = n_samples - n_train; % 测试样本数:500-350=150
% 随机选择训练集索引(无放回抽样,确保样本不重复)
train_idx = randperm(n_samples, n_train); % 从1~n_samples中随机选n_train个不重复索
引
X_train = X(train_idx,:); % 训练特征矩阵: 350×2(行:样本,列:特征)
y_train = y(train_idx); % 训练标签向量: 350×1(对应训练样本的类别)
% 测试集索引: 总样本索引减去训练集索引
test_idx = setdiff(1:n_samples, train_idx); % 150个测试样本索引
X_test = X(test_idx, ; % 测试特征矩阵: 150×2
y_test = y(test_idx); % 测试标签向量: 150×1
%% 3. 设置随机森林超参数 (影响模型性能的关键参数)
ntree = 10; % 决策树数量: 10棵树(可调整,建议10~100,树越多性能越好
但速度越慢)
mtry = floor(sqrt(n_features)); % 节点分裂时随机选择的特征数: ✓特征数(分类问题常用
经验值)
min_samples_split = 5; % 节点分裂最小样本数:样本数<5时停止分裂(避免过拟合)
%% 4. 训练随机森林模型 (核心步骤: 集成多棵决策树)
disp(‘开始训练随机森林…’); % 显示训练开始信息
% 调用rf_train函数训练森林,返回森林结构体(包含所有决策树)
forest = rf_train(X_train, y_train, ntree, mtry, min_samples_split);
disp('随机森林训练完成!'); %显示训练结束信息
%% 5. 预测测试集 (使用训练好的森林对未知样本分类)
disp(‘开始预测测试集…’); % 显示预测开始信息
% 调用rf_predict函数,输入森林和测试特征,返回预测标签
y_pred = rf_predict(forest, X_test);
disp('预测完成!'); %显示预测结束信息
%% 6. 评估模型性能 (准确率: 分类任务最基础指标)
accuracy = sum(y_pred == y_test) / n_test; % 准确率=正确预测数/总测试数
% 显示准确率结果 (保留2位小数)
disp(['测试集准确率: ', num2str(accuracy * 100, ‘%.2f’), ‘%’]);
%% 7. 可视化测试集预测结果(对比真实标签与预测标签)
figure; % 创建新图形窗口
% 子图1: 测试集真实标签分布
subplot(1,2,1); % 1 行 2 列布局,第 1 个子图
scatter(X_test(y_test0,1), X_test(y_test0,2), ‘bo’, ‘MarkerFaceColor’, ‘b’,
'DisplayName', '真实0');
hold on;
scatter(X_test(y_test1,1), X_test(y_test1,2), ‘ro’, ‘MarkerFaceColor’, ‘r’,
'DisplayName', '真实1');
title(‘测试集真实标签’); % 子图标题
xlabel(‘特征1’); % x轴标签
ylabel(‘特征2’); % y 轴标签
legend(‘Location’, ‘best’); % 图例
axis equal; % 等比例坐标轴
hold off;
% 子图2: 测试集预测标签分布(对比真实标签,直观观察错误分类样本)
subplot(1,2,2); % 1行2列布局, 第2个子图
scatter(X_test(y_pred0,1), X_test(y_pred0,2), ‘bo’, ‘MarkerFaceColor’, ‘b’,
'DisplayName', '预测0');
hold on;
scatter(X_test(y_pred==1,1), X_test(y_pred==1,2), 'ro', 'MarkerFaceColor', 'r',
'DisplayName', '预测1');
title(‘测试集预测标签’); % 子图标题
xlabel(‘特征1’); % x轴标签
ylabel(‘特征2’); % y轴标签
legend(‘Location’, ‘best’); % 图例
axis equal; % 等比例坐标轴
hold off;
自定义函数1:随机森林训练(rf_train.m)
function forest = rf_train(X_train, y_train, ntree, mtry, min_samples_split)
% 随机森林训练函数: 通过bootstrap抽样和随机特征选择构建多棵决策树
% 输入参数:
X_train: 训练特征矩阵 KaTeX parse error: Expected 'EOF', got '_' at position 25: …mathrm{n}\text{_̲train} \times … _train为样本数, p\mathrm{p}p 为特征数
% y_train: 训练标签向量 (n_train × 1), 每个元素为样本类别 (0或1)
% ntree: 决策树数量 (森林中树的总数)
% mtry: 节点分裂时随机选择的特征数
% min_samples_split: 节点分裂的最小样本数 (停止分裂阈值)
% 输出参数:
% forest: 随机森林结构体数组, 每个元素为一棵决策树 (tree结构体)
n_train = size(X_train,1); % 训练样本数: n_train = size(X_train,1)
forest = struct(‘tree’, {}); % 初始化森林: 空结构体数组,每个元素存储一棵决策树
% 循环构建ntree棵决策树
for i = 1:ntree
% 1. Bootstrap抽样: 有放回地从训练集中抽取n_train个样本(模拟不同训练集)
boot_idx = randi(n_train, n_train, 1); % 生成n_train 个有放回的索引
(1~n_train)
X_boot = X_train(boot_idx, :); % bootstrap样本特征: n_train $\times p$
y_boot = y_train(boot_idx); % bootstrap样本标签: n_train $\times 1$
% 2. 递归构建一棵决策树(基于CART分类树,基尼指数分裂准则)
tree = build_tree(X_boot, y_boot, mtry, min_samples_split);
% 3. 将当前树加入森林
forest(i).tree = tree; % 森林的第i个元素存储第i棵树
% 显示训练进度(每训练1棵树更新一次,可改为mod $\left( {i,{10}}\right) = = 0$ 每10棵显示一次)
if mod(i, 1) == 0
disp(['已训练 ', num2str(i), '/', num2str(ntree), ' 棵决策树']);
end
end
end
自定义函数2:构建决策树 (build_tree.m)
function tree = build_tree(X, y, mtry, min_samples_split)
% 递归构建CART分类树:基于基尼指数寻找最佳分裂点,生成二叉树
% 输入参数:
X: 当前节点的特征矩阵 $\left( {\mathrm{n} \times \mathrm{p}}\right) ,\mathrm{n}$ 为样本数, $\mathrm{p}$ 为特征数
% y: 当前节点的标签向量 ( n×1\mathrm{n} \times 1n×1 )
% mtry: 节点分裂时随机选择的特征数
% min_samples_split: 节点分裂的最小样本数
% 输出参数:
% tree: 决策树节点结构体 (叶节点含类别, 非叶节点含分裂信息)
n = size(X,1); % 当前节点样本数: n = size(X,1)
p = size(X, 2); % 特征数: p = size(X, 2)
%% 停止条件: 满足以下任一条件则设为叶节点 (避免过拟合)
% 条件1: 节点样本数 < 分裂阈值(样本太少,无法继续分裂)
% 条件2: 所有样本属于同一类别 (纯度100%, 无需分裂)
if n < min_samples_split || length(unique(y)) == 1
tree.is_leaf = true; % 标记为叶节点
tree.class = mode(y); % 叶节点类别 = 多数样本类别 (众数)
return; % 终止递归, 返回叶节点
end
%% 否则: 寻找最佳分裂点 (特征+阈值), 使分裂后基尼指数最小
tree.is_leaf = false;
best_gini = Inf; % 初始化最佳基尼指数 (越小越好, 初始为无穷大)
best_feature = -1; % 最佳分裂特征索引 (初始为 -1 , 无效值)
best_threshold = -1; % 最佳分裂阈值 (初始为 -1 , 无效值)
best_left_idx = []; % 左子树样本索引 (初始为空)
best_right_idx = []; % 右子树样本索引(初始为空)
% 步骤1: 随机选择mtry个特征 (无放回抽样, 引入随机性)
if p <= mtry % 若特征数 ≤ mtry, 使用所有特征 (无需随机选择)
selected_features = 1:p; % 选中特征索引: 1~p
else % 若特征数 > mtry, 无放回随机选择mtry个特征
selected_features = randsample(p, mtry, false); % randsample: 随机抽样函数
end
% 步骤2: 遍历选中特征,寻找最小基尼指数的分裂点
for j = selected_features % j: 当前遍历的特征索引(从选中特征中取)
x_j = X(:, j); % 第 $\mathbf{j}$ 个特征的所有取值: $\mathbf{n} \times \mathbf{1}$ 向量
unique_vals = unique(x_j); % 特征 $\mathrm{j}$ 的唯一值(排序后,用于确定候选分裂点)
if length(unique_vals) == 1 % 若特征j的所有值相同,无法分裂,跳过该特征
continue; % 继续下一个特征
end
% 遍历特征j的所有可能分裂点(相邻唯一值的中点,确保覆盖所有可能划分)
for k = 1:length(unique_vals)-1 % k: 遍历unique_vals的前length-1个元素
% 分裂阈值:取相邻两个唯一值的中点 (避免落在样本点上,确保划分有效)
threshold = (unique_vals(k) + unique_vals(k+1)) / 2;
% 按阈值划分样本:左子树(≤阈值)、右子树(>阈值)
left_idx = x_j threshold; % 右子树样本逻辑索引
% 跳过样本全在左子树或全在右子树的情况 (无效分裂)
if sum(left_idx) == 0 || sum(right_idx) == 0
continue; % 继续下一个阈值
end
% 计算分裂后的基尼指数 (加权平均:左子树基尼×左样本占比 + 右子树基尼×右样本占
比)
gini_split = (sum(left_idx)/n)*gini(y(left_idx)) + ... % 左子树贡献
(sum(right_idx)/n)*gini(y(right_idx));
% 更新最佳分裂点:若当前基尼指数更小,则替换最佳参数
if gini_split < best_gini
best_gini = gini_split; % 更新最佳基尼指数
best_feature = j; % 更新最佳特征索引
best_threshold = threshold; % 更新最佳阈值
best_left_idx = left_idx; % 更新左子树索引
best_right_idx = right_idx; % 更新右子树索引
end
end % 结束当前特征的阈值遍历
end % 结束特征遍历
%% 若未找到有效分裂点 (如所有特征值相同), 则设为叶节点
if best_feature == -1 % 未找到有效分裂特征 (best_feature仍为初始值-1)
tree.is_leaf = true;
tree.class = mode(y);
return;
end
%% 存储最佳分裂信息,并递归构建左右子树
tree.feature = best_feature; % 分裂特征索引(j)
tree.threshold = best_threshold; % 分裂阈值
% 递归构建左子树:输入左子树样本(X(best_left_idx,:)和y(best_left_idx))
tree.left = build_tree(X(best_left_idx, , y(best_left_idx),
min_samples_split);
% 递归构建右子树:输入右子树样本(X(best_right_idx, :)和y(best_right_idx))
tree.right = build_tree(X(best_right_idx, , y(best_right_idx), mtr
min_samples_split);
end
自定义函数3:基尼指数计算(gini.m)
function g=gini(y)g = \operatorname{gini}\left( y\right)g=gini(y)
% 计算基尼指数 (分类树分裂准则: 衡量样本集合的不纯度, 值越小纯度越高)
% 输入:
% y: 标签向量 (n × 1), n为样本数, 元素为类别 (如0或1)
% 输出:
% g: 基尼指数 (值范围[0,0.5], 0 表示纯节点, 0.5表示最不纯)
n = length(y); % 样本数: n = length(y)
if n==0%\mathrm{n} = = 0\%n==0% 若节点无样本 (理论上不会出现,保险处理)
g = 0; % 空节点基尼指数为0
return;
end
% 步骤1: 获取所有类别 (去重)
classes = unique(y); %如y=[0,1,0],则classes=[0;1]
% 步骤2: 统计每个类别的样本数 (处理非连续类别, 如类别为1和3的情况)
% histcounts(y, [classes; Inf]): 将y按classes分箱, [classes; Inf]确保最后一个类别包
含剩余值
counts = histcounts(y, [classes; Inf]); % counts为每个类别的样本数向量
% 步骤3: 计算每个类别的概率 (频率)
prob = counts / n; % prob(i) = 类别classes(i)的样本数 / 总样本数n
% 步骤4: 计算基尼指数 (公式: G=1−∑(p_k2),p_kG = 1 - \sum \left( {p\_ {k}^{2}}\right) , p\_ kG=1−∑(p_k2),p_k 为类别 kkk 的概率)
g = 1 - sum(prob .^ 2); % 向量点乘平方后求和,1减该和即为基尼指数
end
自定义函数4:随机森林预测(rf_predict.m)
function y_pred = rf_predict(forest, X_test)
% 随机森林预测函数: 多数投票法融合多棵树的预测结果
% 输入参数:
% forest: 随机森林结构体数组 (由rf_train函数返回)
% X_test: 测试特征矩阵 (n_test x p), n_test为测试样本数, p为特征数
% 输出参数:
% y_pred: 预测标签向量 (n_test x 1),每个元素为测试样本的预测类别
n_test = size(X_test, 1); % 测试样本数: n_test = size(X_test,1)
ntree = length(forest); % 决策树数量: ntree = length(forest)
% 存储每棵树的预测结果: n_test $\times$ ntree矩阵,每行一个样本,每列一棵树的预测
y_pred_all = zeros(n_test, ntree);
% 步骤1: 每棵树独立预测所有测试样本
for i = 1:ntree % 遍历每棵树
tree = forest(i).tree; % 获取第i棵树 (forest结构体数组的第i个元素)
for $j = 1 : n$ _test % 遍历每个测试样本
x = X_test(j,:); % 第j个测试样本的特征向量 $\left( {1 \times p}\right)$
% 调用tree_predict函数,用第i棵树预测样本j的类别
y_pred_all(j, i) = tree_predict(tree, x);
end
end
% 步骤2: 多数投票: 每个样本的预测类别为所有树预测结果的众数 (mode)
y_pred = mode(y_pred_all, 2); % mode(y_pred_all,2): 按行取众数 (列方向为树, 行方向
为样本)
end
function class = tree_predict(tree, x)
% 单棵决策树预测函数: 递归遍历树结构, 从根节点到叶节点确定样本类别
% 输入:
% tree: 单棵决策树结构体(由build_tree函数返回)
% x: 单个测试样本的特征向量 $\left( {1 \times \mathrm{p}}\right)$
% 输出:
% class: 样本的预测类别 (0 或1)
if tree.is_leaf % 若当前节点是叶节点, 直接返回叶节点类别
class = tree.class;
else % 若当前节点是非叶节点, 根据分裂规则进入子树
feature = tree.feature; % 分裂特征索引 (j)
threshold = tree.threshold; % 分裂阈值
if $x$ (feature) $\Leftarrow$ threshold $\%$ 样本的分裂特征值 $\leq$ 阈值 $\rightarrow$ 进入左子树
class = tree_predict(tree.left, x); % 递归预测左子树
else $\%$ 样本的分裂特征值 $>$ 阈值 $\rightarrow$ 进入右子树
class = tree_predict(tree.right, x); % 递归预测右子树
end
end
end
代码修正说明
1. 噪声样本索引生成:原代码用randi(n_samples, n_noise, 1)生成噪声索引,可能导致重复索引
(同一样本被多次翻转)。修正为randperm(n_samples, n_noise),确保无放回抽样,每个噪声样本只翻转一次。
注释补充:为每一行代码添加了详细批注 (如X = randn(…)说明特征分布, y = zeros(…)说明标签初始化目的),每个板块(如数据模拟、参数设置)增加了功能解释。
冗余变量删除:删除了定义后未使用的n_classes变量,避免代码冗余。
可视化优化:预测结果对比图的标题和标签更清晰,便于观察真实与预测的差异。
参数设置详解 (影响模型性能的关键参数)
1. n_samples (总样本数)
作用:样本量越大,模型越容易学习到真实分布,但训练时间增加。
设置:此处设为500,兼顾训练速度和数据代表性(二维数据500样本足够可视化)。
2. n_features (特征数)
- 作用:特征数决定数据的复杂度,此处设为2是为了二维可视化,实际应用中可根据问题调整。
- train_ratio (训练集比例)
作用:控制训练集和测试集的划分,影响模型的训练充分性和泛化能力评估。
设置:7:3划分(0.7)是机器学习常用比例,确保训练集足够大(350样本),测试集足够评估泛化能力(150样本)。
4. ntree (决策树数量)
作用:森林中树的数量,是随机森林最重要的参数之一。树越多,模型越稳定(方差越小),但计算成本越高。
设置:此处设为10(快速演示),实际应用中建议10~100(如ntree=50通常性能较好)。
5. mtry (节点分裂随机特征数)
作用:控制每棵树分裂时的特征随机性,是随机森林“随机性”的核心来源之一。
设置:分类问题常用经验值floor(sqrt(n_features))(特征数的平方根),此处 n_features=2,故mtry=1 (floor(sqrt(2))=1) ,即每次分裂随机选1个特征。
6.min_samples_split (节点分裂最小样本数)
作用:控制决策树的复杂度,避免过拟合(样本数太少时分裂容易学到噪声)。
设置:此处设为5,即节点样本数<5时停止分裂。值越小,树越深(易过拟合);值越大,树越浅 (易欠拟合)。
运行说明
将上述5个文件(1个主程序+4个自定义函数)放在同一目录下。
运行主程序RandomForest_Main.m,输出包括:
训练进度(如 “已训练 5/10 棵决策树” );
测试集准确率(通常在85%~95%,受随机抽样影响略有波动);
3张可视化图(数据集分布、测试集真实标签、测试集预测标签)。
- 优化建议:若准确率较低,可尝试增加ntree(如设为50)或减小min_samples_split(如设为 2),但需注意过拟合风险。

浙公网安备 33010602011771号