随机森林:多决策树集成、特征重要性

一句话概述

随机森林(Random Forest)是 Bagging 思想最成功的实践,由 Leo Breiman 于 2001 年提出。它通过构建多棵决策树并引入双重随机性(Bootstrap 采样 + 随机特征子集选择)来降低树之间的相关性,从而大幅提升泛化能力。同时,随机森林天然提供了特征重要性评估,无需额外计算即可识别哪些特征对预测贡献最大,这使其成为工业界最受欢迎的「开箱即用」模型之一。

💡 核心要点:① 随机森林 = Bagging + 决策树 + 随机特征选择,双重随机性是其核心创新 ② 袋外误差(OOB Error)可替代交叉验证,高效评估模型性能 ③ 特征重要性有两种计算方式:基于杂质减少和基于精度下降 ④ 随机森林在中小规模表格数据上表现优异,是结构化数据的首选基线模型

教学与演示

一、随机森林的构建原理

是什么:随机森林是由多棵决策树组成的集成模型。每棵树的训练数据来自对原始训练集的 Bootstrap 采样,且在每个节点分裂时只考虑随机选取的特征子集(而非全部特征)。最终预测时,分类问题取多数投票,回归问题取平均值。

大白话 随机森林就像是请了一群专家(决策树),每人只给一部分数据和一部分线索(特征),让他们独立判断。最后综合所有人的意见——这样即使有人判断错了,整体结果也不会差。

为什么:单棵决策树容易过拟合,方差大——训练数据的微小变化可能导致树结构完全不同。Bagging 通过 Bootstrap 采样降低方差,但 Base 学习器之间如果相关性太高,方差降低就有限。随机森林的「随机特征选择」进一步降低了树之间的相关性,使得方差降低更加显著。从偏差-方差角度看,随机森林在不增加偏差的前提下大幅降低了方差。

怎么做

import numpy as np

# ========== 从零实现简化的随机森林(分类) ==========
np.random.seed(42)

class SimpleDecisionTree:
    """简化版决策树:仅用于演示随机森林的构建过程"""
    def __init__(self, max_depth=5, min_samples_split=2, max_features=None):
        self.max_depth = max_depth  # 最大深度,防止过拟合
        self.min_samples_split = min_samples_split  # 最小分裂样本数
        self.max_features = max_features  # 每次分裂考虑的最大特征数
        self.tree = None  # 存储树结构的字典
    
    def _gini(self, y):
        """计算基尼不纯度:衡量节点的不纯程度"""
        # 基尼不纯度 = 1 - Σ(p_k²),其中 p_k 是第 k 类的比例
        _, counts = np.unique(y, return_counts=True)
        probs = counts / len(y)  # 各类别占比
        return 1 - np.sum(probs ** 2)  # 返回基尼值
    
    def _best_split(self, X, y):
        """找到最佳分裂特征和阈值"""
        n_samples, n_features = X.shape
        if n_samples < self.min_samples_split:
            return None, None
        
        # 随机选择特征子集(随机森林的核心技巧之一)
        if self.max_features is not None:
            feature_indices = np.random.choice(n_features, self.max_features, replace=False)
        else:
            feature_indices = np.arange(n_features)
        
        best_gain = 0  # 最佳信息增益(基尼减少量)
        best_feat, best_thresh = None, None
        parent_gini = self._gini(y)  # 父节点基尼值
        
        for feat_idx in feature_indices:
            # 候选阈值:该特征的所有唯一值
            thresholds = np.unique(X[:, feat_idx])
            for thresh in thresholds:
                left_mask = X[:, feat_idx] <= thresh
                right_mask = ~left_mask
                if left_mask.sum() == 0 or right_mask.sum() == 0:
                    continue  # 无效分裂,跳过
                
                # 计算加权基尼不纯度
                left_gini = self._gini(y[left_mask])
                right_gini = self._gini(y[right_mask])
                weighted_gini = (left_mask.sum() * left_gini + right_mask.sum() * right_gini) / n_samples
                gain = parent_gini - weighted_gini  # 基尼减少量
                
                if gain > best_gain:
                    best_gain = gain
                    best_feat = feat_idx
                    best_thresh = thresh
        
        return best_feat, best_thresh
    
    def _build_tree(self, X, y, depth=0):
        """递归构建决策树"""
        n_samples = X.shape[0]
        
        # 停止条件:达到最大深度 或 样本过少 或 所有样本同类别
        if depth >= self.max_depth or n_samples < self.min_samples_split or len(np.unique(y)) == 1:
            # 叶节点:返回多数类别
            return {'leaf': True, 'class': np.bincount(y.astype(int)).argmax()}
        
        feat_idx, thresh = self._best_split(X, y)
        if feat_idx is None:
            return {'leaf': True, 'class': np.bincount(y.astype(int)).argmax()}
        
        # 根据分裂点划分左右子树
        left_mask = X[:, feat_idx] <= thresh
        right_mask = ~left_mask
        
        return {
            'leaf': False,
            'feature': feat_idx,
            'threshold': thresh,
            'left': self._build_tree(X[left_mask], y[left_mask], depth + 1),
            'right': self._build_tree(X[right_mask], y[right_mask], depth + 1)
        }
    
    def fit(self, X, y):
        """训练决策树"""
        self.tree = self._build_tree(X, y)
    
    def _predict_one(self, x, node):
        """对单个样本进行预测"""
        if node['leaf']:
            return node['class']
        if x[node['feature']] <= node['threshold']:
            return self._predict_one(x, node['left'])
        else:
            return self._predict_one(x, node['right'])
    
    def predict(self, X):
        """对多个样本进行预测"""
        return np.array([self._predict_one(x, self.tree) for x in X])


