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

1次阅读
没有评论

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

image.webp

初学者的决策树困境

刚开始接触决策树时,我常被各种教程弄得晕头转向:要么是满屏数学公式却不解释实际用途,要么给个 sklearn 调包示例了事。最头疼的是,当我想自己实现时,发现连最基本的节点分裂逻辑都写不出来——这就像给了菜谱却不教怎么开火。

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

决策树家族三兄弟

决策树算法主要有三大流派:

  • ID3:用信息增益选特征,只能处理离散值,容易过拟合
  • C4.5:改进版用信息增益率,支持连续值,但树结构复杂
  • CART(本期主角):采用二叉树结构,既可用基尼系数也能用平方误差,既能分类也能回归

CART 的优势在于它的二叉树特性:每个节点只分左右两支,这种结构在代码实现时递归逻辑更简单,计算效率也更高。

手撕 CART 核心原理

1. 基尼系数:数据不纯度测量仪

基尼系数公式看似吓人,其实理解后很简单:

Gini = 1 - Σ(p_i)^2  # p_i 是第 i 类样本占比

举个例子,一个袋子有 3 红球 7 蓝球:

Gini = 1 - (0.3² + 0.7²) = 0.42

计算分裂后的加权基尼系数:

Gini_split = (N_left/N_total)*Gini_left + (N_right/N_total)*Gini_right

2. 特征选择:寻找最佳拆包姿势

对于每个特征,我们需要:

  1. 排序特征值
  2. 计算相邻值的中间点作为候选分割阈值
  3. 评估每个阈值划分后的基尼系数
  4. 选择使基尼系数下降最多的划分方案

3. 停止条件:什么时候该收手

  • 节点样本数小于预设 min_samples
  • 基尼系数下降小于阈值
  • 达到最大树深度

Python 实战:从零搭建 CART 树

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

class CART:
    def __init__(self, max_depth=5, min_samples_split=2):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split

    def _gini(self, y):
        # 计算基尼系数
        classes = np.unique(y)
        return 1 - sum([(np.sum(y == c) / len(y))**2 for c in classes])

    def _best_split(self, X, y):
        # 寻找最优分裂特征和阈值
        best_gini = float('inf')
        best_feature, best_thresh = None, None

        for feature_idx in range(X.shape[1]):
            thresholds = np.unique(X[:, feature_idx])
            for threshold in thresholds:
                left_idx = X[:, feature_idx] <= threshold
                gini = self._weighted_gini(y[left_idx], y[~left_idx])
                if gini < best_gini:
                    best_gini = gini
                    best_feature = feature_idx
                    best_thresh = threshold
        return best_feature, best_thresh

    def fit(self, X, y):
        self.n_classes = len(np.unique(y))
        self.tree = self._grow_tree(X, y)

    def predict(self, X):
        return np.array([self._traverse_tree(x, self.tree) for x in X])

六大避坑指南

  1. 连续值处理:
  2. 排序后取相邻值中点作为候选阈值
  3. 边界情况要单独处理

  4. 类别不平衡:

  5. 在基尼系数计算中加入类别权重
  6. 使用 class_weight 参数

  7. 剪枝策略:

  8. 预剪枝:控制 max_depth/min_samples_split
  9. 后剪枝:用验证集评估剪枝收益

  10. 缺失值处理:

  11. 常见方法:分配到使基尼系数下降更多的分支

  12. 特征重要性:

  13. 统计特征在分裂时带来的基尼下降总量
  14. 归一化后得到重要性评分

  15. 计算效率:

  16. 提前对特征值排序可加速分裂点查找
  17. 对于大数据集可考虑特征采样

性能优化方向

  • 时间复杂度:O(n_features * n_samples * log(n_samples))
  • 空间复杂度:O(2^max_depth)
  • 实用技巧:
  • 对连续特征只考虑百分位点作为候选阈值
  • 并行化特征搜索过程

下一步挑战

试着给决策树添加这些高级功能:

  1. 可视化决策路径(可以用 graphviz 库)
  2. 实现回归树模式(改用平方误差)
  3. 尝试多特征组合分裂

完整代码已放在 GitHub 仓库(虚构地址),包含更多注释和测试案例。其实决策树就像乐高积木,理解基础原理后,各种改进都是在这个框架上叠加新模块。

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