决策树原理与实现:ID3/C4.5 算法的 Python 代码
·
决策树原理与实现:ID3/C4.5 算法
决策树是一种监督学习模型,用于分类任务。它通过递归分割数据集来构建树结构,每个内部节点表示一个特征测试,每个叶节点表示一个类标签。ID3 和 C4.5 是经典的决策树算法,核心区别在于特征选择标准:ID3 使用信息增益(Information Gain),而 C4.5 使用增益比(Gain Ratio)来避免偏向取值多的特征。下面我将逐步解释原理,并提供 Python 实现代码。
决策树原理概述
决策树的构建过程包括:
- 特征选择:从可用特征中选择最佳分割特征。
- 节点分裂:根据特征值将数据集分成子集。
- 递归构建:对每个子集重复过程,直到满足停止条件(如所有样本属于同一类、无特征可用或达到最大深度)。
- 生成叶节点:为子集分配多数类标签。
关键概念:
- 熵(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 特征),导致过拟合。
步骤:
- 计算数据集 $D$ 的熵 $H(D)$。
- 对每个特征 $A$,计算信息增益 $Gain(D, A)$。
- 选择 $Gain(D, A)$ 最大的特征作为分割点。
- 根据特征值分裂数据集,递归构建子树。
C4.5 算法原理
C4.5 是 ID3 的改进版本,由同一作者提出:
- 使用增益比代替信息增益,解决特征偏向问题。
- 支持连续特征(通过二分法离散化)。
- 处理缺失值。
- 引入剪枝(pruning)减少过拟合。
步骤:
- 计算数据集 $D$ 的熵 $H(D)$。
- 对每个特征 $A$,计算信息增益 $Gain(D, A)$ 和 $SplitInfo(A)$。
- 计算增益比 $GainRatio(D, A)$。
- 选择 $GainRatio(D, A)$ 最大的特征作为分割点。
- 递归构建子树,并应用剪枝优化。
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("决策树构建完成!")
代码说明
- 核心函数:
entropy(y):计算标签向量 $y$ 的熵。information_gain(X, y, feature_idx):计算特征的信息增益(ID3)。gain_ratio(X, y, feature_idx):计算增益比(C4.5)。build_tree(...):递归构建树,参数algorithm可指定'id3'或'c45'。
- 使用示例:
- 数据集基于天气预测(特征如天气、温度,标签如是否打网球)。
- 运行后输出决策树根节点。
- 注意事项:
- 本代码仅处理离散特征;如需连续特征,需在
build_tree中添加二分法处理(C4.5 支持)。 - 实际应用中,应添加剪枝函数(如后剪枝)以防止过拟合。
- 测试时,可添加预测函数遍历树。
- 本代码仅处理离散特征;如需连续特征,需在
这个实现简洁明了,可用于教育目的。实际库如 scikit-learn 有优化版本,但此代码帮助理解核心逻辑。
更多推荐


所有评论(0)