卷 IV · 逼近CH 16深度 16/20

最小二乘把条件数平方了

线性回归是这个星球上被执行次数最多的数值算法。它的教科书解法叫「正规方程」,形式优美、推导简单、三行就能实现。它也有一个几乎从不被提起的性质:它把问题的条件数平方了。这意味着你手上 16 位的精度,一进门就只剩 8 位——而这件事在数据有共线性时会立刻要命。

κ(AᵀA) = κ(A)²QR 与 SVDnumpy 直接拒绝求解

▷ 猜一下差多少

一个 3×2 的最小二乘问题(这是数值分析里的标准测试例,叫 Läuchli 矩阵):

      ⎡ 1  1 ⎤        ⎡ 1 ⎤
  A = ⎢ ε  0 ⎥    b = ⎢ 0 ⎥        ε = 10⁻⁸
      ⎣ 0  ε ⎦        ⎣ 0 ⎦

真解(精确算出来的):x = [0.5, 0.5]
κ(A) = 1.414 × 10⁸ —— 有点病态,但离 double 的极限 10¹⁶ 还差八个数量级

用教科书的正规方程解:x = solve(AᵀA, Aᵀb)

问:会得到什么?

A [0.5, 0.5]。κ(A) 才 10⁸,double 有 16 位,绰绰有余 B [0.50000001, 0.49999999] 之类。掉几位而已 C 一个离谱但合法的数,比如 [1.2, -0.2] D 算不出来。numpy 会抛「奇异矩阵」

第二问:改用 QR 分解解同一个问题呢?

把 AᵀA 写出来

AᵀA = ⎡ 1 + ε²    1    ⎤        ε = 10⁻⁸,所以 ε² = 10⁻¹⁶
      ⎣    1    1 + ε² ⎦

而 1 + 10⁻¹⁶ 在 double 里是多少?

  ulp(1) = 2.22 × 10⁻¹⁶
  10⁻¹⁶ 比半格(1.11 × 10⁻¹⁶)还小
  ⇒ ⟨1 + 10⁻¹⁶ = 1⟩

所以 double 里的 AᵀA 是:

  ⎡ 1  1 ⎤
  ⎣ 1  1 ⎦          ← 两行完全相同,奇异矩阵

numpy 抛 LinAlgError: Singular matrix。答案是 D。

ε 出现在 A 里的时候好好的(10⁻⁸ 离 double 的极限远得很)。一进 AᵀA 就被平方成 10⁻¹⁶,正好卡在机器 epsilon 上,整个消失。

第二问:QR 分解给出 [0.5, 0.5],和精确解逐位相同。numpy.linalg.lstsq(内部走 SVD)也给出 [0.5, 0.5]。

κ(AᵀA) = κ(A)²,这是个定理

把 ε 扫一遍,这条关系会以最直白的方式显示出来:

εκ(A)κ(AᵀA)正规方程解QR 解
10⁻²1.414 × 10²2.000 × 10⁴[0.4999750012, 0.4999750013][0.4999750012, 0.4999750012]
10⁻⁴1.414 × 10⁴2.000 × 10⁸[0.4999999978, 0.4999999972][0.4999999975, 0.4999999975]
10⁻⁶1.414 × 10⁶2.000 × 10¹²[0.5000000000, 0.5000000000][0.5000000000, 0.5000000000]
10⁻⁸1.414 × 10⁸5.962 × 10¹⁶奇异矩阵,解不出来[0.5000000000, 0.5000000000]

κ(AᵀA) 那一列,每一行都是左边那一列的平方(1.414×10²)² = 2×10⁴(1.414×10⁴)² = 2×10⁸(1.414×10⁶)² = 2×10¹²

这不是巧合,是一行代数:A 的奇异值是 σᵢ,那么 AᵀA 的特征值就是 σᵢ²,于是最大比最小的比值被平方。

◆ 主线

正规方程要求 κ(A)² < 1/eps,也就是 κ(A) < 10⁸。QR 只要求 κ(A) < 10¹⁶

换一种说法,也是更该记住的说法:用正规方程,等于把你的 double 当 float 用。float32 的 eps 是 1.19×10⁻⁷,正好是 double 开根号的量级。

这条不等式是这一章的全部内容。它有一个很实用的推论:如果你的特征之间有强相关(共线性),κ(A) 会轻松突破 10⁸——而那正是正规方程失效的地方。共线性和病态是同一件事的两个名字。

那为什么正规方程还到处都是

因为它有真实的优点,不是纯粹的历史包袱:

正规方程QRSVD
运算量(m×n,m≫n)mn² + n³/32mn² − 2n³/3约 2mn² + 11n³
大致快慢最快(约快一倍)最慢
能承受的 κ(A)10⁸10¹⁶10¹⁶
能处理秩亏不能要带列主元才行能,而且能告诉你秩是多少
能流式/增量更新(只要累加 AᵀA 和 Aᵀb)要用 Givens 旋转