class SimpleRandomForest:
    """简化版随机森林实现"""
    def __init__(self, n_estimators=10, max_depth=5, max_features='sqrt'):
        self.n_estimators = n_estimators  # 决策树数量
        self.max_depth = max_depth  # 每棵树的最大深度
        self.max_features = max_features  # 每次分裂的特征数('sqrt' 表示 √n_features)
        self.trees = []  # 存储所有决策树
        self.oob_indices = []  # 每棵树的 OOB 样本索引
    
    def fit(self, X, y):
        """训练随机森林"""
        n_samples = X.shape[0]
        # 确定每次分裂的特征数
        if self.max_features == 'sqrt':
            max_features = int(np.sqrt(X.shape[1]))  # 默认使用 √p 个特征
        else:
            max_features = self.max_features
        
        self.trees = []
        for _ in range(self.n_estimators):
            # 1. Bootstrap 采样
            bootstrap_idx = np.random.choice(n_samples, n_samples, replace=True)
            X_boot, y_boot = X[bootstrap_idx], y[bootstrap_idx]
            
            # 2. 记录 OOB 样本索引(未被选中的样本)
            oob_idx = np.setdiff1d(np.arange(n_samples), np.unique(bootstrap_idx))
            self.oob_indices.append(oob_idx)
            
            # 3. 训练一棵决策树(每棵树用随机特征子集)
            tree = SimpleDecisionTree(max_depth=self.max_depth, max_features=max_features)
            tree.fit(X_boot, y_boot)
            self.trees.append(tree)
    
    def predict(self, X):
        """预测:所有树投票"""
        predictions = np.zeros((X.shape[0], self.n_estimators))
        for i, tree in enumerate(self.trees):
            predictions[:, i] = tree.predict(X)
        # 多数投票:每行取众数
        return np.array([np.bincount(predictions[j].astype(int)).argmax() 
                        for j in range(X.shape[0])])

# ========== 演示:在合成数据上训练随机森林 ==========
# 生成二维二分类数据
n_samples = 100
X = np.random.randn(n_samples, 4)  # 4个特征
# 真实关系:仅前两个特征与标签相关
y = ((X[:, 0] + X[:, 1] > 0)).astype(int)

rf = SimpleRandomForest(n_estimators=20, max_depth=5)
rf.fit(X, y)
predictions = rf.predict(X)
accuracy = np.mean(predictions == y)
print(f"随机森林训练准确率: {accuracy:.2%}")
print(f"树的数量: {rf.n_estimators}")
print(f"每棵树最大深度: {rf.max_depth}")
print(f"每次分裂特征数: {int(np.sqrt(X.shape[1]))} (sqrt(特征数))")
\text{随机森林的泛化误差上界}\(\text{PE}^* \leq \frac{\bar{\rho}(1 - s^2)}{s^2}\)
大白话 随机森林厉害的秘密就是「随机」二字:每棵树看到的数据不一样,做决策时能用的特征也不一样。这样树之间就不会「串通一气」,集成起来效果自然好。

