☰
决策树从if-else到数学之美:信息增益与剪枝实战
2026/9/28 6:19:17 网站建设 项目流程

提到“机器学习”,很多初学者脑子里蹦出来的第一个画面可能是神经网络那一堆神秘符号,或者卷积、注意力机制,但翻开周志华老师的《机器学习》(西瓜书)第4章,迎面而来的却是一个非常“程序员友好”的概念——决策树。我当年第一次看这一章时的第一反应是:这不就是把一层一层的 if-else 堆起来而已吗?直觉没有错,决策树的推理过程和 if-else 几乎同构:每个内部节点是一个特征判断,每个分支对应一个判断结果,叶子节点给出最终结论。那问题来了:既然 if-else 谁都会写,决策树在“数学之美”上到底美在哪里?

答案藏在三个字里——怎么选。if-else 的顺序是人工拍的,而决策树的判断顺序是算法算出来的。同样是做分类,你是先问“是否为年轻人”还是先问“是否有房”?决策树不需要拍脑袋,它用信息论帮你算出一个全局最优的划分顺序。这也是西瓜书+南瓜书第4章最值得啃透的地方。这篇文章就是我自己的学习日志加踩坑记录,覆盖从信息熵、信息增益到 ID3/C4.5/CART 的完整推导,同时附上 sklearn 在鸢尾花分类和收入预测上的实操代码,最后聊聊剪枝、连续值与缺失值处理,以及“随机森林和决策树到底有什么区别”这个高频面试问题。无论你是刚入门机器学习的新手,还是准备面试需要系统温习决策树的老同学,这篇都值得你收藏后慢慢读。

1. 决策树的直觉解构:它不就是个高级 if-else 吗

1.1 决策树到底长什么样

决策树的物理结构很简单:一棵树,最上面叫根节点,中间叫内部节点,最下面叫叶子节点。每个内部节点对应一个特征的判断,每条边对应一个判断结果,每个叶子节点对应一个类别或数值。说人话就是,它把“如果...那么...否则...”这种规则组织成了一棵树。

我举一个最通俗的例子:判断一个人是否有还款能力。第一个判断可以是“年收入是否大于10万”,如果是,再看“是否有房贷”,如果否,直接判断为“有还款能力”。这个流程画出来就是一棵三层的树。你在实际业务里写过类似的 if-else 规则逻辑,那你已经掌握了决策树的“形”。

但形似不等于神似。人工写规则时,你靠的是业务经验和肉眼观察,比如“年轻人可能没钱”,这种规则往往带偏见,而且在特征多的时候根本不work。决策树的厉害之处在于,它自动从数据里学到“哪条规则该放前面,阈值该取多少”,整个过程不需要人干预。

1.2 从 if-else 到数学之美:核心差异

那“数学之美”到底是什么?我理解就是两件事:第一,把“怎么选特征、选什么阈值”这个决策问题形式化成一个可优化的数学目标;第二,用信息论给“不确定性的减少”做定量刻画。人工 if-else 完全没有这两个维度,你怎么排规则的顺序,完全靠猜。而决策树从数据中计算每个特征带来的“纯度提升”,然后贪心地选全局最优的那个特征作为当前划分。

这个过程有点像玩“猜物品”游戏。假设我心里想了一个水果,你每问一个问题(比如“是热带水果吗”),答案都让你离答案更近一步。如果你问的是“好吃吗”这种主观问题,信息量很低;如果你问“是苹果吗”,一次就能猜中,信息量极高。决策树做的就是:在每一步选一个“信息量最大”的问题来问。这也是机器学习里“贪心算法”和“人类经验”最直观的碰撞。

2. 决策树背后的数学:信息熵、信息增益与三种经典算法

2.1 信息熵:不确定性的一把尺

先记住一句话:信息熵越大,数据越“乱”。假设一个数据集 D 里,第 k 类样本所占比例为 p_k(k=1,2,...,|Y|),那么信息熵定义为:

