新闻详情

新闻详情

首页 / 资讯中心 / 详情

CART回归树原理与剪枝实战:从手写代码到sklearn调参

发布时间:2026/10/2 3:05:23来源:尧图网络
CART回归树原理与剪枝实战:从手写代码到sklearn调参
决策树这个东西很多入门教程讲分类讲得头头是道一到回归就含糊带过。我当年学的时候也卡在这儿明明ID3、C4.5都能用信息增益做分类怎么到预测连续数值就不灵了后来真正把CART回归树和剪枝流程走了一遍才明白不是决策树不能做回归而是你缺一个合适的误差度量方式外加一套防止模型长得太野的刹车机制。这篇文章适合正在学机器学习、刷西瓜书或者准备期末考的朋友也适合那些用过sklearn的DecisionTreeRegressor但始终没搞懂里面参数含义的人。我会从CART回归树的拆分原理讲起手推一遍构建过程再把预剪枝和后剪枝讲透最后用Python从零撸一个简化版回归树再用sklearn做完整对比。整个过程不绕弯子直接给你能复现的代码和能记住的判断标准。1. 为什么回归问题要用CART回归树1.1 分类树和回归树的本质差异先说一个很多人忽略的问题ID3和C4.5为什么不能直接做回归因为它们依赖的信息增益、信息增益比都是用熵或者基尼系数来衡量“类别纯度”。这两个指标的前提是目标变量是离散的、有明确的类别标签。你让模型预测房价是350万还是371万熵这东西根本没法算你总不能把每个价格当成一个分类吧那样类别数比样本还多树直接炸掉。CARTClassification And Regression Tree全称就是“分类与回归树”它本身支持回归场景关键是换了一套划分标准不再看类别纯度改看数值误差。分类时用的基尼系数我后面会提一下但回归树的核心就一句话找切分点让切分后两边的平方误差之和最小。这个思路朴素得有点像在二维平面上画阶梯去逼近一条曲线每个区间给一个常数预测值。1.2 平方误差作为划分依据的逻辑回归问题里每个样本的真实值是 ( y_i )预测值暂定为 ( \hat{y} )误差可以用 ( (y_i - \hat{y})^2 ) 来衡量。CART回归树对某个候选切分点 ( s ) 做评估时会把当前节点数据分成左区域 ( R_1 ) 和右区域 ( R_2 )然后分别计算两个区域内部的目标值均值作为该区域的预测值 ( c_1 )、( c_2 )再算总的误差平方和 ( \sum_{x_i \in R_1} (y_i - c_1)^2 \sum_{x_i \in R_2} (y_i - c_2)^2 )。这里面有两个关键决策第一遍历所有特征、所有可能的切分值找出误差最小的那一个第二左右区域的预测值为什么用均值而不是中位数或其他统计量因为平方误差的最优解就是均值这个用微积分就能验证对一个固定的区域使 ( \sum (y_i - c)^2 ) 最小的 ( c ) 就是均值。后面的实现里你就知道这个均值直接决定了预测结果也决定了叶子节点的输出。2. 手推一遍CART回归树的构建过程2.1 特征与切分点的暴力搜索现在用一个特别小的例子走一遍流程你就理解了。假设有5个样本只有一个特征 ( x ) 和目标值 ( y )样本xy11322533844105514第一步确定候选切分点。对于连续特征通常取相邻两个样本值的中间点做候选比如 ( (12)/21.5 )( (23)/22.5 )这样依次得到 1.5、2.5、3.5、4.5 四个切分点。第二步对每个切分点计算划分后的误差平方和。切分点1.5表示左区域只有样本1右区域是样本2到5。左区域预测值 ( c_13 )误差为0右区域预测值应该是 ( (581014)/49.25 )误差平方和是 ( (5-9.25)^2(8-9.25)^2(10-9.25)^2(14-9.25)^242.75 )。总误差就是42.75。再算切分点2.5左区域是样本1、2预测值 ( (35)/24 )误差是2右区域样本3、4、5预测值 ( (81014)/310.67 )误差大约是 ( (8-10.67)^2(10-10.67)^2(14-10.67)^218.67 )总误差20.67。继续算下去切分点3.5对应左区域样本1-3预测值 ( (358)/35.33 )误差12.67右区域样本4、5预测值12误差8总误差20.67。切分点4.5的左区域样本1-4平均值6.5误差 ( (3-6.5)^2(5-6.5)^2(8-6.5)^2(10-6.5)^229 )右区域单点14总误差29。比较下来切分点2.5和3.5的总误差都是最小的20.67。任选一个作为根节点的切分你会发现这个操作本质上就是把相邻的两个y值差距大的地方切开。直观理解就是树在找“突变点”让每一段内部的数字尽量接近。数据量一旦变大这个暴力搜索仍然可行因为候选切分点最多就是 ( n-1 ) 个。2.2 为什么叶子节点的预测值是均值构建过程继续往深处走比如切分点2.5把数据分成左右两块后左右各自成为子节点。如果此时没有设任何停止条件树会继续在左区域找切分点比如把样本1和样本2再切开左节点就只有样本1右节点只有样本2。最后你会得到一棵把所有样本都单独放在叶子上的树训练误差当然是0。这听起来很完美对吧但一测试马上露馅因为这棵树已经死记硬背了每个样本新数据进来它只能靠最近邻区间硬套。所谓回归树叶子节点的预测值取均值就是它把一段区间内所有样本的 y 平均一下当作这段区间最终的输出。这个输出对区间内的数据是“中庸”的不会刻意贴合任何一个点也因此保留了泛化能力。叶子节点覆盖的样本越多预测越稳但也越粗糙越少预测越精细却越容易过拟合。这个矛盾是整篇文章后面所有剪枝操作的根源所在。2.3 停止条件没有刹车的树就是过拟合机器实际使用中CART回归树一般会设置几类停止条件按优先级排序最小样本数节点样本数小于阈值时不再分裂比如 ( min_samples_split5 )。深度限制树的最大深度到达设定值就停止比如 ( max_depth3 )。误差改善阈值如果分裂前后的误差下降幅度小于某个阈值就不值得继续切例如 ( min_impurity_decrease )。叶子节点样本阈值分裂后只要有任何一边的子节点样本数小于阈值就不允许切比如 ( min_samples_leaf2 )。这几种条件的本质不同值得说一下( max_depth ) 是从“纵向”限制树的生长( min_samples_split ) 是从“全局横向”限制节点再分裂的资格( min_samples_leaf ) 则是从“分裂结果”上约束两边的样本量确保不会出现某个叶子只有一个点的情况。这三个参数在实际调参时优先级不一样很多人直接死磕深度但如果在样本量较小的数据集上( min_samples_leaf ) 往往更管用因为它直接拦住了那些“叶子只装一个样本”的危险分裂。3. 决策树剪枝从预剪枝到后剪枝3.1 过拟合的根源与剪枝的整体思路踢开理论包装树模型过拟合的根本原因就是一个分裂次数太多每个区间装得太满太细。训练样本里的噪声会被当成规律学习进去比如某个样本因为偶然因素 ( y ) 特别大树就会刻意在这个样本附近多切一刀好让这个异常点“落地归位”。这种刀法对训练集很爽对测试集就是灾难。于是有了两条对抗路线。一条叫预剪枝也就是构建过程中提前喊停从头避免树长得过于复杂简单说就是边建边刹车另一条叫后剪枝先让树不管不顾地长到完整形态再自底向上把一些不能带来实际收益的分支剪掉。预剪枝的好处是省计算缺点是策略性比较强容易“一刀切”导致欠拟合后剪枝更接近“先给自由再看表现”的路子效果通常更好但计算量更大。现实中两者应该搭配着来。3.2 预剪枝的具体做法与两手准备预剪枝本质上就是前面刚提过的那些停止条件。操作上你可以在sklearn的 ( DecisionTreeRegressor ) 里直接设 ( max_depth3 )、( min_samples_leaf5 ) 这些参数模型在构建过程中一旦触达条件就停止分裂。但这里有个坑预剪枝的阈值很难一次设准。设太严比如 ( max_depth2 )模型可能连一个像样的特征交互都学不到设太松比如 ( max_depth15 )小数据集上还是照常过拟合。我的习惯是先用默认参数跑一遍看看训练集和验证集的分数差距有多大如果训练集 ( R^2 ) 已经0.98但验证集只有0.72说明过拟合很严重这时候先把 ( max_depth ) 从大到小扫一遍画出验证集分数的曲线选最高点附近的值。预剪枝还有个容易忽略的好处它能让模型更稳定。回归树对数据扰动本来就敏感训练数据里加几个点就可能改变整体切分结构。提前限制深度后树没那么容易把偶然波动纳入结构里方差自然就降下来了。3.3 后剪枝CCP代价复杂度剪枝后剪枝在sklearn里面最标准的方法是代价复杂度剪枝Cost Complexity Pruning简称CCP。思路是给“树的复杂度”定义一个代价然后让模型在误差和复杂度之间做权衡。引入一个参数 ( \alpha )对一个叶子数为 ( |T| ) 的子树 ( T )定义它的损失函数( C_\alpha(T) R(T) \alpha \cdot |T| )这里的 ( R(T) ) 是回归树的训练误差平方和( |T| ) 是叶子节点数量。当 ( \alpha0 ) 的时候损失只看训练误差完整树因为误差最小被选中( \alpha ) 增大惩罚项变大模型就倾向于用更少的叶子节点去换稍微大一点的误差树被剪得越来越干净。具体剪枝过程要怎么看对树中的某个内部节点 ( t )把它当作叶子时整棵子树的误差会变大大出来的这部分记为 ( R(t)-R(T_t) )同时叶子数从 ( |T_t| ) 降到1减少的叶子数是 ( |T_t|-1 )。两者相除就得到一个临界值( \alpha_{eff} \frac{R(t)-R(T_t)}{|T_t|-1} )这个值的直观含义是每少一片叶子需要额外付出的误差增量。从根往叶子方向每个内部节点都能算出一个临界值CCP算法自底向上逐个剪掉临界值最小的那个节点每剪一次就生成一棵新的子树同时记录当前的 ( \alpha ) 值。整个过程会得到一棵剪枝路径每一步对应一个 ( \alpha ) 和一棵子树。最后用交叉验证去选让验证集误差最小的那个 ( \alpha )再对子树做最终预测。这个思路设计得很优雅它把“树要多大”的问题变成一个连续超参 ( \alpha ) 的选择问题。4. 用Python把回归树和剪枝跑一遍4.1 从零实现一个最小版CART回归树理解了原理之后动手写一个简版实现会帮你把模糊的地方彻底压实。下面这段代码不做任何优化只为展示核心逻辑递归找切分点、切分后分别计算均值、设置最小样本量停止条件。import numpy as np class SimpleCARTRegressor: def __init__(self, min_samples_split5, max_depthNone): self.min_samples_split min_samples_split self.max_depth max_depth self.tree None def _compute_error(self, y): if len(y) 0: return 0.0 return np.sum((y - np.mean(y)) ** 2) def _best_split(self, X, y): best_feature, best_threshold, best_error None, None, np.inf n_samples, n_features X.shape for f in range(n_features): values np.unique(X[:, f]) if len(values) 2: continue thresholds (values[:-1] values[1:]) / 2 for t in thresholds: left_mask X[:, f] t right_mask X[:, f] t if np.sum(left_mask) 0 or np.sum(right_mask) 0: continue error self._compute_error(y[left_mask]) self._compute_error(y[right_mask]) if error best_error: best_error error best_feature f best_threshold t return best_feature, best_threshold, best_error def _build(self, X, y, depth): n_samples len(y) if n_samples self.min_samples_split or (self.max_depth is not None and depth self.max_depth): return float(np.mean(y)) f, t, _ self._best_split(X, y) if f is None: return float(np.mean(y)) left_mask X[:, f] t right_mask X[:, f] t node { feature: f, threshold: t, left: self._build(X[left_mask], y[left_mask], depth 1), right: self._build(X[right_mask], y[right_mask], depth 1) } return node def fit(self, X, y): X np.asarray(X) y np.asarray(y).reshape(-1) self.tree self._build(X, y, depth0) return self def _predict_one(self, x): node self.tree while isinstance(node, dict): if x[node[feature]] node[threshold]: node node[left] else: node node[right] return node def predict(self, X): return np.array([self._predict_one(x) for x in np.asarray(X)])注意几个设计取舍这里的切分阈值取了相邻唯一值的中点实际工程里可能会用更多干扰项处理比如带权重的分位数切分停止条件方面我只实现了 ( min_samples_split ) 和 ( max_depth )没有实现 ( min_samples_leaf )是因为这个最小版本想保持逻辑干净。每次分裂都要对所有特征的所有候选切分点做扫描复杂度是 ( O(n \times m \times n_samples) )在小数据集上完全可以接受。4.2 用合成数据测试手写模型造一条带噪声的曲线数据来检验import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split rng np.random.RandomState(42) X np.linspace(0, 3, 200) y np.sin(2 * X) 0.3 * np.random.randn(200) X X.reshape(-1, 1) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model SimpleCARTRegressor(min_samples_split10, max_depth4) model.fit(X_train, y_train) pred model.predict(X_test) r2 1 - np.sum((y_test - pred) ** 2) / np.sum((y_test - np.mean(y_test)) ** 2) print(手动实现R2:, round(r2, 4))如果一切顺利这个简化版模型大概能拿到0.6到0.7左右的 ( R^2 )具体取决于随机种子和深度设置。它的每一步都对应前面讲过的原理分裂时计算平方误差、叶子输出均值、深度控制让树不至于疯狂细化。把预测结果画出来你会看到一条明显的阶梯状折线每一段水平线就是一个叶子节点。4.3 sklearn实战预剪枝参数的对照实验手写模型只是用来验证理解真正干活还得用sklearn的 ( DecisionTreeRegressor )。先用默认参数建立一棵完整生长的树再对比受限条件下的效果from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import r2_score reg_default DecisionTreeRegressor(random_state42) reg_default.fit(X_train, y_train) print(默认树 训练R2:, round(reg_default.score(X_train, y_train), 4), 测试R2:, round(reg_default.score(X_test, y_test), 4)) reg_pruned DecisionTreeRegressor( max_depth3, min_samples_split5, min_samples_leaf4, random_state42 ) reg_pruned.fit(X_train, y_train) print(预剪枝 训练R2:, round(reg_pruned.score(X_train, y_train), 4), 测试R2:, round(reg_pruned.score(X_test, y_test), 4))这种小数据上默认树几乎就是教科书级过拟合训练 ( R^2 ) 接近1测试 ( R^2 ) 稳定低于预剪枝版本。你多做几次不同比例的train_test_split就会发现默认树的测试分数波动很大而加了深度和叶子限制的树更平稳。这就是前面说的“方差下降”在数值上的体现。4.4 sklearn实战CCP后剪枝完整流程后剪枝走的是另一条路线。先把树放开长再通过cost_complexity_pruning_path拿到每个候选 ( \alpha ) 值对应的树信息用交叉验证挑出最合适的 ( \alpha )reg_tree DecisionTreeRegressor(random_state42) reg_tree.fit(X_train, y_train) path reg_tree.cost_complexity_pruning_path(X_train, y_train) ccp_alphas, impurities path.ccp_alphas, path.impurities # 去掉最后一个纯叶子的alpha避免树只剩一个节点 ccp_alphas ccp_alphas[:-1] # 对每个alpha训练一棵对应剪枝程度的树 pruned_trees [] for alpha in ccp_alphas: reg DecisionTreeRegressor(random_state42, ccp_alphaalpha) reg.fit(X_train, y_train) pruned_trees.append(reg) # 简单用验证集选最优 scores [reg.score(X_test, y_test) for reg in pruned_trees] best_idx np.argmax(scores) best_alpha ccp_alphas[best_idx] print(最优alpha:, round(best_alpha, 5), 测试R2:, round(scores[best_idx], 4))这里有一个极易踩到的坑ccp_alphas列表是单调递增的每个 ( \alpha ) 训练出来的树叶子数逐步减少但最优 ( \alpha ) 并不是越小越好。如果你拿测试集来选 ( \alpha )相当于把测试集信息泄露进了模型选择过程正确做法是把数据切成三份训练集、验证集、测试集用验证集选 ( \alpha )最后再用测试集评估。数据量小的时候可以用交叉验证替代sklearn里配合GridSearchCV扫ccp_alpha就非常方便。后剪枝的另一个优势是它保留了完整树可能挖掘出的复杂模式只是在复杂度代价面前做了取舍因此往往比纯预剪枝拿到的模型更“聪明”。实际业务里我更倾向于先用较大深度训练再用CCP剪枝配合验证集选参。5. 常见问题与排查技巧实录5.1 树太深导致的过拟合最典型的现象就是训练集 ( R^2 ) 接近1测试集分数低得离谱。解决逻辑分两步先限制深度重新训练看测试分数是否有提升如果提升不明显就上CCP后剪枝。注意不要一上来就把深度设为2那种欠拟合是另一种极端。扫描法更靠谱深度从1到10逐个跑画出训练分数和测试分数的变化曲线找交叉点附近的位置作为参考值。5.2 离群点让叶子均值失真回归树对异常值不算敏感但也谈不上稳健。如果一个离群点单独占据一个叶子预测时落在那个区域就会吃到一个不合理的极端值。排查方法很简单打印树的叶子样本数和对应预测值如果某个叶子的预测值比整体分布高出几个量级就需要考虑在数据预处理时做截尾或者winsorize处理。另一个思路是提高min_samples_leaf强迫离群点落在一个更大的区间里让它被稀释掉而不是单独成区。5.3 预剪枝参数的选择min_samples_split和min_samples_leaf有什么区别这两个参数经常被混用但实际上管的事情不一样。min_samples_split管的是“一个节点至少有多少样本才能继续分裂”比如设为10样本量小于10的节点直接变叶子min_samples_leaf管的是“分裂之后每个子节点至少要保留多少样本”比如设为5哪怕当前节点有100个样本只要某个候选切分点把一边切到只剩3个这个切分就会被拒绝。从效果看min_samples_leaf对“叶子太细”的限制更直接也更适合小样本数据集。我个人的默认配置偏向min_samples_leaf5再配合max_depth兜底很少单独只调min_samples_split。5.4 特征量纲与切分阈值需要归一化吗很多初学者会惯性给树模型做标准化其实没必要。CART按特征值大小排序后取相邻中点作为切分候选这个操作是单调的特征放大缩小只影响阈值的绝对数值不影响划分结果和预测值。真正影响树结构的是特征的取值范围和分布——取值范围大的特征候选切分点更多更容易被选中这只能说是一种选择偏差不是量纲问题。如果你想避免某个连续特征因为取值种类太多而抢占分裂机会可以对特征做分箱或者限制max_features但不是必须的。树模型的天然优势就是能抓非线性关系这也是它始终没被线性模型完全取代的重要原因。5.5 分类树和回归树的剪枝差异最后提一个容易混淆的知识点CART分类树剪枝和回归树剪枝思路完全一致都是代价复杂度剪枝区别只在评估指标。分类树用基尼系数算节点不纯度回归树用平方误差总和。基尼系数评估的是“这个节点里面类别有多杂”越杂越需要继续分裂平方误差评估的是“这个节点里面数值波动有多大”波动越大越值得继续切。懂得了这个区别看西瓜书里关于剪枝的公式就不会再懵了。我个人在实际项目中已经习惯把决策树的调参流程固定成两段式第一阶段用预剪枝参数粗暴限制树规模快速确认特征有效性和基线精度第二阶段把限制放宽让树尽量长出来再用CCP后剪枝加交叉验证精调。这样既避免了预剪枝的盲目性也控制了后剪枝的计算开销。如果你正被决策树的超参数搞得焦头烂额不妨照这个流程跑一轮应该能少踩不少坑。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2026年精选最值得推荐的5款AI智能降重工具 2026/10/2 5:00:26

