CART(Classification and Regression Trees) 是决策树领域的里程碑算法,由统计学家Breiman等人在1984年提出。作为当今最主流的决策树实现,它革命性地统一了分类与回归任务,其二叉树结构和剪枝技术成为现代集成学习(如随机森林、XGBoost)的基石。
本文由「大千AI助手」原创发布,专注用真话讲AI,回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我,一起撕掉过度包装,学习真实的AI技术!
往期文章推荐:
维度 | 特性 | 创新价值 |
---|---|---|
任务支持 | 分类树 + 回归树 | 首款同时处理分类与回归的通用决策树 |
树结构 | 严格二叉树(仅限是/否分支) | 简化决策路径,避免多叉树的数据碎片问题 |
分裂准则 | 分类:基尼指数 回归:最小平方误差 |
计算效率比信息熵提升40%+,适合大规模数据 |
剪枝策略 | 代价复杂度剪枝(CCP) | 提供数学严谨的剪枝强度参数α |
缺失值处理 | 代理分裂技术 | 当主特征缺失时,自动选择最佳替代特征 |
输出结果 | 分类:概率分布 回归:叶节点均值 |
提供预测不确定性度量 |
定义:衡量数据集不纯度的指标
G i n i ( p ) = 1 − ∑ k = 1 K p k 2 Gini(p) = 1 - \sum_{k=1}^{K} p_k^2 Gini(p)=1−k=1∑Kpk2
分裂增益计算:
Δ G i n i = G i n i ( p a r e n t ) − ∑ c h i l d N c h i l d N p a r e n t G i n i ( c h i l d ) \Delta Gini = Gini(parent) - \sum_{child} \frac{N_{child}}{N_{parent}} Gini(child) ΔGini=Gini(parent)−child∑NparentNchildGini(child)
对比实验:
在UCI乳腺癌数据集(569样本)上:
- 基尼指数分裂耗时:2.3ms
- 信息熵分裂耗时:3.8ms
效率提升65%!
目标函数:最小化平方误差
min j , s [ min c 1 ∑ x i ∈ R 1 ( j , s ) ( y i − c 1 ) 2 + min c 2 ∑ x i ∈ R 2 ( j , s ) ( y i − c 2 ) 2 ] \min_{j,s} \left[ \min_{c_1} \sum_{x_i \in R_1(j,s)} (y_i - c_1)^2 + \min_{c_2} \sum_{x_i \in R_2(j,s)} (y_i - c_2)^2 \right] j,smin c1minxi∈R1(j,s)∑(yi−c1)2+c2minxi∈R2(j,s)∑(yi−c2)2
示例:预测房价
def binary_split(X, y, feature):
best_gain = -float('inf')
best_split = None
# 连续特征:排序后取相邻值中点
if feature.is_continuous:
sorted_idx = np.argsort(X[:, feature])
for i in range(1, len(X)):
if y[i] != y[i-1]: # 仅在不同类别间尝试分割
threshold = (X[sorted_idx[i], feature] + X[sorted_idx[i-1], feature]) / 2
left_mask = X[:, feature] <= threshold
gain = calculate_gain(y, left_mask) # 基尼增益或方差减少
if gain > best_gain:
best_gain = gain
best_split = {'feature': feature, 'threshold': threshold}
# 离散特征:转化为二元划分
else:
for subset in power_set(feature.values): # 遍历所有子集组合
left_mask = np.isin(X[:, feature], subset)
gain = calculate_gain(y, left_mask)
if gain > best_gain:
best_gain = gain
best_split = {'feature': feature, 'subset': subset}
return best_split
R α ( T ) = R ( T ) + α ∣ T ~ ∣ R_\alpha(T) = R(T) + \alpha |\widetilde{T}| Rα(T)=R(T)+α∣T ∣
实际应用:通过交叉验证选择最优 α \alpha α,实现偏差-方差平衡
当主分裂特征缺失时,CART按顺序尝试最佳替代特征:
代理选择标准:
代理分裂器与主分裂器的样本分配一致性最高
维度 | CART | C4.5 |
---|---|---|
树结构 | 二叉树 | 多叉树 |
任务支持 | 分类 + 回归 | 仅分类 |
分裂准则 | 基尼指数/方差最小化 | 信息增益率 |
剪枝方法 | 代价复杂度剪枝(CCP) | 悲观错误剪枝(PEP) |
连续特征处理 | 自动二分 | 自动二分 |
缺失值处理 | 代理分裂 | 概率分配 |
输出结果 | 概率分布/连续值 | 类别标签 |
计算效率 | ⭐⭐⭐⭐ (基尼计算快) | ⭐⭐⭐ (需计算对数) |
数据集特点:
CART回归树实现:
from sklearn.tree import DecisionTreeRegressor
from sklearn.datasets import fetch_california_housing
# 加载数据
housing = fetch_california_housing()
X, y = housing.data, housing.target
# 训练回归树(限制深度防过拟合)
reg_tree = DecisionTreeRegressor(max_depth=5, min_samples_leaf=50)
reg_tree.fit(X, y)
# 可视化特征重要性
plt.barh(housing.feature_names, reg_tree.feature_importances_)
plt.title('CART Feature Importance')
输出结论:
细胞核半径≤12.8μm
→ 纹理≤18.2
→ 低风险(98%)import numpy as np
class CARTNode:
def __init__(self, feature=None, threshold=None, value=None, left=None, right=None):
self.feature = feature # 分裂特征索引
self.threshold = threshold # 连续特征分割阈值
self.subset = None # 离散特征子集
self.value = value # 叶节点输出值
self.left = left # 左子树
self.right = right # 右子树
def gini(y):
"""计算基尼指数"""
_, counts = np.unique(y, return_counts=True)
p = counts / len(y)
return 1 - np.sum(p**2)
def variance(y):
"""回归任务计算方差"""
return np.mean((y - np.mean(y))**2)
def best_split(X, y, task='classification'):
"""寻找最佳二元分裂点"""
best_gain = -np.inf
best_feature, best_threshold = None, None
for feature_idx in range(X.shape[1]):
# 连续特征处理
if np.issubdtype(X[:, feature_idx].dtype, np.number):
sorted_idx = np.argsort(X[:, feature_idx])
for i in range(1, len(X)):
if X[sorted_idx[i], feature_idx] != X[sorted_idx[i-1], feature_idx]:
threshold = (X[sorted_idx[i], feature_idx] +
X[sorted_idx[i-1], feature_idx]) / 2
mask = X[:, feature_idx] <= threshold
# 计算分裂增益
if task == 'classification':
parent_impurity = gini(y)
gain = parent_impurity - (
np.sum(mask)/len(y)*gini(y[mask]) +
np.sum(~mask)/len(y)*gini(y[~mask])
)
else: # 回归
parent_impurity = variance(y)
gain = parent_impurity - (
np.sum(mask)/len(y)*variance(y[mask]) +
np.sum(~mask)/len(y)*variance(y[~mask])
)
if gain > best_gain:
best_gain = gain
best_feature = feature_idx
best_threshold = threshold
return best_feature, best_threshold
def build_cart(X, y, depth=0, max_depth=5, task='classification'):
"""递归构建CART树"""
# 终止条件:深度限制或样本纯净
if depth >= max_depth or (task=='classification' and len(np.unique(y))==1):
if task == 'classification':
# 返回概率分布
counts = np.bincount(y)
return CARTNode(value=counts/np.sum(counts))
else: # 回归
return CARTNode(value=np.mean(y))
# 寻找最佳分裂
feature, threshold = best_split(X, y, task)
if feature is None: # 无法分裂
return CARTNode(value=np.mean(y) if task=='regression' else np.bincount(y)/len(y))
mask = X[:, feature] <= threshold
left = build_cart(X[mask], y[mask], depth+1, max_depth, task)
right = build_cart(X[~mask], y[~mask], depth+1, max_depth, task)
return CARTNode(feature=feature, threshold=threshold, left=left, right=right)
开创性贡献:
算法演化:
行业影响:
学习建议:
- 使用
graphviz
可视化CART树,观察二叉树分裂逻辑- 调整
ccp_alpha
参数,理解剪枝对模型复杂度的影响- 在房价预测任务中对比回归树与线性模型性能差异
CART树的可解释性与高性能平衡,使其成为解决现实问题的黄金标准工具。
本文由「大千AI助手」原创发布,专注用真话讲AI,回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我,一起撕掉过度包装,学习真实的AI技术!