Ent(D) = - Σ_{k=1}^{|Y|} p_k · log2(p_k)

这个公式里对数底数取2,单位是比特。为什么用 log?因为信息是可以“叠加”的,两个独立事件的信息量相加,而概率相乘,log 正好把乘法变加法。举个具体例子:如果 D 里全是正样本,p=1,那 Ent(D) = -1·log2(1) = 0,说明数据一点不乱,纯度最高;如果正负样本各占一半,p=0.5,Ent(D) = -0.5·log2(0.5)-0.5·log2(0.5)=1,说明不确定性最大。

我在南瓜书里看到作者反复强调一个点:信息熵只依赖于概率分布,不依赖于样本数量。也就是说,100个样本中一半正一半负的信息熵,和1000个样本中一半正一半负的信息熵是一样的,都是1。这个理解很重要,后面算信息增益时你就不会被样本量干扰。

2.2 ID3 算法:信息增益最大者优先

ID3 是决策树的开山算法,核心思想很简单:每次划分时,计算每个特征带来的“信息增益”,选增益最大的那个特征。信息增益的公式是:

Gain(D, a) = Ent(D) - Σ_{v=1}^{V} (|D^v| / |D|) · Ent(D^v)

其中 a 是特征,有 V 个取值,D^v 是第 v 个取值对应的子集。这个公式的内涵是:划分前有一个初始熵 Ent(D),划分后按比例加权各子集的熵,前后之差就是这次划分“减少的不确定性”,也就是信息增益。

我当时自己手算了一遍西瓜书里的数据集才真正懂了。比如“色泽”这个特征,有三个取值:青绿、乌黑、浅白。先把数据按取值分组,分别计算每个组的熵,再按样本占比加权,最后用根节点的熵一减,就得到信息增益。对照书里的表,0.108这个数值我记得很清楚,因为这个数太小了,我一度怀疑自己算错了,后来才发现是自己把 log 的底数换成了自然对数,导致所有值都偏大。用 sklearn 的时候,默认也是用自然对数的变体,但教材推导都用 log2,建议你手推时统一用 log2,省得自我怀疑。

ID3 有一个很明显的缺点:它偏好取值数目多的特征。极端情况下,如果给每个样本都编一个独一无二的编号,把“编号”当作特征来划分,每个分支只有一个样本,子集的熵全是0,信息增益直接拉满。但这样的树完全没有泛化能力,因为编号没有实际意义。

2.3 C4.5 算法:增益率解决 ID3 的偏好问题

C4.5 就是为了治这个毛病。它不直接用信息增益,而是用增益率:

Gain_ratio(D, a) = Gain(D, a) / IV(a)

其中 IV(a) = - Σ_{v=1}^{V} (|D^v| / |D|) · log2(|D^v| / |D|),叫固有值。特征取值越多,IV 通常越大,相当于给“爱劈叉”的特征做了一次惩罚。但你注意,增益率又会反过来偏好取值少的特征,所以 C4.5 不是直接选增益率最大的,而是先用信息增益筛选出一批候选特征,再在候选里选增益率最高的。这个细节很多人不知道,面试问到了容易翻车。

还有一点,C4.5 在书里是处理连续值、缺失值的重要算法,它用二分法处理连续特征,用“样本权重”处理缺失值,这两块我在第5节单独展开。C4.5 虽然在实际工程里用得不算多,但它的思想全部沉淀在了后来的 CART 和 sklearn 的设计里。

2.4 CART 算法:基尼指数与二叉树

CART 是现在工业界最常用的决策树,sklearn 里的 DecisionTreeClassifier 底层就用它。它最核心的改动是:把树强制做成二叉树,并且用基尼值代替信息熵。

基尼值的公式是:

Gini(D) = 1 - Σ_{k=1}^{|Y|} p_k²

