☰
从信息熵到剪枝:手把手构建一棵可解释的决策树
2026/10/17 14:40:05 网站建设 项目流程

1. 为什么“选西瓜”能讲清楚决策树

做机器学习的人,手里几乎都有一本封面画着西瓜的书。翻开目录,第一眼看见的算法往往就是决策树,而用来讲解决策树的案例,恰恰就是“如何挑一个好西瓜”。这个例子看起来简单到甚至有点土,但真把它吃透了,你会发现它把决策树最核心的东西全讲透了:特征怎么选、纯度怎么衡量、怎么避免过拟合、连续值和缺失值怎么处理。我见过不少入行的朋友,一开始觉得“选西瓜”太小儿科,直接跳过去看后面的支持向量机和神经网络,结果学到最后回头补决策树时才发现,当初丢掉的那几页才是真正的地基。

这个案例能成为经典,关键在三点。第一,它足够具体。一提“选西瓜”,每个人脑子里立刻能浮现出西瓜的长相、敲起来的声音、掂在手里的分量,特征维度天然就是多元的:颜色、根蒂、敲声、纹理、脐部、触感,全都对应着真实世界的感官判断,比拿一堆抽象的数字特征(比如坐标点、用户ID)直观得多。第二,它有清晰的标签——这个瓜到底好不好。好坏的判断标准虽然偶有争议,但大体上大家都认,这正好对应监督学习里的分类任务。第三,也是最重要的一点,它天然存在“歧义”。你挑瓜时不会真的把所有瓜都切开验证,只能靠外观和敲击经验去估计,这跟机器学习用已知样本去预测未知样本的逻辑一模一样。

所以别小看“选西瓜”这三个字。它背后对应的是完整的一条机器学习问题求解链路:拿到一批带标签的样本 → 确定特征空间 → 选择特征划分策略 → 构建可解释的分类模型 → 对新样本做预测。本文后面的所有内容,都是沿着这条链路逐步展开的,适合正在学机器学习基础、想真正搞懂决策树原理,以及准备做数据挖掘相关面试的人参考。我会尽量用能亲手验算的小例子,把每一步都算给你看。

2. 决策树的底牌:信息熵和信息增益

2.1 先理解“信息熵”:抽盲盒的不确定性

决策树的核心问题只有一个:怎么选特征来划分数据。但“怎么选”需要一把尺子,这把尺子就是信息论里的信息熵。

信息熵衡量的是系统的“不确定性”。我举个例子你就明白了。假设有一个盒子,里面只有一个黑球,你伸手进去摸一个,摸出的必然是黑球,这个系统的熵是0,因为没有不确定性。再假设盒子有一半黑球一半白球,你猜结果时每次都是心里没底,熵就达到了1比特。如果盒子有红、黄、蓝、绿四种球各四分之一,你猜中的难度进一步上升,熵就是2比特。熵的本质可以理解成“如果要你编码每次摸球的结果,平均至少需要几个二进制位”,也可以理解成“你对这个系统到底有多没底”。

这个思想迁移到数据集里,公式如下:

Ent(D) = -Σ p_k * log2(p_k)

其中p_k是第k类样本占的比例。对于二分类问题,如果好瓜和坏瓜各占一半,即p=0.5,那么Ent(D)=-(0.5log2 0.5 + 0.5log2 0.5)=1,不确定性最大。如果全是好瓜,某一类p=1、另一类p=0,log2(1)=0,整个熵等于0,数据集完全“纯”了。

决策树的整个构建过程,就是不断选择某个特征,把数据集切成几个子集,让每个子集的熵尽可能降低。每切一刀,不确定性就减少一点。减少多少,就用信息增益来度量。

信息增益的定义是:

Gain(D, a) = Ent(D) - Σ (|Dv| / |D|) * Ent(Dv)

其中a是某个特征,v是该特征的取值,Dv是特征a取值为v的那部分样本。公式的意思很直白:划分之前的数据熵,减去划分之后各子集熵的加权平均。差值越大,说明用这个特征切完之后“不确定性降得越多”,这个特征就越重要。ID3决策树就是拿这个指标选择特征的。

