import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image
- Four Fundamental Subspaces
- Skeleton decomposition
- Low-rank Approximation
- SVD and its Applications
4.1 Geometric interpretation
4.2 Image Reconstruction
Four Fundamental Subspaces
Let A = \begin{bmatrix} 1 & -1 \\ 2 & 1 \\ -1 & 3 \end{bmatrix}
- Find the column space \text{col}(A) and null space \text{null}(A) of A.
- Determine if \vec{b} = \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} \in \text{col}(A).
- Determine if \vec{u} = \begin{bmatrix} 1 \\ 1 \end{bmatrix} \in \text{null}(A).
def swap(matrix, row1, row2):
"""Swap two rows in a matrix."""
= matrix[[row2, row1]]
matrix[[row1, row2]] return matrix
def scale(matrix, row, scalar):
"""Multiply all entries in a specified row by a scalar."""
*= scalar
matrix[row] return matrix
def replace(matrix, row1, row2, scalar):
"""Replace a row by itself plus a scalar multiple of another row."""
+= scalar * matrix[row2]
matrix[row1] return matrix
= np.array([[1, -1, 0], [2, 1, 0], [-1, 3, 0]], dtype=float) M
= replace(M.copy(), 1, 0, -2)
M1 = replace(M1, 2, 0, 1)
M2 = scale(M2, 1, 1/3)
M3 = replace(M3, 2, 1, -2)
M4
print("Reduced Matrix M4:")
print(M4)
= np.array([[1, -1, 1], [2, 1, 2], [-1, 3, 1]], dtype=float)
M_augm M
# Row Reduction Example
= replace(M_augm.copy(), 1, 0, -2)
M1 = replace(M1, 2, 0, 1)
M2 = scale(M2, 1, 1/3)
M3 = replace(M3, 2, 1, -2)
M4 M4
A = \begin{bmatrix} 1 & -1 \\ 2 & 1 \\ -1 & 3 \end{bmatrix} \sim \begin{bmatrix} 1 & -1 \\ 0 & 3 \\ 0 & 2 \end{bmatrix} \sim \begin{bmatrix} 1 & -1 \\ 0 & 1 \\ 0 & 0 \end{bmatrix} \sim \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 0 & 0 \end{bmatrix}
\mathrm{im}(A) = \mathrm{col}(A) = \mathrm{span}\left\{ \begin{bmatrix} 1 \\ 2 \\ -1 \end{bmatrix}, \begin{bmatrix} -1 \\ 1 \\ 3 \end{bmatrix} \right\}
\mathrm{im}(A^T) = \mathrm{col}(A^T) = \mathrm{span}\left\{ \begin{bmatrix} 1 \\ 0 \end{bmatrix}, \begin{bmatrix} 0 \\ 1 \end{bmatrix} \right\}
\mathrm{ker}(A) = \mathrm{null}(A) = \{ 0 \}
\mathrm{ker}(A^T) = \mathrm{null}(A^T) = \mathrm{span}\left\{ \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix} \right\}
Skeleton decomposition
A = \sum_{\alpha = 1}^r U_{\alpha} V_{\alpha}^T
##Low-rank Approximation
= Image.open('./sk_campus_img.jpg').convert('L')
img = np.array(img)
img_array = img_array.shape
original_shape
=(8, 4))
plt.figure(figsize='gray')
plt.imshow(img_array, cmap"Original Image")
plt.title('off')
plt.axis( plt.show()
= np.linalg.svd(img_array, full_matrices=False) # economy SVD
U, S, Vt U.shape, S.shape, Vt.shape
=(12, 4))
plt.figure(figsize
1, 2, 1)
plt.subplot(/ S[0])
plt.semilogy(S r"$k$")
plt.xlabel(r"$\sigma_k / \sigma_1$")
plt.ylabel(r"$\sigma_k / \sigma_1$")
plt.title(
plt.grid()
= np.cumsum(S**2) / np.sum(S**2)
cumulative_energy 1, 2, 2)
plt.subplot(
plt.plot(cumulative_energy)r"$k$")
plt.xlabel(r"Cumulative Energy")
plt.ylabel(r"$(\sum_{i=1}^k \sigma_i) / \sum_{i=0}^n \sigma_i)$")
plt.title(
plt.grid()
plt.show()
def reconstruct_image(k):
return (U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :])
= [5, 20, 50, 100, original_shape[1]]
ranks =(15, 5))
plt.figure(figsize
for i, rank in enumerate(ranks, 1):
1, len(ranks), i)
plt.subplot(= reconstruct_image(rank)
recon_img ='gray')
plt.imshow(recon_img, cmapf'Rank {rank}') if rank!= original_shape[1] else plt.title(f'Original Image')
plt.title('off')
plt.axis(
"Low-Rank Approximations of Image")
plt.suptitle( plt.show()
SVD and its Applications
Singular Value Decomposition (SVD) is a versatile tool in numerical linear algebra, implemented in many programming languages, typically relying on the LAPACK (Linear Algebra Package) library written in Fortran for its underlying computations.
Geometric interpretation
def plot_transformed_circle_and_vectors(A, plot_singular_vectors=False, singular_values=None, singular_vectors=None,
='black', vector_colors=['blue', 'deeppink'],
circle_color=['red', 'green'],
singular_vector_colors=[r'$\sigma_1 u_1$', r'$\sigma_2 u_2$'],
singular_labels=0.2, xlim=(-8, 8), ylim=(-8, 8)):
label_offset= np.linspace(0, 2 * np.pi, 300)
theta = np.vstack((np.cos(theta), np.sin(theta)))
unit_circle
= A @ unit_circle
transformed_circle
0, :], transformed_circle[1, :], color=circle_color, alpha=0.5)
plt.plot(transformed_circle[
= A @ np.array([1, 0])
e1_transformed = A @ np.array([0, 1])
e2_transformed
for i, vec in enumerate([e1_transformed, e2_transformed]):
= vector_colors[i]
color 0, 0, vec[0], vec[1], angles='xy', scale_units='xy', scale=1, color=color)
plt.quiver(
if plot_singular_vectors and singular_values is not None and singular_vectors is not None:
for i, (sigma, vec) in enumerate(zip(singular_values, singular_vectors.T)):
= sigma * vec
vec_scaled = singular_vector_colors[i]
color = singular_labels[i]
label 0, 0, vec_scaled[0], vec_scaled[1], angles='xy', scale_units='xy', scale=1, color=color)
plt.quiver(0] * (1 + label_offset), vec_scaled[1] * (1 + label_offset), label, color=color, fontsize=12)
plt.text(vec_scaled[
=0, color='black', lw=1)
plt.axvline(x=0, color='black', lw=1)
plt.axhline(y'x')
plt.xlabel('y')
plt.ylabel('equal', adjustable='box')
plt.gca().set_aspect(
plt.xlim(xlim) plt.ylim(ylim)
= np.array([[1, 3], [4, 5]])
A
= np.linalg.svd(A)
U, D, Vt
print('Unit circle (before transformation):')
2), xlim=(-1.5, 1.5), ylim=(-1.5, 1.5))
plot_transformed_circle_and_vectors(np.eye(
plt.show()
print('1st rotation by V (right singular vectors):')
=(-1.5, 1.5), ylim=(-1.5, 1.5))
plot_transformed_circle_and_vectors(Vt.T, xlim
plt.show()
print('Scaling by D:')
= np.diag(D) @ Vt
scaling_matrix =(-8, 8), ylim=(-8, 8))
plot_transformed_circle_and_vectors(scaling_matrix, xlim
plt.show()
print('2nd rotation by U (final transformation by A):')
= U @ np.diag(D) @ Vt
final_transformation =(-8, 8), ylim=(-8, 8))
plot_transformed_circle_and_vectors(final_transformation, xlim plt.show()
= np.array([[1, 3], [4, 5]])
A
= np.linalg.svd(A)
U, D, Vt
print("Transformed unit circle, basis vectors, and singular vectors:")
plot_transformed_circle_and_vectors(A,=True,
plot_singular_vectors=D,
singular_values=U,
singular_vectors=[r'$\sigma_1 u_1$', r'$\sigma_2 u_2$'])
singular_labels plt.show()
Applications in Image Reconstruction
Matrix completion, commonly used for filling missing data, can be applied to image and recommendation systems. A well-known example is movie recommendation systems, where a ratings matrix is often only partially filled, as users have not rated every movie. To provide accurate recommendations, we aim to predict these missing ratings.
This task is feasible because user ratings tend to follow patterns, meaning the ratings matrix is often low-rank; only a limited amount of information is needed to approximate it well.
A similar approach applies to images, where pixel values often depend on neighboring pixels, making low-rank approximations effective for reconstructing images with missing or corrupted data.
SVD in Facial Recognition: Eigenfaces
The “Eigenfaces for Recognition” paper introduced a novel approach to facial recognition. Unlike earlier methods that focused on detecting individual features (e.g., eyes or nose), Eigenfaces uses SVD to extract and encode essential information from face images. This encoding allows for efficient comparisons between faces by compressing the most relevant facial information into a low-dimensional representation. This method paved the way for data-driven approaches in face recognition, relying on similarities within the encoded space rather than feature-by-feature comparison.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
import cv2
from sklearn.datasets import fetch_lfw_people
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.neural_network import MLPClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import fetch_lfw_people
= fetch_lfw_people(min_faces_per_person=100)
lfw_dataset = lfw_dataset.data, lfw_dataset.target, lfw_dataset.target_names
X, y, target_names = lfw_dataset.images.shape[1:3]
h, w print(f"Number of samples: {X.shape[0]}, Image size: {h}x{w}, Unique classes: {len(target_names)}")
= train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) X_train, X_test, y_train, y_test
= np.linalg.svd(X_train, full_matrices=False)
U, S, VT
= 100
num_components = VT[:num_components, :]
face_space
= X_train @ face_space.T
X_train_transformed = X_test @ face_space.T
X_test_transformed
=(8, 4))
plt.figure(figsizelen(S)), S / S[0], marker="o")
plt.semilogy(np.arange("$\sigma_k / \sigma_1$")
plt.title("$k$")
plt.xlabel("$\sigma_k / \sigma_1$")
plt.ylabel(
plt.grid() plt.show()
def plot_reconstructed_images(original, transformed, face_space, h, w, index=0):
= transformed[index] @ face_space
reconstructed = plt.subplots(1, 2, figsize=(8, 4))
fig, ax 0].imshow(original[index].reshape(h, w), cmap="gray")
ax[0].set_title("Original Image")
ax[1].imshow(reconstructed.reshape(h, w), cmap="gray")
ax[1].set_title("Reconstructed Image")
ax[
plt.show()
=0)
plot_reconstructed_images(X_train, X_train_transformed, face_space, h, w, index
= KNeighborsClassifier().fit(X_train_transformed, y_train)
clf_knn = MLPClassifier(hidden_layer_sizes=(1024,), batch_size=256, early_stopping=True).fit(X_train_transformed, y_train)
clf_mlp
= clf_knn.predict(X_test_transformed)
y_pred_knn = clf_mlp.predict(X_test_transformed)
y_pred_mlp print("k-NN Classifier Report:\n", classification_report(y_test, y_pred_knn, target_names=target_names))
print("MLP Classifier Report:\n", classification_report(y_test, y_pred_mlp, target_names=target_names))
It might seem discouraging, but it’s worthwhile to check if the data is imbalanced and go through the steps again (exercise).