
三天一门课的节奏到底有多酸爽我是在Datawhale 7月“吃瓜教程”组队学习里真切体会到的。这个月的活动把整本《机器学习》拆成了七个任务Task03正好落在第四章“决策树”也是我这次打卡过程里最纠结、但收获最大的一段。如果你也在跟这个教程或者正打算用西瓜书打机器学习基础这篇东西应该对你有用——我会把这一关到底要学什么、公式怎么推、代码怎么写、打卡怎么交全部摊开来讲。先交代一下背景吃瓜教程的学习闭环基本是“看视频、读西瓜书、用南瓜书补推导、写代码、交笔记打卡”。Task03的内容量说大不大但决策树这一章是后面所有集成学习的地基你在Task05、Task06遇到随机森林、梯度提升树的时候会发现全都是在跟树打交道。所以这一关值得认真啃而不是为了打个卡随便抄一份笔记。1. 这一关到底在练什么1.1 一次组队学习的标准任务单我是过了两天才摸清Task03的路数。任务单看起来就几行字实际上拆开是这样的看对应章节的讲解视频吃瓜教程会把西瓜书内容用课件过一遍重点是补背景和挖坑点通读西瓜书第4章整理核心概念笔记对照南瓜书把信息增益、增益率、基尼指数这几个公式自己推一遍手写一颗简单决策树的划分代码再用sklearn跑一遍对比结果把笔记和代码运行结果整理成Markdown在规定时间前提交到打卡页面这套流程里最花时间的不是读课文而是“手推公式”和“手写代码”。我一开始也想偷懒书上的公式都现成的抄一遍不就行了结果一合上书让我自己写信息增益的计算过程我连条件熵里面的累加符号都差点搞反。所以后来我总结出一条南瓜书的作用不是给你答案而是逼你从“看得懂”变成“算得出”。1.2 为什么决策树值得单独用一整章讲决策树在西瓜书里的地位比较特殊。它本身是一个完整的监督学习模型但它更是后面一堆强模型的基础组件。你后面学集成学习随机森林就是“多棵决策树投票”AdaBoost里的基学习器也常用决策树GBDT和XGBoost更是把“树”玩出了花。可以说如果Task03这一章没吃透后面Task05、Task06你会非常难受。另外决策树是少数几个“白盒”模型。神经网络你很难解释为什么给这个结果但决策树你完全可以把预测路径画出来告诉别人“因为色泽青绿、根蒂蜷缩、敲声浊响所以判断为好瓜”。这种可解释性在风控、医疗、金融这些需要向监管和用户解释的场景里特别值钱。理解这一点你就能明白为什么教材要花一整章把树的生成、划分、剪枝讲得那么细。1.3 键打卡前先想清楚目标我个人的经验是打卡学习最怕沦为“形式主义”。如果你只是把别人的笔记转发到自己的博客然后提交链接那你参加这个活动就亏大了。真正有效的打卡应该满足三个目标第一你能不看任何资料把决策树生成算法的伪代码写出来。第二你能手算一个只有十来个样本的小数据集的信息增益并且算出下一步该选哪个属性做划分。第三你能说清楚预剪枝和后剪枝各自的代价是什么以及为什么sklearn里没有直接暴露“后剪枝”按钮。带着这三个目标去学再看视频和教材你会发现自己看东西的视角完全不一样——你不再是“读故事”而是在“找答案”。2. 从信息熵到三种划分准则的推演2.1 信息熵到底在量什么决策树的核心问题只有一个怎么选属性来划分数据。西瓜书给出的答案是“信息增益”这一套而信息增益的源头是信息熵。信息熵的定义式很好记若样本集合D中第k类样本所占比例为p_k则D的信息熵为Ent(D) -Σ p_k · log2(p_k)我第一次看到这个式子的时候心里想的只有一件事这玩意儿凭什么能度量“不确定性”后来我找了个生活化的类比才转过弯来一个盒子里的球全是红的你猜下一个摸出什么颜色一次就猜中这叫不确定性低如果红球白球各一半你猜中的概率只有一半叫不确定性高。信息熵就是在度量“如果你要做一次分类预测平均需要多少比特信息才能确定结果”。全是同一类时熵是0两类五五开时熵是1类别越多越均匀熵就越大。条件熵则是“在知道某个属性取值之后不确定性还剩多少”。信息增益就是“知道这个属性前后的不确定性下降量”下降越多说明这个属性越关键所以优先用它做划分。2.2 西瓜数据集的计算现场光说不练没有用。我拿西瓜书那个经典数据集17个样本好瓜/坏瓜二分类原始数据集熵Ent(D)≈0.998手动算了几个属性的信息增益。过程不复杂但特别容易出错色泽青绿6个好瓜3坏瓜3、乌黑6个好瓜4坏瓜2、浅白5个好瓜1坏瓜4算下来增益约0.108根蒂蜷缩8个好瓜5坏瓜3、稍蜷7个好瓜4坏瓜3、硬挺2个好瓜0坏瓜2增益约0.143敲声浊响8个好瓜5坏瓜3、沉闷7个好瓜2坏瓜5、清脆2个好瓜0坏瓜2增益约0.141纹理清晰8个好瓜7坏瓜1、稍糊5个好瓜1坏瓜4、模糊4个好瓜0坏瓜4增益约0.380脐部凹陷7个好瓜6坏瓜1、稍凹6个好瓜4坏瓜2、平坦4个好瓜0坏瓜4增益约0.289触感硬滑9个好瓜6坏瓜3、软粘8个好瓜3坏瓜5增益约0.006算完你就发现纹理的增益最大0.380所以第一层划分应该选纹理。这种手算的体验是看视频替代不了的——你在草稿纸上累加的时候才会注意到每个子集的熵都要加权权重就是子集占全集的比例漏掉任何一个加权因子结果都会偏。2.3 增益率与基尼指数到底改进了什么信息增益有个毛病它天然偏好取值数目多的属性因为把数据切得越碎每个子集越“纯”增益自然就高。比如“编号”这个属性每个样本一个值按它划分后每个子集只有一个样本熵直接变成0增益直接满分。可这个划分没有任何泛化能力。C4.5的增益率就是来治这个毛病的用固有值Intrinsic Value惩罚取值多的属性本质是“增益除以复杂度”相当于绩效要除难度系数。CART则是换了个度量工具用基尼指数代替熵。基尼指数衡量的是“从集合里随机抽两个样本它们的类别不一致的概率”值越小越纯。它的好处是计算比熵快——没有log运算在工程实现和大规模数据集上更友好。所以你会看到sklearn里DecisionTreeClassifier的criterion参数有“entropy”和“gini”两个选项默认是gini不是因为gini一定更好而是因为它在计算上更省。这里有一个很容易踩的细节不管是熵、增益率还是基尼指数它们在不同数据集上算出来的绝对数值不可跨数据集比较你只需要在同一个数据集内部排序选最优属性就行。我打卡的时候一度纠结“为什么纹理增益0.38别人笔记里写的却是0.42”后来发现人家用的是简化版数据集或者没有加权版本不同而已。所以在笔记里写清楚自己用的是哪个数据集、熵值是多少比盲目对齐别人的数字重要得多。3. 剪枝与过拟合一道绕不过去的坎3.1 树为什么会“长歪”决策树最容易犯的错就是过拟合。划分属性越细、树越深训练集上表现越好但一到新数据就翻车。这跟你背答案应付考试是一个道理你说得出每一道题的选项但换道同考点的题就懵了。西瓜书给的例子特别典型如果让树无限制往下分最后一层可能每个叶子只装一两个样本这些样本的“规则”其实是噪声不是规律。剪枝的意义就是主动砍掉一些分支用训练集上的一点“损失”换泛化能力的大幅提升。3.2 预剪枝边建边剪的短视策略预剪枝的思路很直白树还没开始长这个分支之前先判断“这个分支对验证集有没有帮助”。有就长没有就当场剪掉。我照着书上的例子把训练集和验证集分开每次考虑要不要展开一个节点就先假设展开拿验证集精度对比不展开的情况只有展开后精度提升才继续。预剪枝的好处是省时间——不需要完整建树再回头剪分支数少训练快。但它的缺点也很明显贪心算法只看眼前一步。可能当前这一步划分验证集精度没提升但再深一层之后整体会更好预剪枝会把这个“潜力股”直接掐死导致欠拟合。用我同伴的话说这就像买房只看首付结果月供扛不住分期方案也不对。3.3 后剪枝先长满再回头砍后剪枝是另一个极端先把树完整长出来然后自底向上考察每个非叶节点判断“把它变成一个叶子节点也就是把整个子树剪掉会不会提升验证集精度”会就剪。这个过程计算开销大但因为每一步都是基于完整树的信息做判断保留分支的决策更全局泛化能力通常比预剪枝更好。我当时在笔记里画了个对比表这个表后来被好几个同组同学抄走了我觉得值得贴出来对比项预剪枝后剪枝剪枝时机建树过程中建树完成后计算开销低高欠拟合风险较高低过拟合风险较低低泛化能力通常弱于后剪枝通常更强工程常用度高省时间低样本大时太慢3.4 sklearn里怎么对应这两种思路有个地方特别容易让人迷糊sklearn的DecisionTreeClassifier里你看到max_depth、min_samples_split、min_samples_leaf这些参数它们都是在建树过程中生效的本质是预剪枝。而经典意义上的“后剪枝”sklearn老版本并没有直接提供现在你可以用ccp_alpha参数做成本复杂度剪枝Cost-Complexity Pruning这个机制从某种角度更接近后剪枝——它先建完整棵树再按复杂度代价从大到小砍分支用交叉验证选合适的alpha。实操时我建议这样组合先放宽预剪枝参数比如max_depth设得大一点min_samples_split设小一点让树充分生长再跑一遍ccp_alpha路径画出“树大小 vs 精度”曲线选拐点附近的alpha。这样既利用了后剪枝的泛化优势也避开了手写后剪枝的复杂度。这个套路我在打卡笔记里专门写了一段助教点评的时候还特别标了句“这里理解到位”。4. 连续值、缺失值、多变量树真正的进阶点4.1 连续属性怎么用二分法决策树天然擅长处理离散属性但现实数据集里大量特征是连续值比如含糖率、密度。西瓜书给出了二分法把连续值排序相邻取值的中点作为候选划分点然后逐个试试选信息增益最大的那个阈值。我一开始有个误区以为阈值就是某个真实样本值。后来推导才发现划分点t把集合分成“取值≤t”和“取值t”两部分所以t选在任意两个不同取值中间效果一样取中点最方便。计算时还有个小技巧只需要考虑“类别发生变化”的相邻点的中点作为候选能少算一大半。这个细节书上没展开但在南瓜书里提了一句我自己推了一遍才真正理解。写代码时要注意连续属性的阈值可以在同一节点的不同分支里重复使用而且每次划分后要重新算最优阈值不能沿用父节点的值。这些都是面试爱考、动手容易错的点。4.2 缺失值处理不只是一个“填值”问题现实数据没有西瓜数据集那么干净属性经常缺失。处理缺失值有两个层面的问题一是如何选划分属性二是选定属性后缺失该属性的样本该分到哪个子节点。西瓜书的做法很精巧用样本权重来估计。计算信息增益时只拿无缺失样本算然后乘一个比例ρ无缺失样本占全体的比例来修正划分时无缺失样本正常分到子节点带缺失样本则按每个子节点的样本权重比例分配到所有子节点同时样本权重也跟着缩放。我第一次看这段的时候直接看懵了。后来我打了个比方一班同学投票决定中午吃啥有人没来那统计口味偏好时只能拿在场的人估但要把“缺席率”折进去没来的人虽然没投票但万一他们下午回来了也得按投票比例安排他们的饭。懂了这层再看公式就顺了。4.3 多变量决策树用斜线代替直角切传统的决策树每次划分只用一个属性叫“轴平行划分”画出来是一堆横平竖直的边界。多变量决策树则是让每个非叶节点学习一个线性分类器用多个属性的线性组合来决定划分方向边界可以倾斜。相当于你不再只能沿着坐标轴切数据而是可以斜着一刀切。这个知识点打卡时容易忽略但我建议大家还是仔细读一遍。一方面它是从“决策树”过渡到“神经网络”之间的桥梁——每个节点做一个线性决策多层组合起来就能表达复杂边界另一方面很多面试题会拿它考察你对“模型表达力”的理解。我笔记里就画了个简单的二维示意图说明用单变量要三步才能完成的划分多变量一步就能切出来。5. 从公式到代码南瓜书配套与手写实现5.1 南瓜书该怎么用南瓜书本质上是西瓜书公式的“推导笔记”它把教材里跳步的公式一步步补全。我踩过坑这里必须提醒千万别把南瓜书当小说从头读到尾。正确的用法是先自己推卡住了再看。我都是先拿草稿纸推一遍信息增益推不出来再看南瓜书对应小节看它在哪里用了变量替换、哪里用了期望的性质拿红笔圈出来然后合上书再推一遍。打卡笔记里最值钱的不是你抄了南瓜书的推导而是你写出“我卡在第二步原因是没意识到分母要按样本量加权”这种记录。这种“卡点复盘”在组队学习里非常受欢迎批改助教也更愿意给这类笔记高分。5.2 手写一颗最简决策树Task03要求的手写代码不需要完整实现剪枝、缺失值处理但至少要把“信息增益选属性”这个核心逻辑跑通。我写的核心代码大概是这样的import numpy as np def entropy(labels): n len(labels) if n 0: return 0 counts np.bincount(labels) probs counts[counts 0] / n return -np.sum(probs * np.log2(probs)) def info_gain(feature, labels): # H(D) - H(D|A) base entropy(labels) total len(labels) cond 0.0 for v in np.unique(feature): subset labels[feature v] cond len(subset) / total * entropy(subset) return base - cond这段代码看起来很简单但你盯着它看一会儿就会发现条件熵是把每个取值子集的熵按占比加权加总占比如果漏乘结果就错得离谱。CART版本只要把entropy换成基尼指数即可def gini(labels): n len(labels) if n 0: return 0 probs np.bincount(labels) / n return 1 - np.sum(probs ** 2)连续值划分也不难核心是“候选点遍历”def best_split(feature, labels): vals np.sort(np.unique(feature)) candidates (vals[:-1] vals[1:]) / 2 best_gain, best_t -np.inf, None for t in candidates: left labels[feature t] right labels[feature t] cond len(left) / len(labels) * entropy(left) len(right) / len(labels) * entropy(right) gain entropy(labels) - cond if gain best_gain: best_gain, best_t gain, t return best_t, best_gain我把自己手写的树跑在西瓜数据集上生成的第一次划分就是纹理跟书上完全一致。那一刻非常有成就感——你用一页代码复现了教材里的算法说明你确实懂了而不是背下来了。5.3 sklearn版本的对照实验手写代码是为了理解原理工程上当然直接用sklearn。对照实验我建议固定同一份数据分别用gini和entropy跑再对比不同max_depth的效果from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) clf DecisionTreeClassifier(criterionentropy, max_depth3, random_state42) clf.fit(X_train, y_train) print(Train acc:, clf.score(X_train, y_train)) print(Test acc:, clf.score(X_test, y_test))这里有个非常常见的坑决策树对数据缩放完全不敏感你不用做归一化因为划分只看阈值比较。很多在神经网络里养成的习惯到这里会误导你我见过不止一个同学一上来就StandardScaler折腾半天发现结果一模一样。可视化是决策树学习的大杀器sklearn直接支持from sklearn.tree import plot_tree import matplotlib.pyplot as plt plt.figure(figsize(12, 8)) plot_tree(clf, filledTrue, feature_namesfeature_names, class_names[bad, good]) plt.show()画出来的树一眼就能看到每个节点的划分条件、样本数量和类别分布我打卡笔记里贴的就是这张图。对比手写树和sklearn树的划分结果你会发现自己实现的细节差异这就是最真实的学习反馈。6. 打卡过程中的常见坑与心得6.1 我见过的几个典型翻车现场组队学习群里每天都有求助帖我整理了一下Task03的高频问题基本集中在这几类症状原因解法信息增益算出来是负数加权因子漏了或概率算错先画全概率表逐项核对占比增益率结果总比ID3小很多固有值没取对数或计算有误再推一遍固有值公式注意它不涉及类标签手写树和sklearn树第一层不一致sklearn用的默认基尼或数据集版本不同显式指定criterionentropy检查数据编码测试精度远低于训练精度树太深导致过拟合加max_depth或用ccp_alpha剪枝不知道怎么处理数值型数据没看4.4节连续值部分回去做一遍二分法手算再跑代码对照我记得最典型的是一个小伙伴问“为什么我算的根蒂增益是0.146书上是0.143”后来一对发现他把“硬挺”两个样本中一个错误地归到了正类。这就是为什么我反复强调手算要写清楚每个子集的计数和类别分布过程暴露出来错误一眼就能定位。6.2 时间安排和打卡笔记的组织技巧吃瓜教程的打卡是有时间窗口的我身边不少人卡在最后一天晚上熬夜补笔记质量自然上不去。我的建议是三天任务用“111”分配第一天纯输入看视频、通读教材、把不懂的地方标记出来。第二天做推导和代码用南瓜书对照推导公式写核心代码跑通sklearn对照实验。第三天做输出整理笔记、画图、写卡点复盘、提交打卡。笔记的组织我推荐“任务目标—核心概念—推导过程—代码实现—结果分析—踩坑记录”的结构。不要写成教材复述重点写“你理解之后用自己的话怎么讲”。比如我会在笔记里写“信息增益就是看先知道哪个属性最压惊”这种白话解释比公式堆砌更能体现你学会了。6.3 组队学习真正的价值在哪打卡本身只是形式真正的价值在交流。我记得这次Task03的讨论区里有人把后剪枝和预剪枝在同一个数据集上的精度曲线对比贴了出来那篇帖子让我一眼就看懂了两者差距还有人分享了他用pandas逐步打印每个候选阈值的信息增益的调试过程那个调试思路比我自己的硬算高效得多。所以我强烈建议不要只盯着自己的笔记多去翻同组同学的作业和讨论。看别人的推导卡点和错误原因比自己闷头学效率高一倍。你甚至可以主动在别人的作业下面留言提问被问到的同学往往为了解释清楚会再复习一遍这对双方都是收获。我个人在实际操作中还有一个体会Task03打卡如果只做到“代码跑通”是不够的一定要把“为什么选这个划分点”用文字写出来。你写不出来的地方就是你还没懂的地方。决策树整个章节都是这个套路先把每棵树生长的逻辑写明白后面学随机森林、梯度提升的时候你就会发现地基已经打牢了——别人还在啃损失函数的时候你已经在琢磨树结构怎么设计更稳了。最后再分享一个小技巧打卡笔记里贴图的时候记得把plot_tree的字体调大一点默认图例根本看不清评论区三个人问我“你这个叶子的样本数在哪看”都是因为图太小。