从零实现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
评论(没有评论)