第三章:决策树与随机森林 —— 像人一样做判断的算法

3 阅读21分钟

适合人群:零基础小白,只要你玩过"二十个问题"猜谜游戏,就能理解决策树!


一、从生活说起:你每天都在用"决策树"

1.1 你今天要不要带伞?

每天出门前,你的大脑其实在做这样的事情:

                        今天要不要带伞?
                             |
                     ┌───────┴───────┐
                     |               |
                 天气预报说有雨?
                     |               |
                ┌────┴────┐     不带伞,出门!
                |         |
             概率>60%?  概率<=60%?
                |         |
            带伞出门    看看窗外有没有乌云
                             |
                        ┌────┴────┐
                        |         |
                      有乌云    没乌云
                        |         |
                     带伞出门   不带伞

恭喜你!你已经理解了决策树的核心思想!

决策树就是:通过一连串的"是/否"问题,最终做出决定。

1.2 二十个问题游戏

还记得小时候玩的猜物游戏吗?

  你心里想一个动物,我来猜:

  Q1: 它是哺乳动物吗? ——> 
  Q2: 它比猫大吗?     ——> 
  Q3: 它是家养的吗?   ——> 
  Q4: 它生活在非洲吗? ——> 
  Q5: 它有长脖子吗?   ——> 

  答案:长颈鹿!🦒

这就是决策树的工作方式!每个问题就是一个"节点",每个回答就是一个"分支"。


二、决策树的基本结构

2.1 决策树长什么样?

                    ┌─────────────┐
                      根节点        <-- 第一个问题(最重要的特征)
                     年收入>10万?│
                    └──────┬──────┘
                     ┌─────┴─────┐
                     |           |
                    是          否
                     |           |
              ┌──────┴──────┐  ┌──┴───────────┐
               内部节点         内部节点     
               有房产?         有担保人?   
              └──────┬──────┘  └──────┬───────┘
               ┌─────┴─────┐    ┌────┴────┐
               |           |    |         |
              是          否   是        否
               |           |    |         |
          ┌────┴───┐  ┌───┴──┐ ┌┴──────┐ ┌┴──────┐
          │叶子节点│  │叶子   │叶子    │叶子   
          │批准贷款│  │审核   │批准    │拒绝   
          └────────┘  └──────┘ └───────┘ └───────┘

   根节点(Root):树的最顶端,第一个判断条件
   内部节点(Internal Node):中间的判断条件
   叶子节点(Leaf):最终的决策结果
   分支(Branch):连接节点的线,代表判断结果

2.2 决策树的关键术语

术语大白话解释生活类比
根节点最先问的问题面试第一个问题
内部节点后续继续问的问题面试追问
叶子节点最终结论面试结果:录用/不录用
深度从根到叶子最长要问几个问题面试有几轮
分支每个问题的不同回答不同的回答路径

三、核心问题:先问哪个问题?

3.1 好问题 vs 坏问题

假设你在相亲,要判断对方是否合适。你会先问什么?

  ❌ 坏问题:"你喜欢吃苹果还是香蕉?"
     → 这个问题几乎不能帮你做任何判断

  ✅ 好问题:"你想要孩子吗?"
     → 这个问题能一下子把人分成两类,帮你快速做决定

决策树的核心就是:每次都选"最好的问题"来问!

那什么是"最好的问题"呢?这就需要用到——信息熵

3.2 信息熵(Entropy)—— 衡量"混乱程度"

大白话:信息熵就是衡量"一堆东西有多乱"。

  想象两个箱子,每个装了10个球:

  箱子A:🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴  (100蓝)
  箱子B:🔴🔴🔴🔴🔴🔵🔵🔵🔵🔵  (55蓝)

  问:从哪个箱子里摸球,你更"不确定"摸到什么颜色?

  答:当然是箱子B!因为红蓝各半,完全猜不到。

  箱子A的熵 = 0      (完全确定,一点都不乱)
  箱子B的熵 = 1      (最不确定,最乱)

数学公式(看不懂可以跳过,理解上面的直觉就够了):

  信息熵 H(S) = -Σ pᵢ × log₂(pᵢ)

  其中 pᵢ 是第 i 类样本的概率

  箱子A: H = -(1×log₂1) = 0
  箱子B: H = -(0.5×log₂0.5 + 0.5×log₂0.5) = 1

信息熵越大 → 越混乱 → 越不确定 信息熵越小 → 越纯净 → 越确定