直观理解是:从 D 中随机抽两个样本,它们类别不一致的概率。这个概率越小,数据越纯。基尼指数则是按特征取值加权后的基尼值:

Gini_index(D, a) = Σ_{v=1}^{V} (|D^v| / |D|) · Gini(D^v)

CART 选择基尼指数最小的特征作为划分属性。因为基尼值计算里没有 log,只有平方和减法,运算速度比信息熵快得多,在工程上优势明显。我实际测过一个 10 万行数据集,用 gini 比用 entropy 大概快 20% 到 30%,这就是为什么生产环境默认都用 gini。

顺便说一个我初期经常混淆的点:CART 既能做分类也能做回归。做回归时,叶子节点的输出不再是类别,而是落入该叶子节点的所有样本的均值,划分目标也不再是基尼指数,而是最小化均方误差。所以你在 sklearn 里看到 DecisionTreeRegressor,用的还是 CART 的结构,只是损失函数从 Gini 换成了 MSE。

3. 决策树如何逼近真实曲线:从分类边界到回归实操

3.1 决策树的边界为什么是“分段常数”

如果你把二维平面上的决策树分类结果画出来,会发现边界全是横平竖直的线,而且整个平面被切成一块块矩形区域。这是因为决策树每一层只对一个特征做判断,比如 x1<3.5 往左,x2>2.1 往上,这种判断叠加出来的边界必然是轴对齐的分段常数边界。换句话说,决策树本质上是在用“矩形”去逼近真实分布。

这也就解释了决策树的两个性格特点:第一,树越深,矩形块越多,拟合能力越强,但特别容易过拟合,因为真实边界很少是完美的矩形;第二,它对线性关系不敏感,比如 y=x1+x2 这种对角线性边界,单棵决策树需要很多层才能逼近,效率极低。这也是为什么集成模型如随机森林、梯度提升树会在实际任务中更占优势。

3.2 新手必做:鸢尾花分类的 sklearn 实战

我在学头歌平台上那道“决策树进行鸢尾花分类-sklearn版”的题时,把一段代码反复改了好几遍才通过。头歌这类平台一般已经把环境配好了,只要求你把算法核心补全,但如果你是本地跑,完整流程是这样:

from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score iris = load_iris() # 训练集测试集三七开,固定随机种子保证结果可复现 X_train, X_test, y_train, y_test = train_test_split( iris.data, iris.target, test_size=0.3, random_state=42 ) # criterion 可选 'gini' 或 'entropy' clf = DecisionTreeClassifier(criterion='entropy', max_depth=3, random_state=42) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print("Accuracy:", accuracy_score(y_test, y_pred))

这段代码很简单,但我建议你一定要亲手输出一下 clf.tree_.feature 和 clf.tree_.threshold,然后再和可视化结果对一下。你会发现树先用了花瓣长度,再用了花瓣宽度,最后才考虑花萼特征。这说明花瓣的区分度比花萼强得多,模型自己就学出来了,不用你告诉它哪个特征重要。

头歌题里容易踩的坑有两个。第一个是参数不一致:平台判题时可能期待 criterion='gini',而你提交的是 'entropy',虽然两者在鸢尾花上准确率都很高,但“评分脚本只认标准输出”是这类平台的老毛病,所以跑题之前先看清楚题干要求。第二个坑是输入数据格式:平台给的 csv 里特征列可能是字符串,比如“Iris-setosa”会被当成特征,这时候如果不做编码,模型会报 ValueError,需要在 fit 之前用 LabelEncoder 把标签做一下处理。

3.3 回归实战:用决策树预测收入

热词里还有一个“头歌决策树进行收入预测-sklearn版”,这个任务本质上是回归,或者是把收入分成几档的分类。如果你面对的是连续收入值,不要用 DecisionTreeClassifier,改用 DecisionTreeRegressor。

from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import train_test_split # 假设 X 是特征,y 是连续的工资数值 reg = DecisionTreeRegressor(max_depth=4, min_samples_leaf=5, random_state=42) reg.fit(X_train, y_train) # 决策树回归预测结果是一段段水平线 y_pred = reg.predict(X_test)

