共计 3683 个字符,预计需要花费 10 分钟才能阅读完成。
决策树算法基础
决策树是机器学习中最直观的算法之一,它通过一系列规则对数据进行分类或回归。CART(Classification and Regression Trees)是其中最经典的算法,由 Breiman 等人于 1984 年提出。与 ID3 和 C4.5 不同,CART 可以同时处理分类和回归问题,并且总是生成二叉树。

基尼系数 vs 信息熵
决策树的核心在于如何选择最优划分特征,CART 使用基尼系数(Gini Index)作为划分标准:
$$\text{Gini}(D) = 1 – \sum_{k=1}^{K} (\frac{|C_k|}{|D|})^2$$
其中 $D$ 是当前数据集,$C_k$ 是第 $k$ 类的样本子集。基尼系数反映了从数据集中随机抽取两个样本,其类别不一致的概率。与信息熵相比:
- 基尼系数计算更快(无对数运算)
- 两者在实际效果上非常接近
- ID3/C4.5 只能用于分类任务,而 CART 可用于回归(使用平方误差)
特征选择与实现
伪代码描述
function find_best_split(data):
best_gini = ∞
best_feature = None
best_value = None
for feature in features:
if feature is continuous:
values = get_sorted_unique_values(data[feature])
for i in range(len(values)-1):
threshold = (values[i] + values[i+1])/2
left, right = split_data(data, feature, threshold)
current_gini = weighted_gini(left, right)
if current_gini < best_gini:
update_best_parameters()
else:
# 处理离散特征类似
return best_feature, best_value
Python 实现关键部分
import numpy as np
def calc_gini(y):
_, counts = np.unique(y, return_counts=True)
probabilities = counts / len(y)
return 1 - np.sum(probabilities**2)
def find_best_split(X, y):
best_gini = float('inf')
best_feature = None
best_threshold = 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
if len(y[left_mask]) == 0 or len(y[right_mask]) == 0:
continue
gini_left = calc_gini(y[left_mask])
gini_right = calc_gini(y[right_mask])
weighted_gini = (len(y[left_mask]) * gini_left +
len(y[right_mask]) * gini_right) / len(y)
if weighted_gini < best_gini:
best_gini = weighted_gini
best_feature = feature_idx
best_threshold = threshold
return best_feature, best_threshold
连续特征处理与剪枝
连续值特征二分法
对于连续特征,CART 采用二分法:
- 对特征值排序
- 取相邻值的中间点作为候选切分点
- 选择使基尼系数最小的切分点
这种方法比多分法效率更高,且在实践中效果良好。
剪枝策略
过拟合是决策树的主要问题,解决方法包括:
- 预剪枝 :在构建过程中提前停止
- 最大深度限制
- 最小样本分割数
-
基尼系数提升阈值
-
后剪枝 :先构建完整树再修剪
- 代价复杂度剪枝(CCP)
$$\alpha = \frac{R(t) – R(T_t)}{|T_t| – 1}$$
其中 $R(t)$ 是节点 $t$ 的误差,$T_t$ 是以 $t$ 为根的子树
完整实现示例
class TreeNode:
def __init__(self, feature_idx=None, threshold=None, left=None, right=None, value=None):
self.feature_idx = feature_idx # 分裂特征索引
self.threshold = threshold # 分裂阈值
self.left = left # 左子树
self.right = right # 右子树
self.value = value # 叶节点的预测值
def build_tree(X, y, max_depth=5, min_samples_split=2):
# 终止条件
if len(y) < min_samples_split or max_depth == 0 or len(np.unique(y)) == 1:
return TreeNode(value=np.argmax(np.bincount(y)))
# 寻找最佳分裂
feature_idx, threshold = find_best_split(X, y)
if feature_idx is None:
return TreeNode(value=np.argmax(np.bincount(y)))
# 递归构建子树
left_mask = X[:, feature_idx] <= threshold
right_mask = ~left_mask
left = build_tree(X[left_mask], y[left_mask], max_depth-1, min_samples_split)
right = build_tree(X[right_mask], y[right_mask], max_depth-1, min_samples_split)
return TreeNode(feature_idx=feature_idx, threshold=threshold, left=left, right=right)
def predict(node, x):
if node.value is not None:
return node.value
if x[node.feature_idx] <= node.threshold:
return predict(node.left, x)
else:
return predict(node.right, x)
生产环境注意事项
特征重要性评估
通过计算每个特征在分裂时的基尼系数下降总量来评估重要性:
def feature_importance(tree, X, y):
importance = np.zeros(X.shape[1])
def traverse(node):
if node.feature_idx is not None:
left_mask = X[:, node.feature_idx] <= node.threshold
gini_parent = calc_gini(y)
gini_left = calc_gini(y[left_mask])
gini_right = calc_gini(y[~left_mask])
improvement = gini_parent - (len(y[left_mask])/len(y) * gini_left +
len(y[~left_mask])/len(y) * gini_right
)
importance[node.feature_idx] += improvement
traverse(node.left)
traverse(node.right)
traverse(tree)
return importance / np.sum(importance)
防止过拟合技巧
- 预剪枝 :
- 限制 max_depth=3-5
- 设置 min_samples_leaf=10
-
要求 gini 减少量 >0.01
-
后剪枝 :
- 使用交叉验证选择最优 alpha
-
计算复杂度路径后选择子树
-
类别不平衡处理 :
- 使用 class_weight 参数
- 对少数类样本上采样
- 修改分裂标准,如平衡基尼系数
思考题
- 如何扩展算法支持多输出任务?
- 可以修改基尼系数计算方式,考虑多标签的纯度
-
或者在每个节点为每个输出构建单独的分裂标准
-
对比随机森林中 CART 树的差异
- 随机森林中的树通常生长得更深(不剪枝)
- 每棵树只使用部分特征和样本
-
最终通过投票 / 平均获得结果
-
解释 CART 树与 GBDT 的关系
- GBDT 使用 CART 作为基学习器
- 但 GBDT 中的树是浅层的(通常 max_depth=3-6)
- GBDT 通过梯度下降逐步改进预测
总结
CART 决策树以其简单直观、可解释性强著称,是理解更复杂算法(如随机森林、GBDT)的基础。实现过程中需要注意:
- 连续特征的特殊处理
- 合理的终止条件设置
- 剪枝策略的选择
建议读者尝试在真实数据集上运行代码,观察不同参数对模型性能的影响。
正文完
