4.2 决策树与随机森林

决策树与随机森林

本文适合谁:了解基本编程、想知道"为什么神经网络之外还要学树模型"的读者。树模型在表格数据上至今仍是首选,理解它也是理解集成学习这一核心思想的最佳切入点。

本文阅读时间:约12分钟

决策树是最直观的机器学习模型,随机森林是工业界的"瑞士军刀"。本篇从"线性模型为什么不够用"出发,一步步建立你对树模型的直觉理解。


一、为什么需要决策树:线性模型的局限

线性模型只能画直线边界

上一篇介绍的线性回归和逻辑回归,都有一个根本性的假设:数据可以用一条直线(或超平面)分开。在很多真实场景中,这个假设并不成立。

考虑一个例子:判断一笔交易是否是欺诈。欺诈交易的特征可能是:"金额在1000元到5000元之间,且发生在凌晨2点到4点之间,且是新注册用户"。这个规则是三个条件的组合,在特征空间中形成的是一个"矩形区域",而不是一条直线。逻辑回归无法学出这种边界,因为它的决策边界只能是直线。

更本质地说,线性模型假设"每个特征对预测结果的贡献是独立的"。但现实中,特征之间往往有交互:"年龄<25"单独看不能判断是否欺诈,但"年龄<25 且 消费金额>10000"组合在一起就是一个强烈的信号。线性模型很难捕捉这种特征交互。

决策树提供了一种完全不同的思路:不用直线划分,而是用一系列if-else规则。每个规则关注一个特征,通过逐层嵌套,就能描述任意复杂的区域边界。


二、决策树:像玩猜动物游戏一样做决策

猜动物游戏的类比

小时候玩过猜谜游戏吗?一个人心里想一种动物,另一个人通过问"是/否"问题来猜:

复制代码
问:有四条腿吗?
答:有
问:体型很大吗?
答:不是(体型中等)
问:会游泳吗?
答:不会
问:主要是黑白两色吗?
答:不是
→ 猜测:猫

每个问题都能把可能性缩小一半。问题提得越好,猜的速度越快。决策树做的事情完全相同:通过一系列"是/否"问题,逐步缩小范围,直到给出最终预测。

复制代码
是否下雨?
├── 是 → 带伞
└── 否 → 气温是否超过30度?
         ├── 是 → 穿短袖
         └── 否 → 穿外套

决策树示例:天气穿搭决策树

这就是决策树:一系列if-else判断,每个节点(树中的每个判断点)基于某个特征做分裂。

信息增益:选哪个特征分割最有效

决策树的关键问题是:每一步应该选哪个特征来做分割?

直觉是:好的问题能把数据分得更"纯"。如果一堆数据里既有猫又有狗,一个好问题(比如"有没有胡须")可以把猫和狗大部分分开,问完之后每个分支里基本都是同一类动物。而一个差问题(比如"体重是否超过3公斤")可能分完之后每个分支里还是乱的。

**基尼不纯度(Gini Impurity)**度量"混乱程度":

  • 一个节点里只有一种类别(完全"纯"):Gini = 0,最好
  • 一个节点里各类别各占一半(完全"混"):Gini = 0.5(二分类),最差

信息增益(Information Gain) = 分割前的不纯度 - 分割后的加权不纯度。增益越大,说明这个分割越有价值。

每次分裂,决策树会穷举所有特征和所有可能的分割点,选择信息增益最大的那个------这就是决策树的"贪心"策略:每一步都做当前最优的选择。

决策树代码实战

这段代码在做什么:加载鸢尾花数据集(150个样本,4个特征,3类),训练决策树,限制深度为3防止过拟合,然后可视化决策规则。

python 复制代码
from sklearn.tree import DecisionTreeClassifier, export_text, plot_tree
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 训练决策树
# max_depth=3:限制树的深度,防止过拟合(深度越大,越容易记住训练数据细节)
# min_samples_split=5:节点至少要有5个样本才能继续分裂
dt = DecisionTreeClassifier(
    max_depth=3,
    min_samples_split=5,
    random_state=42
)
dt.fit(X_train, y_train)

print(f"训练集准确率: {dt.score(X_train, y_train):.4f}")
print(f"测试集准确率: {dt.score(X_test, y_test):.4f}")

# 打印文本形式的决策规则(人类可读)
print("\n决策树规则:")
print(export_text(dt, feature_names=list(iris.feature_names)))

你应该看到类似这样的输出

复制代码
训练集准确率: 0.9750
测试集准确率: 0.9667

决策树规则:
|--- petal length (cm) <= 2.45
|   |--- class: 0               ← 花瓣长度<=2.45 → setosa(直接确定)
|--- petal length (cm) > 2.45
|   |--- petal width (cm) <= 1.75
|   |   |--- petal length (cm) <= 4.95
|   |   |   |--- class: 1       ← versicolor
|   |   |--- petal length (cm) > 4.95
|   |   |   |--- class: 2       ← virginica
|   |--- petal width (cm) > 1.75
|   |   |--- class: 2           ← virginica

