🤖 第10章 概率分类法
📄 一句话概括

同一份数据,两个独立的最小二乘各解一次,完全不迭代。 最优一组(带宽 3.0、20 个中心)对数损失 0.1761、准确率 94.38%,Logistic 是 0.1188、 95.12%。 800 个测试点里有 318 到 402 个点上至少一个函数值是负的——它不是概率, 是先截断再归一化的产物。 截断的代价很硬:一个点被压成精确的 0,单它就吃掉全部对数损失的 24.5%。 中心数一多就崩:条件数从 1.590e+03 涨到 1.385e+18,中心数 160 时秩只有 110 而列有 161。 必须配 ℓ2 惩罚才能用。

📝 本节和前面几节的关系

10.1 的牛顿法 11 步就收敛,看着很漂亮。 可它每一步都要算 Hessian、解方程组,在类别多、参数多的场合并不便宜。 这一节讲一个「一次线性求解、完全不迭代」的替代方案。 它的想法非常直接:既然要的是「每一类的概率」,那就给每一类各拟合一个函数,再把它们归一化。 代价是这些函数值本身不受约束,可能是负数,也可能大于一——所以归一化之前必须先打补丁。 这一节的重点不是这个办法多好,而是「目标函数选错了,参数再多也补不回来」。

一、一类一个函数,再归一化

做法分两步。 第一步:对每一类 c,拟合一个函数 g 下标 c,目标是「这个点是不是第 c 类」的 0/1 指标, 用最小二乘解。 第二步:把两个函数值归一化成概率,p 等于 g 正 除以 (g 正 加 g 负)。 第一步为什么说得通?因为最小二乘的解在理论上逼近条件期望。 0/1 指标的条件期望恰好就是「属于这一类的概率」。 所以「对 0/1 标签做最小二乘」这件事,本身就是在估计概率——只是估计得很粗糙, 因为最小二乘只保证平方误差小,不保证值域。 这里的 g 用高斯核基展开:g(x) 等于常数项加一系列 exp(负 距离平方 除以 2h 平方) 的加权和。 最优那组用了 20 个中心,所以每个 g 有 21 个参数,两个 g 一共 42 个系数,但它们各自独立求解, 互不影响。 训练过程里没有迭代、没有 Hessian、没有学习率、没有停止条件。 就是两次 lstsq,各解一个 160 乘 21 的超定方程组。 训练时间是可预测的,这一点在工程上很值钱——它不会因为数据难就卡在那里不收敛。 这和 10.1 最根本的差别在目标函数:一个优化平方误差,一个优化对数似然。 两边都在估计同一个东西(条件概率),但用的尺子不一样。 尺子不一样,解就不一样——下一节所有现象都从这句话长出来。

二、它给出的不是概率

先看那张扫描表里最扎眼的一列:负值点。 五组参数下,800 个测试点里分别有 324、369、392、318、402 个点上,两个 g 里至少有一个是负的。 一个「概率」是负数,这本身就说明它不是概率——它只是一个被最小二乘拟合出来的函数值。 所以代码里必须先截断:把负数压成 0,再归一化。 这一步没有任何理论依据,纯粹是工程补救。 这就是它和 Logistic 回归最根本的区别:后者的输出天然在 0 到 1 之间,不需要打补丁。 补丁的代价比看上去大。 截断会把「差得远」压成「精确的 0」,而精确的 0 在对数损失里是无穷大的罚。 实测最优那组里,下标 721 那个点(真正类)拿到的 g 正 等于 负 0.0125,截断后 p 精确等于 0, 对数损失只能按 1e-15 兜底算成 34.54。 这一个点就吃掉全部 140.85 的 24.5%。 如果它拿到的是 0.0125 这么小的概率而不是精确的 0,损失只有 4.38。 换掉这一个点,整体均值会从 0.1761 降到 0.1384,和 Logistic 的差距从 0.0573 缩到 0.0196。 换句话说,两者的差距里有四分之三是一个点被截断造成的。 这不是巧合,是「截断」这个动作必然带来的后果:它把「几乎为零」升级成了「绝对为零」。 对比一下 Logistic:它的最小概率是 3.25e-09,罚 19.5,而不是 34.54。 sigmoid 永远到不了 0,所以它永远不会给出「绝对不可能」这种断言。 一个模型肯不肯说「绝对」,在算对数损失的时候是决定性的差别。