3.3 信息增益(Information Gain)—— 问了这个问题后,混乱减少了多少?

  信息增益的直觉理解:

  ┌─────────────────────────────────────────────────┐
  │                                                 │
  │  问问题之前:一锅粥,很混乱(熵很大)            │
  │                                                 │
  │  ┌───────────────────────┐                      │
  │  │ 🔴🔵🔴🔵🔴🔵🔴🔵🔴🔵│  熵 = 1.0            │
  │  └───────────────────────┘                      │
  │              |                                  │
  │         问了一个问题                             │
  │        "体重>50kg?"                             │
  │         /          \                            │
  │        是            否                          │
  │        |             |                          │
  │  ┌──────────┐  ┌──────────┐                     │
  │  │🔴🔴🔴🔵 │  │🔵🔵🔵🔴 │                     │
  │  │ 熵=0.81  │  │ 熵=0.81  │                     │
  │  └──────────┘  └──────────┘                     │
  │                                                 │
  │  问问题之后:稍微清晰了一点                       │
  │                                                 │
  │  信息增益 = 问之前的熵 - 问之后的熵               │
  │           = 1.0 - 0.81 = 0.19                   │
  │                                                 │
  │  信息增益越大 → 这个问题越"好" → 越该先问!      │
  └─────────────────────────────────────────────────┘

3.4 基尼不纯度(Gini Impurity)—— 另一种衡量"混乱"的方式

如果信息熵是"称重量",那基尼不纯度就是"量身高"——都是衡量同一个东西,只是方式不同。

  基尼不纯度的直觉理解:

  从一堆样本中随机抽两个,它们类别不同的概率。

  Gini(S) = 1 - Σ pᵢ²

  例子:10个人中,6个会买、4个不买
  Gini = 1 - (0.6² + 0.4²) = 1 - (0.36 + 0.16) = 0.48

  如果10个人全都会买:
  Gini = 1 - (1.0²) = 0    ← 完全纯净!

  如果5个买5个不买:
  Gini = 1 - (0.5² + 0.5²) = 0.5  ← 最混乱!

快速对比:

指标信息熵 (Entropy)基尼不纯度 (Gini)
取值范围[0, log₂(类别数)][0, 0.5](二分类)
计算速度稍慢(有log运算)稍快(只有乘法)
使用场景ID3、C4.5算法CART算法
直觉理解猜对需要多少信息随机猜猜错的概率

四、三大经典算法:ID3、C4.5、CART

4.1 ID3 算法 —— 元老级

  ID3 = 用"信息增益"来选择最佳问题

  优点:简单直观
  缺点:偏爱"选项多"的特征

  举例说明缺点:
  如果有个特征是"身份证号",每个人都不同,
  信息增益会非常大!但这个特征完全没用!
  ID3 会傻傻地选它作为最佳特征。

4.2 C4.5 算法 —— ID3的改良版

  C4.5 = 用"信息增益率"来选择最佳问题

  信息增益率 = 信息增益 / 特征本身的熵

  这样就惩罚了"选项多"的特征,
  因为选项多的特征自身的熵也大,一除就变小了。

  优点:修复了ID3的缺点
  还能处理:连续值特征、缺失值

4.3 CART 算法 —— 现在最常用

  CART = Classification And Regression Trees
       = 分类与回归树

  特点:
  ★ 用"基尼不纯度"来选择最佳问题(分类任务)
  ★ 用"均方误差MSE"来选择最佳问题(回归任务)
  ★ 每次只做二分(是/否),形成二叉树
  ★ sklearn 中默认使用的就是 CART

三兄弟对比总结:

  ┌──────────┬────────────┬────────────┬──────────────┐
  │          │    ID3     │   C4.5CART      │
  ├──────────┼────────────┼────────────┼──────────────┤
  │ 划分标准 │ 信息增益    │ 信息增益率  │ 基尼不纯度   │
  │ 树类型   │ 多叉树      │ 多叉树     │ 二叉树       │
  │ 连续特征 │ 不支持      │ 支持       │ 支持         │
  │ 缺失值   │ 不处理      │ 能处理     │ 能处理       │
  │ 回归任务 │ 不支持      │ 不支持     │ 支持         │
  │ 实际使用 │ 已很少用    │ 偶尔用     │ 最常用 ★     │
  └──────────┴────────────┴────────────┴──────────────┘

五、剪枝(Pruning)—— 给决策树"减肥"

5.1 为什么要剪枝?

  没剪枝的树(过拟合):

  问题1: 你姓什么?
    → 问题2: 你今天穿什么颜色衣服?
      → 问题3: 你早上吃了什么?
        → 问题4: 你今天几点起床的?
          → 结论:你适合买A产品

  这棵树把训练数据"背"下来了,太复杂了!
  换一批新数据就完全不准了。

  这就好比:考试前把答案背下来,但换套题就不会了。