这就是决策树的魅力:规则完全可读,你能直接看懂模型在做什么判断。这在需要解释模型决策的场景(医疗、金融)非常有价值。


三、决策树的致命问题:过拟合

为什么决策树容易过拟合

决策树的贪心策略有一个严重问题:如果不加限制,它会一直生长,直到每个叶子节点里只有一个样本------这时候训练集准确率是100%,但对新数据几乎没有泛化能力。

直觉:一棵极深的树,等于把训练数据的每一个细节都"记住了",包括噪声和偶然规律。就像一个学生死记硬背了所有练习题的答案,但换一道题就不会了。

python 复制代码
# 不限制深度的决策树会严重过拟合
dt_overfit = DecisionTreeClassifier(random_state=42)  # 没有 max_depth 限制
dt_overfit.fit(X_train, y_train)

print(f"不限深度 - 训练集: {dt_overfit.score(X_train, y_train):.4f}")
print(f"不限深度 - 测试集: {dt_overfit.score(X_test, y_test):.4f}")
# 你会看到训练集接近1.0,测试集明显低 → 过拟合

解决过拟合的方案:随机森林


四、随机森林:投票委员会

为什么多棵弱树比一棵强树更好

设想你要做一个重要决策,比如是否接受一份工作。你可以:

  • 方案A:找一位"最聪明的"朋友问意见
  • 方案B:问20个不同背景的朋友,然后综合大家的意见

方案B往往更可靠,即使这20个人单独来看都不如那位"最聪明的"朋友。原因在于:每个人的错误往往是不同的(一个人看重薪资,另一个人看重发展空间),当你综合多人意见时,个人的偏见会相互抵消,留下的是共同的判断。

这就是**集成学习(Ensemble Learning)**的核心思想,背后有严格的理论支撑------偏差-方差权衡(Bias-Variance Tradeoff)

单棵深决策树的问题是高方差------它对训练数据太敏感,换一批训练数据,学出的树可能完全不同。随机森林的解决思路:训练很多棵各不相同的树,让它们投票。每棵树都有高方差,但因为它们的错误不相关,投票之后错误会相互抵消,整体的方差大大降低。

这就是"多棵弱树比一棵强树更好"的原因:不是因为每棵树变强了,而是因为它们的错误相互抵消了。

复制代码
样本1 → 树1 → 预测A
样本1 → 树2 → 预测B  → 多数投票(2:1投票A获胜)→ 最终预测A
样本1 → 树3 → 预测A

随机森林:多棵决策树投票决策

两个关键随机性:为什么要"随机"

随机森林的"随机"体现在两个地方,目的是让树与树之间更不一样(相关性更低),集成效果更好:

1. Bootstrap采样:每棵树用随机有放回抽样的样本训练。每棵树只看到约63%的数据,而且每棵树看到的数据集不同,保证了树之间的差异性。

2. 特征随机:每次分裂时,不考虑全部特征,而是随机选一个子集来考虑,进一步增加树的多样性。

随机森林代码实战

这段代码在做什么:在乳腺癌数据集上训练随机森林,展示准确率,并打印特征重要性(每个特征对预测的贡献有多大)。

python 复制代码
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import pandas as pd

data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 随机森林
# n_estimators=100:训练100棵树
# max_depth=10:每棵树最大深度10
# n_jobs=-1:并行训练(用全部CPU核心)
rf = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    min_samples_split=5,
    n_jobs=-1,
    random_state=42
)
rf.fit(X_train, y_train)
print(f"随机森林准确率: {rf.score(X_test, y_test):.4f}")

# 特征重要性:每个特征对预测贡献多大
feature_importance = pd.Series(
    rf.feature_importances_,
    index=data.feature_names
).sort_values(ascending=False)

print("\n最重要的5个特征:")
print(feature_importance.head(5))

你应该看到类似这样的输出

复制代码
随机森林准确率: 0.9737

最重要的5个特征:
worst perimeter        0.1523
worst radius           0.1311
mean concave points    0.1124
worst concave points   0.0987
mean perimeter         0.0876

准确率97.4%,比单棵决策树更高。特征重要性帮你理解:哪些特征对预测最有用,这对特征选择和业务理解都很有价值。


五、XGBoost:竞赛神器

梯度提升 vs 随机森林:两种不同的集成策略

随机森林是并行集成:所有树独立训练,没有先后关系,最后投票。优点是训练快(可以并行),缺点是每棵树都只是"凑数",没有针对性地改进。

梯度提升(Gradient Boosting)是串行集成:每棵树都专门针对前面所有树的错误来训练。类比:

复制代码
第1棵树:做了一份预测,有些地方对有些地方错
第2棵树:专门学第1棵树预测错的那些案例
第3棵树:学前两棵树都预测错的案例
...
最终:把所有树的预测加起来(加权求和)