三、中心数一多就崩

扫描表里还有一处异常:中心数 80 那一行,g 正 的范围是 负 180.48 到 68.65。 一个「概率的分子」冲到负 180 和 68,明显不是拟合得好,是解出了问题。 原因在设计矩阵的条件数:

中心数 条件数 秩 列数
10 1.590e+03 11 11
20 8.425e+03 21 21
40 1.401e+07 41 41
80 1.978e+12 81 81
160 1.385e+18 110 161
条件数是「最大奇异值除以最小奇异值」,它衡量「输入的一点小扰动会被放大成多少倍」。
双精度大概有 16 位有效数字,条件数到 1e16 就意味着全丢了。
实测一路涨到 1.385e+18——那个解在数学上存在,在浮点里没有意义。
更严重的是秩:中心数 160 时矩阵只有 110 的秩,却有 161 列。
不满秩意味着最小二乘解不唯一,lstsq 会返回一个「最小范数解」,
可那个解和「拟合得好」已经没有关系了。
为什么会重叠得这么厉害?因为核宽 h 和数据尺度是同一个量级。
这份数据的标准差是 0.95、两团中心距 2.2,而 h 取 1.5——相邻两个中心的高斯核几乎一样,
矩阵的列几乎线性相关。
把所有中心挤在一起,就能造出任意大的条件数。
解法是 ℓ2 惩罚(第 4 章那一套):不求最小二乘解,
求「残差平方和 加 lam 乘 系数平方和」的最小值。
法方程从 T 转置乘 T 变成 T 转置乘 T 加 lam 乘 单位阵,条件数立刻被 lam 按住。
这是所有核方法的通病,不是这一个方法的 bug:核基之间天生高度相关,不加惩罚就不可逆。

四、和 Logistic 的取舍

项 Logistic 回归 最小二乘概率分类
训练方式 牛顿法迭代 11 步 一次线性求解
输出范围 天然 0 到 1 要截断才在 0 到 1
对数损失 0.1188 0.1761
测试准确率 95.12% 94.38%
参数个数 3 21
最后一行值得盯着看:Logistic 只有 3 个参数,这个方法用了 21 个。
参数多得多,效果反而略差。
原因很清楚——它优化的是平方误差,而平方误差根本不关心「归一化之后概率准不准」。
两个方法优化的东西不一样:一个直接优化对数似然,一个优化平方误差再事后归一化。
但「差 0.0573」这个结论要打折看,因为中心是随机挑的。
同一组带宽和中心数,换六个种子重跑,对数损失是 0.1761、0.1734、0.2151、0.2154、0.2246、
0.1796——波动幅度 0.0512,和那个差距差不多大。
所以「差多少」不能说死;能说死的是方向:六次里最低的 0.1734 仍然高于 Logistic 的 0.1188。
结论成立,但幅度不可靠。
它的卖点不是「更准」,是另外三件事。
一是不用迭代:训练时间可预测,不会卡在收敛上。
二是可以借最小二乘的全套工具:增量更新、分布式求解、递推最小二乘,都能直接用。
三是天然支持多类:每类解一个回归再归一化,类别数变了只是多解几个方程。
第三点最要紧。
Logistic 做多类要么 one-vs-rest(训练多个二分类器再投票),要么 softmax(耦合在一起解)。
这个方法天然就是「每类一个 + 归一化」,各类之间完全解耦——
第 19 章的多任务学习正是建立在这个性质上,多个任务共享一套基函数,各自解各自的。
要说明:这一节的两类数据是两团高斯,本质线性可分,所以线性模型占便宜。
如果决策边界是弯的,这个方法配上核基能自动弯,
而 Logistic 得自己加核——那时它的优势就没这么明显了。
方法的强弱永远要连着数据说。

