SVD 应用笔记:图像压缩、推荐系统、PCA 与 SVM Kernel

这篇笔记整理 SVD 在机器学习中的几个典型用法:低秩近似做图像压缩、矩阵分解做推荐系统、PCA 做降维,以及 SVM 中常见 kernel 的选择。主线是同一个:把高维数据表示成更少、更重要的方向。

1. SVD 的直觉

对一个矩阵

XRm×n\mathbf{X}\in \mathbb{R}^{m\times n}

奇异值分解写作:

X=UΣVT\mathbf{X}=\mathbf{U}\mathbf{\Sigma}\mathbf{V}^{T}

其中:

  • U\mathbf{U}:左奇异向量,描述行空间方向。
  • V\mathbf{V}:右奇异向量,描述列空间方向。
  • Σ\mathbf{\Sigma}:奇异值矩阵,对角线上是 σ1,σ2,\sigma_1,\sigma_2,\ldots,通常按从大到小排列。

大的奇异值对应数据中最重要的结构,小的奇异值往往对应细节、噪声或较弱的变化。因此,只保留前 kk 个奇异值,就可以得到一个低秩近似:

XUkΣkVkT\mathbf{X} \approx \mathbf{U}_k\mathbf{\Sigma}_k\mathbf{V}_k^T

其中:

Uk=[u1,,uk],Vk=[v1,,vk]\mathbf{U}_k=[\mathbf{u}_1,\ldots,\mathbf{u}_k], \quad \mathbf{V}_k=[\mathbf{v}_1,\ldots,\mathbf{v}_k]

这就是很多 SVD 应用的核心。

2. 图像压缩:低秩近似

灰度图像可以看成一个矩阵,每个像素值就是矩阵中的一个元素。彩色图像本质上是三通道数组,可以把 RGB 分别处理,也可以先转成灰度图。

对灰度图矩阵 X\mathbf{X} 做 SVD:

X=UΣVT\mathbf{X}=\mathbf{U}\mathbf{\Sigma}\mathbf{V}^T

只保留前 kk 个奇异值:

Xk=UkΣkVkT\mathbf{X}_k = \mathbf{U}_k\mathbf{\Sigma}_k\mathbf{V}_k^T

kk 越小,压缩越强,但图像越模糊;kk 越大,保留的信息越多,但存储成本也越高。

原图

原始灰度图

Rank 5

rank 5 低秩近似

Rank 5 只保留很少的主方向,所以能看出大概轮廓,但人脸和背景细节已经明显丢失。

Rank 25

rank 25 低秩近似

Rank 25 已经能恢复主要结构,人脸、栏杆和海面轮廓都清楚很多,但仍然有明显的模糊和纹理损失。

Rank 50

rank 50 低秩近似

Rank 50 保留了更多奇异值,图像细节明显增强,但相比原图仍然有压缩痕迹。

Python 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import svd


def compress_image(A, k):
U, s, Vt = svd(A, full_matrices=False)
A_k = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
return A_k


image = Image.open("your_image_path.png").convert("L")
A = np.array(image)

for k in [5, 25, 50]:
A_k = compress_image(A, k)
plt.imshow(A_k, cmap="gray")
plt.title(f"rank-{k}")
plt.axis("off")
plt.show()

这段代码的关键是:

Ak=U:,1:kΣ1:k,1:kV1:k,:T\mathbf{A}_k = \mathbf{U}_{:,1:k} \mathbf{\Sigma}_{1:k,1:k} \mathbf{V}_{1:k,:}^{T}

也就是只拿前 kk 个奇异值和对应方向重构图片。

3. 推荐系统:矩阵分解

推荐系统里常见的数据形式是用户-物品评分矩阵:

RRm×n\mathbf{R}\in \mathbb{R}^{m\times n}

其中 mm 是用户数,nn 是物品数,RijR_{ij} 表示用户 ii 对物品 jj 的评分。

但真实推荐数据通常很稀疏:大部分用户没有给大部分物品打分。矩阵分解的目标是学习:

RUVT\mathbf{R}\approx \mathbf{U}\mathbf{V}^{T}

其中:

  • URm×k\mathbf{U}\in \mathbb{R}^{m\times k}:用户隐向量矩阵。
  • VRn×k\mathbf{V}\in \mathbb{R}^{n\times k}:物品隐向量矩阵。
  • kk 是隐因子数量。

预测评分为:

R^ij=uivjT\hat{R}_{ij} = \mathbf{u}_i\mathbf{v}_j^T

训练目标常写成:

minU,V(i,j)Ω(RijuivjT)2+λ(UF2+VF2)\min_{\mathbf{U},\mathbf{V}} \sum_{(i,j)\in \Omega} (R_{ij}-\mathbf{u}_i\mathbf{v}_j^T)^2 +\lambda(\|\mathbf{U}\|_F^2+\|\mathbf{V}\|_F^2)