这种"查漏补缺"的方式,理论上能用更少的树达到更好的效果。**残差(Residual)**是梯度提升的核心概念:当前模型的预测值和真实值之间的差距,每棵新树学的就是"上一轮的残差"。

XGBoost(极端梯度提升,eXtreme Gradient Boosting)是这类算法的最佳实现之一,Kaggle竞赛中最常用的算法。

python 复制代码
# pip install xgboost
import xgboost as xgb

data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

xgb_model = xgb.XGBClassifier(
    n_estimators=100,
    max_depth=6,
    learning_rate=0.1,       # 每棵树的贡献权重,越小越保守
    subsample=0.8,            # 每棵树随机使用80%的样本
    colsample_bytree=0.8,     # 每棵树随机使用80%的特征
    random_state=42,
    eval_metric='logloss'
)
xgb_model.fit(X_train, y_train)
print(f"XGBoost准确率: {xgb_model.score(X_test, y_test):.4f}")

随机森林 vs XGBoost:如何选择

对比 随机森林 XGBoost
训练方式 并行(树之间独立) 串行(每棵树修正前一棵的错误)
训练速度 较慢,但有优化
过拟合风险 不容易过拟合 需要调参控制
最终效果 很好 通常更好
调参难度 较简单 较复杂
选择建议 快速验证可行性 追求最佳效果

实践中,先用随机森林快速验证方案可行性,再用XGBoost追求最高精度


六、实战:信用卡欺诈检测

这个场景完美展示了随机森林的优势:数据不平衡(欺诈比例极低)、特征之间有交互关系、需要一定的可解释性。

这段代码在做什么:生成模拟的信用卡交易数据(99%正常,1%欺诈),用随机森林训练,观察它在极度不平衡数据下的表现。

python 复制代码
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split

np.random.seed(42)
n_samples = 10000
n_fraud = 100  # 1%欺诈率(类别不平衡问题)

# 正常交易:5个特征,来自标准正态分布
normal = np.random.randn(n_samples - n_fraud, 5)
normal_labels = np.zeros(n_samples - n_fraud)

# 欺诈交易:特征分布有明显偏移(欺诈行为模式不同)
fraud = np.random.randn(n_fraud, 5) + [2, -2, 3, -1, 2]
fraud_labels = np.ones(n_fraud)

X = np.vstack([normal, fraud])
y = np.concatenate([normal_labels, fraud_labels])

# 随机打乱顺序
idx = np.random.permutation(len(X))
X, y = X[idx], y[idx]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42,
    stratify=y  # 保证训练/测试集中欺诈比例一致
)

# class_weight='balanced':自动给少数类(欺诈)更高权重
# 如果不设置,模型可能会完全忽略欺诈,只预测"正常",准确率99%但没意义
rf = RandomForestClassifier(
    n_estimators=100,
    class_weight='balanced',
    random_state=42
)
rf.fit(X_train, y_train)

y_pred = rf.predict(X_test)
print(classification_report(y_test, y_pred, target_names=['正常', '欺诈']))

你应该看到 欺诈检测的召回率明显高于不加class_weight的版本,说明平衡类权重对不平衡数据集很重要。


小结

模型 核心思想 适用场景 关键参数
决策树 if-else规则树 需要可解释性 max_depth, min_samples_split
随机森林 多棵树投票 通用,稳定,快速 n_estimators, max_depth
XGBoost 串行补短板 追求最佳效果 n_estimators, learning_rate, max_depth

决策树解决了线性模型"只能画直线边界"的问题。随机森林通过"多树投票"解决了决策树容易过拟合的问题。XGBoost通过"串行补短板"在大多数任务上进一步提升了效果。

这三个算法(以及XGBoost的变体LightGBM)至今仍然是表格数据任务的首选,在很多实际业务中的效果甚至超过神经网络。

下一篇,我们学模型评估------怎么知道模型是否真的好?这是一个被很多初学者忽视但极其重要的话题。

相关推荐
菜菜的顾清寒1 小时前
力扣HOT100(51) 动态规划-单词拆分
算法·leetcode·动态规划
风筝在晴天搁浅1 小时前
剑指Offer LCR 143.子结构判断
算法
咖啡八杯1 小时前
GoF设计模式——装饰模式
java·算法·设计模式·装饰器模式
装不满的克莱因瓶1 小时前
实现矩阵的点积:从数学原理到 NumPy 实战
人工智能·线性代数·算法·机器学习·矩阵·numpy
HZ·湘怡1 小时前
树 的定义 与 性质
算法·
梦想的颜色1 小时前
Docker 入门指南:从零开始掌握容器化技术
运维·服务器·vscode·python·算法·docker·云原生
cpp_25011 小时前
P10109 [GESP202312 六级] 工作沟通
数据结构·c++·算法·题解·洛谷·gesp六级
吴可可1232 小时前
CAD二次开发中多段线定点分割技巧
算法
ʚ希希ɞ ྀ2 小时前
全排列 --- 回溯
算法·leetcode·深度优先