🧮 第2章 有监督学习
📄 一句话概括

随机森林 = 很多棵决策树 + 两处随机 + 投票。 单棵树会一路切到底、把噪声也背下来(实测训练 0.9929、测试只有 0.7917); 八十棵树各看一份重采样的训练集、每次只准在一个随机特征上切,投票之后测试升到 0.8417。 代价是模型变成一片森林,再也画不成一张图。

📝 本节和前面几节的关系

前面六个算法都是「一个模型」。 这一节引入一个新思路:把很多不完美的模型凑起来。 它属于「集成学习」, 是表格数据上最常用的方法之一。 想系统看集成学习的其他分支(AdaBoost、GBDT), 可以去 40 课 第 9 章。

一、决策树:一路问问题,直到分出结论

决策树解决问题的办法很像医生问诊:先问一个最能把人分开的问题,根据回答分成两拨, 再对每一拨问下一个问题,直到能下结论。 在数据上,「问题」就是「某个特征是不是小于某个值」。每问一次就把数据切成两半,这叫一次切分。 怎么选问题?教材用的是基尼不纯度。 它衡量一批标签有多杂:两类各占一半时最大(0.5),全是一类时最小(0)。 每次切分都选「切完不纯度降得最多」的那个特征和那个阈值。 这个选法有个特点:它贪心。 每一步只管当下降得最多,不管后面会不会更好。 所以决策树容易切出一个「局部最优但整体别扭」的结构。这是它不稳定、方差大的根源。 树还有个好处是别的算法比不了的: 它的结果能画成一张图给人看。 「x2 小于 1.2 且 x1 大于 0.3 判为 A 类」—— 这种规则业务方一看就懂,这也是它在风控、医疗里长期占一席之地的原因。

二、树为什么会背答案

如果不限制深度,树会一直切下去,直到每个叶子节点里只剩一个样本。 这时训练集准确率必然是 1.0——每个点都有自己专属的一条路径。 但这条路径只对那个点有效。新来的点稍微挪一点,就走到别的叶子里去了。 这就是过拟合:模型记住了样本,没学到规律。 Demo 里量得很清楚:深度从 1 加到 16,训练准确率从 0.8250 爬到 0.9929, 测试却从 0.7833 掉到 0.7917,中间最高只到 0.8083。训练涨了 17 个点,测试只涨了不到 1 个点。 为什么涨不上去?因为数据里有 12% 的标签是故意翻错的。 这部分错误无法被任何模型学到,只能被平均掉。 而单棵树不会平均,它会把每条错误都当真。 常规的解法是剪枝:限制最大深度、限制叶子最少样本数。但剪枝很考验经验——剪多了欠拟合,剪少了没用。 随机森林给了另一条路:不剪,而是种很多棵,让它们互相抵消。

三、Bagging:让很多棵树投票

思路很朴素:如果一棵树容易犯错,那就种一百棵,让它们投票。 前提是这些树的错误不能一样——如果一百棵树错得一模一样,投票等于没投。 怎么让它们不一样? 每棵树看一份不同的训练集。 具体做法是有放回地抽样(bootstrap):从 280 个样本里随机抽 280 次, 有些样本被抽中多次,有些一次没抽中。每棵树的训练集都不一样。 这个做法叫 Bagging(Bootstrap Aggregating)。 它降低的是方差:单棵树的预测随训练集波动很大,平均之后波动互相抵消,整体就稳了。 注意它降不了偏差。如果所有树都系统性地偏(比如真实边界是曲线,而树只学到直线),投票也救不回来。 Bagging 治的是「不稳定」,不是「不准」。

四、随机森林:再加一处随机,让树更不像

光靠重采样还不够。 如果数据里有一个特别强的特征,每棵树都会优先在它上面切,结果一百棵树长得差不多,投票的意义就小了。 随机森林加了第二处随机: 每次切分时,只准在随机挑的一小部分特征里找切点。 分类问题通常取「特征数的平方根」, 回归取「特征数的三分之一」。 Demo 里只有两个特征, 所以每次只随机挑 1 个——这等于强制每棵树一半的切分用 x1、一半用 x2,树与树之间差异被拉到最大。 两处随机叠加,效果是树的预测之间相关性变低。 数学上可以证明: 集成的误差上限取决于单棵树的误差和树之间的相关性,相关性越低,上限越低。 这就是随机森林比单纯 Bagging 更好的原因。 实测:单棵 0.8083,五棵 0.8250,二十棵 0.8167,八十棵 0.8417。 中间那一下回落是随机波动——树的棵数不是越多越好,但方向是对的。

