第七周复现了《机器学习实战》第 9 章的树回归源码,包括数据二元切分、回归树、预剪枝、后剪枝、模型树、预测函数和 Tkinter 交互界面。代码主线仍然是递归建树,但叶节点不再保存类别,而是保存一个平均值或一条线性模型。

这篇复盘会讲清楚

  • 回归树与分类树的叶节点有什么不同
  • tolS、tolN 怎样控制树的复杂度
  • 预剪枝和后剪枝分别在什么时候发生
  • 为什么模型树的拟合曲线比回归树平滑

一、把“选择类别”改成“预测数值”

第 3 章的分类树会连续提出问题,最后在叶节点给出“是”或“否”等类别。回归树的结构相似,只是叶节点换成连续数值。书中最简单的叶节点直接使用所在区域目标值的平均数:

def regLeaf(dataSet):
    return float(np.mean(dataSet[:, -1]))

def regErr(dataSet):
    return float(np.var(dataSet[:, -1]) * np.shape(dataSet)[0])

方差 × 样本数正好对应目标值相对均值的残差平方和。建树时要寻找能够让左右两部分总误差下降最多的特征和阈值。

像先按照某条规则把顾客分组,再用每组的平均消费金额作为组内所有人的预测。

二、一次切分到底做了什么

binSplitDataSet把数据分成“大于阈值”和“小于等于阈值”两部分:

def binSplitDataSet(dataSet, feature, value):
    matrix0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
    matrix1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
    return matrix0, matrix1

ex00.txt 的 200 个样本上,按第 0 列和 0.5 切分后,左右分别有 116 和 84 个样本。最终简单回归树为:

根节点:第0个特征 > 0.48813
左叶节点:1.0180968
右叶节点:-0.0446503

新样本大于 0.48813 就走左边,得到约 1.0181;否则走右边,得到约 -0.0447。

三、为什么回归树的曲线是阶梯

普通回归树的每个叶节点只返回一个常数。同一区域内,无论输入发生多小的变化,预测值都保持不变;跨过某个切分阈值时,预测值才突然跳到另一个叶节点的常数。

回归树的阶梯拟合曲线与模型树的分段线性拟合曲线
图 1:左边的回归树在每个区域输出常数,所以呈阶梯状;右边的模型树在叶节点内继续使用线性模型。

阶梯状不是绘图错误,而是回归树叶节点定义决定的。树越复杂,台阶越多,也越可能追随训练数据中的噪声。

四、chooseBestSplit 的三个停止条件

递归不能无限继续。书中的 chooseBestSplit 有三个主要停止条件:

  1. 当前区域内所有目标值都相同,不需要再切分。
  2. 最佳切分带来的误差下降小于 tolS
  3. 切分后任意一个子集的样本数小于 tolN

tolS像“这次分组至少要带来多少收益”,tolN像“每组至少要有多少人”。前者太小、后者太小时,树会不断细分;限制太强时,树又可能没有足够表达能力。

五、预剪枝参数对树大小的影响

参数 ops含义节点数量
(1, 4)书中默认限制83
(0, 1)几乎不限制切分399
(10000, 4)要求很大的误差下降3

这叫预剪枝:树在生长过程中就根据规则提前停止。它速度快,但参数如果设得过严,可能把原本有用的分支也阻止掉。

六、后剪枝:先长出来,再决定是否合并

后剪枝先使用较宽松的参数生成复杂树,然后使用独立测试数据,从树的底部开始判断两个叶节点是否应该合并:

不合并误差 = 左侧误差 + 右侧误差

合并误差 = 所有测试样本相对两个叶节点平均值的误差

只有合并误差更小时才合并。本次在 ex2test.txt 上剪枝后,节点数由 399 减少到 281。

七、模型树:叶节点里不再只有一个数字

模型树的叶节点保存一条线性模型。进入某个叶节点后,不是直接返回平均值,而是继续计算:

ŷ = w₀ + w₁x₁ + ... + wₙxₙ

exp2.txt 上生成的模型树只有 3 个节点,两个叶节点分别为:

左叶节点:[0.001699, 11.964774]
右叶节点:[3.468779, 1.185217]

每个叶节点的第一项是截距,第二项是一次项系数。因为区域内部仍能随输入变化,所以图中的模型树曲线是分段直线,而不是水平台阶。

八、三种回归方法的实际比较

书中使用自行车速度与智商数据比较回归树、模型树和标准线性回归。训练集和测试集各有 200 条数据,预测值与真实值的相关系数为:

方法测试相关系数
回归树0.964085
模型树0.976041
标准线性回归0.943468

这一次模型树最高,说明分段线性关系比较适合这组数据。但相关系数只描述共同变化程度,不等于所有样本的预测误差都最小,也不能据此断言模型树在任何数据上都更好。

九、作者 Notebook 中的一处笔误

作者 Notebook 计算标准线性回归预测时写成了:

yHat[i] = testMat[i, 0] + ws[0, 0]

这行只把输入与截距相加,漏掉了斜率。根据前面求出的权重,正确的线性模型应为:

yHat[i] = ws[0, 0] + testMat[i, 0] * ws[1, 0]

本次按照后一种方式计算,并在案例代码中用中文注明。核心树回归函数仍按作者源码的结构复现。

十、新版环境中的兼容处理

这些地方都在源码中有中文注释,算法的切分规则、字典结构和递归流程没有改成其他实现。

十一、目前还没有真正弄懂的问题

  1. 三个停止条件的配合。单独看懂了每个判断,但面对较深递归时还不容易判断最终会在哪里停止。
  2. 后剪枝的返回过程。知道它从底部尝试合并,但复杂树中每次返回到哪一层仍容易看乱。
  3. 参数选择。现在只比较了节点数量,还没有用验证集系统选择 tolS 和 tolN。
  4. 评价指标。相关系数方便比较趋势,但还需要同时计算测试 RSS 或 RMSE。

十二、下周计划

  1. 01
    手画一遍递归树

    记录每次切分的数据范围、误差变化和停止原因。

  2. 02
    比较剪枝前后测试误差

    除节点数量外,再计算测试 RSS,确认合并确实有价值。

  3. 03
    补做上周遗留实验

    继续用独立测试数据比较局部加权回归的不同 k 值。

  4. 04
    进入第 10 章 K 均值聚类

    学习距离计算、质心更新和误差平方和。

十三、第七周最值得保留的认识

切分递归台阶剪枝局部模型

回归树通过不断切分递归建立区域,每个叶节点输出常数,因此形成台阶;为了避免树追随噪声,需要预剪枝或后剪枝;模型树再把叶节点升级成局部模型,让每个区域内部继续保持线性变化。

如果只用一句话总结第七周,那就是:树模型不是只会做分类,它也可以把复杂的回归关系拆成多个容易处理的小区域。