从树到森林:决策树、随机森林与可解释性
决策树利用条件规则实现预测,具备良好可解释性但易过拟合。随机森林利用 Bagging 策略与特征随机化降低方差,显著提升泛化能力。SHAP 工具可量化特征贡献以增强黑箱模型透明度。相比线性模型,树模型无需特征缩放且擅长捕捉非线性交互,适用于高精度需求场景。

决策树利用条件规则实现预测,具备良好可解释性但易过拟合。随机森林利用 Bagging 策略与特征随机化降低方差,显著提升泛化能力。SHAP 工具可量化特征贡献以增强黑箱模型透明度。相比线性模型,树模型无需特征缩放且擅长捕捉非线性交互,适用于高精度需求场景。

'如果你不能向酒吧侍者解释清楚你的模型,那你可能还没真正理解它。' 而决策树,正是那个既能讲清道理,又能打胜仗的算法。
线性模型优雅、透明,但它有一个致命假设:特征与目标之间是线性关系。 现实世界却充满非线性、交互效应和分段规则:
这些条件判断天然适合用'树'来表达。
🎯 本章目标:理解决策树如何通过'提问'进行预测;掌握信息增益、基尼不纯度等分裂准则;实现一棵简单的决策树;理解集成思想:从单棵树到随机森林;辩证看待'可解释性':树真的那么透明吗?
想象你在猜一个名人:
每一步都根据答案缩小范围,最终锁定目标。
决策树正是如此:通过一系列 if-else 规则,将样本分到不同叶子节点,每个叶子给出一个预测值(分类标签或回归均值)。