5.2 两种剪枝方式

  ┌─────────────────────────────────────────────────────────┐
  │                                                         │
  │  预剪枝(Pre-pruning)—— 边长边剪                        │
  │  ──────────────────────                                 │
  │  在树生长的过程中,提前停止。                              │
  │                                                         │
  │  方法:                                                  │
  │  • 限制树的最大深度(max_depth=5)                        │
  │  • 限制叶子节点最少样本数(min_samples_leaf=10)          │
  │  • 限制分裂最少样本数(min_samples_split=20)            │
  │                                                         │
  │  类比:规定面试最多只能问3轮                              │
  │                                                         │
  ├─────────────────────────────────────────────────────────┤
  │                                                         │
  │  后剪枝(Post-pruning)—— 先长完再剪                     │
  │  ──────────────────────                                 │
  │  先让树完全长大,然后从叶子往回剪。                        │
  │                                                         │
  │  方法:                                                  │
  │  • 如果剪掉某个分支,整体表现没变差,就剪掉               │
  │  • 用 ccp_alpha 参数控制(代价复杂度剪枝)               │
  │                                                         │
  │  类比:写完作文后再删掉啰嗦的段落                         │
  │                                                         │
  └─────────────────────────────────────────────────────────┘

六、实战案例1:贷款审批预测

6.1 数据说明

  我们有一些历史贷款数据:

  | 年龄段 | 有工作 | 有房子 | 信用等级 | 结果:批准? |
  |--------|--------|--------|----------|-------------|
  | 青年   |  否    |  否    |  一般    |    否       |
  | 青年   |  否    |  否    |  好      |    否       |
  | 青年   |  是    |  否    |  好      |    是       |
  | 青年   |  是    |  是    |  一般    |    是       |
  | 中年   |  否    |  否    |  一般    |    否       |
  | 中年   |  否    |  否    |  好      |    否       |
  | 中年   |  是    |  是    |  好      |    是       |
  | 中年   |  否    |  是    |  非常好  |    是       |
  | 老年   |  否    |  是    |  非常好  |    是       |
  | 老年   |  否    |  是    |  好      |    是       |
  | 老年   |  是    |  否    |  好      |    是       |
  | 老年   |  是    |  否    |  非常好  |    是       |
  | 老年   |  否    |  否    |  一般    |    否       |

6.2 Python 代码实现

import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeClassifier, export_text, plot_tree
from sklearn.preprocessing import LabelEncoder
import matplotlib.pyplot as plt

# ============ 1. 准备数据 ============
data = {
    '年龄段': ['青年','青年','青年','青年','中年','中年','中年','中年',
              '老年','老年','老年','老年','老年'],
    '有工作': ['否','否','是','是','否','否','是','否',
              '否','否','是','是','否'],
    '有房子': ['否','否','否','是','否','否','是','是',
              '是','是','否','否','否'],
    '信用等级': ['一般','好','好','一般','一般','好','好','非常好',
                '非常好','好','好','非常好','一般'],
    '批准': ['否','否','是','是','否','否','是','是',
            '是','是','是','是','否']
}

df = pd.DataFrame(data)
print("原始数据:")
print(df)
print()

# ============ 2. 数据编码(把中文变成数字) ============
# 决策树需要数字输入,所以要把文字转成数字
le_dict = {}
df_encoded = df.copy()

for col in df.columns:
    le = LabelEncoder()
    df_encoded[col] = le.fit_transform(df[col])
    le_dict[col] = le
    print(f"'{col}' 的编码映射: {dict(zip(le.classes_, le.transform(le.classes_)))}")

print()

# ============ 3. 拆分特征和目标 ============
X = df_encoded.drop('批准', axis=1)  # 特征(输入)
y = df_encoded['批准']               # 目标(输出)

# ============ 4. 创建决策树并训练 ============
# criterion='entropy' 使用信息熵(ID3/C4.5风格)
# criterion='gini'    使用基尼不纯度(CART风格,默认)
tree_model = DecisionTreeClassifier(
    criterion='entropy',    # 使用信息熵
    max_depth=3,            # 最大深度为3(预剪枝)
    random_state=42         # 随机种子,保证结果可复现
)

tree_model.fit(X, y)
print("决策树训练完成!")
print(f"训练集准确率: {tree_model.score(X, y):.2%}")
print()

# ============ 5. 查看决策树的文本表示 ============
tree_rules = export_text(
    tree_model,
    feature_names=['年龄段', '有工作', '有房子', '信用等级']
)
print("决策树规则:")
print(tree_rules)

# ============ 6. 可视化决策树 ============
plt.figure(figsize=(16, 8))
plot_tree(
    tree_model,
    feature_names=['年龄段', '有工作', '有房子', '信用等级'],
    class_names=['拒绝', '批准'],
    filled=True,           # 填充颜色
    rounded=True,          # 圆角方框
    fontsize=12
)
plt.title("贷款审批决策树", fontsize=16)
plt.tight_layout()
plt.savefig('loan_decision_tree.png', dpi=150, bbox_inches='tight')
plt.show()

