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

1次阅读
没有评论

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

image.webp

算法背景

决策树是机器学习中最基础且直观的算法之一,它通过树状结构对数据进行分类或回归。CART(Classification and Regression Trees)算法由 Breiman 等人于 1984 年提出,是目前最常用的决策树算法之一,广泛应用于数据挖掘和机器学习领域。

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

CART 算法的特点包括:

  • 可以处理分类和回归问题
  • 使用基尼系数(Gini Index)作为特征选择的指标
  • 生成二叉树结构
  • 支持剪枝操作防止过拟合

核心原理

1. 基尼系数计算

基尼系数是 CART 算法用于分类问题的纯度衡量标准。对于一个包含 K 个类别的数据集 D,基尼系数计算公式为:

Gini(D) = 1 - Σ(p_k)^2,其中 p_k 是第 k 个类别在 D 中出现的概率。

2. 特征选择

CART 算法通过计算每个特征的基尼指数增益来选择最佳分裂特征。对于特征 A,将数据集 D 划分为 D1 和 D2 后的基尼指数增益为:

ΔGini(A) = Gini(D) - (|D1|/|D|)*Gini(D1) - (|D2|/|D|)*Gini(D2)

算法会选择使 ΔGini 最大的特征作为当前节点的分裂特征。

3. 节点分裂

对于选定的特征,算法会寻找最佳分裂点将数据划分为两个子集。对于连续特征,通常通过排序后尝试所有可能的分割点;对于离散特征,则尝试所有可能的二分组合。

Python 实现

下面我们使用面向对象的方式实现 CART 决策树算法。完整代码包含以下主要部分:

import numpy as np
from collections import Counter

class TreeNode:
    """决策树节点类"""
    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  # 叶节点值

class CARTDecisionTree:
    """CART 决策树实现"""
    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 fit(self, X, y):
        """训练决策树"""
        self.root = self._build_tree(X, y)

    def _build_tree(self, X, y, depth=0):
        """递归构建决策树"""
        n_samples, n_features = X.shape
        n_classes = len(np.unique(y))

        # 停止条件
        if (self.max_depth is not None and depth >= self.max_depth) or \
           n_samples < self.min_samples_split or n_classes == 1:
            leaf_value = self._most_common_label(y)
            return TreeNode(value=leaf_value)

        # 寻找最佳分裂
        best_feature, best_threshold = self._best_split(X, y)

        # 如果无法找到有效分裂则返回叶节点
        if best_feature is None:
            return TreeNode(value=self._most_common_label(y))

        # 根据最佳分裂点划分数据
        left_idxs = X[:, best_feature] <= best_threshold
        right_idxs = ~left_idxs

        # 递归构建左右子树
        left = self._build_tree(X[left_idxs], y[left_idxs], depth+1)
        right = self._build_tree(X[right_idxs], y[right_idxs], depth+1)

        return TreeNode(best_feature, best_threshold, left, right)

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

        for feature in range(X.shape[1]):
            thresholds = np.unique(X[:, feature])
            for threshold in thresholds:
                left_idxs = X[:, feature] <= threshold
                right_idxs = ~left_idxs

                if len(y[left_idxs]) == 0 or len(y[right_idxs]) == 0:
                    continue

                # 计算基尼指数
                gini = self._gini_index(y[left_idxs], y[right_idxs])

                if gini < best_gini:
                    best_gini = gini
                    best_feature = feature
                    best_threshold = threshold

        return best_feature, best_threshold

    def _gini_index(self, y_left, y_right):
        """计算基尼指数"""
        n = len(y_left) + len(y_right)
        p_left = len(y_left) / n
        p_right = len(y_right) / n

        return p_left * self._gini(y_left) + p_right * self._gini(y_right)

    def _gini(self, y):
        """计算基尼系数"""
        counts = Counter(y)
        gini = 1
        for label in counts:
            prob = counts[label] / len(y)
            gini -= prob ** 2
        return gini

    def _most_common_label(self, y):
        """返回出现次数最多的类别"""
        counts = Counter(y)
        return counts.most_common(1)[0][0]

    def predict(self, X):
        """预测"""
        return np.array([self._traverse_tree(x, self.root) for x in X])

    def _traverse_tree(self, x, node):
        """遍历决策树进行预测"""
        if node.value is not None:
            return node.value

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

优化技巧

预剪枝

预剪枝是在决策树生成过程中提前停止树的构建,常用策略包括:

  • 设置最大深度(max_depth)
  • 设置叶节点最小样本数(min_samples_leaf)
  • 设置分裂最小样本数(min_samples_split)
  • 设置最小基尼增益(min_impurity_decrease)

后剪枝

后剪枝是先让决策树充分生长,然后自底向上对树进行剪枝,常用的方法是代价复杂度剪枝(CCP)。

避坑指南

  1. 过拟合问题
  2. 解决方案:合理设置预剪枝参数或使用后剪枝

  3. 类别不平衡问题

  4. 解决方案:使用加权基尼系数或调整类别权重

  5. 连续特征处理

  6. 解决方案:确保正确排序并尝试所有可能的分割点

  7. 内存消耗过大

  8. 解决方案:限制树的最大深度或使用随机森林等集成方法

实战建议

  1. 小规模数据
  2. 可以尝试让决策树完全生长,然后使用后剪枝优化

  3. 大规模数据

  4. 使用预剪枝控制模型复杂度
  5. 考虑使用并行化实现加速训练

  6. 高维数据

  7. 在特征选择时增加随机性
  8. 考虑结合特征重要性分析

思考题

  1. 如何改进我们的实现以支持回归任务?
  2. 当特征数量非常多时,如何优化特征选择过程?
  3. 如何实现决策树的可视化功能?
  4. 如何将我们的实现扩展到多输出问题?

希望这篇教程能帮助你理解并实现 CART 决策树算法。通过逐步拆解算法核心流程,我们不仅理解了原理,还获得了可直接使用的 Python 实现。在实际应用中,记得根据数据特点调整参数,平衡模型复杂度和泛化能力。

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