这里有一个非常直观的“逼近真实曲线”的画面:你把单特征作为横轴、收入作为纵轴画散点图,再叠加 DecisionTreeRegressor 的预测结果,看到的是一条阶梯状的水平线。深度越大台阶越密,深度越小台阶越少。如果你把 max_depth 调到 20 以上,这条阶梯线几乎穿过每一个点,看起来“拟合得完美”,但在新数据上误差反而变大,这就是过拟合最直接的视觉证据。

我用同样的数据对比过线性回归和决策树回归。线性回归得到一条斜线,对非线性关系无能为力;决策树回归得到台阶线,能适应复杂的非线性模式,但台阶之间完全不连续,预测值永远不会超出训练集里见过的范围。这两种模型各有各的问题,没有谁万能,这也是后来要上随机森林和梯度提升的直接原因。

4. 剪枝:决策树防止过拟合的必修课

4.1 预剪枝和后剪枝的机制与对比

决策树如果不加限制,理论上可以把每个训练样本都单独分到一个叶子节点里,训练集准确率高达 100%,但泛化能力一塌糊涂。剪枝就是用来治这个病的,分为预剪枝和后剪枝。

预剪枝的思想是“边建边停”:每次划分前,用验证集评估一下,如果这个划分不能让验证集准确率提升,就干脆不划分,当前节点直接变成叶子节点。优点是效率高,缺点是“短视”,可能当前这次划分没用,但下一步划分的组合是有用的,预剪枝会因为贪心而错过这种机会。

后剪枝的思想是“先建后砍”:先把整棵树建到最大深度,然后自底向上尝试把某个内部节点换成叶子节点,如果替换后验证集准确率不降,就执行剪枝。后剪枝比预剪枝保留更多的结构,泛化能力通常更好,但训练开销大,因为要先造一棵完整的树再慢慢剪。

sklearn 里没有直接提供后剪枝 API,但从 0.24 版本开始有 cost_complexity_pruning_path 方法,它实现了基于代价复杂度(CCP)的后剪枝。你只需要拿到剪枝路径里的 alpha 值,然后用不同的 ccp_alpha 重新训练树,再用验证集选一个最优 alpha 就行。实操时我建议先跑一遍路径,看下有效 alpha 的取值区间,然后在这个区间里二分搜索,不要直接用全量网格。

4.2 实操心得:决策树核心参数的调法

我调决策树参数的经验是:优先调 max_depth,其次调 min_samples_leaf,最后才考虑 min_samples_split。原因是 max_depth 直接控制模型复杂度,最直观;min_samples_leaf 能防止叶子节点里样本太少,减少噪声影响;而 min_samples_split 在数据量不大时影响不明显。

具体数值我给个参考:小数据集(几千行)用 max_depth=5 到 8,min_samples_leaf=10 左右;中等数据集(几万行)max_depth 可以放到 10 到 15,min_samples_leaf=20 左右。但这只是起点,真正靠谱的做法是用交叉验证搜参:

from sklearn.model_selection import GridSearchCV param_grid = { 'max_depth': [3, 5, 7, 9], 'min_samples_leaf': [1, 5, 10, 20], 'criterion': ['gini', 'entropy'] } grid = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring='accuracy' ) grid.fit(X_train, y_train) print(grid.best_params_)

这里我踩过一个坑:直接在整个训练集上做 GridSearchCV,然后把 grid.best_estimator_ 拿去和测试集对比,准确率虚高。正确做法是先切出独立的测试集,只在训练集上做交叉验证搜索参数,最后再用测试集评估一次。否则你相当于把测试集的信息泄漏进了调参过程,结果不可信。

5. 连续值与缺失值处理:决策树的“补丁”艺术

5.1 连续值离散化:二分法

