决策树(Decision Tree)是一种非常符合人类直觉的机器学习算法。它的预测过程就像做选择题:先问一个问题,根据答案走到下一步,再继续问问题,直到得到最终结论。

例如判断一个学生考试是否通过,可以问:

  1. 学习时间是否大于 3 小时?
  2. 睡眠时间是否大于 7 小时?
  3. 是否完成复习?

最后走到某个叶子节点,得到“通过”或“不通过”的预测结果。

决策树结构示意图

1. 决策树的基本思想

决策树的核心思想是:不断选择一个最合适的特征,把数据集切分得越来越“纯”。

这里的“纯”可以理解为:一个节点里的样本类别越统一,就越纯。

例如一个节点里有 10 个样本:

  • 如果 10 个都是“通过”,这个节点非常纯。
  • 如果 5 个“通过”、5 个“不通过”,这个节点就很混乱。

训练决策树时,算法会不断寻找最佳划分条件,例如:

学习时间 >= 3 小时?

如果这个问题能把“通过”和“不通过”分得更开,就说明它是一个不错的划分。

2. 决策树由哪些部分组成

一棵决策树通常包含三类节点:

结构 含义
根节点 整棵树的起点,包含全部训练数据
内部节点 一个判断条件,例如“年龄是否大于 18”
叶子节点 最终预测结果,例如“通过”或“不通过”

从根节点走到叶子节点,就是模型预测一个样本的过程。

例如:

学习时间 >= 3 小时?
    是 -> 完成复习?
        是 -> 预测:通过
        否 -> 预测:不通过
    否 -> 睡眠时间 >= 7 小时?
        是 -> 预测:通过
        否 -> 预测:不通过

这种结构的好处是非常容易解释。相比逻辑回归、支持向量机等模型,决策树更像是一套可读的判断规则。

3. 如何判断哪个特征更适合划分

决策树训练时最关键的问题是:

当前节点应该用哪个特征来划分?

常见的划分标准包括:

  • 信息增益
  • 信息增益率
  • 基尼指数
  • 均方误差

其中分类树常用信息熵、信息增益和基尼指数;回归树常用均方误差。

4. 信息熵:衡量混乱程度

信息熵(Entropy)可以用来衡量一个数据集的混乱程度。

对于分类问题,信息熵定义为:

$$
H(D) = -\sum_{k=1}^{K} p_k \log_2 p_k
$$

其中:

  • $D$ 表示当前数据集。
  • $K$ 表示类别数量。
  • $p_k$ 表示第 $k$ 类样本所占比例。

如果一个节点里所有样本都属于同一类,那么熵为 0,说明完全不混乱。

如果不同类别各占一半,熵会比较大,说明节点比较混乱。

以二分类为例:

通过 不通过
10 0 0
8 2 较小
5 5 较大

决策树希望每次划分后,子节点的熵尽可能小。

5. 信息增益:划分后混乱减少了多少

信息增益(Information Gain)表示划分前后信息熵减少了多少。

假设用特征 $A$ 对数据集 $D$ 进行划分,信息增益为:

$$
Gain(D, A) = H(D) - \sum_{v=1}^{V}\frac{|D_v|}{|D|}H(D_v)
$$

其中:

  • $H(D)$ 是划分前的熵。
  • $D_v$ 是按特征 $A$ 划分后的第 $v$ 个子集。
  • $\frac{|D_v|}{|D|}$ 表示子集占原数据集的比例。

信息增益越大,说明这个特征让数据变得越清晰。

ID3 决策树算法就是优先选择信息增益最大的特征进行划分。

6. 基尼指数:衡量随机错分概率

CART 决策树常用基尼指数(Gini Index)作为分类划分标准。

基尼指数定义为:

$$
Gini(D) = 1 - \sum_{k=1}^{K}p_k^2
$$

它可以理解为:从当前节点随机抽一个样本,再随机按类别比例猜它的类别,猜错的概率大不大。

如果节点越纯,基尼指数越小。

二分类下:

  • 如果一个节点全是正类,$Gini=0$。
  • 如果正负类各占一半,$Gini=0.5$。

使用特征 $A$ 划分后的基尼指数为:

$$
Gini(D, A) = \sum_{v=1}^{V}\frac{|D_v|}{|D|}Gini(D_v)
$$

训练时会选择让划分后基尼指数最小的特征。

7. 回归树怎么划分

决策树不仅能做分类,也能做回归。

分类树的叶子节点输出类别;回归树的叶子节点输出数值。

例如预测房价时,一个叶子节点中可能有多个训练样本:

90 万、95 万、100 万

那么这个叶子节点的预测值通常取平均值:

$$
\hat{y} = \frac{1}{m}\sum_{i=1}^{m}y^{(i)}
$$

回归树在选择划分时,通常希望划分后的平方误差最小:

$$
MSE = \frac{1}{m}\sum_{i=1}^{m}(y^{(i)}-\hat{y})^2
$$

所以分类树更关心“类别是否更纯”,回归树更关心“数值是否更接近”。

8. 决策树为什么容易过拟合

决策树很容易把训练数据记得太细。

如果不加限制,它可以一直划分,直到每个叶子节点几乎只剩一个样本。这样训练集准确率可能非常高,但对新数据的泛化能力会下降。

