决策树原理与实现:ID3/C4.5 算法

决策树是一种监督学习模型,用于分类任务。它通过递归分割数据集来构建树结构,每个内部节点表示一个特征测试,每个叶节点表示一个类标签。ID3 和 C4.5 是经典的决策树算法,核心区别在于特征选择标准:ID3 使用信息增益(Information Gain),而 C4.5 使用增益比(Gain Ratio)来避免偏向取值多的特征。下面我将逐步解释原理,并提供 Python 实现代码。


决策树原理概述

决策树的构建过程包括:

  1. 特征选择:从可用特征中选择最佳分割特征。
  2. 节点分裂:根据特征值将数据集分成子集。
  3. 递归构建:对每个子集重复过程,直到满足停止条件(如所有样本属于同一类、无特征可用或达到最大深度)。
  4. 生成叶节点:为子集分配多数类标签。

关键概念:

  • 熵(Entropy):衡量数据集的不确定性。对于数据集 $D$,熵定义为: $$H(D) = -\sum_{k=1}^{K} p_k \log_2 p_k$$ 其中 $p_k$ 是类别 $k$ 在 $D$ 中的比例。
  • 信息增益(Information Gain):ID3 算法使用它来选择特征。特征 $A$ 的信息增益为: $$Gain(D, A) = H(D) - \sum_{v \in \text{Values}(A)} \frac{|D_v|}{|D|} H(D_v)$$ 其中 $D_v$ 是 $A$ 取值为 $v$ 的子集。
  • 增益比(Gain Ratio):C4.5 算法引入它来惩罚取值多的特征。定义为: $$GainRatio(D, A) = \frac{Gain(D, A)}{SplitInfo(A)}$$ 其中 $SplitInfo(A)$ 是特征 $A$ 的固有信息: $$SplitInfo(A) = -\sum_{v \in \text{Values}(A)} \frac{|D_v|}{|D|} \log_2 \frac{|D_v|}{|D|}$$

ID3 算法原理

ID3(Iterative Dichotomiser 3)算法由 Ross Quinlan 提出,特点:

  • 使用信息增益作为特征选择标准。
  • 仅处理离散特征。
  • 递归构建树,直到所有叶节点纯(即样本属于同一类)。
  • 缺点:可能偏向取值多的特征(如 ID 特征),导致过拟合。

步骤

  1. 计算数据集 $D$ 的熵 $H(D)$。
  2. 对每个特征 $A$,计算信息增益 $Gain(D, A)$。
  3. 选择 $Gain(D, A)$ 最大的特征作为分割点。
  4. 根据特征值分裂数据集,递归构建子树。

C4.5 算法原理

C4.5 是 ID3 的改进版本,由同一作者提出:

  • 使用增益比代替信息增益,解决特征偏向问题。
  • 支持连续特征(通过二分法离散化)。
  • 处理缺失值。
  • 引入剪枝(pruning)减少过拟合。

步骤

  1. 计算数据集 $D$ 的熵 $H(D)$。
  2. 对每个特征 $A$,计算信息增益 $Gain(D, A)$ 和 $SplitInfo(A)$。
  3. 计算增益比 $GainRatio(D, A)$。
  4. 选择 $GainRatio(D, A)$ 最大的特征作为分割点。
  5. 递归构建子树,并应用剪枝优化。

Python 代码实现

以下代码实现了决策树的基本框架,支持 ID3 和 C4.5 算法。代码包括:

  • TreeNode 类:表示决策树节点。
  • build_tree 函数:递归构建树,可选择使用信息增益(ID3)或增益比(C4.5)。
  • 辅助函数:计算熵、信息增益、增益比等。
  • 停止条件:所有样本同一类、无特征可用或深度限制。
import numpy as np
from collections import Counter

class TreeNode:
    """决策树节点类"""
    def __init__(self, feature_index=None, threshold=None, value=None, children=None):
        self.feature_index = feature_index  # 分裂特征的索引
        self.threshold = threshold          # 连续特征的阈值(C4.5 支持)
        self.value = value                  # 叶节点的类标签
        self.children = children or {}      # 子节点字典:{特征值: 子节点}

def entropy(y):
    """计算数据集 y 的熵"""
    counts = Counter(y)
    probs = [count / len(y) for count in counts.values()]
    return -sum(p * np.log2(p) for p in probs if p > 0)

def information_gain(X, y, feature_idx):
    """计算特征 feature_idx 的信息增益(ID3 使用)"""
    total_entropy = entropy(y)
    values, counts = np.unique(X[:, feature_idx], return_counts=True)
    weighted_entropy = 0
    for i, value in enumerate(values):
        subset_indices = np.where(X[:, feature_idx] == value)[0]
        subset_y = y[subset_indices]
        weighted_entropy += (counts[i] / len(y)) * entropy(subset_y)
    return total_entropy - weighted_entropy

