Python实战:从零实现C4.5决策树算法及避坑指南

1次阅读
没有评论

共计 4715 个字符,预计需要花费 12 分钟才能阅读完成。

image.webp

为什么选择 C4.5 算法?

决策树是机器学习中最直观的算法之一,它将数据通过一系列规则划分成不同的类别。相比于 ID3 算法,C4.5 有两大改进:

Python 实战:从零实现 C4.5 决策树算法及避坑指南

  • 使用信息增益比(而非单纯的信息增益)选择划分属性,解决了 ID3 对取值数目多的属性偏好问题
  • 增加了对连续属性的处理能力,使算法能直接处理现实数据中常见的数值型特征

举个实际例子:在预测贷款风险时,既有『年龄』这样的连续值,也有『职业』这种离散值。C4.5 可以同时处理这两种特征,而 ID3 只能处理离散值。

核心实现四步走

1. 信息增益比计算

信息增益比 = 信息增益 / 分裂信息熵,其计算分为三步:

  1. 计算数据集的经验熵 H(D)
  2. 计算按某特征 A 划分后的条件熵 H(D|A)
  3. 计算特征 A 的分裂信息熵(即特征 A 的固有值)

Python 实现关键代码:

def calc_entropy(y):
    """计算信息熵"""
    counter = Counter(y)
    total = len(y)
    return -sum((count/total)*math.log2(count/total) for count in counter.values())

def calc_gain_ratio(X, y, feature_idx):
    """计算信息增益比"""
    # 计算原始熵
    base_entropy = calc_entropy(y)
    # 计算按特征划分后的条件熵
    feature_values = X[:, feature_idx]
    unique_values = set(feature_values)
    new_entropy = 0.0
    split_info = 0.0

    for value in unique_values:
        sub_y = y[feature_values == value]
        prob = len(sub_y) / len(y)
        new_entropy += prob * calc_entropy(sub_y)
        split_info -= prob * math.log2(prob)

    # 避免除零错误
    if split_info == 0:
        return 0
    return (base_entropy - new_entropy) / split_info

2. 连续属性离散化

C4.5 采用二分法处理连续属性:

  1. 将该特征所有取值排序
  2. 计算每两个相邻值的中间点作为候选划分点
  3. 选择使信息增益比最大的划分点

实现技巧:

def handle_continuous_feature(X, y, feature_idx):
    """连续特征离散化处理"""
    feature_values = sorted(set(X[:, feature_idx]))
    best_gain_ratio = 0.0
    best_split_point = None

    for i in range(1, len(feature_values)):
        split_point = (feature_values[i-1] + feature_values[i]) / 2
        # 临时将连续值转为离散值(大于 / 小于分割点)temp_feature = (X[:, feature_idx] > split_point).astype(int)
        current_ratio = calc_gain_ratio(temp_feature.reshape(-1,1), y, 0)

        if current_ratio > best_gain_ratio:
            best_gain_ratio = current_ratio
            best_split_point = split_point

    return best_split_point

3. 递归构建决策树

终止条件需要处理四种情况:

  1. 当前节点所有样本属于同一类别
  2. 没有剩余特征可供划分
  3. 当前节点样本集为空(通过父节点投票决定类别)
  4. 信息增益比小于阈值(预剪枝)

核心递归逻辑:

def build_tree(X, y, used_features=set(), max_depth=5):
    # 终止条件 1:所有样本同类别
    if len(set(y)) == 1:
        return {'class': y[0]}

    # 终止条件 2:无特征可用或达到最大深度
    if len(used_features) == X.shape[1] or max_depth == 0:
        return {'class': Counter(y).most_common(1)[0][0]}

    # 选择最佳划分特征
    best_gain = -1
    best_feature = None
    split_point = None

    for feature_idx in range(X.shape[1]):
        if feature_idx in used_features:
            continue

        if is_continuous(feature_idx):  # 判断是否连续特征
            point = handle_continuous_feature(X, y, feature_idx)
            temp_feature = (X[:, feature_idx] > point).astype(int)
            gain = calc_gain_ratio(temp_feature.reshape(-1,1), y, 0)

            if gain > best_gain:
                best_gain = gain
                best_feature = feature_idx
                split_point = point
        else:
            gain = calc_gain_ratio(X, y, feature_idx)
            if gain > best_gain:
                best_gain = gain
                best_feature = feature_idx

    # 终止条件 4:信息增益比过小
    if best_gain < 0.01:
        return {'class': Counter(y).most_common(1)[0][0]}

    # 构建子树
    tree = {'feature': best_feature, 'children': {}}

    if split_point is not None:  # 连续特征处理
        left_idx = X[:, best_feature] <= split_point
        right_idx = X[:, best_feature] > split_point

        tree['split_point'] = split_point
        tree['children']['left'] = build_tree(X[left_idx], y[left_idx], 
            used_features | {best_feature}, max_depth-1)
        tree['children']['right'] = build_tree(X[right_idx], y[right_idx],
            used_features | {best_feature}, max_depth-1)
    else:  # 离散特征处理
        feature_values = set(X[:, best_feature])
        for value in feature_values:
            sub_idx = X[:, best_feature] == value
            tree['children'][value] = build_tree(X[sub_idx], y[sub_idx],
                used_features | {best_feature}, max_depth-1)

    return tree

