适合人群:零基础小白,只要你玩过"二十个问题"猜谜游戏,就能理解决策树!
一、从生活说起:你每天都在用"决策树"
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:🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴 (10红0蓝)
箱子B:🔴🔴🔴🔴🔴🔵🔵🔵🔵🔵 (5红5蓝)
问:从哪个箱子里摸球,你更"不确定"摸到什么颜色?
答:当然是箱子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.5 │ CART │
├──────────┼────────────┼────────────┼──────────────┤
│ 划分标准 │ 信息增益 │ 信息增益率 │ 基尼不纯度 │
│ 树类型 │ 多叉树 │ 多叉树 │ 二叉树 │
│ 连续特征 │ 不支持 │ 支持 │ 支持 │
│ 缺失值 │ 不处理 │ 能处理 │ 能处理 │
│ 回归任务 │ 不支持 │ 不支持 │ 支持 │
│ 实际使用 │ 已很少用 │ 偶尔用 │ 最常用 ★ │
└──────────┴────────────┴────────────┴──────────────┘
五、剪枝(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),一种在高维空间中寻找最优分隔面的强大算法。
本章要点回顾:
- 决策树 = 通过一连串问题做决定
- 选"好问题"的标准:信息增益/基尼不纯度
- 剪枝防止过拟合
- 随机森林 = 多棵树投票 = Bagging + 随机特征选择
- 随机森林是最好用的"开箱即用"算法之一