2.2 手把手算一遍:信息增益的完整计算过程

公式摆出来不亲手算一遍,等于没学。这里我给出一份简化版的西瓜数据集,只保留三个特征和标签,一共12个样本,方便你拿计算器或者纸笔跟随验算。

编号色泽纹理触感是否好瓜
1青绿清晰硬滑好瓜
2青绿清晰硬滑好瓜
3青绿清晰软粘坏瓜
4青绿稍糊硬滑坏瓜
5乌黑清晰硬滑好瓜
6乌黑清晰软粘好瓜
7乌黑稍糊硬滑坏瓜
8乌黑稍糊软粘坏瓜
9乌黑稍糊硬滑坏瓜
10乌黑稍糊软粘坏瓜
11青绿稍糊软粘好瓜
12青绿清晰软粘好瓜

先算根节点的熵。12个样本里好瓜6个、坏瓜6个,各占0.5,所以Ent(D)=-(0.5log2 0.5 + 0.5log2 0.5)=1,这是标准的抛硬币式最大不确定状态。

接着算特征“色泽”的信息增益。青绿有6个样本(1、2、3、4、11、12),其中好瓜4个、坏瓜2个,子集熵=-(4/6log2(4/6) + 2/6log2(2/6))≈0.9183。乌黑有6个样本(5、6、7、8、9、10),其中好瓜2个、坏瓜4个,子集熵同样是0.9183。划分后的加权熵=6/120.9183 + 6/120.9183=0.9183。所以色泽的信息增益=1-0.9183=0.0817。

再算“触感”。硬滑的样本有6个(1、2、4、5、7、9),好瓜3个、坏瓜3个,熵=1。软粘的样本有6个(3、6、8、10、11、12),好瓜3个、坏瓜3个,熵=1。加权后还是1,信息增益=0。也就是说,在当前这组数据里,单看“触感”对判断好瓜没有任何帮助,它的划分结果和随机猜测没区别。

最后算“纹理”。纹理清晰的样本是(1、2、3、5、6、12),好瓜5个、坏瓜1个,熵=-(5/6log2(5/6) + 1/6log2(1/6))≈0.6500。纹理稍糊的样本是(4、7、8、9、10、11),好瓜1个、坏瓜5个,熵同样是0.6500。加权熵=0.6500。信息增益=1-0.6500=0.3500。

三个特征对比下来,纹理的增益最大:0.35 > 0.0817 > 0。因此根节点选择“纹理”做第一次划分。这一个计算过程,本质上是把“我挑西瓜的经验”转化成了可量化的数学判断,以后每遇到一个新样本,都沿着这条规则往下走就行。

2.3 信息增益的软肋,以及增益率、基尼指数为什么被提出

用信息增益作为特征选择的准则有一个实际缺陷:它天然偏向取值数目多的特征。你想想,如果把样本按“编号”来划分,每个编号只对应一个样本,划分之后每个子集都绝对纯,信息增益会直接拉满到最大。但这样的模型毫无泛化能力,换个新西瓜来,编号是它没见过的,规则直接失效。这就是著名的“偏好取值多”问题。

C4.5决策树用增益率来修正这个问题。增益率在信息增益基础上,除以一个“固有值”,对取值多的特征施加惩罚。不过增益率又会反过来偏好取值少的特征,因此C4.5的做法是从信息增益超过平均水平的候选中,再选增益率最高的特征,两边做一个折中。

CART决策树则换了一个思路,采用基尼指数。基尼指数衡量的是从数据集里随机抽两个样本,它们类别不一样的概率。计算公式是Gini(D)=1-Σ p_k²,取值越小表示数据越纯。CART在选择划分特征时,计算划分前后的基尼指数变化,选择让变化最大的切分方式。基尼指数比信息熵的好处是,不用算对数,计算速度快很多,在工程上尤其是大规模数据场景下优势明显。

