ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

决策树从if-else到数学之美:信息增益与剪枝实战

决策树从if-else到数学之美:信息增益与剪枝实战 提到“机器学习”很多初学者脑子里蹦出来的第一个画面可能是神经网络那一堆神秘符号或者卷积、注意力机制但翻开周志华老师的《机器学习》西瓜书第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_kk1,2,...,|Y|那么信息熵定义为Ent(D) - Σ_{k1}^{|Y|} p_k · log2(p_k)这个公式里对数底数取2单位是比特。为什么用 log因为信息是可以“叠加”的两个独立事件的信息量相加而概率相乘log 正好把乘法变加法。举个具体例子如果 D 里全是正样本p1那 Ent(D) -1·log2(1) 0说明数据一点不乱纯度最高如果正负样本各占一半p0.5Ent(D) -0.5·log2(0.5)-0.5·log2(0.5)1说明不确定性最大。我在南瓜书里看到作者反复强调一个点信息熵只依赖于概率分布不依赖于样本数量。也就是说100个样本中一半正一半负的信息熵和1000个样本中一半正一半负的信息熵是一样的都是1。这个理解很重要后面算信息增益时你就不会被样本量干扰。2.2 ID3 算法信息增益最大者优先ID3 是决策树的开山算法核心思想很简单每次划分时计算每个特征带来的“信息增益”选增益最大的那个特征。信息增益的公式是Gain(D, a) Ent(D) - Σ_{v1}^{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) - Σ_{v1}^{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 - Σ_{k1}^{|Y|} p_k²直观理解是从 D 中随机抽两个样本它们类别不一致的概率。这个概率越小数据越纯。基尼指数则是按特征取值加权后的基尼值Gini_index(D, a) Σ_{v1}^{V} (|D^v| / |D|) · Gini(D^v)CART 选择基尼指数最小的特征作为划分属性。因为基尼值计算里没有 log只有平方和减法运算速度比信息熵快得多在工程上优势明显。我实际测过一个 10 万行数据集用 gini 比用 entropy 大概快 20% 到 30%这就是为什么生产环境默认都用 gini。顺便说一个我初期经常混淆的点CART 既能做分类也能做回归。做回归时叶子节点的输出不再是类别而是落入该叶子节点的所有样本的均值划分目标也不再是基尼指数而是最小化均方误差。所以你在 sklearn 里看到 DecisionTreeRegressor用的还是 CART 的结构只是损失函数从 Gini 换成了 MSE。3. 决策树如何逼近真实曲线从分类边界到回归实操3.1 决策树的边界为什么是“分段常数”如果你把二维平面上的决策树分类结果画出来会发现边界全是横平竖直的线而且整个平面被切成一块块矩形区域。这是因为决策树每一层只对一个特征做判断比如 x13.5 往左x22.1 往上这种判断叠加出来的边界必然是轴对齐的分段常数边界。换句话说决策树本质上是在用“矩形”去逼近真实分布。这也就解释了决策树的两个性格特点第一树越深矩形块越多拟合能力越强但特别容易过拟合因为真实边界很少是完美的矩形第二它对线性关系不敏感比如 yx1x2 这种对角线性边界单棵决策树需要很多层才能逼近效率极低。这也是为什么集成模型如随机森林、梯度提升树会在实际任务中更占优势。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_size0.3, random_state42 ) # criterion 可选 gini 或 entropy clf DecisionTreeClassifier(criterionentropy, max_depth3, random_state42) 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然后再和可视化结果对一下。你会发现树先用了花瓣长度再用了花瓣宽度最后才考虑花萼特征。这说明花瓣的区分度比花萼强得多模型自己就学出来了不用你告诉它哪个特征重要。头歌题里容易踩的坑有两个。第一个是参数不一致平台判题时可能期待 criteriongini而你提交的是 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_depth4, min_samples_leaf5, random_state42) 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_depth5 到 8min_samples_leaf10 左右中等数据集几万行max_depth 可以放到 10 到 15min_samples_leaf20 左右。但这只是起点真正靠谱的做法是用交叉验证搜参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_state42), param_grid, cv5, scoringaccuracy ) 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 思路为什么多棵树更稳单个决策树很容易过拟合方差大。随机森林用了一个特别直白的思路既然一棵树不稳定那我就种很多棵树让它们投票。这就是 BaggingBootstrap Aggregating的核心。具体做法是每棵树在训练时从原始训练集里有放回地随机抽取 n 个样本作为自己的训练集这个过程叫 bootstrap 抽样。因为有放回不同树的训练集会有所差异树和树之间就有了多样性。最后分类时全体投票回归时取平均。方差被大大压低偏差基本不变整体泛化能力明显强于单棵树。这里的数学直觉是对 k 棵树的输出取平均方差的期望会随着树的多样性而下降但又不会因为 bootstrap 抽样而产生系统性偏差偏移。所以随机森林往往不需要像单棵树那样精细调参也能拿到不错的准确率这也是很多 Kaggle 早期比赛里大家无脑上随机森林的原因。6.2 随机森林的“随机”体现在哪里随机森林和普通 Bagging 的区别在于它加了一个“特征扰动”每棵树在做节点划分时不是从所有特征里挑最优而是先从全部特征里随机抽出一个子集通常大小是 sqrt(n_features)再从这个子集里挑最优特征。这样一来就算某个特征特别强它也不能垄断所有树的划分别的特征因此有了出场机会树之间的相关性进一步降低。我用个生活化类比如果公司里每次提拔员工都只看“学历”这一个指标那不管怎么换评审团结果都差不多但如果每次先从候选人里随机抽一组人只在这组人里比较能力那不同背景的人就有更多机会冒头。随机森林就是靠这一招让整片森林比单棵树强得多。给你一张常见的对比表面试前看这个就够对比维度决策树随机森林模型结构单棵树多棵树集成方差大易过拟合小泛化更强训练速度快慢但可并行可解释性高可完整可视化低只能看特征重要性调参难度中等较低默认参数即可用对缺失值处理需要预填充同样需要预填充我个人体验是当数据量不大、特征少、业务方要求“讲得清规则”时用单棵决策树或带剪枝的决策树当数据量大、追求效果上线时默认上随机森林。但随机森林也不是万能的对于可以无限增长的训练数据和高稀疏特征梯度提升树往往更强这是后话。7. 常见问题与避坑心得7.1 头歌实验里最容易踩的三个坑头歌这类平台和本地跑 sklearn 有天然差别我把自己和身边同学踩过的坑汇总一下。第一个是随机种子问题。很多平台判题时要求输出指定结果比如准确率保留几位小数。如果你不设 random_state每次跑结果都不一样机器判题自然对不上。无论你用什么模型第一步就是固定 random_state42 或者题目指定的种子。第二个是数据清洗的坑。平台给的数据可能包含空值、异常值、字符串类别直接用 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, filledTrue, feature_namesiris.feature_names, class_namesiris.target_names) plt.show()filledTrue 会给不同类别染色看起来非常直观。如果你要导出高清图给报告用可以用 graphviz 那套。graphviz 方式更灵活但需要单独安装 graphviz 软件我本地装了之后又因为环境变量没配置折腾了半小时建议按官方文档一步步来别跳过 PATH 设置那一步。画图之后重点看三块第一是根节点选了什么特征这代表模型觉得最重要的区分维度第二是每个节点的 threshold 阈值大不大阈值太细说明对噪声敏感第三是叶子节点的样本数如果出现只有 1 个样本的叶子基本就是过拟合需要加深剪枝参数。7.3 特征重要性不能盲信但它能帮你看门道sklearn 里 feature_importances_ 可以输出每个特征的重要性数值机制是对某个特征在树的划分中带来的不纯度减少之和再按树的棵数去平均。随机森林里这个值更稳定。但我要提醒一句这个重要性偏向连续特征和高基数特征和业务重要性不是一回事。我遇到过这样的案例一个噪音特征因为取值特别多重要性排到了第二名差点误导了业务方的判断。所以我的习惯是用 feature_importances_ 做第一轮筛选然后再结合业务经验和 SHAP 值交叉验证才敢决定真正删哪些特征。另外一个小技巧如果你分别用 gini 和 entropy 训练两棵树然后对比它们给出的特征重要性排序如果排序差异很大说明数据里可能存在多重共线性或者特征冗余这时候需要警惕模型对特征选择的稳定性。最后再分享一个我学习决策树时最重要的小习惯每学一个算法一定拿一个小数据集手算一遍。信息增益、基尼指数、增益率这三个指标你至少手推一次才能在看 sklearn 文档时真正明白每个参数在干什么。等到你能不看公式就把一棵树的构建过程完整讲给别人听第4章你就真正拿下了。
返回列表