什么用:在 AI 工业应用中,随机森林常被用作基线模型和特征筛选工具。在信用评分、欺诈检测、用户流失预测等场景中表现优异。它不需要特征缩放(如归一化),对缺失值鲁棒,能处理混合类型特征(数值 + 类别),且训练速度快(可并行)。在 Kaggle 竞赛中,随机森林 + 特征工程是许多获胜方案的起点。

哪些坑:随机森林在超高维稀疏数据(如文本 TF-IDF 特征)上效果不如线性模型。当树的深度过大时仍然可能过拟合。对噪声特别多的特征,随机森林可能错误地赋予高重要性。模型体量较大(数百棵树),推理时需要存储所有树结构。

二、袋外误差(OOB Error)

是什么:袋外误差(Out-of-Bag Error)是随机森林独有的免费验证机制。对于每棵树,约有 36.8% 的样本未被 Bootstrap 选中(即 OOB 样本),这些样本可以作为该树的验证集。对所有树的 OOB 预测取平均,就得到了 OOB 误差估计。

大白话 不需要额外划分验证集!随机森林自带「免费」的验证机制——每棵树在训练时天然有一部分数据没看到,这些没看到的数据就可以用来测试这棵树好不好。

为什么:OOB 误差被证明是交叉验证的无偏估计,且计算成本几乎为零(训练过程中自然产生)。Breiman 的论文证明了 OOB 误差估计的收敛性——当树的数量足够多时,OOB 误差稳定且接近真实泛化误差。这使得随机森林在调参时无需额外划分验证集。

怎么做

import numpy as np

# ========== 展示 OOB 误差的计算过程 ==========
np.random.seed(42)

def compute_oob_error_demo():
    """演示 OOB 误差的计算原理"""
    n_samples = 100  # 样本数
    n_estimators = 50  # 树的数量
    
    # 模拟数据:4个特征 + 二分类标签
    X = np.random.randn(n_samples, 4)
    y = ((X[:, 0] + X[:, 1] > 0)).astype(int)
    
    # 存储每棵树的 OOB 预测
    oob_predictions = np.zeros((n_samples, n_estimators))  # 样本 × 树的 OOB 预测
    oob_counts = np.zeros((n_samples, n_estimators))  # 标记哪些样本是 OOB
    
    for t in range(n_estimators):
        # Bootstrap 采样
        bootstrap_idx = np.random.choice(n_samples, n_samples, replace=True)
        # 找出 OOB 样本(未被选中的样本)
        oob_idx = np.setdiff1d(np.arange(n_samples), np.unique(bootstrap_idx))
        
        # 标记 OOB 样本
        oob_counts[oob_idx, t] = 1
        
        # 模拟在 Bootstrap 样本上训练,在 OOB 样本上预测
        # 这里简化:用简单规则模拟预测(实际中应在 Bootstrap 上训练,在 OOB 上预测)
        X_boot, y_boot = X[bootstrap_idx], y[bootstrap_idx]
        # 模拟:用特征0的中位数作为阈值做简单分类
        threshold = np.median(X_boot[:, 0])
        for idx in oob_idx:
            oob_predictions[idx, t] = 1 if X[idx, 0] > threshold else 0
    
    # 对每个样本,聚合所有以该样本为 OOB 的树的预测
    final_oob_pred = np.zeros(n_samples)
    for i in range(n_samples):
        # 收集所有将该样本作为 OOB 的树的预测
        valid_trees = np.where(oob_counts[i] == 1)[0]
        if len(valid_trees) > 0:
            # 多数投票
            tree_preds = oob_predictions[i, valid_trees]
            final_oob_pred[i] = np.bincount(tree_preds.astype(int)).argmax()
        else:
            final_oob_pred[i] = -1  # 标记为无 OOB 预测
    
    # 计算 OOB 准确率
    valid_mask = final_oob_pred != -1
    oob_accuracy = np.mean(final_oob_pred[valid_mask] == y[valid_mask])
    print(f"OOB 样本占比: {valid_mask.mean():.1%}")
    print(f"OOB 准确率: {oob_accuracy:.2%}")
    print(f"OOB 误差率: {1 - oob_accuracy:.2%}")
    print(f"\n每个样本平均被 {oob_counts.sum(axis=1).mean():.1f} 棵树作为 OOB 样本")
    print(f"理论值: {n_estimators} * 0.368 = {n_estimators * 0.368:.1f}")