现实数据里几乎没有纯离散特征,年龄、收入、温度都是连续的。C4.5 采用的方案是二分法:先把这个特征的所有取值排序,然后取相邻取值的均值作为候选划分点,每个候选点把样本分成“小于等于”和“大于”两部分,再分别计算信息增益或基尼指数,取最优的候选点作为阈值。

比如年龄特征有 20、30、40 三个值,候选点是 25 和 35。算一下按 25 划分的信息增益,再算按 35 划分的,选大的。sklearn 里你不需要手动做这件事,底层已经实现了,但我建议你至少手算一次,因为这是理解“决策树如何选阈值”的关键一步。

有一点要注意:同一个连续特征可以被多次使用。离散特征一旦被选中就不会再在该分支里出现,但连续特征可以在不同分支以不同阈值反复使用。比如第一层用“年龄>30”划分,左分支可能再用“年龄>18”继续划分。这在树结构上很常见,别被吓到。

5.2 缺失值处理:给样本加权重

真实数据集里经常有缺失值,决策树在训练时处理缺失值的思路非常巧妙。它不丢弃含缺失值的样本,而是给每个样本一个权重 w。最初所有样本权重为 1。当某个特征在当前节点缺失时,样本不再强制进入某一个分支,而是按比例拆分权重,同时进入所有分支,每个分支分配到的权重和该分支的非缺失样本占比成正比。

这个机制听起来抽象,但核心目的是保持“无偏”:让缺失样本对该特征的信息增益计算贡献,但不偏向任何一个分支。sklearn 的 DecisionTreeClassifier 默认不接受 NaN 值,会在 fit 时直接报错,所以你在用 sklearn 之前还是要自己处理缺失,常见做法是填充均值/中位数,或者用 SimpleImputer。我个人的习惯是先用 SimpleImputer 做中位数填充,再对比一下填充前后模型效果,如果树的结构变化很大,说明缺失模式携带了信息,这时你可以把“是否缺失”本身做成一个特征参与建模,往往会有奇效。

6. 随机森林与决策树的区别:从一棵树到一片森林

6.1 Bagging 思路:为什么多棵树更稳

单个决策树很容易过拟合,方差大。随机森林用了一个特别直白的思路:既然一棵树不稳定,那我就种很多棵树,让它们投票。这就是 Bagging(Bootstrap Aggregating)的核心。

具体做法是:每棵树在训练时,从原始训练集里有放回地随机抽取 n 个样本作为自己的训练集,这个过程叫 bootstrap 抽样。因为有放回,不同树的训练集会有所差异,树和树之间就有了多样性。最后分类时全体投票,回归时取平均。方差被大大压低,偏差基本不变,整体泛化能力明显强于单棵树。

这里的数学直觉是:对 k 棵树的输出取平均,方差的期望会随着树的多样性而下降,但又不会因为 bootstrap 抽样而产生系统性偏差偏移。所以随机森林往往不需要像单棵树那样精细调参,也能拿到不错的准确率,这也是很多 Kaggle 早期比赛里大家无脑上随机森林的原因。

6.2 随机森林的“随机”体现在哪里

随机森林和普通 Bagging 的区别在于,它加了一个“特征扰动”:每棵树在做节点划分时,不是从所有特征里挑最优,而是先从全部特征里随机抽出一个子集(通常大小是 sqrt(n_features)),再从这个子集里挑最优特征。这样一来,就算某个特征特别强,它也不能垄断所有树的划分,别的特征因此有了出场机会,树之间的相关性进一步降低。

我用个生活化类比:如果公司里每次提拔员工都只看“学历”这一个指标,那不管怎么换评审团,结果都差不多;但如果每次先从候选人里随机抽一组人,只在这组人里比较能力,那不同背景的人就有更多机会冒头。随机森林就是靠这一招让整片森林比单棵树强得多。

给你一张常见的对比表,面试前看这个就够:

