资讯动态

回归树超参数调优实战:从CART原理到网格搜索

发布时间:2026/9/7 3:51:28 来源:尧图企业网站定制
当业务需要预测一个连续型数值比如房价、销量、温度、设备寿命时很多人第一反应就是上线性回归。但真实数据往往不是一条直线能拟合的特征之间可能存在明显的交互关系此时线性模型的偏差会很大。我也是在做了几个回归项目后发现决策树算法中的回归树是一个非常适合做“快速 baseline”的模型训练快、可解释、能处理非线性并且几乎不需要对数据做太多的标准化处理。网上关于决策树的资料很多但大部分集中在分类场景专门讲回归树超参数调优和代码示例的内容相对零散。本文将围绕回归树从概念、原理到超参数调优再到完整代码示例整理出一套可以照着做的实战笔记。如果你正准备用决策树算法做回归任务或者已经用 sklearn 的DecisionTreeRegressor跑出了结果但效果不理想这篇文章能帮助你理解回归树到底在做什么、哪些超参数会影响模型、怎么用网格搜索和随机搜索去找参数、以及调参之后怎么评估模型。下面我们直接进入主题。1. 决策树与回归树概念先行1.1 决策树算法在做什么决策树算法是一种监督学习方法它的核心思想非常朴素通过学习训练数据自动找到一系列“特征条件判断”然后把样本逐步划分成更小的、目标值或类别更一致的子集。最终得到的模型可以看成一组嵌套的 if-else 规则哪怕完全不了解算法细节的人也能看懂树的结构。例如在回归场景中模型可能学出这样的规则如果“房间数量”大于等于 5 且“地理位置”属于 A 区那么预估房价为 350 万。如果“房间数量”大于等于 5 且“地理位置”不属于 A 区那么预估房价为 280 万。如果“房间数量”小于 5那么继续看“面积”是否大于 80 平米再细分。这种规则并不是人工写出来的而是算法自动从数据中选择特征和划分阈值。正因为决策树算法具备这种“自动生成规则”的能力它在中小型数据集上经常能取得不错的拟合效果并且结果容易向业务方解释。回归树属于决策树算法的一个分支专门用于目标变量为连续数值的任务。与线性回归不同回归树不需要假设特征与目标之间是线性关系它更像是一个分段常数模型整个特征空间被划分为若干矩形区域每个区域输出一个固定数值。后续的随机森林、GBDT、XGBoost 等集成模型本质上也是以回归树作为基础学习器所以理解单棵回归树是学习这些更强模型的前提。1.2 分类树与回归树的本质区别很多初学者分不清决策树算法中“分类树”和“回归树”的区别这里我们做一个明确的对比。分类树处理的是离散标签比如“垃圾邮件 / 正常邮件”“用户是否流失”“图片中是猫还是狗”。在划分节点时分类树追求的是划分后子节点类别纯度更高常用的指标有基尼不纯度Gini Impurity、信息增益Information Gain和信息增益比。最终叶节点的预测结果是该子区域内样本数量最多的类别或者各类别的概率分布。回归树处理的是连续数值比如“销售额”“温度”“点击率”。在划分节点时回归树不再看类别纯不纯而是看划分后的子区域中目标值的离散程度是不是变小了。最常用的指标是平方误差或均方误差MSE。如果一个节点划分之后两个子区域内的目标值都比之前更集中在平均值附近说明这次划分是有效的。最终叶节点的预测结果通常是该子区域内所有样本目标值的均值或中位数。从实现角度看分类树和回归树的整体流程非常相似差别主要在于“损失函数”和“叶节点输出方式”。在 scikit-learn 中分类树对应DecisionTreeClassifier回归树对应DecisionTreeRegressor。这两者在参数风格上高度一致但评价指标、叶节点预测值计算方式不同不能混用。1.3 回归树的常见应用场景回归树适合哪些场景呢根据我的实际经验下面几类情况很适合直接用回归树来做中小型数据集上的回归任务。比如几千条到几万条样本特征数量不算特别多回归树可以快速给出一个可解释的 baseline。特征与目标之间存在非线性关系。比如某个特征小时候目标值高、中间低、再往后又升高这种“非单调关系”用线性模型很难表达回归树可以自动分段。业务上需要解释预测逻辑。比如银行审批、医疗评分、工业检测场景决策树可以输出规则路径方便审计和业务理解。作为集成学习的基础模型。随机森林和 GBDT 内部都依赖大量回归树理解单棵树的参数含义才能更好地调整集成模型的超参数。快速验证特征有效性。在特征工程阶段可以先跑一棵回归树查看特征重要性快速判断哪些特征对目标值有贡献。当然回归树也有明显的弱点单棵树容易过拟合、对数据中的噪声敏感、训练集上的表现常常好得离谱但在测试集上大幅下跌。这也是为什么后面我们要重点讨论超参数调优和剪枝。2. CART 回归树的构建原理2.1 核心思想递归划分scikit-learn 中的决策树算法采用的是 CARTClassification And Regression Tree结构也就是二叉树。每个节点只做一次二元判断比如x1 0.5走左子树x1 0.5走右子树。递归地重复这个过程就能把特征空间逐步切分成多个小区域。下面用简单的数学方式描述一下回归树的构建过程并不复杂。假设当前节点有 (N) 个样本每个样本包含特征 (x (x_1, x_2, ..., x_p)) 和目标值 (y)。当前节点的预测值为该节点所有样本 (y) 的均值记为 (\bar{y})。节点的平方误差为[ \text{MSE} \frac{1}{N} \sum_{i1}^{N}(y_i - \bar{y})^2 ]现在我们要选择一个特征 (j) 和一个阈值 (s)把样本分成左右两个子集左子集(x_j \le s)右子集(x_j s)划分后新的总误差等于左右两个子节点误差的加权和。我们希望找到最能降低总误差的 ((j, s)) 组合。这个过程会递归执行直到满足停止条件比如树达到最大深度、节点样本数小于阈值或无法再找到有效划分。CART 之所以是二叉树而不是多叉树是因为二叉树在计算和存储上更高效而且可以通过多次划分表达任意的多分叉结构。例如“颜色是红、绿、蓝”三分类完全可以通过两次二分类判断来表达。2.2 特征选择为什么用方差而不是信息增益这是初学者最容易困惑的问题。既然决策树在分类任务中经常用信息增益、基尼不纯度来评估划分好坏为什么回归树不沿用这些指标原因很简单信息增益和基尼不纯度是针对“离散类别”设计的。指标计算时统计的是每个类别出现的频率而回归任务的目标值是一个连续数值可能每个样本的 (y) 都不同无法直接统计类别频率。因此回归树改用“方差”或“平方误差”来评价一次划分的好坏。直观理解是这样的如果划分后左右两个子组的 (y) 值都各自非常集中分别接近各自的均值说明这两个子组的样本在目标值上更“纯”。平方误差越小说明组内数值越集中。CART 回归树中常用的损失函数包括平方误差squared error最常用对异常值敏感。绝对误差absolute error更稳健对异常值不敏感。弗里德曼均方误差friedman mse对潜在划分质量做了修正在某些场景更高效。泊松偏差poisson适用于非负计数类目标值。在 scikit-learn 的DecisionTreeRegressor中默认的criterionsquared_error也就是平方误差。大多数情况下使用默认值即可不需要频繁改动。2.3 树结构如何决定预测值当一棵回归树训练完成后预测过程非常简单新样本从根节点出发根据每个节点的特征条件不断向下走最终落入某一个叶节点该叶节点保存的预测值就是输出结果。叶节点的预测值怎么确定呢默认情况下是训练时落入该叶节点的所有样本目标值的平均值。举个例子某个叶节点中有 3 个训练样本目标值分别是 10、12、16那么预测值就是[ \frac{10 12 16}{3} \approx 12.67 ]如果设定criterionabsolute_error则叶节点不再使用均值而是使用中位数作为预测值这样能减少异常值的影响。这种设计让回归树成为一个“分段常数函数”。特征空间被切分成多个矩形区域每个区域内预测值是个常数。这也解释了为什么回归树在训练集上可以做到非常低的误差只要树足够深理论上可以让每个叶节点只包含一个样本从而完全记住训练数据。但这种做法会带来严重的过拟合所以我们需要限制树的复杂度。2.4 回归树 vs 模型树 M5在机器学习领域除了我们常用的 CART 回归树还有一种经典模型叫“模型树”代表性算法是 Quinlan 提出的 M5。两者的区别很关键。CART 回归树的叶节点输出一个常数比如均值。整体模型呈阶梯状无法精确表达平滑的线性趋势。如果目标变量随某个特征连续变化回归树就需要把特征切成很多段用阶梯去逼近直线这样树会很大泛化能力也受影响。模型树 M5 的做法是在叶节点不再输出常数而是用落到该叶节点的训练样本拟合一个局部线性回归模型。这样每个叶节点都对应一条小直线整体模型是分段线性的能更平滑地表达连续变化。对于分段线性的数据模型树往往比常数型回归树更精确树也更小。不过 scikit-learn 官方并没有提供 M5 的现成实现。如果你想在项目中使用模型树需要自己实现或者寻找第三方库。在多数工程实践中CART 回归树已经足够作为 baseline后续如果精度不够更常见的做法是换用随机森林或 GBDT而不是直接上模型树。另外研究领域还有“模糊决策树算法”它在特征取值和节点划分中引入隶属度和模糊逻辑用于处理边界模糊的数据。模糊决策树通常不是工程首选但在某些特定场景下有研究价值。普通开发者先掌握标准回归树即可。3. 环境准备与示例数据3.1 运行环境本文的代码基于 Python 和 scikit-learn 实现。版本方面请根据你本机实际环境调整。我建议使用以下环境Python 3.9 或更高版本。scikit-learn 1.2 或更高版本。numpy、pandas 用于数组和表格操作。matplotlib 用于可视化。graphviz 可选如果只想用plot_tree画图不安装 graphviz 二进制也可以。如果你的环境比较新直接使用 pip 安装即可pip install scikit-learn matplotlib pandas numpy如果你使用 conda可以执行conda install scikit-learn matplotlib pandas numpy本文不依赖 GPU普通 CPU 就能运行。下面的示例中我会尽量使用通用 API避免写死某一个小版本特有的函数。3.2 生成示例回归数据为了便于演示我们不使用外部 CSV 文件而是采用 sklearn 自带的make_friedman1函数生成一组模拟回归数据。这个数据集是 Friedman 提出的回归测试问题特征之间存在非线性交互非常适合用来观察回归树的效果。代码如下import numpy as np import pandas as pd from sklearn.datasets import make_friedman1 X, y make_friedman1( n_samples500, n_features4, noise0.2, random_state42 ) df pd.DataFrame(X, columns[fx{i} for i in range(1, 5)]) df[target] y print(df.head())输出大概长这样x1 x2 x3 x4 target 0 0.020584 0.969910 0.832443 0.212339 15.537132 1 0.181875 0.183405 0.304242 0.524756 6.856813 2 0.963663 0.779598 0.715279 0.655099 16.249925 3 0.166510 0.531452 0.699770 0.368486 12.335115 4 0.816028 0.495453 0.711657 0.901029 13.750612make_friedman1会生成 5 个有效特征但我们这里设置n_features4只保留前 4 个特征方便后续查看特征重要性和可视化结果。noise0.2表示目标变量带有少量噪声让训练结果更接近真实场景。3.3 数据划分为了避免模型“记住”训练样本而没有泛化能力我们需要把数据集划分成训练集和测试集。这里使用 sklearn 的train_test_split训练集占比 80%测试集占比 20%。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) print(f训练集样本数: {X_train.shape[0]}) print(f测试集样本数: {X_test.shape[0]})输出训练集样本数: 400 测试集样本数: 100后面所有训练和评估都在这个划分基础上进行这样每次运行结果都是可复现的。如果你使用自己的业务数据只需要把X和y换成实际的特征矩阵和目标变量即可整体流程完全一样。4. 核心超参数详解DecisionTreeRegressor的超参数非常多如果毫无章法地乱试会很浪费时间。我习惯把这些参数分成三类控制树结构的参数、控制切分方式的参数、控制剪枝与正则化的参数。下面逐一说明。4.1 控制树结构的参数这类参数直接决定决策树长得多大、多复杂也是调参时最应该优先关注的。max_depth树的最大深度。如果不设置树会一直生长直到所有叶节点都足够“纯”或样本数过少。深度越大模型越容易过拟合。推荐在调参时从 3 到 15 之间尝试。min_samples_split内部节点再划分所需的最小样本数。如果节点样本数小于这个值就不再继续划分。默认值是 2也就是只要节点里还有至少 2 个样本且不纯就可以继续分裂。增大这个值可以限制树生长。min_samples_leaf叶节点最少样本数。它比min_samples_split对模型的影响更直接因为即使某个内部节点满足了分裂条件它分裂出的子节点也必须满足叶节点最小样本数。增大这个值可以明显降低过拟合。max_leaf_nodes最大叶节点数量。限制叶节点数量的上限相当于从整体上限约束树的复杂度。这个参数和max_depth二选一或组合使用都可以。从经验来看max_depth和min_samples_leaf是对模型表现影响最大的两个参数。大部分拟合不足的问题可以通过放宽深度解决大部分过拟合问题可以先尝试增大min_samples_leaf。4.2 控制切分方式的参数这类参数影响决策树在节点处如何选择特征和阈值。criterion划分质量的评价函数。默认是squared_error表示平方误差。也可以选择friedman_mse、absolute_error、poisson。大多数任务保持默认即可。splitter选择划分点的策略。默认best即在所有特征中寻找最优切分点改为random会在部分特征中随机选择切分点通常用于训练速度优化但效果不一定更好。max_features每次划分时最多考虑的特征数量。默认值为None也就是考虑全部特征。设置为较小的整数或比例可以引入随机性减少树之间的相关性在随机森林中更常用。对于单棵回归树通常保持默认不必刻意调小。random_state随机种子。如果你设置了splitterrandom或使用随机搜索固定随机种子能保证结果可复现。4.3 剪枝与正则化参数决策树训练完成后通常还应该考虑剪枝。最常用的参数是ccp_alpha它控制“成本复杂度剪枝”的强度。简单来说这个参数会在“树的总误差”和“树的大小”之间做一个权衡。sklearn 提供了一条很方便的路径调用cost_complexity_pruning_path获取一系列候选的ccp_alpha值然后通过交叉验证选择最优的一个。这样往往能找到一棵比默认模型更小、泛化能力更强的树。这个参数是非常重要的调优手段后面的实战部分会专门演示。另外min_weight_fraction_leaf也属于正则化类的参数它要求叶节点的样本权重之和占总权重的最小比例。如果设置了样本权重这个参数会有用普通场景下可以先忽略。4.4 超参数调优的一般顺序超参数之间存在相互影响逐个单独调往往不够准确但全组合网格搜索的计算开销又太大。我建议按下面的顺序来做先固定一个较大的深度范围用交叉验证找到max_depth的大致合理区间。保持这个深度再调min_samples_leaf和min_samples_split。检查是否还需要限制max_features如果特征很多可以考虑随机特征选择。最后用ccp_alpha做一次成本复杂度剪枝看能不能用更小的树达到接近的精度。把上面找到的参数合在一起做一个小范围的网格搜索或随机搜索。这样分步调参每一步调整都有参照物不会在参数空间里盲目乱撞。5. 回归树超参数调优实战5.1 构建基础回归树模型先训练一个默认参数的回归树作为后续调优的基线。默认参数下树的复杂度不受明显限制因此大概率会过拟合。from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error, r2_score # 默认参数模型 base_model DecisionTreeRegressor(random_state42) base_model.fit(X_train, y_train) y_train_pred base_model.predict(X_train) y_test_pred base_model.predict(X_test) train_rmse mean_squared_error(y_train, y_train_pred, squaredFalse) test_rmse mean_squared_error(y_test, y_test_pred, squaredFalse) train_r2 r2_score(y_train, y_train_pred) test_r2 r2_score(y_test, y_test_pred) print(f训练集 RMSE: {train_rmse:.4f}) print(f测试集 RMSE: {test_rmse:.4f}) print(f训练集 R2: {train_r2:.4f}) print(f测试集 R2: {test_r2:.4f}) print(f决策树深度: {base_model.get_depth()}) print(f叶节点数量: {base_model.get_n_leaves()})需要说明一下在 scikit-learn 1.4 及之后的版本中mean_squared_error的squared参数被改名但 1.2 到 1.3 版本仍兼容squaredFalse。如果你使用的是更新版本推荐使用root_mean_squared_error函数或者直接取均方误差的平方根train_rmse mean_squared_error(y_train, y_train_pred) ** 0.5 test_rmse mean_squared_error(y_test, y_test_pred) ** 0.5这段代码更稳定我们后面统一使用这种写法。也就是说base_model DecisionTreeRegressor(random_state42) base_model.fit(X_train, y_train) y_train_pred base_model.predict(X_train) y_test_pred base_model.predict(X_test) train_rmse mean_squared_error(y_train, y_train_pred) ** 0.5 test_rmse mean_squared_error(y_test, y_test_pred) ** 0.5 train_r2 r2_score(y_train, y_train_pred) test_r2 r2_score(y_test, y_test_pred) print(f训练集 RMSE: {train_rmse:.4f}) print(f测试集 RMSE: {test_rmse:.4f}) print(f训练集 R2: {train_r2:.4f}) print(f测试集 R2: {test_r2:.4f}) print(f决策树深度: {base_model.get_depth()}) print(f叶节点数量: {base_model.get_n_leaves()})运行后你会发现训练集 R2 非常高通常接近 1而测试集 R2 明显低不少。这就是过拟合的信号树把训练样本的噪声也记住了。此时不要急着调整参数先观察模型复杂度再逐步限制。5.2 观察 max_depth 对模型的影响max_depth是最直观的复杂度控制参数。我们可以做一个循环把max_depth从 1 递增到 10分别记录训练集和测试集的 RMSE然后画一条曲线观察变化趋势。import matplotlib.pyplot as plt train_scores [] test_scores [] depth_range range(1, 11) for depth in depth_range: model DecisionTreeRegressor( max_depthdepth, random_state42 ) model.fit(X_train, y_train) train_pred model.predict(X_train) test_pred model.predict(X_test) train_rmse mean_squared_error(y_train, train_pred) ** 0.5 test_rmse mean_squared_error(y_test, test_pred) ** 0.5 train_scores.append(train_rmse) test_scores.append(test_rmse) print(fmax_depth{depth:2d} | 训练 RMSE{train_rmse:.4f} | 测试 RMSE{test_rmse:.4f}) plt.figure(figsize(8, 5)) plt.plot(depth_range, train_scores, markero, labelTrain RMSE) plt.plot(depth_range, test_scores, markers, labelTest RMSE) plt.xlabel(max_depth) plt.ylabel(RMSE) plt.title(max_depth 对回归树的影响) plt.legend() plt.grid(alpha0.3) plt.show()运行后会看到类似结果max_depth 1 | 训练 RMSE1.5898 | 测试 RMSE1.6128 max_depth 2 | 训练 RMSE1.1772 | 测试 RMSE1.2048 max_depth 3 | 训练 RMSE0.8744 | 测试 RMSE0.9078 max_depth 4 | 训练 RMSE0.7358 | 测试 RMSE0.7681 max_depth 5 | 训练 RMSE0.6070 | 测试 RMSE0.6547 max_depth 6 | 训练 RMSE0.4774 | 测试 RMSE0.5387 max_depth 7 | 训练 RMSE0.3243 | 测试 RMSE0.4423 max_depth 8 | 训练 RMSE0.1817 | 测试 RMSE0.4238 max_depth 9 | 训练 RMSE0.0828 | 测试 RMSE0.4722 max_depth10 | 训练 RMSE0.0325 | 测试 RMSE0.5443从曲线中可以看到max_depth7或8附近测试集 RMSE 最低之后继续加深训练集误差还在下降但测试集误差开始回升这就是典型的过拟合。所以在这个数据集上深度 7 左右是比较合理的候选值。5.3 GridSearchCV 网格搜索手动观察只能确定一个大方向更严谨的做法是同时调整多个超参数用交叉验证评估不同组合的效果。这里使用GridSearchCV对max_depth、min_samples_split、min_samples_leaf做网格搜索。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [5, 6, 7, 8, 9], min_samples_split: [2, 5, 10, 20], min_samples_leaf: [1, 2, 4, 8] } grid_model DecisionTreeRegressor(random_state42) grid_search GridSearchCV( estimatorgrid_model, param_gridparam_grid, scoringneg_root_mean_squared_error, cv5, n_jobs-1, verbose1 ) grid_search.fit(X_train, y_train) print(f最优参数: {grid_search.best_params_}) print(f最优交叉验证 RMSE: {-grid_search.best_score_:.4f})这里解释一下几个关键点scoringneg_root_mean_squared_error表示用负的 RMSE 作为得分sklearn 中得分越高越好所以 RMSE 越小负值越大。cv5表示 5 折交叉验证每一组参数都会训练 5 次结果更稳定。n_jobs-1表示用所有 CPU 核心并行计算能明显加速。运行完成后我们用最优参数在测试集上做一次最终评估best_grid_model grid_search.best_estimator_ y_test_pred best_grid_model.predict(X_test) test_rmse mean_squared_error(y_test, y_test_pred) ** 0.5 test_r2 r2_score(y_test, y_test_pred) print(f测试集 RMSE: {test_rmse:.4f}) print(f测试集 R2: {test_r2:.4f}) print(f决策树深度: {best_grid_model.get_depth()}) print(f叶节点数量: {best_grid_model.get_n_leaves()})网格搜索的好处是系统、全面但缺点也很明显参数组合是指数级增长。如果参数很多、每个参数的候选值也很多搜索时间会非常长。此时可以用随机搜索代替。5.4 RandomizedSearchCV 随机搜索补充随机搜索的思路是在指定的参数分布或候选列表中随机抽取固定数量的组合然后逐一评估。它不保证找到全局最优但通常能以更少的计算量找到足够好的参数。当参数空间巨大时我一般优先使用RandomizedSearchCV。from sklearn.model_selection import RandomizedSearchCV from scipy.stats import randint param_dist { max_depth: randint(3, 15), min_samples_split: randint(2, 30), min_samples_leaf: randint(1, 20), max_features: [None, 0.5, 0.7, 0.9] } random_model DecisionTreeRegressor(random_state42) random_search RandomizedSearchCV( estimatorrandom_model, param_distributionsparam_dist, n_iter50, scoringneg_root_mean_squared_error, cv5, n_jobs-1, random_state42, verbose1 ) random_search.fit(X_train, y_train) print(f最优参数: {random_search.best_params_}) print(f最优交叉验证 RMSE: {-random_search.best_score_:.4f})随机搜索中的n_iter表示尝试的参数组合数量。设置 50 意味着最多训练 50 组参数每组 5 折交叉验证也就是最多 250 次模型训练。这个开销通常是可以接受的。有一点要特别提醒随机搜索的结果并不稳定不同random_state可能得到不同最优参数。所以最好固定随机种子或者多做几次搜索对比。网格搜索则适合参数范围较小、组合数量可控的场景。5.5 剪枝参数 ccp_alpha 的使用前面我们用深度、叶节点最小样本数来限制模型复杂度但这种方法通常是“从头限制”。剪枝则不同它是先让树尽量生长再通过条件砍掉部分分支让整棵树变小。DecisionTreeRegressor支持成本复杂度剪枝。我们可以用cost_complexity_pruning_path得到不同ccp_alpha对应的树复杂度和总不纯度然后用交叉验证选出最优剪枝强度。from sklearn.tree import DecisionTreeRegressor # 先训练一棵不限制深度的树用于获取剪枝路径 unpruned_model DecisionTreeRegressor(random_state42) unpruned_model.fit(X_train, y_train) path unpruned_model.cost_complexity_pruning_path(X_train, y_train) ccp_alphas path.ccp_alphas print(f候选 ccp_alpha 数量: {len(ccp_alphas)})接下来对每个候选ccp_alpha训练模型并观察训练集和测试集误差变化train_scores [] test_scores [] for alpha in ccp_alphas: model DecisionTreeRegressor( random_state42, ccp_alphaalpha ) model.fit(X_train, y_train) train_pred model.predict(X_train) test_pred model.predict(X_test) train_rmse mean_squared_error(y_train, train_pred) ** 0.5 test_rmse mean_squared_error(y_test, test_pred) ** 0.5 train_scores.append(train_rmse) test_scores.append(test_rmse) plt.figure(figsize(8, 5)) plt.plot(ccp_alphas, train_scores, markero, labelTrain RMSE) plt.plot(ccp_alphas, test_scores, markers, labelTest RMSE) plt.xlabel(ccp_alpha) plt.ylabel(RMSE) plt.title(ccp_alpha 剪枝参数效果) plt.legend() plt.grid(alpha0.3) plt.show()可以看到当ccp_alpha很小时模型几乎和未剪枝状态一样测试集误差偏高随着ccp_alpha增大树被剪得更小测试集误差先下降后上升。在曲线上找到最低点对应的ccp_alpha就是比较合理的剪枝强度。更严谨的做法是直接用交叉验证搜索最优ccp_alpha。可以把待选的ccp_alpha作为GridSearchCV的搜索空间param_grid { ccp_alpha: ccp_alphas } prune_search GridSearchCV( estimatorDecisionTreeRegressor(random_state42), param_gridparam_grid, scoringneg_root_mean_squared_error, cv5, n_jobs-1 ) prune_search.fit(X_train, y_train) print(f最优 ccp_alpha: {prune_search.best_params_}) print(f最优交叉验证 RMSE: {-prune_search.best_score_:.4f})需要留意的是ccp_alphas中最后几个值会让树退化到只有一个根节点此时模型过于简单拟合不足。交叉验证会自动避开这些候选值。使用剪枝后你可能会发现一棵深度只有 6 或 7 的树效果比之前手动调参的模型还要好而且树更小、更稳定。5.6 可视化决策树调参完成后把树的结构画出来看看能帮助理解模型学到了什么规律。sklearn 提供了plot_tree函数可以直接绘制决策树。这个函数只依赖 matplotlib不需要额外安装 graphviz。import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(20, 10)) plot_tree( best_grid_model, feature_names[fx{i} for i in range(1, 5)], filledTrue, roundedTrue, fontsize10 ) plt.title(Optimized Regression Tree) plt.show()如果树比较大图片会非常大可以只画前几层。比如只画深度 3 以内的部分plt.figure(figsize(15, 8)) plot_tree( best_grid_model, max_depth3, feature_names[fx{i} for i in range(1, 5)], filledTrue, roundedTrue, fontsize10 ) plt.show()如果不方便看图形也可以使用export_text输出纯文本格式的树规则在终端里阅读非常方便from sklearn.tree import export_text tree_rules export_text( best_grid_model, feature_names[fx{i} for i in range(1, 5)] ) print(tree_rules)输出类似|--- x2 0.68 | |--- x1 0.68 | | |--- x4 0.68 | | | |--- value: [10.73] | | |--- x4 0.68 | | | |--- value: [12.11] | |--- x1 0.68 | | |--- value: [14.02] |--- x2 0.68 | |--- x3 0.71 | | |--- value: [15.78] | |--- x3 0.71 | | |--- value: [18.36]通过这份规则你可以清楚地知道模型在哪些特征上做了划分阈值大约是多少每个子区域预测值是多少。这也是决策树算法相比深度学习模型最大的优势——可解释性。5.7 特征重要性分析决策树可以输出特征重要性它表示每个特征在整棵树中参与划分时带来的总误差减少量再做一个归一化。特征重要性越高说明该特征对预测结果的贡献越大。feature_names [fx{i} for i in range(1, 5)] importances best_grid_model.feature_importances_ for name, imp in zip(feature_names, importances): print(f{name}: {imp:.4f}) plt.figure(figsize(8, 5)) plt.barh(feature_names, importances) plt.xlabel(Importance) plt.title(Feature Importance) plt.show()特征重要性有两个实际用途。第一用于特征筛选。如果某个特征的重要性非常低可以考虑删除它简化模型并减少训练时间。第二用于业务解释。比如在销售预测中发现“历史销量”重要性最高而“星期几”重要性很低这个结论可以直接用于运营决策。但需要注意特征重要性并不能完全代表因果性。它只能说明在训练数据中该特征对划分误差的贡献大小。特征之间如果存在强相关性某个重要特征被另一个特征替代后重要性分布可能发生明显变化。所以在解读特征重要性时要结合业务经验判断。6. 常见问题与排查思路在实际使用决策树算法的过程中大家遇到的问法五花八门但背后的问题往往集中在几类。下面整理了一份常见问题排查表。问题现象常见原因解决思路训练集 R2 接近 1测试集 R2 明显偏低树过深过拟合限制 max_depth增大 min_samples_leaf使用 ccp_alpha 剪枝测试集 R2 为负数模型严重过拟合或特征与目标无关先检查数据再大幅限制树复杂度必要时更换模型树很大但可视化图片拥挤没有限制深度或叶节点数设置 max_depth或用 max_leaf_nodes 限制叶节点数量网格搜索耗时太长参数组合太多数据集较大改用 RandomizedSearchCV先粗调再精调特征重要性不稳定样本量不足或特征之间存在强共线性固定随机种子增加样本量进行多次验证类别型特征无法直接训练决策树不支持字符串类别特征使用 OneHotEncoder 或 OrdinalEncoder 编码预测值呈现阶梯状不平滑回归树叶节点输出常数换用模型树 M5或改用随机森林 / GBDT中文标签在绘图中乱码matplotlib 默认字体不支持中文设置中文字体或使用英文标签关于最后一栏的中文乱码问题是个高频坑。绘图前可以加如下配置但不同系统字体名称不一样Windows 和 mac 可能会有差异。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False如果是在服务器上运行建议直接使用英文标签省去字体配置的麻烦。还有一点很常见有人拿分类问题里调好的参数直接用到回归问题上。虽然DecisionTreeClassifier和DecisionTreeRegressor参数名很像但任务目标不同最优参数并不能直接迁移。建议针对回归任务单独调参。7. 最佳实践与工程建议7.1 把回归树当成强 baseline我的经验是接一个回归任务时先不要急着用复杂模型。先跑一棵默认参数的回归树看看训练集和测试集差距有多大再跑一个深度限制后的回归树作为 baseline记下 RMSE 和 R2。得到的结论有几个数据量够不够、特征是否有效、模型是否过拟合。只有基线清晰了后续上随机森林或 XGBoost 才有对比价值。有时候一棵剪枝后的回归树已经足够满足业务精度要求而且解释性比集成模型好得多。如果你的业务场景要求“预测结果可以讲出依据”单棵回归树是性价比很高的选择。7.2 调参要“小步快跑”不要迷信最优参数很多初学者容易陷入一个误区试图找到一个“万能最优参数组合”。实际上超参数调优是在偏差和方差之间做权衡所谓最优参数也只是在特定数据集、特定划分方式下的局部最优。建议先固定随机种子在一个大的参数空间里跑一次随机搜索找到大致范围然后再围绕这个范围做小网格搜索。每一步改变一个变量观察训练集和测试集误差变化这样能够理解每个参数的影响方向。另外不要用测试集反复挑选参数。测试集只能用来做最终评估如果你在调参过程中反复看测试集结果其实已经发生了“对测试集过拟合”。正确的做法是只用交叉验证的得分选择参数最后再在测试集上评估一次。7.3 数据质量和特征工程永远优先回归树虽然对特征尺度不敏感不需要标准化、归一化但这不代表数据预处理不重要。缺失值、异常值、错误标签都会严重影响树的质量。缺失值方面sklearn 的决策树并不支持直接处理缺失值你需要提前填充或删除异常值会拉高平方误差影响节点的切分位置。特征工程上最常见的提升手段包括构造与目标强相关的业务特征、把时间特征转换成周期变量星期几、是否节假日、把类别特征编码到合适的粒度。调参只能带来几个百分点的提升而一个高质量特征可能让模型效果提升一个档次。7.4 单棵树和集成模型的取舍回归树虽然可解释性强但在大数据量和复杂数据上单棵树的拟合能力有限而且方差很大稍微换一批训练数据树的结构就可能完全改变。工程上如果样本量较大、追求精度通常会直接使用随机森林或梯度提升树。随机森林通过并行训练多棵有差异的回归树再对预测结果取平均能显著降低方差是单棵回归树最自然的升级方案。GBDT 则通过串行拟合残差逐步减小误差精度通常更高但对参数更敏感训练时间也更长。理解回归树的超参数对调整随机森林和 GBDT 同样有帮助因为它们共享很多参数语义比如max_depth、min_samples_leaf、max_features。如果项目对解释性要求很高可以用单棵回归树如果精度要求优先团队能够接受黑盒模型建议直接上集成模型然后用 SHAP 等方法补充解释。8. 总结回归树是决策树算法中容易被低估的一个模型。它结构简单、训练快速、可以处理非线性关系还能输出清晰的规则和特征重要性。但单棵树的问题也很突出那就是容易过拟合。想要用好回归树本质上是在做“复杂度控制”也就是通过max_depth、min_samples_leaf、max_leaf_nodes、ccp_alpha等参数限制树的大小找到偏差和方差之间的平衡点。如果你现在正准备用回归树跑一个项目我的建议是先训练默认模型观察过拟合情况再手动观察深度的影响然后使用网格搜索或随机搜索做小范围调参最后用ccp_alpha剪枝收尾。每一步都以训练集和测试集的误差变化为依据而不是盲目堆参数。希望这篇文章能让你对回归树的原理、超参数含义以及调参代码有一个系统的认识下次再遇到决策树算法相关的回归任务可以直接把这份流程用起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价