从树到森林——决策树、随机森林与可解释性博弈

从树到森林——决策树、随机森林与可解释性博弈

从树到森林——决策树、随机森林与可解释性博弈

“如果你不能向酒吧侍者解释清楚你的模型,那你可能还没真正理解它。”
而决策树,正是那个既能讲清道理,又能打胜仗的算法。

一、为什么需要树模型?

线性模型优雅、透明,但它有一个致命假设:特征与目标之间是线性关系
现实世界却充满非线性、交互效应和分段规则:

  • “如果年龄 > 60 且 血压 > 140,则高风险”;
  • “当用户点击过广告 A 且未购买,则推送优惠券 B”。

这些条件判断天然适合用“树”来表达。

🎯 本章目标:理解决策树如何通过“提问”进行预测;掌握信息增益、基尼不纯度等分裂准则;实现一棵简单的决策树;理解集成思想:从单棵树到随机森林;辩证看待“可解释性”:树真的那么透明吗?

二、决策树:用问答游戏做预测

1. 直觉:像玩“20个问题”游戏

想象你在猜一个名人:

  • “是男性吗?” → 是
  • “还活着吗?” → 否
  • “是科学家吗?” → 是
  • ……

每一步都根据答案缩小范围,最终锁定目标。

决策树正是如此:通过一系列 if-else 规则,将样本分到不同叶子节点,每个叶子给出一个预测值(分类标签或回归均值)。

2. 树的结构

  • 根节点(Root):第一个判断条件;
  • 内部节点(Internal Node):中间判断;
  • 叶子节点(Leaf):最终预测结果;
  • 分裂(Split):选择一个特征和阈值,将数据分为两组。
💡 决策树不需要特征缩放、能自动处理类别变量、对异常值鲁棒——这是它广受欢迎的原因。

三、如何构建一棵好树?——分裂准则

关键问题:在每个节点,该选哪个特征、哪个阈值来分裂?

目标:让子节点尽可能“纯净”(即同一类样本聚集在一起)。

1. 分类任务:基尼不纯度 vs 信息熵

基尼不纯度(Gini Impurity)

对于一个节点,若有 KKK 个类别,第 kkk 类占比为 pkp_kpk​,则:

Gini=1−∑k=1Kpk2 \text{Gini} = 1 - \sum_{k=1}^{K} p_k^2 Gini=1−k=1∑K​pk2​

  • Gini = 0:完全纯净(所有样本属于同一类);
  • Gini 最大:各类均匀分布。
信息熵(Entropy)

源自信息论:

Entropy=−∑k=1Kpklog⁡2pk \text{Entropy} = -\sum_{k=1}^{K} p_k \log_2 p_k Entropy=−k=1∑K​pk​log2​pk​

  • Entropy = 0:完全确定;
  • Entropy 越大:不确定性越高。
✅ 实践中,基尼不纯度计算更快(无对数),效果与熵相近,sklearn 默认使用 Gini。

2. 回归任务:方差减少(Variance Reduction)

目标:让左右子节点的目标值方差之和最小

分裂后的总方差:

Varleft⋅nleftn+Varright⋅nrightn \text{Var}_{\text{left}} \cdot \frac{n_{\text{left}}}{n} + \text{Var}_{\text{right}} \cdot \frac{n_{\text{right}}}{n} Varleft​⋅nnleft​​+Varright​⋅nnright​​

我们选择使该值最小的特征和切分点。


四、动手实现:从零写一个简易决策树(分类)

为简化,我们只处理数值型特征,并采用递归构建

import numpy as np from collections import Counter classNode: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,则是内部节点)defis_leaf_node(self):return self.value isnotNoneclassDecisionTree: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 =Nonedeffit(self, X, y): self.n_feats = X.shape[1]ifnot self.n_feats elsemin(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 ==1or 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,Nonefor 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)iflen(left_idxs)==0orlen(right_idxs)==0:return0# 加权子节点不纯度 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)return1- 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]defpredict(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每次分裂只考虑部分特征

但即使调参,单棵树的性能仍有限。


六、集成的力量:随机森林

“三个臭皮匠,顶个诸葛亮。”
——中国谚语
随机森林正是这一思想的工程实现。

1. 核心思想:Bagging + 随机特征

  • Bagging(Bootstrap Aggregating):
    从原始数据中有放回地抽样 BBB 次,生成 BBB 个子数据集;
  • 每棵树在子集上独立训练
  • 预测时,分类取多数投票,回归取平均值
🔑 关键创新:每次分裂时,只从随机选择的特征子集中找最佳分裂(如 p\sqrt{p}p​ 个特征,ppp 为总特征数)。
这增加了树之间的多样性,避免所有树都关注最强特征。

2. 为什么有效?

  • 降低方差:多棵树平均后,过拟合被抑制;
  • 保持低偏差:每棵树仍足够深;
  • 自动评估特征重要性
  • 几乎无需调参,默认参数往往表现优异。

七、使用 scikit-learn 快速建模

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被拒贷,因为:收入 10层就成迷宫);

更好的解释工具:SHAP

现代做法是用 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 能告诉你:每个特征对当前预测的贡献是正还是负,有多大

九、树模型 vs 线性模型:何时用谁?

维度线性模型树模型
可解释性全局清晰(系数意义明确)局部清晰(路径可追溯),全局模糊
非线性能力弱(需手动特征工程)强(自动捕捉交互与非线性)
特征缩放必须(影响系数大小)不需要
缺失值处理需预处理部分实现支持(如 LightGBM)
训练速度快(尤其解析解)中等(单树快,森林慢)
预测速度极快快(但森林需遍历多棵树)
默认性能中等高(尤其随机森林)
经验法则:若业务要求严格可解释(如信贷审批),先试线性模型 + 特征工程;若追求高精度且可接受局部解释,用随机森林 + SHAP;若需部署到资源受限设备,考虑剪枝后的单棵树

