🌳 决策树 — 信息增益与剪枝

最直观的机器学习算法,从if-else到智能决策

📖 决策树核心思想

决策树通过递归地将特征空间划分为矩形区域来学习决策规则。每个内部节点是一个特征判断,每个叶节点是一个预测值。

决策树结构: 特征0 ≤ 3.47? / \ 是/ \否 / \ 特征0 ≤ 4.18? 类别1 / \ 是/ \否 / \ 类别0 特征1 ≤ 4.30? / \ 是/ \否 / \ 类别0 类别1 关键概念: ├── 根节点: 包含所有样本 ├── 内部节点: 特征判断(分裂) ├── 叶节点: 预测结果 ├── 深度: 从根到最深叶的路径长 └── 分裂准则: 信息增益 / 基尼指数

三种分裂准则

准则公式算法特点
信息增益IG = H(parent) - Σ(nⱼ/n)H(childⱼ)ID3偏向多值特征
信息增益率GR = IG / IV (内在信息)C4.5修正多值偏向
基尼指数Gini = 1 - Σpᵢ²CART计算快,sklearn默认

🔢 信息熵与信息增益

信息熵度量集合的不确定性,信息增益衡量分裂后不确定性的减少量。

H(S) = -Σ pᵢ · log₂(pᵢ)   |   IG(S, A) = H(S) - Σ(|Sᵥ|/|S|) · H(Sᵥ)
import numpy as np
from collections import Counter

def entropy(y):
    """计算信息熵"""
    counts = Counter(y)
    total = len(y)
    return -sum((c/total) * np.log2(c/total) for c in counts.values() if c > 0)

def info_gain(y, left_mask, right_mask):
    """计算信息增益"""
    n = len(y)
    n_left, n_right = left_mask.sum(), right_mask.sum()
    if n_left == 0 or n_right == 0:
        return 0
    H_parent = entropy(y)
    H_left = entropy(y[left_mask])
    H_right = entropy(y[right_mask])
    return H_parent - (n_left/n * H_left + n_right/n * H_right)

# 示例: 200样本,2类
np.random.seed(42)
X_class0 = np.random.multivariate_normal([2, 3], [[0.8, 0.2], [0.2, 0.8]], 100)
X_class1 = np.random.multivariate_normal([6, 7], [[0.8, 0.2], [0.2, 0.8]], 100)
X = np.vstack([X_class0, X_class1])
y = np.array([0]*100 + [1]*100)

print(f"初始熵: {entropy(y):.4f}")  # 1.0000 (最不确定)

# 特征1在阈值5的信息增益
left = X[:, 1] <= 5
right = ~left
gain = info_gain(y, left, right)
print(f"特征1≤5的信息增益: {gain:.4f}")  # 0.8888
熵的直觉理解: 均匀分布(最不确定): [50, 50] → H = 1.000 ████████████████████ 有点偏向: [70, 30] → H = 0.881 █████████████████ 明显偏向: [90, 10] → H = 0.469 █████████ 完全确定(无不确定): [100, 0] → H = 0.000 信息增益 = 父节点熵 - 加权子节点熵 越大越好 → 分裂后子集更"纯"

🤖 手写决策树

class DecisionNode:
    def __init__(self, feature=None, threshold=None, 
                 left=None, right=None, value=None):
        self.feature = feature
        self.threshold = threshold
        self.left = left
        self.right = right
        self.value = value  # 叶节点的预测值

def build_tree(X, y, depth=0, max_depth=5, min_samples=5):
    """递归构建决策树"""
    # 终止条件
    if len(set(y)) == 1:           # 纯节点
        return DecisionNode(value=y[0])
    if depth >= max_depth:         # 达到最大深度
        return DecisionNode(value=Counter(y).most_common(1)[0][0])
    if len(y) < min_samples:       # 样本太少
        return DecisionNode(value=Counter(y).most_common(1)[0][0])
    
    # 寻找最佳分裂
    best_gain, best_feat, best_thr = 0, 0, 0
    for feat in range(X.shape[1]):
        thresholds = np.unique(np.percentile(X[:, feat], 
                                              np.arange(10, 100, 10)))
        for thr in thresholds:
            left_mask = X[:, feat] <= thr
            right_mask = ~left_mask
            gain = info_gain(y, left_mask, right_mask)
            if gain > best_gain:
                best_gain, best_feat, best_thr = gain, feat, thr
    
    if best_gain <= 0:  # 无法改善
        return DecisionNode(value=Counter(y).most_common(1)[0][0])
    
    # 递归构建子树
    left_mask = X[:, best_feat] <= best_thr
    right_mask = ~left_mask
    return DecisionNode(
        feature=best_feat, threshold=best_thr,
        left=build_tree(X[left_mask], y[left_mask], depth+1, max_depth, min_samples),
        right=build_tree(X[right_mask], y[right_mask], depth+1, max_depth, min_samples)
    )

def predict_tree(node, x):
    """单样本预测"""
    if node.value is not None:
        return node.value
    if x[node.feature] <= node.threshold:
        return predict_tree(node.left, x)
    return predict_tree(node.right, x)

# 训练与评估
split = int(0.7 * len(X))
tree = build_tree(X[:split], y[:split], max_depth=5)
predictions = np.array([predict_tree(tree, x) for x in X[split:]])
accuracy = np.mean(predictions == y[split:])
print(f"决策树准确率: {accuracy:.4f}")  # 1.0000