最后一行是它在工程里活下来的真正原因:AᵀA 只有 n×n,和样本数无关。一亿行数据、二十个特征,你只需要累加出一个 20×20 的矩阵,数据扫一遍就能丢掉。这在分布式和流式场景里是不可替代的(每个分片各自累加,最后把矩阵加起来就行)。

所以正确的态度不是「永远别用」,而是:用之前先看 κ(A),或者至少先做标准化。

三件几乎免费的补救

  1. 中心化 + 标准化。每一列减均值、除标准差。这一步能把「因为量纲不同而虚高的条件数」全部消掉——一列是「年龄(20–80)」、一列是「收入(10⁴–10⁶)」,不标准化的话 κ 里有四个数量级纯粹来自单位。统计软件默认做这件事,理由就是这个。
  2. 正则化(岭回归)。(AᵀA + λI)x = Aᵀb。加上 λI 把最小的特征值从 σₘᵢₙ² 抬到 σₘᵢₙ² + λ,条件数应声下降。岭回归的数值意义就是买条件数——它的统计意义(缩减方差)和数值意义(降 κ)是同一件事的两面,这不是巧合。
  3. 直接调 lstsqnumpy、scipy、R 的 lm()、statsmodels 内部走的都是 QR 或 SVD,不是正规方程。你只要不自己写 solve(A.T@A, A.T@b),就已经在正确的路上了。
✎ 术语正名

共线性(collinearity)、病态(ill-conditioning)、秩亏(rank deficiency)——统计课和数值课用不同的词说同一件事:

  • 统计说「两个特征高度相关,系数不稳定,VIF 很大」。
  • 数值说「设计矩阵的两列几乎平行,最小奇异值接近 0,κ 很大」。
  • 秩亏是极限情况:完全平行,σₘᵢₙ = 0,κ = ∞。

方差膨胀因子 VIF 和条件数是可以互相换算的(都在量同一件事)。知道这一点之后,统计里那些「处理共线性」的手段(删特征、PCA、岭回归)和数值里那些「降条件数」的手段(换基、正则化、SVD 截断)就会显示出它们本来就是同一批工具。

⌨ 自己跑一遍

Läuchli 矩阵只要三行就能构造,值得亲手看一次「1 + ε² 变成 1」:

import numpy as np

print('%-8s %-12s %-12s %-24s %s' % ('ε', 'κ(A)', 'κ(AᵀA)', '正规方程解', 'QR 解'))
for e in (1e-2, 1e-4, 1e-6, 1e-8):
    A = np.array([[1.0, 1.0], [e, 0.0], [0.0, e]])
    b = np.array([1.0, 0.0, 0.0])
    AtA = A.T @ A
    try:
        xn = np.linalg.solve(AtA, A.T @ b)
        s = '[%.10f %.10f]' % (xn[0], xn[1])
    except np.linalg.LinAlgError:
        s = '奇异矩阵,解不出来'
    q, r = np.linalg.qr(A)
    xq = np.linalg.solve(r, q.T @ b)
    print('%-8.0e %-12.3e %-12.3e %-24s [%.10f %.10f]'
          % (e, np.linalg.cond(A), np.linalg.cond(AtA), s, xq[0], xq[1]))

# 看清楚 AᵀA 变成了什么
A = np.array([[1.0, 1.0], [1e-8, 0.0], [0.0, 1e-8]])
print('AᵀA =', (A.T @ A).tolist())        # [[1.0, 1.0], [1.0, 1.0]]
print('1 + 1e-16 ==', repr(1 + 1e-16))    # 1.0
print('真解恒为 [0.5, 0.5]')

会打出:

ε        κ(A)         κ(AᵀA)       正规方程解               QR 解
1e-02    1.414e+02    2.000e+04    [0.4999750012 0.4999750013] [0.4999750012 0.4999750012]
1e-04    1.414e+04    2.000e+08    [0.4999999978 0.4999999972] [0.4999999975 0.4999999975]
1e-06    1.414e+06    2.000e+12    [0.5000000000 0.5000000000] [0.5000000000 0.5000000000]
1e-08    1.414e+08    5.962e+16    奇异矩阵,解不出来           [0.5000000000 0.5000000000]

注意 ε=10⁻⁸ 那一行的 κ(AᵀA) 是 5.962×10¹⁶,而不是理论值 2×10¹⁶——因为这时候连「计算 κ(AᵀA)」这件事本身都不可靠了。条件数很大时,条件数的估计值本身也不准。

python3 lauchli.py

在线:Google Colab

▸ 在现实里

线性回归。R 的 lm()、Python 的 statsmodels、scikit-learn 的 LinearRegression——全部走 QR 或 SVD(sklearn 用的是 scipy.linalg.lstsq,也就是 SVD)。你如果自己写 np.linalg.inv(X.T@X) @ X.T@y,就同时踩了第 15 章和第 16 章两个坑。这行代码在教程和面试题里出现的频率高得惊人。

