颜力刚Ligang Yan

· NSCC

手算 SVD:把一个 2×3 矩阵拆成 UΣVᵀ

奇异值分解能把任何矩阵拆成「旋转、拉伸、再旋转」三步。拿一个 2×3 的矩阵从头算一遍:先求 AAᵀ 的特征值,开根号得到奇异值,再一列一列拼出 U 和 V,最后乘回去验算。顺便讲清楚为什么要开根号、为什么正负号会出问题。

线性代数SVD机器学习NumPy

English version: SVD by Hand: Splitting a 2×3 Matrix into UΣVᵀ

奇异值分解(SVD)说的是:任何一个矩阵,不管是不是方阵,都能拆成三个矩阵相乘。

A  =  U   ·   Σ   ·   Vᵀ
m×n   m×m     m×n     n×n
部分 名字 性质
U 左奇异向量 正交矩阵:每一列长度为 1,两两垂直
Σ 奇异值 对角矩阵,对角线上 σ₁ ≥ σ₂ ≥ … ≥ 0,其余全是 0
Vᵀ 右奇异向量 V 也是正交矩阵,Vᵀ 是它的转置,也是它的逆

几何上,A 作用在一个向量上,可以看成三步:Vᵀ 先转一下,Σ 沿坐标轴拉伸,U 再转一下。任何线性变换都逃不出「旋转、拉伸、旋转」这个模式。

下面拿一个具体的矩阵从头手算一遍。

题目

A = [3   2   2]
    [2   3  −2]

A 是 2×3,所以各部分的形状是:

  A    =   U    ·    Σ    ·   Vᵀ
 2×3      2×2       2×3      3×3

奇异值的个数是 min(2, 3) = 2。Σ 是 2×3,放完两个奇异值后多出一整列 0;V 却需要 3 列,第 3 列要另外想办法找。

思路:先算小的那个

手算 SVD 的标准做法来自下面两个等式(推导见第 3 步):

  • V 的列是 AᵀA 的特征向量
  • U 的列是 AAᵀ 的特征向量
  • 两者的特征值都是 σ²

这里 AAᵀ 是 2×2,AᵀA 是 3×3。2×2 求特征值只要解一个一元二次方程,所以从 AAᵀ 下手:

  1. 求 AAᵀ 的特征值,开根号得到 Σ;特征向量组成 U
  2. 用 vᵢ = Aᵀuᵢ / σᵢ 推出 V 的前两列
  3. V 的第 3 列用叉积补上

第 1 步:算 AAᵀ

AAᵀ 的每个元素,是 A 的两行互相做点积。记 r₁ = (3, 2, 2),r₂ = (2, 3, −2):

r₁ · r₁ = 9 + 4 + 4  = 17
r₁ · r₂ = 6 + 6 − 4  = 8
r₂ · r₂ = 4 + 9 + 4  = 17

AAᵀ = [17   8]
      [ 8  17]

第 2 步:求特征值

det(AAᵀ − λI) = (17 − λ)² − 8² = 0
17 − λ = ±8

λ₁ = 25,  λ₂ = 9          # 从大到小排

第 3 步:开根号,写出 Σ

σ₁ = √25 = 5,   σ₂ = √9 = 3

Σ = [5  0  0]              # 2×3,和 A 同形状
    [0  3  0]

为什么要开根号? 把 A = UΣVᵀ 代进 AAᵀ:

AAᵀ = (UΣVᵀ)(UΣVᵀ)ᵀ
    = UΣ(VᵀV)ΣᵀUᵀ        # V 是正交矩阵,VᵀV = I
    = U · ΣΣᵀ · Uᵀ

ΣΣᵀ 就是把对角线上每个数平方。AAᵀ 里 A 出现了两次,拉伸也作用了两次,所以它的特征值是 σ² 而不是 σ。开根号,才是 A 自己拉伸一次的倍数。

可以直接验证:单位向量 v₁ = (1, 1, 0)/√2 经过 A 一次,长度变成 5;再经过 Aᵀ,变成 25 倍。

为什么不直接对 A 求特征值?因为 A 不是方阵,根本没有特征值。就算是方阵,特征值也可能是负数或复数。AAᵀ 永远是对称矩阵,特征值永远 ≥ 0,所以先绕到它那里求,最后开根号回来。

第 4 步:AAᵀ 的特征向量 → U

λ = 25:(17 − 25)x₁ + 8x₂ = 0  →  x₁ = x₂   →  (1, 1)
λ = 9: (17 − 9)x₁  + 8x₂ = 0  →  x₁ = −x₂  →  (1, −1)

两个向量长度都是 √2。归一化之后,按 σ 从大到小的顺序放成列:

U = (1/√2) [1   1]
           [1  −1]
            ↑    ↑
          σ₁=5  σ₂=3

检查:两列点积 1 − 1 = 0,互相垂直。

第 5 步:用 vᵢ = Aᵀuᵢ / σᵢ 求 V 的前两列

Aᵀ = [3   2]
     [2   3]
     [2  −2]

Aᵀ · (1, 1)  = (5, 5, 0)
v₁ = (5, 5, 0) ÷ (√2 · 5)  = (1, 1, 0) / √2