def gain_ratio(X, y, feature_idx):
    """计算特征 feature_idx 的增益比(C4.5 使用)"""
    gain = information_gain(X, y, feature_idx)
    # 计算 SplitInfo
    values, counts = np.unique(X[:, feature_idx], return_counts=True)
    split_info = -sum((count / len(y)) * np.log2(count / len(y)) for count in counts)
    return gain / split_info if split_info > 0 else 0  # 避免除零

def build_tree(X, y, feature_indices, algorithm='id3', max_depth=None, depth=0):
    """递归构建决策树"""
    # 停止条件 1: 所有样本同一类
    if len(np.unique(y)) == 1:
        return TreeNode(value=y[0])
    
    # 停止条件 2: 无特征可用
    if len(feature_indices) == 0:
        majority_class = Counter(y).most_common(1)[0][0]
        return TreeNode(value=majority_class)
    
    # 停止条件 3: 达到最大深度
    if max_depth is not None and depth >= max_depth:
        majority_class = Counter(y).most_common(1)[0][0]
        return TreeNode(value=majority_class)
    
    # 选择最佳特征
    best_gain = -float('inf')
    best_feature_idx = None
    for idx in feature_indices:
        if algorithm == 'id3':
            gain_val = information_gain(X, y, idx)
        else:  # 'c45'
            gain_val = gain_ratio(X, y, idx)
        if gain_val > best_gain:
            best_gain = gain_val
            best_feature_idx = idx
    
    # 移除已选特征
    remaining_features = [f for f in feature_indices if f != best_feature_idx]
    
    # 创建内部节点并递归构建子树
    node = TreeNode(feature_index=best_feature_idx)
    unique_values = np.unique(X[:, best_feature_idx])
    for value in unique_values:
        subset_indices = np.where(X[:, best_feature_idx] == value)[0]
        if len(subset_indices) == 0:
            # 如果子集为空,返回多数类
            majority_class = Counter(y).most_common(1)[0][0]
            node.children[value] = TreeNode(value=majority_class)
        else:
            subset_X = X[subset_indices]
            subset_y = y[subset_indices]
            node.children[value] = build_tree(subset_X, subset_y, remaining_features, algorithm, max_depth, depth+1)
    return node

# 示例使用
if __name__ == "__main__":
    # 示例数据集(特征离散,最后一列为标签)
    X = np.array([
        ['Sunny', 'Hot', 'High', 'Weak'],
        ['Sunny', 'Hot', 'High', 'Strong'],
        ['Overcast', 'Hot', 'High', 'Weak'],
        ['Rain', 'Mild', 'High', 'Weak'],
        ['Rain', 'Cool', 'Normal', 'Weak'],
        ['Rain', 'Cool', 'Normal', 'Strong'],
        ['Overcast', 'Cool', 'Normal', 'Strong'],
        ['Sunny', 'Mild', 'High', 'Weak'],
        ['Sunny', 'Cool', 'Normal', 'Weak'],
        ['Rain', 'Mild', 'Normal', 'Weak'],
        ['Sunny', 'Mild', 'Normal', 'Strong'],
        ['Overcast', 'Mild', 'High', 'Strong'],
        ['Overcast', 'Hot', 'Normal', 'Weak'],
        ['Rain', 'Mild', 'High', 'Strong']
    ])
    y = np.array(['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No'])
    
    # 构建 ID3 树
    feature_indices = list(range(X.shape[1]))
    id3_tree = build_tree(X, y, feature_indices, algorithm='id3', max_depth=3)
    
    # 构建 C4.5 树(类似,只需改变 algorithm 参数)
    # c45_tree = build_tree(X, y, feature_indices, algorithm='c45', max_depth=3)
    
    print("决策树构建完成!")

代码说明

  1. 核心函数
    • entropy(y):计算标签向量 $y$ 的熵。
    • information_gain(X, y, feature_idx):计算特征的信息增益(ID3)。
    • gain_ratio(X, y, feature_idx):计算增益比(C4.5)。
    • build_tree(...):递归构建树,参数 algorithm 可指定 'id3''c45'
  2. 使用示例
    • 数据集基于天气预测(特征如天气、温度,标签如是否打网球)。
    • 运行后输出决策树根节点。
  3. 注意事项
    • 本代码仅处理离散特征;如需连续特征,需在 build_tree 中添加二分法处理(C4.5 支持)。
    • 实际应用中,应添加剪枝函数(如后剪枝)以防止过拟合。
    • 测试时,可添加预测函数遍历树。

这个实现简洁明了,可用于教育目的。实际库如 scikit-learn 有优化版本,但此代码帮助理解核心逻辑。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