五、特征重要性:森林顺手送的礼物

森林没法画成一张图,但它能回答另一个问题:哪个特征最要紧? 做法是统计每个特征在所有树的所有切分里,一共贡献了多少不纯度下降。 贡献多的排前面,就是「重要特征」。 Demo 里深度 4 的树第一刀切在 x2 上,切点 −0.8904。 这题的生成规则是「x2 和 sin(1.6 x1) 比大小」—— x2 直接参与比较,当然更值得先切。树自己发现了这一点。 这个指标有两个坑要说清。 第一,它偏向「取值多的特征」:连续特征有很多可切的点,随便切都能降一点不纯度; 而只有两个取值的特征一共就一个切点。所以连续特征会显得更重要。 第二,共线的特征会互相分票。两个高度相关的特征,重要性会各分一半,看起来都不重要。 所以看重要性要成组看,不要只看单个排名。

六、动手 Demo:一棵树会背答案,一百棵树投票就不会

400 个点撒在 3 乘 3 的方格里,真实边界是曲线 x2 = sin(1.6 x1)。 然后把 12% 的标签故意翻错——这一步是关键,没有噪声的话,单棵树也能做到接近满分, 就看不出森林的价值。 训练 280 个、测试 120 个。决策树是手写的,只有七十行:基尼不纯度、扫所有切点、递归建树。

import numpy as np

def gini(y):                                  # 不纯度:两类各半时最大
    p = float(y.mean())
    return 2.0 * p * (1.0 - p)

def best_split(X, y):                         # 扫所有特征、所有切点
    base = gini(y)
    best = (None, 0.0)
    for j in range(X.shape[1]):
        for t in np.unique(X[:, j]):
            m = X[:, j] <= t
            if m.sum() == 0 or (~m).sum() == 0:
                continue
            g = base - (m.sum() * gini(y[m]) + (~m).sum() * gini(y[~m])) / len(y)
            if g > best[1]:
                best = ((j, float(t)), g)
    return best

# 森林:每次重采样训练集、每次只准看一个随机特征
for k in range(80):
    idx = rg.choice(len(Xtr), len(Xtr), replace=True)
    t = Tree(max_depth=16, n_feat=1).fit(Xtr[idx], ytr[idx])
    votes += t.predict(Xte)

实跑输出:

--------------------------------------------------------------------
随机森林:一棵树会背答案,一百棵树投票就不会
--------------------------------------------------------------------

  400 个点撒在 3 乘 3 的方格里,真实边界是曲线 x2 = sin(1.6 x1)。然后
  **故意把 12% 的标签翻错**——真实数据里总有标错的、模棱两可的。训练 280 个、测试 120 个。

  【一】单棵树:越深越会背
  最大深度    实际深度    训练准确率    测试准确率
  --------------------------------------------------
  1           2           0.8250        0.7833
  2           3           0.8286        0.7750
  4           5           0.8893        0.7833
  8           9           0.9786        0.8083
  16          13          0.9929        0.7917
  深度 1 是一条横线(只能按一个特征切一刀),欠拟合,训练和测试都只有 0.8 出头。
  深度放开之后,训练准确率一路爬到 0.9929,
  **测试却在 0.79 附近打转,深到 16 时还掉回 0.7917。**
   训练几乎满分、测试反而降,这就是过拟合的样子。树会一路切下去,
  直到把每一个翻错的标签都单独隔出来——**它记的是噪音,不是规律。**

  【二】森林:让一百棵树吵架,然后投票
  树的棵数    测试准确率
  ------------------------
  1           0.8083
  5           0.8250
  20          0.8167
  80          0.8417
  每棵树都只看到**一份重采样出来的训练集**(bootstrap),每次切分只准在**随机挑的一个特征**
  上找切点。这两处「随机」让每棵树的错法不一样,投票之后错误互相抵消。单棵 0.8083,
  凑到 80 棵是 0.8417,涨了三个多百分点。
  **别小看这三个点:测试集里本来就有 12% 的标签是错的,天花板大概在 0.88,**
  **森林已经顶到很近了。**这就是集成学习最朴素的经验:**多个不完美的判断,比一个好。**

  【三】代价:收益递减,而且再也画不出图
  表里 20 棵到 80 棵的涨幅明显变小了——**森林的收益是递减的。**
   到某个数量之后,再加树的边际收益接近零,只是白烧算力。另一个代价是模型变成一整片森林,
  **再也没法画成一张图给人看**。森林换来的是准确率,牺牲的是可解释性。

  【四】特征重要性:哪个特征在干活
  看深度 4 那棵树的第一刀切在哪个特征上:**x2**,
  切点是 -0.8904。这题的规则是「x2 和 sin(1.6 x1) 比大小」,x2 直接参与比较,
  所以它比 x1 更值得先切——树自己发现了这一点。
  这就是「特征重要性」的来源:**统计每个特征在所有树里贡献了多少不纯度下降。**
  它不用人来指定权重,是从切分过程中自然长出来的——这也是随机森林在业务里受欢迎的原因之一,
  报告里能说清「哪个变量最要紧」。