对比维度决策树随机森林
模型结构单棵树多棵树集成
方差大,易过拟合小,泛化更强
训练速度快慢(但可并行)
可解释性高,可完整可视化低,只能看特征重要性
调参难度中等较低,默认参数即可用
对缺失值处理需要预填充同样需要预填充

我个人体验是:当数据量不大、特征少、业务方要求“讲得清规则”时,用单棵决策树或带剪枝的决策树;当数据量大、追求效果上线时,默认上随机森林。但随机森林也不是万能的,对于可以无限增长的训练数据和高稀疏特征,梯度提升树往往更强,这是后话。

7. 常见问题与避坑心得

7.1 头歌实验里最容易踩的三个坑

头歌这类平台和本地跑 sklearn 有天然差别,我把自己和身边同学踩过的坑汇总一下。

第一个是随机种子问题。很多平台判题时要求输出指定结果,比如准确率保留几位小数。如果你不设 random_state,每次跑结果都不一样,机器判题自然对不上。无论你用什么模型,第一步就是固定 random_state=42 或者题目指定的种子。

第二个是数据清洗的坑。平台给的数据可能包含空值、异常值、字符串类别,直接用 DataFrame 的原始数据喂给 fit 会报错。我建议养成一套固定流程:先看 df.info() 和 df.head(),确认特征类型;再用 LabelEncoder 或 OneHotEncoder 处理类别特征;最后用 SimpleImputer 处理缺失值。

第三个是评估指标的坑。分类任务不一定只看 accuracy。如果预测收入时正负样本不平衡,平台可能要求你输出 AUC 或 F1。你提交前一定要看题目到底要求算哪个指标,不然哪怕模型是对的,格式不对也是零分。

7.2 决策树可视化:让树真正“看得见”

学习决策树时最忌讳只看准确率数字,一定要把树画出来。sklearn 自带 plot_tree 是最省事的方案:

import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize=(12, 8)) plot_tree(clf, filled=True, feature_names=iris.feature_names, class_names=iris.target_names) plt.show()

filled=True 会给不同类别染色,看起来非常直观。如果你要导出高清图给报告用,可以用 graphviz 那套。graphviz 方式更灵活,但需要单独安装 graphviz 软件,我本地装了之后又因为环境变量没配置折腾了半小时,建议按官方文档一步步来,别跳过 PATH 设置那一步。

画图之后重点看三块:第一是根节点选了什么特征,这代表模型觉得最重要的区分维度;第二是每个节点的 threshold 阈值大不大,阈值太细说明对噪声敏感;第三是叶子节点的样本数,如果出现只有 1 个样本的叶子,基本就是过拟合,需要加深剪枝参数。

7.3 特征重要性不能盲信,但它能帮你看门道

sklearn 里 feature_importances_ 可以输出每个特征的重要性数值,机制是:对某个特征,在树的划分中带来的不纯度减少之和,再按树的棵数去平均。随机森林里这个值更稳定。

但我要提醒一句:这个重要性偏向连续特征和高基数特征,和业务重要性不是一回事。我遇到过这样的案例:一个噪音特征因为取值特别多,重要性排到了第二名,差点误导了业务方的判断。所以我的习惯是:用 feature_importances_ 做第一轮筛选,然后再结合业务经验和 SHAP 值交叉验证,才敢决定真正删哪些特征。

另外一个小技巧:如果你分别用 gini 和 entropy 训练两棵树,然后对比它们给出的特征重要性排序,如果排序差异很大,说明数据里可能存在多重共线性或者特征冗余,这时候需要警惕模型对特征选择的稳定性。

最后再分享一个我学习决策树时最重要的小习惯:每学一个算法,一定拿一个小数据集手算一遍。信息增益、基尼指数、增益率,这三个指标你至少手推一次,才能在看 sklearn 文档时真正明白每个参数在干什么。等到你能不看公式就把一棵树的构建过程完整讲给别人听,第4章你就真正拿下了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询