# ============ 7. 预测新数据 ============
# 预测一个"青年、有工作、没房子、信用好"的人
new_person = pd.DataFrame({
    '年龄段': ['青年'],
    '有工作': ['是'],
    '有房子': ['否'],
    '信用等级': ['好']
})

# 编码新数据
new_encoded = new_person.copy()
for col in new_person.columns:
    new_encoded[col] = le_dict[col].transform(new_person[col])

prediction = tree_model.predict(new_encoded)
result = le_dict['批准'].inverse_transform(prediction)
print(f"\n预测结果: 青年、有工作、没房子、信用好 → {result[0]}")

七、决策树的优缺点

  ┌─────────────────────────────────┬──────────────────────────────────┐
  │          ✅ 优点                │           ❌ 缺点               │
  ├─────────────────────────────────┼──────────────────────────────────┤
  │ • 直观易懂,可以画出来看        │ • 容易过拟合(背答案)            │
  │ • 不需要数据标准化              │ • 对数据微小变化很敏感            │
  │ • 能处理数值和类别特征          │ • 容易生成过于复杂的树            │
  │ • 可以看出哪些特征重要          │ • 处理连续值不如其他算法          │
  │ • 训练速度快                    │ • 不擅长学习"异或"这类复杂关系    │
  │ • 可以处理多分类问题            │ • 单棵树的准确率往往不够高        │
  └─────────────────────────────────┴──────────────────────────────────┘

一句话总结:单棵决策树就像一个人做决定,难免有偏见和失误。怎么办?找一群人投票!这就引出了——随机森林。


八、随机森林(Random Forest)—— 三个臭皮匠顶个诸葛亮

8.1 核心思想

  你一个人做决定 → 容易犯错
  你问10个朋友的意见,少数服从多数 → 更靠谱!

  随机森林 = 很多棵决策树一起投票

8.2 随机森林的可视化

  ┌─────────────────────────────────────────────────────────────┐
                        输入数据                               
                [特征1, 特征2, ..., 特征N]                     
  └───────┬──────────────┬──────────────┬───────────────┬──────┘
          |              |              |               |
     有放回抽样      有放回抽样      有放回抽样       有放回抽样
     +随机选特征     +随机选特征     +随机选特征      +随机选特征
          |              |              |               |
    ┌─────┴─────┐  ┌─────┴─────┐ ┌─────┴─────┐  ┌─────┴─────┐
      决策树1       决策树2      决策树3       决策树N   
        /\           /\          /\           /\     
       /  \        /  \       /  \        /  \   
      /\  /\     /\  /\    /\  /\     /\  /\ 
    └─────┬─────┘  └─────┬─────┘ └─────┬─────┘  └─────┬─────┘
          |              |              |               |
       预测:是        预测:否        预测:是          预测:是
          |              |              |               |
          └──────────────┴──────────┬───┴───────────────┘
                                    |
                            ┌───────┴───────┐
                            │   投票箱       │
                            │  是:3  否:1    │
                            │               │
                            │ 最终结果:是!  │
                            └───────────────┘

8.3 两个关键的"随机"

随机森林之所以叫"随机",是因为有两层随机性:

  ┌─────────────────────────────────────────────────────┐
  │                                                     │
  │  随机性1:数据的随机 —— Bagging(自助采样)          │
  │  ─────────────────────────────────────               │
  │                                                     │
  │  原始数据:[A, B, C, D, E, F, G, H, I, J]          │
  │                                                     │
  │  树1的数据:[A, A, C, D, F, F, G, H, I, J]  ← 有重复│
  │  树2的数据:[B, B, C, E, E, F, G, H, I, J]  ← 有重复│
  │  树3的数据:[A, C, C, D, E, F, G, H, J, J]  ← 有重复│
  │                                                     │
  │  就像抽签:每次从10张签里抽10次,每次抽完放回去再抽   │
  │  所以有些签会被重复抽到,有些签一次都没被抽到         │
  │                                                     │
  ├─────────────────────────────────────────────────────┤
  │                                                     │
  │  随机性2:特征的随机 —— Random Subspace              │
  │  ─────────────────────────────────────               │
  │                                                     │
  │  全部特征:[年龄, 收入, 学历, 工作, 房产, 信用]      │
  │                                                     │
  │  树1能用的特征:[年龄, 学历, 信用]    ← 只用一部分    │
  │  树2能用的特征:[收入, 工作, 房产]    ← 只用一部分    │
  │  树3能用的特征:[年龄, 工作, 信用]    ← 只用一部分    │
  │                                                     │
  │  就像考试:每个考官只看其中几项指标来打分             │
  │  这样可以避免所有树都做出一样的判断                   │
  │                                                     │
  │  通常选 sqrt(总特征数) 个特征                        │
  │  如果有16个特征,每棵树随机选4个                      │
  │                                                     │
  └─────────────────────────────────────────────────────┘

