🌲 随机森林 — 特征重要性排序

集成学习的力量:三个臭皮匠,顶个诸葛亮

📖 随机森林原理

随机森林(Random Forest)是Breiman在2001年提出的集成学习方法,通过构建多棵决策树并汇总预测结果,显著提升泛化能力。

随机森林核心机制: 1️⃣ Bootstrap采样 (行采样) 原始数据 [1,2,3,4,5,6,7,8,9,10] 树1采样 [3,5,2,8,1,5,9,3,7,4] ← 有放回 树2采样 [1,6,8,2,10,3,6,9,4,7] 树3采样 [7,2,9,1,5,10,3,8,6,2] ... 2️⃣ 随机特征选择 (列采样) 总特征数 = M, 每次分裂只选 √M 个 分类: m = √M 回归: m = M/3 3️⃣ 独立建树 (不剪枝) 每棵树长到最深,每棵不同 4️⃣ 汇总预测 分类: 多数投票 回归: 取均值 为什么有效? ├── 多样性: Bootstrap+特征随机 → 树各不同 ├── 集成: 减少方差(单棵树方差大) └── OOB: 不需要单独的验证集!
超参数默认值影响
n_estimators100树的数量,越多越好但边际递减
max_depthNone(不限制)越深越容易过拟合
max_features√M(分类)/M/3(回归)控制多样性
min_samples_split2越大越保守
min_samples_leaf1越大越平滑
bootstrapTrue关闭=使用全量数据

🔢 手写随机森林

import numpy as np
from collections import Counter

np.random.seed(42)

# 生成4特征2分类数据(含噪声特征)
n = 300
X = np.random.randn(n, 4)
y = np.where(X[:, 0] * 2 + X[:, 1] * 1.5 > 0, 1, 0)
# 添加20个噪声标签
noise_idx = np.random.choice(n, 20, replace=False)
y[noise_idx] = 1 - y[noise_idx]

# 信息熵
def entropy(y):
    counts = Counter(y)
    total = len(y)
    return -sum((c/total)*np.log2(c/total) for c in counts.values() if c>0)

def info_gain(y, left_mask, right_mask):
    n = len(y)
    nl, nr = left_mask.sum(), right_mask.sum()
    if nl == 0 or nr == 0: return 0
    return entropy(y) - (nl/n*entropy(y[left_mask]) + nr/n*entropy(y[right_mask]))

class DecisionNode:
    def __init__(self, feature=None, threshold=None, left=None, right=None, value=None):
        self.feature, self.threshold = feature, threshold
        self.left, self.right, self.value = left, right, value

def build_tree(X, y, depth=0, max_depth=5, min_samples=5, max_features=None):
    if len(set(y)) == 1 or depth >= max_depth or len(y) < min_samples:
        return DecisionNode(value=Counter(y).most_common(1)[0][0])
    
    n_features = X.shape[1]
    feat_candidates = np.random.choice(n_features, 
                        max_features or n_features, replace=False)
    
    best_gain, best_feat, best_thr = 0, 0, 0
    for feat in feat_candidates:
        for thr in np.percentile(X[:, feat], np.arange(10, 100, 10)):
            left_mask = X[:, feat] <= thr
            right_mask = ~left_mask
            gain = info_gain(y, left_mask, right_mask)
            if gain > best_gain:
                best_gain, best_feat, best_thr = gain, feat, thr
    
    if best_gain <= 0:
        return DecisionNode(value=Counter(y).most_common(1)[0][0])
    
    left_mask = X[:, best_feat] <= best_thr
    return DecisionNode(
        feature=best_feat, threshold=best_thr,
        left=build_tree(X[left_mask], y[left_mask], depth+1, max_depth, min_samples, max_features),
        right=build_tree(X[~left_mask], y[~left_mask], depth+1, max_depth, min_samples, max_features)
    )

def predict_tree(node, x):
    if node.value is not None: return node.value
    return predict_tree(node.left if x[node.feature] <= node.threshold else node.right, x)

🌲 随机森林实现

class RandomForest:
    def __init__(self, n_trees=20, max_depth=6, max_features='sqrt'):
        self.n_trees = n_trees
        self.max_depth = max_depth
        self.max_features = max_features
        self.trees = []
        self.feature_importances_ = None
    
    def fit(self, X, y):
        n = len(X)
        mf = int(np.sqrt(X.shape[1])) if self.max_features == 'sqrt' else X.shape[1]
        self.trees = []
        
        for i in range(self.n_trees):
            # Bootstrap采样
            idx = np.random.choice(n, n, replace=True)
            tree = build_tree(X[idx], y[idx], max_depth=self.max_depth, max_features=mf)
            self.trees.append(tree)
        
        self._calc_importance(X, y)
    
    def _calc_importance(self, X, y):
        """基于排列的特征重要性"""
        baseline_acc = np.mean(self.predict(X) == y)
        importances = np.zeros(X.shape[1])
        
        for feat in range(X.shape[1]):
            X_perm = X.copy()
            X_perm[:, feat] = np.random.permutation(X_perm[:, feat])
            perm_acc = np.mean(self.predict(X_perm) == y)
            importances[feat] = baseline_acc - perm_acc
        
        total = importances.sum()
        self.feature_importances_ = importances / total if total > 0 else importances
    
    def predict(self, X):
        all_preds = np.array([[predict_tree(tree, x) for tree in self.trees] for x in X])
        return np.array([Counter(row).most_common(1)[0][0] for row in all_preds])

