奇异值分解
幂迭代法 SVD、截断 SVD、图像压缩、伪逆、LSA、PCA 与 SVD 的关系
奇异值分解
SVD 是线性代数的瑞士军刀。PCA、推荐系统、图像压缩、自然语言处理——它们都是 SVD。
类型: 构建 语言: Python 前置条件: 阶段 1,第 01-03 课 预计时间: ~90 分钟
学习目标
- 使用幂迭代法从零计算 SVD
- 应用截断 SVD 进行低秩近似和图像压缩
- 计算 Moore-Penrose 伪逆求解最小二乘问题
- 解释 LSA(潜在语义分析)和 PCA 与 SVD 的关系
问题所在
你有一个 10000x10000 的矩阵。存储它需要 100M 个浮点数。但如果它的有效秩只有 50,你只需要存储 50 个奇异值和两组 50 维向量。压缩比 200:1。
SVD 将任何矩阵分解为三个矩阵的乘积,揭示其秩结构。这是线性代数中最有用的分解。
核心概念
SVD 定义
任何矩阵 A(m x n)都可以分解为:
A = U S V^T
- U(m x m):左奇异向量,A A^T 的特征向量
- S(m x n):对角矩阵,奇异值按降序排列
- V^T(n x n):右奇异向量,A^T A 的特征向量
import numpy as np
A = np.random.randn(4, 3)
U, S, Vt = np.linalg.svd(A, full_matrices=False)
print(f"U: {U.shape}")
print(f"S: {S.shape}")
print(f"Vt: {Vt.shape}")
# 验证重建
reconstructed = U @ np.diag(S) @ Vt
print(f"重建误差: {np.allclose(A, reconstructed)}")
奇异值的含义
奇异值告诉你矩阵在每个”方向”上的强度。大的奇异值对应重要的方向,小的奇异值对应噪声。
# 奇异值的能量分布
total_energy = np.sum(S ** 2)
cumulative = np.cumsum(S ** 2) / total_energy
for i, (s, c) in enumerate(zip(S, cumulative)):
print(f"奇异值 {i}: {s:.4f}, 累积能量: {c:.2%}")
截断 SVD
只保留最大的 k 个奇异值,实现低秩近似。
def truncated_svd(A, k):
"""截断 SVD:保留前 k 个奇异值"""
U, S, Vt = np.linalg.svd(A, full_matrices=False)
return U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :]
# 创建低秩矩阵加噪声
rank = 3
A_clean = np.random.randn(100, 100)[:, :rank] @ np.random.randn(rank, 100)
A_noisy = A_clean + 0.1 * np.random.randn(100, 100)
# 截断 SVD 去噪
A_denoised = truncated_svd(A_noisy, rank)
print(f"去噪误差: {np.mean((A_clean - A_denoised) ** 2):.6f}")
print(f"噪声误差: {np.mean((A_clean - A_noisy) ** 2):.6f}")
图像压缩
SVD 可以压缩图像:只保留最重要的奇异值。
def compress_image(image, k):
"""使用截断 SVD 压缩图像"""
compressed = np.zeros_like(image)
for c in range(image.shape[2]):
compressed[:, :, c] = truncated_svd(image[:, :, c], k)
return compressed
# 压缩比
original_size = 100 * 100 # 假设 100x100 灰度图
compressed_size = 100 * k + k + k * 100 # U[:, :k] + S[:k] + Vt[:k, :]
ratio = original_size / compressed_size
print(f"k={k}: 压缩比 {ratio:.1f}:1")
伪逆
SVD 用于计算 Moore-Penrose 伪逆,即使矩阵不可逆也能求解线性系统。
def pseudo_inverse(A):
"""使用 SVD 计算伪逆"""
U, S, Vt = np.linalg.svd(A, full_matrices=False)
S_inv = np.diag(1.0 / S)
return Vt.T @ S_inv @ U.T
# 欠定系统(方程少于未知数)
A = np.random.randn(3, 5)
b = np.random.randn(3)
x = pseudo_inverse(A) @ b
print(f"解: {x}")
print(f"验证: A @ x ≈ b: {np.allclose(A @ x, b)}")
LSA(潜在语义分析)
LSA 使用 SVD 发现文档-词矩阵中的潜在主题。
from sklearn.feature_extraction.text import CountVectorizer
docs = ["machine learning algorithms",
"deep learning neural networks",
"natural language processing",
"computer vision detection"]
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(docs).toarray()
U, S, Vt = np.linalg.svd(X, full_matrices=False)
# Vt 的行是潜在主题
k = 2
topics = Vt[:k, :]
terms = vectorizer.get_feature_names_out()
for i, topic in enumerate(topics):
top_terms = [terms[j] for j in topic.argsort()[-5:]]
print(f"主题 {i}: {top_terms}")
PCA 与 SVD 的关系
PCA 就是中心化数据的 SVD。对中心化矩阵做 SVD,右奇异向量就是主成分方向。
X = np.random.randn(100, 10)
X_centered = X - X.mean(axis=0)
# PCA 方法
cov = X_centered.T @ X_centered / (len(X) - 1)
eigenvalues, eigenvectors = np.linalg.eigh(cov)
# SVD 方法
U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)
# 验证:SVD 的右奇异向量 = PCA 的特征向量
print(f"方向一致: {np.allclose(np.abs(Vt.T), np.abs(eigenvectors[:, ::-1]))}")
实际应用
| 应用 | SVD 的角色 |
|---|---|
| 图像压缩 | 低秩近似 |
| 推荐系统 | 矩阵补全(Netflix 大奖) |
| LSA | 发现文本中的潜在主题 |
| PCA | 等价于中心化数据的 SVD |
| 伪逆 | 求解最小二乘问题 |
| 去噪 | 截断小奇异值 |
练习
- 创建已知秩的矩阵,加噪声,用截断 SVD 恢复,比较不同 k 值的效果
- 对灰度图像进行 SVD 压缩,观察 k=10, 50, 100 时的视觉质量
- 用伪逆求解超定系统(方程多于未知数),与最小二乘解对比
- 对文档集合运行 LSA,解释发现的主题