compute_oob_error_demo()
\text{OOB 样本比例推导}\(\lim_{n \to \infty} \left(1 - \frac{1}{n}\right)^n = \frac{1}{e} \approx 0.368\)
大白话 OOB 误差就像随机森林自带的「模拟考试」——每棵树都用自己没见过的数据来检验自己,所有树的检验结果平均起来,就能估计整个森林在真实考试中的表现。

三、特征重要性评估

是什么:随机森林训练完成后,可以计算每个特征对预测的贡献程度,即特征重要性(Feature Importance)。主要有两种方法:基于杂质减少(Mean Decrease in Impurity, MDI)和基于精度下降(Mean Decrease in Accuracy, MDA)。前者统计每个特征在所有树中作为分裂节点时减少的基尼不纯度总和,后者通过随机打乱某特征的值来观察模型精度下降的程度。

大白话 就像你可以问森林里的每棵树「你最看重哪个线索来做判断」,然后统计所有树的回答——被提到次数最多的线索就是最重要的特征。

为什么:特征重要性对于模型解释和特征选择至关重要。在 AI 应用中,知道哪些特征最重要可以帮助:① 理解模型的决策逻辑(可解释性),② 筛选冗余特征以降低模型复杂度,③ 验证业务假设(如「用户年龄是否真的影响购买意愿」)。MDI 方法计算快但倾向于高估高基数特征,MDA 方法更准确但计算成本更高。

怎么做

import numpy as np

# ========== 演示特征重要性的计算 ==========
np.random.seed(42)

def compute_feature_importance_demo():
    """演示两种特征重要性计算方法"""
    # 生成数据:只有前3个特征与标签相关
    n_samples = 200
    X = np.random.randn(n_samples, 6)  # 6个特征
    # 标签由前3个特征决定(特征0最重要,特征1次之,特征2最弱)
    y = (3 * X[:, 0] + 2 * X[:, 1] + 0.5 * X[:, 2] + np.random.randn(n_samples) * 0.5 > 0).astype(int)
    
    # ===== 方法一:基于杂质减少(MDI) =====
    print("=== 方法一:基于杂质减少(MDI)===")
    print("原理:统计每个特征在所有分裂中减少的基尼不纯度总值")
    
    # 模拟:假设我们训练了若干棵树,统计每个特征的分裂次数和重要性
    # 这里用相关系数来近似(实际 MDI 需要完整训练树结构)
    mdi_importance = np.zeros(6)
    for i in range(6):
        # 计算特征与标签的相关系数(绝对值),近似 MDI 重要性
        corr = np.abs(np.corrcoef(X[:, i], y)[0, 1])
        mdi_importance[i] = corr
    
    mdi_importance /= mdi_importance.sum()  # 归一化
    print("特征重要性(MDI,归一化后):")
    for i, imp in enumerate(mdi_importance):
        bar = '█' * int(imp * 50)
        print(f"  特征{i}: {imp:.4f} {bar}")
    
    # ===== 方法二:基于精度下降(MDA) =====
    print("\n=== 方法二:基于精度下降(MDA)===")
    print("原理:随机打乱某特征的值,观察模型精度下降多少")
    
    # 基准准确率(模拟)
    baseline_acc = 0.85  # 原始模型准确率
    mda_importance = np.zeros(6)
    
    for i in range(6):
        # 模拟:打乱特征 i 后,模型准确率下降
        # 特征越重要,打乱后准确率下降越多
        if i < 3:  # 前3个是真实特征,后3个是噪声
            noise_factor = (3 - i) / 3  # 特征0最重要,下降最多
            permuted_acc = baseline_acc - noise_factor * 0.1
        else:
            # 噪声特征打乱后准确率几乎不变(甚至可能略微上升,纯随机)
            permuted_acc = baseline_acc + np.random.uniform(-0.01, 0.01)
        mda_importance[i] = max(0, baseline_acc - permuted_acc)
    
    print("特征重要性(MDA,准确率下降量):")
    for i, imp in enumerate(mda_importance):
        bar = '█' * int(imp * 200)
        print(f"  特征{i}: {imp:.4f} {bar}")
    
    print("\n结论:MDI 和 MDA 都正确识别出前3个特征为重要特征,后3个为噪声特征")
    print("MDI 优点:计算快(训练时自然产生),缺点:偏向高基数特征")
    print("MDA 优点:更准确,缺点:计算成本高(需要多次打乱和预测)")

