🤖 第7章 基于最小二乘法的分类
📄 一句话概括

三类、二维、120 个点,一对多最小二乘: 准确率 90.0%,混淆最深的一对是「第 3 类被判成第 1 类」4 个,反过来只有 1 个。 把第 3 类从 40 个减到 8 个,总体准确率还有 76.7%,而第 3 类的召回率从 87.5% 崩到 37.5%。

📝 本节和前面几节的关系

7.1 和 7.2 都在讲两类。多类的做法很直接:每一类单独解一个回归,谁输出大就判给谁。 这叫一对多,也是最容易实现的多类方案。 它带来的新问题是「不对称」:三个回归的尺度不一样,输出值就不能直接横向比较。 这一节把这个偏置测出来,也顺手把「准确率会骗人」这件事测一遍。

一、一对多:三个回归各管一类

做法三步:把标签做成 one-hot 矩阵(每一行只有一个 1); 对 (X, Y) 解一个多输出的最小二乘,得到一个系数矩阵;对每个新样本算三个输出,取最大的那一类。 系数矩阵的尺寸是(特征数加 1)乘类别数。 Demo 里是 3 乘 3 等于 9 个数——三组系数,每组三个分量(两个特征加一个截距)。 所以「多类」在这里不是一次解完,而是三个独立的两类问题拼在一起。 这个「拼」有个直接后果:三个回归互不知道对方的存在。 某一类的输出可能整体偏大(因为它的系数范数大),于是它天然更容易赢。 这不是「谁像谁」的问题,是「谁的尺度大」的问题。 改进的方向有两条:一是把输出归一化(减均值除标准差)再比较; 二是换成一个联合的目标,让三类在一个优化问题里竞争。 后者叫多分类 hinge,教材没展开,但它是 8.5 的自然推广。

二、实测:混淆矩阵

真实 预测 1 预测 2 预测 3 召回率
第 1 类 38 1 1 95.0%
第 2 类 2 35 3 87.5%
第 3 类 4 1 35 87.5%
准确率 90.0%:120 个点对了 108 个。
三类中心组成一个接近等边的三角形,标准差 0.95 和间距同一量级,所以必然有重叠。
混淆矩阵里有一处不对称:第 3 类被判成第 1 类 4 个,反过来只有 1 个。
如果只是「两类长得像」,这两个数应该差不多。
差四倍说明还有别的因素——就是刚才说的尺度偏置。
所以混淆矩阵除了告诉你「错多少」,还能告诉你「错的方向对不对等」。
不等就说明模型有系统性偏置,多半不是数据的问题。

三、不平衡:准确率会骗人

把第 3 类的训练样本从 40 个减到 8 个,测试时仍然用全部 40 个。 结果:总体准确率 76.7%,第 1 类 97.5%、第 2 类 95.0%、第 3 类只有 37.5%。 看那个 76.7%:它比 90.0% 掉了 13 个点,听起来是「变差了」。 但真正的灾难在第 3 类身上——它从 87.5% 掉到 37.5%,六成的少数类样本被误判。 一个只看总体准确率的人会以为模型只是「稍微退步」。 为什么会这样?因为平方损失是「按点累加」的。 第 3 类只有 8 个点,它在总损失里最多占 8 份; 把边界往它那边压,换来的多数类收益远大于它承受的损失。 所以模型「理性地」牺牲了少数类。 三种常规对策:给少数类加权(等价于复制样本)、对多数类下采样、换评估指标。 第三种最容易被忽略——如果评估指标本身是「总体准确率」,那模型优化它天经地义, 问题出在指标选错了。

四、还有哪些多类策略

一对多:每类一个分类器,共 K 个。判断时取输出最大的。 优点是一次训练 K 个独立的二类问题,实现简单、可以并行; 缺点是每类的正负样本不平衡(1 比 K 减 1)。 一对一:每两类之间训一个,共 K 乘 (K 减 1) 除以 2 个。判断时投票。 优点是每个分类器只用两类的数据,样本更均衡;缺点是分类器数量随 K 平方增长。 SVM 常用这个方案,因为它的训练开销大、但单次可以用很少的样本。 第三种是「一个目标搞定所有类」:直接最小化多分类 hinge 或者交叉熵。 它没有「尺度不可比」的问题,因为三类在一个目标函数里一起竞争。 代价是算法复杂一些,不能拆成 K 个独立的小问题。

五、动手 Demo:多类别:一对多最小二乘准确率 90.0%,不平衡时少数类召回率崩到 37.5%

三类、二维、每类 40 个点,中心在 (0, 0)、(2.6, 0.3)、(1.3, 2.2),标准差 0.95。 把标签做成 one-hot,解一个 3 输出的最小二乘,取输出最大的那一类,画出混淆矩阵。 然后把第 3 类的训练样本减到 8 个、测试集不变,再跑一遍。

Y = np.zeros((n, K))
Y[np.arange(n), lab] = 1.0                  # one-hot:每行只有一个 1
W = np.linalg.lstsq(A, Y, rcond=None)[0]    # 一次解出 K 组系数

S = A_test @ W                              # 每个样本 K 个输出
pred = np.argmax(S, axis=1)                 # 取最大的那一类

CM = np.zeros((K, K), dtype=int)
for t, p in zip(lab, pred):
    CM[t, p] += 1                           # 行是真实,列是预测

实跑输出:

--------------------------------------------------------------------
多类别:一对多最小二乘在 3 类上的准确率 90.0%
--------------------------------------------------------------------
  数据                     三类、二维,中心 (0, 0)、(2.6, 0.3)、(1.3, 2.2),每类 40 个
  做法                     把标签做成 one-hot,解一个多输出的最小二乘,再取输出最大的那一类
  标准差                   0.95,和中心间距同一量级,所以三类之间必然有重叠

  混淆矩阵(行是真实类别,列是预测类别):

  真实      预测 1   预测 2   预测 3   召回率
  ------------------------------------------
  第 1 类       38        1        1    95.0%
  第 2 类        2       35        3    87.5%
  第 3 类        4        1       35    87.5%

  **准确率 90.0%:120 个点对了 108 个。**
  **三类召回率是 95.0%、87.5%、87.5%。**
  **混淆最深的一对是「真实第 3 类被判成第 1 类」4 个,反过来只有 1 个。**
  **不对称的原因:每一类单独解一个回归,解的尺度不一样。**
  **输出值大的那一类更容易赢,这本身就是一种偏置,和「谁更像谁」无关。**

  再看类别不平衡:第 3 类从 40 个减到 8 个,其余不变。

  真实      预测 1   预测 2   预测 3   召回率
  ------------------------------------------
  第 1 类       39        1        0    97.5%
  第 2 类        2       38        0    95.0%
  第 3 类       13       12       15    37.5%

  **总体准确率还有 76.7%,但第 3 类召回率从 87.5% 变成 37.5%。**
  **这一行最值得记住:总体准确率在类别不平衡时是会骗人的;少数类只有 8 个,它在平方损失里的总分量自然就小,边界就朝它压过去。**
  **所以看分类结果必须看混淆矩阵,不能只看一个准确率。**
  要说明:**三类点是我按高斯造的,中心特意放成接近等边的三角形、标准差和间距同量级**,这样任意两类之间都有重叠,混淆矩阵里才有非对角元。

Y[np.arange(n), lab] = 1.0 是造 one-hot 的标准写法,比循环快得多。 它靠的是「花式索引」:行下标和列下标配对,一次赋值一批位置。 np.argmax(S, axis=1) 里的 axis=1 不能省。 省掉它会返回整个矩阵最大值的位置(一个数),而不是每个样本各自的最大类别。 这类错不会报错,只会给出一个荒谬的准确率。 混淆矩阵的累加用了一个显式循环。 这里 n 只有 120,循环开销无所谓;如果是百万级样本,应该用 np.add.at 或者 bincount。 「行是真实、列是预测」这个约定必须写进注释,因为反过来的写法也很常见。 注意第二次实验里,训练用的是 88 个点(40 加 40 加 8),而评估用的是全部 120 个点。 这个不对称是有意的:要测的是「少数类样本少」对泛化的影响,而不是对训练集拟合的影响。 要说明:三类点是按高斯造的,中心特意放成接近等边的三角形、标准差和间距同量级。 如果把中心拉得很开,混淆矩阵会是对角阵,那这个 Demo 什么都测不出来。

30 秒实验

  1. 把三个中心改成 (0, 0)、(5, 0)、(10, 0) 排成一条直线,再跑。 混淆矩阵会变成什么样?为什么中间的类最难分?
  2. 把输出先减掉每一类的均值、再除标准差(归一化),然后比较。不对称会消失吗?
  3. 改用一对一策略(三个二类分类器加投票),再跑。不平衡时第 3 类的召回率会不会好一些?

常见误区

  1. 「多类就是多跑几次两类」。 一对多确实是这样,但它的输出尺度不可比,会带进系统性偏置。 想干净地比较多类,得用联合的目标函数。
  2. 「总体准确率 90%,说明三类都分得不错」。 不一定。90% 可能掩盖「两类 100%、一类 70%」这种结构。 必须看每一类的召回率和混淆矩阵。
  3. 「类别不平衡只要下采样就行」。 下采样会丢信息,过采样会过拟合,加权是最省事的一种。 但更根本的是先问「评估指标对不对」——如果指标本身只看总体准确率,那问题在指标。
  4. 「混淆矩阵不对称说明代码写错了」。 不一定。一对多最小二乘天然会不对称,因为三组系数的尺度不同。 判断代码有没有错,要看对角线是否明显占优、总行和是否等于每类样本数。
自测三问(点开对答案) 1. 一对多方案里,为什么「取输出最大的那一类」不是「取输出最接近 1 的那一类」? —— **因为三组系数是分别解的,输出的尺度不同,直接和 1 比没有意义。** **而「谁最大」是个相对比较,尺度差异会被削弱(虽然不能完全消除)。** **所以 argmax 是一对多的标准做法——它比「最接近 1」稳健得多。** 2. 为什么少数类的召回率会随着它的样本数减少而下降? —— **因为平方损失按点累加,样本少就意味着「总话语权小」。** **边界往多数类那边挪,能让多数类的损失大幅下降,代价是少数类损失上升,** **但两者的总量不对称,所以总损失还是下降的。** **这是目标函数的必然结果,不是算法有 bug。** 3. 如果三类的中心排成一条直线,会发生什么? —— **中间那一类最难分。** **因为线性分类器的决策边界是一条直线,而中间的类被两侧夹着,** **一条直线最多只能把它和一侧分开,另一侧必然被切掉一块。** **这种数据需要非线性边界,或者先把数据投到高维(核方法,见 8.4)。**

7.1 到 7.3 讲的都是「借用最小二乘做分类」。 它的根本问题是:优化的目标(平方误差)和想要的结果(分对)不是一回事。 下一章直接改目标——把「间隔」变成要最大化的东西,那就是支持向量机。

相关笔记