五、动手 Demo:最小二乘概率分类:一次求解,对数损失 0.1761(Logistic 是 0.1188)

数据和 D034 完全一样:160 个训练点、800 个测试点。 每一类各拟合一个高斯核基展开的函数,用最小二乘解;两个函数值先截成非负,再归一化成概率。 扫五组「带宽乘中心数」,记录对数损失、准确率、负值点个数、正类函数值的范围。 然后把中心数从 10 加到 160,看设计矩阵的条件数和秩怎么变。最后和 Logistic 回归逐项对照。

def lspc(Xtr, ytr, Xte, h, ncen, seed=0):
    rg = np.random.default_rng(seed)
    C = Xtr[rg.choice(len(ytr), ncen, replace=False)]   # 随机挑中心
    T = np.column_stack([np.ones(len(Xtr)), kgauss(Xtr, C, h)])
    Tte = np.column_stack([np.ones(len(Xte)), kgauss(Xte, C, h)])
    wp = np.linalg.lstsq(T, (ytr > 0) * 1.0, rcond=None)[0]
    wm = np.linalg.lstsq(T, (ytr < 0) * 1.0, rcond=None)[0]
    gp, gm = Tte @ wp, Tte @ wm                          # 两个函数值
    neg = int(((gp < 0.0) | (gm < 0.0)).sum())           # 截断前统计
    ap = np.clip(gp, 0.0, None)                          # 负数压成 0
    am = np.clip(gm, 0.0, None)
    p = ap / np.maximum(ap + am, 1e-12)                  # 再归一化
    return p, neg, gp, gm, T

实跑输出:

--------------------------------------------------------------------
最小二乘概率分类:一次求解,对数损失 0.1761(Logistic 是 0.1188)
--------------------------------------------------------------------
  数据                     和 D034 完全一样:160 个训练点、800 个测试点
  模型                     每一类各拟合一个函数 g,用高斯核基展开;再把两个 g 归一化成概率
  训练                     两个独立的最小二乘问题,各解一次,没有迭代
  归一化                   p 等于 g 正 除以 (g 正 加 g 负),负值先截成 0

  扫几组带宽和中心数:

  带宽 h   中心数   对数损失   准确率   负值点         g 正的范围
  --------------------------------------------------------------
     0.8       20     0.2514   93.38%      324      -0.14 到 1.13
     1.5       20     0.2587   94.62%      369      -0.30 到 1.10
     3.0       20     0.1761   94.38%      392      -0.72 到 1.21
     1.5       40     0.3118   93.25%      318      -2.27 到 1.58
     1.5       80     1.3747   92.00%      402   -180.48 到 68.65

  **先看「负值点」这一列:318 到 402 个测试点上,两个 g 里至少有一个是负的。**
  **一个「概率」是负数,这本身就说明它不是概率——它只是一个被最小二乘拟合出来的函数值。**
  **所以我必须在归一化之前把它们截成 0,而这一步没有任何理论依据,纯粹是工程上的补救。**
  **这就是它和 Logistic 回归最根本的区别:后者的输出天然在 0 到 1 之间,不需要打补丁。**

  **再看对数损失:最好的一组是带宽 3.0、20 个中心,0.1761。**
  **Logistic 是 0.1188,比它低了 0.0573——也就是说 Logistic 的概率更「敢下判断」,错的时候罚得更少。**
  **准确率那边两者差不多:最优这组 94.38%,Logistic 95.12%。**
  **所以这个方法的卖点不是「更准」,是「不用迭代」——一次线性求解就完事,训练时间是可预测的。**

  可为什么中心数一多就崩?看设计矩阵的条件数:

  中心数      条件数    秩   列数
  ------------------------------
      10   1.590e+03    11     11
      20   8.425e+03    21     21
      40   1.401e+07    41     41
      80   1.978e+12    81     81
     160   1.385e+18   110    161

  **条件数从 1.59e+03 一路涨到 1.38e+18,中心数到 160 时矩阵已经不满秩(秩 110,列数 161)。**
  **这就是「中心数 80 时 g 正的范围冲到负 180 到 68」的原因:解一个病态方程组,系数被放大到荒唐的量级。**
  **所以这个方法必须配正则化(第 4 章的 ℓ2 惩罚)才能用,否则只是「看起来解出来了」。**
  **这也是所有核方法的通病:核基之间的相关性太高,不加惩罚就不可逆。**

  把两种做法摆在一起:

  项              Logistic 回归    最小二乘概率分类
  ------------------------------------------------
  训练方式     牛顿法迭代 11 步        一次线性求解
  输出范围          天然 0 到 1   要截断才在 0 到 1
  对数损失               0.1188              0.1761
  测试准确率             95.12%              94.38%
  参数个数                    3                  21

  **最后一行值得看一眼:Logistic 只有 3 个参数,最小二乘概率分类用了 21 个。**
  **参数多得多,效果反而略差。原因很清楚——它优化的目标是「平方误差」,而平方误差根本不关心「归一化之后概率准不准」。**
  **两个方法优化的东西不一样:一个直接优化对数似然,一个优化平方误差再事后归一化。**
  **目标函数选错了,参数再多也补不回来。**
  要说明:**这里的两类数据是「两团高斯」,本质上是线性可分的,所以线性模型占便宜。**
  **如果决策边界是弯的,最小二乘概率分类配上核基就能弯,Logistic 得自己加核——那时它的优势就没这么明显了。**

