回归任务中的决策树:分裂逻辑、sklearn调参与过拟合优化

发布时间:2026/10/13 20:01:35
回归任务中的决策树:分裂逻辑、sklearn调参与过拟合优化
去年我做某个价格预测项目一开始理所当然按老思路上了线性模型结果验证集R²卡在0.4上下怎么调都上不去。后来把模型换成决策树回归基于sklearn接口重写训练流程代码改动不超过十行效果立刻不一样了。这篇就是这个决策树系列的第二篇案例重点讲清楚怎么把决策树从分类场景切到回归场景分裂逻辑和分类有什么不同、参数怎么调、过拟合怎么治以及什么时候它比线性回归和随机森林更值得优先试一把。适合已经知道决策树能做分类、但还没认真碰过回归场景的读者也适合想快速建立回归树调参手感的人。1. 决策树回归到底改了什么分裂指标和输出方式都换了1.1 从Gini到MSE回归树用一个数值指标决定一切在sklearn里DecisionTreeClassifier和DecisionTreeRegressor的接口高度相似看起来就是换了个类名可实际上树在生长时的判断标准完全不同。分类树的目标是让每个子节点里的样本类别尽量纯所以算Gini系数或者信息增益回归树面对的是连续的目标值没法要求“纯”只能尽量让每个子节点里的y取值聚集在一起方差越小越好。这里推荐一份我之前踩坑后整理的经验如果你已经跑过分类案例千万别默认回归树会沿用同样的分裂准则它内部默认用squared_error。在sklearn的DecisionTreeRegressor中默认的criterion参数是squared_error也就是均方误差MSE。分裂时算法会遍历每个特征的每个可能取值计算把当前节点分成左右两边之后MSE之和能下降多少下降最多的那个特征和阈值就会被选中。这里的MSE就是每个样本的真实值与节点内均值之差的平方再取平均。说得直白一点树在搜索的时候像一个强迫症每次都挑那个最能“让两边各自变得规矩”的切法。纯粹从数学上看这和K-Means更新聚类中心有类似的味道节点里的均值是当前这一块的“代表值”分割点则让代表值对数据点的解释力更强。理解了这一点后面看特征重要性也就顺了特征重要性本质上就是“如果拿这个特征来分割MSE能降多少个单位”的累加。所以你问回归树到底改了什么答案不是树形结构变了而是它衡量“好坏”的那把尺子从离散纯度换成了连续误差。1.2 叶子节点的预测值不是函数公式是样本平均值回归树另一个让人容易误解的地方是它的预测过程没有一条光滑的拟合曲线。一个叶子节点里训练样本的y平均值或者中位数就是该叶子给所有新样本的预测值所以整棵回归树其实是一个分段常数函数。这是决策树回归和线性回归最直观的区别也是它被很多人觉得“粗糙”的原因。比如用年龄预测保费如果树的某个叶子条件是“年龄小于25”这个叶子里面训练样本的平均保费是3200元那么所有年龄小于25的新样本进来不管具体是18岁还是24岁预测值都是3200元。这就是分段常数的含义它不问“年龄每增加1岁保费涨多少”而是直接问“你落在哪个区间我就给你这个区间的平均答案”。这个特点带来两个重要推论。第一树模型的预测范围不可能超过训练数据里y的取值区间你没法指望它做外推线性回归却可以沿着斜率一直延伸出去所以在增长率预测这类需要外推的任务里树模型天然吃亏。第二分段常数允许预测值出现剧烈的跳变比如年龄从24跨到25预测保费可能瞬间跳涨一大截这正是它能够刻画非线性关系、门槛效应和复杂交互结构的原因。手上数据里“超过某个阈值行为就完全变了”这类规律越多决策树就越有发挥空间。1.3 分类与回归的接口差异一个表看清关键参数变化既然接口相似很多人直接把分类案例的代码套回归绝大多数情况下能跑通但有几个细节不一样。我列一个表看得更清楚对比项分类树 DecisionTreeClassifier回归树 DecisionTreeRegressor作用对象离散类别标签连续数值目标默认分裂准则Gini基尼系数squared_error均方误差MSE叶子输出样本中出现最多的类别叶子内样本的均值常用评估指标accuracy、F1、AUCR²、MSE、MAE预测结果类别标签或概率具体数值这个表看着简单但实战中有个坑分类树判断叶子是否“合格”看类别是否全一致回归树看的是y的方差是不是足够小。如果你的目标y取值范围很大比如房价从几十万到几千万MSE会让树优先去拟合那些离群的大数值样本因为把大数值的错误平方一下就非常大。这种时候把criterion换成absolute_error回归树支持MAE作为分裂准则树对离群值的敏感度会低很多训练可能更稳。我在一组收入预测数据上验证过换成MAE准则之后验证集的中位数误差确实下降代价只是训练时间稍微长一点。2. 案例数据设计先造一组合适的“考题”再动手2.1 为什么我不用现成的数据集很多教程习惯直接加载某个知名数据集拿来就跑。我之前也这么干后来发现有两个问题一是现成数据集已经被大家跑烂了你很难看出模型差异到底是因为方法好还是因为数据集特殊二是真实数据集的特征相关性、量纲、噪声分布全是未知数读者没法在自己的电脑上复现同一种“手感”。所以我在这篇案例里选择自己生成一份模拟数据。好处是总共有多少个样本、噪声多大、真实规律长什么样都完全可控。哪怕你和我用的随机种子不同只要生成逻辑一样结论的大方向也不会偏差。这样也更方便理解“为什么线性回归在这份数据上表现不好而决策树可以”。很多读者第一次接触决策树回归时总觉得自己没理解透其实缺的就是这种“已知答案再反推模型行为”的过程。2.2 生成带非线性关系的数据正弦曲线加交互项我构造一份500条样本的二维回归数据。特征x1在0到10之间均匀分布特征x2在0到10之间均匀分布目标y由下面的公式产生y sin(x1) 0.5 * x2 noise这里的sin(x1)是强烈的非线性项x2则提供一部分线性贡献noise是均值为0、标准差为0.1的高斯噪声。用这段代码可以直接生成import numpy as np import pandas as pd rng np.random.RandomState(42) X rng.uniform(0, 10, size(500, 2)) x1, x2 X[:, 0], X[:, 1] y np.sin(x1) 0.5 * x2 rng.normal(0, 0.1, size500) df pd.DataFrame(X, columns[x1, x2]) df[y] y print(df.head())这份数据有三个特点值得注意。第一x1与y的关系是非线性的线性回归如果只用x1大概只能拟合一条直线很难追上正弦曲线。第二x2对y是线性的这意味着数据里最朴素的规律也在不会让人觉得这份数据是故意刁难线性模型。第三噪声很小只要模型架构合适R²上限会很高便于观察调参带来的变化。如果你希望更像真实业务数据可以把噪声标准差从0.1加到0.5模型之间的差距依然清晰只是绝对数值会变差。2.3 决策树回归在预处理上的便利不用标准化也不用归一化线性回归跑之前一般要检查特征的量纲差异要么标准化要么归一化否则某些特征会由于数值范围大而主导系数。决策树模型完全没有这个烦恼因为它每一次分裂都是单独看一个特征上的阈值比较特征取值范围是100还是0.01最终结果完全等价。对数值型特征来说树的切分只关心排序关系不关心绝对尺度。这一点在项目里的实际收益很大拿到乱七八糟的原始特征我可以先直接喂给决策树跑基线不用浪费时间做特征缩放。不过要注意sklearn的DecisionTreeRegressor目前不支持缺失值喂进去的X里如果有NaNfit会直接报错。所以缺失值必须预先处理一般用均值、中位数或者插值补上。另外树模型非常讨厌“高基数类别特征”直接做整数编码因为模型会把3、7、15这些数值当成有大小关系的数去切分真实类别关系可能完全不是这个意思。我的建议是类别特征要么转成one-hot要么对这个任务有充分把握再直接保留。3. sklearn接口跑通最小回归模型核心参数逐个排雷3.1 先跑一个最朴素的版本看看会发生什么我直接使用默认参数的DecisionTreeRegressor拟合这份数据代码非常短from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error, r2_score X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) reg DecisionTreeRegressor(random_state42) reg.fit(X_train, y_train) y_train_pred reg.predict(X_train) y_test_pred reg.predict(X_test) print(train MSE:, mean_squared_error(y_train, y_train_pred)) print(test MSE:, mean_squared_error(y_test, y_test_pred)) print(test R2 :, r2_score(y_test, y_test_pred))在我这组数据上输出大概是训练集MSE接近0.005甚至更低测试集MSE变成0.3左右R²掉到0.65附近。这就是教科书里说的“高方差”表现模型太自由在训练集上把噪声都背下来了换一批新数据就露馅。如果你跑出来不一样多半是数据规模或随机种子不同但“训练远好于测试”这个现象基本是确定的。之所以要拿默认参数先跑一遍就是要亲眼看到这个现象后面调参才有对照。3.2 五个最容易影响回归树性能的参数DecisionTreeRegressor的参数不少但真正决定模型行为的就是下面这几个max_depth树的最大深度。默认是None也就是不限制树可以一直往下长。深度越大能捕获的特征交互越复杂但也越容易过拟合。回归树我建议起步就在3到7之间试。min_samples_split一个内部节点至少要有多少个样本才允许继续分裂。默认是2意思是只要节点里还有两个样本就可能继续分这显然是过拟合的温床。调大这个值会让树更保守。min_samples_leaf叶子节点至少要有多少个样本。这个参数比上一个更“狠”因为它直接要求最终每个叶子都得凑够一批样本做预测。默认是1对回归来说太不稳健后面我会专门讲为什么。max_features每次分裂考虑多少个特征。默认None也就是全部特征。当特征数很少时保持默认就行特征特别多时设置成例如0.5或者sqrt才有效果。ccp_alpha代价复杂度剪枝系数这个放到后面单独说。我在实际调参时优先动的是min_samples_leaf和max_depth这两个参数对模型复杂度的影响最直接。min_samples_leaf从1调到5再到10很容易看到验证集误差先降后升那个拐点就是合适的位置。网格搜索当然可以同时扫全部参数但先把这两个参数手动扫一遍你会对树模型的行为有更具体的感知。3.3 参数背后的偏差方差权衡多看形态少背结论在sklearn里调参数最忌讳死记硬背“max_depth5最好”这种结论。同样的深度在样本量不同的数据上表现完全不同样本充足时深度大的树没那么容易过拟合样本稀薄时深度3的树可能都显得太复杂。所以调参的第一原则是把样本量放进决策里。一个常用的策略是控制变量。先把min_samples_leaf固定在一个贴合样本量的值比如样本量几百时设成5到20然后单独扫max_depth扫完深度再回头扫min_samples_leaf。原因是max_depth限制的是树的层级结构min_samples_leaf限制的是每片叶子的人口规模两者有交互但侧重点不同一起扫的话网格会非常大分开扫更有效率。我个人的经验是回归树对min_samples_leaf更敏感。因为回归的预测值要依赖叶子内的平均值叶子样本太少平均值就受单个离群点影响很大。如果叶子样本有10个以上哪怕其中混入一个极端值对平均值的冲击也会小很多。后面网格搜索的例子里你会看到最优参数通常落在min_samples_leaf5到20之间而不是默认的1。4. 交叉验证与网格搜索别让测试集陪着调参4.1 为什么不能反复用同一份测试集试参数初学者很容易犯一个错误先用测试集跑出一个准确率调一下参数再跑看到测试集分数变好就高兴多调几次之后测试集分数越来越好但这个分数已经失真了。因为你在拿测试集当训练信号测试集里的信息已经被模型间接学会了。最后真正上线时模型面对新数据表现往往比测试分数差一截。解决方法是把数据划成训练集、测试集再在训练集内部用交叉验证来挑参数。测试集只在最后一刻被用一次分数才相对可信。有些项目还会把训练集再切出一个小验证集但交叉验证一般已经够用而且对几百条样本的数据来说更节省。4.2 GridSearchCV与DecisionTreeRegressor的组合拳sklearn里调参最方便的工具是GridSearchCV我一般和决策树配对使用。比如下面这个网格把max_depth、min_samples_split、min_samples_leaf都放进搜索空间from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 5, 7, 10, None], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 5, 10, 20], } grid_search GridSearchCV( DecisionTreeRegressor(random_state42), param_grid, cv5, scoringneg_mean_squared_error, n_jobs-1, ) grid_search.fit(X_train, y_train) print(best params:, grid_search.best_params_) print(best cv score:, -grid_search.best_score_)注意scoring要传neg_mean_squared_error原因是sklearn内部约定分数越高越好MSE本身是越小越好所以要取负号。如果你用MAE就传neg_mean_absolute_error。GridSearchCV会在训练集上做5折交叉验证最后得到最合适的一组参数。跑完之后最好用这组参数在测试集上验证一次测试集分数通常比交叉验证平均分数略低一点点这很正常不用紧张。我实际用下来的体会是网格不必太密。max_depth从3到10按2递增min_samples_leaf从5到20按5递增往往已经足够了。太密的网格让计算量暴增收益却微乎其微。交叉验证的折数样本量小用5样本量大可以用10不一定要死磕某个数字。4.3 回归评估指标怎么选MSE、MAE与R²不是一回事评估回归模型时容易犯的第二个错误是指标混用。MSE对离群值非常敏感因为它把每个误差都平方了。假如100个样本里99个误差是11个误差是10MSE算出来是1.99好像不错但你自己复盘时会发现预测结果其实对那个离群点完全失控。MAE只要1.09看起来好很多但它不区分离群值和普通误差优化时会显得过于迟钝。R²表示模型解释了多少比例的目标方差取值范围通常小于等于1。R²0.7的意思不是预测准确率70%而是说模型比“直接用平均值预测”强了70%。在刚才这份模拟数据里线性回归R²大约0.3决策树调到最好的时候能到0.85以上对比价值非常大。实际业务中我的选择逻辑是如果错误成本随误差平方增长比如库存过量会线性扩大损失那用MSE调参如果只想保证中位数预测别太离谱用MAE更稳如果想跟业务方说“这个模型大概抓到了数据八成的波动规律”用R²讲故事最直观。指标选择不是一个“随便挑一个就行”的步骤它会直接影响网格搜索最终选出的那组参数。5. 过拟合的视觉证据与剪枝实操从树结构图看哪里出了问题5.1 画出一棵默认树你会被它的复杂程度吓到sklearn提供了plot_tree函数可以直接把训练好的决策树画出来。默认参数的树在500个样本上会长得非常深画出来很容易撑满整张画布我只截取一部分import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(24, 12)) plot_tree(reg, feature_names[x1, x2], filledTrue, roundedTrue, max_depth4) plt.show()画出来之后你会发现两个直观现象。第一树的节点密密麻麻很多叶子只有一两个样本。第二分裂条件经常是x17.56这种非常精确的阈值并且不同分支的阈值挨得很近。这些都是模型在记忆噪声而不是学习规律的表现。一棵真正泛化良好的回归树结构应该很稀疏一个节点下面往往有几十上百个样本。这个可视化不是花架子它是排查过拟合的第一手证据。如果画完发现某个叶子里的样本量只有1那个叶子的预测值完全可能被一个异常点带偏。你都不用把min_samples_leaf调到20先看看调到5的效果树立马清爽很多。许多时候我判断一个树模型健不健壮最直接的粗筛方式就是数叶子里的样本量而不是看一堆指标数字。5.2 预剪枝参数怎么组合先压叶子规模再压树身高度预剪枝就是在树生长的过程中就限制它常见手段就是max_depth、min_samples_split、min_samples_leaf。我通常的操作路径是把min_samples_leaf先设成样本量的1%到3%。500个样本就设5到155000个样本就设50到150。用交叉验证扫max_depth观察验证曲线。深度从3开始每加1看一次通常在5到7之间就会出现平台期。固定刚刚选出的max_depth再回头微调min_samples_leaf选择R²或MSE最优的点。这套流程比直接穷举大网格快很多而且每步之间的因果关系清楚。比如样本量很大时min_samples_leaf设大一点完全没问题叶子多了预测均值越稳定但也不能太大否则连两个特征的交互都学不出来。有一个很简单的判断标准如果训练集和测试集分数差距依然很大就继续加压预剪枝如果两个分数都差说明模型欠拟合反而该放松限制。5.3 代价复杂度剪枝CCP比手调深度更精细的后剪枝如果觉得手动组合预剪枝参数不够精细sklearn还提供了一个非常值得用的后剪枝手段cost complexity pruning代价复杂度剪枝。思路是先把树完整长出来然后计算每个子树在“拟合误差”和“树复杂度”之间权衡的系数ccp_alpha再挑一个合适的alpha重新训练并剪枝。第一段代码是拿到alpha路径reg_default DecisionTreeRegressor(random_state42) reg_default.fit(X_train, y_train) path reg_default.cost_complexity_pruning_path(X_train, y_train) ccp_alphas path.ccp_alphas然后对不同alpha分别训练看验证集分数变化trees [] for alpha in ccp_alphas: tree DecisionTreeRegressor(random_state42, ccp_alphaalpha) tree.fit(X_train, y_train) trees.append(tree) # 根据alpha列表逐一计算验证集MSE找谷底附近的alphaCCP的好处是它把“树有多复杂”量化成一个连续的惩罚项而不是像max_depth那样只能取整数。你会看到alpha从0开始增大时验证误差先下降因为树在去除噪声分支到某个点后开始上升因为把真实规律也剪掉了。取那个谷底附近的alpha就是当前数据下树复杂度最合适的位置。需要提醒的是ccp_alpha对数据量敏感同一份数据换一个随机种子最佳alpha可能会差一个量级所以还是要用交叉验证去选不要拍脑袋定。补充一点CCP和预剪枝参数并不矛盾。我一般先设一个合理的min_samples_leaf压住最离谱的叶子再配合CCP剪掉后续长出的冗余分支两者一起用往往比单用某一个更稳。6. 和线性回归、随机森林同场对比什么时候该让决策树出战6.1 同一份模拟数据上的三模型实测结果为了更直观地理解决策树回归的定位我在同样这份sin加线性加噪声的数据上又跑了线性回归和随机森林回归代码如下from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor models { LinearRegression: LinearRegression(), DecisionTreeRegressor: DecisionTreeRegressor(max_depth5, min_samples_leaf5), RandomForestRegressor: RandomForestRegressor( n_estimators200, max_depth5, min_samples_leaf5, random_state42 ), } for name, model in models.items(): model.fit(X_train, y_train) y_hat model.predict(X_test) mse mean_squared_error(y_test, y_hat) r2 r2_score(y_test, y_hat) print(f{name}: MSE{mse:.4f}, R2{r2:.4f})我在这份数据上跑出来的结果大致是线性回归MSE在0.35左右R²不到0.5修剪过的单棵决策树MSE在0.05到0.08之间R²能到0.88随机森林MSE还能再往下降一截R²到0.92以上。这个对比清楚地说明数据结构里一旦有门槛式的非线性关系线性模型再怎么调都补不上因为它的表达空间被直线形状限制死了。单棵决策树已经能逼近真实规律只是方差比随机森林大一些所以从单棵树换成200棵树的随机森林效果还能继续涨。6.2 特征重要性和可解释性单棵树独有的价值回归树训练完之后可以输出feature_importances_表示每个特征对减少MSE的贡献占比。拿这份数据来说x1通常贡献比x2大因为sin项的波动强烈而x2只是一个斜率为0.5的线性项。这个结果和真实生成公式是吻合的。不过对特征重要性要留个心眼。sklearn里的feature_importance是基于训练时节点分裂带来的不纯度下降量累计出来的它偏向数值型特征和高基数特征。如果两个特征高度相关重要性会把功劳集中到其中一个上面另一个看起来就没用但它其实也是有用的。所以我的习惯是特征重要性用来做方向性筛选比如“前五个特征明显重要后面十几个都接近0”可以放心扔掉后面的但不要在两个相关特征之间过度解读谁比谁重要。单棵树的可解释性还体现在它可以完整画出分裂路径业务人员可以直接看到“如果x1小于3.7且x2大于5.2那么预测值是某个数”。这种透明性是随机森林和深度学习无法提供的也是决策树在很多需要审计的场景里依然没被淘汰的原因。6.3 什么时候优先选它什么时候该换集成模型把这次案例的经验沉淀下来我给自己定了几条选择标准数据量在几千条以内特征关系存在明显的非线性和阈值效应单棵决策树是性价比最高的起点。业务或合规上要求能解释每个预测结果背后的规则选浅层决策树深度五层以内配合可视化直接汇报。数据量过万且特征很多时单棵树容易不稳定这个时候更推荐随机森林或梯度提升树它们通过组合大量树来压方差。目标值有明确的增长趋势未来预测区间可能超出历史范围比如做销量月度增长预测树模型会因为无法外推而吃亏应该考虑线性趋势模型或专门的外推方法。只想快速拿到一个不丢人的基线分数决策树回归配合参数网格半小时内就能搭完后面的集成模型可以在这个基线上评估提升量。实际做项目时我不会纠结“决策树和随机森林谁更好”这种问题。决策树回归是基础盘先把树画出来特征关系看明白再去决定是直接上线还是升级成集成模型。哪怕最终上线的是随机森林决策树给出的特征重要性、分裂阈值和叶节点均值都是理解数据规律时最顺手的辅助工具。最后再说一个我从跑这个案例开始就养成的习惯无论任务看起来多简单我都会把“最大深度为3的决策树”作为第一个模型先跑掉。为什么是深度3因为这个深度的树画出来还能看得懂每一条分裂规则都是完整的业务语句。这一步花费不超过两分钟却能让你在调任何复杂模型之前先把该有的特征关系、异常点和交互效应摸一遍。很多朋友一上来就堆集成模型参数一调一大片最后却说不出数据里到底发生了什么我觉得那才是真正的得不偿失。