前置知识: AI工程

奇异值分解

3 minIntermediate

幂迭代法 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
伪逆求解最小二乘问题
去噪截断小奇异值

练习

  1. 创建已知秩的矩阵,加噪声,用截断 SVD 恢复,比较不同 k 值的效果
  2. 对灰度像进行 SVD 压缩,观察 k=10, 50, 100 时的视觉质量
  3. 用伪逆求解超定系统(方程多于未知数),与最小二乘解对比
  4. 对文档集合运行 LSA,解释发现的主题