十、进阶方向:梯度提升树(GBDT)

随机森林通过并行训练+平均降低方差,而 GBDT(如 XGBoost、LightGBM)通过串行训练+残差拟合降低偏差。

  • 每棵树学习前一轮的预测误差
  • 最终预测是所有树的加权和
  • 通常比随机森林更准,但更易过拟合、调参复杂。
我们将在后续章节深入探讨 GBDT。

十一、结语:在透明与性能之间走钢丝

决策树给了我们一个珍贵的启示:模型不必是黑箱才能强大
它像一位经验丰富的老医生,用“如果…那么…”的规则做出判断。

但当我们把100位老医生的意见简单平均(随机森林),虽然诊断更准了,却再也听不到清晰的推理链条。

真正的智能,不是选择“可解释”或“高性能”,而是在两者之间找到平衡点

下一篇文章,我们将进入梯度提升树的世界——那里有更高的精度,也有更深的调参艺术。

但在那之前,请亲手训练一棵树,看看它如何“思考”。


行动建议

  1. 在 Titanic 数据集上训练决策树,可视化前3层
  2. 对比单棵树、随机森林、逻辑回归的准确率与训练时间
  3. 用 SHAP 解释一个随机森林的预测结果
  4. 尝试调整 max_depthmin_samples_leaf,观察过拟合变化
记住:一棵好树,不仅长得高,还要扎得稳

Read more

Java字符串算法核心攻略

Java字符串算法核心攻略

Java字符串算法核心攻略 提示:在 ASCII 码(美国信息交换标准代码)中,大小写英文字母和数字的十进制数值范围如下: * 大写字母 (A - Z):65 到 90 * ‘A’ 是 65 * ‘Z’ 是 90 * 小写字母 (a - z):97 到 122 * ‘a’ 是 97 * ‘z’ 是 122 * 数字是 (0-9):48 到 57 * ‘0’ 是 48 * ‘9’ 是 57 一、必会的字符串方法 刷题前先把这些方法练熟,后面会反复用到。 1.

By Ne0inhk
【动态规划】买卖股票相关问题

【动态规划】买卖股票相关问题

一、买卖股票的最佳时机含手续费 714. 买卖股票的最佳时机含手续费 题目描述: 题目分析: 1、状态分析 其实一共就只有两种状态,不是处于 持有 状态,就是处于 未持有 状态。 * f[i]表示第i天结束后,处于 持有 状态时,最大的利润 * g[i]表示第i天结束后,处于 未持有 状态时,最大的利润 2、状态转移方程 对于f[i],有两种情况可以到达这种状态: * 在第 i-1 天时就已处于 持有 状态,然后第 i 天啥也不干,仍然保持 持有 状态。此时的最大利润就为 f[i - 1]

By Ne0inhk
从冒泡到模拟q sort函数——初见排序算法的探索和思考

从冒泡到模拟q sort函数——初见排序算法的探索和思考

国庆中秋喜相连,万家团圆乐同庆。 各位小伙伴们大家好,我是此方,在此,先祝大家双节快乐! 我们都知道排序有很多种:例如冒泡排序,插入排序,快速排序,等等很多种。 而冒泡排序,是各种计算机语言中最经典的一种排序算法。 今天我将从冒泡排序开始,到实现qsort函数的模拟。逐层深入,探索排序问题。 并给出鄙人的一些拙见。 上正文: 一,冒泡排序:最经典的排序算法 假如有一个十元素整型数组,他是完全倒着排序的:就像这样 now,我们要按照从小到大的顺序将这十个数字重新排列。 如果我们想要用冒泡排序:那么他的逻辑应该是这样的: 首先让最左边的数字和他右边的数字比较:9>8,将9和8互换位置: 让9继续和他右边的数字比较,再互换位 以此类推:9不断的比较——>移动——>再比较:最后;会到达最右边,这样,我们就让最大的数字9放在了最低位置 然后是8,接下来是7,6,5.

By Ne0inhk
算法思想之深度优先搜索(DFS)、递归以及案例(最多能得到多少克黄金、精准核酸检测、最富裕的小家庭)

算法思想之深度优先搜索(DFS)、递归以及案例(最多能得到多少克黄金、精准核酸检测、最富裕的小家庭)

深度优先搜索(DFS)、递归 * 深度优先搜索(Depth First Search,DFS)是一种用于遍历或搜索树或图的算法。在 DFS 算法中,从起始节点开始,沿着一条路径尽可能深地访问节点,直到到达叶子节点或者无法继续前进为止。然后退回到最近的一个有未探索节点的分支节点,继续探索其他路径,直到所有节点都被访问过为止。 * 深度优先搜索常常用于解决以下类型的问题:深度优先搜索是一种简单而强大的算法,可以解决许多实际问题。 * 图遍历:在无向图或有向图中寻找特定节点之间的路径、判断图的连通性等。 * 连通性问题:判断图中是否存在环、判断图的强连通分量等。 * 组合问题:生成排列、组合或子集等组合型问题。 * 寻路问题:求解从起始点到目标点的最短路径或所有可行路径。 * 递归问题:通过递归实现深度优先搜索,例如二叉树的遍历等。 小华最多能得到多少克黄金 * 题目描述小华按照地图去寻宝,地图上被划分成 m 行和 n 列的方格,横纵坐标范围分别是 [0, n-1] 和 [0, m-1]。在横坐标和纵坐标的数位之和不大于 k

By Ne0inhk