1. 为什么西瓜书选“挑西瓜”来讲决策树
1.1 一个挑瓜场景里的隐含决策逻辑
你有没有过这种经历:西瓜书从线性模型一路读到决策树,公式突然变多,例子也跟着变多,眼睛看懂了,合上书又讲不清楚。我也卡过这一章,卡到后来把手算过程老老实实抄了三遍才通。今天这篇就把决策树(DT)这章从头到尾拆一遍,不绕弯子,把书上那些“跳步”的地方补上。
先说决策树到底在干什么。你去菜市场挑西瓜,会先看瓜蒂是不是新鲜,再看纹路是不是清晰,敲两下听声音是浊是脆。这套经验凑在一起,就是一个if-else规则链:若纹路清晰且敲声浊响,则多半是好瓜;若纹路模糊,则大概率不是好瓜。决策树算法做的事情,就是让机器从数据里自动学出这样一棵规则树。《机器学习》这本书里大名鼎鼎的西瓜数据集,就是用来讲这件事的极好素材。
为什么选西瓜而不是别的?因为西瓜数据集天然覆盖了决策树章节要讲的所有考点:有离散属性(色泽、根蒂、敲声、纹理、脐部、触感),有连续属性(密度、含糖率),有缺失值的情况,还是二分类问题(好瓜/坏瓜)。一个例子贯穿全章,背后其实是精心设计过的教学结构。你要把这章学透,不能光记结论,得知道周志华老师为什么在每个小节换不同的处理策略。
1.2 西瓜书第4章的主线其实是四个问题
整个决策树章节看似内容很多,实际上就是在回答四个问题:
第一,每到一个节点,到底用哪个属性来划分?这是属性选择度量问题,对应的是信息增益、增益率、基尼指数这三套标准,分别对应ID3、C4.5、CART算法。
第二,树一层层往下长,什么时候该停?如果不停,树会为了把训练样本全分对而疯狂长分支,结果就是过拟合。这对应剪枝处理,包括预剪枝和后剪枝。
第三,数据里出现连续值或者缺失值怎么办?书上专门讲了连续值的二分法,以及C4.5那套带权重的缺失值处理方式。
第四,能不能不只用一个属性划分,而用多个属性组合出更灵活的边界?这就是多变量决策树的内容。
把这四个问题挂在脑子里再看书,第4章的脉络一下就清楚了。很多同学觉得决策树杂,是因为把本章当知识清单背,而不是当作“四个决策问题”的组合拳来理解。
1.3 西瓜数据集2.0的基本盘
先认识一下接下来要反复用到的数据集。西瓜数据集2.0有17条样本,其中好瓜8个、坏瓜9个,每个样本有6个离散属性、2个连续属性。离散属性包括:色泽(青绿、乌黑、浅白),根蒂(蜷缩、稍蜷、硬挺),敲声(浊响、沉闷、清脆),纹理(清晰、稍糊、模糊),脐部(凹陷、稍凹、平坦),触感(硬滑、软粘)。连续属性是密度和含糖率。
这个数据集的分类难度适中,不是那种随便一个属性就能彻底分开的数据,所以卡在某个属性上继续深挖的余地很大,非常适合演示“为什么一步选不对,后面就全偏了”。包括后面讲剪枝时的验证集精度,也是在这套数据上做的。
你需要把这张表的结构记在心里。因为接下来手算信息增益时,我要带你把17条样本逐个掰开看,每个计数都会用到这组数据。纸上得来终觉浅,决策树这一章尤其如此,哪怕你看懂了公式,也要亲手算一遍才算真懂。
2. “信息增益”手算一遍才算真正看懂ID3选属性
2.1 熵的直觉:信息量就是“惊讶程度”
信息熵这个概念,初看公式吓人,其实直觉非常简单。你可以把它理解成“对一件事的惊讶程度”。掷一枚硬币,正反面各有50%概率,结果出来之前你完全无法预测,惊讶程度最大;如果一枚硬币几乎每次都是正面,那你就不会惊讶,信息量就小。熵衡量的就是这种不确定性:概率分布越均匀,熵越大;越偏斜,熵越小。
用天气打比方。在一个常年晴天的城市,明天下不下雨很好预测,天气的熵很低;在一个每天随机晴雨的沿海城市,明天天气极不确定,熵就很高。对分类问题来说同理:一个节点里两个类别的样本各占一半,你最拿不准;如果全是同一类,就不需要再划分了。
公式是:
Ent(D) = - ∑ p_k · log₂(p_k)
p_k表示第k类样本占比。二分类的话,p取0.5时熵是1,p接近0或1时熵趋近0。西瓜数据集里,17个样本中好瓜8个、坏瓜9个,比例非常接近一半一半,所以根节点的熵大约是0.998,接近最大值1。也就是说,在没做任何划分之前,判断一个西瓜是好是坏的难度接近“抛硬币”。
2.2 根节点手算:色泽、根蒂、敲声的信息增益
信息增益的概念一句话就能说明白:用某个属性把数据切开之后,不确定性减少了多少。减少得越多,说明这个属性越能帮我们做判断。公式是:
Gain(D, a) = Ent(D) - ∑ (|Dᵛ| / |D|) · Ent(Dᵛ)
其中Dᵛ表示属性a取值为v的那批子样本。先算根节点的基础熵,上面说了大约是0.998。然后逐个属性算划分后的加权熵,两者一减就是信息增益。
拿“色泽”举例。17个样本里,青绿6个(3好3坏),乌黑6个(4好2坏),浅白5个(1好4坏)。三个子集合的熵分别是:青绿Ent=1.000,乌黑Ent约0.918,浅白Ent约0.722。加权后的总熵是:
(6/17)×1.000 + (6/17)×0.918 + (5/17)×0.722 ≈ 0.889
所以色泽的信息增益 = 0.998 - 0.889 = 0.109。
按同样的算法规规矩矩把所有离散属性都过一遍,结果就是书上表4.1的数据:纹理的信息增益0.381最大,脐部0.289排第二,根蒂0.143,敲声0.141,色泽0.109,触感0.006垫底。于是根节点选纹理。
你看到“纹理清晰”分支下面好瓜比例高,就会自然想继续往下分;“纹理模糊”分支全是坏瓜,直接标成叶子就行。这棵树的生长逻辑,其实每一步都是重复上面的计算,区别只是换到子数据集上继续算。
2.3 信息增益的局限:编号属性为什么“作弊”成功
看到这里你可以试着想一个极端问题:如果数据集里加一列“编号”,每个样本一个独一无二的值,用信息增益会怎么选?
答案非常尴尬。编号这个属性有17个取值,划分成17个分支,每个分支恰好一个样本,分支内纯得一塌糊涂,熵全是0。加权的总熵是0,信息增益直接拉满到0.998,碾压纹理。但这棵树对新样本完全没有泛化能力,因为编号这个属性跟“瓜好不好”没有任何关系,它只是把训练集背下来了。
这就是ID3算法用信息增益做属性选择时著名的“偏科”问题:它天然偏爱取值数很多的属性。你越能分,我越选你,可分得多不等于分得对。决策树章节从ID3讲到C4.5,核心动机之一就是修掉这个毛病。理解不了这一点,后面增益率那部分就不知道为什么存在。
3. C4.5的增益率:信息增益的“偏科”是怎么被修正的
3.1 固有值:给“爱分裂”的属性加个惩罚
C4.5给出的纠正办法是增益率(gain ratio)。它的思路很直接:一个属性如果取值特别多,天然就能把数据切成很多小片,熵降得快是“作弊”。那就把“取值多”这个特征本身做成惩罚项,在算信息增益之后再除以一个量,这个量叫固有值(Intrinsic Value):
IV(a) = - ∑ (|Dᵛ| / |D|) · log₂(|Dᵛ| / |D|)
你看这个公式,跟信息熵长得一模一样,只不过它衡量的是“这个属性自身取值分布的均匀程度”。色泽有3个取值,比例是6/6/5,分布均匀,IV算出来大约在1.5左右。编号有17个取值,每个分支只占1/17,IV就是log₂17≈4.09。把信息增益除以这个固有值,编号属性的增益率立刻被压到很低,作弊无效。
这就是增益率公式:
Gain_ratio(D, a) = Gain(D, a) / IV(a)
它等于是说:你信息增益高,但如果是因为你本身爱把数据切碎,我就扣你的分。
3.2 西瓜书的双阶段选择:不是简单地选增益率最大
如果你准备考试或者面试只看公式推导,容易踩一个坑:以为C4.5就是选增益率最大的属性。其实周志华老师在书上写得很清楚,C4.5的做法是先算信息增益,把信息增益高于平均水平的属性过滤出来,再在这一小批属性里挑增益率最高的。
为什么绕这么一圈?因为增益率也有自己的毛病:它在惩罚取值多的属性的同时,会反过来偏爱取值数较少的属性。一个属性只有两个取值且分布极不均匀,IV就会很小,除出来的增益率虚高。如果直接全局选增益率最大,很可能选到这种没什么区分度的属性。两阶段策略相当于中庸了一下——先用信息增益筛掉明显不行的,再用增益率在质量过关的候选人里压一压“爱分裂”的倾向。
放到西瓜数据集上,虽然书上没有把每个属性的增益率都列出来,但纹理的增益本来就高,IV也不算离谱,大概率仍然能胜出。重点是你要记住:增益率不是独立替代信息增益,而是与它配合使用。
3.3 增益率在剪枝和其他场景里别乱用
还有一个容易混淆的地方:C4.5用的增益率,主要用在“节点分裂时的属性选择”,不会用在剪枝判断里。做剪枝的时候,无论你是预剪枝还是后剪枝,验证集精度才是最终裁判。精度不涨就剪,精度涨就留,这跟上不上增益率没关系。
另外,处理连续属性时,书里选最佳划分点用的判据仍然是信息增益,也不套增益率。原因很简单:候选划分点都是同一个连续属性产生的,固有值大同小异,套增益率没意义。你把增益率的适用范围记成“离散属性的选择阶段”,基本就不会出大错。
4. 剪枝才是决策树的胜负手:预剪枝与后剪枝的取舍
4.1 一棵完全生长的树为什么容易过拟合
如果不做任何限制,让决策树一直生长,它会做一件很恐怖的事:把每个训练样本都单独用一个叶子节点装起来。训练集精度100%,看起来完美,可它记住的是训练样本的“名字”,不是背后的规律。换一批新样本,准确率往往掉得惨不忍睹。
这就好比一个学生把练习册的答案一字不差背下来,碰上题目改了数字就不会做。决策树天生表达能力太强,尤其在属性多、样本少的时候,几乎必然过拟合。过拟合的结果是泛化能力差,而机器学习模型的真正价值恰恰在于“对新样本的预测能力”,而不是对旧样本的复读。
所以,长树是学习,剪枝是克制。用一点点训练精度换来更大的泛化空间,这笔买卖在大多数情况下都划算。西瓜书这一节的核心矛盾,就是什么时候该克制、克制到什么程度。
4.2 预剪枝:边生长边检查,省时间但容易“近视”
预剪枝的思想用一句话概括:每次准备分裂一个节点,先用验证集比较一下划分前后精度,如果划分后验证集精度没有提升,就压根不让它长。
听起来很美,不需要树长完整再动手。但预剪枝有一个固有的缺点:它只看眼前一步,不知道两步之后会不会柳暗花明。很多属性单独看没什么用,但再往下分一层,就能把数据完美切开。预剪枝在第一步就把这条路堵死了,结果就是树长得太浅,欠拟合。
西瓜书里的例子也很典型:根据脐部划分后,某些分支看似纯度提升有限,预剪枝就不让纹理继续参与划分,于是整棵树只保留根节点附近的少量分支。你在验证集上得到一个还行的精度,但如果你把树放开来长,会发现某些深层分支比浅层的更可靠、更能提升验证精度。这就是“因果”和“果因”的问题:当前节点看不出来的价值,可能是指向后续更优划分的桥。
从工程实践讲,预剪枝的开销小,训练速度快,适合数据量大、特征多的场景,但它有可能过早砍掉潜力股。你需要在业务里权衡:如果你的数据噪声大,贪多确实容易出错,预剪枝保守一点反而更好;如果数据相对干净,预剪枝常常会欠拟合。
4.3 后剪枝:先生长再修剪,西瓜书中42.9%到71.4%的变化
后剪枝走的是另一条路线:先把树养到最大,让它把训练集吃透,然后自底向上考察每个内部节点,尝试把它替换成叶子节点,用验证集看精度是否下降。不下降就剪掉子树,只保留这个叶子;精度上涨或持平,就接受修剪;精度下降,就保留原来的子树结构。
这里有一个我非常喜欢的数字对比:在西瓜数据集上,不剪枝的完整树在验证集上精度只有42.9%,而后剪枝之后精度提升到71.4%。为什么完整树反而不行?因为完整树把训练集背得太死,验证集上一点面子不给。后剪枝大刀阔斧地砍掉大量冗余分支,保留下来的都是经过验证集检验的“真本事”。
后剪枝的开销比预剪枝大,要先完整建树再做反向扫描,训练时间更长。但它通常能保住更多有用的结构,泛化性能一般比预剪枝更好,这也是为什么在经典对比实验里后剪枝的胜率更高。实战中你如果只允许用一种剪枝策略,优先级通常是后剪枝高于预剪枝。
4.4 工程里的剪枝选择:参数限制与代价复杂度剪枝
落到工具层面,你打开scikit-learn的DecisionTreeClassifier,会发现它默认完全不剪枝,只是给你留了一堆参数来限制树的生长。min_samples_split、min_samples_leaf、max_depth、max_leaf_nodes,本质都是“参数化预剪枝”:一开始就不允许长太深、不允许叶子样本太少。
scikit-learn从0.22版本开始提供了基于代价复杂度(cost complexity pruning)的ccp_alpha参数。这属于后剪枝的路线:先生成完整树,再通过ccp_alpha控制剪枝强度。实际操作时,一个很常见的工作流是先不设限制让树/D树长满,再画一条“不同ccp_alpha下验证集精度”的曲线,选择一个让精度保持在高位的较小alpha值。这个流程比瞎调max_depth更科学,因为它相当于在完整树的基础上系统性地做最优裁剪。
顺带提醒一个新手容易踩的坑:决策树不需要特征缩放,因为它只看特征的相对顺序和阈值,不受平移和缩放影响。很多人先把数据标准化再喂树,纯属多此一举。树模型和线性模型在这一点的处理逻辑完全不同。
5. CART的基尼指数:不取对数也能衡量纯度
5.1 基尼值的含义:随机抽两个样本“打架”的概率
信息熵要算对数,公式虽然不复杂,但真跑大数据量时也是有开销的。CART算法选择了另一个度量——基尼值。它的定义极其优雅:从数据集中随机抽两个样本,它们的类别不一致的概率。公式是:
Gini(D) = 1 - ∑ p_k²
如果某个节点里全是好瓜,随机抽两个样本,类别必然相同,基尼值为0,最纯;如果两类各占一半,抽两个样本类别不一致的概率是0.5,基尼值最大。节点越不纯,基尼值越大;越纯,基尼值越小。所以CART分类树在选属性时,用的是“基尼指数”:
Gini_index(D, a) = ∑ (|Dᵛ| / |D|) · Gini(Dᵛ)
信息增益是越大越好,基尼指数是越小越好。方向反了,别记混。
“随机抽两个样本打架的概率”这个解释,是我见过最形象的机器学习比喻之一。它不是从信息量出发,而是从“分类错误”出发:数据越不纯,随手抓两个样本就越容易出不同类别,分类器要犯的错误概率就越高。
5.2 熵、基尼、分类误差率的三角对比
很多教材会把信息熵、基尼指数、分类误差率放在一张图里对比。二分类问题下,纵轴是“不纯度”,横轴是正样本占比p,三条曲线的形状非常接近:都在p=0.5时达到最高点,在两端降到0。
它们之间的差别也很微妙。分类误差率在p小于某个阈值之前保持一条直线,到达阈值点突然拐弯,是个分段折线,对“不纯度”的刻画最粗糙;信息熵的曲线最弯,在接近两端时斜率趋近无穷大,意味着它对“几乎纯净”的状态非常敏感;基尼指数居中,没有对数运算,形状上和信息熵Top接近。
实操中,在大多数数据集上,用基尼指数和信息熵选出来的划分差别很小。你选gini还是entropy,更多是计算开销和习惯问题,而不是一个明显的性能差异问题。可以记住一个结论:熵对纯度变化的判断更敏感,基尼在超大样本上计算更快,普通数据集二者基本等价。
5.3 同一棵数据,不同判据为什么可能选出不同的根
回到西瓜数据集。如果换成基尼指数在17条样本上重新选根,某些属性的排序可能会和信息增益下的排序有差异。这不是算法有bug,而是两种判据对“不纯度下降多少”的计分方式不同。信息增益是用熵的差值衡量,基尼指数是用混乱概率差衡量,二者对同一个分裂方案打出的分数比例不一样。
理解这一点,比记住某个具体数据集的结论重要得多。因为你做项目时换了数据,最优判据可能完全变化。哪怕gini和entropy常给出相近的结果,也不意味着你可以完全忽略它们之间的细微差别。遇到敏感业务,花时间在两个判据下都跑一遍,用验证集看最终精度,比站在理论层面争论谁更优更有价值。
5.4 sklearn里的决策树与西瓜书的主要差异
这里要特别提醒一个坑:西瓜书里讲的决策树,默认数据本身就是离散类别值,比如“青绿”“乌黑”。但scikit-learn的DecisionTreeClassifier不接受字符串类型的类别特征,你需要提前做编码,比如OneHotEncoder。等价于把每个离散取值变成一列0/1特征。
CART本身是二叉树,而ID3/C4.5可以生成多叉树。你打开sklearn源码或者画出来的树图会发现,离散特征编码后每个节点只做一次“是否等于某个值”的判断,一路分下去。这跟西瓜书里的多叉划分不一样,但本质逻辑一致:每次分裂都是在降低节点不纯度。
还有一个常见误解:sklearn的决策树不是严格意义上的CART原版,它在实现上做了修改和优化。比如它支持entropy作为划分标准,而原始CART并不用熵;比如它对连续特征找最优切分点的方式和数据存储做了优化。你如果论文里引用,说“scikit-learn实现了基于CART优化的决策树”更严谨。
6. 连续值、缺失值、多变量:把课本上的决策树拉到现实数据里
6.1 连续属性怎么选划分点:排序后找中点
真实数据里到处都是连续值,比如西瓜的密度、含糖率。连续属性没法像离散属性那样直接列出几个取值分成好几叉,常规做法是二分法。先把连续值从小到大排序,对任意相邻两个取值的中点是候选划分点,然后逐个算信息增益,找信息增益最大的那个点作为划分阈值。
比如“密度”属性,17个样本排序后出现许多候选划分点,算下来最优阈值往往落在0.381附近。划分规则就是“密度≤0.381”进左分支,“密度>0.381”进右分支。含糖率同理,可能最佳阈值在0.126附近。
有一个书上老提但大家常忽略的细节:连续属性可以在一条路径上被重复使用。比如第一次用“密度≤0.381”划分,之后在某个分支里还可以继续用“密度≤0.203”再划分。这是因为连续值是范围的判断,一次阈值只能切一刀,多次阈值才能逐步逼近真实边界。离散属性则不同,每个离散属性用一次就基本不再选它,因为取值被分尽了。
6.2 缺失值:C4.5那一套带权重的玩法
现实数据里缺失值非常常见,如果哪个属性有缺失就删样本,数据量很快就不够用。西瓜书里的缺失值处理,继承的是C4.5的思路,核心是两个问题:属性有缺失时怎么算信息增益?样本有缺失时划分到哪个分支?
第一个问题,计算属性a的信息增益时,只使用在处理样本集合中那些在属性a上没有缺失的样本。信息增益要通过无缺失的样本比例做加权修正,避免因为样本量少了导致数值失真。
第二个问题,有缺失值的样本,不再硬性划入某一个分支,而是带着一个权重同时进入所有分支,权重等于该分支样本数量占无缺失样本总数的比例。你可以想象成:一个缺失了“色泽”的西瓜,无法判断它是青绿还是乌黑,那就开三份“分灵子”,按青绿分支的比例进入青绿分支,按乌黑分支的比例进入乌黑分支,按浅白分支的比例进入浅白分支。后面的熵计算,再把每个分支的样本权重加起来算。
这套方法比直接删样本或者均值填充要稳。因为均值填充会把一个本来不确定的值强行变成一个确定值,扭曲了原始分布的随机性。C4.5的思路相当于在计算时保留了不确定性,而且让缺失样本的影响根据分支规模自然分摊。工程上用sklearn时,默认不接受缺失值,要么提前填补,要么换用支持缺失值的算法(如某些梯度提升库)。这倒是教材和工具的差异点,实际项目里要留意。
6.3 多变量决策树:从轴平行划分到斜划分
普通决策树每个内部节点只考察一个属性,画到二维平面上,每一刀都是平行于坐标轴的直线。西瓜书正文里也提到,这样的“轴平行划分”在某些场景下会非常笨拙。比如决策边界如果是45度斜线,普通决策树就要用好几段阶梯状折线去逼近它,树又深又长。
多变量决策树想解决这个问题:每个内部节点不再只挑一个属性,而是用多个属性的线性组合作为划分条件。比如不是“密度≤0.381”,而是“0.2×密度 + 0.8×含糖率 ≤ 阈值”。这样做出来的边界是斜的,能一步就切开斜向分布的数据,树的结构会更紧凑。
代价也很明显:可解释性大大下降。原本“纹理是否清晰”一句话就能解释,多变量节点变成一堆系数加权组合,用户看不懂。从模型演进的角度看,多变量决策树已经有点接近线性模型与树的结合体,它提醒你一件事:决策树不是死板的if-else集合,它只是一个框架,节点内部的判别函数完全可以替换成更灵活的模型。
实践项目里,其实很少直接用多变量决策树,因为它的复杂度高、可解释性弱、实现库也不常见。但你要知道它的存在意义,面试里如果被问到“决策树只能做轴平行划分吗”,你能把它和斜划分、线性判别结合起来的思路讲明白,就胜过大多数背概念的人。
我个人啃完这章,最大的体会是:别急着调库跑模型,先把西瓜数据集上纹理那一列的信息增益手算明白,再对照sklearn在鸢尾花数据集上的结果看gini和entropy的差异。等你能从根节点一路算到叶子,决策树才真正从书里长到你脑子里。按这个思路学一遍,再回头复习就是降维打击。