2026年精选最值得推荐的5款AI智能降重工具

2026 年毕业季即将到来,各大高校对论文 AIGC 检测的审核标准愈发严格。面对市面上种类繁多的降 AI 工具,很多同学开始困惑:到底该选哪个才能真正有效降低查重率?我花两周时间,对当前市面主流的 5 款降 AI 工具进行了实…

阅读更多 →
Unity文件操作安全指南:AssetDatabase替代System.IO 2026/10/2 5:00:26

Unity文件操作安全指南:AssetDatabase替代System.IO

1. 这不是简单的“右键新建”——Unity里文件系统操作的本质约束很多人第一次在Unity里想“创建个配置文件”或“删掉临时资源”,直接写System.IO.Directory.CreateDirectory("Assets/Config"),结果发现Editor里路径对了,Build出来…

阅读更多 →
白盒测试四大覆盖方法实战指南:从语句到路径的工程化落地 2026/10/2 5:00:26

白盒测试四大覆盖方法实战指南:从语句到路径的工程化落地

1. 这不是标题党,是真正在一线写测试用例的人在喊话“耗子尾汁”这四个字刚火起来那会儿,我正蹲在客户现场改第17版支付模块的单元测试覆盖率报告。运维同事甩过来一张截图:核心交易链路的分支覆盖才63.2%,而客户合同里白纸黑字写…

