查看全课大纲(第 28 / 82 节)
- 1.函数与数据泛化
- 2.向量:从几何直觉到高维空间
- 3.矩阵与张量
- 4.特征值与特征向量:矩阵变换的不动方向
- 5.凸函数:从向量求导的矩阵记号到好优化的函数形状
- 6.概率与统计:机器学习的数学基础
- 7.独立性与随机变量:贝叶斯公式与二项分布
- 8.期望、方差与协方差:随机变量的数字画像
- 9.联合概率:当一条轴不够用的时候
- 10.概率分布(一):从伯努利到伽马函数
- 11.概率分布(二):贝塔、泊松、正态与两个贝叶斯故事
- 12.机器学习的模型:找输入与输出的关系
- 13.机器学习的应用
- 14.过拟合
- 15.机器学习的分类
- 16.有监督学习:从房价预测说起
- 17.损失函数
- 18.梯度下降(上):更新规则与学习速率
- 19.梯度下降(2)
- 20.牛顿方法
- 21.KNN简介
- 22.距离测算
- 23.朴素贝叶斯
- 24.朴素贝叶斯的实际应用
- 25.信息熵:决策树构建的度量基础
- 26.信息增益与决策树的构建
- 27.ID3算法
- 28.决策树节点的划分
- 29.CART树
- 30.Bagging
- 31.Boosting:自适应提升算法
- 32.Gradient Boosting
- 33.线性模型:回归与分类的同一副骨架
- 34.最小二乘:把最优参数直接解出来
- 35.最小二乘法的几何与概率视角
- 36.极大似然:给最小二乘一个概率出身
- 37.逻辑回归(1):把线性模型接到分类上
- 38.逻辑回归(2):损失函数与多分类
- 39.---- 手造数据: 5 个样本, 2 个特征; x2 几乎 = 2*x1 (偏差手工指定, 无随机) ----
- 40.讲师例: 向量 "23014" -- 三把范数尺子量同一个向量
- 41.优化方法(1):坐标下降与前向选择
- 42.优化方法(2):前向策略与最小角回归
- 43.拉格朗日乘子法(1):从无约束到等式约束
- 44.拉格朗日乘子法(2):等式约束下的平行条件与拉格朗日函数
- 45.拉格朗日乘子法(3):不等式约束与 KKT 条件
- 46.最大间隔:把分类问题变成优化问题
- 47.拉格朗日对偶问题:把 w 和 b 全部换成 α
- 48.支持向量:边界上的少数派
- 49.SVM 计算过程:一笔到底算完,再走进核方法
- 50.贝叶斯公式:概率即信念
- 51.隐含变量和概率模型:给看不见的原因建模
- 52.EM 算法:期望与最大化的迭代
- 53.琴生不等式:EM 的数学钥匙
- 54.三个硬币问题:EM 的复盘与衔接
- 55.高斯混合模型:用隐变量解释双峰数据
- 56.隐马尔可夫模型
- 57.隐马尔可夫模型(2):前向算法与动态混合模型
- 58.隐马尔可夫模型在 NLP 中的运用
- 59.EM 算法:把「为什么收敛」推到底
- 60.HMM 在 NLP 中的运用:从分词到机器翻译
- 61.HMM 参数估计:Baum-Welch 的三块拉格朗日
- 62.高维下的高斯模型
- 63.主题模型:从词袋到隐含主题
- 64.Dirichlet 分布(1)
- 65.Dirichlet 分布(2)
- 66.统计学习内容回顾
- 67.K-means 算法
- 68.PCA 降维
- 69.SVD 降维
- 70.从聚类到流形
- 71.局部线性嵌入
- 72.随机邻域嵌入:SNE 与 t-SNE
- 73.无参模型:让数据自己说话
- 74.高斯过程:函数空间里的正态分布
- 75.高斯过程(2):噪声、正则化与核的线性组合
- 76.神经网络简介:从感知器到自编码器
- 77.反向传播:把误差一层层传回去
- 78.卷积神经网络(CNN):让特征自己学出来
- 79.循环神经网络(RNN):给网络装上记忆
- 80.深度学习回顾:静态一类,动态一类
- 81.增强学习(1):蒙特卡洛、树搜索与 Q-learning
- 82.强化学习(2):从乌鸦吃核桃到井字棋的 value 回传
决策树节点的划分
约 34 分钟
小象实战讲义 · 机器学习算法精讲
上一节我们用信息熵与信息增益搭起了 ID3。但真实数据比 Play-Tennis 复杂得多:特征有名义型、有序型、连续型之分,决策树要能”对症下刀”。本节讲清楚三件事:不同类型的特征怎么分叉(连续特征必须先离散化)、信息熵之外还有哪些划分度量(基尼指数、误分类误差,以及”没有免费午餐”),以及树该长到什么时候停——为下一章的集成学习埋下伏笔。
💡 核心导读
- 特征三类型:名义型(nominal)直接多路分叉;有序型(ordinal)分叉时要尊重顺序;连续型(continuous)必须先离散化。
- 离散化三法:等宽分箱、等频(按分布)分箱、基于信息增益/基尼的动态找切点——第三种是决策树的正牌做法。
- 基尼指数与基尼分裂:CART 决策树的度量,不用算 log、手算即可;与信息增益方向相反——基尼分裂越小越好。
- 三种度量怎么选:熵、基尼、误分类误差形状相似、目标一致,但没有免费午餐——不同数据各有胜场。
- 树何时停:训练误差随树的长大一直降,测试误差却先降后升——停早了欠学、停晚了过拟合。
特征的三种类型
决策树的核心是”按特征值分叉”,但特征本身形态各异,直接决定这一刀怎么砍。
- 名义型(nominal / discrete):完全离散且无序。比如婚姻状况(未婚、已婚、离异)、是否退税(Yes/No)。有多少个取值就分多少叉,最省事。
- 有序型(ordinal):离散但内部有顺序。比如年龄(baby → kid → young → middle age → old)、尺寸(small/medium/large)。既可以多路分叉,也可以把相邻取值合并成组做二分——但合并要尊重顺序(后面细说)。
- 连续型(continuous / numerical):取值为实数,比如身高、体重、应税年收入。取值无穷多,没法一个值一叉,必须先离散化。
一个小技巧:当离散取值特别多时,可以先把若干标签聚成大类再分——比如把 baby、kid、young 先并成”年轻”一档,与”中年""老年”三分;若第一刀分得不够好,再往下细拆。先粗后细,省计算。
连续特征离散化:三种砍刀法
离散化(discretization)就是把连续量变成离散区间,是决策树处理连续特征的必经之路。讲师画了张二维图:在 轴上取一个值 把平面切成左右两半,再在 轴上取 、 继续切——两次切割就把平面分成了四个矩形区域,每块区域对应树的一片叶子。方法上有三种:
- 等宽分箱:把整个值域三等分(或 k 等分),完全不关心数据怎么分布。简单,但可能有的箱挤满数据、有的箱空着。
- 等频分箱(按分布):让每个区间里落进去的数据个数一样多。数据密的区间窄、数据疏的区间宽——比如从 60 千美元到 220 千美元的收入数据,低段密集、高段稀疏,等频切出来的边界会明显偏向低段。
- 动态找切点:遍历相邻数据点之间的候选切点,选信息增益最大(或加权基尼最小)的那一个——这是决策树构建时的正牌做法。
用课件里的 10 个应税年收入(千美元)演示前两种:
# 讲师课件中的 10 个应税年收入(千美元)
incomes = [60, 70, 75, 85, 90, 95, 100, 120, 125, 220]
def equal_width_binning(data, k):
"""等宽分箱:把值域均分 k 段,返回 k-1 个边界"""
lo, hi = min(data), max(data)
width = (hi - lo) / k
return [round(lo + i * width, 2) for i in range(1, k)]
def equal_frequency_binning(data, k):
"""等频分箱:让每段落入大致相同数量的数据,返回边界"""
s = sorted(data)
n = len(data)
boundaries = []
for i in range(1, k):
idx = -(-i * n // k) - 1 # 即 ceil(i*n/k)-1
if idx + 1 < n:
boundaries.append(round((s[idx] + s[idx + 1]) / 2, 2))
return boundaries
print("收入数据:", incomes)
print("等宽分箱(k=3)边界:", equal_width_binning(incomes, 3))
print("等频分箱(k=3)边界:", equal_frequency_binning(incomes, 3))收入数据: [60, 70, 75, 85, 90, 95, 100, 120, 125, 220]
等宽分箱(k=3)边界: [113.33, 166.67]
等频分箱(k=3)边界: [87.5, 110.0]可以看到:等宽的边界 113.33/166.67 只是把 60~220 的值域机械三等分;等频的边界 87.5/110.0 则把三段切成大约 4/3/3 个数据——密集的低收入段切得更细。
顺带回答一个课堂问题:候选切点为什么总取”正中间”?比如 75 和 85 之间,为什么切 80 而不是 81?原则上两者之间任意一点效果都一样(都没有数据落在切点附近),我们只是取两个相邻数据点的中点作为代表,既均匀又不遗漏。
基尼指数:不用算 log 的不纯度度量
最早的决策树算法之一 CART(Classification and Regression Trees)用的度量叫基尼指数。名字来源于经济学里衡量收入不均等的基尼指数——两者不是一回事,但都在某种层面上刻画概率分布的”混杂”程度。
对一个节点 ,基尼指数定义为:
其中 是节点 中类别 的相对频率。讲师用三个口算例子讲透它:
- 节点里 6 个样本全是一类(如全红叉):,——最纯;
- 3 叉 3 圈对半混合:——二分类时最混,基尼取最大值 0.5;
- 5 比 1(如 5 叉 1 圈):。由对称性,1 比 5 的结果完全一样。
和熵一样,基尼也是”纯则 0、混则大”,最大值在各类均分时取到。最大的好处是不用算 log——手算平方和即可,工程上计算也更省。
基尼分裂与最佳切点
有了节点的不纯度,怎么评价一次切分的好坏?仿照信息增益的结构,定义基尼分裂(Gini Split):父节点切成 个子节点后,各子节点基尼的加权和:
其中 是落进第 个子节点的数据量, 是总量。它与信息增益”同出一炉”,但方向相反:信息增益挑最大,基尼分裂挑最小。
用课件的逃税(Cheat)例子走一遍完整流程。10 个人的应税年收入与是否逃税——逃税(Yes)的恰好是 85K、90K、95K 三个人:
| 收入(千) | 60 | 70 | 75 | 85 | 90 | 95 | 100 | 120 | 125 | 220 |
|---|---|---|---|---|---|---|---|---|---|---|
| 逃税 | No | No | No | Yes | Yes | Yes | No | No | No | No |
把 10 个收入排序后取相邻中点作为候选切点,逐一算加权基尼:
def gini_index(class_counts):
"""基尼指数:Gini = 1 - Σ (p_j)^2,输入为各类别计数"""
total = sum(class_counts)
if total == 0:
return 0.0
return 1 - sum((c / total) ** 2 for c in class_counts)
def split_weighted_gini(data, cut):
"""以 cut 切分后,左右子节点基尼的加权和"""
left = [lab for inc, lab in data if inc <= cut]
right = [lab for inc, lab in data if inc > cut]
n = len(data)
w_l, w_r = len(left) / n, len(right) / n
return (w_l * gini_index([left.count('No'), left.count('Yes')])
+ w_r * gini_index([right.count('No'), right.count('Yes')]))
# 课件 Cheat 表:收入(千美元)与是否逃税
data = [(60, 'No'), (70, 'No'), (75, 'No'), (85, 'Yes'), (90, 'Yes'),
(95, 'Yes'), (100, 'No'), (120, 'No'), (125, 'No'), (220, 'No')]
vals = sorted(set(v for v, _ in data))
candidates = [(vals[i] + vals[i+1]) / 2 for i in range(len(vals) - 1)]
print("候选切点与加权基尼指数:")
best_cut, best_gini = None, 1.0
for cut in candidates:
g = split_weighted_gini(data, cut)
print(f" 切点 {cut:>6}: 加权基尼 = {g:.3f}")
if g < best_gini:
best_cut, best_gini = cut, g
print(f"最优切点: {best_cut},加权基尼 = {best_gini:.3f}")候选切点与加权基尼指数:
切点 65.0: 加权基尼 = 0.400
切点 72.5: 加权基尼 = 0.375
切点 80.0: 加权基尼 = 0.343
切点 87.5: 加权基尼 = 0.417
切点 92.5: 加权基尼 = 0.400
切点 97.5: 加权基尼 = 0.300
切点 110.0: 加权基尼 = 0.343
切点 122.5: 加权基尼 = 0.375
切点 172.5: 加权基尼 = 0.400
最优切点: 97.5,加权基尼 = 0.300对照讲师在课上的现场口算:他先试了在 80 处切(75 与 85 的正中)——左边 3 个数据全是 No(基尼 0),权重 3/10;右边 7 个(3 Yes 4 No)基尼约 0.49——加权后约 0.343;又试了 87.5,得到约 0.417;把候选切点都算完后,最小的是 0.3——与代码扫出的 97.5 处 0.300 完全一致。在 97.5 切一刀,右边剩下 100/120/125/220 四个纯 No,左边把三个逃税者与其他人隔到一起继续细分。
误分类误差与三种度量的选择
最直观的第三种度量是误分类误差(misclassification error):按多数类预测时的错误率。
- 全一类(0 与 1):,误差 0;
- 3 叉 3 圈:,误差 1/2。
三种度量(熵、基尼、误分类误差)画在同一个坐标系里形状神似:纯时为 0,对半混合时最大,且左右对称。讲师的评价很干脆:误分类误差最好算;熵理论最完整;基尼介于两者之间,不用 log、手算即可。
那到底哪个更好?——没有免费午餐(No Free Lunch)。机器学习里不存在一个处处最优的度量:有的数据用熵好,有的数据基尼好,误分类误差也可能在某个问题上表现不错。三种度量在多数问题上差别其实不大,选哪个要看数据和情况,不能迷信某一种。ID3 用熵,CART 用基尼——它们都长成了经典。
树该长到什么时候停?
最后是决策树绕不开的问题:什么时候停止分叉?
- 停得太早:树没学好数据的规律,欠学;
- 停得太晚:树长得过大、分支过多,过拟合。
把”节点数”看作模型复杂度:随着树不断长大,训练误差一路下降——极端情况是每个叶子只剩一个样本,训练误差归零;但测试误差先降后升——降到某个节点数时达到最低,继续长大反而掉头向上,这就是过拟合在曲线上的样子。
那测试误差与训练误差之间的这种背离,背后的原理是什么?讲师留了个大钩子:误差其实可以分解成不同类型——偏差(bias)与方差(variance),而且对付它的一个大杀器是把很多树集成在一起做判断(ensemble)。这正是下一章集成学习(CART/Bagging/Boosting)要展开的主线。
📝 动手练一练
- 概念辨析:有序特征(尺寸 small/medium/large)强制二分时,为什么”small 与 large 一组、medium 单独一组”通常不是首选?什么情况下它反而可能成立?
👉 点击查看参考答案
有序特征的取值之间有距离语义:small 和 large 是两个极端,把它们并成一组,组内共性往往只剩”不是中号”,子集纯度难看,信息增益低。首选保持顺序连贯的切法:{small} vs {medium, large} 或 {small, medium} vs {large}——组内样本更相似,也省一次无谓的组合尝试(每多试一种组合就多一份计算量)。
但”通常不是首选”不等于”永不成立”。讲师举的极端例子:特别年轻和特别老的人都需要尿布,中间的反而不需要——两端共享一个真实特性时,把两端并组就是对的。所以结论是经验而非铁律:先按顺序切,除非你有理由相信两端有共性。
- 代码实践:主示例里
split_weighted_gini的类别计数写死了 ‘No’/‘Yes’ 两个标签。若数据有 3 个类别(如 ‘No’/‘Yes’/‘Under-review’),函数还能用吗?改写为支持任意类别数的版本。
👉 点击查看参考答案
原实现按 ‘No’/‘Yes’ 计数,第三个类别会被漏掉,结果必然出错。改成动态统计任意标签(collections.defaultdict 是标准库成员):
from collections import defaultdict
def gini_index_counts(counts):
"""基尼指数:Gini = 1 - Σ (p_j)^2"""
total = sum(counts)
if total == 0:
return 0.0
return 1 - sum((c / total) ** 2 for c in counts)
def split_weighted_gini_multiclass(data, cut):
"""支持任意类别数的加权基尼"""
left = defaultdict(int)
right = defaultdict(int)
for value, lab in data:
if value <= cut:
left[lab] += 1
else:
right[lab] += 1
n = len(data)
return (sum(left.values()) / n * gini_index_counts(left.values())
+ sum(right.values()) / n * gini_index_counts(right.values()))
demo = [(60, 'No'), (70, 'No'), (75, 'Under-review'),
(85, 'Yes'), (90, 'Yes'), (95, 'Yes'),
(100, 'No'), (120, 'No'), (125, 'No'), (220, 'No')]
print(f"切点 80.0 的加权基尼 = {split_weighted_gini_multiclass(demo, 80.0):.3f}")切点 80.0 的加权基尼 = 0.476三个类别各自的计数被自动追踪,无需预先写死标签集合。
本章小结
- 特征分三类:名义型直接分叉、有序型按顺序组合二分(两端并组要谨慎,除非真有”尿布”式的共性)、连续型先离散化。
- 离散化三法:等宽、等频、按信息增益/基尼动态找切点;候选切点取相邻数据点的中点。
- 基尼指数 :纯则 0、混则大,免 log、好手算;基尼分裂(加权和)越小越好,与信息增益方向相反——课件逃税例的最优切点是 97.5,加权基尼 0.300。
- 误分类误差最简单,熵理论最完整,基尼居中;没有免费午餐——度量之间没有绝对赢家。
- 树的停止时机是一门平衡艺术:训练误差随树长大一路降,测试误差先降后升。误差可以分解为偏差与方差,多条树可以集成——这是下一章的主角。
📋 行动清单
- 用手头的连续数据分别做等宽、等频、基尼动态三种离散化,对比切出的边界差异
- 把主示例代码里的度量从基尼换成信息熵(去掉 log 的地方换成 ),重扫一遍切点,看最优切点是否还是 97.5
- 思考:为什么”训练误差归零”几乎从来不是好消息?(带着这个问题进入下一章:偏差、方差与集成学习)
—— 小象教研组
配套资料 · 直接下载
课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。