determin.ant
08-svd / 8.4

Module 8: SVD

Image Compression

Where you'll see this: Netflix's early recommendation engine was built on SVD of a user–movie rating matrix. The low-rank approximation captures the dominant patterns (genre taste, mood preferences) while ignoring noise. The same idea applies to image compression, document retrieval, and genomics data.

SVD isn't just for decomposing matrices — it lets you throw away the unimportant parts and keep only the structure that matters.

Low-rank approximation

Any matrix can be written as a sum of rank-1 matrices:

A=σ1u1v1T+σ2u2v2T+A = \sigma_1 \mathbf{u}_1 \mathbf{v}_1^T + \sigma_2 \mathbf{u}_2 \mathbf{v}_2^T + \cdots

Each term σiuiviT\sigma_i \mathbf{u}_i \mathbf{v}_i^T is a rank-1 matrix — an outer product scaled by the singular value. The singular values are sorted largest-first, so the first few terms capture the most "energy" in the matrix.

Truncated SVD

Keep only the first k terms and you get the rank-k approximation:

Ak=i=1kσiuiviTA_k = \sum_{i=1}^{k} \sigma_i \mathbf{u}_i \mathbf{v}_i^T

By the Eckart–Young theorem, Aₖ is the closest rank-k matrix to A (in Frobenius norm). There's no better way to compress a matrix to rank k.

Storage savings

An m×n matrix has mn values. A rank-k approximation stores k copies of (u vector, v vector, σ scalar) = k(m + n + 1) values. For large matrices with small k, the savings are dramatic.

An image is a matrix of pixel values. A rank-k approximation captures the k dominant "patterns" in the image. Low k = blurry but much smaller. High k = sharp but large. The tradeoff is always quality vs. size.

Try it

The interactive shows an 8×8 grayscale matrix treated as an image. The slider adds more rank-1 pieces. At rank 1 you see the roughest structure; by full rank it's exact. Watch how quickly the approximation improves with each additional piece.

Image compression via truncated SVD:
import numpy as np

# img is an m×n grayscale matrix
U, s, Vh = np.linalg.svd(img, full_matrices=False)

def compress(k):
    return U[:, :k] @ np.diag(s[:k]) @ Vh[:k, :]

# k=10 often retains most of the visual quality
approx = compress(10)
compression_ratio = (img.size) / (k * (U.shape[0] + Vh.shape[1] + 1))
Matrix Norms
original (rank 4)
rank-1 approx

Original storage: 64 values

Rank-1 storage: 17 values (1 × (u + v + σ))

73% smaller · · with some loss

Each rank-1 piece is one singular value + two vectors. Drag the slider to add more pieces.