💡 决策树不需要特征缩放、能自动处理类别变量、对异常值鲁棒——这是它广受欢迎的原因。
关键问题:在每个节点,该选哪个特征、哪个阈值来分裂?
目标:让子节点尽可能'纯净'(即同一类样本聚集在一起)。
对于一个节点,若有 K 个类别,第 k 类占比为 p_k,则:
$$\text{Gini} = 1 - \sum_{k=1}^{K} p_k^2$$
源自信息论:
$$\text{Entropy} = -\sum_{k=1}^{K} p_k \log_2 p_k$$
✅ 实践中,基尼不纯度计算更快(无对数),效果与熵相近,sklearn 默认使用 Gini。
目标:让左右子节点的目标值方差之和最小。
分裂后的总方差:
$$\text{Var}{\text{left}} \cdot \frac{n{\text{left}}}{n} + \text{Var}{\text{right}} \cdot \frac{n{\text{right}}}{n}$$
我们选择使该值最小的特征和切分点。
为简化,我们只处理数值型特征,并采用递归构建。
import numpy as np
from collections import Counter
class Node:
def __init__(self, feature=None, threshold=None, left=None, right=None, *, value=None):
self.feature = feature # 分裂特征索引
self.threshold = threshold # 分裂阈值
self.left = left # 左子树
self.right = right # 右子树
self.value = value # 叶子节点的预测值(若为 None,则是内部节点)
def is_leaf_node(self):
return self.value is not None
class DecisionTree:
def __init__(self, min_samples_split=2, max_depth=100, n_feats=None):
self.min_samples_split = min_samples_split
self.max_depth = max_depth
self.n_feats = n_feats # 随机选择部分特征(为后续随机森林做准备)
self.root = None
def fit(self, X, y):
self.n_feats = X.shape[1] if not self.n_feats else min(self.n_feats, X.shape[1])
self.root = self._grow_tree(X, y)
def _grow_tree(self, X, y, depth=0):
n_samples, n_features = X.shape
n_labels = len(np.unique(y))
# 停止条件
if (depth >= self.max_depth or n_labels == 1 or n_samples < self.min_samples_split):
leaf_value = self._most_common_label(y)
return Node(value=leaf_value)
# 随机选择特征子集
feat_idxs = np.random.choice(n_features, self.n_feats, replace=False)
# 寻找最佳分裂
best_feat, best_thresh = self._best_split(X, y, feat_idxs)
# 创建子节点
left_idxs, right_idxs = self._split(X[:, best_feat], best_thresh)
left = self._grow_tree(X[left_idxs, :], y[left_idxs], depth + 1)
right = self._grow_tree(X[right_idxs, :], y[right_idxs], depth + 1)
return Node(best_feat, best_thresh, left, right)
def _best_split(self, X, y, feat_idxs):
best_gain = -1
split_idx, split_thresh = None, None
for feat_idx in feat_idxs:
X_column = X[:, feat_idx]
thresholds = np.unique(X_column)
for th in thresholds:
gain = self._information_gain(y, X_column, th)
if gain > best_gain:
best_gain = gain
split_idx = feat_idx
split_thresh = th
return split_idx, split_thresh
def _information_gain(self, y, X_column, split_thresh):
# 父节点不纯度
parent_gini = self._gini(y)
# 分割
left_idxs, right_idxs = self._split(X_column, split_thresh)
if len(left_idxs) == 0 or len(right_idxs) == 0:
return 0
# 加权子节点不纯度
n = len(y)
n_l, n_r = len(left_idxs), len(right_idxs)
gini_l, gini_r = self._gini(y[left_idxs]), self._gini(y[right_idxs])
child_gini = (n_l / n) * gini_l + (n_r / n) * gini_r
# 信息增益 = 父 - 子
ig = parent_gini - child_gini
return ig
def _gini(self, y):
hist = np.bincount(y)
ps = hist / len(y)
return 1 - np.sum(ps ** 2)
def _split(self, X_column, split_thresh):
left_idxs = np.argwhere(X_column <= split_thresh).flatten()
right_idxs = np.argwhere(X_column > split_thresh).flatten()
return left_idxs, right_idxs
def _most_common_label(self, y):
counter = Counter(y)
return counter.most_common(1)[0][0]
def predict(self, X):
return np.array([self._traverse_tree(x, self.root) for x in X])
def _traverse_tree(self, x, node):
if node.is_leaf_node():
return node.value
if x[node.feature] <= node.threshold:
return self._traverse_tree(x, node.left)
return self._traverse_tree(x, node.right)
✅ 这个实现包含了核心逻辑:递归分裂、基尼不纯度、停止条件。 它也是随机森林的基础(只需稍作修改)。
决策树有一个严重问题:极易过拟合。
| 方法 | 说明 |
|---|---|
max_depth | 限制树的最大深度 |
min_samples_split | 内部节点至少需多少样本才分裂 |
min_samples_leaf | 叶子节点至少需多少样本 |
max_features | 每次分裂只考虑部分特征 |
但即使调参,单棵树的性能仍有限。
'三个臭皮匠,顶个诸葛亮。' ——中国谚语 随机森林正是这一思想的工程实现。
🔑 关键创新:每次分裂时,只从随机选择的特征子集中找最佳分裂(如 $\sqrt{p}$ 个特征,p 为总特征数)。 这增加了树之间的多样性,避免所有树都关注最强特征。
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.datasets import load_wine, make_regression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, mean_squared_error
import matplotlib.pyplot as plt
# 分类示例:葡萄酒数据集
wine = load_wine()
X, y = wine.data, wine.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 单棵决策树
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(X_train, y_train)
print("Decision Tree Accuracy:", accuracy_score(y_test, dt.predict(X_test)))
# 随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
print("Random Forest Accuracy:", accuracy_score(y_test, rf.predict(X_test)))
# 可视化单棵树(前几层)
plt.figure(figsize=(20, 10))
plot_tree(dt, feature_names=wine.feature_names, class_names=wine.target_names, filled=True, max_depth=2)
plt.title("Decision Tree (Depth ≤ 2)")
plt.show()
📊 通常,随机森林的准确率显著高于单棵树,且更稳定。
决策树常被称为'可解释模型',但这需要辩证看待。
你可以追踪一个样本的预测路径:
'客户 A 被拒贷,因为:收入较低且信用分不足';
现代做法是用 SHAP(SHapley Additive exPlanations)解释树模型:
import shap
explainer = shap.TreeExplainer(rf)
shap_values = explainer.shap_values(X_test[:1])
# 解释第一个测试样本
shap.initjs()
shap.force_plot(explainer.expected_value[0], shap_values[0], X_test[:1], feature_names=wine.feature_names)
SHAP 能告诉你:每个特征对当前预测的贡献是正还是负,有多大。
| 维度 | 线性模型 | 树模型 |
|---|---|---|
| 可解释性 | 全局清晰(系数意义明确) | 局部清晰(路径可追溯),全局模糊 |
| 非线性能力 | 弱(需手动特征工程) | 强(自动捕捉交互与非线性) |
| 特征缩放 | 必须(影响系数大小) | 不需要 |
| 缺失值处理 | 需预处理 | 部分实现支持(如 LightGBM) |
| 训练速度 | 快(尤其解析解) | 中等(单树快,森林慢) |
| 预测速度 | 极快 | 快(但森林需遍历多棵树) |
| 默认性能 | 中等 | 高(尤其随机森林) |
✅ 经验法则:若业务要求严格可解释(如信贷审批),先试线性模型 + 特征工程;若追求高精度且可接受局部解释,用随机森林 + SHAP;若需部署到资源受限设备,考虑剪枝后的单棵树。
随机森林通过并行训练 + 平均降低方差,而 GBDT(如 XGBoost、LightGBM)通过串行训练 + 残差拟合降低偏差。
我们将在后续章节深入探讨 GBDT。
决策树给了我们一个珍贵的启示:模型不必是黑箱才能强大。 它像一位经验丰富的老医生,用'如果…那么…'的规则做出判断。
但当我们把 100 位老医生的意见简单平均(随机森林),虽然诊断更准了,却再也听不到清晰的推理链条。
真正的智能,不是选择'可解释'或'高性能',而是在两者之间找到平衡点。
下一篇文章,我们将进入梯度提升树的世界——那里有更高的精度,也有更深的调参艺术。
但在那之前,请亲手训练一棵树,看看它如何'思考'。
max_depth 和 min_samples_leaf,观察过拟合变化。记住:一棵好树,不仅长得高,还要扎得稳。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online