三维重建与 SLAM。Bundle adjustment 解的是一个巨大的非线性最小二乘,每次迭代都要解一个正规方程(那个 JᵀJ 就是 AᵀA)。这里不得不用正规方程,因为 JᵀJ 的稀疏结构(舒尔补)是唯一能让问题算得动的东西。代价是必须靠 Levenberg–Marquardt 的阻尼项(也就是那个 λ)来把条件数压住——LM 算法的阻尼项在数值上就是岭回归。

卡尔曼滤波(又是它)。协方差矩阵 P 本质上是一个 AᵀA,所以它天生条件数平方。平方根滤波之所以叫这个名字,就是因为它维护的是 P 的 Cholesky 因子——相当于一直待在 QR 那一侧,从不形成 AᵀA

深度学习里的二阶方法。K-FAC、Shampoo 这些方法要估计并求逆 Fisher 信息矩阵(也是个 AᵀA)。它们全都带阻尼项,理由完全相同。

✗ 这个直觉是错的

「κ(A) = 10⁸ 不算大,double 有 16 位,还剩 8 位,够用。」

「还剩 8 位」这个算法只在你直接解 A 时成立。走正规方程,实际面对的条件数是 κ(A)² = 10¹⁶——剩 0 位。

这条直觉错在把「问题的条件数」和「我实际求解的那个问题的条件数」当成了同一个。你选的解法会创造出一个新的问题,而新问题有自己的条件数。这是这本书的一条暗线:第 8 章「先收后支再相减」把条件数从 1 变成 8.4×10¹⁰,第 15 章的求逆丢掉后向稳定,这一章的正规方程把 κ 平方——三次都是「解法制造了新的病态」。

可操作的判据:算完 κ(A) 之后,问一句「我的解法会不会动这个数」。会动,就用动完之后那个数去判断精度够不够。

◇ 对账

正确答案是 D:numpy 抛 LinAlgError: Singular matrix,因为 AᵀA 在 double 里变成了 [[1,1],[1,1]]。QR 给出 [0.5, 0.5],精确。

A 「κ(A) 才 10⁸,绰绰有余」——如果直接对 A 做 QR,这句话完全正确(QR 给出的答案是精确的)。它错在没有把解法本身算进去。κ(A) 描述的是问题,κ(AᵀA) 描述的是你实际在解的那个东西。 B 「掉几位而已」——在 ε = 10⁻⁴ 那一行,B 是对的(正规方程给出 [0.4999999978, 0.4999999972],掉了几位但可用)。ε 只再小四个数量级,就从「掉几位」跳到「解不出来」——因为平方让退化速度也翻了倍。 C 「一个离谱但合法的数」——这是最危险的那种结果,而这道题上你运气好,没碰到。AᵀA 恰好退化成两行完全相同,numpy 才检测得出来。如果 ε 是 10⁻⁷.⁵ 这类值,AᵀA 会是「几乎奇异但不完全奇异」——numpy 不会报错,会安静地返回一个错得离谱的数。C 描述的是更常见的情形,只是这个特定例子恰好跳过了它。
◈ 换一种写法
x = np.linalg.inv(A.T @ A) @ A.T @ b  —— 第 15 章 + 第 16 章,两个坑一起踩 x = np.linalg.lstsq(A, b, rcond=None)[0]  —— 走 SVD,能承受 κ(A) 到 10¹⁶,还能处理秩亏 或者 x = scipy.linalg.lstsq(A, b)[0];要快就 lapack_driver='gelsy'(带列主元 QR) 数据没标准化就直接拟合 先中心化 + 标准化。免费,而且往往能把 κ 降好几个数量级——因为大部分条件数来自量纲,不是来自数据结构 发现结果不稳定,于是加大迭代次数/换优化器 先算 np.linalg.cond(A)如果它是 10¹⁰,那不是优化器的问题,加多少迭代都没用——要么加正则化,要么删特征,要么承认这几个系数就是估不准

非要用正规方程(因为要流式累加、或者要利用稀疏结构)时,至少做两件事: 累加 AᵀA 时用更高精度或补偿求和(第 8 章); 加一个小的 λI这两条加起来还是比 QR 便宜,而且把最坏情况堵住了。

这一章的一句话

正规方程的推导只有三行,代价却是把条件数平方——它等于悄悄把你的 double 降级成 float,而这份降级从不出现在任何报错里。

卷 IV 到此结束。前十六章的全部内容,都发生在同一个格子里:IEEE 754 双精度。

卷 V 换格子。接下来四章会看到:钱为什么必须离开这个格子(第 17 章);AI 为什么主动搬到一个只有 7 位尾数的格子里去,还训得更好(第 18 章);以及一个让所有人都不舒服的事实——就算格子不变,同一份源代码在同一台机器上,只要加一个编译开关,答案就会变。第 19 章会在这台机器上用真的 C 编译器把它跑出来。