8.4 为什么两层随机能提升效果?

  类比:班级投票选班长

  坏情况(没有随机性):
  → 全班同学看到的信息一样,想法也类似
  → 投票结果 = 一个人的意见,集体智慧没发挥出来

  好情况(有随机性):
  → 每个同学只了解候选人的部分情况
  → 有人看重学习成绩,有人看重组织能力,有人看重性格
  → 投票结果综合了多种角度,更全面客观!

  这就是"多样性"的力量!

九、特征重要性(Feature Importance)

9.1 什么是特征重要性?

  特征重要性 = 每个特征对最终决策的贡献有多大

  ┌──────────────────────────────────────────────┐
  │          贷款审批模型的特征重要性              │
  │                                              │
  │  有房子   ████████████████████████████ 0.45  │
  │  有工作   ████████████████████         0.30  │
  │  信用等级 ████████████                 0.18  │
  │  年龄段   ████                         0.07  │
  │                                              │
  │  结论:有没有房子是最重要的判断依据!          │
  └──────────────────────────────────────────────┘

9.2 怎么计算的?

  在决策树中:
  一个特征被用来分裂的次数越多、
  每次分裂减少的不纯度越大,
  → 这个特征就越重要。

  在随机森林中:
  把所有树的特征重要性取平均值。
  因为每棵树只用了部分特征,
  所以随机森林的特征重要性更加稳定可靠。

十、实战案例2:天气预测(是否适合户外运动)

10.1 完整的随机森林代码

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import (accuracy_score, classification_report,
                             confusion_matrix)
import matplotlib.pyplot as plt

# ============ 1. 创建天气数据 ============
np.random.seed(42)
n_samples = 200

data = {
    '温度': np.random.randint(0, 40, n_samples),           # 0-40°C
    '湿度': np.random.randint(20, 100, n_samples),          # 20-100%
    '风速': np.random.randint(0, 50, n_samples),            # 0-50 km/h
    '天气': np.random.choice(['晴天','多云','小雨','大雨'], n_samples),
    '是否周末': np.random.choice([0, 1], n_samples),
}

df = pd.DataFrame(data)

# 根据规则生成标签(是否适合户外运动)
# 规则:温度适中 + 湿度不太高 + 风速不大 + 不下大雨 → 适合
conditions = (
    (df['温度'].between(10, 35)) &
    (df['湿度'] < 85) &
    (df['风速'] < 30) &
    (df['天气'] != '大雨')
)
df['适合运动'] = conditions.astype(int)

# 加一些噪音,让数据更真实
noise_idx = np.random.choice(n_samples, 15, replace=False)
df.loc[noise_idx, '适合运动'] = 1 - df.loc[noise_idx, '适合运动']

print("数据预览(前10行):")
print(df.head(10))
print(f"\n数据形状: {df.shape}")
print(f"适合运动的比例: {df['适合运动'].mean():.2%}")
print()

# ============ 2. 数据预处理 ============
# 把"天气"这个文字特征转成数字(独热编码)
df_processed = pd.get_dummies(df, columns=['天气'], prefix='天气')

# 分离特征和目标
X = df_processed.drop('适合运动', axis=1)
y = df_processed['适合运动']

print(f"特征列: {list(X.columns)}")
print()

# ============ 3. 划分训练集和测试集 ============
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"训练集大小: {len(X_train)}")
print(f"测试集大小: {len(X_test)}")
print()

# ============ 4. 创建随机森林并训练 ============
rf_model = RandomForestClassifier(
    n_estimators=100,       # 100棵树
    max_depth=5,            # 每棵树最大深度5
    min_samples_split=5,    # 分裂最少需要5个样本
    min_samples_leaf=2,     # 叶子节点最少2个样本
    max_features='sqrt',    # 每次分裂随机选 sqrt(特征数) 个特征
    random_state=42,
    n_jobs=-1               # 用所有CPU核心并行训练
)

rf_model.fit(X_train, y_train)
print("随机森林训练完成!")
print()

# ============ 5. 模型评估 ============
y_pred = rf_model.predict(X_test)

print(f"测试集准确率: {accuracy_score(y_test, y_pred):.2%}")
print()

print("分类报告:")
print(classification_report(y_test, y_pred,
                            target_names=['不适合', '适合']))

