同一份数据,两个独立的最小二乘各解一次,完全不迭代。 最优一组(带宽 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 秒实验
- 把中心的挑法从「随机取」改成「在训练集上等间隔取」,重跑六次(改随机种子)。 对数损失还会波动 0.05 吗?这说明波动来自哪里?
- 给最小二乘加 ℓ2 惩罚,也就是解 (T 转置乘 T 加 lam 乘 单位阵) w 等于 T 转置乘 t, 中心数固定 160,lam 取 1e-3、1e-1、1、10。 条件数降到多少?对数损失能追上 Logistic 的 0.1188 吗?
- 改成三类数据,两种方法各跑一遍(Logistic 用 one-vs-rest)。 谁更省事?谁的对数损失更低?多一类的时候各自的代码要改几行?
常见误区
- 「最小二乘概率分类就是 Logistic 回归的近似」。 不是。两者优化的目标函数不同:一个平方误差,一个对数似然。 目标不同,解就不同,而且差别可以很大——实测参数多 7 倍,对数损失反而高 0.0573。 「都是估计概率」这句话是对的,但「估计同一个东西」不等于「得到同一个答案」。
- 「负数截断一下就没事了」。 截断不是无关紧要的清理,它会改变结果:把「几乎为零」升级成「绝对为零」。 实测一个点被压成精确的 0,单它就吃掉全部对数损失的 24.5%。 而且截断之后你再也数不出有多少点本来是负的,缺陷被藏起来了。
- 「它比 Logistic 快,所以在工程上更好」。 快是真的,不用迭代也是真的。 但概率质量确实差一截,而且条件数一失控就完全不可用,必须配 ℓ2。 选方法要看下游要什么:只是排序,它够用;要拿概率算期望、定阈值,就得掂量。
- 「核基越多拟合能力越强,所以中心数越多越好」。 实测中心数从 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 章把「输出概率」这条路走完了:一条是直接优化似然,一条是平方误差加事后归一化。 到这里为止,所有方法都假设样本之间是独立的——每个点单独算自己的分。 下一章要打破这个假设:数据是一个序列,前一个标签会影响后一个。 那时连「损失函数怎么写」都要重新发明一套。
- ☐ (本章完,下一节 11.1 11.1)