Python实战:用NumPy手撕SVD分解(附完整代码与可视化)

在数据科学和机器学习领域,矩阵分解技术扮演着至关重要的角色。其中,奇异值分解(SVD)因其强大的数学性质和广泛的应用场景,成为每个开发者都应该掌握的利器。本文将带你从零开始,用NumPy实现完整的SVD分解流程,并通过可视化手段深入理解其数学本质。

1. SVD基础与数学原理

奇异值分解(Singular Value Decomposition)是线性代数中一种强大的矩阵分解方法。它将任意m×n的实数矩阵A分解为三个特殊矩阵的乘积:

A = UΣV^T

其中:

  • U是m×m的正交矩阵(左奇异向量)
  • Σ是m×n的对角矩阵(奇异值矩阵)
  • V是n×n的正交矩阵(右奇异向量)

关键数学性质

  • 奇异值σ₁ ≥ σ₂ ≥ ... ≥ σᵣ > 0(按降序排列)
  • 非零奇异值的数量等于矩阵的秩r
  • U的列向量是AAᵀ的特征向量
  • V的列向量是AᵀA的特征向量

提示:在实际计算中,我们通常通过求解对称矩阵AᵀA的特征分解来获得V和Σ,再推导出U

2. 从零实现SVD关键步骤

2.1 计算特征值与特征向量

首先我们需要计算对称矩阵AᵀA的特征分解:

import numpy as np

def svd(A):
    # 计算A的转置乘以A
    ATA = A.T @ A
    
    # 计算特征值和特征向量
    eigenvalues, eigenvectors = np.linalg.eig(ATA)
    
    # 确保特征值按降序排列
    idx = eigenvalues.argsort()[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]
    
    # 计算奇异值(取平方根并处理负值)
    singular_values = np.sqrt(np.abs(eigenvalues))
    
    return singular_values, eigenvectors

2.2 构造正交基矩阵

获得特征向量后,我们需要将其标准化并构造V矩阵:

def construct_V(eigenvectors):
    # 特征向量已经是正交的,只需确保归一化
    norms = np.linalg.norm(eigenvectors, axis=0)
    V = eigenvectors / norms
    return V

2.3 计算U矩阵

根据SVD的定义,我们可以通过以下方式计算U:

def construct_U(A, V, singular_values, r):
    # 计算前r个U向量
    U = np.zeros((A.shape[0], r))
    for i in range(r):
        if singular_values[i] > 1e-10:  # 避免除以0
            U[:, i] = A @ V[:, i] / singular_values[i]
    
    # 如果U的列数不足,用Gram-Schmidt过程补充
    if U.shape[1] < A.shape[0]:
        remaining = A.shape[0] - U.shape[1]
        Q, _ = np.linalg.qr(U, mode='complete')
        U = Q[:, :A.shape[0]]
    
    return U

3. 完整SVD实现与验证

将上述步骤整合成完整的SVD实现:

def my_svd(A, full_matrices=True):
    # 步骤1:计算AᵀA的特征分解
    ATA = A.T @ A
    eigenvalues, V = np.linalg.eig(ATA)
    
    # 排序特征值和特征向量
    idx = eigenvalues.argsort()[::-1]
    eigenvalues = eigenvalues[idx]
    V = V[:, idx]
    
    # 计算奇异值
    singular_values = np.sqrt(np.abs(eigenvalues))
    r = np.sum(singular_values > 1e-10)  # 有效秩
    
    # 构造Σ矩阵
    sigma = np.zeros((A.shape[0], A.shape[1]))
    np.fill_diagonal(sigma, singular_values)
    
    # 计算U矩阵
    U = np.zeros((A.shape[0], A.shape[0]))
    for i in range(r):
        U[:, i] = A @ V[:, i] / singular_values[i]
    
    # 补充U的剩余列(如果需要)
    if r < A.shape[0]:
        U[:, r:] = np.linalg.qr(U[:, :r], mode='complete')[0][:, r:]
    
    if not full_matrices:
        U = U[:, :r]
        sigma = sigma[:r, :r]
        V = V[:, :r]
    
    return U, sigma, V.T

验证我们的实现:

# 测试矩阵
A = np.array([[1, 2, 3], 
              [4, 5, 6], 
              [7, 8, 9]])

# 我们的实现
U, S, Vt = my_svd(A)

# 官方实现
U_official, S_official, Vt_official = np.linalg.svd(A)

print("我们的U矩阵:\n", U)
print("官方U矩阵:\n", U_official)
print("\n我们的奇异值:", np.diag(S))
print("官方奇异值:", S_official)

4. 可视化奇异值能量分布

理解奇异值的能量分布对于实际应用至关重要。我们可以通过可视化来直观展示:

import matplotlib.pyplot as plt