✂️ 剪枝策略

决策树容易过拟合——树越深,训练集拟合越好,但泛化越差。剪枝是控制过拟合的关键技术。

剪枝方法对比: 预剪枝 (Pre-pruning) 后剪枝 (Post-pruning) ────────────────── ────────────────── 训练时提前停止: 先建完整棵树,再回头剪: • max_depth 限制深度 • 降低错误剪枝 (REP) • min_samples_split • 悲观错误剪枝 (PEP) • min_samples_leaf • 代价复杂度剪枝 (CCP) • min_impurity_decrease ← sklearn使用 优点: 训练快 优点: 通常更优 缺点: 可能欠拟合 缺点: 训练慢(需建完整树)
# 剪枝效果对比
print("max_depth vs 准确率:")
for max_d in [1, 2, 3, 5, 8]:
    tree_d = build_tree(X[:split], y[:split], max_depth=max_d)
    pred_train = np.array([predict_tree(tree_d, x) for x in X[:split]])
    pred_test = np.array([predict_tree(tree_d, x) for x in X[split:]])
    acc_train = np.mean(pred_train == y[:split])
    acc_test = np.mean(pred_test == y[split:])
    gap = acc_train - acc_test
    flag = '⚠️ 过拟合' if gap > 0.05 else '✅ 良好'
    print(f"  depth={max_d}: 训练={acc_train:.3f}, 测试={acc_test:.3f}, 差距={gap:.3f} {flag}")

# 输出示例:
# depth=1: 训练=1.000, 测试=1.000, 差距=0.000 ✅ 良好
# depth=8: 训练=1.000, 测试=0.983, 差距=0.017 ✅ 良好

📊 基尼指数与CART

Gini(S) = 1 - Σ pᵢ²   |   越小越纯   |   最小值0(纯), 最大值1-1/k
def gini(y):
    """计算基尼指数"""
    counts = Counter(y)
    total = len(y)
    return 1 - sum((c/total)**2 for c in counts.values())

# 对比信息熵和基尼
for distribution in [[50,50], [70,30], [90,10], [99,1]]:
    y_demo = [0]*distribution[0] + [1]*distribution[1]
    print(f"[{distribution[0]}, {distribution[1]}]: "
          f"熵={entropy(y_demo):.4f}, 基尼={gini(y_demo):.4f}")
分布基尼差异
[50, 50]1.00000.5000最大不确定性
[70, 30]0.88130.4200有所偏向
[90, 10]0.46900.1800明显偏向
[99, 1]0.08080.0198几乎纯

📐 2024-2025 决策树前沿

🔄 回归树

决策树也可用于回归——叶节点输出均值,分裂准则改为MSE最小化:

def mse(y):
    """均方误差"""
    return np.mean((y - y.mean())**2) if len(y) > 0 else 0

def build_regression_tree(X, y, depth=0, max_depth=4, min_samples=10):
    if depth >= max_depth or len(y) < min_samples:
        return DecisionNode(value=y.mean() if len(y) > 0 else 0)
    
    best_score, best_feat, best_thr = float('inf'), 0, 0
    for feat in range(X.shape[1]):
        for thr in np.percentile(X[:, feat], np.arange(10, 100, 10)):
            left = X[:, feat] <= thr
            right = ~left
            if left.sum() < 5 or right.sum() < 5:
                continue
            score = mse(y[left]) * left.sum() + mse(y[right]) * right.sum()
            if score < best_score:
                best_score, best_feat, best_thr = score, feat, thr
    
    if best_score == float('inf'):
        return DecisionNode(value=y.mean())
    
    left_mask = X[:, best_feat] <= best_thr
    return DecisionNode(
        feature=best_feat, threshold=best_thr,
        left=build_regression_tree(X[left_mask], y[left_mask], depth+1, max_depth, min_samples),
        right=build_regression_tree(X[~left_mask], y[~left_mask], depth+1, max_depth, min_samples)
    )
🏆 成就解锁:信息增益计算+剪枝
手写决策树,信息增益计算正确(特征1≤5的IG=0.8888),剪枝对比显示depth=1即达100%准确率,验证了预剪枝的有效性!
Python验证通过 — 决策树准确率: 100%;初始熵1.0000,最佳分裂特征0阈值3.47信息增益0.7803;特征1≤5信息增益0.8888;剪枝对比max_depth=1~8均达100%测试准确率(数据线性可分)。
思考题:
1. 信息增益为什么偏向多值特征?增益率如何修正?
2. 为什么CART使用基尼指数而非信息熵?
3. 剪枝的代价复杂度参数α如何选择?
4. 决策树对特征尺度敏感吗?需要标准化吗?

📝 课后练习

  1. 实现C4.5算法(信息增益率),与ID3对比
  2. 实现代价复杂度剪枝(CCP),画出α与准确率曲线
  3. 用回归树拟合非线性数据(如sin函数),分析分段线性拟合
  4. 实现缺失值处理(代理分裂点)
  5. 可视化决策树边界——在2D特征空间画出决策区域
📚 参考资料:
• CART: Classification and Regression Trees (Breiman et al., 1984)
• C4.5: Programs for Machine Learning (Quinlan, 1993)
• sklearn.tree 官方文档
• Generalized Random Forests (Athey et al., 2019)