print("混淆矩阵:")
print(confusion_matrix(y_test, y_pred))
print()

# ============ 6. 交叉验证(更可靠的评估) ============
cv_scores = cross_val_score(rf_model, X, y, cv=5, scoring='accuracy')
print(f"5折交叉验证准确率: {cv_scores.mean():.2%} ± {cv_scores.std():.2%}")
print()

# ============ 7. 特征重要性可视化 ============
feature_importance = pd.Series(
    rf_model.feature_importances_,
    index=X.columns
).sort_values(ascending=True)

plt.figure(figsize=(10, 6))
feature_importance.plot(kind='barh', color='steelblue')
plt.title('特征重要性排名', fontsize=14)
plt.xlabel('重要性分数')
plt.tight_layout()
plt.savefig('feature_importance.png', dpi=150, bbox_inches='tight')
plt.show()

print("特征重要性排名:")
for feat, imp in feature_importance.sort_values(ascending=False).items():
    bar = '█' * int(imp * 50)
    print(f"  {feat:12s} {bar} {imp:.4f}")

# ============ 8. 预测新数据 ============
new_weather = pd.DataFrame({
    '温度': [25],
    '湿度': [60],
    '风速': [10],
    '是否周末': [1],
    '天气_多云': [1],
    '天气_大雨': [0],
    '天气_小雨': [0],
    '天气_晴天': [0],
})

# 确保列顺序一致
new_weather = new_weather.reindex(columns=X.columns, fill_value=0)

pred = rf_model.predict(new_weather)
prob = rf_model.predict_proba(new_weather)

print(f"\n预测:25°C、湿度60%、风速10km/h、周末、多云")
print(f"结果:{'适合运动' if pred[0] == 1 else '不适合运动'}")
print(f"概率:适合={prob[0][1]:.2%}  不适合={prob[0][0]:.2%}")

十一、随机森林 vs 决策树 vs 其他算法

  ┌──────────────┬──────────┬──────────┬──────────┬──────────┐
  │   方面       │ 决策树   │ 随机森林  │ 逻辑回归  │ SVM     │
  ├──────────────┼──────────┼──────────┼──────────┼──────────┤
  │ 准确率       │ 一般     │ 高 ★     │ 一般     │ 高       │
  │ 训练速度     │ 快 ★     │ 较慢     │ 快       │ 慢       │
  │ 预测速度     │ 快 ★     │ 较快     │ 快       │ 快       │
  │ 可解释性     │ 强 ★     │ 中等     │ 中等     │ 弱       │
  │ 过拟合风险   │ 高       │ 低 ★     │ 低       │ 中       │
  │ 需要调参     │ 少       │ 少 ★     │ 少       │ 多       │
  │ 处理缺失值   │ 能       │ 能 ★     │ 不能     │ 不能     │
  │ 特征重要性   │ 有       │ 更可靠 ★  │ 有      │ 无       │
  └──────────────┴──────────┴──────────┴──────────┴──────────┘

  结论:随机森林是"性价比"最高的算法之一,
       适合作为大多数分类任务的第一选择!

十二、调参指南:随机森林的超参数

12.1 最重要的参数

RandomForestClassifier(
    # ====== 最重要的参数 ======
    n_estimators=100,       # 树的数量,越多越好(但更慢)
                            # 建议:100-500,先试100

    max_depth=None,         # 树的最大深度
                            # None=不限制,容易过拟合
                            # 建议:5-20,根据数据复杂度调

    max_features='sqrt',    # 每次分裂考虑的特征数
                            # 'sqrt'=根号下特征数(分类默认)
                            # 'log2'=log2特征数
                            # 建议:先用'sqrt'

    # ====== 防止过拟合的参数 ======
    min_samples_split=2,    # 节点分裂最少需要的样本数
                            # 建议:5-20

    min_samples_leaf=1,     # 叶子节点最少样本数
                            # 建议:2-10

    # ====== 其他参数 ======
    random_state=42,        # 随机种子
    n_jobs=-1,              # 并行计算(-1=用所有CPU)
    oob_score=True,         # 是否用袋外数据评估(免费的验证集!)
)

12.2 用 GridSearchCV 自动调参

from sklearn.model_selection import GridSearchCV

# 定义参数搜索范围
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'max_features': ['sqrt', 'log2']
}

# 创建网格搜索
grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42),
    param_grid,
    cv=5,                  # 5折交叉验证
    scoring='accuracy',    # 评估指标
    n_jobs=-1,            # 并行搜索
    verbose=1             # 显示进度
)

# 开始搜索最佳参数
grid_search.fit(X_train, y_train)

print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳准确率: {grid_search.best_score_:.2%}")