np.column_stack([np.ones(n), kgauss(...)]) 的第一列常数是截距项,不能省。 少了它,函数值被迫在原点附近过零,两团点偏在一侧时拟合会系统性偏掉。 加了它,每个 g 的参数个数是「中心数 加 1」,所以 20 个中心对应 21 个参数。 kgauss 里除的是 2.0 * h * h,这是高斯核的标准写法。 写成 h * h 也能跑,只是 h 的含义换了个比例(相当于标准写法的 h 乘根号二)。 这类「常数差一个因子」的坑在核方法里非常常见,跨论文复现时一定先对齐定义。 neg 必须在截断之前统计,否则永远是 0。 这行看起来是废话,但它记录的是这个方法最本质的缺陷——截断之后信息就没了, 你再也数不出有多少个点本来是负的。 np.maximum(ap + am, 1e-12) 处理的是「两个都是 0」的退化情形。 如果某个点上两个 g 都是负的,截断后分子分母全零,直接除会得到 nan。 兜底之后 p 落成 0,也就是「绝对不可能」——这个断言毫无依据,但代码不会报错。 这回的五组参数下这种点一次都没出现(实测 0 个),纯属运气好。 C = Xtr[rg.choice(...)] 让中心是随机的,所以换种子结果就会变。 这就是 10.2 正文里那个「波动 0.0512」的来源:它不是一个被控制的变量, 而是代码里顺带引入的随机性。 做对照实验时必须把种子钉死,否则你比的是两批不同的中心,不是两种方法。 条件数用 np.linalg.cond,它算的就是最大奇异值除以最小奇异值; 秩用 np.linalg.matrix_rank,默认容差是最大奇异值乘机器精度乘维数。 看这两个数就知道「这个方程组还能不能解」——条件数过 1e16、秩小于列数,答案就是不能。 要说明:这里的带宽 h 和中心数是手挑的,而且挑的时候看着测试集的对数损失。 这是名副其实的偷看——真做要留验证集,或者做交叉验证(第 4 章)。 所以 0.1761 这个数偏乐观,别拿它当这个方法在真实数据上的预期成绩。