这三个准则你都需要理解它们的来龙去脉:不是谁替代谁,而是适用场景不同。面试里如果被问到“信息增益、增益率、基尼指数有什么区别”,能讲出这层的取舍逻辑,比背定义有用得多。

3. 实操演示:从零构建一棵西瓜决策树

3.1 环境准备与数据准备

纸上谈兵结束,实际操作一遍。我用Python的scikit-learn库来演示,这是目前最常用的机器学习库,接口稳定、文档丰富,用来验证决策树原理再合适不过。先安装依赖并准备数据:

pip install scikit-learn pandas

然后构造3.2节里的12条样本数据:

import pandas as pd from sklearn.tree import DecisionTreeClassifier, export_text data = { "色泽": ["青绿", "青绿", "青绿", "青绿", "乌黑", "乌黑", "乌黑", "乌黑", "乌黑", "乌黑", "青绿", "青绿"], "纹理": ["清晰", "清晰", "清晰", "稍糊", "清晰", "清晰", "稍糊", "稍糊", "稍糊", "稍糊", "稍糊", "清晰"], "触感": ["硬滑", "硬滑", "软粘", "硬滑", "硬滑", "软粘", "硬滑", "软粘", "硬滑", "软粘", "软粘", "软粘"], "好瓜": ["好瓜", "好瓜", "坏瓜", "坏瓜", "好瓜", "好瓜", "坏瓜", "坏瓜", "坏瓜", "坏瓜", "好瓜", "好瓜"] } df = pd.DataFrame(data)

在把数据喂给决策树之前,需要把中文特征值转成数值。这是因为sklearn的经典决策树接口不直接支持字符串特征,必须编码。我用最简单的标签编码,把每个取值映射成0、1、2之类的整数:

from sklearn.preprocessing import LabelEncoder le_dict = {} for col in ["色泽", "纹理", "触感", "好瓜"]: le = LabelEncoder() df[col] = le.fit_transform(df[col]) le_dict[col] = le

这一步在实际项目中相当关键。要注意的是,LabelEncoder做的事情是把类别无脑编号,它本身不带顺序意义。对有序特征(比如“敲声”从沉闷到清脆)用OrdinalEncoder是合理的,对无序类别(比如色泽的青绿、乌黑、浅白)用OneHotEncoder更表述准确。这里演示数据简单,我先用标签编码简化处理,但实际项目中不要无脑照搬这个操作。

3.2 训练第一棵完整的树,并解读结果

划分出特征和标签,直接训练:

X = df[["色泽", "纹理", "触感"]] y = df["好瓜"] clf = DecisionTreeClassifier(criterion="entropy", random_state=42) clf.fit(X, y) print(export_text(clf, feature_names=["色泽", "纹理", "触感"], decimals=3))

你会发现sklearn并没有严格复现上文手算得到的“根节点选纹理”的结论。这是为什么?答案在于编码方式。我用的标签编码把“青绿”编码为0、把“乌黑”编码为1,决策树在计算时是按照数值大小来判断划分方向的,它能做的分裂方式是“青绿 vs 乌黑”这种二分,而不是“纹理=清晰 vs 纹理=稍糊”这种类别分组。这点非常重要:sklearn的DecisionTreeClassifier原生只支持数值特征,它每次分裂都是在一个特征上找阈值做二值划分,不是像原始决策树算法那样按特征的所有取值进行多叉分裂。

如果只想验证“特征重要性排序”,这个树的结果其实已经能看出脉络。用一行代码就能打出特征重要度:

print(dict(zip(["色泽", "纹理", "触感"], clf.feature_importances_)))

在我跑的结果里,纹理的重要性显著高于色泽和触感,这跟手算的结论保持一致。特征重要度是决策树在实际项目中最有价值的副产品之一,它可以快速告诉你业务数据里哪个字段对标签的预测贡献最大,在做特征筛选时非常好用。