# 用最佳模型预测
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test)
print(f"测试集准确率: {accuracy_score(y_test, y_pred_best):.2%}")

十三、袋外评估(OOB Score)—— 免费的验证集

  Bagging采样时,大约有 36.8% 的数据不会被某棵树抽到。
  这些"没被抽到"的数据叫做袋外数据(Out-of-Bag)。

  ┌───────────────────────────────────────────────┐
  │  原始数据: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]   │
  │                                               │
  │  树1抽到:  [1, 1, 3, 5, 5, 6, 7, 8, 9, 10]   │
  │  树1没抽到: [2, 4]  ← 用这些来测试树1          │
  │                                               │
  │  树2抽到:  [2, 2, 3, 4, 6, 6, 7, 8, 9, 10]   │
  │  树2没抽到: [1, 5]  ← 用这些来测试树2          │
  │                                               │
  │  每棵树都用自己没见过的数据来测试,              │
  │  最后取平均,就得到了 OOB 准确率。              │
  │                                               │
  │  好处:不需要额外划分验证集!数据利用率更高!     │
  └───────────────────────────────────────────────┘
# 使用 OOB 评估
rf_oob = RandomForestClassifier(
    n_estimators=100,
    oob_score=True,        # 开启袋外评估
    random_state=42
)
rf_oob.fit(X_train, y_train)

print(f"OOB 准确率: {rf_oob.oob_score_:.2%}")
# 这个准确率不需要额外的测试集,非常方便!

十四、常见问题 Q&A

Q1: 决策树和随机森林哪个好?

  答:绝大多数情况下,随机森林更好。

  决策树适合:
  • 需要很强的可解释性(比如向老板解释为什么拒绝贷款)
  • 数据量很小
  • 快速原型验证

  随机森林适合:
  • 追求更高准确率
  • 数据量较大
  • 不想花太多时间调参

Q2: 随机森林有什么缺点?

  • 不如单棵树那么好解释(100棵树怎么解释?)
  • 训练时间比单棵树长(但可以并行加速)
  • 对极端不平衡数据效果一般
  • 不擅长处理高维稀疏数据(如文本分类)
  • 内存占用较大(存了很多棵树)

Q3: n_estimators(树的数量)越多越好吗?

  基本上是的,但有边际递减效应:

  树的数量    准确率(示意)
  1           70%
  10          85%
  50          90%
  100         91%       ← 一般到这里就够了
  500         91.5%     ← 提升不大,但训练时间翻了5倍
  1000        91.6%     ← 几乎没提升了

  建议:先从100开始,如果效果不够好再往上加。

Q4: 随机森林能做回归吗?

  # 当然可以!用 RandomForestRegressor
  from sklearn.ensemble import RandomForestRegressor

  rf_reg = RandomForestRegressor(n_estimators=100)
  rf_reg.fit(X_train, y_train)   # y_train 是连续值
  predictions = rf_reg.predict(X_test)

十五、完整项目模板

下面给一个从头到尾的实战模板,直接复制修改就能用:

"""
随机森林分类项目模板
直接修改数据部分就能应用到你自己的项目
"""

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, classification_report
from sklearn.preprocessing import LabelEncoder
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

# ===== Step 1: 加载数据 =====
# 替换为你自己的数据文件
# df = pd.read_csv('your_data.csv')

# 这里用示例数据
from sklearn.datasets import load_iris
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['target'] = iris.target

print("=" * 50)
print("Step 1: 数据概览")
print("=" * 50)
print(f"数据形状: {df.shape}")
print(f"前5行:\n{df.head()}")
print(f"\n各类别数量:\n{df['target'].value_counts()}")

# ===== Step 2: 数据预处理 =====
print("\n" + "=" * 50)
print("Step 2: 数据预处理")
print("=" * 50)

X = df.drop('target', axis=1)
y = df['target']

# 检查缺失值
print(f"缺失值:\n{X.isnull().sum()}")

# 如果有文字特征,需要编码
# le = LabelEncoder()
# X['文字列'] = le.fit_transform(X['文字列'])

# ===== Step 3: 划分数据集 =====
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"\n训练集: {X_train.shape}, 测试集: {X_test.shape}")

# ===== Step 4: 训练模型 =====
print("\n" + "=" * 50)
print("Step 4: 训练随机森林")
print("=" * 50)

model = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    min_samples_split=5,
    min_samples_leaf=2,
    max_features='sqrt',
    oob_score=True,
    random_state=42,
    n_jobs=-1
)

model.fit(X_train, y_train)
print("模型训练完成!")

# ===== Step 5: 评估模型 =====
print("\n" + "=" * 50)
print("Step 5: 模型评估")
print("=" * 50)

train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
oob_acc = model.oob_score_