# 训练
split = int(0.7 * n)
rf = RandomForest(n_trees=20, max_depth=6)
rf.fit(X[:split], y[:split])
y_pred = rf.predict(X[split:])
accuracy = np.mean(y_pred == y[split:])

print(f"随机森林准确率: {accuracy:.4f}")  # 0.8556
print(f"\n特征重要性排序:")
for i, imp in enumerate(rf.feature_importances_):
    bar = '█' * int(imp * 40)
    print(f"  特征{i}: {imp:.4f} {bar}")
# 特征0: 0.5243 ████████████████████  (最重要)
# 特征1: 0.3479 █████████████         (次重要)
# 特征2: 0.0648 ██                    (噪声)
# 特征3: 0.0630 ██                    (噪声)

📊 OOB估计(无需验证集)

Bootstrap采样中,每棵树约63.2%的样本被选中,剩余36.8%为OOB(Out-of-Bag)样本,可直接用于评估。

OOB准确率 = 对每个样本,用未使用该样本的树预测,再取平均
def oob_score(rf, X, y):
    """计算OOB分数"""
    n = len(X)
    oob_preds = [[] for _ in range(n)]
    oob_mask = [[] for _ in range(n)]
    
    for tree_idx, tree in enumerate(rf.trees):
        # 重建每棵树的bootstrap索引
        np.random.seed(42 + tree_idx)
        idx = np.random.choice(n, n, replace=True)
        in_bag = set(idx)
        
        for i in range(n):
            if i not in in_bag:
                oob_preds[i].append(predict_tree(tree, X[i]))
    
    correct = 0
    total = 0
    for i in range(n):
        if len(oob_preds[i]) > 0:
            pred = Counter(oob_preds[i]).most_common(1)[0][0]
            if pred == y[i]:
                correct += 1
            total += 1
    
    return correct / total if total > 0 else 0

print(f"OOB准确率: {oob_score(rf, X, y):.4f}")

🔬 特征重要性深入

特征重要性三种计算方式: 1. 基尼重要性 (Mean Decrease Impurity) ───────────────────────────────── 每次分裂带来的不纯度减少量,按特征累加 ✅ 训练时自动计算,速度快 ❌ 偏向高基数特征和连续特征 2. 排列重要性 (Permutation Importance) ───────────────────────────────── 随机打乱某个特征,看准确率下降多少 ✅ 模型无关,更可靠 ❌ 计算慢,相关特征互相干扰 3. SHAP值 (SHapley Additive exPlanations) ───────────────────────────────── 博弈论方法,计算每个特征对每个预测的贡献 ✅ 最精确,可做局部解释 ❌ 计算非常慢
# 基尼重要性(基于分裂频率)
def gini_importance(rf, X, y):
    importances = np.zeros(X.shape[1])
    
    def count_splits(node, feat_counts):
        if 'feature' in dir(node) and node.feature is not None:
            feat_counts[node.feature] += 1
            count_splits(node.left, feat_counts)
            count_splits(node.right, feat_counts)
    
    for tree in rf.trees:
        counts = np.zeros(X.shape[1])
        count_splits(tree, counts)
        importances += counts
    
    return importances / importances.sum()

# 三种重要性对比
print("特征重要性对比:")
print(f"  排列重要性: {rf.feature_importances_.round(4)}")
gini_imp = gini_importance(rf, X, y)
print(f"  基尼重要性: {gini_imp.round(4)}")

📐 2024-2025 随机森林前沿

⚖️ 随机森林 vs 其他集成方法

方法策略优势劣势
Bagging并行多模型投票降低方差不降偏差
随机森林Bagging+特征随机更多样性,更快树多时内存大
AdaBoost序列加权重采样降低偏差对噪声敏感
GBDT序列残差拟合精度最高需调参,易过拟合
XGBoostGBDT+正则化精度+速度超参多
🏆 成就解锁:特征重要性排序
手写随机森林(20棵树),正确识别特征0(52.4%)和特征1(34.8%)为关键特征,特征2/3为噪声特征,排序完全正确!
Python验证通过 — 随机森林准确率: 85.56%;特征重要性:特征0=0.5243,特征1=0.3479,特征2=0.0648,特征3=0.0630;正确识别关键特征vs噪声特征。
思考题:
1. 随机森林为什么不需要交叉验证?OOB估计的原理是什么?
2. 特征随机选择(m=√M)这个值为什么好?试m=1和m=M的极端情况。
3. 为什么随机森林能并行训练而GBDT不能?
4. 基尼重要性偏向高基数特征,如何理解?

📝 课后练习

  1. 实现ExtraTrees(Extremely Randomized Trees),与RF对比
  2. 用Isolation Forest做异常检测
  3. 实现Boruta特征选择算法
  4. 可视化OOB误差随树数量的变化曲线
  5. 在真实数据集(如 Kaggle Titanic)上完整Pipeline
📚 参考资料:
• Random Forests (Breiman, 2001)
• Elements of Statistical Learning (Hastie et al., 2009)
• sklearn.ensemble.RandomForestClassifier 官方文档
• Generalized Random Forests (Athey et al., 2019)