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

1次阅读
没有评论

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

image.webp

背景痛点

很多机器学习初学者在第一次手写 CART 决策树时,往往会遇到一些共性问题。我自己刚开始实现时也踩过不少坑,这里总结几个典型的误区:

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

  • 连续特征处理不当:直接把连续值当作类别特征处理,导致决策树效果很差
  • 缺失值忽略:没有考虑数据中可能存在的缺失值情况
  • 递归终止条件太简单:仅用 ” 所有样本属于同一类 ” 作为停止条件,容易过拟合
  • 代码结构混乱:把建树、预测、剪枝等逻辑全部写在一起,难以维护

相比直接调用 sklearn 的 DecisionTreeClassifier,自己实现决策树有几个优势:

  1. 更深入理解算法原理
  2. 可以定制特殊需求(如特定的剪枝策略)
  3. 对代码性能优化有完全控制权

当然现成库也有优势,比如优化更好、功能更全。但对于学习来说,从零实现是不可替代的。

技术实现

基尼系数计算

CART 决策树使用基尼系数 (Gini Index) 来衡量节点的不纯度。对于一个包含 K 类的数据集 D,基尼系数计算公式为:

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

其中 $C_k$ 是第 k 类样本的集合。基尼系数越小,说明数据越纯。

在 Python 中实现如下:

def gini_index(groups, classes):
    """计算基尼系数"""
    n_samples = sum(len(group) for group in groups)
    gini = 0.0
    for group in groups:
        size = len(group)
        if size == 0:
            continue
        score = 0.0
        for class_val in classes:
            p = [row[-1] for row in group].count(class_val) / size
            score += p * p
        gini += (1.0 - score) * (size / n_samples)
    return gini

特征选择

特征选择的目标是找到使子节点基尼系数最小的分裂方式。对于每个特征,我们需要:

  1. 计算所有可能的分裂点
  2. 计算分裂后的加权基尼系数
  3. 选择使基尼系数最小的分裂方案

这里以连续特征为例:

def get_split(dataset):
    """找到最佳分裂特征和值"""
    class_values = list(set(row[-1] for row in dataset))
    b_index, b_value, b_score, b_groups = 999, 999, 999, None

    for index in range(len(dataset[0])-1):
        for row in dataset:
            groups = test_split(index, row[index], dataset)
            gini = gini_index(groups, class_values)
            if gini < b_score:
                b_index, b_value, b_score, b_groups = index, row[index], gini, groups
    return {
        'index': b_index, 
        'value': b_value,
        'groups': b_groups
    }

递归建树

有了上面的基础,建树过程就是一个递归分割的过程:

  1. 创建根节点
  2. 寻找最佳分裂
  3. 如果满足停止条件则创建叶节点
  4. 否则递归处理左右子树

停止条件通常包括:

  • 节点样本数小于阈值
  • 基尼系数低于阈值
  • 达到最大深度
def build_tree(node, max_depth, min_size, depth):
    """递归构建决策树"""
    left, right = node['groups']
    del(node['groups'])

    # 检查是否需要终止
    if not left or not right:
        node['left'] = node['right'] = to_terminal(left + right)
        return
    if depth >= max_depth:
        node['left'], node['right'] = to_terminal(left), to_terminal(right)
        return

    # 处理左子树
    if len(left) <= min_size:
        node['left'] = to_terminal(left)
    else:
        node['left'] = get_split(left)
        build_tree(node['left'], max_depth, min_size, depth+1)

    # 处理右子树
    if len(right) <= min_size:
        node['right'] = to_terminal(right)
    else:
        node['right'] = get_split(right)
        build_tree(node['right'], max_depth, min_size, depth+1)

代码规范

完整实现需要注意几个关键点:

  1. 类型注解:Python 3.5+ 支持类型注解,可以提高代码可读性
  2. PEP8 规范:保持一致的代码风格
  3. 模块化设计:将不同功能拆分为独立函数
  4. 详细注释:特别是算法关键步骤

完整代码结构建议如下:

from typing import List, Dict, Union

class DecisionTree:
    """CART 决策树实现"""

    def __init__(self, max_depth: int = 5, min_size: int = 10):
        self.max_depth = max_depth
        self.min_size = min_size
        self.tree = None

    def fit(self, X: List[List[float]], y: List[int]) -> None:
        """训练模型"""
        dataset = [x + [label] for x, label in zip(X, y)]
        self.tree = self._build_tree(dataset)

    def _build_tree(self, dataset: List[list], depth: int = 0) -> Dict:
        """内部建树方法"""
        # 实现细节同上
        pass

    def predict(self, X: List[List[float]]) -> List[int]:
        """预测方法"""
        return [self._predict(self.tree, x) for x in X]

    def _predict(self, node: Dict, row: List[float]) -> int:
        """单样本预测"""
        if row[node['index']] < node['value']:
            if isinstance(node['left'], dict):
                return self._predict(node['left'], row)
            return node['left']
        else:
            if isinstance(node['right'], dict):
                return self._predict(node['right'], row)
            return node['right']

生产建议

内存优化

当特征维度很高时,可以采取以下优化措施:

  1. 对连续特征只考虑分位数点作为候选分裂点
  2. 使用稀疏矩阵存储数据
  3. 实现增量学习(partial_fit)

可视化调试

决策树可视化是很好的调试工具:

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

# 将自实现的树转换为 sklearn 格式(需要适配)plot_tree(sklearn_tree)
plt.show()

算法对比

CART 与 ID3/C4.5 的主要区别:

特性 CART ID3 C4.5
分裂标准 基尼系数 信息增益 信息增益比
处理连续特征
生成树类型 二叉树 多叉树 多叉树
剪枝方式 代价复杂度 悲观剪枝

常见错误排查

错误现象 可能原因 解决方案
基尼系数为负数 样本计数错误 检查类别计数逻辑
所有样本都分到同一侧 分裂值选择不当 检查特征值范围
树深度过大 停止条件不合理 调整 max_depth/min_size
预测结果全相同 树没有正确分裂 检查建树过程

延伸思考

  1. 当遇到类别不平衡数据时,如何调整决策树的训练过程?
  2. 对于高维稀疏数据(如文本特征),决策树是否仍然适用?
  3. 如何实现决策树的并行化训练以加速大规模数据?

实现 CART 决策树是理解机器学习基础的重要一步。虽然实际项目中我们更常用现成库,但掌握底层原理能帮助我们更好地调参和解决问题。希望这篇指南能帮你避开我当年踩过的坑,顺利实现第一个决策树模型!

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