概述
决策树是一种极其直观的分类方法,它通过一系列的判断问题来逐步缩小分类的范围,最终将对象归类到某个类别中。
决策树的构建过程
在构建决策树时,我们主要面临三个关键问题:
- 选择哪些变量来构建树?
- 如何选择划分的阈值?
- 何时停止树的扩展?
核心思想是选择能够最大限度降低“杂质”(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)
何时停止树的扩展
我们可以使用以下几种方式来决定何时停止决策树的扩展:
- 当某个节点达到 0 杂质时(即 Gini 指数为 0),停止对该节点的进一步划分。
- 当划分后的 Gini 指数相对于父节点的 Gini 指数没有显著改善时,停止划分。
- 当树达到预设的最大高度时,停止扩展。
虽然这些停止准则可以防止过拟合,但出于简化考虑,以下代码示例并未实现这些准则,树会一直划分下去。
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 库快速构建和可视化决策树。决策树凭借其直观性和强大的分类能力,在处理异构数据方面表现出色。然而,正如我们所讨论的,决策树也容易陷入过拟合,尤其是在树的深度过大时。因此,尽管决策树在某些任务中效果显著,但在实际应用中,集成方法如随机森林往往更为有效。
下一篇文章中,我们将探讨随机森林算法。
如果你觉得这篇博文对你有帮助,请点赞、收藏、关注我,并且可以打赏支持我!
欢迎关注我的后续博文,我将分享更多关于人工智能、自然语言处理和计算机视觉的精彩内容。
谢谢大家的支持!