从零实现CART决策树算法:原理详解与Python实战

1次阅读
没有评论

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

image.webp

1. 算法背景

决策树是机器学习中最基础且直观的算法之一,而 CART(Classification and Regression Trees)算法则是其中最经典的实现方式。它不仅能处理分类问题(通过基尼系数),还能解决回归问题(通过方差最小化)。与 ID3、C4.5 等算法相比,CART 的特点是:

从零实现 CART 决策树算法:原理详解与 Python 实战

  • 严格生成二叉树结构
  • 同时支持分类和回归任务
  • 默认使用基尼不纯度作为分类标准

2. 核心原理

2.1 基尼系数计算

基尼系数衡量数据的不纯度,值越小表示纯度越高。计算公式为:

Gini = 1 - Σ(p_i)^2

其中 p_i 是第 i 类样本在节点中的比例。计算示例:

  • 假设一个节点有 10 个样本,类别 A 有 6 个,类别 B 有 4 个
  • Gini = 1 – (6/10)^2 – (4/10)^2 = 0.48

2.2 特征选择

CART 采用贪婪算法选择最佳分割特征和分割点:

  1. 遍历所有特征
  2. 对每个特征,遍历所有可能的分割点
  3. 计算分割后的加权基尼系数
  4. 选择使基尼系数下降最多的特征和分割点

2.3 节点分裂

分裂过程递归进行,直到满足停止条件:

  1. 当前节点样本数小于阈值
  2. 基尼系数低于阈值
  3. 达到最大树深度

3. Python 实现

以下是完整的 CART 决策树实现:

import numpy as np

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

class CARTDecisionTree:
    def __init__(self, max_depth=None, min_samples_split=2):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.root = None

    def _gini(self, y):
        classes = np.unique(y)
        gini = 1.0
        for cls in classes:
            p = np.sum(y == cls) / len(y)
            gini -= p**2
        return gini

    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 thresh in thresholds:
                left_indices = X[:, feature_idx] <= thresh
                right_indices = ~left_indices

                if len(y[left_indices]) == 0 or len(y[right_indices]) == 0:
                    continue

                gini_left = self._gini(y[left_indices])
                gini_right = self._gini(y[right_indices])
                weighted_gini = (len(y[left_indices]) * gini_left + 
                                len(y[right_indices]) * gini_right) / len(y)

                if weighted_gini < best_gini:
                    best_gini = weighted_gini
                    best_feature = feature_idx
                    best_thresh = thresh

        return best_feature, best_thresh

    def _build_tree(self, X, y, depth=0):
        if (self.max_depth is not None and depth >= self.max_depth) or \
           len(y) < self.min_samples_split or len(np.unique(y)) == 1:
            return TreeNode(value=np.argmax(np.bincount(y)))

        feature_idx, threshold = self._best_split(X, y)
        if feature_idx is None:
            return TreeNode(value=np.argmax(np.bincount(y)))

        left_indices = X[:, feature_idx] <= threshold
        right_indices = ~left_indices

        left = self._build_tree(X[left_indices], y[left_indices], depth+1)
        right = self._build_tree(X[right_indices], y[right_indices], depth+1)

        return TreeNode(feature_idx, threshold, left=left, right=right)

    def fit(self, X, y):
        self.root = self._build_tree(X, y)

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

    def _predict_tree(self, x, node):
        if node.value is not None:
            return node.value

        if x[node.feature_idx] <= node.threshold:
            return self._predict_tree(x, node.left)
        else:
            return self._predict_tree(x, node.right)

4. 优化技巧

4.1 预剪枝

在树构建过程中提前停止生长:

  • 限制最大深度
  • 设置最小样本分裂数
  • 设置信息增益阈值

4.2 后剪枝

先构建完整树,再自底向上剪枝:

  1. 计算剪枝前后的验证集准确率
  2. 如果准确率不下降则剪枝
  3. 重复直到无法继续剪枝

5. 避坑指南

  1. 过拟合问题 :
  2. 解决方案:合理设置 max_depth 和 min_samples_split

  3. 类别不平衡 :

  4. 解决方案:使用 class_weight 参数或重采样

  5. 连续值处理 :

  6. 解决方案:离散化或直接使用原始值

  7. 计算效率低 :

  8. 解决方案:对特征值排序后计算分割点

6. 实战建议

  1. 小型数据集 :
  2. 可尝试不限制树深度
  3. 重点关注可解释性

  4. 大型数据集 :

  5. 建议使用预剪枝
  6. 考虑使用并行化实现

  7. 高维数据 :

  8. 先进行特征选择
  9. 考虑集成学习方法

思考题

  1. 如何修改算法使其支持回归任务?
  2. 当特征大部分是类别型时,应该如何优化分割策略?
  3. 如何实现决策树的可视化展示?

实现一个完整的 CART 决策树需要考虑很多细节,但核心思想并不复杂。希望这篇文章能帮助你理解算法原理并实现自己的决策树模型。在实际项目中,建议先使用 scikit-learn 中的 DecisionTreeClassifier 验证思路,再尝试自己的实现。

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