compute_feature_importance_demo()
\text{MDI 特征重要性公式}\(\text{Imp}(X_j) = \frac{1}{M} \sum_{m=1}^{M} \sum_{t \in T_m} \mathbb{1}(v(t) = j) \cdot \Delta I(t)\)
大白话 MDI 就是问每棵树「你用了哪些特征来分裂」,然后统计每个特征帮了大忙(减少了多少混乱度)。MDA 则是「如果没有这个特征,你的判断会变差多少」——有点像考试时去掉一个信息源,看成绩下降多少。

什么用:特征重要性在 AI 工业中有广泛应用。银行风控模型用它解释为什么拒绝了一笔贷款(如「收入过低」和「信用历史不良」是主要因素)。电商推荐系统用它筛选最有区分力的用户行为特征。在医疗诊断中,它帮助医生理解模型判断疾病时依赖哪些指标,增强模型的可信度。

哪些坑:MDI 方法对高基数特征(如 ID 类特征)有系统性偏差——即使该特征无预测能力,也可能因为分裂点多而被赋予高重要性。当特征之间存在高度相关性时,重要性会被分散到多个相关特征上,导致每个看起来都不重要。MDA 方法虽然更准确,但计算成本随特征数量线性增长。

四、随机森林的调参与实践

是什么:随机森林的关键超参数包括:树的数量(n_estimators)、每棵树的最大深度(max_depth)、每次分裂的特征数(max_features)、最小分裂样本数(min_samples_split)和叶节点最小样本数(min_samples_leaf)。合理的调参可以显著提升性能。

大白话 调参就像调试森林的「生态」——树太少不够用,太多浪费资源;树太深容易「钻牛角尖」,太浅又学不到东西。关键是找到平衡点。

为什么:n_estimators 越大越好,但边际收益递减且计算成本增加——通常 100-500 棵就足够。max_features 控制随机性强度:分类问题默认 √p,回归问题默认 p/3(p 为特征总数)。max_depth 和 min_samples_split 控制树的复杂度,防止过拟合——相比单棵决策树,随机森林对这些参数不那么敏感,但适度限制仍然有益。

怎么做

import numpy as np

# ========== 演示随机森林超参数的影响 ==========
np.random.seed(42)

def hyperparameter_demo():
    """演示不同超参数对随机森林性能的影响"""
    # 生成数据
    n_samples = 300
    X = np.random.randn(n_samples, 10)  # 10个特征,其中5个是噪声
    y = (X[:, 0] + X[:, 1] + X[:, 2] + X[:, 3] + X[:, 4] + 
         np.random.randn(n_samples) * 0.5 > 0).astype(int)
    
    # 简化的训练/测试划分
    split = int(n_samples * 0.7)
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]
    
    # 模拟不同参数下的性能(简化模拟,实际需要训练模型)
    print("=== 超参数影响分析(模拟) ===")
    
    # 1. 树的数量(n_estimators)的影响
    print("\n1. 树的数量对性能的影响(max_depth=10, max_features='sqrt'):")
    for n_trees in [1, 5, 10, 50, 100, 200]:
        # 模拟:树越多,OOB 误差越低(但边际递减)
        base_error = 0.25  # 单棵树误差
        error = base_error / np.sqrt(n_trees)  # 模拟误差随树数增加而降低
        print(f"  树数={n_trees:3d}: 模拟测试误差 ≈ {error:.4f}")
    
    # 2. 最大深度的影响
    print("\n2. 最大深度对性能的影响(n_estimators=100, max_features='sqrt'):")
    for depth in [1, 3, 5, 10, 20, None]:
        depth_str = str(depth) if depth else "无限制"
        # 模拟:深度太小欠拟合,太大过拟合
        if depth is None:
            train_err, test_err = 0.01, 0.15  # 过拟合
        elif depth < 3:
            train_err, test_err = 0.25, 0.25  # 欠拟合
        elif depth < 8:
            train_err, test_err = 0.10, 0.12  # 最佳
        else:
            train_err, test_err = 0.05, 0.13  # 轻微过拟合
        print(f"  深度={depth_str:>6}: 训练误差≈{train_err:.3f}, 测试误差≈{test_err:.3f}")
    
    # 3. max_features 的影响
    print("\n3. max_features 对性能的影响(n_estimators=100, max_depth=10):")
    for mf in ['sqrt', 'log2', 2, 5, 10]:
        label = str(mf)
        if mf == 'sqrt':
            actual = int(np.sqrt(10))
            label = f'sqrt(n)={actual}'
        elif mf == 'log2':
            actual = int(np.log2(10))
            label = f'log2(n)={actual}'
        else:
            actual = mf
        # 模拟:特征太少会欠拟合,太多会降低随机性
        if actual < 3:
            error = 0.18  # 太少,欠拟合
        elif actual < 8:
            error = 0.12  # 最佳范围
        else:
            error = 0.15  # 太多,随机性不足
        print(f"  max_features={label:>12}: 模拟测试误差 ≈ {error:.3f}")
    
    print("\n总结:")
    print("  - n_estimators: 越大越好,但100-500通常足够")
    print("  - max_depth: 分类 √n_features,回归 n_features/3 是常用默认值")
    print("  - max_features: 先用默认值,再微调")
    print("  - 随机森林对超参数不像神经网络那么敏感,默认值通常就很好")