如果非要严格复现多叉分裂的ID3决策树,不建议继续硬怼sklearn,可以直接用Python手写一个递归构建逻辑,或者使用支持类别型特征的库。手写决策树并不难,核心逻辑就是前面手算信息增益的循环过程:算出所有特征的信息增益,取最大的作为当前节点,然后递归处理每个子集,直到子集纯到只剩一个类别,或者特征用完为止。我强烈建议学习阶段自己写一次这个递归逻辑,写完之后你对决策树的理解会产生质变。

3.3 手动实现ID3分裂逻辑的关键代码

放一段精简但完整可跑的ID3核心代码,帮你理解递归构建的本质:

import math from collections import Counter def entropy(labels): total = len(labels) counts = Counter(labels) ent = 0.0 for c in counts.values(): p = c / total ent -= p * math.log2(p) return ent def info_gain(data, labels, feature_idx): total_ent = entropy(labels) feature_values = set(row[feature_idx] for row in data) weighted_ent = 0.0 for v in feature_values: subset_idx = [i for i, row in enumerate(data) if row[feature_idx] == v] subset_labels = [labels[i] for i in subset_idx] weighted_ent += (len(subset_idx) / len(data)) * entropy(subset_labels) return total_ent - weighted_ent def id3(data, labels, feature_indices): if len(set(labels)) == 1: return labels[0] if not feature_indices: return Counter(labels).most_common(1)[0][0] gains = [info_gain(data, labels, idx) for idx in feature_indices] best_idx = feature_indices[gains.index(max(gains))] tree = {best_idx: {}} remaining = [idx for idx in feature_indices if idx != best_idx] for v in set(row[best_idx] for row in data): subset_idx = [i for i, row in enumerate(data) if row[best_idx] == v] tree[best_idx][v] = id3( [data[i] for i in subset_idx], [labels[i] for i in subset_idx], remaining ) return tree

这段代码去掉了所有花哨的优化,只保留ID3骨子里的递归逻辑。跑出来以后,你就能得到一棵完全按多叉特征划分的树,根节点一定是纹理。这种从公式到代码到结果的完整闭环,是只看书完全得不到的体验。

3.4 把树翻译成挑西瓜规则

无论用哪种工具,决策树最后都要翻译成可读的规则,这是它相比神经网络最大的优势——天然就是白盒模型。用上文手算出的结构,树可以读成这样:

如果纹理清晰:

  • 如果触感硬滑:好瓜(样例全部为好瓜,置信度100%)
  • 如果触感软粘:再看色泽(此分支还有1个坏瓜和2个好瓜) 如果纹理稍糊:
  • 如果色泽乌黑:坏瓜
  • 如果色泽青绿:再看触感

这套规则放到真实生活场景里,就变成一句句人话:“先看瓜的纹理,裂纹清晰说明大概率是好瓜;纹理清晰但摸着发软的再斟酌一下;纹理模糊的,不管颜色深浅都要多留个心眼。”这就是决策树“可解释性”的威力。在金融风控、医疗诊断、政务审批这些必须交代判断理由的行业,模型的预测结果需要给监管方或业务方一个说得通的解释,决策树和由其衍生的随机森林、梯度提升树,天然比深层神经网络更受青睐。

4. 过拟合这道坎:预剪枝和后剪枝

4.1 为什么树长太深会出问题

决策树有一个经典毛病:如果放任它一直长下去,它能把训练数据里所有样本都记下来,包括噪音和巧合。比如12个样本里,如果有一个样本恰好是“色泽乌黑、纹理稍糊、触感软粘、但却是好瓜”,这明显是个反常识的异常样本,但决策树为了把所有训练样本都分对,会专门为它生出一条分支。这么一来,训练集上的准确率能达到100%,但换个新样本过来,这条分支几乎不可能命中,模型表现立刻崩盘。这就是过拟合。

打个比方,学习挑西瓜时记住了“某个瓜贩摊位上从左往右数第三个瓜是好瓜”,这种经验换个摊位就完全没用。决策树要做的是学到“纹理清晰通常是好瓜”这种规律性的知识,而不是记住每个样本的ID。

