基于Python的机器学习系列(12):决策树

概述

        决策树是一种极其直观的分类方法,它通过一系列的判断问题来逐步缩小分类的范围,最终将对象归类到某个类别中。

决策树的构建过程

        在构建决策树时,我们主要面临三个关键问题:

  1. 选择哪些变量来构建树?
  2. 如何选择划分的阈值?
  3. 何时停止树的扩展?

        核心思想是选择能够最大限度降低“杂质”(impurity)的特征来划分树,使得树能够以最快的速度、最小的高度达到决策。杂质最低意味着节点中的样本大多数属于同一类别。相反,杂质最高意味着节点中的样本属于完全不同的类别。

        衡量杂质的一种方法是使用 Gini 指数(另一种方法是熵),其公式如下:

        其中 c 是类别数,pi 是每个类别的概率。例如,假设我们的特征数据 X 是 [[2],[3],[10],[19]],目标标签 y 是 [0, 0, 1, 1]。如果一个节点包含 4 个样本,其中 2 个属于类别 0,2 个属于类别 1,则每个类别的概率为:

        因此,该节点的 Gini 指数为:

        接下来,我们需要决定如何划分这个节点,以便获得最低的 Gini 指数(最高的纯度)的子节点。例如,如果我们以 x < 4 进行划分,左子节点的 X 为 [[2],[3]],y 为 [0, 0],右子节点的 X 为 [[10],[19]],y 为 [1, 1]。此时 Gini 指数为 0,表示划分后的节点纯度最高。

连续变量的阈值选择

        对于连续变量,我们首先对样本进行排序,然后在所有连续值之间的中点作为潜在的划分阈值。例如,给定 X 为 [[2],[3],[10],[19]],我们可以考虑的阈值是 2.5、6.5 和 14.5。

        以下代码展示了如何实现这一过程:

# 单特征的阈值选择示例
X = np.array([[2],[3],[10],[19]])
y = np.array([0, 0, 1, 1])

def find_split(X, y, n_classes):
    n_samples, n_features = X.shape
    if n_samples <= 1:
        return None, None
    
    feature_ix, threshold = None, None
    sample_per_class_parent = [np.sum(y == c) for c in range(n_classes)]
    best_gini = 1.0 - sum((n / n_samples) ** 2 for n in sample_per_class_parent)

    for feature in range(n_features):
        sample_sorted = sorted(X[:, feature])
        sort_idx = np.argsort(X[:, feature])
        y_sorted = y[sort_idx]
                
        sample_per_class_left = [0] * n_classes   
        sample_per_class_right = sample_per_class_parent.copy()

        for i in range(1, n_samples): 
            c = y_sorted[i - 1]
            sample_per_class_left[c] += 1  
            sample_per_class_right[c] -= 1
            
            gini_left = 1.0 - sum(
                (sample_per_class_left[x] / i) ** 2 for x in range(n_classes)
            )
                        
            gini_right = 1.0 - sum(
                (sample_per_class_right[x] / (n_samples - i)) ** 2 for x in range(n_classes)
            )

            weighted_gini = ((i / n_samples) * gini_left) + ( (n_samples - i) /n_samples) * gini_right

            if sample_sorted[i] == sample_sorted[i - 1]:
                continue

            if weighted_gini < best_gini:
                best_gini = weighted_gini
                feature_ix = feature
                threshold = (sample_sorted[i] + sample_sorted[i - 1]) / 2

    return feature_ix, threshold

feature, threshold = find_split(X, y, len(set(y)))
print("Best feature used for split: ", feature)
print("Best threshold used for split: ", threshold)
何时停止树的扩展

        我们可以使用以下几种方式来决定何时停止决策树的扩展:

  1. 当某个节点达到 0 杂质时(即 Gini 指数为 0),停止对该节点的进一步划分。
  2. 当划分后的 Gini 指数相对于父节点的 Gini 指数没有显著改善时,停止划分。
  3. 当树达到预设的最大高度时,停止扩展。

        虽然这些停止准则可以防止过拟合,但出于简化考虑,以下代码示例并未实现这些准则,树会一直划分下去。