hyperparameter_demo()
大白话 随机森林是个「傻瓜式」的好模型——默认参数往往就能拿到不错的结果,不像神经网络那样需要精心调参。这也是它成为工业界「瑞士军刀」的原因。

概念关系图谱

概念说明关系
Bagging并行集成,降低方差随机森林是 Bagging + 决策树 + 随机特征
Bootstrap有放回采样为每棵树生成不同的训练数据
OOB Error袋外误差免费验证,替代交叉验证
MDI基于杂质减少的重要性计算快,偏向高基数特征
MDA基于精度下降的重要性更准确,计算成本高
基尼不纯度节点分裂标准衡量节点内样本的混乱程度

重点答疑

Q1: 随机森林中「随机」体现在哪些方面?

体现在两个层面:① 样本随机——每棵树使用 Bootstrap 采样得到的子数据集训练(约 63.2% 的样本被选中);② 特征随机——在每个节点分裂时,只考虑随机选取的特征子集(默认大小为 √p,p 为总特征数),而非全部特征。这两重随机性使得树之间相关性降低,集成效果更好。

Q2: 随机森林树的数量越多越好吗?

理论上是的——树的数量越多,泛化误差越稳定,且不会过拟合(因为随机森林的方差随树数增加而单调递减)。但边际收益递减,从 10 棵增加到 100 棵的提升远大于从 100 棵增加到 1000 棵。同时,树越多意味着模型越大、推理越慢。实践中 100-500 棵树通常足够。

Q3: 随机森林如何处理缺失值?

随机森林有两种处理缺失值的方式:① 预处理阶段——在训练前用均值/中位数/众数填充缺失值;② 近邻填充——利用随机森林的邻近度矩阵(Proximity Matrix),用相似样本的值来填充缺失值。第二种方式更能利用数据内在结构,但计算成本更高。

章节单词汇总

英文音标术语/释义
Random Forest/ˈrændəm ˈfɔːrɪst/随机森林;多棵决策树的集成模型
Bootstrap/ˈbuːtstræp/自助法;有放回地随机抽样
Gini Impurity/ˈdʒiːni ɪmˈpjʊrəti/基尼不纯度;衡量节点纯度的指标
OOB Error/ˌoʊ.oʊˈbiː ˈerər/袋外误差;用未采样数据评估模型
Feature Importance/ˈfiːtʃər ɪmˈpɔːrtəns/特征重要性;特征对预测的贡献度
Proximity Matrix/prɑːkˈsɪməti ˈmeɪtrɪks/邻近度矩阵;样本间的相似度度量
MDI/ˌem.diːˈaɪ/Mean Decrease in Impurity;基于杂质减少的重要性
MDA/ˌem.diːˈeɪ/Mean Decrease in Accuracy;基于精度下降的重要性

面试练习

Q1 [单选] 随机森林在每棵树的每个节点分裂时,考虑多少个特征?

  • A. 全部特征
  • B. 随机选择 1 个特征
  • C. 随机选择特征子集(默认 √p 个,p 为总特征数)
  • D. 由用户指定的固定数量,默认 3 个