解决过拟合的手段就是剪枝。剪枝分为两类,预剪枝和后剪枝。理解这两者,是真正用好决策树的分水岭。

4.2 预剪枝:在生长过程中提前叫停

预剪枝的思路是在构建树的过程中,一边长一边检查:如果当前这个分裂动作不能让模型在验证集上的表现变好,就不允许它分裂,直接把当前节点标为叶子节点。

具体做法通常依赖几个超参数:

参数作用经验值范围
max_depth树的最大深度,限制最多分裂几层3~10,小数据集建议3~5
min_samples_split节点里至少要有多少样本才允许继续分裂5~20,数据量大时取更大值
min_samples_leaf每个叶子节点至少保留多少样本1~10,越大越保守
max_leaf_nodes限制整棵树最多有多少个叶子节点视数据量而定

在sklearn里,这些参数直接放在DecisionTreeClassifier里面:

clf = DecisionTreeClassifier( criterion="entropy", max_depth=3, min_samples_leaf=2, random_state=42 )

预剪枝的好处是效率高,边建边剪,不用额外花时间。坏处也明显:有时候当前这步分裂看起来没好处,但分裂一层之后再分裂下去却可能有好处,预剪枝基于“贪心”直接放弃,容易欠拟合。工程上,预剪枝配合交叉验证调参,是最常用的方案。

4.3 后剪枝:先长满,再修剪

后剪枝的思路正好反过来:先让决策树把训练集完全拟合到位,长成一棵茂密的大树,然后从底部开始,把不重要的子树替换为叶子节点,看替换后验证集准确率是否下降,如果没下降甚至上升,就实施替换。

后剪枝的效果通常比预剪枝更好,因为它保留了足够多的信息再去做取舍,不容易误杀未来有潜力的分支。代价是训练时间更长,需要先生成一棵完整大树,再自底向上遍历。

在sklearn里没有直接暴露经典的后剪枝接口,但可以通过cost_complexity_pruning实现基于代价复杂度剪枝。它的核心是一个参数ccp_alpha,代表剪掉某个子树需要付出的“复杂度代价”阈值。简单用法如下:

path = clf.cost_complexity_pruning_path(X, y) ccp_alphas = path.ccp_alphas # 遍历不同的ccp_alpha,用验证集选出表现最好的一个

拟合出一批不同alpha的树,再用交叉验证挑出准确率最高的那棵,本质上就是在“树的大小”和“预测准确率”之间找平衡点。随着alpha增大,树越来越小,从极端过拟合逐步走到极端欠拟合,中间通常存在一个最优点。

注意:剪枝的核心原则是“看验证集表现,而不是看训练集表现”。如果你把预剪枝和后剪枝都试了,仍然过拟合,第一步不是继续加大剪枝力度,而是检查数据本身——训练集和验证集分布是否一致、样本量是否太少、特征是否包含了和标签高度相关的“泄漏字段”(比如直接记录了结果好坏)。

4.4 实际项目里怎么选剪枝策略

我自己的实践习惯是这样的:小数据集、追求快速理解和验证,优先用max_depth和min_samples_leaf做预剪枝,把树控制在3到5层,先看一个粗糙基线。如果业务强调可解释性,比如需要向非技术同事介绍规则,我会选择深度不超过4的浅树,规则数量少,大家都能看懂。如果追求精度且不太担心解释成本,我会把树放开,然后用代价复杂度剪枝配合交叉验证选择最优alpha。

还有一点常被忽略:随机森林和梯度提升决策树这类集成模型,本质上也是大量决策树的组合,剪枝策略同样适用。但集成模型对单棵树的深度要求通常更低,比如随机森林里树的max_depth设置为5到10就够用,反而通过增加树的数量来提升稳定性。单一决策树更容易过拟合,所以在业务落地时,如果追求稳定效果,我的建议永远是优先考虑集成方案,而不是一味地把单棵树加复杂。

5. 决策树实战中的易踩坑点