Aᵀ · (1, −1) = (1, −1, 4)
v₂ = (1, −1, 4) ÷ (√2 · 3) = (1, −1, 4) / √18

检查:|v₂|² = (1 + 1 + 16) / 18 = 1,v₁ · v₂ = 1 − 1 + 0 = 0。

为什么不直接求 AᵀA 的特征向量? 特征向量的正负号是任意的:(1, 1) 是特征向量,(−1, −1) 也是。如果 U 和 V 分别独立去求,符号可能配不上,最后乘出来不等于 A。用 uᵢ 直接推出 vᵢ,两边一定配套,而且省掉了 3×3 的特征值问题。

第 6 步:用叉积求 v₃

v₃ 要和 v₁、v₂ 都垂直,并且满足 Av₃ = 0,也就是和 A 的两行都垂直。在三维空间里,同时垂直于两个向量的方向就是它们的叉积:

(3, 2, 2) × (2, 3, −2)
  = (2·(−2) − 2·3,  2·2 − 3·(−2),  3·3 − 2·2)
  = (−10, 10, 5)
  → 约分 (−2, 2, 1)

v₃ = (−2, 2, 1) / 3

检查:Av₃ = (−6 + 4 + 2, −4 + 6 − 2) = (0, 0);v₃ · v₁ = 0;v₃ · v₂ = 0。

第 7 步:V 转置成 Vᵀ

V 的列变成 Vᵀ 的行:

      [ 1/√2     1/√2      0    ]   ← v₁
Vᵀ =  [ 1/√18   −1/√18   4/√18  ]   ← v₂
      [−2/3      2/3      1/3   ]   ← v₃

结果

[3  2   2]  =  (1/√2) [1   1]  ·  [5  0  0]  ·  Vᵀ
[2  3  −2]            [1  −1]     [0  3  0]

验算:按层相加

UΣVᵀ 也可以写成一层一层叠加:

A = σ₁·u₁v₁ᵀ + σ₂·u₂v₂ᵀ

每一层是「一列乘一行」,形状都是 2×3。

第 1 层:σ₁ · (1/√2) · (1/√2) = 5 · ½

  5 · ½ · [1  1  0]  =  [2.5  2.5  0]
          [1  1  0]     [2.5  2.5  0]

第 2 层:σ₂ · (1/√2) · (1/√18) = 3 · ⅙ = ½

  ½ · [ 1  −1   4]  =  [ 0.5  −0.5   2]
      [−1   1  −4]     [−0.5   0.5  −2]

相加:                  [3  2   2]   = A
                        [2  3  −2]

v₃ 去哪了?它对应 Σ 里全是 0 的第 3 列,乘上 0 就没了。所以实际使用时通常用「瘦 SVD」,直接不算它。

每层的分量按 σ² 算:第 1 层 25,第 2 层 9。只保留第 1 层,就保留了 25/34 ≈ 74% 的信息。

容易出错的地方

  • 列的顺序跟着 Σ 走。 U 和 V 的第 1 列都必须配最大的 σ。顺序错位,乘出来就不是 A。
  • Σ 是 2×3,不是 2×2。 它和 A 形状一样,多出来的位置全是 0。
  • uᵢ 和 vᵢ 的符号要配套。 可以两个一起取负号,但不能只改一个。
  • v₃ 只在完整版 SVD 里需要。 瘦 SVD 不要它。

用 NumPy 核对

import numpy as np

A = np.array([[3, 2, 2], [2, 3, -2]])
U, s, Vt = np.linalg.svd(A)

s    # [5. 3.]
U    # [[-0.7071 -0.7071]
     #  [-0.7071  0.7071]]
Vt   # [[-0.7071 -0.7071  0.    ]
     #  [-0.2357  0.2357 -0.9428]
     #  [-0.6667  0.6667  0.3333]]

奇异值一样是 5 和 3。NumPy 把 (u₁, v₁) 和 (u₂, v₂) 都整体取了负号,两个负号相乘抵消,乘出来还是 A。对照一下数字:0.7071 = 1/√2,0.2357 = 1/√18,0.9428 = 4/√18。

两个小坑:

  • s 是一维数组,不是矩阵。要还原 A 得写 U @ np.diag(s) @ Vt。矩阵不是方阵时,要加 full_matrices=False 形状才对得上,或者先把 np.diag(s) 补成正确的形状。
  • 第三个返回值已经是 Vᵀ,不要再转置一次。

算这个有什么用

A = UΣVᵀ 本身不是目的。SVD 的价值在于它把矩阵按重要性排好了序:σ 从大到小,前几层占了大部分信息,后面的可以扔掉。

拿一张 600×512 的灰度照片试一下,它就是一个有 307,200 个数的矩阵。只保留前 k 层,需要存 k × (600 + 512 + 1) 个数:

保留层数 k 存储量 占原图 保留的信息量
1 1,113 0.4% 75%
20 22,260 7.2% 96.6%
50 55,650 18% 98.9%
100 111,300 36% 99.6%

k = 50 时,图片肉眼看已经基本清楚了。PCA 降维、推荐系统、去噪,用的都是同一个想法:留下大的 σ,扔掉小的。