从零构建CART决策树:原理详解与Python实战指南

1次阅读
没有评论

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

image.webp

为什么需要理解决策树?

在机器学习项目中,特征工程直接影响模型效果。决策树作为可解释性极强的算法,能自动完成特征选择,输出清晰的判断规则。但许多教程用信息增益(ID3 算法)讲解分裂过程,而 scikit-learn 等工具实际默认使用基尼系数(CART 算法),这种差异常让初学者困惑。

从零构建 CART 决策树:原理详解与 Python 实战指南

核心数学原理拆解

  1. 基尼系数 vs 信息增益
  2. 基尼系数计算节点不纯度:$Gini(p) = 1 – \sum_{k=1}^K p_k^2$
  3. 相比信息增益,基尼系数计算更快(无对数运算),且对类别不平衡更敏感

  4. CART 与 ID3/C4.5 的关键区别

  5. CART 是二叉树结构,每次分裂产生两个子节点
  6. 支持连续特征和回归任务(ID3 仅处理离散分类)
  7. 无信息增益率计算(C4.5 的特性)

手把手代码实现

1. 基尼不纯度计算

def gini_impurity(y):
    """计算节点的基尼不纯度"""
    _, counts = np.unique(y, return_counts=True)
    probabilities = counts / len(y)
    return 1 - np.sum(probabilities ** 2)

2. 寻找最佳分裂点

def find_best_split(X, y):
    best_gini = float('inf')
    best_feature, best_threshold = None, None

    for feature_idx in range(X.shape[1]):  # 遍历所有特征
        thresholds = np.unique(X[:, feature_idx])  # 所有可能的分裂值
        for threshold in thresholds:
            left_mask = X[:, feature_idx] <= threshold
            gini = (len(y[left_mask]) * gini_impurity(y[left_mask]) + 
                    len(y[~left_mask]) * gini_impurity(y[~left_mask])) / len(y)
            if gini < best_gini:
                best_gini = gini
                best_feature, best_threshold = feature_idx, threshold
    return best_feature, best_threshold

3. 递归建树(关键终止条件)

class Node:
    def __init__(self, feature=None, threshold=None, left=None, right=None, value=None):
        self.feature = feature  # 分裂特征索引
        self.threshold = threshold  # 分裂阈值
        self.left = left  # 左子树
        self.right = right  # 右子树
        self.value = value  # 叶节点预测值

def build_tree(X, y, max_depth=5, min_samples_split=2):
    # 终止条件 1:样本数过少
    if len(y) < min_samples_split:
        return Node(value=np.argmax(np.bincount(y)))

    # 终止条件 2:所有样本同类
    if len(np.unique(y)) == 1:
        return Node(value=y[0])

    # 终止条件 3:达到最大深度
    if max_depth <= 0:
        return Node(value=np.argmax(np.bincount(y)))

    feature, threshold = find_best_split(X, y)
    left_mask = X[:, feature] <= threshold

    # 递归构建子树
    left = build_tree(X[left_mask], y[left_mask], max_depth-1, min_samples_split)
    right = build_tree(X[~left_mask], y[~left_mask], max_depth-1, min_samples_split)

    return Node(feature, threshold, left, right)

工程优化技巧

  1. 预剪枝策略
  2. 限制最大深度(max_depth)防止过拟合
  3. 设置最小分裂样本数(min_samples_split)

  4. 后剪枝方法

  5. 代价复杂度剪枝(CCP)
  6. 验证集准确率判断子树是否保留

新手常见三大坑

  1. 连续特征处理
  2. 错误做法:直接按类别特征处理
  3. 正确方案:排序后尝试所有可能的分割点

  4. 类别不平衡问题

  5. 错误现象:模型总是预测多数类
  6. 解决方案:使用 class_weight 参数或过采样

  7. 缺失值处理

  8. 错误做法:直接删除含缺失值的样本
  9. 推荐方案:分裂时增加缺失值分支方向

验证实现正确性

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris

# 加载数据
X, y = load_iris(return_X_y=True)

# 自实现模型
my_tree = build_tree(X, y, max_depth=3)

# sklearn 对比
sk_tree = DecisionTreeClassifier(criterion='gini', max_depth=3)
sk_tree.fit(X, y)

# 打印前 5 个样本预测结果
print("自实现结果:", [predict(my_tree, x) for x in X[:5]])
print("sklearn 结果:", sk_tree.predict(X[:5]))

思考题延伸

当前算法全程使用 CPU 计算,当特征维度很高时,暴力搜索会成为性能瓶颈。如何改进?

  1. 将特征排序等操作移植到 GPU(使用 CuPy 替代 NumPy)
  2. 对连续特征采用分桶策略减少候选分割点
  3. 实现并行化特征搜索(多进程 / 多线程)

通过这个实战项目,你会发现很多机器学习库的设计细节其实源于这类基础算法的优化。建议尝试实现剪枝功能,这会让你更深入理解模型泛化能力的本质。

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