CART回归决策树实战:解决连续值预测的模型选择难题

1次阅读
没有评论

共计 3140 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

问题定义:为什么需要回归树?

在预测房屋价格、销售额等连续值时,很多工程师第一反应是用线性回归。但现实数据往往长这样:

CART 回归决策树实战:解决连续值预测的模型选择难题

  • 特征间存在复杂交互(如房价 = 地段×面积×楼层)
  • 存在明显阈值效应(如降雨量超过 50mm 时农作物产量骤降)
  • 不同区间的规律完全不同(如短视频观看时长对留存率的影响)

这时候如果用线性回归强行拟合,就像用直尺画曲线——效果可想而知。而 CART 回归树的优势在于:

  1. 自动捕捉数据中的非线性关系
  2. 通过树结构直观展现决策逻辑
  3. 不需要人工设计特征交叉项

算法核心:平方误差最小化

回归树的每个节点分裂时,会遍历所有特征和可能的切分点,选择使平方误差减少最多的方案。具体来说:

  1. 对于当前节点数据集 D,计算目标值的均值作为预测值:
    $$ \hat{y} = \frac{1}{|D|} \sum_{i \in D} y_i $$

  2. 对每个特征 j 和切分点 s,将数据分为左右子树:
    $$ D_L = {x|x_j \leq s}, D_R = {x|x_j > s} $$

  3. 找到使左右子树平方误差和最小的 (j,s) 组合:
    $$ \min_{j,s} \left[\sum_{i \in D_L} (y_i – \hat{y}L)^2 + \sum_R)^2 \right] $$} (y_i – \hat{y

终止条件通常包括:
– 节点样本数少于 min_samples_leaf
– 所有特征的分裂增益小于阈值
– 达到 max_depth 限制

手把手实现回归树

节点类定义

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, depth=0, max_depth=5, min_samples_leaf=5):
    # 终止条件判断
    if len(y) < min_samples_leaf or depth >= max_depth:
        return TreeNode(value=np.mean(y))

    best_error = float('inf')
    best_feature, best_thresh = None, None

    # 遍历所有特征和可能的分割点
    for feature_idx in range(X.shape[1]):
        thresholds = np.unique(X[:, feature_idx])
        for thresh in thresholds:
            # 计算分割后的平方误差
            left_idx = X[:, feature_idx] <= thresh
            y_left, y_right = y[left_idx], y[~left_idx]

            if len(y_left) == 0 or len(y_right) == 0:
                continue

            error = np.sum((y_left - np.mean(y_left))**2) + \
                    np.sum((y_right - np.mean(y_right))**2)

            # 更新最优分割
            if error < best_error:
                best_error = error
                best_feature = feature_idx
                best_thresh = thresh

    # 递归构建子树           
    if best_error < float('inf'):
        left_idx = X[:, best_feature] <= best_thresh
        left = build_tree(X[left_idx], y[left_idx], depth+1, max_depth, min_samples_leaf)
        right = build_tree(X[~left_idx], y[~left_idx], depth+1, max_depth, min_samples_leaf)
        return TreeNode(best_feature, best_thresh, left, right)

    return TreeNode(value=np.mean(y))

与 sklearn 的对比实验

from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error

# 我们的实现
tree = build_tree(X_train, y_train, max_depth=3)
preds_custom = [tree.predict(x) for x in X_test]

# sklearn 实现
sk_tree = DecisionTreeRegressor(max_depth=3)
sk_tree.fit(X_train, y_train)
preds_sk = sk_tree.predict(X_test)

print(f"Custom Tree RMSE: {np.sqrt(mean_squared_error(y_test, preds_custom)):.2f}")
print(f"Sklearn Tree RMSE: {np.sqrt(mean_squared_error(y_test, preds_sk)):.2f}")

生产环境关键考量

特征缩放真的不需要吗?

虽然树模型不依赖特征尺度,但在实践中:

  • 对连续特征做标准化能加速最优切分点搜索
  • 对类别特征建议使用均值编码(mean encoding)

过拟合控制三板斧

  1. 绘制学习曲线确定最佳深度:

    depths = range(1, 10)
    train_scores, test_scores = [], []
    
    for d in depths:
        model = DecisionTreeRegressor(max_depth=d)
        model.fit(X_train, y_train)
        train_scores.append(mean_squared_error(y_train, model.predict(X_train)))
        test_scores.append(mean_squared_error(y_test, model.predict(X_test)))
    
    plt.plot(depths, train_scores, label='Train')
    plt.plot(depths, test_scores, label='Test')
    plt.xlabel('Tree Depth')
    plt.ylabel('MSE')

  2. 使用 min_impurity_decrease 参数提前停止分裂

  3. 后剪枝(代价复杂度剪枝)

特征重要性可视化

importance = sk_tree.feature_importances_
plt.barh(range(len(importance)), importance, tick_label=feature_names)
plt.title('Feature Importance (Gini Decrease)')

避坑指南

  1. 类别特征处理:
  2. 回归树不能直接处理字符串特征
  3. 优先使用均值编码而非 one-hot(避免产生稀疏分裂)

  4. 缺失值处理:

  5. sklearn 默认将缺失值分到增益更大的方向
  6. 也可单独作为特殊值处理(如 XGBoost 的默认缺失方向)

  7. 随机性控制:

  8. 设置 random_state 保证可复现性
  9. 对数据敏感时可增加 bagging(随机森林)

延伸思考

  1. 如何将单棵树扩展为梯度提升树(GBDT)?
  2. 在哪些场景下回归树会比神经网络更合适?
  3. 如何利用回归树做异常值检测?

通过这篇笔记,希望你能感受到:回归树就像数据界的瑞士军刀——简单但实用,是每个 ML 工程师工具箱里的必备品。

正文完
 0
评论(没有评论)