CART 决策树算法详解
摘要: 本文详细阐述了 CART(Classification and Regression Trees,分类与回归树)决策树算法。首先介绍了决策树的基本概念和 CART 算法在决策树家族中的地位与特点。接着深入剖析 CART 算法的原理,包括特征选择标准、树的构建过程、剪枝策略等方面的数学原理与逻辑。通过丰富的代码示例展示了 CART 算法在分类和回归任务中的实际应用,涵盖数据预处理、模型构建、训练与评估等完整流程。最后探讨了 CART 算法的优势、局限性以及其在不同领域的广泛应用,旨在帮助读者全面深入地理解 CART 决策树算法并能熟练应用于实际数据挖掘与机器学习项目中。
一、引言
决策树是一种常见且重要的机器学习算法,它以树形结构对数据进行分类或回归预测。CART 决策树算法是决策树算法中的经典代表之一,具有广泛的应用。与其他决策树算法相比,CART 既能处理分类问题,又能处理回归问题,并且其构建的树结构简洁明了,易于理解和解释。
二、CART 算法原理
(一)特征选择标准
- 分类任务
- 在分类问题中,CART 采用基尼指数(Gini index)来选择最优特征。对于数据集 D D D,其基尼指数定义为: G i n i ( D ) = 1 − ∑ k = 1 ∣ Y ∣ p k 2 Gini(D)=1-\sum_{k = 1}^{|\mathcal{Y}|}p_{k}^{2} Gini(D)=1−∑k=1∣Y∣pk2,其中 Y \mathcal{Y} Y是类别集合, p k p_{k} pk是数据集中属于第 k k k类的样本比例。对于特征 A A A,其可能有多个取值 { a 1 , a 2 , ⋯ , a m } \{a_{1},a_{2},\cdots,a_{m}\} {a1,a2,⋯,am},将数据集 D D D根据特征 A A A的取值分为 D 1 , D 2 , ⋯ , D m D_{1},D_{2},\cdots,D_{m} D1,D2,⋯,Dm个子集,特征 A A A的基尼指数为: G i n i A ( D ) = ∑ i = 1 m ∣ D i ∣ ∣ D ∣ G i n i ( D i ) Gini_{A}(D)=\sum_{i = 1}^{m}\frac{|D_{i}|}{|D|}Gini(D_{i}) GiniA(D)=∑i=1m∣D∣∣Di∣Gini(Di)。在构建决策树时,选择基尼指数最小的特征作为划分特征,即 min A G i n i A ( D ) \min_{A}Gini_{A}(D) minAGiniA(D)。
- 回归任务
- 对于回归问题,CART 使用均方误差(MSE)作为特征选择标准。对于数据集 D D D,其均方误差为: M S E ( D ) = 1 ∣ D ∣ ∑ i ∈ D ( y i − y ˉ ) 2 MSE(D)=\frac{1}{|D|}\sum_{i\in D}(y_{i}-\bar{y})^{2} MSE(D)=∣D∣1∑i∈D(yi−yˉ)2,其中 y i y_{i} yi是样本 i i i的真实值, y ˉ \bar{y} yˉ是数据集 D D D中样本真实值的均值。对于特征 A A A,同样根据其取值将数据集 D D D划分为 D 1 , D 2 , ⋯ , D m D_{1},D_{2},\cdots,D_{m} D1,D2,⋯,Dm个子集,特征 A A A的均方误差为: M S E A ( D ) = ∑ i = 1 m ∣ D i ∣ ∣ D ∣ M S E ( D i ) MSE_{A}(D)=\sum_{i = 1}^{m}\frac{|D_{i}|}{|D|}MSE(D_{i}) MSEA(D)=∑i=1m∣D∣∣Di∣MSE(Di)。选择使均方误差最小的特征进行划分,即 min A M S E A ( D ) \min_{A}MSE_{A}(D) minAMSEA(D)。
(二)树的构建过程
- 初始化
- 从根节点开始,将整个训练数据集作为当前节点的数据集。
- 特征选择与划分
- 对于当前节点的数据集,根据分类或回归任务的特征选择标准(基尼指数或均方误差),选择最优特征 A A A及其最优划分点 a a a,将当前数据集划分为左右子节点数据集 D l e f t D_{left} Dleft和 D r i g h t D_{right} Dright。
- 递归构建
- 对左右子节点数据集重复上述特征选择与划分步骤,直到满足停止条件,如节点中的样本数小于某个阈值、节点的纯度达到一定要求(如基尼指数小于某个阈值或均方误差足够小)或者树的深度达到预设的最大深度等。
(三)剪枝策略
- 预剪枝
- 在树的构建过程中,提前设定一些停止条件来限制树的生长,如限制树的深度、限制节点中的最小样本数等。预剪枝的优点是可以减少计算量和防止过拟合,但可能会因为过早停止生长而导致欠拟合,无法得到最优的树结构。
- 后剪枝
- 先构建完整的决策树,然后从叶节点开始,逐步向上判断是否对某个子树进行剪枝。剪枝的依据是比较剪枝前后树的损失函数值。对于分类树,损失函数可以定义为: C α ( T ) = ∑ t = 1 ∣ T ∣ N t G i n i ( t ) + α ∣ T ∣ C_{\alpha}(T)=\sum_{t = 1}^{|T|}N_{t}Gini(t)+\alpha|T| Cα(T)=∑t=1∣T∣NtGini(t)+α∣T∣,其中 T T T是树的结构, N t N_{t} Nt是节点 t t t的样本数, G i n i ( t ) Gini(t) Gini(t)是节点 t t t的基尼指数, α \alpha α是正则化参数,用于控制树的复杂度。对于回归树,损失函数可以类似定义,如 C α ( T ) = ∑ t = 1 ∣ T ∣ M S E ( t ) + α ∣ T ∣ C_{\alpha}(T)=\sum_{t = 1}^{|T|}MSE(t)+\alpha|T| Cα(T)=∑t=1∣T∣MSE(t)+α∣T∣。如果剪枝后损失函数值减小,则进行剪枝操作,不断重复这个过程直到不能再剪枝为止。后剪枝能够得到更优的树结构,但计算复杂度相对较高。
三、CART 算法代码示例
(一)分类任务代码示例
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
# 加载鸢尾花数据集
iris = load_iris()
X = iris.data
y = iris.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 使用 scikit-learn 的 CART 决策树分类器
clf = DecisionTreeClassifier()
# 训练模型
clf.fit(X_train, y_train)
# 预测测试集
y_pred = clf.predict(X_test)
# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print('准确率:', accuracy)
# 自定义 CART 决策树分类器(简化版)
class MyDecisionTreeClassifier:
def __init__(self, max_depth=None, min_samples_split=2):
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.tree = None
def fit(self, X, y):
# 构建决策树
self.tree = self._build_tree(X, y, depth=0)
def _build_tree(self, X, y, depth):
# 如果满足停止条件,返回叶节点
if (self.max_depth is not None and depth >= self.max_depth) or len(X)<self.min_samples_split:
# 返回叶节点的类别(多数类)
unique_classes, class_counts = np.unique(y, return_counts=True)
return unique_classes[np.argmax(class_counts)]
# 选择最优特征和划分点
best_feature, best_threshold = self._choose_best_feature(X, y)
# 创建节点
node = {'feature': best_feature, 'threshold': best_threshold}
# 根据特征和阈值划分数据集
left_indices = X[:, best_feature]<=best_threshold
right_indices = X[:, best_feature]>best_threshold
# 递归构建左右子树
node['left'] = self._build_tree(X[left_indices], y[left_indices], depth + 1)
node['right'] = self._build_tree(X[right_indices], y[right_indices], depth + 1)
return node
def _choose_best_feature(self, X, y):
best_gini = float('inf')
best_feature = None
best_threshold = None
# 遍历特征
for feature in range(X.shape[1]):
# 遍历特征的取值
unique_values = np.unique(X[:, feature])
for value in unique_values[:-1]:
# 划分数据集
left_indices = X[:, feature]<=value
right_indices = X[:, feature]>value
# 计算基尼指数
gini = self._gini_index(y[left_indices], y[right_indices])
if gini<best_gini:
best_gini = gini
best_feature = feature
best_threshold = value
return best_feature, best_threshold
def _gini_index(self, y_left, y_right):
# 计算左右子节点的基尼指数
def gini(y):
unique_classes, class_counts = np.unique(y, return_counts=True)
gini = 1
for count in class_counts:
gini -= (count/len(y))**2
return gini
# 计算总的基尼指数
total_samples = len(y_left)+len(y_right)
return (len(y_left)/total_samples)*gini(y_left)+(len(y_right)/total_samples)*gini(y_right)
def predict(self, X):
# 预测数据
y_pred = []
for x in X:
node = self.tree
while isinstance(node, dict):
if x[node['feature']]<=node['threshold']:
node = node['left']
else:
node = node['right']
y_pred.append(node)
return np.array(y_pred)
# 使用自定义 CART 决策树分类器
my_clf = MyDecisionTreeClassifier(max_depth=5)
my_clf.fit(X_train, y_train)
my_y_pred = my_clf.predict(X_test)
my_accuracy = accuracy_score(y_test, my_y_pred)
print('自定义分类器准确率:', my_accuracy)
(二)回归任务代码示例
import numpy as np
import pandas as pd
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error
# 加载波士顿房价数据集
boston = load_boston()
X = boston.data
y = boston.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 使用 scikit-learn 的 CART 决策树回归器
reg = DecisionTreeRegressor()
# 训练模型
reg.fit(X_train, y_train)
# 预测测试集
y_pred = reg.predict(X_test)
# 计算均方误差
mse = mean_squared_error(y_test, y_pred)
print('均方误差:', mse)
# 自定义 CART 决策树回归器(简化版)
class MyDecisionTreeRegressor:
def __init__(self, max_depth=None, min_samples_split=2):
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.tree = None
def fit(self, X, y):
# 构建决策树
self.tree = self._build_tree(X, y, depth=0)
def _build_tree(self, X, y, depth):
# 如果满足停止条件,返回叶节点
if (self.max_depth is not None and depth >= self.max_depth) or len(X)<self.min_samples_split:
# 返回叶节点的均值
return np.mean(y)
# 选择最优特征和划分点
best_feature, best_threshold = self._choose_best_feature(X, y)
# 创建节点
node = {'feature': best_feature, 'threshold': best_threshold}
# 根据特征和阈值划分数据集
left_indices = X[:, best_feature]<=best_threshold
right_indices = X[:, best_feature]>best_threshold
# 递归构建左右子树
node['left'] = self._build_tree(X[left_indices], y[left_indices], depth + 1)
node['left_size'] = len(y[left_indices])
node['right'] = self._build_tree(X[right_indices], y[right_indices], depth + 1)
node['right_size'] = len(y[right_indices])
return node
def _choose_best_feature(self, X, y):
best_mse = float('inf')
best_feature = None
best_threshold = None
# 遍历特征
for feature in range(X.shape[1]):
# 遍历特征的取值
unique_values = np.unique(X[:, feature])
for value in unique_values[:-1]:
# 划分数据集
left_indices = X[:, feature]<=value
right_indices = X[:, style="text-decoration: underline;">_mse_index(y[left_indices], y[right_indices])
if mse<best_mse:
best_mse = mse
best_feature = feature
best_threshold = value
return best_feature, best_threshold
def _mse_index(self, y_left, y_right):
# 计算左右子节点的均方误差
def mse(y):
return np.mean((y - np.mean(y))**2)
# 计算总的均方误差
total_samples = len(y_left)+len(y_right)
return (len(y_left)/total_samples)*mse(y_left)+(len(y_right)/total_samples)*mse(y_right)
def predict(self, X):
# 预测数据
y_pred = []
for x in X:
node = self.tree
while isinstance(node, dict):
if x[node['feature']]<=node['threshold']:
node = node['left']
else:
node = node['right']
y_pred.append(node)
return np.array(y_pred)
# 使用自定义 CART 决策树回归器
my_reg = MyDecisionTreeRegressor(max_depth=5)
my_reg.fit(X_train, y_train)
my_y_pred = my_reg.predict(X_test)
my_mse = mean_squared_error(y_test, my_y_pred)
print('自定义回归器均方误差:', my_mse)
四、CART 算法的优势与局限性
(一)优势
- 易于理解和解释:决策树的结构直观,能够清晰地展示数据的分类或回归规则,容易被非专业人士理解。
- 处理混合数据类型:可以处理数值型和分类型数据,无需对数据进行特殊的预处理。
- 特征选择与建模一体:在构建树的过程中自动进行特征选择,不需要单独的特征工程步骤来确定重要特征。
(二)局限性
- 容易过拟合:如果不进行剪枝或其他正则化处理,决策树可能会过度拟合训练数据,导致在测试数据上的性能下降。
- 不稳定:数据的微小变化可能导致构建出完全不同的决策树结构,因为在特征选择时可能会选择不同的特征作为最优特征。
- 对缺失数据敏感:在处理缺失数据时需要额外的处理策略,否则可能会影响树的构建和预测准确性。
五、CART 算法的实际应用
- 金融领域:用于信用风险评估,根据客户的各种特征(如收入、信用记录、年龄等)构建决策树模型来判断客户的信用风险等级,决定是否发放贷款以及贷款额度等。
- 医疗领域:在疾病诊断中,根据患者的症状、检查结果等数据构建决策树模型,辅助医生进行疾病的诊断和治疗方案的制定。
- 市场营销:对客户进行分类,如根据客户的消费行为、偏好、人口统计学特征等将客户分为不同的群体,以便制定针对性的营销策略。
六、结论
CART 决策树算法以其独特的特征选择标准、树构建过程和剪枝策略在分类与回归任务中发挥着重要作用。通过详细的原理阐述和丰富的代码示例,我们可以看到其在实际应用中的具体操作和效果。尽管存在一些局限性,但通过适当的改进措施(如剪枝、集成学习等)可以在很大程度上克服。在众多领域的广泛应用也证明了 CART 决策树算法的实用性和价值,在未来的数据挖掘和机器学习领域仍将是一种重要的基础算法,并不断与其他技术相结合以适应更复杂的数据分析需求。