手算 SVD:把一个 2×3 矩阵拆成 UΣVᵀ
奇异值分解能把任何矩阵拆成「旋转、拉伸、再旋转」三步。拿一个 2×3 的矩阵从头算一遍:先求 AAᵀ 的特征值,开根号得到奇异值,再一列一列拼出 U 和 V,最后乘回去验算。顺便讲清楚为什么要开根号、为什么正负号会出问题。
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ᵀ 下手:
- 求
AAᵀ的特征值,开根号得到Σ;特征向量组成U - 用
vᵢ = Aᵀuᵢ / σᵢ推出V的前两列 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 降维、推荐系统、去噪,用的都是同一个想法:留下大的 σ,扔掉小的。