解答:随机森林在每个节点分裂时从全部 p 个特征中随机选取 √p 个(分类)或 p/3 个(回归)作为候选特征,然后从中选最优分裂。这降低了树与树之间的相关性。

Q2 [单选] 袋外误差(OOB Error)的样本比例理论值约为多少?

  • A. 50%
  • B. 63.2%
  • C. 36.8%
  • D. 10%
解答:每个样本未被选入 Bootstrap 样本的概率为 (1−1/n)^n → 1/e ≈ 36.8%。这些样本就是 OOB 样本,可用于免费验证。

Q3 [多选] 随机森林的特征重要性评估方法有哪些?

  • A. 基于杂质减少(MDI)
  • B. 基于精度下降(MDA)
  • C. 基于梯度大小
  • D. 基于特征值的方差
解答:随机森林主要有两种特征重要性:MDI 统计每个特征在分裂中减少的基尼不纯度;MDA 通过打乱特征值观察精度下降。梯度大小和特征方差不是随机森林的评估方法。

Q4 [单选] 随机森林与单棵决策树相比,主要优势是什么?

  • A. 训练速度更快
  • B. 泛化能力更强,不容易过拟合
  • C. 更易于解释
  • D. 需要更少的计算资源
解答:随机森林通过集成多棵决策树并引入随机性,显著降低了方差,因此泛化能力更强,不容易过拟合。但训练速度更慢,可解释性也差于单棵决策树。

Q5 [单选] 关于随机森林的 max_depth 参数,以下说法正确的是?

  • A. 越大越好,不应限制
  • B. 适度限制可以防止过拟合,但随机森林对其不敏感
  • C. 必须设为 1 才能获得最佳性能
  • D. 只影响训练速度,不影响模型性能
解答:随机森林对 max_depth 不像单棵决策树那样敏感,因为集成天然抗过拟合。但适度限制(如 10-30)仍有助于进一步提升泛化能力,尤其是在数据噪声较大时。

Q6 [多选] 以下哪些是随机森林的优点?

  • A. 不需要特征缩放(归一化/标准化)
  • B. 能处理混合类型的特征(数值 + 类别)
  • C. 天然提供特征重要性评估
  • D. 在文本数据上比线性模型表现更好
解答:随机森林基于决策树,不依赖特征缩放,能处理混合类型特征,且天然提供特征重要性。但在高维稀疏的文本数据上,线性模型(如逻辑回归 + TF-IDF)通常表现更好。

Q7 [单选] MDI 特征重要性方法的一个主要缺陷是什么?

  • A. 计算过于复杂
  • B. 偏向高基数特征(如 ID 类特征)
  • C. 无法处理连续特征
  • D. 需要额外的验证集
解答:MDI 方法对高基数特征(分类特征有很多不同取值)有系统性偏差——即使该特征无预测能力,也可能因为分裂点多而被赋予较高的重要性。这是 MDI 方法的主要局限性。

Q8 [单选] 随机森林中 max_features 参数设为 'sqrt' 时,假设有 100 个特征,每次分裂会考虑多少个?

  • A. 100 个
  • B. 50 个
  • C. 10 个
  • D. 1 个
解答:sqrt(100) = 10。这是分类问题中随机森林的默认值,能有效降低树之间的相关性,提高集成效果。

Q9 [多选] 随机森林的 OOB 误差有什么特点?

  • A. 可以作为交叉验证的替代,无需额外划分验证集
  • B. 是泛化误差的无偏估计
  • C. 计算成本几乎为零(训练过程中自然产生)
  • D. 比交叉验证的估计更保守(悲观)
解答:OOB 误差是泛化误差的无偏估计(而非悲观估计),可以用作交叉验证的免费替代,计算成本极低。这是随机森林的一个重要实用优势。

Q10 [单选] 为什么随机森林中每棵树只使用部分特征而非全部?

  • A. 为了减少训练时间
  • B. 为了降低树之间的相关性,提高集成效果
  • C. 因为全部特征会导致内存溢出
  • D. 为了模拟特征选择的过程
解答:随机特征选择的目的是降低树与树之间的相关性。如果每棵树都使用全部特征,树之间会高度相关,集成后方差降低的效果就会大打折扣——这正是随机森林区别于普通 Bagging 的核心创新。