查看全课大纲(第 29 / 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 回传
CART树
约 31 分钟
小象实战讲义 · 机器学习算法精讲
上节课我们学习了决策树如何利用信息增益进行特征分割,解决分类问题。本节课,我们将深入探讨决策树的另一个强大分支——CART(Classification and Regression Tree),它不仅能做分类,还能做回归预测。我们将理解CART如何将输入空间分割成多个区域,并学习其用于回归和分类的核心算法与损失函数。
💡 核心导读
- CART是什么:了解CART(分类与回归树)的提出背景与核心思想,即通过树结构同时处理分类与回归任务。
- 从分类到回归:直观理解决策树如何从输出类别标签(如0/1)扩展到输出连续值(如均值),实现回归预测。
- 回归树的损失函数:掌握CART回归树用于寻找最优分割点的平方误差损失函数及其最小化过程。
- 算法流程与示例:通过一维和二维的具体例子,一步步拆解CART如何选择特征和分割点来构建树。
- 分类树的基尼指数:了解CART分类树使用基尼指数(Gini index)作为不纯度度量进行特征分割。
CART:分类与回归树
CART,全称Classification and Regression Tree,由统计学家Leo Breiman提出。它扩展了决策树的能力,使其不仅能处理输出为离散类别的分类问题,还能处理输出为连续值的回归问题。
决策树的本质是在输入空间(特征空间)中进行一系列垂直分割。例如,在二维空间中,我们可能先根据特征 是否小于等于 将空间分为左右两部分,然后在左半部分根据特征 是否小于等于 进一步分割,如此递归进行,最终将整个空间划分成若干个互不重叠的矩形区域。
对于分类问题,每个区域会被赋予一个类别标签(如C1或C2)。当一个新的数据点落入某个区域,就预测它属于该区域对应的类别。
对于回归问题,思路是类似的。每个区域不再对应一个类别,而是对应一个连续的预测值。这个值通常取为该区域内所有训练样本输出值 的均值。因此,CART回归树的预测函数是一个分段常数函数:在同一个区域内的所有点,预测值相同。
CART回归树算法
假设我们有一个训练数据集 ,其中 是一个 维特征向量, 是连续值输出。
CART回归树采用二叉递归分割。在每一步,我们需要选择一个特征 ()和一个分割点 ,将当前节点对应的数据区域 划分为两个子区域:
我们的目标是找到最优的 ,使得划分后两个子区域内的预测误差之和最小。对于回归问题,最常用的误差度量是平方误差。因此,我们最小化以下损失函数:
其中, 和 分别是区域 和 内的最优预测值。可以证明,当损失函数为平方误差时,区域 内的最优预测值 就是该区域内所有 的均值:
这里 是区域 中样本的数量。
因此,算法步骤可以简化为:
- 遍历所有特征 。
- 对每个特征 ,遍历所有可能的分割点 (通常取相邻样本值之间的中点,如代码里的 1.5、2.5、…、9.5)。
- 对于每一对 ,按规则划分区域,并计算两个区域的均值 。
- 计算当前划分下的总平方误差损失 。
- 选择使 最小的 作为当前节点的分割规则。
- 对得到的两个子区域 和 ,递归地重复步骤1-5,直到满足停止条件(如区域样本数过少、深度限制等)。
下面,我们通过一个极简的一维例子来演示这个过程。
import math
# 训练数据:一维特征x,连续目标值y
# 这里构造一个简单的线性关系 y ≈ x - 1,但算法并不知道这个关系
data = [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9)]
# 计算给定分割点s下的损失
def calculate_loss_for_split(s, data):
R1 = [y for x, y in data if x <= s]
R2 = [y for x, y in data if x > s]
if not R1 or not R2: # 如果有一个区域为空,返回一个很大的损失
return float('inf'), None, None
c1 = sum(R1) / len(R1)
c2 = sum(R2) / len(R2)
loss = sum((y - c1)**2 for y in R1) + sum((y - c2)**2 for y in R2)
return loss, c1, c2
# 可能的分割点(取相邻x值的中点)
possible_splits = [x + 0.5 for x in range(1, 10)]
print("分割点 s | 左区域均值 c1 | 右区域均值 c2 | 损失 L(s)")
print("-------------------------------------------------------")
results = []
for s in possible_splits:
loss, c1, c2 = calculate_loss_for_split(s, data)
results.append((s, c1, c2, loss))
print(f" {s:.1f} | {c1:.2f} | {c2:.2f} | {loss:5.2f} ")
# 找到最优分割点
best_split = min(results, key=lambda x: x[3])
print(f"\n最优分割点 s = {best_split[0]:.1f}")
print(f"此时左区域均值 c1 = {best_split[1]:.2f}, 右区域均值 c2 = {best_split[2]:.2f}")
print(f"最小损失 L(s) = {best_split[3]:.2f}")分割点 s | 左区域均值 c1 | 右区域均值 c2 | 损失 L(s)
-------------------------------------------------------
1.5 | 0.00 | 5.00 | 60.00
2.5 | 0.50 | 5.50 | 42.50
3.5 | 1.00 | 6.00 | 30.00
4.5 | 1.50 | 6.50 | 22.50
5.5 | 2.00 | 7.00 | 20.00
6.5 | 2.50 | 7.50 | 22.50
7.5 | 3.00 | 8.00 | 30.00
8.5 | 3.50 | 8.50 | 42.50
9.5 | 4.00 | 9.00 | 60.00
最优分割点 s = 5.5
此时左区域均值 c1 = 2.00, 右区域均值 c2 = 7.00
最小损失 L(s) = 20.00从结果可以看出,当分割点 时,总平方误差损失最小,为20.0。这意味着将数据在 处一分为二,左区域用均值2.0预测,右区域用均值7.0预测,是当前所有二分方案中最优的。讲师在课上还口头验算了两刀: 时左区域只有 、右区域均值是 5; 时左均值 1.5、右均值 6.5——与表中数字一致。这完成了第一次分割。接下来,我们可以对左区域()和右区域()分别递归地进行同样的分割过程,从而构建出一棵更深的树。
为什么每次只二分、不一次切三块五块?讲师的解释很直接:先切两大块,每一大块再各切两小块就成四块,四块再切就是八块——按 2 的幂次推进,同样能达到把区域切细的目的。所以每次只需要求”最优二分点”就够了。
多维时怎么选特征?把所有特征轮一遍:比如有 5 个特征,分别求出各自的最优分割损失,假设结果是 20、18、10、19、31——那就选第 3 个特征(损失最小 10)作为根节点的切割维度,正如决策树选根一样。之后在每个子区域里重新循环剩余特征继续找最优二分(讲师的演示里跳过了已用过的维度,只在其余特征里找切点;实践中二叉分割也允许在不同深度重复使用同一特征)。
CART分类树与基尼指数
对于分类问题,CART树通常使用基尼指数(Gini Index) 来衡量一个区域的不纯度(Impurity),并以此选择最优分割。基尼指数定义为:
其中, 是类别总数, 是区域中样本属于第 类的比例。基尼指数越小,表示区域的纯度越高(样本越属于同一类)。
对于二分类问题(类别为0和1),若正类比例为 ,则基尼指数为 。
在构建分类树时,对于每个候选分割 将区域 划分为 和 ,我们计算划分后的加权基尼指数:
其中 是 中样本总数, 分别是 的样本数。我们选择使 最小的 进行分割。
下面的代码演示了一个简化的CART分类树分割过程。
import random
random.seed(42)
# 生成简单的二维分类数据
def generate_classification_data():
# 类别0大致在左下,类别1大致在右上
data = []
# 类别0的样本
data.append(([1, 2], 0))
data.append(([2, 3], 0))
data.append(([6, 5], 0))
# 类别1的样本
data.append(([3, 1], 1))
data.append(([4, 4], 1))
data.append(([5, 2], 1))
data.append(([7, 1], 1))
data.append(([8, 3], 1))
return data
# 计算一个区域的基尼指数
def gini_index(region_labels):
if not region_labels:
return 0
total = len(region_labels)
# 统计各类别数量
from collections import Counter
label_counts = Counter(region_labels)
gini = 1.0
for count in label_counts.values():
prob = count / total
gini -= prob ** 2
return gini
# 寻找最优分割
def find_best_split_classification(data):
n_features = len(data[0][0])
best_gini = float('inf')
best_feature = None
best_split_value = None
best_left_pred = None
best_right_pred = None
# 遍历每个特征
for j in range(n_features):
# 收集该特征所有可能的分割点(取样本特征值的中点)
feature_values = sorted(list(set([x[j] for x, _ in data])))
split_candidates = [(feature_values[i] + feature_values[i+1]) / 2 for i in range(len(feature_values)-1)]
for s in split_candidates:
left_labels = []
right_labels = []
for x, label in data:
if x[j] <= s:
left_labels.append(label)
else:
right_labels.append(label)
# 计算加权基尼指数
n_left, n_right = len(left_labels), len(right_labels)
n_total = n_left + n_right
if n_left == 0 or n_right == 0:
continue
weighted_gini = (n_left / n_total) * gini_index(left_labels) + (n_right / n_total) * gini_index(right_labels)
if weighted_gini < best_gini:
best_gini = weighted_gini
best_feature = j
best_split_value = s
# 预测值为区域内的多数类
from collections import Counter
best_left_pred = Counter(left_labels).most_common(1)[0][0]
best_right_pred = Counter(right_labels).most_common(1)[0][0]
return best_feature, best_split_value, best_left_pred, best_right_pred, best_gini
# 主程序
data = generate_classification_data()
print("分类训练数据:")
for i, (x, label) in enumerate(data):
print(f" 样本{i}: x={x}, 类别={label}")
print("\n开始训练简化CART分类树...")
best_feature, best_split, left_pred, right_pred, min_gini = find_best_split_classification(data)
print(f"最优分割特征: 第{best_feature}个,分割点: {best_split:.2f}")
print(f"左区域预测类别: {left_pred}, 右区域预测类别: {right_pred}")
print(f"最小加权基尼指数: {min_gini:.4f}")
# 预测测试
print("\n预测测试:")
test_points = [[2.5, 2], [5.5, 3]]
for test_x in test_points:
if test_x[best_feature] <= best_split:
pred = left_pred
else:
pred = right_pred
print(f" 测试点 {test_x} 的预测类别为: {pred}")分类训练数据:
样本0: x=[1, 2], 类别=0
样本1: x=[2, 3], 类别=0
样本2: x=[6, 5], 类别=0
样本3: x=[3, 1], 类别=1
样本4: x=[4, 4], 类别=1
样本5: x=[5, 2], 类别=1
样本6: x=[7, 1], 类别=1
样本7: x=[8, 3], 类别=1
开始训练简化CART分类树...
最优分割特征: 第0个,分割点: 2.50
左区域预测类别: 0, 右区域预测类别: 1
最小加权基尼指数: 0.2083
预测测试:
测试点 [2.5, 2] 的预测类别为: 0
测试点 [5.5, 3] 的预测类别为: 1结果显示,算法选择在第一个特征(索引0)的 处进行分割,使得加权基尼指数最小(0.2083)。左区域()的样本都属于类别0,右区域()的样本大多属于类别1。这完成了分类树的第一次有效分割。
📝 动手练一练
概念辨析:在CART回归树的损失函数中,为什么区域 内的最优预测值 是 的均值?如果我们将损失函数从平方误差 改为绝对误差 ,那么 的最优值应该是什么?
👉 点击查看参考答案
当损失函数为平方误差 时,令其关于 的导数为零:
解得 ,即 ,为均值。
如果损失函数改为绝对误差 ,则最优的 是 的中位数。因为中位数是使绝对误差和最小的点。你可以尝试修改上面回归示例的代码,将平方误差计算改为绝对误差,观察最优分割点和预测值的变化。代码实践:在上面回归树的示例代码中,我们只评估了特征 的分割。请扩展代码,模拟一个二维特征 的数据集,并实现完整的 遍历搜索,找到全局最优的第一个分割特征和分割点。
👉 点击查看参考答案
关键步骤是构造二维数据,并循环遍历两个特征。以下是核心修改思路: ```text # 假设二维数据 data_2d = [([x1, x2], y), ...](伪代码示意) best_loss = float('inf') best_j = None best_s = None for j in range(2): # 遍历两个特征 # 提取第j个特征的所有值并排序,生成候选分割点 feat_vals = sorted(list(set([x[j] for x, _ in data_2d]))) split_candidates = [(feat_vals[i]+feat_vals[i+1])/2 for i in range(len(feat_vals)-1)] for s in split_candidates: # 划分区域并计算损失 loss = compute_loss_for_2d_split(data_2d, j, s) if loss < best_loss: best_loss, best_j, best_s = loss, j, s ``` 你需要实现 `compute_loss_for_2d_split` 函数,根据特征 `j` 和分割点 `s` 划分数据并计算平方误差损失。通过这个练习,你能更深刻理解CART如何在高维空间中选择分割维度。
本章小结
本节课我们深入学习了CART树,它把分类与回归统一到了同一个决策树框架下。核心在于理解其通过递归二叉分割来划分特征空间,并针对不同任务(回归/分类)采用不同的损失度量(平方误差/基尼指数)来指导分割点的选择。
📋 行动清单
- 理解分割过程:在纸上画出一个二维特征空间和若干样本点,手动模拟一次CART回归树的分割过程,计算不同分割点下的平方误差损失。
- 运行示例代码:将讲义中的代码复制到本地Python环境运行,并尝试修改数据点,观察最优分割点如何变化。
- 对比ID3:回顾上章的 ID3 决策树(信息增益、多路分叉、只做分类),思考它与 CART(基尼/平方误差、二叉分叉、能做回归)在分割准则与应用场景上有何异同。
—— 小象教研组
配套资料 · 直接下载
课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。