Python实战:CART决策树算法原理与高效实现

1次阅读
没有评论

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

image.webp

决策树基础与 CART 算法特点

决策树是机器学习中的一种树形结构模型,通过递归地对数据进行划分来完成分类或回归任务。CART(Classification and Regression Trees)是其中最经典的算法之一,与其他决策树算法相比有显著特点:

Python 实战:CART 决策树算法原理与高效实现

  • 二叉树结构 :每个节点只分裂为两个子节点,简化决策路径
  • 基尼系数 :采用基尼不纯度(Gini Impurity)作为分类任务的分裂标准
  • 多功能性 :既可处理分类问题,也可处理回归问题(使用方差最小化)

核心数学原理详解

基尼系数计算

基尼系数衡量数据集的不纯度,值越小表示纯度越高。对于包含 K 个类别的数据集 D:

def gini_index(y):
    _, counts = np.unique(y, return_counts=True)
    probabilities = counts / len(y)
    return 1 - np.sum(probabilities**2)

最佳分裂点选择

CART 通过穷举所有可能的分裂方式(特征 + 阈值)来寻找最优分裂点:

  1. 对每个特征的所有可能取值进行排序
  2. 取相邻值的中间点作为候选阈值
  3. 计算每个分裂点的加权基尼系数
  4. 选择使基尼系数降低最多的分裂方案

与其他算法的对比分析

算法 分裂标准 树结构 连续值处理 缺失值处理
ID3 信息增益 多叉树 不支持 不支持
C4.5 信息增益比 多叉树 支持 支持
CART 基尼系数 / 方差 二叉树 支持 支持

主要优势:
– 二叉树结构计算效率更高
– 基尼系数计算比信息熵更简单
– 天然支持回归任务

Python 完整实现

树节点结构设计

from dataclasses import dataclass
from typing import Union

@dataclass
class Node:
    feature_idx: int = None    # 分裂特征索引
    threshold: float = None    # 分裂阈值
    left: 'Node' = None        # 左子树
    right: 'Node' = None       # 右子树
    value: Union[int, float] = None  # 叶节点预测值 

核心训练逻辑

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

    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_mask = X[:, feature_idx] <= threshold
                gini = self._weighted_gini(y[left_mask], y[~left_mask])
                if gini < best_gini:
                    best_gini = gini
                    best_feature = feature_idx
                    best_thresh = threshold
        return best_feature, best_thresh

    def _weighted_gini(self, left_y, right_y):
        n = len(left_y) + len(right_y)
        return (len(left_y)/n)*gini_index(left_y) + \
               (len(right_y)/n)*gini_index(right_y)

    def fit(self, X, y, node=None, depth=0):
        if node is None:
            self.root = Node()
            node = self.root

        # 终止条件检查
        if (depth >= self.max_depth or 
            len(y) < self.min_samples_split or 
            len(np.unique(y)) == 1):
            node.value = np.mean(y) if self._is_regression else np.argmax(np.bincount(y))
            return

        # 寻找最佳分裂
        feature_idx, threshold = self._best_split(X, y)
        if feature_idx is None:
            return

        # 递归构建子树
        left_mask = X[:, feature_idx] <= threshold
        node.feature_idx = feature_idx
        node.threshold = threshold
        node.left = Node()
        node.right = Node()

        self.fit(X[left_mask], y[left_mask], node.left, depth+1)
        self.fit(X[~left_mask], y[~left_mask], node.right, depth+1)

过拟合处理策略

预剪枝技术

  1. 最大深度限制 :通过 max_depth 参数控制树生长
  2. 最小样本分裂 :节点样本数少于 min_samples_split 时停止分裂
  3. 最小纯度提升 :分裂必须带来显著的基尼系数降低

后剪枝实现(代价复杂度剪枝)

def prune(self, node, X_val, y_val):
    if node.value is not None:
        return node.value

    # 在验证集上测试剪枝效果
    left_mask = X_val[:, node.feature_idx] <= node.threshold

    # 计算剪枝前后的错误率
    original_error = self._calculate_error(X_val, y_val)

    # 临时转换为叶节点
    original_value = node.value
    node.value = np.mean(y_val) if self._is_regression else np.argmax(np.bincount(y_val))
    pruned_error = self._calculate_error(X_val, y_val)

    # 决策是否剪枝
    if pruned_error <= original_error:
        node.left = node.right = None
    else:
        node.value = original_value
        if node.left:
            self.prune(node.left, X_val[left_mask], y_val[left_mask])
        if node.right:
            self.prune(node.right, X_val[~left_mask], y_val[~left_mask])

生产环境优化建议

  1. 特征预处理 :
  2. 对连续特征进行离散化(等宽 / 等频分箱)
  3. 对高基数类别特征采用目标编码

  4. 内存优化 :

  5. 使用稀疏矩阵存储处理 one-hot 编码
  6. 限制树的深度防止内存爆炸

  7. 计算加速 :

  8. 对排序等耗时操作使用 numpy 向量化
  9. 对大数据集采用随机子采样

  10. API 设计规范 :

  11. 实现 sklearn 的 fit/predict 接口
  12. 提供 get_params/set_params 方法

实战案例:鸢尾花分类

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# 数据准备
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.2)

# 模型训练
tree = CARTDecisionTree(max_depth=3)
tree.fit(X_train, y_train)

# 评估
preds = tree.predict(X_test)
accuracy = np.mean(preds == y_test)
print(f"Test accuracy: {accuracy:.2%}")

延伸思考

  1. 如何处理特征中的连续值?可以考虑动态寻找最优分裂点
  2. 当特征维度很高时,如何加速最佳分裂点的搜索?可以尝试特征采样
  3. 如何扩展实现回归树?需要将分裂标准改为最小化均方误差

总结

通过本文的实现,我们完整构建了一个工业级可用的 CART 决策树,关键收获包括:
– 深入理解了基尼系数与二叉树分裂机制
– 掌握了递归构建决策树的编程范式
– 学习了多种防止过拟合的实用技巧

决策树作为许多复杂模型(如随机森林、GBDT)的基础组件,这些实现经验将为后续学习打下坚实基础。建议读者尝试在更大规模数据集上测试算法性能,并思考如何进一步优化计算效率。

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