阅读更多 →
RuoYi + RAGFlow 私有化知识库全栈实战:从选型、权限打通到部署调优 2026/10/2 5:00:26

RuoYi + RAGFlow 私有化知识库全栈实战:从选型、权限打通到部署调优

这是 RuoYi RAGFlow 私有化知识库系列文章的第三篇。前两篇我们聊完了整体架构设计和基础环境搭建,这一篇我打算换个节奏,把过去两个月在不同环境里跑这套方案时攒下的实操细节、踩坑记录和选型结论一次说清楚。网上讲 RuoYi 的、讲 RAGFlow 的文章都不…

阅读更多 →
NAND门:数字电路的物理起点与最优解本质 2026/10/2 5:00:13

NAND门:数字电路的物理起点与最优解本质

1. 这不是游戏,是数字电路的成人礼“NandGame个人最优解”——看到这个标题,很多人第一反应是:又一个通关攻略?刷分技巧?或者某个速通玩家的炫耀帖?但如果你真点进去,会发现里面没有角色、没有血…

阅读更多 →
RAGFlow实战:企业知识库从解析到溯源的完整方案 2026/10/2 5:00:13

RAGFlow实战:企业知识库从解析到溯源的完整方案

企业知识库这件事,我前后折腾了不少开源方案,也踩过不少坑。一开始图省事,直接拿通用大模型接私有数据,结果问啥啥不对,幻觉严重到能把项目周期说错;后来换传统方案,用向量库套 embedding&#…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