Scratch 实现
class Node:
    def __init__(self, gini, num_samples, num_samples_per_class, predicted_class):
        self.gini = gini
        self.num_samples = num_samples
        self.num_samples_per_class = num_samples_per_class
        self.predicted_class = predicted_class
        self.feature_index = 0
        self.threshold = 0
        self.left = None
        self.right = None

def fit(Xtrain, ytrain, n_classes, depth=0):  
    n_samples, n_features = Xtrain.shape
    num_samples_per_class = [np.sum(ytrain == i) for i in range(n_classes)]
    predicted_class = np.argmax(num_samples_per_class)
    
    node = Node(
        gini = 1 - sum((np.sum(ytrain == c) / n_samples) ** 2 for c in range(n_classes)),
        predicted_class=predicted_class,
        num_samples = ytrain.size,
        num_samples_per_class = num_samples_per_class,
        )
        
    feature, threshold = find_split(Xtrain, ytrain, n_classes)
    if feature is not None:
        indices_left = Xtrain[:, feature] < threshold
        X_left, y_left = Xtrain[indices_left], ytrain[indices_left]
        X_right, y_right = Xtrain[~indices_left], ytrain[~indices_left]

        node.feature_index = feature
        node.threshold = threshold
        node.left = fit(X_left, y_left, n_classes, depth + 1)
        node.right = fit(X_right, y_right, n_classes, depth + 1)
    return node

def predict(sample, tree):
    while tree.left:
        if sample[tree.feature_index] < tree.threshold:
            tree = tree.left
        else:
            tree = tree.right
    return tree.predicted_class

Xtrain = np.array([[2, 5],[3, 5],[10, 5],[19, 5]])
ytrain = np.array([0, 0, 1, 1])
Xtest = np.array(([[4, 6],[6, 9],[9, 2],[12, 8]]))
ytest = np.array([0, 0, 1, 1])

tree = fit(Xtrain, ytrain, len(set(ytrain)))
pred = [predict(x, tree) for x in Xtest]

print("Tree feature ind: ", tree.feature_index)
print("Tree threshold: ", tree.threshold)
print("Pred: ", np.array(pred))
print("ytest: ", ytest)
使用Sklearn 实现决策树
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=300, centers=4, random_state=0, cluster_std=1.0)

model = DecisionTreeClassifier().fit(X, y)

def plot_tree(model, X, y):
    plt.grid()
    plt.scatter(X[:, 0], X[:, 1], c=y, s=30)
    xx, yy = np.meshgrid(np.linspace(-5, 5, num=200), np.linspace(-2, 11, num=200))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
    contours = plt.contourf(xx, yy, Z, alpha=0.2)

plot_tree(model, X, y)
何时使用决策树

        决策树在处理异构特征时非常强大,能够有效地处理多种类型的数据。然而,它也容易过拟合,特别是在树的深度过大时。因此,虽然决策树在某些任务中表现出色,但在实际应用中通常更倾向于使用集成方法,如随机森林,这种方法通过结合多个决策树的预测结果,往往能获得更好的效果。

结语

        通过这篇文章,我们深入探讨了决策树算法的基本原理,包括如何选择最佳的划分特征和阈值,以及何时停止树的扩展。我们从零开始实现了决策树模型,并展示了如何利用 Scikit-learn 库快速构建和可视化决策树。决策树凭借其直观性和强大的分类能力,在处理异构数据方面表现出色。然而,正如我们所讨论的,决策树也容易陷入过拟合,尤其是在树的深度过大时。因此,尽管决策树在某些任务中效果显著,但在实际应用中,集成方法如随机森林往往更为有效。

        下一篇文章中,我们将探讨随机森林算法。

如果你觉得这篇博文对你有帮助,请点赞、收藏、关注我,并且可以打赏支持我!

欢迎关注我的后续博文,我将分享更多关于人工智能、自然语言处理和计算机视觉的精彩内容。

谢谢大家的支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的Anthony

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值