30 秒实验

  1. 把中心的挑法从「随机取」改成「在训练集上等间隔取」,重跑六次(改随机种子)。 对数损失还会波动 0.05 吗?这说明波动来自哪里?
  2. 给最小二乘加 ℓ2 惩罚,也就是解 (T 转置乘 T 加 lam 乘 单位阵) w 等于 T 转置乘 t, 中心数固定 160,lam 取 1e-3、1e-1、1、10。 条件数降到多少?对数损失能追上 Logistic 的 0.1188 吗?
  3. 改成三类数据,两种方法各跑一遍(Logistic 用 one-vs-rest)。 谁更省事?谁的对数损失更低?多一类的时候各自的代码要改几行?

常见误区

  1. 「最小二乘概率分类就是 Logistic 回归的近似」。 不是。两者优化的目标函数不同:一个平方误差,一个对数似然。 目标不同,解就不同,而且差别可以很大——实测参数多 7 倍,对数损失反而高 0.0573。 「都是估计概率」这句话是对的,但「估计同一个东西」不等于「得到同一个答案」。
  2. 「负数截断一下就没事了」。 截断不是无关紧要的清理,它会改变结果:把「几乎为零」升级成「绝对为零」。 实测一个点被压成精确的 0,单它就吃掉全部对数损失的 24.5%。 而且截断之后你再也数不出有多少点本来是负的,缺陷被藏起来了。
  3. 「它比 Logistic 快,所以在工程上更好」。 快是真的,不用迭代也是真的。 但概率质量确实差一截,而且条件数一失控就完全不可用,必须配 ℓ2。 选方法要看下游要什么:只是排序,它够用;要拿概率算期望、定阈值,就得掂量。
  4. 「核基越多拟合能力越强,所以中心数越多越好」。 实测中心数从 10 加到 160,条件数从 1.590e+03 涨到 1.385e+18, 中心数 80 时函数值已经冲到负 180 到 68。 那不是「拟合能力变强」,是「方程解不动了」。 核方法的参数个数必须和正则化一起调,不能单方面加。
自测三问(点开对答案) 1. 为什么这个方法要先截断再归一化? —— **因为两个函数值是两次独立最小二乘的解,彼此没有任何约束,** **谁也不保证自己落在 0 到 1 之间。** **实测 800 个测试点里有 318 到 402 个点上至少一个是负的。** **负数没法当概率用,也没法做除法归一化(会出现负的概率),所以只能先压成 0。** **这一步是工程补丁,没有理论依据——这正是它和 Logistic 的分水岭。** 2. 为什么参数多得多,效果反而更差? —— **因为优化的目标不对。** **它优化的是平方误差,而平方误差衡量的是「函数值离 0/1 标签有多远」,** **根本不关心「归一化之后的概率准不准」。** **Logistic 直接优化对数似然,也就是直接优化「概率的质量」。** **目标函数选错了,参数再多也补不回来——多出来的参数只是让平方误差更小,** **对概率的准确性没有帮助。** 3. 中心数 160 时秩只有 110,这意味着什么? —— **意味着设计矩阵的列线性相关,方程组不满秩,最小二乘解不唯一。** **`lstsq` 会返回一个「最小范数解」,但那个解是算法约定的产物,不是数据决定的。** **换句话说,同一个问题换一个求解器可能给出另一组系数,而两者的残差一样小。** **这种解不能用来做解释,只能用来做预测——而且预测值还会因为条件数 1.385e+18 而剧烈抖动。**

第 10 章把「输出概率」这条路走完了:一条是直接优化似然,一条是平方误差加事后归一化。 到这里为止,所有方法都假设样本之间是独立的——每个点单独算自己的分。 下一章要打破这个假设:数据是一个序列,前一个标签会影响后一个。 那时连「损失函数怎么写」都要重新发明一套。

相关笔记