其中 Ω\Omega 是已知评分的位置,λ\lambda 是正则化系数,用来减少过拟合。

4. ALS:Alternating Least Squares

ALS 的思想是交替优化:

  1. 固定 U\mathbf{U},求最优 V\mathbf{V}
  2. 固定 V\mathbf{V},求最优 U\mathbf{U}
  3. 重复直到收敛。

固定 U\mathbf{U} 时,对第 jj 个物品向量 vj\mathbf{v}_j,有:

vj=(UΩjTUΩj+λI)1UΩjTrj\mathbf{v}_j = (\mathbf{U}_{\Omega_j}^{T}\mathbf{U}_{\Omega_j}+\lambda\mathbf{I})^{-1} \mathbf{U}_{\Omega_j}^{T}\mathbf{r}_j

其中 Ωj\Omega_j 表示给物品 jj 打过分的用户集合。

一个简化版 ALS 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import numpy as np


def als(R, num_factors=3, num_iterations=10, lambda_reg=0.1):
num_users, num_items = R.shape
U = np.random.rand(num_users, num_factors)
V = np.random.rand(num_items, num_factors)

for _ in range(num_iterations):
for i in range(num_users):
rated = R[i, :] > 0
if rated.any():
V_i = V[rated]
R_i = R[i, rated]
U[i] = np.linalg.solve(
V_i.T @ V_i + lambda_reg * np.eye(num_factors),
V_i.T @ R_i
)

for j in range(num_items):
rated = R[:, j] > 0
if rated.any():
U_j = U[rated]
R_j = R[rated, j]
V[j] = np.linalg.solve(
U_j.T @ U_j + lambda_reg * np.eye(num_factors),
U_j.T @ R_j
)

return U, V


R = np.array([
[4, 0, 0, 5, 1],
[5, 5, 4, 0, 0],
[0, 3, 0, 4, 0],
[0, 0, 5, 3, 2],
])

U, V = als(R)
R_pred = U @ V.T
print(R_pred)

ALS 的优点是每一步都是一个相对清楚的最小二乘问题;缺点是如果矩阵很大,求解线性方程也会有计算成本。

5. Gradient Descent 做矩阵分解

也可以用梯度下降直接优化:

minU,V(i,j)Ω[(RijuivjT)2+λ(ui2+vj2)]\min_{\mathbf{U},\mathbf{V}} \sum_{(i,j)\in \Omega} [(R_{ij}-\mathbf{u}_i\mathbf{v}_j^T)^2 +\lambda(\|\mathbf{u}_i\|^2+\|\mathbf{v}_j\|^2)]

令误差:

eij=RijuivjTe_{ij}=R_{ij}-\mathbf{u}_i\mathbf{v}_j^T

更新方式可以写成:

uiui+α(eijvjλui)\mathbf{u}_i \leftarrow \mathbf{u}_i +\alpha(e_{ij}\mathbf{v}_j-\lambda\mathbf{u}_i)

vjvj+α(eijuiλvj)\mathbf{v}_j \leftarrow \mathbf{v}_j +\alpha(e_{ij}\mathbf{u}_i-\lambda\mathbf{v}_j)

其中 α\alpha 是学习率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np


def matrix_factorization_gd(R, num_factors=3, num_iterations=100, alpha=0.01, lambda_reg=0.1):
num_users, num_items = R.shape
U = np.random.rand(num_users, num_factors)
V = np.random.rand(num_items, num_factors)

for _ in range(num_iterations):
for i in range(num_users):
for j in range(num_items):
if R[i, j] > 0:
error = R[i, j] - U[i] @ V[j].T
U_old = U[i].copy()
U[i] += alpha * (error * V[j] - lambda_reg * U[i])
V[j] += alpha * (error * U_old - lambda_reg * V[j])

return U, V

梯度下降更像深度学习里的训练过程,适合扩展到更复杂的模型。

6. WALS:Weighted ALS

普通 ALS 只在已知评分上训练。如果把未知位置简单当成 0,可能会引入误导,因为“没评分”不等于“不喜欢”。

WALS 引入权重矩阵:

wij={1,Rij 已知0,Rij 未知w_{ij} = \begin{cases} 1,& R_{ij}\ \text{已知}\\ 0,& R_{ij}\ \text{未知} \end{cases}

目标函数变为:

minU,Vi,jwij(RijuivjT)2+λ(UF2+VF2)\min_{\mathbf{U},\mathbf{V}} \sum_{i,j} w_{ij}(R_{ij}-\mathbf{u}_i\mathbf{v}_j^T)^2 +\lambda(\|\mathbf{U}\|_F^2+\|\mathbf{V}\|_F^2)

固定 U\mathbf{U} 时:

vj=(UTWjU+λI)1UTWjrj\mathbf{v}_j = (\mathbf{U}^T\mathbf{W}_j\mathbf{U}+\lambda\mathbf{I})^{-1} \mathbf{U}^T\mathbf{W}_j\mathbf{r}_j

固定 V\mathbf{V} 时:

ui=(VTWiV+λI)1VTWiri\mathbf{u}_i = (\mathbf{V}^T\mathbf{W}_i\mathbf{V}+\lambda\mathbf{I})^{-1} \mathbf{V}^T\mathbf{W}_i\mathbf{r}_i

WALS 的核心是:让模型知道哪些评分可信,哪些位置只是缺失。

7. PCA 与 SVD

PCA,Principal Component Analysis,主成分分析,是一种经典降维方法。它希望找到新的坐标轴,使得:

  • 数据在前几个方向上的方差尽可能大。
  • 不同方向之间的协方差尽可能小。
  • 用更少的维度保留尽可能多的信息。

设数据矩阵为:

X=[a1a2amb1b2bm]\mathbf{X} = \begin{bmatrix} a_1 & a_2 & \cdots & a_m\\ b_1 & b_2 & \cdots & b_m \end{bmatrix}

协方差矩阵可以写作:

1mXXT=[Cov(a,a)Cov(a,b)Cov(b,a)Cov(b,b)]\frac{1}{m}\mathbf{X}\mathbf{X}^T = \begin{bmatrix} \mathrm{Cov}(a,a) & \mathrm{Cov}(a,b)\\ \mathrm{Cov}(b,a) & \mathrm{Cov}(b,b) \end{bmatrix}

PCA 本质上就是找这个协方差矩阵的特征向量,把数据投影到方差最大的方向。

用 SVD 做 PCA 的常见写法:

1
2
3
4
5
6
7
8
9
10
import numpy as np
from sklearn.datasets import load_iris


iris = load_iris()
X = iris.data
X_centered = X - X.mean(axis=0)

U, S, Vt = np.linalg.svd(X_centered / np.sqrt(X.shape[0] - 1), full_matrices=False)
Y = X_centered @ Vt.T

这里 Vt.T 的列就是主成分方向,Y 是原始数据投影后的低维表示。

8. SVM Kernel 简记

SVM 的核心思想是找到一个分类边界,让不同类别之间的 margin 尽量大。当数据在原始空间里不容易线性分开时,可以用 kernel trick 把数据映射到更高维的特征空间。

常见 kernel:

Linear Kernel

K(x,y)=xTyK(\mathbf{x},\mathbf{y})=\mathbf{x}^T\mathbf{y}

Polynomial Kernel

K(x,y)=(xTy+c)dK(\mathbf{x},\mathbf{y}) = (\mathbf{x}^T\mathbf{y}+c)^d

Gaussian / RBF Kernel

K(x,y)=exp(xy22σ2)K(\mathbf{x},\mathbf{y}) = \exp\left( -\frac{\|\mathbf{x}-\mathbf{y}\|^2}{2\sigma^2} \right)

Sigmoid Kernel

K(x,y)=tanh(αxTy+c)K(\mathbf{x},\mathbf{y}) = \tanh(\alpha\mathbf{x}^T\mathbf{y}+c)

一个粗略经验:

  • 特征维度很高、样本数相对少时,可以先试 linear kernel。
  • 特征维度较低、样本数较多、边界明显非线性时,可以试 RBF kernel。
  • Polynomial kernel 对参数更敏感,通常需要更细的调参。

SVR,Support Vector Regression,是 SVM 的回归版本:

1
2
3
4
5
from sklearn.svm import SVR


model = SVR(kernel="rbf", gamma="scale", C=1.0)
model.fit(X_train, y_train)

9. 总结

这篇笔记可以连成一条线:

  1. SVD 把矩阵拆成重要方向和奇异值。
  2. 低秩近似用前 kk 个奇异值保留主要信息,可以做图像压缩。
  3. 推荐系统矩阵分解用 UVT\mathbf{U}\mathbf{V}^T 学用户和物品的隐向量。
  4. PCA 可以通过 SVD 得到主成分方向。
  5. SVM kernel 解决的是“在什么空间里更容易分开数据”的问题。

从线性代数角度看,这些方法都在回答同一个问题:如何用更合适的坐标系和更少的维度表示数据。

SVD 应用笔记:图像压缩、推荐系统、PCA 与 SVM Kernel

https://richardf123.github.io/2026/06/24/svd-applications-notes/

作者

RichardF

发布于

2026-06-24

更新于

2026-06-24

许可协议