一、树的构建
1.1 构建流程
决策树(Decision Tree)是一类白盒模型:通过对特征做 if-else 式测试,把样本逐层划分到不同子集。它在分类与回归任务上都能用(后者称回归树,§3 详述)。
基本结构
- 内部节点 :对单一特征的测试(如
x_3 < 0.5?)。 - 边:测试结果的分支。
- 叶节点:落到此处的样本给出一个预测(分类:多数类;回归:均值)。
- 路径:从根到叶的一条规则,等价于 AND 连接的若干测试。
一个最小示例
text
天气 == 晴
├── 湿度 < 70% → 出门
└── 湿度 >= 70% → 出门带伞
预测时从根开始沿测试结果下行,到叶即得预测。
构建流程(自顶向下贪心)
#mermaid-svg-ZKfx0GLofs21ohpg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZKfx0GLofs21ohpg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZKfx0GLofs21ohpg .error-icon{fill:#552222;}#mermaid-svg-ZKfx0GLofs21ohpg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZKfx0GLofs21ohpg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZKfx0GLofs21ohpg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZKfx0GLofs21ohpg .marker.cross{stroke:#333333;}#mermaid-svg-ZKfx0GLofs21ohpg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZKfx0GLofs21ohpg p{margin:0;}#mermaid-svg-ZKfx0GLofs21ohpg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZKfx0GLofs21ohpg .cluster-label text{fill:#333;}#mermaid-svg-ZKfx0GLofs21ohpg .cluster-label span{color:#333;}#mermaid-svg-ZKfx0GLofs21ohpg .cluster-label span p{background-color:transparent;}#mermaid-svg-ZKfx0GLofs21ohpg .label text,#mermaid-svg-ZKfx0GLofs21ohpg span{fill:#333;color:#333;}#mermaid-svg-ZKfx0GLofs21ohpg .node rect,#mermaid-svg-ZKfx0GLofs21ohpg .node circle,#mermaid-svg-ZKfx0GLofs21ohpg .node ellipse,#mermaid-svg-ZKfx0GLofs21ohpg .node polygon,#mermaid-svg-ZKfx0GLofs21ohpg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZKfx0GLofs21ohpg .rough-node .label text,#mermaid-svg-ZKfx0GLofs21ohpg .node .label text,#mermaid-svg-ZKfx0GLofs21ohpg .image-shape .label,#mermaid-svg-ZKfx0GLofs21ohpg .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZKfx0GLofs21ohpg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZKfx0GLofs21ohpg .rough-node .label,#mermaid-svg-ZKfx0GLofs21ohpg .node .label,#mermaid-svg-ZKfx0GLofs21ohpg .image-shape .label,#mermaid-svg-ZKfx0GLofs21ohpg .icon-shape .label{text-align:center;}#mermaid-svg-ZKfx0GLofs21ohpg .node.clickable{cursor:pointer;}#mermaid-svg-ZKfx0GLofs21ohpg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZKfx0GLofs21ohpg .arrowheadPath{fill:#333333;}#mermaid-svg-ZKfx0GLofs21ohpg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZKfx0GLofs21ohpg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZKfx0GLofs21ohpg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZKfx0GLofs21ohpg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZKfx0GLofs21ohpg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZKfx0GLofs21ohpg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZKfx0GLofs21ohpg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZKfx0GLofs21ohpg .cluster text{fill:#333;}#mermaid-svg-ZKfx0GLofs21ohpg .cluster span{color:#333;}#mermaid-svg-ZKfx0GLofs21ohpg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZKfx0GLofs21ohpg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZKfx0GLofs21ohpg rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZKfx0GLofs21ohpg .icon-shape,#mermaid-svg-ZKfx0GLofs21ohpg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZKfx0GLofs21ohpg .icon-shape p,#mermaid-svg-ZKfx0GLofs21ohpg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZKfx0GLofs21ohpg .icon-shape .label rect,#mermaid-svg-ZKfx0GLofs21ohpg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZKfx0GLofs21ohpg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZKfx0GLofs21ohpg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZKfx0GLofs21ohpg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
全部训练样本
选最优划分特征与阈值
左子集 D_left
右子集 D_right
满足停止条件?
叶节点预测
满足停止条件?
叶节点预测
算法骨架(ID3 / C4.5 / CART 通用形式)
text
function BuildTree(D, depth):
if 满足停止条件(depth, D) or D 不可再分:
return LeafNode(预测值)
best_feature, best_threshold, best_score = None
for each feature j:
for each candidate threshold t (在 D 上扫描):
D_left, D_right = split(D, j, t)
score = gain(D, D_left, D_right) # 信息增益 / 基尼下降 / MSE 下降
if score > best_score:
best_feature, best_threshold, best_score = j, t, score
D_left, D_right = split(D, best_feature, best_threshold)
left_child = BuildTree(D_left, depth+1)
right_child = BuildTree(D_right, depth+1)
return InternalNode(best_feature, best_threshold, left_child, right_child)
关键工程要点
- 特征与阈值的扫描 :每次划分时遍历所有特征的所有候选阈值(连续特征常取相邻值中点),计算分裂后的纯度提升。O(NdlogN)O(N d \log N)O(NdlogN) 单层复杂度。
- 停止条件:树深度上限、叶节点最小样本数、最小信息增益、节点样本数低于阈值等。
- 叶子预测:分类 = 该节点训练样本的多数类(也可以是各类概率);回归 = 训练样本均值(也可取中位数减少异常值影响)。
多分类天然支持
决策树的多分类能力"开箱即用"------叶节点取多数类概率,无需 OvR/OvO。sklearn 的 DecisionTreeClassifier 直接处理 KKK 类。
对特征缩放不敏感
树只看特征的排序与阈值,与量级无关------StandardScaler 对决策树无影响(但用 Pipeline 保留无害)。
自含特征选择
树的构建过程本身在做特征选择:每次选"对划分贡献最大"的特征。最终被选中的特征即"重要特征",未选中的可视为"次要"。
scikit-learn 实现概览
python
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
clf = DecisionTreeClassifier(
criterion="gini", # "entropy" / "log_loss" 皆可
max_depth=None, # 树最大深度
min_samples_split=2, # 节点可分最小样本数
min_samples_leaf=1, # 叶节点最小样本数
max_features=None, # 每次划分考虑的特征数(None=全部)
random_state=0,
)
clf.fit(X_train, y_train)
print(clf.tree_.node_count, clf.get_depth()) # 节点数、最大深度
print(clf.feature_importances_) # 特征重要性
易错点
- 不限制深度 → 过拟合 :默认参数会一直分裂到每个叶节点纯度 1(分类准确率 100% / 回归训练 R2=1R^2 = 1R2=1),但测试集表现极差。必须设
max_depth或做剪枝。 - 类别标签字符串直接传入:现代 sklearn 接受字符串标签,但自定义实现需做整数编码。
- 不平衡数据下叶节点预测偏向多数类 :可用
class_weight="balanced"加权、或后续做阈值调整(见第九章 §2.2)。 - 树模型对训练集的小扰动敏感:单棵树方差大,需要集成(Bagging / Boosting,见第八章)才能稳定。
1.2 划分准则
每次划分要回答:哪个特征 + 哪个阈值 使划分后的子集"更纯"?纯度由不纯度函数(impurity)量化。三类主流准则:
信息熵(ID3 / C4.5)
对节点 mmm 的样本集 DmD_mDm,各类概率 pkp_kpk:
H(Dm)=−∑k=1Kpklog2pkH(D_m) = -\sum_{k=1}^K p_k \log_2 p_kH(Dm)=−k=1∑Kpklog2pk
(HHH 的更多性质见第二章 §4.1。)
信息增益 = 父节点熵 − 加权子节点熵:
Gain(D,j,t)=H(D)−∣DL∣∣D∣H(DL)−∣DR∣∣D∣H(DR)\text{Gain}(D, j, t) = H(D) - \frac{|D_L|}{|D|} H(D_L) - \frac{|D_R|}{|D|} H(D_R)Gain(D,j,t)=H(D)−∣D∣∣DL∣H(DL)−∣D∣∣DR∣H(DR)
C4.5C4.5C4.5 引入信息增益比 (gain ratio)Gain(D,j,t)/H(j)\text{Gain}(D, j, t) / H(j)Gain(D,j,t)/H(j),避免"取值多"的特征天然获得大增益的偏置。其中 H(j)=−∑v∣Dv∣∣D∣log2∣Dv∣∣D∣H(j) = -\sum_v \frac{|D_v|}{|D|} \log_2 \frac{|D_v|}{|D|}H(j)=−∑v∣D∣∣Dv∣log2∣D∣∣Dv∣ 是按特征 jjj 各分支样本占比计算的经验熵(split information),与节点类分布熵 H(D)H(D)H(D) 不同。
基尼系数(CART)
G(Dm)=1−∑k=1Kpk2=∑k≠k′pkpk′G(D_m) = 1 - \sum_{k=1}^K p_k^2 = \sum_{k \ne k'} p_k p_{k'}G(Dm)=1−k=1∑Kpk2=k=k′∑pkpk′
直观:随机抽两个样本,类别不同的概率。越小越纯。criterion="gini" 是 sklearn DecisionTreeClassifier 的默认。
示例:基尼下降的手算
设节点有 10 个样本(5 正 5 负),父节点基尼 G=1−0.52−0.52=0.5G = 1 - 0.5^2 - 0.5^2 = 0.5G=1−0.52−0.52=0.5。某特征按阈值 ttt 分裂为左(4 正 1 负)与右(1 正 4 负):
Gleft=1−0.82−0.22=0.32,Gright=0.32G_{\text{left}} = 1 - 0.8^2 - 0.2^2 = 0.32, \quad G_{\text{right}} = 0.32Gleft=1−0.82−0.22=0.32,Gright=0.32
加权子节点基尼 =510×0.32+510×0.32=0.32= \frac{5}{10} \times 0.32 + \frac{5}{10} \times 0.32 = 0.32=105×0.32+105×0.32=0.32,基尼下降 Δ=0.5−0.32=0.18\Delta = 0.5 - 0.32 = 0.18Δ=0.5−0.32=0.18。对每个候选特征算出各自的 Δ\DeltaΔ,取最大者作为该节点的划分------整棵树就是把这一步递归下去。
均方误差 / 平均绝对误差(回归树)
回归任务用回归误差度量:
MSE(Dm)=1∣Dm∣∑i∈Dm(yi−yˉDm)2\text{MSE}(D_m) = \frac{1}{|D_m|} \sum_{i \in D_m} (y_i - \bar{y}_{D_m})^2MSE(Dm)=∣Dm∣1i∈Dm∑(yi−yˉDm)2
叶子预测 yˉDm=1∣Dm∣∑yi\bar{y}_{D_m} = \frac{1}{|D_m|} \sum y_iyˉDm=∣Dm∣1∑yi。DecisionTreeRegressor(criterion="squared_error") 默认;criterion="absolute_error" 用 MAE,对异常值更鲁棒。
三类准则对比
| 准则 | 公式 | 偏向 | 计算成本 | 适用 |
|---|---|---|---|---|
| 信息熵 | −∑plogp-\sum p \log p−∑plogp | 信息增益;多值特征天然获大增益(偏置) | 较慢(log) | ID3/C4.5、需要概率 |
| 基尼系数 | 1−∑p21 - \sum p^21−∑p2 | 与熵近似、计算快 | 快 | CART 分类默认 |
| MSE | $\frac{1}{ | D_m | }\sum (y - \bar y)^2$ | --- |
| MAE | $\frac{1}{ | D_m | }\sum \lvert y - \tilde y \rvert$ | 抗异常值 |
信息增益与基尼的等价性
对二分类 p∈0,1p \in 0, 1p∈0,1:
- 熵:−plogp−(1−p)log(1−p)-p \log p - (1-p)\log(1-p)−plogp−(1−p)log(1−p)(最大值 1)
- 基尼:2p(1−p)2p(1-p)2p(1−p)(最大值 0.5)
两曲线形状相似、纯度排序一致,但纵坐标值差约 2 倍。实际分类效果差异很小(< 1% 准确率),基尼更快(少一个 log),因此 sklearn 默认。
多分类基尼
1−∑kpk21 - \sum_k p_k^21−∑kpk2 对 KKK 类同样适用。当 K=2K=2K=2 时退化为 2p(1−p)2p(1-p)2p(1−p)。
回归 MSE 的来源
回归树的叶节点预测取均值 yˉ\bar yyˉ,可证明在节点 mmm 上最小化 ∑(yi−c)2\sum(y_i - c)^2∑(yi−c)2 的最优 ccc 就是 yˉ\bar yyˉ(一阶导为 0)。这与线性回归 MSE 损失是同一族。
划分数值特征的阈值选择
对连续特征 xjx_jxj,候选阈值常取相邻排序值的中点 {(x(k)+x(k+1))/2:k=1,...,N−1}\{ (x_{(k)} + x_{(k+1)})/2 : k = 1, \dots, N-1 \}{(x(k)+x(k+1))/2:k=1,...,N−1}。计算量 O(N)O(N)O(N),整体划分 O(NdlogN)O(N d \log N)O(NdlogN)。
特征重要性的计算
sklearn 的 feature_importances_ 来自"该特征带来的不纯度下降总和"(MDI,Mean Decrease Impurity):
imp(j)=∑m:split on j∣Dm∣N⋅Δimpurity(m,j)\text{imp}(j) = \sum_{m : \text{split on } j} \frac{|D_m|}{N} \cdot \Delta \text{impurity}(m, j)imp(j)=m:split on j∑N∣Dm∣⋅Δimpurity(m,j)
归一化到和为 1。详细见第三章 §3.2。
易错点
- 基尼 vs 熵选哪个:差异通常 < 1% 准确率;如无特殊理由,用基尼(默认、计算快)。
- 类别极不平衡时 :基尼 / 熵都偏向多数类主导的划分;考虑
class_weight。 - 回归树用 MSE 损失会受异常值影响 :可改
criterion="absolute_error"或先做目标变换。 - 多值特征偏置:用信息增益时,特征取值越多天然增益越大(每片样本更纯);C4.5 用 gain ratio 缓解,sklearn 不用此校正。
二、剪枝策略
2.1 预剪枝与后剪枝
决策树对训练集的拟合能力极强------若不限深度,可达训练误差 0、R2=1R^2 = 1R2=1。但这种"完美拟合"在测试集上往往惨败,过拟合是树模型的核心问题。剪枝(pruning)是控制复杂度、缓解过拟合的主要手段。
预剪枝(Pre-pruning)
在建树过程中提前停止,使树不"长大":
- 限制
max_depth:达到深度上限的节点不再分裂。 - 限制
min_samples_split:节点样本数 < 该值时不再分裂。 - 限制
min_samples_leaf:分裂后任一子节点的样本数 < 该值则禁止分裂。 - 限制
min_weight_fraction_leaf:与上类似但按权重(用于带样本权重的场景)。 - 限制
max_leaf_nodes:最多生成多少叶节点(sklearn 采用 best-first 增长:用优先队列每次扩展当前不纯度改善最大的节点,直到叶数达上限)。 - 限制
min_impurity_decrease:分裂带来的不纯度下降 < 该值则不分。
预剪枝本质是"早停"------优点是计算快,缺点是可能过早截断好的分裂(horizon 效应),对最终性能不一定最优。
后剪枝(Post-pruning)
先让树完全生长(甚至过拟合),再自底向上逐节点评估"是否该子树应替换为叶节点"。
经典算法:Reduced Error Pruning (REP) 、Cost-Complexity Pruning (CCP, CART 论文) 、Minimum Error Pruning 、Pessimistic Error Pruning (PEP, C4.5)。
CCP(代价复杂度剪枝)
对每个内部节点 ttt(∣Tt∣|T_t|∣Tt∣ 为子树 TtT_tTt 的叶节点数),定义:
- 子树误差:R(Tt)=∑m∈Tt∣Dm∣⋅impurity(Dm)R(T_t) = \sum_{m \in T_t} |D_m| \cdot \text{impurity}(D_m)R(Tt)=∑m∈Tt∣Dm∣⋅impurity(Dm)
- 替换为叶的误差:R(t)=∣Dt∣⋅impurity(Dt)R(t) = |D_t| \cdot \text{impurity}(D_t)R(t)=∣Dt∣⋅impurity(Dt)
- 树的复杂度:∣T∣|T|∣T∣(叶节点数)
定义剪枝增益:
α(t)=R(t)−R(Tt)∣Tt∣−1\alpha(t) = \frac{R(t) - R(T_t)}{|T_t| - 1}α(t)=∣Tt∣−1R(t)−R(Tt)
α(t)\alpha(t)α(t) 衡量"把子树换成叶节点、每减少 1 个叶节点的误差下降量"。(此处的 α\alphaα 基于未归一化的加权误差;sklearn 的 ccp_alpha 用按总样本数归一化的版本,数值上相差 NNN 倍,但剪枝路径的排序完全一致。)
CCP 流程
- 建完全树 T0T_0T0。
- 对每个内部节点计算 α(t)\alpha(t)α(t),取 αmin\alpha_{\min}αmin 对应的所有节点剪掉 → T1T_1T1。
- 在 T1T_1T1 上重复,得到 T0⊃T1⊃T2⊃⋯⊃{根}T_0 \supset T_1 \supset T_2 \supset \dots \supset \{\text{根}\}T0⊃T1⊃T2⊃⋯⊃{根},对应的 α0≤α1≤...\alpha_0 \le \alpha_1 \le \dotsα0≤α1≤...
- 在验证集上选最优 TkT_kTk(或交叉验证)。
sklearn 通过 ccp_alpha 参数暴露 α\alphaα,剪掉代价复杂度路径上有效 α\alphaα 超过 ccp_alpha 的子树。
python
import numpy as np
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(random_state=0)
path = clf.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas, impurities = path.ccp_alphas, path.impurities
# 对每个 alpha 训练一棵树
clfs = [DecisionTreeClassifier(random_state=0, ccp_alpha=a).fit(X_train, y_train)
for a in ccp_alphas]
# 在验证集上选最优
val_scores = [clf.score(X_val, y_val) for clf in clfs]
best = clfs[np.argmax(val_scores)]
预剪枝 vs 后剪枝对比
| 维度 | 预剪枝 | 后剪枝 |
|---|---|---|
| 时机 | 建树中提前停 | 树建完后再剪 |
| 计算成本 | 低(少建一些节点) | 高(建完全树再遍历) |
| horizon 效应 | 容易欠拟合 | 不易欠拟合 |
| 最终性能 | 一般略差 | 通常更好 |
| sklearn 支持 | max_depth 等参数 |
ccp_alpha |
正则化直觉
树的复杂度(叶节点数 + 深度)↔ 模型的"有效参数"。叶节点越多 = 越容易过拟合 = 越需要约束。预剪枝 / 后剪枝本质是控制这个"有效参数"。
等价于 L0 正则
树模型剪枝可视为"叶节点数"上的 L0 正则:选择最小化 L+λ∣T∣L + \lambda |T|L+λ∣T∣ 的树。CCP 的 α\alphaα 正是 λ\lambdaλ。这种"硬"正则不像 L1/L2 凸,但搜索效率高(CCP 在 α\alphaα 单调路径上枚举)。
易错点
- 过拟合未识别 :训练集 R2≈1R^2 \approx 1R2≈1 但测试集 R2≪1R^2 \ll 1R2≪1 → 树过深。诊断后用剪枝或集成。
min_samples_leaf=1默认过松:尤其在不平衡数据上,单样本叶节点易学噪声。- CCP 在小数据上波动大 :α\alphaα 路径不连续,验证集小的话选 α\alphaα 不稳;改用交叉验证选
ccp_alpha。 - 剪枝后再用集成 :单棵树后剪枝后表现仍有限,工业上更常用多棵树的集成(第八章),通过 Bagging 替代剪枝。
三、CART 算法
3.1 分类树与回归树
CART(Classification And Regression Trees) 由 Breiman et al. 1984 提出,是最广泛使用的树模型框架。它有两大特点:
- 二分树:每次划分都把节点分成两支(不是 ID3 的多分支)。
- 统一框架:分类与回归用同一构建流程,只换叶子预测与不纯度。
CART 分类树
- 划分准则:基尼系数(默认)或熵。
- 叶节点预测:训练样本的多数类(或各类概率)。
- 预测:沿测试下行到叶,返回该叶的预测。
CART 回归树
- 划分准则:MSE(默认)或 MAE。
- 叶节点预测:训练样本均值(
criterion="squared_error")或中位数(criterion="absolute_error")。 - 预测:沿测试下行到叶,返回叶节点均值/中位数。
回归树的几何解释
回归树的预测面是分段常数的------把特征空间划分为若干矩形区域,每个区域内预测为常数(叶节点的均值)。这种近似对真实函数是粗糙的,但易解释,对非线性有"局部逼近"能力。
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeRegressor
# 简单一维回归
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = np.sin(X).ravel() + np.random.randn(100) * 0.1
reg = DecisionTreeRegressor(max_depth=4).fit(X, y)
X_test = np.linspace(0, 10, 300).reshape(-1, 1)
y_pred = reg.predict(X_test)
plt.plot(X_test, y_pred, label="tree prediction")
plt.scatter(X, y, s=5, alpha=0.5, label="data")
plt.legend()
CART 的训练算法细节
- 二分分裂 :对每个数值特征 jjj,阈值 ttt 划分 {xj≤t}\{x_j \le t\}{xj≤t} 与 {xj>t}\{x_j > t\}{xj>t};对类别特征,独热后 sklearn 内部把每个布尔列当数值特征扫阈值。
- 类别特征处理 :长期版本(sklearn 1.0--1.3)的
DecisionTreeClassifier要求类别特征先数值化(独热或目标编码);HistGradientBoostingClassifier自 0.24 起原生支持类别特征、1.4 起支持categorical_features="from_dtype";最新的 sklearn 1.10 才为单棵决策树与森林新增categorical_features参数(约束:仅二分类 / 单输出回归,且需splitter="best")。面向主流 sklearn 版本时,仍建议把类别特征编码后传入。 - 缺失值处理 :sklearn 1.3 起
DecisionTreeClassifier/DecisionTreeRegressor原生支持 NaN(splitter="best"下,分裂时把缺失样本同时向左、向右尝试并学习"默认方向");1.4 起随机森林也支持。更旧版本需先填充(第三章 §1.1)或换HistGradientBoostingClassifier。 - 类别特征的"序数"问题 :独热后多列分裂等价于"等值二分",可能切错。最优处理是对类别特征用 Ordered TS / mean target encoding 等目标编码后当数值特征(见第三章 §2.2)。
CART 的偏差-方差
- 单棵树:高方差(数据微小变化会改变很多划分)、低偏差。
- 降低方差:剪枝(CCP / 预剪枝)→ 简化树,约束方差。
- 进一步降低:集成(Bagging、Random Forest、Boosting)→ 第八章主题。
CART 与集成的关系
CART 是 Random Forest / GBDT / XGBoost / LightGBM 等的基学习器。这些算法通过组合多棵 CART 把单棵树的高方差问题压下去。
CART 预测路径(推理复杂度)
预测复杂度 O(树深度)O(\text{树深度})O(树深度),与样本数无关------这是树模型推理极快的原因。
叶子预测的扩展
- MAE 叶:用中位数代替均值,对异常值更鲁棒。
- 分位数叶:用分位数(如 0.5 = 中位数)支持概率预测。
- 保留训练样本 :叶节点保留训练样本,推理时返回分布(如
predict()给均值,predict_proba()可在分类树上给各类频率)。
回归树的局限性
- 预测面是阶梯形(piecewise constant),不平滑------对光滑函数近似粗糙。
- 树结构对外推(extrapolation)能力差:训练集范围之外预测为常数叶值;对单调趋势数据(如时间序列价格)尤为明显,训练范围之外的预测会保持最后一个叶节点的常数。
- 决策树对特征的单调变换不变 (如 log\loglog、⋅\sqrt{\cdot}⋅ ,前提是变换严格单调------注意 x2x^2x2 在负半轴非单调,会改变划分)------但对 yyy 关于 xxx 的单调关系无内建假设,不会"自动外推"趋势。
改进方向
- 模型树(Model Tree):叶节点不是常数而是线性模型,预测面是分段线性。
- Gradient Boosting:以 CART 为基学习器,序贯训练逐步修正残差(第八章 §2)。
- Isolation Forest:用随机切分隔离点检测异常(第三章 §1.2)。
- LightGBM / XGBoost 的近似分箱:用直方图加速特征扫描,能扩展到亿级数据。
CART 在 scikit-learn 之外的生态
- XGBoost / LightGBM / CatBoost:工业级 GBDT 实现,CART 是其基学习器;CatBoost 原生支持类别特征。
- CART 的可解释性 :单棵 CART 可被可视化(
plot_tree/export_text),输出 if-else 规则集,便于业务解释。 - CART 的局限:对特征共线性、特征交互、时序结构等无内建机制------需结合特征工程或换模型族。
易错点
- 叶子用均值 → 异常值敏感 :先做目标变换(log、Box-Cox)或换
criterion="absolute_error"。 - 类别特征独热后切分点散乱:高分位特征切了很多次,每次只切走少量样本;考虑用目标编码 / 频次编码。
- 训练集小、特征多:单棵 CART 易过拟合;优先集成(第八章)。
- 训练集与测试集分布差异大:树模型无分布假设,外推时预测为常数 → 在工程上需监控分布漂移。