
最近在啃周志华老师的《机器学习》也就是大家说的西瓜书配合 DataWhale 的《南瓜书》一起看。读到第 4 章决策树的时候确实有一种通了的感觉。因为这一章正好把我在学编程时最早接触的if-else逻辑和真正做机器学习时用到的信息熵、信息增益这些数学概念串了起来。如果你也正准备入门机器学习或者已经上手写过一些分类模型但一直对基础原理有点模糊那么决策树这一章几乎是必读的。我只能说从朴素的规则判断走到数学化的特征选择这个跨度就是程序思维和机器学习思维的分界线这篇日志就把我的学习过程、代码验证和踩过的坑完整记录下来。1. 决策树if-else 直觉的体系化1.1 从手写规则到自动构建决策树到底解决了什么问题很多人第一次接触决策树的时候第一反应就是这不就是写一堆 if-else 吗。这个直觉没错而且非常准确。但关键在于实际写程序的时候我们面对的规则往往不是三五个而是几十个上百个。特征之间的组合情况是指数增长的人脑根本不可能手工枚举出所有场景。比如你要判断一个用户会不会购买某件商品特征可能有年龄、收入、地区、浏览时长、收藏次数、历史订单金额……这些特征的组合数量非常庞大而且很多判断条件之间存在重叠甚至矛盾手工写规则根本不可维护。决策树解决的就是这个问题它不是人写规则而是从带标签的数据里长出规则。它自动选择最合适的特征作为根节点再逐步在子节点上选择次优特征最终形成一棵树。每一层的分支本质上是一组如果特征 A 满足条件 X就走这个分支的判断和 if-else 没有任何区别。区别只在于判断条件的选择不是拍脑袋定的而是基于数据分布计算出来的比如信息增益最大化、基尼指数最小化。举一个最直觉的例子。假设你要判断今天适合打球吗你可能会先看天气如果是晴天再看湿度如果是阴天可能直接去。这棵树的根节点选天气是因为天气这个特征对最终决策的解释力最强。决策树算法做的事情就是把这个解释力量化。量化方式有很多种ID3 用信息增益C4.5 用增益率CART 用基尼指数。这也是为什么我说这一章是从 if-else 走向数学之美算法把哪个特征更重要这个玄学问题变成了可计算的数学问题。1.2 西瓜书场景为什么选色泽、根蒂、敲声而不选编号西瓜书第 4 章用了一个非常经典的数据集通过色泽、根蒂、敲声、纹理、脐部、触感等特征判断一个西瓜是好瓜还是坏瓜。这个数据集看着简单但细想一下你会发现一个很关键的问题如果算法纯粹追求分类正确率那么最省事的做法是直接用编号作为特征。因为每个样本的编号都不同每个编号对应一个确定的标签这样树可以长得极深每个叶子节点只有一个样本训练集正确率 100%。但这样的树有意义吗没有任何意义。它完全没有泛化能力遇到新样本直接歇菜。这就是决策树算法里特征选择模块存在的意义我们不是选一个能完美划分训练数据的特征而是选一个能带来最大信息量的特征。信息量这个概念要用熵来度量。一个特征划分之后如果各个子集的类别分布变得非常纯比如全是好瓜或者几乎全是坏瓜说明这个特征带来的信息量大我们就优先选它。而编号这种特征虽然划分后每个子集都很纯但它引入的其实是毫无规律的记忆而不是规律本身这在数学上会被惩罚——不是被某个明确的公式惩罚而是通过信息增益的计算结果自然体现出来如果所有特征都参与比拼编号的信息增益通常会非常大但这正暴露了它没有泛化意义的本质。所以实际使用决策树时必须注意特征的语义合理性不能盲目让算法自由选择所有属性。这里顺便提一下南瓜书的价值。西瓜书在讲信息增益时写了公式但没有一步步展开推导。南瓜书把公式 4.1 到 4.5 的熵、条件熵、信息增益这些概念掰开揉碎每一步写清楚为什么这样定义、怎么从全概率公式推过来。我当时照着推导了一遍确实比直接跳着读要扎实很多后面代码里的每个参数也不再是黑箱了。2. 特征选择背后的数学度量2.1 信息熵不确定性的定量刻画信息熵是这一章的地基。公式很简单对数底通常取 2单位是比特bit。如果你拿一个只包含两类样本的数据集每类各占一半那么熵就是 1 bit如果只有一类概率为 1熵就是 0。也就是说熵越大数据越混乱不确定性越高熵越小数据越纯越好分类。我一开始不太理解为什么一定要用 log。后来换个角度就通了log 的本质是编码长度的度量。假设你有 4 个等概率事件需要 2 个比特才能唯一区分00、01、10、11对应的熵正好是 log₂4 2。所以熵也可以理解为要完全区分这批样本平均至少需要多少个比特。当概率不均匀时比如 90% 的好瓜、10% 坏瓜平均需要的信息量就比均匀分布少因为你可以用很短的编码表示大概率是好瓜这个事实。这样理解之后信息熵就不再是一个抽象公式而是不确定性度量这个非常直接的直觉概念。条件熵更关键。它表示在已知特征 A 的条件下数据集 D 的熵是多少。如果某个特征 A 能让条件熵大幅下降说明这个特征对分类很有用如果条件熵几乎没有变化说明这个特征是废的。信息增益就是这两者的差值信息增益 信息熵 − 条件熵这个差值越大说明特征 A 带来的确定性提升越多就越应该选它作为当前节点的划分特征。决策树递归地重复这个步骤每层都选信息增益最大的特征最终长成一棵树。2.2 信息增益的计算示例以色泽为例光看公式容易飘我建议手算一次。拿西瓜书表 4.1 的数据一共 17 个样本正例好瓜8 个反例坏瓜9 个所以根节点的信息熵是Ent(D) −(8/17)·log₂(8/17) − (9/17)·log₂(9/17) ≈ 0.998现在看色泽这个特征它有三个取值青绿、乌黑、浅白。按色泽划分后青绿6 个样本其中好瓜 3 个坏瓜 3 个熵 −(3/6)log₂(3/6) − (3/6)log₂(3/6) 1.000乌黑6 个样本好瓜 4 个坏瓜 2 个熵 ≈ 0.918浅白5 个样本好瓜 1 个坏瓜 4 个熵 ≈ 0.722条件熵就是按比例加权Ent(D|色泽) (6/17)×1.000 (6/17)×0.918 (5/17)×0.722 ≈ 0.889所以色泽的信息增益 Gain(D, 色泽) 0.998 − 0.889 ≈ 0.109。同理可以算出纹理的信息增益约为 0.381是所有特征里最高的所以树的根节点选纹理而不是色泽。这一步非常重要因为很多人凭直觉以为色泽更重要——毕竟我们挑西瓜确实先看颜色——但在这个数据集里纹理的区分能力更强。机器学习的特征选择不依赖直觉依赖计算这就是数学之美的一部分。2.3 增益率与 C4.5修正信息增益的偏好信息增益不是完美的。它有一个明显的缺点偏向取值数目多的特征。回到编号这个例子如果把编号也放进特征集它的信息增益最大因为每个编号对应一个样本条件熵为 0。编号的取值是 17 种很多所以信息增益夸大了它的价值。但在实际应用中显然不能选编号。C4.5 算法用增益率来修正。增益率是信息增益除以固有值固有值其实就是特征取值本身的熵。取值越多、越分散固有值越大增益率就被压低。比如编号的固有值极大因为每个取值只出现一次分布非常均匀所以增益率反而很小。但增益率也有自己的毛病它偏向取值数目少的特征。所以 C4.5 采取了一个启发式策略先从候选特征里挑出信息增益高于平均水平的那些再从中选增益率最高的。这个先过滤再挑优的思路本质是在信息增益和增益率两个偏好之间找平衡。我在实际用 sklearn 的决策树时没有直接用 C4.5因为 sklearn 的DecisionTreeClassifier用的是 CART也就是基尼指数。但理解 C4.5 的思路对于调参还是有帮助的特别是理解为什么不直接最大化某个单一指标这个问题。2.4 基尼指数与 CART另一条路CART分类与回归树用的是基尼指数。基尼值的定义是Gini(D) 1 − Σ pₖ²这个公式更简洁把所有类别的概率平方加起来然后被 1 减。如果数据集非常纯比如全是好瓜p₁1Gini0如果两类各占一半Gini1−0.5²−0.5²0.5。基尼值度量的是从数据集中随机抽取两个样本其类别不一致的概率。这个概率越小说明数据集越纯。没错它就是分类问题里随机抽取两个样本标签不一致的概率理解成混乱程度完全合理。对于二分类问题熵和基尼值其实高度相关二分之一熵的关系就不用去记知道它们的变化趋势一致就够用。CART 在候选属性集合中选的是划分后基尼指数最小的那个特征。因为基尼指数越小划分后的子集越纯。如果你用 sklearn 的决策树做过项目你会注意到它默认的 criterion 是gini。国内很多教材习惯用 C4.5 讲决策树但工程上 CART 才是主流。原因之一是 CART 只生成二叉树处理起来更简单也更容易做剪枝另一个原因是 CART 可以直接处理回归问题而 ID3 和 C4.5 主要面向分类。所以这一章学习时建议把 ID3、C4.5、CART 三者放在一起对比理解而不是只记住其中一个。3. 剪枝对抗过拟合的修炼3.1 预剪枝 vs 后剪枝一个贪心一个周全决策树有一个天然的问题如果不加限制它可以一直生长直到每个叶子节点都只剩一个样本。这样训练集上准确率接近 100%但泛化能力极差。这就是过拟合。剪枝是决策树对抗过拟合的核心手段分为预剪枝和后剪枝。预剪枝是在生成树的过程中每次准备划分之前先判断划分之后泛化性能有没有提升如果没有就停止划分把当前节点变成叶子。这个策略很节省计算时间但也有明显风险。因为某个特征单独看可能提升不大但它是后续更好划分的垫脚石一旦提前停止可能错过更优的子树结构。西瓜书上有一个例子预剪枝只留下了一层树虽然验证集准确率不错但没有后剪枝得到的树结构完整。后剪枝则是先生成一棵完整的树然后从下往上检查非叶节点如果把某个非叶节点替换成叶子泛化性能有没有提升如果有就剪掉这棵子树。后剪枝的计算开销更大但通常比预剪枝保留更多分支偏差更小。工程上后剪枝的效果往往更好这也是 CART 剪枝采用后剪枝思路的原因。如果用一句话区分预剪枝是边建边剪后剪枝是建完再剪。前者贪心、高效、可能欠拟合后者周全、耗时、通常效果更好。这有点像写作预剪枝是列提纲时就想好哪些段落不写后剪枝是写完初稿再删废段落。3.2 损失函数视角剪枝的本质是结构风险最小化西瓜书里 CART 剪枝提到一个损失函数C(T) Σ Nₜ·Hₜ(T) λ|T|这里 Nₜ 是叶子节点 t 的样本数Hₜ(T) 是节点 t 的经验熵|T| 是叶子节点个数。这个式子的含义非常清楚第一项是经验误差也就是这棵树在训练集上拟合得好不好第二项是结构惩罚项叶子节点越多模型越复杂惩罚越大。λ 控制两者之间的平衡。当 λ 0 时只要叶子节点能降低损失就会分裂树容易过拟合λ 越大树越倾向用更少的叶子节点换取更简单的结构。CART 剪枝做的就是对每个内部节点尝试剪掉子树对比剪枝前后的损失函数大小保留提升泛化能力的那次剪枝。这个过程可以理解为结构风险最小化——不光看拟合误差还要看模型复杂度。这个框架对我帮助很大。以前调参时只知道max_depth设小一点能防过拟合但不理解为什么。现在我知道了限制深度本质上是限制叶子数量也就是在加大结构惩罚迫使树保持简单。这不是玄学是优化目标里明确写着的。3.3 sklearn 剪枝的实操心得参数怎么调才不慌我自己用DecisionTreeClassifier调参时最常用的三个参数是max_depth、min_samples_leaf和ccp_alpha。先说max_depth简单粗暴限制树的最大深度。但要注意深度太小容易欠拟合深度太大又失去了限制意义建议结合交叉验证从小到大试。实践中我用鸢尾花数据集试过max_depth3已经接近最优精度max_depth10反而在测试集上掉点典型的过拟合信号。min_samples_leaf限制叶子节点的最少样本数。这个参数比max_depth温和因为它不会切断某些有价值的深分支只会阻止那些样本极少的细分。经验值一般从 1、5、10 开始试。我遇到噪声比较多的数据集时通常把min_samples_leaf调大一点效果比硬限制深度更自然。ccp_alpha是 sklearn 里的 CART 剪枝代价复杂度参数对应上面提到的 λ。调它的思路是先不管复杂度用很小的ccp_alpha比如 0.001训练一棵树然后逐渐增大ccp_alpha观察训练集和验证集的精度曲线。选择一个让验证集精度最高且树不至于太简单的 α。这种做法比手动调深度更有全局视角因为它是真正在做结构风险最小化而不是靠经验试探。需要提醒的是剪枝不是越多越好。过度剪枝会把模型变成一层树虽然简单但丢失了数据中的有效模式。我见过一些新手上来就把max_depth设为 2结果模型在训练集上表现都一般。树的复杂度和数据本身的非线性程度必须匹配这个平衡只能靠验证集曲线来寻找没有一劳永逸的万能参数。4. 连续值与缺失值的处理4.1 连续属性离散化二分法与候选划分点西瓜书里的西瓜数据大多是离散属性但真实数据几乎必有连续特征比如年龄、收入、身高、体重。决策树处理连续值常用的方法是二分法把连续值排序之后取相邻两个取值的中点作为候选划分点逐一计算信息增益选最优的那个划分点。假设某个连续特征有 n 个不同的取值排序后有 n−1 个候选划分点。每个候选点把数据分成小于等于 T和大于 T两个子集然后按离散特征的方式计算信息增益。这里有个细节需要注意连续特征可以被多次重复使用。离散特征一旦在某个节点用掉了通常后面不会再作为划分特征但连续特征可以因为它每次用的划分点可以不同比如第一次用年龄 ≤ 30下一次还能用年龄 ≤ 50。这个特性在实现决策树时也要注意sklearn 的 CART 实现每次都会重新对所有特征搜索最优划分点包括之前用过的连续特征。我在实践中的体会是连续值的划分点选择非常依赖数据分布。如果数据里某个特征的取值整体偏大或偏小划分点的中位数位置可能并不代表最佳信息增益位置。所以不要想当然认为取均值就好一定要让算法遍历候选点。好在 sklearn 里这个过程是自动的你不需要手写二分法。但理解原理有助于你解释一个现象为什么决策树的边界都是水平或垂直的直线因为每次划分都是基于单个特征的阈值边界一定是特征轴平行的。这也是后面多变量决策树要解决的问题。4.2 缺失值处理的数学机制从公式 4.8 到 4.11真实数据里缺失值非常常见。很多人在做特征工程时直接删除有缺失的行这很浪费。西瓜书给了两套处理思路一是如何在特征值缺失的情况下选择划分特征二是选定特征后如何对有缺失值的样本进行划分。核心用到了三个量无缺失值样本占比 ρ无缺失样本中第 k 类占比 p̃ₖ无缺失样本中在特征 a 上取值为 v 的占比 r̃ᵥ。信息增益的计算需要把这些权重乘上去。也就是说缺失值特征的信息增益用无缺失样本的分布来估计但最终按比例缩放了整体样本数量。划分阶段更巧妙对有缺失值的样本不直接丢掉而是按权重同时进入所有分支。样本的初始权重是 1进入每个分支时乘以对应分支的样本占比 r̃ᵥ相当于让缺失值样本按概率分散到各个子节点。南瓜书对这几个公式的推导做得特别细我照着推了一遍发现本质上就是用无缺失样本估计分布再用分布处理缺失样本。这个过程在理论上很优雅但实操上需要注意sklearn 的决策树不支持缺失值处理它会直接报错或无法处理 NaN。所以用 sklearn 做项目时缺失值要么填充均值、中位数、众数要么用专门支持缺失值的库比如 HistGradientBoosting 这类直方图提升方法。理解西瓜书里的公式更多是为了建立完整的知识框架而不是指望 sklearn 直接用上。4.3 实操中的细节归一化对决策树有意义吗有一个问题经常被提到决策树需不需要归一化答案是不需要。因为决策树每次划分只看特征取值的相对大小不涉及距离计算所以特征缩放不影响结果。这和 SVM、KNN 这类基于距离的模型完全不同。我在处理收入预测这类数据时如果用的是决策树或随机森林基本可以跳过归一化步骤直接喂原始数值。但是有一个例外情况如果你同时使用了多个模型做融合模型对比有些模型需要归一化那统一处理可以省事虽然对决策树本身不产生帮助。另外决策树对异常值也不太敏感因为它把连续值离散化成阈值比较一个极端大的异常值最多影响某个阈值的选取不会被直接算进均值或距离。这种鲁棒性也是决策树在表格数据上依然流行的重要原因之一。5. 从一棵树到一堆树拓展视野5.1 多变量决策树打破轴平行的边界前面提到经典决策树的划分边界都是轴平行的也就是水平或垂直的直线。这在真实数据里是个局限因为很多分类问题的最优边界是斜线、曲线或者分段函数。多变量决策树试图解决这个问题它不再用单个特征作为划分维度而是用多个特征的线性组合作为划分条件。换句话说每个内部节点不再问age 是否小于 30而是问0.2×age 0.8×income 是否大于某个阈值。这个思路其实就是把线性判别分析和决策树结合起来。西瓜书里用公式 4.12、4.13 给出了形式化定义但说实话这种树工程中用得不多因为引入线性组合后树的解释性会下降而且计算复杂度明显上升。我理解这一节更多是为了打开思路决策树不是只能做单个特征阈值的划分它同样可以嵌入更复杂的判别函数。5.2 随机森林和决策树区别为什么集成后边界更平滑决策树有一个毛病高方差。数据稍微变一点点树的结构可能完全不同。这导致单棵决策树在新数据上的表现波动很大。随机森林的思路很简单用 Bagging自助采样产生多份训练集每份训练集上训练一棵树最后投票出结果。更关键的一步是随机森林在每棵树的每个节点做特征选择时不是从全部特征里选最优而是先随机抽一个特征子集比如总特征的 sqrt 个再在这个子集里选最优划分。这相当于给每棵树人为注入随机性让它们之间的相关性尽可能低。为什么要降低树之间的相关性因为 Bagging 本身只能降低方差但如果所有树长得都一样投票结果和单棵树没区别。只有树之间足够不同集成后的方差下降才明显。随机森林和单棵决策树最核心的区别就在这里单棵树可能过拟合、边界不光滑随机森林通过大量树的平均让决策边界更平滑、泛化更强。这一点和热词决策树如何逼近真实曲线很契合单棵决策树用阶梯状的边界去逼近曲线逼近得粗糙随机森林相当于多条阶梯边界的投票叠加整体边界更接近真实曲线。但随机森林也有代价可解释性变差。单棵决策树你可以把整棵树画出来走一遍从根到叶子的路径给用户一个清晰解释。随机森林几百棵树没法可视化解释。所以如果是需要强解释性的业务场景比如金融风控、医疗诊断有时候宁可用单棵树或者逻辑回归也不愿意牺牲可解释性。如果是纯看效果的钱反而不是问题随机森林在表格数据上通常比单棵树好不少。5.3 用 sklearn 快速复现实验鸢尾花与收入预测我建议读者在学完这一章之后动手做两个实验。第一个是鸢尾花分类数据集内置于 sklearnload 出来就能用。用DecisionTreeClassifier(criterionentropy, max_depth3)试一下打印出决策树可视化的结果对照西瓜书里的树结构体会特征选择的过程。第二个是收入预测可以用成人收入数据集UCI Adult特征包括年龄、教育年限、职业、工作时长等目标是否收入超过 50K。这类数据特征类型复杂更适合体验连续值、离散值、缺失值处理的完整流程。代码结构大致是import pandas as pd from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score # 假设 df 是处理好的训练数据X 是特征y 是标签 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) clf DecisionTreeClassifier( criteriongini, max_depth5, min_samples_leaf5, random_state42 ) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(Accuracy:, accuracy_score(y_test, y_pred))这里要注意random_state必须固定否则每次运行树结构都不一样不利于对比实验。我一开始没设随机种子调参时差点被坑惨——以为参数有效果其实是随机抖动。固定随机种子之后每次实验结果才可复现这才谈得上调参比较。可视化决策树可以用sklearn.tree.plot_tree或者export_graphviz导出后画图。plot_tree 简单直接适合演示。输出时建议限制深度否则树太大画出来根本看不清。给树上色的参数filledTrue能直观看到每个叶子节点的类别分布非常好用。6. 学习心得与避坑指南6.1 西瓜书南瓜书怎么配合阅读最高效我个人的经验是先读西瓜书的正文把概念和公式的位置弄清楚不需要急着看推导。读到公式卡住时再翻开南瓜书对应章节一步步跟推导。南瓜书不是替代西瓜书而是给西瓜书里的公式补上为什么能这样推的缺环。比如信息增益公式里为什么是求和而不是平均比如条件熵中概率 P(x) 怎么从数据集里估计出来这些细节南瓜书都写得很到位。重点是要亲手推导。光看南瓜书觉得自己懂了合上书再推一遍就会发现自己漏了很多细节。我在推导条件熵公式时因为对某一特征取值下的子集比例这个概念理解不清来回看了好几遍。后来我在笔记本上用西瓜书表 4.1 的数据把信息增益从零开始算了一遍才算真正把这个公式内化了。6.2 容易踩的四个理解误区第一个误区是信息增益越大特征一定越好。在同一个数据集、同一套候选特征里比大小没问题但跨数据集或特征语义完全不同的场景下这个数值没有绝对意义。第二个误区是剪枝一定提升准确率。剪枝的目的是提升泛化能力如果数据本身没有过拟合剪枝可能反而降低准确率。必须先用验证集确认是否过拟合再决定下多重的手。第三个误区是决策树不需要数据清洗。虽然决策树对缺失值和异常值有一定容忍度但重复特征、无关特征、极度不平衡的类别还是会显著影响树的构建效果。第四个误区是树越深越好这是新手最容易犯的错训练集上精度确实高但测试集上的表现往往一塌糊涂。6.3 高频考点速查三个算法的对比把 ID3、C4.5、CART 放在一张表里对比方便复习算法特征选择度量树结构连续值处理缺失值处理适用任务ID3信息增益多叉树不支持不支持分类C4.5增益率多叉树二分法支持分类CART基尼指数二叉树二分法支持分类回归这个表格基本囊括了面试里关于决策树的高频考点。特别是 CART 和 ID3、C4.5 在树结构上的差异很多初学者会忽略CART 永远生成二叉树而 ID3 和 C4.5 会根据特征的取值数量生成多叉树。这也是为什么在 sklearn 里即使某个离散特征有 5 个取值它也会通过是否为某某值这样的条件来做二分而不是一次性分出 5 个分支。还有一个细节容易在面试里被追问ID3 为什么不支持连续值因为它的特征选择公式是为离散取值设计的处理连续值时要引入额外的离散化流程而 C4.5 明确实现了这个流程所以大家默认把连续值处理当作 C4.5 的能力之一。最后再分享一个小技巧每次学完一章我都要求自己用 3 分钟的时间在白纸上默写出核心公式和它们之间的关系。比如这一章的核心链条就是熵 → 条件熵 → 信息增益/增益率/基尼指数 → 特征选择 → 树生成 → 剪枝——如果你能流畅地默写出这条链并且能准确解释每个环节的为什么这一章就真正学透了。决策树本身不复杂复杂的是隐藏在公式背后的设计动机把动机搞明白代码和调参都有底气。