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

1次阅读
没有评论

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

image.webp

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

在预测房价、销售额等连续值时,线性回归常因两点陷入困境:

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

  • 无法捕捉特征间的交互作用(如 ” 面积 * 学区 ” 对房价的复合影响)
  • 对非线性关系表达能力弱(如房价与区位呈分段函数关系)

而 CART 回归树通过递归划分特征空间,能自动发现这些复杂模式。举个例子:当预测用户付费金额时,可能发现 ” 年龄 >30 且使用频次 >5 次 / 周 ” 的用户群有显著更高的付费值——这正是回归树的优势所在。

算法核心:从数学到代码

分裂准则:最小化平方误差

回归树每次分裂时选择使左右子树 MSE(均方误差)加权和最小的特征 j 和阈值 s:

\min_{j,s} \left[\sum_{x_i \in R_1(j,s)}(y_i - \hat{y}_{R_1})^2 + \sum_{x_i \in R_2(j,s)}(y_i - \hat{y}_{R_2})^2 \right]

其中 $\hat{y}_{R}$ 是区域 R 内样本的目标均值。这个贪婪策略保证了局部最优性。

Python 实现关键步骤

1. 定义树节点结构

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              # 叶节点预测值

2. 递归建树函数(带预剪枝)

def build_tree(X, y, max_depth=5, min_samples_split=2):
    # 终止条件 1:样本数不足或达到最大深度
    if len(y) < min_samples_split or max_depth == 0:
        return TreeNode(value=np.mean(y))

    # 寻找最佳分裂
    best_mse = float('inf')
    for feat_idx in range(X.shape[1]):
        thresholds = np.unique(X[:, feat_idx])
        for thresh in thresholds:
            left_mask = X[:, feat_idx] <= thresh
            y_left, y_right = y[left_mask], y[~left_mask]

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

            mse = (np.var(y_left)*len(y_left) + 
                   np.var(y_right)*len(y_right))

            if mse < best_mse:
                best_mse = mse
                best_feat = feat_idx
                best_thresh = thresh

    # 终止条件 2:无法降低 MSE
    if best_mse >= np.var(y) - 1e-7:
        return TreeNode(value=np.mean(y))

    # 递归分裂
    left_mask = X[:, best_feat] <= best_thresh
    left = build_tree(X[left_mask], y[left_mask], 
                     max_depth-1, min_samples_split)
    right = build_tree(X[~left_mask], y[~left_mask],
                      max_depth-1, min_samples_split)

    return TreeNode(best_feat, best_thresh, left, right)

3. 预测函数示例

def predict_tree(node, x):
    if node.value is not None:
        return node.value

    if x[node.feature_idx] <= node.threshold:
        return predict_tree(node.left, x)
    else:
        return predict_tree(node.right, x)

生产环境实战要点

特征缩放:树模型不需要?

与线性模型不同,回归树的决策边界基于特征值比较,因此:

  • 无需标准化 / 归一化(分裂阈值会自动适应原始量纲)
  • 但类别特征仍需编码(建议 OrdinalEncoder 而非 One-Hot)

过拟合控制:学习曲线绘制

from sklearn.model_selection import learning_curve
import matplotlib.pyplot as plt

train_sizes, train_scores, test_scores = learning_curve(DecisionTreeRegressor(max_depth=3),
    X, y, cv=5, scoring='neg_mean_squared_error'
)

plt.plot(train_sizes, -train_scores.mean(axis=1), label='Train')
plt.plot(train_sizes, -test_scores.mean(axis=1), label='Test')
plt.xlabel('Training samples')
plt.ylabel('MSE')
plt.legend()

特征重要性可视化

model = DecisionTreeRegressor().fit(X, y)
importances = model.feature_importances_

plt.barh(range(X.shape[1]), importances)
plt.yticks(range(X.shape[1]), feature_names)
plt.xlabel('Gini Importance')

避坑经验总结

  1. 类别特征处理
  2. 回归树直接支持数值型类别编码(如将 ” 高中 / 本科 / 硕士 ” 映射为 1 /2/3)
  3. 避免使用 One-Hot 编码(会导致特征空间膨胀)

  4. 缺失值应对

  5. 训练时:将缺失值单独作为一类分裂条件
  6. 预测时:默认走右子树或按训练数据比例随机选择方向

  7. 稳定性保障

  8. 设置固定随机种子(np.random.seed(42)
  9. 增加 min_samples_leaf 参数(建议≥5)

延伸思考

  1. 如何修改当前实现使其支持梯度提升树(GBDT)?
  2. 提示:将预测值改为残差拟合,实现加法模型

  3. 当特征维度极高时(如 >1000),如何优化分裂点查找效率?

  4. 提示:考虑特征采样或近似分位数查找

  5. 回归树如何与神经网络结合发挥各自优势?

  6. 提示:研究 NODE(Neural Oblivious Decision Ensembles)架构
正文完
 0
评论(没有评论)