单棵树那张表要看两列的走势。训练从 0.8250 一路爬到 0.9929,测试从 0.7833 波动到 0.7917, 中间最好的时候才 0.8083。训练涨了 17 个点,测试涨了不到 1 个点——这就是过拟合的量化样子。 森林那四行:1 棵 0.8083,5 棵 0.8250,20 棵 0.8167,80 棵 0.8417。 20 棵那一下回落不是算法的问题,是随机波动——换了随机种子,20 棵也可能比 5 棵高。 为什么只涨三个多点?因为天花板就在那儿。测试集里本来有 12% 的标签是错的, 任何模型的上限大概 0.88。森林从 0.8083 顶到 0.8417,已经吃掉一大半能吃的空间。 「两处随机」值得单独说。重采样让每棵树看的样本不同,随机特征让每棵树切的维度不同。 Demo 里只有两个特征,所以 n_feat=1 等于强制一半的树先看 x1、一半先看 x2——差异被拉到了最大。 最后那段特征重要性不是算出来的,是「看树长什么样」看出来的。第一刀切 x2 而不是 x1, 是因为 x2 直接参与比较。真实的随机森林要统计所有树的所有切分,但道理就是这个道理。

30 秒实验

  1. 把标签翻错的比例从 12% 改成 0,单棵树的测试准确率会到多少?森林呢?
  2. 把 n_feat 从 1 改成 2(每棵树都能看全部特征),森林的准确率会变高还是变低?
  3. 把树的数量加到 300 棵,准确率还能涨吗?为什么?

常见误区

  1. 以为「树越多越准」。收益是递减的,而且树之间相关性高时(比如特征很少、 n_feat 设成全部)加树几乎没用。先保证树的差异性,再谈数量。
  2. 用随机森林做外推。树的预测永远是训练集里出现过的标签值的平均, 它不可能预测出训练集里没有的数。线性回归至少还敢往外 extrapolate,树连试都不试。
  3. 把特征重要性当因果。它只说「这个特征在切分时有用」,不说明「改这个特征会改变结果」。 相关不等于因果,这一点在业务解释里尤其要小心。
自测三问(点开对答案) 1. Bagging 降的是偏差还是方差?为什么? —— 降方差。它把很多棵大致无偏但各自抖动的树平均起来,抖动互相抵消。 但如果所有树都系统性地偏,平均也救不回来——所以它降不了偏差。 2. 随机森林的两处随机分别是什么?各自有什么用? —— 一是对训练集有放回重采样(bootstrap),让每棵树看不同的样本; 二是每次切分只准看随机的一小部分特征,让每棵树在不同维度上切。 两者都是为了让树的预测之间「不那么像」,降低相关性。 3. 为什么单棵树的训练准确率能到 0.99,测试却只有 0.79? —— 因为树一路切到底,把每个样本(包括标错的)都单独隔离出来了。 训练集里那 12% 的错误标签它全记住了,而这些错误对新数据毫无帮助。

随机森林是「很多简单模型凑起来」。 2.8 节要讲另一种「凑」——把简单的加权和一层层摞起来,这就是神经网络。

相关笔记