print(f"训练集准确率: {train_acc:.2%}")
print(f"测试集准确率: {test_acc:.2%}")
print(f"OOB准确率:    {oob_acc:.2%}")

# 交叉验证
cv_scores = cross_val_score(model, X, y, cv=5)
print(f"5折交叉验证:  {cv_scores.mean():.2%} ± {cv_scores.std():.2%}")

print(f"\n分类报告:\n{classification_report(y_test, model.predict(X_test))}")

# ===== Step 6: 特征重要性 =====
print("\n" + "=" * 50)
print("Step 6: 特征重要性")
print("=" * 50)

importances = pd.Series(model.feature_importances_, index=X.columns)
importances = importances.sort_values(ascending=False)

for feat, imp in importances.items():
    bar = '█' * int(imp * 40)
    print(f"  {feat:25s} {bar} {imp:.4f}")

# 可视化
plt.figure(figsize=(10, 6))
importances.sort_values().plot(kind='barh', color='steelblue')
plt.title('Feature Importance', fontsize=14)
plt.xlabel('Importance Score')
plt.tight_layout()
plt.savefig('feature_importance.png', dpi=150)
plt.show()

print("\n项目完成!")

十六、知识总结脑图

  决策树与随机森林
  │
  ├── 决策树 (Decision Tree)
  │   ├── 核心思想:通过一连串判断做决定
  │   ├── 关键概念
  │   │   ├── 信息熵 —— 衡量混乱程度
  │   │   ├── 信息增益 —— 问题带来的"确定性提升"
  │   │   └── 基尼不纯度 —— 随机猜错的概率
  │   ├── 三大算法
  │   │   ├── ID3:用信息增益(有缺陷)
  │   │   ├── C4.5:用信息增益率(改良版)
  │   │   └── CART:用基尼/MSE(最常用 ★)
  │   ├── 剪枝
  │   │   ├── 预剪枝:限制深度/样本数
  │   │   └── 后剪枝:先长完再剪
  │   └── 优缺点
  │       ├── 优:直观、快速、可解释
  │       └── 缺:容易过拟合
  │
  ├── 随机森林 (Random Forest)
  │   ├── 核心思想:多棵树投票,少数服从多数
  │   ├── 两个"随机"
  │   │   ├── 数据随机:有放回采样(Bagging)
  │   │   └── 特征随机:每棵树只用部分特征
  │   ├── 重要参数
  │   │   ├── n_estimators:树的数量(100起步)
  │   │   ├── max_depth:树的深度
  │   │   └── max_features:特征采样数
  │   ├── 特殊功能
  │   │   ├── OOB评估:免费的验证集
  │   │   └── 特征重要性:哪些特征最有用
  │   └── 优缺点
  │       ├── 优:准确率高、不易过拟合、少调参
  │       └── 缺:不好解释、训练较慢
  │
  └── 实践建议
      ├── 先用随机森林试试(大多数情况够用)
      ├── 需要解释性时用单棵决策树
      ├── n_estimators 从100开始
      └── 用交叉验证评估,别只看训练集准确率

附录:sklearn 常用 API 速查

# ===== 决策树 =====
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor

# 分类树
dtc = DecisionTreeClassifier(criterion='gini', max_depth=5)
dtc.fit(X_train, y_train)
dtc.predict(X_test)
dtc.predict_proba(X_test)       # 预测概率
dtc.feature_importances_        # 特征重要性

# 回归树
dtr = DecisionTreeRegressor(max_depth=5)
dtr.fit(X_train, y_train)
dtr.predict(X_test)

# 可视化决策树
from sklearn.tree import export_text, plot_tree
print(export_text(dtc, feature_names=list(X.columns)))
plot_tree(dtc, filled=True, rounded=True)

# ===== 随机森林 =====
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor

# 分类
rfc = RandomForestClassifier(n_estimators=100, oob_score=True)
rfc.fit(X_train, y_train)
rfc.predict(X_test)
rfc.predict_proba(X_test)
rfc.oob_score_                  # 袋外准确率
rfc.feature_importances_        # 特征重要性
rfc.estimators_                 # 所有树的列表

# 回归
rfr = RandomForestRegressor(n_estimators=100)
rfr.fit(X_train, y_train)
rfr.predict(X_test)

下一章预告:第四章将介绍支持向量机(SVM),一种在高维空间中寻找最优分隔面的强大算法。

本章要点回顾

  1. 决策树 = 通过一连串问题做决定
  2. 选"好问题"的标准:信息增益/基尼不纯度
  3. 剪枝防止过拟合
  4. 随机森林 = 多棵树投票 = Bagging + 随机特征选择
  5. 随机森林是最好用的"开箱即用"算法之一