4. 预测函数实现

预测时需要根据节点类型(离散 / 连续)走不同分支:

def predict(tree, x):
    if 'class' in tree:
        return tree['class']

    feature_value = x[tree['feature']]

    if 'split_point' in tree:  # 连续特征
        if feature_value <= tree['split_point']:
            return predict(tree['children']['left'], x)
        else:
            return predict(tree['children']['right'], x)
    else:  # 离散特征
        if feature_value in tree['children']:
            return predict(tree['children'][feature_value], x)
        else:  # 处理未见过的特征值
            return get_leaf_class(tree)

def get_leaf_class(node):
    """获取子树中最常见的类别"""
    if 'class' in node:
        return node['class']

    classes = []
    for child in node['children'].values():
        classes.append(get_leaf_class(child))

    return Counter(classes).most_common(1)[0][0]

生产环境注意事项

缺失值处理三策略

  1. 直接删除 :当缺失样本占比较小时适用
  2. 填充众数 / 均值 :对离散特征用众数,连续特征用均值
  3. 概率分布法 :按特征取值比例分配样本到不同分支

改进后的信息增益比计算:

def calc_gain_ratio_with_missing(X, y, feature_idx):
    # 找出非缺失值索引
    non_missing = ~np.isnan(X[:, feature_idx])
    X_clean = X[non_missing]
    y_clean = y[non_missing]

    if len(X_clean) == 0:
        return 0

    # 计算非缺失部分的信息增益比
    gain_ratio = calc_gain_ratio(X_clean, y_clean, feature_idx)
    # 按有效样本比例加权
    return gain_ratio * (len(X_clean) / len(X))

剪枝优化方案

预剪枝 (更常用):
– 设置最大树深度
– 设置叶子节点最小样本数
– 设置信息增益比阈值

后剪枝 (效果更好但实现复杂):
1. 先构建完整决策树
2. 自底向上考察非叶节点
3. 若剪枝后验证集准确率提升,则替换为叶节点

时间复杂度分析

假设有 m 个特征,n 个样本,d 为树深度:

  • 训练时:O(mnlog(n)*d)
  • 预测时:O(d)

实际优化建议:
– 对连续特征先排序(O(nlogn))可提升性能
– 对大规模数据可采用随机森林等集成方法

进阶思考方向

C4.5 的局限性

  1. 多变量决策 :每次只按一个特征划分,可能错过特征组合带来的信息
  2. 数值敏感 :对数值型特征的微小变化可能导致完全不同的树结构
  3. 数据倾斜 :对类别不均衡数据需要额外处理

扩展为多变量决策树

改进思路:

  1. 在节点划分时考虑特征的线性组合
  2. 使用 LDA(线性判别分析)找到最佳投影方向
  3. 评估划分质量时改用其他度量标准(如 Gini 系数)

实现框架示例:

class MultivariateNode:
    def __init__(self, weights, threshold):
        self.weights = weights  # 特征权重向量
        self.threshold = threshold

    def split(self, X):
        projection = X @ self.weights  # 矩阵乘法
        return projection <= self.threshold

总结

本文完整实现了 C4.5 决策树的七个核心组件:
1. 信息熵计算
2. 信息增益比评估
3. 连续特征离散化
4. 缺失值处理
5. 递归建树逻辑
6. 预测函数
7. 剪枝策略

建议读者在 UCI 数据集(如 iris 或 adult)上测试代码效果。当遇到准确率不高的情况时,可以尝试:

  • 增加 max_depth 参数
  • 调整预剪枝阈值
  • 对连续特征做标准化处理

完整代码已上传 GitHub(伪代码),包含更多工程优化细节如并行计算特征增益、树的可视化等功能。在实际业务中,建议优先使用 sklearn 的 DecisionTreeClassifier(基于 CART 算法),但理解 C4.5 的实现原理对调试模型非常重要。

正文完
 0
评论(没有评论)