从零实现CART决策树代码:机器学习入门实战指南

1次阅读
没有评论

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

image.webp

为什么需要自己实现 CART 决策树?

作为机器学习初学者,直接调用 sklearn.tree.DecisionTreeClassifier 确实方便,但可能会遇到这些困惑:

从零实现 CART 决策树代码:机器学习入门实战指南

  • 黑箱操作导致对分裂过程缺乏直观理解
  • 遇到数据异常时不知道如何调整参数
  • 面试被问到原理时只能回答 ” 调过 API”

自己动手实现一次,你会真正掌握:

  1. 如何用基尼系数衡量数据不纯度
  2. 递归构建树形结构的编程技巧
  3. 避免过拟合的核心方法

基础数学原理

基尼系数计算

决策树分裂的目标是让子节点比父节点更 ” 纯 ”,CART 算法使用基尼系数 (Gini Index) 作为衡量标准:

$$ Gini(D) = 1 – \sum_{k=1}^{K} (\frac{|C_k|}{|D|})^2 $$

其中:
– $D$ 表示当前节点数据集
– $K$ 是类别总数
– $|C_k|$ 是第 $k$ 类样本的数量

当所有样本属于同一类时,基尼系数为 0(最理想情况)。

特征选择流程

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

  1. 计算分裂前的基尼系数
  2. 尝试所有可能的分裂点(对连续特征需要排序后取中点)
  3. 选择使基尼系数下降最多的分裂方案

数学表达式:

$$ \Delta Gini = Gini(D) – \frac{|D_左 |}{|D|}Gini(D_左) – \frac{|D_右 |}{|D|}Gini(D_右) $$

Python 实现详解

核心数据结构

from dataclasses import dataclass
from typing import Any, Optional

@dataclass
class TreeNode:
    feature_idx: Optional[int] = None  # 分裂特征索引
    threshold: Optional[float] = None  # 分裂阈值
    left: Optional['TreeNode'] = None  # 左子树
    right: Optional['TreeNode'] = None # 右子树
    value: Optional[Any] = None       # 叶节点存储的类别

基尼系数计算实现

def gini_impurity(y: np.ndarray) -> float:
    """计算基尼不纯度"""
    if len(y) == 0:
        return 0

    probs = np.bincount(y) / len(y)
    return 1 - np.sum(probs ** 2)

递归建树主逻辑

def build_tree(X, y, max_depth=5, min_samples_split=2):
    # 终止条件检查
    if len(y) < min_samples_split or max_depth <= 0:
        return TreeNode(value=np.argmax(np.bincount(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_mask = X[:, feature_idx] <= thresh
            gini = weighted_gini(y, left_mask)

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

    # 分裂后递归调用
    left_mask = X[:, best_feature] <= best_thresh
    left = build_tree(X[left_mask], y[left_mask], max_depth-1, min_samples_split)
    right = build_tree(X[~left_mask], y[~left_mask], max_depth-1, min_samples_split)

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

生产环境优化技巧

处理大规模数据

  1. 特征预筛选:先用卡方检验选择 Top- K 特征
  2. 增量训练:对超大数据集使用在线学习方式
  3. 并行化:对不同特征的分裂评估可以并行计算

可视化调试

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

# 将自实现的树转换为 sklearn 格式(仅用于可视化)def tree_to_sklearn_format(node):
    # 转换逻辑省略...
    pass

plt.figure(figsize=(12,8))
plot_tree(tree_to_sklearn_format(root), filled=True)
plt.show()

常见问题排查表

问题现象 可能原因 解决方案
基尼系数为负数 样本计数时整数溢出 检查 np.bincount 输入数据类型
树深度爆炸 缺少终止条件 添加 max_depth 和min_samples_split限制
预测准确率低 连续特征未离散化 对连续特征排序后取中点作为候选分裂点

延伸思考

  1. 当特征同时包含离散值和连续值时,如何统一处理?
  2. 类别不平衡数据集中,基尼系数是否仍然适用?
  3. 如何实现基于决策树的回归任务?

个人实践心得

通过这次实现,最深刻的体会是:

  • 递归函数的终止条件需要特别小心,我最初因为漏掉 min_samples_split 导致生成了过深的树
  • 对连续特征的处理比想象中复杂,需要先排序再取中点作为候选分裂阈值
  • 自己实现的版本虽然性能不如 sklearn 优化过的 C 代码,但对原理的理解深入了很多

建议大家在掌握基本原理后,可以对比看看 sklearn 的源码实现(特别是 _criterion.pyx 文件),会有更多收获。

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