决策树实战:从葡萄酒分类到建筑能耗预测,理解 CART 算法与模型剪枝
本文从 CART 决策树的核心原理出发,介绍基尼不纯度、信息增益、均方误差和树模型的分裂机制,并通过两个公开数据案例进行实践:使用葡萄酒化学成分识别葡萄酒类别,以及利用建筑结构参数预测采暖负荷。
最后结合 Python 代码,介绍决策树可视化、规则提取、超参数调优和成本复杂度剪枝,理解为什么一棵更复杂的树,未必具有更好的预测能力。
一、决策树究竟是什么?
假设一家葡萄酒生产企业,需要根据化学检测结果判断葡萄酒属于哪个品种类别。
检测人员可以采用一些简单规则:
- 如果某项化学成分的浓度较低,可能属于类别 A。
- 如果浓度较高,再检查另一项成分。
- 根据不同特征的组合,最终判断葡萄酒类别。
这些规则可以组织成一棵树:
葡萄酒化学检测数据
|
黄酮类含量 ≤ 2?
/ \
是 否
| |
颜色强度 > 4? 酒精度 > 13?
/ \ / \
是 否 是 否
| | | |
类别 C 类别 B 类别 A 类别 B
上面是一棵用于说明原理的示意树,并非实际训练结果。
决策树的工作过程就是:
从训练数据中自动寻找具有区分能力的特征,再逐层划分样本,直到满足停止条件。
一棵决策树主要包含三类节点:
| 组成部分 | 作用 |
|---|---|
| 根节点(Root Node) | 所有样本进入模型的起点 |
| 内部节点(Internal Node) | 根据特征阈值决定样本流向 |
| 叶节点(Leaf Node) | 输出最终类别、类别概率或连续预测值 |
一旦模型训练完成,新样本只需要从根节点开始,按照分裂规则一路走到某个叶节点,就能得到预测结果。
1. 决策树能解决什么问题?
决策树主要用于两类任务。
分类问题: 预测离散类别。
例如:
- 产品质量是否合格。
- 客户是否可能流失。
- 交易是否存在欺诈风险。
- 葡萄酒属于哪个品种类别。
回归问题: 预测连续数值。
例如:
- 建筑采暖负荷。
- 商品未来销量。
- 设备剩余使用寿命。
- 订单配送时间。
在 scikit-learn 中,分别对应:
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import DecisionTreeRegressor
两者的模型结构非常相似,主要区别在于节点分裂时优化的目标,以及叶节点的输出方式。
二、决策树为什么知道应该按哪个特征分裂?
这是理解决策树最重要的问题。
假设我们拥有 100 条样本,包含温度、颜色、酒精浓度等变量。
决策树怎么知道应该先按照温度划分,还是先按照酒精浓度划分?
答案是:计算每一种候选分裂能够让样本变得多么纯粹。
所谓纯粹,就是划分后的样本尽可能属于相同类别。
1. 基尼不纯度(Gini Impurity)
基尼不纯度是 CART 分类决策树常用的分裂指标。
定义为:
[
Gini=1-\sum_{k=1}^{K}p_k^2
]
其中,(p_k) 表示当前节点中属于类别 (k) 的样本比例。
假设某个节点有 10 条记录:
- 6 条属于类别 A。
- 4 条属于类别 B。
那么:
[
Gini=1-0.6^2-0.4^2=0.48
]
如果所有样本属于同一个类别:
[
Gini=1-1^2=0
]
说明这个节点已经完全纯净。
对于二分类问题,基尼不纯度的最大值为 0.5,对应两个类别各占一半。
2. 决策树如何寻找最佳分裂点?
继续使用刚才的 10 条记录。
假设根据某个特征划分后:
| 节点 | 类别 A | 类别 B | Gini |
|---|---|---|---|
| 分裂前 | 6 | 4 | 0.48 |
| 左子节点 | 5 | 0 | 0 |
| 右子节点 | 1 | 4 | 0.32 |
左右节点的加权基尼不纯度为:
[
Gini_{\text{split}}
=\frac{5}{10}\times0+
\frac{5}{10}\times0.32
=0.16
]
因此,分裂带来的基尼不纯度下降为:
[
\Delta Gini=0.48-0.16=0.32
]
决策树会比较不同特征及其候选阈值,寻找能够最大程度降低节点不纯度的分裂方案。
这一过程会递归进行,直到满足停止条件。
3. 信息熵与信息增益
除了 Gini,分类决策树还可以使用信息熵(Entropy)。
信息熵定义为:
[
H(S)=-\sum_{k=1}^{K}p_k\log_2p_k
]
信息增益则定义为:
[
IG=H(S)-\sum_j\frac{|S_j|}{|S|}H(S_j)
]
其中,(S_j) 是分裂后的子节点。
信息增益越大,说明该分裂越能减少类别的不确定性。
经典决策树算法之间存在一些区别:
| 算法 | 主要特点 |
|---|---|
| ID3 | 使用信息增益选择特征 |
| C4.5 | 使用信息增益率,并支持连续变量处理等改进 |
| CART | 使用二叉树结构,支持分类和回归 |
scikit-learn 的决策树主要采用 CART 思路。分类任务可以使用 gini、entropy 等准则。
三、案例一:使用决策树识别葡萄酒类别
1. 业务背景
第一个案例使用 UCI 的经典 Wine 数据集。
该数据集包含意大利同一地区、来自三个不同葡萄栽培品种的葡萄酒化学分析结果。
数据共包含:
- 178 条葡萄酒样本。
- 13 个数值特征。
- 3 个分类标签。
三个类别的样本量分别为 59、71 和 48。
主要特征如下:
| 特征 | 中文含义 |
|---|---|
alcohol |
酒精含量 |
malic_acid |
苹果酸 |
ash |
灰分 |
alcalinity_of_ash |
灰分碱度 |
magnesium |
镁含量 |
total_phenols |
总酚 |
flavanoids |
黄酮类物质 |
color_intensity |
颜色强度 |
hue |
色调 |
proline |
脯氨酸 |
其余变量还包括非黄酮酚、原花青素以及稀释葡萄酒的光密度比值等。
这个案例很适合研究决策树,因为样本量较小、特征均为数值,并且可以直观展示模型如何根据化学检测指标生成分类规则。
2. 数据准备
scikit-learn 已经内置了 Wine 数据集。
因此,无需额外下载文件。
安装依赖:
pip install scikit-learn pandas numpy matplotlib
导入数据:
from sklearn.datasets import load_wine
wine = load_wine(as_frame=True)
X = wine.data
y = wine.target
print(X.shape)
print(y.value_counts())
这里的目标标签为:
0:类别 0
1:类别 1
2:类别 2
注意,这些标签只是公开数据集中的类别编号,并不表示葡萄酒质量等级高低。
3. Python 完整实践:分类树训练与剪枝调参
这里设计三个步骤:
第一,划分训练集和独立测试集。
第二,通过交叉验证选择决策树的深度、叶节点样本数与剪枝参数。
第三,使用最佳模型进行预测,并输出决策规则。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.model_selection import (
train_test_split,
GridSearchCV,
StratifiedKFold
)
from sklearn.tree import (
DecisionTreeClassifier,
plot_tree,
export_text
)
from sklearn.metrics import (
accuracy_score,
f1_score,
classification_report,
confusion_matrix
)
# 1. 加载葡萄酒数据
wine = load_wine(as_frame=True)
X = wine.data
y = wine.target
print("数据规模:", X.shape)
print("类别分布:")
print(y.value_counts())
# 2. 划分训练开发集和测试集
X_dev, X_test, y_dev, y_test = train_test_split(
X,
y,
test_size=0.2,
stratify=y,
random_state=42
)
# 3. 定义决策树超参数搜索范围
param_grid = {
"max_depth": [2, 3, 4, 5, None],
"min_samples_leaf": [1, 2, 4],
"ccp_alpha": [0.0, 0.002, 0.005, 0.01]
}
# 4. 五折交叉验证
cv = StratifiedKFold(
n_splits=5,
shuffle=True,
random_state=42
)
search = GridSearchCV(
DecisionTreeClassifier(random_state=42),
param_grid=param_grid,
scoring="f1_macro",
cv=cv,
n_jobs=-1
)
# 5. 模型训练与调参
search.fit(X_dev, y_dev)
model = search.best_estimator_
print("最佳参数:", search.best_params_)
print("交叉验证F1:", search.best_score_)
# 6. 在独立测试集上预测
pred = model.predict(X_test)
print(
"Accuracy:",
accuracy_score(y_test, pred)
)
print(
"Macro F1:",
f1_score(y_test, pred, average="macro")
)
print("分类报告:")
print(
classification_report(
y_test,
pred,
target_names=wine.target_names
)
)
print("混淆矩阵:")
print(confusion_matrix(y_test, pred))
# 7. 输出树的结构信息
print("决策树深度:", model.get_depth())
print("叶节点数量:", model.get_n_leaves())
# 8. 将决策树转换成文本规则
rules = export_text(
model,
feature_names=list(X.columns),
max_depth=5
)
print("决策规则:")
print(rules)
# 9. 可视化决策树
plt.figure(figsize=(18, 10))
plot_tree(
model,
feature_names=list(X.columns),
class_names=list(wine.target_names),
filled=True,
rounded=True,
fontsize=9
)
plt.tight_layout()
plt.savefig(
"wine_decision_tree.png",
dpi=180,
bbox_inches="tight"
)
plt.show()
# 10. 输出特征重要性
importance = pd.Series(
model.feature_importances_,
index=X.columns
).sort_values(ascending=False)
print("特征重要性:")
print(importance)
4. 实际运行结果
我使用 scikit-learn 1.8.0、固定随机种子 42,执行了上述训练与调参流程。
得到的最佳参数为:
ccp_alpha = 0.0
max_depth = 4
min_samples_leaf = 1
测试集结果为:
| 指标 | 实测结果 |
|---|---|
| 测试样本数 | 36 |
| Accuracy | 94.44% |
| Macro-F1 | 0.9457 |
| 决策树深度 | 4 |
| 叶节点数量 | 8 |
混淆矩阵为:
[[11 1 0]
[ 0 14 0]
[ 0 1 9]]
可以看到,36 条测试记录中,模型正确分类了 34 条。
不过,这个数据集只有 178 条样本,测试样本也只有 36 条。不同的随机划分可能导致评价指标明显波动,因此不能只凭一次测试结果判断模型的长期泛化能力。
5. 决策树究竟学到了哪些规则?
与许多复杂模型相比,决策树最大的特点是可以直接提取判断规则。
例如,在本次训练中,决策树根节点采用了颜色强度:
color_intensity <= 3.82
其他分裂还使用了:
ash
flavanoids
proline
malic_acid
模型内部的一部分判断规则如下:
颜色强度 <= 3.82
|
+-- 灰分 <= 3.00
| |
| +-- 光密度比值 <= 3.73
| | |
| | +--> 类别 1
| |
| +-- 光密度比值 > 3.73
| |
| +--> 类别 0
|
+-- 灰分 > 3.00
|
+--> 类别 0
这里展示的是实测模型的一部分决策路径。
模型会自动寻找对当前数据最有区分能力的特征,并生成判断阈值。
这使得决策树非常适合需要解释预测依据的业务场景。
例如,在产品质量控制中,可以将训练结果转换为可供人工检查的规则。
但要注意,决策树反映的是训练数据中的统计规律。阈值的形成受样本分布、模型参数和特征相关性影响,并不必然对应某种科学上的物理临界点。
四、案例二:建筑采暖负荷预测——决策树如何解决回归问题?
前一个案例的输出是离散类别。
现在我们换一个场景:根据建筑结构参数,预测建筑需要多少采暖负荷。
这属于典型的回归问题。
1. 业务背景
第二个案例使用 UCI 的 Energy Efficiency 数据集。
该数据集来自 2012 年的建筑能源性能研究,通过建筑能耗模拟软件 Ecotect,对不同建筑设计方案进行分析。
研究涉及 12 种建筑形状以及不同窗户面积、朝向和玻璃分布等设计条件,共构造了 768 条模拟样本。
输入特征为 8 个建筑设计参数。
输出包含两个连续目标:
Y1:Heating Load,采暖负荷。Y2:Cooling Load,制冷负荷。
本案例重点预测采暖负荷 Y1。
主要变量如下:
| 字段 | 中文含义 |
|---|---|
X1 |
建筑相对紧凑度 |
X2 |
表面积 |
X3 |
墙面面积 |
X4 |
屋顶面积 |
X5 |
建筑整体高度 |
X6 |
建筑朝向 |
X7 |
玻璃窗面积比例 |
X8 |
玻璃窗面积分布 |
Y1 |
采暖负荷 |
Y2 |
制冷负荷 |
这份数据集是模拟建筑能源性能数据,适合研究回归模型如何学习建筑几何参数与能源需求之间的非线性关系。
2. 回归树和分类树有什么区别?
分类树需要判断样本属于哪个类别。
回归树则需要预测一个连续数值。
例如,一棵回归树可能学习到:
建筑整体高度 > 4?
/ \
是 否
| |
相对紧凑度 > 0.7? 窗户比例 > 0.2?
/ \ / \
是 否 是 否
| | | |
28 35 17 12
这里的采暖负荷预测值属于示意结果。
在使用平方误差作为分裂准则时,回归树通过寻找能够最大程度降低节点内部平方误差的分裂条件建立模型。
其目标可以写成:
[
SSE=\sum_{i=1}^{n}(y_i-\bar y)^2
]
其中:
- (y_i):真实目标值。
- (\bar y):节点内部目标值的平均值。
对于每个候选分裂:
[
SSE_{\text{split}}=SSE_{\text{left}}+SSE_{\text{right}}
]
模型选择能够最大程度降低 SSE 的分裂方式。
最终,每个叶节点输出该节点训练样本目标值的平均值。
3. Python 实践:预测建筑采暖负荷
本案例直接使用 UCI 官方提供的 ucimlrepo 数据加载工具。
安装依赖:
pip install ucimlrepo scikit-learn pandas numpy matplotlib
首次运行需要联网获取数据。
也可以前往 UCI Machine Learning Repository,搜索 Energy Efficiency,下载 ENB2012_data.xlsx 后自行通过 Pandas 读取。
完整代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from ucimlrepo import fetch_ucirepo
from sklearn.model_selection import (
train_test_split,
GridSearchCV
)
from sklearn.tree import (
DecisionTreeRegressor,
plot_tree
)
from sklearn.dummy import DummyRegressor
from sklearn.metrics import (
mean_absolute_error,
mean_squared_error,
r2_score
)
# 1. 获取UCI Energy Efficiency数据集
dataset = fetch_ucirepo(id=242)
X = dataset.data.features
y = dataset.data.targets["Y1"]
print("数据规模:", X.shape)
print("目标变量:", y.name)
# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
random_state=42
)
# 3. 建立一个简单基线模型
# 始终预测训练集目标值的平均值
baseline = DummyRegressor(strategy="mean")
baseline.fit(X_train, y_train)
baseline_pred = baseline.predict(X_test)
print(
"Baseline MAE:",
mean_absolute_error(
y_test,
baseline_pred
)
)
# 4. 定义决策树参数搜索范围
param_grid = {
"max_depth": [2, 3, 4, 5, 6, 8, None],
"min_samples_leaf": [1, 3, 5, 10]
}
# 5. 使用5折交叉验证调参
search = GridSearchCV(
DecisionTreeRegressor(random_state=42),
param_grid=param_grid,
scoring="neg_mean_absolute_error",
cv=5,
n_jobs=-1
)
search.fit(X_train, y_train)
model = search.best_estimator_
print("最佳参数:", search.best_params_)
# 6. 在测试集上预测
pred = model.predict(X_test)
# 7. 计算评价指标
mae = mean_absolute_error(y_test, pred)
rmse = np.sqrt(
mean_squared_error(y_test, pred)
)
r2 = r2_score(y_test, pred)
print("\nDecision Tree Regression")
print("MAE:", round(mae, 4))
print("RMSE:", round(rmse, 4))
print("R²:", round(r2, 4))
# 8. 输出树的复杂度
print("树深度:", model.get_depth())
print("叶节点数量:", model.get_n_leaves())
# 9. 查看特征重要性
importance = pd.Series(
model.feature_importances_,
index=X.columns
).sort_values(ascending=False)
print("\n特征重要性:")
print(importance)
# 10. 可视化回归树的前三层
plt.figure(figsize=(18, 9))
plot_tree(
model,
feature_names=list(X.columns),
max_depth=3,
filled=True,
rounded=True,
fontsize=9
)
plt.tight_layout()
plt.savefig(
"building_energy_tree.png",
dpi=180,
bbox_inches="tight"
)
plt.show()
# 11. 导出预测结果
result = X_test.copy()
result["actual_heating_load"] = y_test
result["predicted_heating_load"] = pred
result["absolute_error"] = np.abs(
result["actual_heating_load"]
- result["predicted_heating_load"]
)
result.to_csv(
"energy_predictions.csv",
index=False,
encoding="utf-8-sig"
)
print("预测结果已保存")
4. 如何评价回归模型?
本案例重点使用三个指标。
| 指标 | 含义 | 判断方向 |
|---|---|---|
| MAE | 平均绝对误差 | 越低越好 |
| RMSE | 均方根误差 | 越低越好 |
| R² | 决定系数 | 越高通常越好 |
例如,假设某次实验得到:
Baseline MAE: 8.20
Decision Tree Regression
MAE: 2.15
RMSE: 3.40
R²: 0.88
以上数值仅用于演示评价方法,属于假设结果。
如果真实实验中出现这样的改善,说明决策树成功学习了建筑结构参数与采暖负荷之间的关系。
在这个案例中,最值得关注的是:
决策树是否能够根据建筑面积、紧凑度、高度和窗户参数,生成不同的采暖负荷预测规则?
对于建筑设计人员来说,模型提供了一种快速估计不同设计方案能源需求的方法。
不过,本数据集中的样本来自有限种类的模拟建筑设计。随机划分后的训练集和测试集可能包含同类建筑形状的不同设计配置。
因此,上述测试结果主要反映模型对相近设计空间的预测能力。
如果希望验证模型能否泛化到从未见过的建筑形状,应该按照建筑形状分组划分训练集和测试集,并重新评价性能。
5. 回归树为什么容易出现阶梯状预测?
这是回归决策树的一个重要特征。
假设一个叶节点中有四条采暖负荷记录:
18.2
19.4
17.8
20.6
那么该叶节点的预测结果就是:
[
\hat y=\frac{18.2+19.4+17.8+20.6}{4}=19.0
]
只要新样本落入这个叶节点,模型就会输出 19.0。
因此,回归树的预测函数通常呈现分段常数形式。
例如:
建筑高度范围 预测采暖负荷
2.0~3.0 12
3.0~4.0 16
4.0~5.0 24
5.0~6.0 30
即使建筑高度从 3.1 增加到 3.9,预测结果也可能保持不变。
这使得回归树对非线性关系具有较好的表达能力,但预测曲线可能缺乏平滑性。
另外,使用平方误差准则的普通回归树,叶节点输出由训练样本均值决定,因此对训练目标值范围之外的长期外推能力较弱。
如果实际任务要求预测不断增长的长期趋势,可以进一步比较线性回归、广义加性模型或其他适合趋势外推的方法。
五、决策树最大的挑战:过拟合
决策树的结构非常灵活。
如果不对树的复杂度进行限制,它可以不断分裂节点,直到每个叶节点只剩下极少数训练样本。
例如:
原始数据
|
第1次分裂
/ \
第2次 第3次
/ \ / \
... ... ... ...
|
持续分裂
|
每个叶节点
只有1个样本
此时,模型可能在训练集上获得极高的准确率。
但问题在于,部分细小规则可能只是训练样本中的随机波动。
在新数据上,这些规则可能失效。
这就是过拟合(Overfitting)。
1. 预剪枝:提前限制树的生长
最常见的方式是通过超参数限制树的复杂度。
| 参数 | 含义 | 主要作用 |
|---|---|---|
max_depth |
最大树深度 | 防止树无限生长 |
min_samples_split |
节点分裂的最少样本数 | 限制小节点继续分裂 |
min_samples_leaf |
叶节点的最少样本数 | 避免产生过小叶节点 |
max_leaf_nodes |
最大叶节点数量 | 限制树的整体规模 |
min_impurity_decrease |
最小不纯度下降 | 只允许收益足够大的分裂 |
ccp_alpha |
成本复杂度剪枝参数 | 控制后剪枝强度 |
例如:
model = DecisionTreeClassifier(
max_depth=5,
min_samples_leaf=10,
random_state=42
)
这意味着:
- 决策树深度最多为 5。
- 每个叶节点至少包含 10 个训练样本。
这样的模型通常比完全不加限制的深树更简单。
但限制过于严格,也可能导致欠拟合。
2. 后剪枝:先生成树,再删除不必要的分支
除了限制树的生长,还可以在训练之后对树进行剪枝。
一个经典方法是 Cost-Complexity Pruning,成本复杂度剪枝。
其目标函数为:
[
R_\alpha(T)=R(T)+\alpha|T|
]
其中:
- (R(T)):树的叶节点总不纯度代价。
- (|T|):叶节点数量。
- (\alpha):控制模型复杂度的参数。
当 (\alpha) 增大时,模型会更加重视结构简单的树。
因此:
ccp_alpha=0:通常不进行成本复杂度后剪枝。- 较小的正值:删除部分复杂分支。
- 更大的值:得到更简单的树。
在 scikit-learn 中,可以使用:
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(
ccp_alpha=0.005,
random_state=42
)
3. 如何自动寻找合适的剪枝参数?
可以使用 cost_complexity_pruning_path() 取得候选剪枝参数。
下面继续使用葡萄酒案例中的训练开发集:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
import numpy as np
# 1. 计算剪枝路径
base_model = DecisionTreeClassifier(
random_state=42
)
path = base_model.cost_complexity_pruning_path(
X_dev,
y_dev
)
# 最后一个alpha通常对应只有根节点的树
alphas = np.unique(path.ccp_alphas[:-1])
# 2. 逐个验证剪枝参数
results = []
for alpha in alphas:
tree = DecisionTreeClassifier(
ccp_alpha=float(alpha),
random_state=42
)
scores = cross_val_score(
tree,
X_dev,
y_dev,
cv=5,
scoring="f1_macro"
)
results.append({
"alpha": alpha,
"mean_f1": scores.mean()
})
# 3. 选择平均F1最高的参数
best = max(
results,
key=lambda row: row["mean_f1"]
)
print("最佳剪枝alpha:", best["alpha"])
print("交叉验证F1:", best["mean_f1"])
这是一段独立的剪枝参数演示代码。更严谨的实验应当在各训练折内部生成剪枝候选,或者通过嵌套交叉验证评价完整调参过程,避免选择过程产生额外乐观偏差。
实际项目中,剪枝的目标也不一定是单纯最大化预测准确率。
如果业务需要清晰、简短的判断规则,那么一棵只有 6 个叶节点、准确率略低的树,可能比一棵拥有 200 个叶节点的树更加实用。
六、如何理解决策树的特征重要性?
决策树可以直接计算特征重要性:
importance = model.feature_importances_
这个指标基于特征在整棵树的分裂过程中带来的不纯度下降。
例如,某次实验的特征重要性可能表现为:
| 特征 | 重要性 |
|---|---|
| 颜色强度 | 0.42 |
| 黄酮类含量 | 0.31 |
| 脯氨酸 | 0.15 |
| 苹果酸 | 0.08 |
| 其他变量 | 0.04 |
以上为示意数据。
可以看到,模型在分类过程中主要依赖少数几个变量。
但特征重要性需要谨慎解释。
如果两个特征高度相关,决策树可能优先使用其中一个进行分裂,导致另一个特征的重要性偏低。
此外,基于不纯度下降的特征重要性也可能偏向取值丰富的变量。
因此,特征重要性可以帮助理解模型在预测时使用了哪些信息,但不能直接证明某个变量对结果的因果影响。
如果需要更稳健地评价特征贡献,可以进一步使用验证集上的 Permutation Importance,观察打乱某个变量后模型性能下降多少。
七、决策树、随机森林和 XGBoost 应该怎么选?
学习完决策树之后,就更容易理解随机森林和 XGBoost 的工作方式。
因为它们都可以使用决策树作为基础学习器。
但三者的训练逻辑存在明显差异。
| 维度 | 决策树 | 随机森林 | XGBoost |
|---|---|---|---|
| 模型结构 | 单棵树 | 多棵树集成 | 多棵树逐轮提升 |
| 核心思想 | 递归划分数据 | Bagging 降低方差 | Gradient Boosting 优化损失 |
| 可解释性 | 较高 | 中等 | 通常需要额外解释工具 |
| 非线性能力 | 有 | 有 | 有 |
| 过拟合风险 | 深树容易过拟合 | 集成通常更稳定 | 需要控制复杂度与学习率 |
| 调参复杂度 | 相对简单 | 中等 | 通常较复杂 |
| 典型应用 | 规则提取、教学、简单预测 | 结构化数据分类与回归 | 高性能结构化数据预测 |
1. 决策树:追求直观规则
决策树最大的优势是模型结构清晰。
对于一棵深度只有 3~5 的树,业务人员可以直接检查判断规则。
因此,决策树非常适合:
- 需要明确判断路径的业务。
- 希望快速理解变量之间的关系。
- 需要将模型转换为简单规则的场景。
2. 随机森林:降低单棵树的波动
随机森林通过 Bootstrap 抽样和随机特征选择,训练大量具有差异的决策树。
最后对预测结果进行综合。
其主要优势在于降低单棵决策树的预测方差。
例如:
训练数据
|
+-- 决策树1
|
+-- 决策树2
|
+-- 决策树3
|
+-- ...
|
+-- 决策树300
|
v
综合预测
不同树之间的误差能够在一定程度上相互抵消。
3. XGBoost:不断修正已有模型的误差
XGBoost 使用梯度提升思想,后续树根据当前模型的损失函数优化方向进行训练。
例如:
第一棵树
|
v
当前预测
|
v
计算损失梯度
|
v
训练第二棵树
|
v
更新整体预测
|
v
继续迭代
因此,XGBoost 能够逐轮改善整体模型的预测表现。
对于复杂结构化数据,它通常具有较强竞争力。
但在只有少量样本、需要直接解释规则的任务中,一棵经过合理剪枝的决策树仍然具有实际价值。
八、真实项目中需要注意哪些问题?
1. 数据泄漏
假设要预测某产品是否合格,但数据中包含了质检完成后才生成的检测结论。
模型可能轻易获得接近 100% 的准确率。
但在实际预测时,这个变量尚未产生。
因此,建模时需要明确:哪些特征能够在预测时点被获得?
对于建筑采暖负荷预测,输入应当来自建筑设计参数或预测时点已经可获得的信息。
对于葡萄酒分类,输入应当来自分类决策前完成的化学检测。
2. 决策树容易受到样本变化影响
单棵决策树具有较高的方差。
例如,在一个小数据集上改变少量训练记录,模型的根节点特征就可能发生变化。
这会导致整棵树的结构明显不同。
因此,正式使用之前,应通过交叉验证、重复抽样或跨时间验证,检查模型表现是否稳定。
3. 决策树通常不需要数值标准化
决策树根据特征阈值划分样本。
例如:
alcohol <= 13.5
如果将酒精含量转换成其他具有相同排序关系的数值尺度,模型仍然可以寻找相应的分裂位置。
因此,决策树通常无需像 KNN、SVM 或部分线性模型那样进行标准化。
不过,类别特征仍然需要合适的编码方式。直接把无序类别转换成连续整数,可能引入人为的大小关系。
4. 模型概率未必可靠
对于分类树,predict_proba() 通常根据叶节点内部的类别比例估计概率。
例如,某个叶节点有 20 条训练记录,其中 16 条属于类别 A:
[
P(A)=\frac{16}{20}=80%
]
如果叶节点只有一条训练记录,那么模型可能输出 100% 的类别概率。
这种概率往往存在较大的不确定性。
因此,当模型输出需要用于实际风险决策时,除了检查 AUC、F1 或 Accuracy,还应评价概率校准情况。
九、总结
决策树通过不断寻找最优特征与分裂阈值,将复杂的预测问题转化为一系列可以解释的判断规则。
对于分类任务,CART 决策树可以根据 Gini 等不纯度指标选择分裂方案。
对于回归任务,决策树可以通过降低节点内部平方误差,预测连续变量。
本文两个案例展示了决策树的不同用途。
葡萄酒分类展示了决策树如何学习化学成分与类别之间的关系,并将预测模型转换为可解释的判断规则。在本次实测中,经过交叉验证调参的决策树,在独立测试集上获得了约 94.44% 的分类准确率。
建筑采暖负荷预测则展示了回归树的基本原理:根据建筑结构和设计参数不断划分样本,并利用叶节点的平均目标值进行预测。
两个案例也说明了决策树的重要局限。
如果树生长得过深,模型可能记住训练数据中的局部噪声;如果树过浅,又可能无法学习足够复杂的规律。
因此,实际开发中需要重点关注:
- 分裂准则: 理解 Gini、Entropy 和平方误差如何影响树的构建。
- 复杂度控制: 通过最大深度、最少叶节点样本数和剪枝防止过拟合。
- 模型验证: 使用独立测试集与交叉验证判断模型的泛化表现。
- 规则解释: 将树结构、叶节点和特征重要性转化为业务能够理解的判断依据。
从机器学习的发展来看,决策树还是随机森林、XGBoost 和 LightGBM 等重要算法的基础。
掌握决策树,最重要的是理解模型如何通过一次次局部最优分裂建立预测规则,以及为什么限制树的复杂度有时反而能让模型在新数据上表现得更好。
理解了这一点,再学习 Bagging、Boosting 和梯度提升树,就能够更清楚地掌握现代树模型的核心逻辑。
- 点赞
- 收藏
- 关注作者
评论(0)