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

两团各有重叠的高斯点,把标签当正负 1 做回归、再取符号:训练准确率 95.6%。 加 12 个错标点之后,全部 172 个点上还有 87.2%,但只看原来那 160 个点, 从 95.6% 掉到 93.8%。 输出的最小值和最大值是负 1.68 和 1.72——它不是概率,也不是置信度。

📝 本节和前面几节的关系

第 3 到 6 章解决的都是「预测一个数」。从这一章开始换问题:判断属于哪一类。 最直接的办法是「不改算法、只改标签」——把类别编成正负 1,照旧做最小二乘。 这个方法本身有点粗糙,但它把「分类」和前面四章接上了。 看清它哪儿不行,才知道第 8 章为什么非要发明间隔。

一、把标签当数

做法只有三步:把两类标签写成正 1 和负 1;对 (X, y) 做最小二乘;把输出值按零的符号判成两类。 训练就是在找一条超平面,让「点离平面有多远再乘上标签」尽量接近 1。 为什么这样也行?因为它和 Fisher 线性判别在数学上很接近。 两者都在找「类中心差得远、类内散得小」的那个方向。 Fisher 判别把这个比值显式写成一个待最大化的式子,最小二乘分类则是「解出来一看, 方向差不多」。 这个等价关系只在两类、且用平方损失时成立。 三类的 one-hot 版本就没有这么干净的等价了——7.3 会看到它的偏置。

二、实测:95.6%,然后掉到 93.8%

数据 系数(w1, w2) 输出最小 输出最大 训练准确率
干净 0.283, 0.367 负 1.68 1.72 95.6%
加 12 个错标点 0.244, 0.323 负 1.58 1.39 87.2%
95.6% 这个数不高,因为两团点的标准差是 0.95、中心只隔 3.11,本来就有重叠。
错的那七八个点是真的落在对方的范围里,任何线性方法都分不开。
第二行的 87.2% 是全部 172 个点上的准确率,被那 12 个错标点本身拉低了。
更该看的是「只看原来那 160 个点」:93.8%,掉了 1.8 个百分点,大约 10 个点翻面。
边界确实被推歪了,只是没歪到垮掉。
原因是那 12 个点的残差特别大:标签是负 1,输出却是正的大数,残差接近负 3。
平方之后是 9,而正常点的残差平方只有 0.1 量级。
一个错标点的分量顶九十个正常点——平方损失对「差得远」的惩罚是超线性的。
这和第 6 章讲的是同一个病,对策也一样:换损失,不换模型。
把平方换成交叉熵(10.1)或者 hinge(8.5),错标点的分量就从「平方」降到「线性」。

三、输出值不是概率

Demo 里输出的范围是负 1.68 到 1.72。 它不是正负 1,也不是 0 到 1 的概率,只是「离超平面多远再乘一个尺度」。 这个尺度是任意的:把 w 和截距同时乘 2,符号一个都不变,但所有输出值翻倍。 所以「输出 0.3 是不是代表 30% 的把握」这个问题没有意义——换个尺度它就变成 0.6。 下一节的缩放实验会把这件事测出来。 想当概率用,必须再套一层:先过 sigmoid 把实数压到 0 到 1,再用交叉验证重新校准。 这一步不能省——直接把输出当概率,会得到系统性失准的置信度。

四、两个绕不开的毛病

第一个是错标点。 平方损失让最离谱的点拥有最大话语权,这和「分类」的直觉正好相反:分类的目标是「分对」, 而离谱的点往往是标错了。 第二个是类别不平衡。 正类 100 个、负类 10 个时,把所有点都判成正类就有 91% 的准确率。 最小二乘在平方损失下会不自觉地往多数类偏,因为那样总残差更小。 7.3 会用三类数据把这件事测出来,少数类的召回率会崩。 这两个毛病都不是「参数没调好」,是目标函数本身的取向问题。 所以最小二乘分类在实践中很少单独使用:它是个好起点、好基线,但真要做分类,得换损失函数。

五、动手 Demo:最小二乘分类:干净数据 95.6%,加 12 个错标点后原数据掉到 93.8%

两团高斯点各 80 个,二维,中心在 (负 1.1, 负 1.1) 与 (1.1, 1.1),标准差 0.95, 所以两团之间本来就有重叠。把标签设成正负 1,做最小二乘,再看输出的符号。 然后往正类靠边界的一侧塞进 12 个标签为负 1 的点,重新解一遍, 分别算「全部点上的准确率」和「只看原来那 160 个点的准确率」。

y = np.r_[np.full(n1, -1.0), np.full(n1, 1.0)]     # 标签直接当数
A = np.column_stack([X, np.ones(len(X))])
w = np.linalg.lstsq(A, y, rcond=None)[0]

s = A @ w                                          # 连续输出
pred = np.where(s >= 0, 1.0, -1.0)                 # 只看符号
print(np.mean(pred == y), s.min(), s.max())

实跑输出:

--------------------------------------------------------------------
最小二乘分类:干净数据 95.6%,加 12 个错标点后原数据掉到 93.8%
--------------------------------------------------------------------
  数据                     两团高斯点各 80 个,二维、中心 (负 1.1, 负 1.1) 与 (1.1, 1.1)
  做法                     把标签设成正负 1,直接做最小二乘,再看输出的符号
  错标点                   12 个点标成负类,位置却挤在正类靠边界的(0.9, 0.8)到(1.7, 1.4)一带

  数据             样本数   系数(w1, w2)   输出最小   输出最大   训练准确率
  --------------------------------------------------------------------------
  干净数据         160        0.283, 0.367      -1.68       1.72        95.6%
  加 12 个错标点   172        0.244, 0.323      -1.58       1.39        87.2%

  **干净数据上 95.6%。两团点中心相距 3.11、标准差 0.95,本来就有重叠,所以错十几个是正常的。**
  **这个准确率说明「把标签当数做回归」不是瞎凑:它和 Fisher 判别在数学上很接近,都在找类间差大、类内散小的那个方向。**

  **加 12 个错标点之后,全部 172 个点上的准确率还有 87.2%,看着还行。**
  **但只看原来那 160 个点,准确率从 95.6% 掉到 93.8%——边界被那几个点推歪了,正常点开始被误判。**
  **原因是那 12 个点的残差特别大:标成负 1,输出却是正的大数,平方损失给它们极大的权重。**
  **这和第 6 章是同一个病:平方损失把「差得远」放大成平方。**
  **对策也一样——换损失(交叉熵或者 hinge),而不是换模型。**

  **还有一件事:输出的最小值和最大值是 -1.68 和 1.72。**
  **它们不是正负 1,而是更大的数。**
  **所以这个输出既不是概率也不是置信度,只是「离超平面多远再乘一个尺度」。**
  **想当概率用,得先过一层 sigmoid 并重新校准,那是 10.1 的内容。**
  要说明:**两团点是按高斯造的、错标点也是我特意放的;真实数据的错标点不会有这么极端的残差,但方向一样:越离谱,破坏越大。**

这里最容易被忽略的一步是 np.ones 那一列——截距必须显式加进去。 不加的话,超平面被迫过原点,两团点中心不在原点对称时准确率会明显下降。 最小二乘本身不会自动给你截距,这一点和逻辑回归的库函数不一样。 np.where(s >= 0, 1.0, -1.0) 里用「大于等于零」还是「大于零」,在恰好为 0 时有区别。 这种样本几乎不会出现,但写代码时想清楚「等于零算哪一类」是个好习惯。 为什么准确率只有 95.6%? 因为两团点的标准差 0.95、中心距 3.11,重叠区里有真实的对向样本。 这部分误差是「贝叶斯误差」,任何线性分类器都消不掉。 想要更高准确率只能换非线性模型(2.2 的核模型、8.4 的核 SVM)。 注意第二行的系数方向和第一行几乎一样(0.283, 0.367 对 0.244, 0.323)。 错标点主要改的是截距——它把整条边界往正类那边推。 方向稳、位置飘,这是「被少数点推歪」的典型形态。 要说明:两团点是按高斯造的,12 个错标点是我特意放在边界附近的。 放在深处(比如 (2.5, 2.3))反而不容易看出破坏——那种点的残差太大, 平方损失把它们压成一个常数项,只抬截距不改方向。

30 秒实验

  1. 把错标点从 12 个加到 30 个,再跑一遍。 原数据上的准确率会掉到多少?会不会出现「全判成一类」的坍缩?
  2. 把两团点的标准差从 0.95 加到 1.4,让重叠更严重。 准确率的下限是多少?这时的错误是模型的问题还是数据的问题?
  3. 把正类的样本数改成 160、负类 20(不平衡),再跑。准确率会变成多少?把全部判成正类能得多少?

常见误区

  1. 「最小二乘分类就是把标签当数做回归,所以它是个回归方法」。 形式上是的,但它和 Fisher 判别等价,所以在两类问题上有明确的分类意义。 「等价于回归」不等于「只是个凑合的办法」。
  2. 「输出值可以当置信度用」。 不行。输出值随系数的尺度变化,而尺度是任意的。 把 w 乘 2,所有输出翻倍,符号一个不变——同一批数据的「置信度」就整体翻倍了,这不合理。
  3. 「准确率高就说明模型好」。 类别不平衡时会骗人。 正类 9 个、负类 1 个,全判成正类就有 90%。 必须看混淆矩阵和每一类的召回率。
  4. 「最小二乘分类是所有分类方法的基础,所以先学它最划算」。 它确实是本书的入口,但它在实践里几乎不用——因为它优化的是平方损失,不是分类损失。 真要用,得换成 hinge(第 8 章)或交叉熵(10.1)。
自测三问(点开对答案) 1. 为什么最小二乘分类和 Fisher 判别会给出相近的方向? —— **因为两者都在解一个「类间方差除以类内方差」的最大化问题。** **把标签写成正负 1 之后,最小二乘解的类间项和类内项正好能拼成这个比值,** **只差一个常数因子。** **所以方向一致、尺度不同——这也是为什么输出值的绝对值没有意义。** 2. 为什么错标点会主要改截距而不是方向? —— **因为错标点的 x 位置和正类的中心在同一个方向,它们要「把标签拉回来」,** **最省力的做法是把边界整体平移。** **如果错标点长在正交方向上(比如 x 轴很远处而两类沿对角线分开),那就会明显改方向。** 3. 如果两类样本数相差十倍,用最小二乘分类会出什么问题? —— **边界会往少数类那边压,少数类的召回率会明显下降。** **因为「把少数类判错」的代价在总残差里只占十分之一,模型宁可牺牲它。** **解法有两种:给少数类加权,或者换损失函数——两种都是「改目标」而不是「改数据」。**

最小二乘分类的目标是平方误差,而分类真正在意的只有「分对没分对」。 下一节把这两个目标摊开对比:0/1 损失长什么样、为什么不能直接优化、 以及「间隔」这个概念是怎么冒出来的。

相关笔记