这就是过拟合。

常见控制方法包括:

  • 限制树的最大深度 max_depth
  • 限制叶子节点最少样本数 min_samples_leaf
  • 限制内部节点继续划分所需的最少样本数 min_samples_split
  • 剪枝,去掉贡献不大的分支。

可以把这些参数理解为“不要让树长得太复杂”。

9. Python 手写:计算基尼指数并选择划分

下面不急着手写完整决策树,而是先手写决策树最核心的一步:计算某个划分是否让数据更纯。

from collections import Counter


def gini(labels):
    # 对应模块:计算当前节点的基尼指数,值越小表示类别越纯
    total = len(labels)
    counts = Counter(labels)

    impurity = 1.0
    for count in counts.values():
        prob = count / total
        impurity -= prob ** 2

    return impurity


def split_dataset(X, y, feature_index, threshold):
    # 对应模块:按照某个特征阈值把数据切成左右两个子节点
    left_y = []
    right_y = []

    for sample, label in zip(X, y):
        if sample[feature_index] <= threshold:
            left_y.append(label)
        else:
            right_y.append(label)

    return left_y, right_y


def weighted_gini(left_y, right_y):
    # 对应模块:计算划分后的加权基尼指数
    total = len(left_y) + len(right_y)
    left_weight = len(left_y) / total
    right_weight = len(right_y) / total

    return left_weight * gini(left_y) + right_weight * gini(right_y)


X = [
    [2, 6],  # 学习 2 小时,睡眠 6 小时
    [3, 7],
    [4, 6],
    [5, 8],
    [6, 7],
]

y = ["不通过", "不通过", "通过", "通过", "通过"]

# 尝试用第 0 个特征,也就是学习时间,以 3.5 为阈值划分
left_y, right_y = split_dataset(X, y, feature_index=0, threshold=3.5)

print("左节点标签:", left_y)
print("右节点标签:", right_y)
print("划分前基尼指数:", gini(y))
print("划分后加权基尼指数:", weighted_gini(left_y, right_y))

这段代码展示了决策树选择划分的核心逻辑:

  1. 先选一个特征和阈值。
  2. 按条件把样本分成左右两组。
  3. 分别计算左右节点的纯度。
  4. 如果划分后的加权基尼指数更小,说明这个划分更好。

完整决策树就是不断重复这个过程,直到满足停止条件。

10. 使用 sklearn 训练决策树分类器

实际项目中可以直接使用 sklearn.tree.DecisionTreeClassifier

import numpy as np
from sklearn.tree import DecisionTreeClassifier, export_text

# 特征:学习时间、睡眠时间
X = np.array([
    [2, 6],
    [3, 7],
    [4, 6],
    [5, 8],
    [6, 7],
    [1, 5],
    [7, 8],
])

y = np.array(["不通过", "不通过", "通过", "通过", "通过", "不通过", "通过"])

# max_depth 限制树的深度,避免树过于复杂
model = DecisionTreeClassifier(
    criterion="gini",
    max_depth=2,
    random_state=42,
)

model.fit(X, y)

new_student = np.array([[4, 7]])
prediction = model.predict(new_student)

print("预测结果:", prediction[0])

# 打印树的规则,方便理解模型是怎么做判断的
rules = export_text(
    model,
    feature_names=["学习时间", "睡眠时间"],
)

print(rules)

可能输出类似:

|--- 学习时间 <= 3.50
|   |--- class: 不通过
|--- 学习时间 >  3.50
|   |--- class: 通过

这也是决策树的一个重要优点:模型结果相对容易解释。

11. 决策树的优缺点

优点

  • 可解释性强,预测过程像一组规则。
  • 可以处理非线性关系。
  • 对特征缩放不敏感,通常不需要标准化。
  • 既能做分类,也能做回归。
  • 可以处理数值特征和类别特征。

缺点

  • 容易过拟合。
  • 对数据扰动比较敏感,数据稍微变化,树结构可能明显改变。
  • 单棵树的泛化能力有限。
  • 贪心划分不一定能找到全局最优树。

这些缺点也解释了为什么后续会出现随机森林、梯度提升树、XGBoost、LightGBM 等集成模型。它们本质上都是在决策树基础上进行改进。

12. 实践建议

使用决策树时,可以重点关注以下几点:

  1. 先限制 max_depth,不要让树长得太深。
  2. 使用交叉验证选择 max_depthmin_samples_leaf 等参数。
  3. 如果数据噪声较多,适当增大 min_samples_leaf
  4. 如果单棵树效果不稳定,可以尝试随机森林。
  5. 如果追求更强的预测性能,可以学习梯度提升树相关算法。

13. 总结

决策树的核心思想很简单:不断提出问题,把数据切分得越来越纯。

对于分类任务,它常用信息熵、信息增益或基尼指数来判断划分好坏;对于回归任务,它通常使用均方误差来选择划分。

理解决策树时,可以抓住三句话:

  • 节点是在问问题。
  • 分支是问题的不同答案。
  • 叶子节点是最终预测结果。

决策树本身简单直观,但它也是很多强大模型的基础。理解决策树之后,再学习随机森林、GBDT、XGBoost 等算法时,会更容易抓住它们的核心思想。