1. 引言:随机森林算法是什么?
在机器学习中,单棵决策树虽然直观、易于理解,却容易受到训练数据变化的影响:即使只增加或删除少量样本,也可能产生一棵结构明显不同的树。为了获得更稳定的预测,我们可以训练多棵决策树,再将它们的结果进行聚合。
在上一篇文章中,我们介绍了 Bagging(Bootstrap Aggregating)思想:通过有放回抽样构造不同的训练集,分别训练多个模型,再通过投票或平均得到最终预测。随机森林(Random Forest)正是在这一思路上发展起来的经典集成算法。
不过,⭐训练集不同,就一定能得到足够不同的决策树吗?
假如某个特征的预测能力很强,即使各棵树使用的样本有所不同,它们仍可能优先选择这个特征,形成相似的划分规则。当多棵树容易在相似的样本上犯错时,仅仅增加树的数量,带来的改善就可能受到限制。
随机森林进一步引入了特征随机性:在每个节点分裂时,随机抽取一部分特征作为候选,再从中寻找最佳划分。它希望在保留单棵树预测能力的同时,降低树之间的相关性,让多棵树的预测更具互补性。
这里我先给一个公式方便大家理解:
随机森林 = Bagging 集成思想 + 决策树作为基学习器 + 随机特征选择
本文将简要回顾 Bagging,重点分析它的功绩与瓶颈,以及随机森林如何通过特征随机性进行改进,最后结合算法流程与 Python 实战,理解随机森林如何训练、预测与评估。
2. 前提回顾:Bagging 的核心思想
Bagging 的完整原理可以参考前文,这里只回顾理解随机森林所需的三个步骤。
- 有放回抽样:从原始训练集中有放回地随机抽取多个子集,每个子集的大小与原数据集相同,但其中可能包含重复样本。
- 并行训练:在每个子集上独立训练一个基学习器,各学习器之间互不干扰。
- 结果聚合:对于分类任务,采用投票法决定最终类别;对于回归任务,采用取平均值的方式得到最终预测结果。
2.1 有放回抽样:生成不同训练集
假设原始训练集包含 (N) 个样本。每次从中随机抽取一个样本,抽取后放回,重复 (N) 次,得到一份 Bootstrap 训练集。
由于采用有放回抽样:
- 某些样本可能出现多次。
- 某些样本可能没有被抽中。
- 不同轮次得到的训练集通常有所不同。
重复这一过程,就可以构造多份训练集。
2.2 并行训练:让模型分别学习
在每份训练集上,分别训练一个基学习器
其中,(B) 表示基学习器的数量。
这些模型之间没有必须按顺序学习的依赖关系,因此可以并行训练。
2.3 结果聚合:共同给出预测
对于回归任务,通常取各模型预测的平均值:
对于分类任务,常见的思路是多数投票,也可以在基模型提供类别概率时先平均概率,再选择概率最大的类别。
Bagging 的基本流程可以概括为:
样本扰动 → 分别训练 → 预测聚合
回顾这三个步骤我们发现了一个问题 -> Bagging思想并没有保证:训练出来的模型一定具有足够大的差异,接下来让我们详细分析一下。
3. Bagging 的功绩与瓶颈:树多,还需要差异
3.1 功绩:降低预测方差
决策树,尤其是生长较深的决策树,这种深决策树是低偏差、高方差的模型。对训练数据的变化往往比较敏感。
例如,训练集中增加或删除少量样本,可能改变某个节点的最佳划分,而这个节点的变化又会影响后续整棵子树,训练数据的微小扰动就会让它面目全非。
这意味着:虽然单棵树可以拟合非常复杂的交互结构,但在不同训练集上训练得到的决策树,可能给出波动较大的预测。
Bagging 通过平均来“平滑”这种波动,而不损害其拟合能力将多棵树的预测进行聚合,可以减弱单棵树波动对最终结果的影响。它尤其适合决策树这类对数据扰动比较敏感的学习器。
不过,平均能够带来多大收益,还取决于这些预测之间的相关性。
3.2 用一个方差公式理解瓶颈
1. 为什么固定输入后,树的预测还有方差?
对于固定输入 (x),例如同一套房屋的特征。如果训练数据和训练过程都固定,一棵树的预测当然是确定的。但当我们改变训练样本或训练随机性时,得到的树会不同,对这个 (x) 的预测也可能不同,我们先分析一棵树的方差,即。
因此,我们把预测看作随机变量,并简写为:
森林对这个输入的平均预测为:
我们要计算的就是:
2. 先理解两棵树的情况
两个随机变量相加,方差公式是:
其中,协方差描述两个预测是否倾向于一起变化。
假设两棵树预测的方差都是,相关系数都是
。根据相关系数的定义:
于是:
再利用:
得到两棵树平均预测的方差:
看两个特殊情况就容易理解:
- 如果
=0 ,平均后的方差为
/2。
- 如果
=1,平均后的方差仍为
。
因为我们假设的是预测的方差都是,即两棵树如果完全同步波动,这种情况下取平均也无法抵消这种波动。
3. 推广到B棵树
多个随机变量之和的方差为:
右边有两部分。
第一部分:各棵树自身的方差。
一共有B棵树,假设每棵树的预测方差为,所以:
第二部分:不同树之间的协方差。
从B棵树中选两棵,共有组合数:
每一对的协方差都是,再乘公式前面的系数2,得到:
求平均相当于乘上,所以方差要乘上
:
这个公式揭示了两个影响因素:
- 树的数量 (B):增加树的数量,可以缩小第二项。
- 树之间的相关性 (
):相关性越高,聚合后保留的共同波动越明显。
4. 为什么它能说明 Bagging 的瓶颈?
把结果拆成两项:
假设单棵树的预测方差为 100,树之间的相关系数为 0.3,则:
| 树的数量 (B) | 平均预测的方差 |
|---|---|
| 1 | 100 |
| 10 | 37 |
| 100 | 30.7 |
| 1000 | 30.07 |
| 趋于无穷大 | 趋于 30 |
即用公式表示:
可以看到,增加树的数量,收益逐渐减小。
因此,在上述假设下在单树方差和相关系数保持不变的假设下,剩下的共同波动(相关性带来的那部分方差)无法靠继续增加树来消除。
需要注意,这个公式用于解释平均预测的方差,不能直接当作分类错误率公式,也不能覆盖真实森林中的全部复杂因素。
4. 随机森林的破局:引入特征随机性
4.1 从“全部特征中选最好”到“候选特征中选最好”
随机森林作为 Bagging 思想的典型代表,在实际应用中展现出诸多优势,同时也存在一些局限性。
随机森林的关键改动,发生在决策树的节点分裂过程中。
普通决策树通常从全部特征中寻找最佳划分。随机森林则在每个节点:
- 从全部 p 个特征中,随机选取 m 个候选特征。
- 只在候选特征范围内搜索划分。
- 选择其中效果最好的特征与划分点。
- 进入子节点后,重新抽取候选特征。
例如,某个节点随机获得的候选集合是:
那么,即使“房屋面积”在全部特征中最有优势,它也无法参与这个节点的分裂竞争。这让其他特征获得了参与构建决策规则的机会。
随机的是候选特征集合,候选集合内仍然要寻找优质划分。随机森林并不是随意选择一个特征,再随意切一刀。
4.2 两种随机性分别作用在哪里?
| 随机性 | 发生位置 | 主要作用 |
|---|---|---|
| 样本随机性 | 每棵树训练前 | 让不同树看到不同的样本组合 |
| 特征随机性 | 每个节点分裂时 | 让不同节点面对不同的候选特征集合 |
这里容易混淆的一点是:
经典随机森林的特征抽样发生在每个节点,而不是只在每棵树开始训练时抽取一次。
举例说明:
假设任务是分类,总特征数 N=100。根据公式,每次节点分裂时抽取的特征数 m=10。
1. 根节点(第一次分裂):
算法从全部100个特征中,随机、独立地抽取10个特征(假设抽到了编号 3, 15, 28, 42, 55, 61, 77, 80, 92, 99)。
在这10个特征中寻找最佳分裂点。假设最终使用了特征15进行分裂。
2. 左子节点(第二次分裂):
注意!此时算法重新从全部100个特征中随机抽取10个(假设这次抽到了 1, 2, 3, 4, 5, 15, 20, 30, 40, 50)。
在这新的10个特征中找最佳分裂点。假设使用了特征2。
3. 右子节点(第三次分裂):
再次从全部100个特征中随机抽取10个(假设抽到了 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)。
找最佳分裂点,假设用了特征90。
以此类推,直到树长满。
因此,一棵树的根节点和后续节点,可能使用不同的候选特征集合。
4.3 为什么特征越随机,效果不一定越好?
既然随机性能够增加差异,是否应该把候选特征数量降得越低越好?
答案是否定的。
当候选特征太少时,某些节点可能无法获得足够有用的特征,导致划分质量下降,单棵树的预测能力受到影响。
因此,随机森林需要平衡两个目标:
- 单棵树需要有一定的预测能力。
- 不同树之间需要有足够的差异。
对上面出现的候选特征公式深入理解一下:为什么是“根号N”?
这是一个偏差-方差权衡(Bias-Variance Tradeoff)的经验法则:
如果 m 太大(比如接近 N):每次分裂都能看到大多数特征,单棵树会很强(偏差小),但树与树之间的差异会变小,变得高度相关。这会导致集成后的整体方差降不下来,失去了随机森林抵抗过拟合的优势。
如果 m 太小:树与树之间差异很大(方差小),但单棵树因为能看的特征太少,可能长得很差(偏差大)。
是一个在“保持单棵树一定准确率”和“降低树与树之间相关性”之间取得良好平衡的经验值(分类时)。
候选特征数量较大时,单棵树有更多机会找到好的划分,但不同树也可能更加相似;候选特征数量较小时,树之间的差异可能增大,但单棵树也可能变弱。
随机森林追求的是有效的差异,而不是随机性本身。
4.4 决策树、Bagging 与随机森林的区别
| 对比项 | 单棵决策树 | Bagging 决策树 | 经典随机森林 |
|---|---|---|---|
| 模型数量 | 一棵 | 多棵 | 多棵 |
| 每棵树的训练样本 | 通常使用完整训练集 | Bootstrap 抽样 | Bootstrap 抽样 |
| 节点候选特征 | 通常为全部特征 | 通常为全部特征 | 随机特征子集 |
| 预测方式 | 单树预测 | 聚合多树预测 | 聚合多树预测 |
| 核心思路 | 学习划分规则 | 通过聚合降低波动 | 在聚合基础上进一步降低相关性 |
这里的 Bagging 决策树指采用常规全特征决策树作为基学习器的配置;Bagging 本身是更一般的框架,也可以搭配其他学习器或特征抽样策略。
5. 随机森林算法:训练与预测流程
5.1 训练过程
设训练集为:
每个样本包含 p 个特征,森林包含 B 棵树,每个节点抽取 m 个候选特征。
训练过程如下:
输入:训练集 D,树的数量 B,候选特征数量 m
对 b = 1, 2, ..., B:
1. 从 D 中进行 Bootstrap 抽样,得到训练集 D_b
2. 使用 D_b 构建决策树:
对每个需要分裂的节点:
a. 随机抽取 m 个候选特征
b. 在候选特征中寻找最佳特征和划分点
c. 按照最佳划分生成子节点
d. 重复上述过程,直到满足停止条件3. 保存决策树 h_b
输出:由 B 棵决策树组成的森林
停止条件可以包括最大深度、节点样本数不足、无法继续有效分裂等。经典随机森林通常使用生长较深的树,实际应用中也可以通过参数限制复杂度。
5.2 节点如何判断划分好坏?
以分类任务常用的基尼不纯度为例:
中,表示节点 t 中第 k 类样本的比例。
节点中的类别越混杂,基尼不纯度通常越高;如果全部样本属于同一类,则:
对于某个候选划分,其不纯度下降量可以写为:
算法在允许的候选范围内寻找使最大的划分。
随机森林改变了参与竞争的特征范围,节点内部仍然遵循决策树的划分原则。
5.3 预测过程
对于回归任务,对所有树的预测取平均:
对于分类任务,常见的概念描述是多数投票:
简单理解:
核心动作:“数人头”。每棵树只给出一个确定的类别(比如A或B),然后大家直接投票,少数服从多数。
盲区:不管树有多确信,也只是一票。极度确信的树和瞎猜的树,权重是一样的。
不过,在本文使用的 scikit-learn 中,RandomForestClassifier会先平均各棵树给出的类别概率,再选择平均概率最大的类别:
简单理解:
核心动作:“算平均分”。每棵树给出的是自己预测各个类别的概率。先所有树把概率加起来求平均,最后谁的平均概率最高,就选谁。
优势:它考虑了树的“确信程度”。如果一棵树极度确信是A,即使别的树稍微偏向B,求平均后A依然能胜出。
概率平均与硬投票并不总是产生相同结果。阅读算法介绍与实际代码时,需要区分概念描述和具体实现。
5.4 袋外评估:利用未被抽中的样本
一次 Bootstrap 抽样中,某个样本在一次抽取时没有被选中的概率为:
连续抽取 N 次都没有选中它的概率为:
当 N 足够大时:
也就是说,一棵树进行等量 Bootstrap 抽样后,平均约有36.8% 的训练样本没有参与这棵树的训练,这些样本称为该树的袋外样本。
对于每个训练样本,可以只汇总那些没有使用过它的树所给出的预测,再计算整体指标,这就是袋外评估(OOB)。
这里要注意:
- 不同树对应的袋外样本集合不同。
- 36.8% 是上述抽样条件下的近似比例。
- 如果反复根据 OOB 分数选择参数,OOB 也参与了模型选择,仍应保留独立测试集做最终评估。
不会有人对这块看不懂吧:
那我来给你举个例子你带入到上面再想一想:
36.8%是怎么来的假设你有一个袋子,里面有 N 个球(代表 N 个训练样本)。重复这个过程 N 次,最后求极值就好啦。
OOB到底是什么:
假设你的整个数据集有 1000 个样本。你训练了 100 棵树。
现在我们盯住其中的第 42 号样本,看看红框里的话是怎么操作的:
第一步:找没用过它的树。在这 100 棵树里,每一棵树都有约 36.8% 没见过的数据。所以,必定有几十棵树在训练时没有抽中第 42 号样本。
第二步:只让这些树给它打分。我们把那些在训练时见过 42 号样本的树全部踢掉,只保留那些没见过它的树。让这几十棵树对 42 号样本进行预测(投票或算概率)。
第三步:对比真实答案。这几十棵“瞎眼”的树给出了一个综合预测结果。我们拿这个结果和 42 号样本的真实标签比一比,看预测对不对。
把上面的过程,对全部 1000 个样本都做一遍。
每一个样本,都只由那些“没在训练时见过它”的树来预测。
最后算出一个总的准确率或误差。
这就叫:袋外评估(OOB Error / OOB Score)。
5.5 关键参数
| 参数 | 含义 | 主要影响 |
|---|---|---|
n_estimators | 树的数量 | 影响聚合稳定性、训练时间与模型大小 |
max_features | 每个节点考虑的候选特征数量 | 调节单树能力与树间差异 |
max_depth | 单棵树最大深度 | 限制树的复杂度 |
min_samples_leaf | 叶节点最小样本数 | 避免叶节点只拟合极少量样本 |
max_samples | Bootstrap 抽样规模 | 控制每棵树使用的样本量 |
bootstrap | 是否采用有放回抽样 | 决定是否引入该类样本随机性 |
oob_score | 是否计算袋外得分 | 在 Bootstrap 配置下提供内部评估 |
random_state | 随机种子 | 帮助复现实验 |
增加树的数量通常会让预测逐渐稳定,但收益可能趋于饱和。若主要问题是信息不足、数据泄漏或单树设置不合理,单纯增加树的数量也无法解决。
6. Python 实战:基于泰坦尼克号数据预测乘客生存情况
理解随机森林的原理后,我们通过泰坦尼克号乘客生存预测任务,观察单棵决策树与随机森林的表现,并使用网格搜索寻找更合适的参数。
本节设置三个实验场景:
- 单棵决策树。
- 使用默认主要参数的随机森林。
- 通过网格搜索调参的随机森林。
通过对照实验,我们重点观察:多树集成是否改善了模型的泛化表现,以及限制树的复杂度能否进一步提高预测效果。
6.1 任务说明与数据准备
本次使用titanic_train.csv数据,根据乘客信息预测其是否生还。
选取以下三个特征:
| 字段 | 含义 | 处理方式 |
|---|---|---|
Pclass | 船票等级 | 保留数值形式 |
Sex | 性别 | 独热编码 |
Age | 年龄 | 使用训练数据的平均值填充缺失值 |
预测目标为Survived:
0:未生还。1:生还。
这里只选择三个特征,便于理解从数据处理到模型评估的完整流程。实际任务中,还可以进一步探索票价、登船港口、同行亲属数量等信息。
6.2 数据预处理:先划分,再学习填充规则
年龄特征存在缺失值,可以使用平均值进行填充。但均值应仅根据训练数据计算,避免测试集的信息提前参与建模。
因此,需要遵循以下顺序:
划分训练集与测试集 → 根据训练集学习预处理规则 → 将相同规则应用到测试集。
交叉验证时也一样:每一折的填充规则,都应只从该折的训练部分学习。
本节使用Pipeline和ColumnTransformer,将以下操作连接起来:
- 对
Age使用平均值填充。 - 对
Sex使用众数填充,再进行独热编码。 - 对
Pclass保留数值形式。 - 将处理后的特征交给分类模型。
这样,预处理会随模型一起参与交叉验证,避免提前利用验证数据的信息。
划分数据时,还使用stratify=y,使训练集与测试集中的类别比例尽量接近原始数据。
6.3 场景一:训练单棵决策树
首先训练一棵决策树,作为后续实验的对照基线:
DecisionTreeClassifier(random_state=23)模型训练完成后,分别计算训练集与测试集准确率:
model.score(x_train, y_train) model.score(x_test, y_test)训练集准确率反映模型对已见数据的拟合程度,测试集准确率则用于评估模型对未见数据的预测表现。
如果训练准确率很高,而测试准确率明显较低,就需要警惕过拟合。因此,不能仅凭训练集上的高分判断模型效果。
6.4 场景二:训练随机森林
接下来训练随机森林:
RandomForestClassifier( random_state=23, n_jobs=-1, )这里保留默认的主要建模参数,只设置随机种子和并行计算。
其中:
random_state:控制随机过程,便于复现实验。n_jobs=-1:允许使用所有可用处理器核心并行训练。
随机森林通过样本随机性和节点特征随机性构建多棵决策树,再聚合各棵树的预测结果。
我们将比较它与单棵决策树的训练、测试准确率,观察集成是否改善了当前任务中的泛化表现。
6.5 场景三:使用网格搜索调整随机森林参数
默认参数提供了建模起点,但不一定适合当前数据。
本次设置以下参数搜索范围:
{ "n_estimators": [60, 90, 100, 130, 150], "max_depth": [3, 5, 7, 9], }两个参数分别控制:
| 参数 | 含义 | 本次实验关注点 |
|---|---|---|
n_estimators | 森林中树的数量 | 增加树的数量能否改善预测表现 |
max_depth | 单棵树的最大深度 | 限制树的复杂度能否改善泛化表现 |
共有: 5 * 4 = 20组候选参数。结合三折交叉验证,需要完成:20 * 3 = 60次候选模型拟合。
搜索结束后,GridSearchCV默认还会使用最佳参数,在完整训练集上重新拟合模型。
由于分类器放在流水线的model步骤中,搜索参数名称需要增加model__前缀:
{ "model__n_estimators": [60, 90, 100, 130, 150], "model__max_depth": [3, 5, 7, 9], }网格搜索前不需要单独训练分类器,直接调用GridSearchCV.fit()即可。
6.6 完整代码
以下代码包含数据加载、预处理、三个场景的模型训练,以及结果对比。运行前,请根据数据文件的实际位置修改DATA_PATH。
from pathlib import Path import pandas as pd from sklearn.compose import ColumnTransformer from sklearn.ensemble import RandomForestClassifier from sklearn.impute import SimpleImputer from sklearn.metrics import classification_report, confusion_matrix from sklearn.model_selection import ( GridSearchCV, StratifiedKFold, train_test_split, ) from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder from sklearn.tree import DecisionTreeClassifier # 根据实际文件位置修改路径 DATA_PATH = Path("../data/titanic_train.csv") RANDOM_STATE = 23 def build_pipeline(model): """将数据预处理与分类模型组合。""" # 年龄:使用当前训练数据的平均值填充 age_transformer = SimpleImputer(strategy="mean") # 性别:使用众数填充,再进行独热编码 sex_transformer = Pipeline([ ("imputer", SimpleImputer(strategy="most_frequent")), ( "encoder", OneHotEncoder(handle_unknown="ignore"), ), ]) # 对不同列分别进行处理 preprocessor = ColumnTransformer([ ("age", age_transformer, ["Age"]), ("sex", sex_transformer, ["Sex"]), ("pclass", "passthrough", ["Pclass"]), ]) return Pipeline([ ("preprocessor", preprocessor), ("model", model), ]) def evaluate_model(name, model, x_train, x_test, y_train, y_test): """记录训练集与测试集准确率。""" return { "模型": name, "训练集准确率": model.score(x_train, y_train), "测试集准确率": model.score(x_test, y_test), } def main(): # 1. 加载数据 data = pd.read_csv(DATA_PATH) x = data[["Pclass", "Sex", "Age"]].copy() y = data["Survived"] print("数据规模:", x.shape) print("\n特征缺失情况:") print(x.isna().sum()) # 2. 划分训练集与测试集 x_train, x_test, y_train, y_test = train_test_split( x, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y, ) results = [] # 3. 场景一:单棵决策树 tree = build_pipeline( DecisionTreeClassifier( random_state=RANDOM_STATE, ) ) tree.fit(x_train, y_train) results.append( evaluate_model( "单棵决策树", tree, x_train, x_test, y_train, y_test, ) ) # 4. 场景二:保留默认主要参数的随机森林 forest = build_pipeline( RandomForestClassifier( random_state=RANDOM_STATE, n_jobs=-1, ) ) forest.fit(x_train, y_train) results.append( evaluate_model( "默认随机森林", forest, x_train, x_test, y_train, y_test, ) ) # 5. 场景三:网格搜索随机森林 # 搜索阶段由 GridSearchCV 并行, # 单个森林内部使用一个工作进程,避免嵌套并行。 search_pipeline = build_pipeline( RandomForestClassifier( random_state=RANDOM_STATE, n_jobs=1, ) ) param_grid = { "model__n_estimators": [60, 90, 100, 130, 150], "model__max_depth": [3, 5, 7, 9], } cv = StratifiedKFold( n_splits=3, shuffle=True, random_state=RANDOM_STATE, ) search = GridSearchCV( estimator=search_pipeline, param_grid=param_grid, scoring="accuracy", cv=cv, n_jobs=-1, refit=True, ) search.fit(x_train, y_train) results.append( evaluate_model( "网格搜索随机森林", search, x_train, x_test, y_train, y_test, ) ) # 6. 输出三个模型的评估结果 result_df = pd.DataFrame(results).set_index("模型") print("\n模型评估对比:") print(result_df.round(4).to_string()) # 去掉流水线参数前缀,方便阅读 best_params = { key.replace("model__", ""): value for key, value in search.best_params_.items() } print("\n最佳参数:") print(best_params) print( "\n最佳参数对应的交叉验证平均准确率:" f"{search.best_score_:.4f}" ) # 7. 查看交叉验证排名靠前的参数组合 cv_results = pd.DataFrame(search.cv_results_) columns = [ "param_model__n_estimators", "param_model__max_depth", "mean_test_score", "std_test_score", "rank_test_score", ] print("\n交叉验证排名前五的参数组合:") print( cv_results[columns] .sort_values("rank_test_score") .head(5) .to_string(index=False) ) # 8. 查看调参模型的详细测试结果 y_pred = search.predict(x_test) print("\n测试集前十条预测:") prediction_df = pd.DataFrame({ "真实值": y_test.to_numpy(), "预测值": y_pred, }) print(prediction_df.head(10).to_string(index=False)) print("\n分类报告:") print( classification_report( y_test, y_pred, labels=[0, 1], target_names=["未生还", "生还"], digits=4, zero_division=0, ) ) print("\n混淆矩阵:") print( confusion_matrix( y_test, y_pred, labels=[0, 1], ) ) if __name__ == "__main__": main()本例保留性别独热编码的全部类别列。对决策树而言,不需要像某些线性模型那样,为避免完全共线性而专门删除其中一列。
6.7 实验结果如何分析?
代码运行后会输出模型对比表、最佳参数、交叉验证得分、分类报告和混淆矩阵。具体数值以实际运行结果为准。
分析时,应重点回答以下问题。
(1)随机森林是否改善了测试表现?
首先比较单棵决策树与默认随机森林的测试准确率。
如果随机森林更高,说明在当前数据划分和参数配置下,多树集成带来了泛化收益。
如果两者接近,甚至随机森林略低,也应如实记录。数据规模、特征信息与参数设置都会影响最终表现。
随机森林并不保证在每一次数据划分中都优于单棵决策树。
(2)限制树深度后,模型表现发生了什么变化?
比较默认随机森林与网格搜索随机森林的训练、测试准确率。
如果最佳参数选择了较小的max_depth,模型的训练准确率可能下降,但测试准确率可能提高。这说明限制树的复杂度,可能减少了对训练细节的拟合。
不过,不能只看训练与测试分数的差距。如果两个分数都很低,即使差距很小,也可能存在欠拟合。
因此,需要同时观察:
- 训练集上的拟合程度。
- 测试集上的预测表现。
- 二者之间的差距。
(3)交叉验证得分与测试得分有什么区别?
search.best_score_表示最佳参数组合在训练集内部的交叉验证平均准确率。
而:
search.score(x_test, y_test)表示模型在独立测试集上的准确率。
两者使用的数据不同,结果不必相同。
此外,最佳参数只是当前候选范围、评价指标与交叉验证划分下的最佳选择,并不代表所有参数组合中的全局最优。
(4)为什么不能只看准确率?
准确率反映整体预测正确的比例,但不能完整展示不同类别的识别情况。
分类报告中的主要指标包括:
| 指标 | 含义 |
|---|---|
| Precision | 被预测为某一类的样本中,实际属于该类的比例 |
| Recall | 实际属于某一类的样本中,被正确识别的比例 |
| F1-score | 精确率与召回率的调和平均 |
| Support | 该类别在评估数据中的样本数量 |
混淆矩阵则可以进一步展示错误的方向。本例固定类别顺序为[0, 1],其结构为:
结合这些指标,才能更具体地理解模型在哪些样本上容易出错。
6.8 本案例的边界与改进方向
本案例展示了从单棵决策树到随机森林,再到参数搜索的完整流程,但还不能单独证明特征随机性的作用。
原因是,单棵树与随机森林之间同时存在样本抽样、多树聚合和特征随机性等差异。如果希望专门研究特征随机性,可以在其他设置相同的情况下,对比不同的max_features配置。
后续还可以从以下方向改进:
- 丰富特征信息:加入票价、登船港口、家庭规模等变量。
- 扩展参数搜索:加入
max_features、min_samples_leaf等参数。 - 完善验证方式:在训练集内部比较候选方案,保留独立测试集用于最终评估。
特别需要注意,不能在查看测试结果后,反复根据测试分数修改特征和参数,否则测试集也会逐渐参与模型选择,失去独立评估的意义。
这个案例的重点不仅是调用随机森林接口,更是建立完整的建模思路:先建立基线,再引入集成与参数搜索,最后通过独立评估判断改进是否有效。
7. 总结
从决策树到 Bagging,再到随机森林,可以看到一条清晰的改进路线:
单棵决策树容易受到训练数据变化的影响;Bagging 通过训练多棵树并聚合预测,减弱这种波动;随机森林进一步限制每个节点的候选特征,尝试降低树之间的相关性。
它的核心启示是:
集成模型的效果,既取决于个体是否具有预测能力,也取决于不同个体能否提供互补信息。
随机森林能够学习非线性关系和特征交互,通常不需要为了树的划分专门进行特征标准化,因此适合作为表格数据任务的重要基线模型。
同时,它也有实际限制:树的数量增加会带来计算和存储成本,整体规则比单棵树更难直接解释;标准回归森林通过叶节点预测再求平均,也不擅长外推到训练目标取值范围之外。
回到文章开头的问题:为什么已经有了 Bagging,还需要特征随机性?
因为让模型看到不同的样本,只是制造差异的一种方式;让模型在不同的候选特征中寻找规则,可以进一步拓宽它们探索问题的路径。而这种差异是否真正有益,最终仍需要通过合理的验证与对照实验来判断。