5.1 连续值特征怎么处理

西瓜的“触感”和“色泽”是离散值,但现实中很多特征是多维连续值,比如“重量”“敲击音频频率”。决策树处理连续值的方法是二分法:先把连续值排序,然后取相邻两个值的中间点作为候选划分点,逐个计算信息增益,选最优的那个。西瓜书里的做法是:对连续特征a,先把取值从小到大排序,生成n-1个候选划分点,每个划分点把数据集分成两部分(大于等于和小于),然后按离散特征的方式计算信息增益。

这里有个实用技巧:连续特征在决策树中被允许重复使用。比如第一个分裂点是重量大于4.5公斤,第二个分裂点可以是重量大于5.2公斤,同一个特征可以在不同层被反复利用。这也是决策树求和数据中连续变量关系比较自然的方式。不过在工程上要注意,一个连续特征如果在很多层反复出现,虽然预测能力可能不错,但解释性会变差,人很难理解“为什么重量大于4.5又大于5.2还要再大于6”。

5.2 缺失值怎么处理

真实数据集里,特征的缺失几乎不可避免。比如买西瓜时,你没法每次都敲同一个位置的西瓜来判断声音,有些瓜可能根本看不到纹理。决策树处理缺失值通常分两部分:一是划分特征时,只用那些特征值非缺失的样本计算信息增益;二是样本进入某个节点后,如果它的特征值缺失,就按各子集的样本比例加权放入所有分支,相当于“分头发送”。

在sklearn里,DecisionTreeClassifier底层实现了类似的分发逻辑,完全可以直接喂包含缺失值的数据。但如果你用手写的ID3代码,就需要自己处理这一层逻辑。实际项目中我的建议是,不要过度依赖模型的缺失值处理能力,建模前先把缺失率高的特征处理掉,或者用均值、众数、业务规则填充,尽量减少进入模型后的不确定性。

5.3 类别不平衡问题

如果好瓜有95个、坏瓜只有5个,决策树几乎一定会偏向好瓜。纯信息增益计算出来的最优特征很可能对坏瓜毫无区分度。处理不平衡数据的常用方法有:调整类别权重(sklearn的class_weight参数)、重采样、改用其他评价指标如AUC或者F1-score。这些方法单独提出来都够各写一篇博文,但在决策树的语境里最立竿见影的还是设置class_weight,它会在计算熵的时候加大少数类的权重,让树更倾向于为少数类分裂。

5.4 过调参、欠调参之外的三个建议

第一,决策树适合处理表格型数据,不适合处理图像、文本、音频这类非结构化数据,别硬套。第二,特征之间的量纲不影响决策树,因为每个特征单独找分裂点,所以做不做标准化都无所谓,这点和线性模型完全不同。第三,决策树对异常值相对不敏感,因为它本质上是找阈值划分,单个异常样本不会把分裂点拉得特别离谱,这是它稳健性好的一面。

6. 写在最后的一点个人体会

带过不少新人,也跑过不少表格型数据的竞赛和项目,我对决策树始终有一种“压舱石”的感觉。它不是最惊艳的算法,但绝对是最值得花时间彻底搞懂的基础模型。从“选西瓜”这个朴素案例出发,你实际上是在学一遍机器学习的核心方法论:如何用熵度量不确定性、如何做特征选择、如何权衡偏差和方差、如何处理现实数据里的脏乱差。这些能力迁移到随机森林、XGBoost、LightGBM身上,你会发现它们解决的是同一批问题,只是把逻辑升级得更复杂。

如果只让我给一条实操建议,那就是:看完这篇文章后,亲手把那个12条西瓜数据的ID3代码跑一遍,再把sklearn的树可视化打出来对比一眼,最后试着把树翻译成三条“挑瓜口诀”。这一套流程下来,决策树就不只是你脑子里的一段公式,而是你真正能用手用的工具了。等你在真实项目里把它用熟,你自然就会懂,为什么一本机器学习教材要从西瓜讲起。

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

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

立即咨询