def plot_singular_values(A):
    _, S, _ = np.linalg.svd(A)
    
    plt.figure(figsize=(10, 6))
    plt.plot(S, 'o-', linewidth=2)
    plt.title('奇异值能量分布')
    plt.xlabel('奇异值索引')
    plt.ylabel('奇异值大小')
    plt.grid(True)
    
    # 计算累积能量
    cumulative_energy = np.cumsum(S) / np.sum(S)
    plt.figure(figsize=(10, 6))
    plt.plot(cumulative_energy, 'o-', linewidth=2)
    plt.title('奇异值累积能量')
    plt.xlabel('奇异值数量')
    plt.ylabel('累积能量比例')
    plt.grid(True)
    plt.axhline(y=0.9, color='r', linestyle='--')
    plt.show()

# 生成随机矩阵测试
random_matrix = np.random.randn(100, 50)
plot_singular_values(random_matrix)

典型分析结果

  1. 前几个奇异值通常包含矩阵的大部分能量
  2. 当奇异值快速衰减时,意味着矩阵可以被低秩近似
  3. 90%能量线(图中红色虚线)可以帮助确定有效秩

5. 工程实践技巧与优化

5.1 数值稳定性处理

在实际实现中,我们需要考虑数值稳定性:

def stable_svd(A):
    # 添加小扰动防止奇异矩阵
    epsilon = 1e-12
    A_stable = A + epsilon * np.random.randn(*A.shape)
    return np.linalg.svd(A_stable)

5.2 稀疏矩阵优化

对于大型稀疏矩阵,可以使用截断SVD:

from scipy.sparse.linalg import svds

def truncated_svd(A, k=10):
    # 只计算前k个奇异值和向量
    return svds(A, k=k)

5.3 性能对比

不同实现的性能差异:

方法 时间复杂度 适用场景
完整SVD O(min(mn², m²n)) 精确分解
随机SVD O(mnk) 大型矩阵近似
幂迭代法 O(mnk) 前k个奇异值
import time

def benchmark_svd(A, k=10):
    methods = {
        '完整SVD': lambda: np.linalg.svd(A),
        '随机SVD': lambda: svds(A, k=k),
        '我们的实现': lambda: my_svd(A)
    }
    
    results = {}
    for name, method in methods.items():
        start = time.time()
        method()
        results[name] = time.time() - start
    
    return results

6. 实际应用案例

6.1 图像压缩

利用SVD进行图像压缩的完整流程:

from PIL import Image

def compress_image(image_path, k=50):
    # 读取图像
    img = Image.open(image_path).convert('L')  # 转为灰度
    img_array = np.array(img, dtype=float)
    
    # 执行SVD
    U, S, Vt = np.linalg.svd(img_array)
    
    # 重建图像
    reconstructed = U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :]
    
    # 显示结果
    plt.figure(figsize=(10, 5))
    plt.subplot(1, 2, 1)
    plt.title('原始图像')
    plt.imshow(img_array, cmap='gray')
    
    plt.subplot(1, 2, 2)
    plt.title(f'压缩图像 (k={k})')
    plt.imshow(reconstructed, cmap='gray')
    plt.show()
    
    # 计算压缩率
    original_size = img_array.size
    compressed_size = U[:, :k].size + S[:k].size + Vt[:k, :].size
    print(f'压缩率: {compressed_size/original_size:.2%}')

# 使用示例
compress_image('example.jpg', k=30)

6.2 推荐系统

基于SVD的简单推荐系统实现:

def recommend_system(ratings, user_index, k=5):
    # ratings: 用户-物品评分矩阵
    # 执行SVD
    U, S, Vt = np.linalg.svd(ratings, full_matrices=False)
    
    # 降维表示
    user_factors = U[:, :k] @ np.diag(S[:k])
    item_factors = Vt[:k, :].T
    
    # 预测评分
    predicted = user_factors @ item_factors.T
    
    # 获取推荐
    user_ratings = predicted[user_index]
    recommended = np.argsort(-user_ratings)
    
    return recommended[:10]  # 返回前10个推荐

7. 常见问题与调试技巧

问题1:奇异值出现复数

解决方案:

# 确保输入矩阵是实数
A = np.real(A)
# 或者处理对称矩阵时使用eigh而不是eig
eigenvalues, eigenvectors = np.linalg.eigh(ATA)

问题2:重构误差大

检查步骤:

  1. 确认奇异值排序正确
  2. 验证U和V的正交性
  3. 检查数值精度是否足够

问题3:性能瓶颈

优化建议:

  • 使用np.linalg.svdfull_matrices=False参数
  • 对于大型矩阵,考虑使用随机SVD
  • 利用GPU加速(如CuPy库)
# 使用更高效的LAPACK驱动
U, S, Vt = np.linalg.svd(A, lapack_driver='gesdd')

在实现过程中,我发现最常遇到的陷阱是特征向量的符号不确定性。由于特征分解的结果中特征向量的符号可能是任意的,这会导致U和V矩阵的列向量符号不一致。一个实用的解决方案是在重构后对结果进行一致性检查,必要时对列向量进行符号校正。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