从零开始构建CART决策树分类预测模型:原理详解与Python实战

1次阅读
没有评论

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

image.webp

背景痛点分析

在机器学习分类任务中,我们常常需要在不同模型之间做出选择。线性模型(如逻辑回归)和树模型(如 CART 决策树)各有其适用场景:

从零开始构建 CART 决策树分类预测模型:原理详解与 Python 实战

  • 线性模型 适合特征与目标之间存在近似线性关系的情况,模型简单且计算高效,但对非线性关系的捕捉能力有限。
  • 树模型 则能够自动发现数据中的非线性关系和交互作用,对数据的分布假设较少,更适合处理复杂模式的数据。

对于新手来说,使用决策树时常见的问题包括:

  1. 容易产生过拟合,特别是在树深度过大时。
  2. 对特征重要性的判断可能存在偏差。
  3. 对连续特征的处理不够灵活。

技术对比:决策树算法家族

决策树算法主要有 ID3、C4.5 和 CART 三种:

  1. ID3:使用信息增益作为分裂标准,只能处理离散特征,容易偏向取值多的特征。

  2. C4.5:改进 ID3,使用信息增益比,可以处理连续特征,但计算复杂度较高。

  3. CART(本文重点):使用基尼系数作为分裂标准,可以处理离散和连续特征,并且总是生成二叉树。

基尼系数公式推导:

对于有 K 个类别的分类问题,在节点 t 处的基尼系数定义为:

$$
Gini(t) = 1 – \sum_{k=1}^{K} p(k|t)^2
$$

其中 $p(k|t)$ 是节点 t 中类别 k 的样本比例。基尼系数越小,表示节点的纯度越高。

核心实现:从零构建 CART 决策树

1. 基尼系数计算函数

import numpy as np

def gini_index(y: np.ndarray) -> float:
    """
    计算基尼系数
    :param y: 目标值数组
    :return: 基尼系数值
    """
    if len(y) == 0:
        return 0

    # 计算每个类别的比例
    _, counts = np.unique(y, return_counts=True)
    proportions = counts / len(y)

    # 计算基尼系数
    return 1 - np.sum(proportions ** 2)

2. 决策树节点类

class TreeNode:
    def __init__(self, feature_idx: int = None, threshold: float = None,
                 left=None, right=None, value=None):
        """
        决策树节点类
        :param feature_idx: 分裂特征索引
        :param threshold: 分裂阈值
        :param left: 左子树
        :param right: 右子树
        :param value: 叶节点的预测值
        """
        self.feature_idx = feature_idx
        self.threshold = threshold
        self.left = left
        self.right = right
        self.value = value

    def is_leaf(self) -> bool:
        """判断是否为叶节点"""
        return self.value is not None

3. 递归构建决策树

def build_tree(X: np.ndarray, y: np.ndarray, max_depth: int = 5, 
              min_samples_split: int = 2) -> TreeNode:
    """
    递归构建决策树
    :param X: 特征矩阵
    :param y: 目标向量
    :param max_depth: 最大深度
    :param min_samples_split: 最小分裂样本数
    :return: 决策树根节点
    """
    # 终止条件 1: 所有样本属于同一类别
    if len(np.unique(y)) == 1:
        return TreeNode(value=y[0])

    # 终止条件 2: 达到最大深度或样本数不足
    if max_depth <= 0 or len(y) < min_samples_split:
        return TreeNode(value=np.argmax(np.bincount(y)))

    # 寻找最佳分裂特征和阈值
    best_gini = float('inf')
    best_feature, best_threshold = 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
            right_mask = ~left_mask

            # 计算分裂后的基尼系数
            gini_left = gini_index(y[left_mask])
            gini_right = gini_index(y[right_mask])

            # 加权平均基尼系数
            total_gini = (len(y[left_mask]) * gini_left + 
                          len(y[right_mask]) * gini_right) / len(y)

            if total_gini < best_gini:
                best_gini = total_gini
                best_feature = feature_idx
                best_threshold = threshold

    # 如果没有找到合适的分裂,返回叶节点
    if best_gini == float('inf'):
        return TreeNode(value=np.argmax(np.bincount(y)))

    # 根据最佳分裂继续构建子树
    left_mask = X[:, best_feature] <= best_threshold
    right_mask = ~left_mask

    left_subtree = build_tree(X[left_mask], y[left_mask], 
                             max_depth-1, min_samples_split)
    right_subtree = build_tree(X[right_mask], y[right_mask], 
                              max_depth-1, min_samples_split)

    return TreeNode(feature_idx=best_feature, 
                   threshold=best_threshold,
                   left=left_subtree, 
                   right=right_subtree)

生产级优化:sklearn 实战

1. 关键参数调优

from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV

# 创建决策树分类器
tree = DecisionTreeClassifier(random_state=42)

# 定义参数网格
param_grid = {'max_depth': [3, 5, 7, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'criterion': ['gini', 'entropy']
}

# 网格搜索
grid_search = GridSearchCV(tree, param_grid, cv=5, n_jobs=-1)
grid_search.fit(X_train, y_train)

# 输出最佳参数
print("Best parameters:", grid_search.best_params_)
print("Best score:", grid_search.best_score_)

2. 模型可视化

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

plt.figure(figsize=(20, 10))
plot_tree(grid_search.best_estimator_, 
          filled=True, 
          feature_names=feature_names,
          class_names=class_names,
          rounded=True)
plt.show()

避坑指南

  1. 类别不平衡处理
  2. 设置 class_weight='balanced' 让模型自动调整类别权重
  3. 手动指定类别权重:class_weight={0: 1, 1: 5}

  4. 无效特征识别

    import pandas as pd
    
    # 获取特征重要性
    feature_importance = pd.DataFrame({
        'feature': feature_names,
        'importance': grid_search.best_estimator_.feature_importances_
    }).sort_values('importance', ascending=False)
    
    # 可视化
    feature_importance.plot.bar(x='feature', y='importance')
    plt.show()

延伸思考

  1. 如何处理连续特征中的缺失值?可以考虑哪些填充策略?
  2. 除了基尼系数,还可以使用哪些指标作为分裂标准?它们各有什么优缺点?
  3. 决策树容易过拟合,除了预剪枝,还可以采用哪些后剪枝策略?

总结

通过本文,我们从理论到实践完整地实现了 CART 决策树分类模型。从基尼系数的数学原理,到 Python 原生实现,再到 sklearn 的生产级应用,希望这篇教程能帮助初学者全面理解决策树的工作原理和实际应用。决策树模型直观易懂,是入门机器学习的不错选择,但也要注意其容易过拟合的特点,合理使用剪枝策略和参数调优。

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