SVD: Singular Vector Decomposition에 대해 다룬다. 각 수식이 어떤 의미를 가지고, 이미지 압축에 어떻게 사용되는지 설명한다. 본 글을 이해하기 위해 아래 개념을 이해하고 있어야 한다.
Vector: 크기와 방향을 가지는 양으로, 2차원 공간의 벡터는
Inversed matrix:
Orthogonal matrix: 모든 column 벡터가 직교하는 행렬로,
Diagonal matrix: 주대각 성분을 제외한 모든 값이 0으로
선형 변환:
Eigenvector의 특징
eigenvector는 고윳값으로 불리며, 선형 변환이 발생해도 방향을 유지하는 벡터를 말한다. eigenvector를 검색하면 다음과 같은 식이 나온다.
식만 봐서는 모르겠으니, 한 단계씩 해석해 보자.

하지만 같은 방향을 유지하는 벡터도 존재한다. 이 벡터를 eigenvector라고 부른다. 방향은 유지하고 있지만 크기는 바뀌었다. 따라서, 변형된 벡터를
>>> eigenvalues, eigenvectors = np.linalg.eig(transformation_matrix)
>>> eigenvectors
[[-0.85065081 -0.52573111]
[ 0.52573111 -0.85065081]]
>>> eigenvalues
[1.38196601 3.61803399]
다시 처음으로 돌아와
행렬 재구성
eigenvector와 eigenvalue를 알면, 변환 행렬

다시 말해, 행렬 A는 eigenvector와 eigenvalue로 분해할 수 있고, 이 값들을 통해 재구성할 수 있다. 이 개념이 이미지를 압축하고 재구성하는 과정에도 적용된다. 하지만 eigenvector는 n x n의 square matrix에만 적용 가능하다. 따라서 eigenvector 대신 singular vector가 등장한다.
SVD
orthogonal matrix
이번에는 orthogonal matrix

식을 조금 더 정리해보면,
글 초반에 소개했던 행렬의 특성을 이용해 정리한 식이다. 위 식을 행렬로 시각화하면 다음과 같다.

눈치챘다시피
식을
따라서
용어 정리
: Left Singular Vector : Right Singular Vector : Singular Value
정리하면,
A = np.array([[1, 2], [3, 4], [5, 6]])
# Perform SVD (A = U * Σ * V^T)
U, sigma, Vt = np.linalg.svd(A)
이미지 분해
이미지 가로 길이가
SVD에 재밌는 특징이 있는데 singular value가 큰 값부터 내림차순으로 나열되어 있다는 점이다.

이미지 행렬
가로 500, 세로 600의 600 x 500 행렬에 대해 실험을 해보았다.

당연히 벡터를 많이 사용할수록 이미지가 선명해진다.

singular value를 시각화해보면 n = 184에서 이미 singular value 총합의 80%를 넘어간다. 184개의 singular vector만으로도 이미지의 80%를 복원할 수 있다.

만약 600 x 500 행렬을 모두 사용하면 총 300,000개의 정보가 필요하다. 하지만, n = 200이라면 600 * n + n + n * 500으로 총 220,200개의 정보만 있으면 된다.
따라서, SVD를 이용해 정보를 압축할 수 있다.
SVD 관련 코드
SVD는 np.linalg.svd를 통해 계산한다. full_matrices 옵션은 불필요한 벡터를 저장할지 결정한다.
"""이미지 분해 및 재구성"""
import numpy as np
from PIL import Image
import os
image_path = "object4.jpg"
output_dir = "svd_images"
image = Image.open(image_path).convert("L")
image = np.array(image, dtype=np.float64)
# Singular Vector Decomposition (SVD)
# Image: (600, 500), S: (500,), Vt: (500, 500)
# U: (600, 500) when full_matrices=False
# U: (600, 600) when full_matrices=True
U, S, Vt = np.linalg.svd(image, full_matrices=False)
# 이미지 재구성
for n in range(1, len(S) + 1):
singular_values = np.zeros((U.shape[1], Vt.shape[0]))
np.fill_diagonal(singular_values, S[:n])
reconstructed = np.dot(
U[:, :n],
np.dot(singular_values[:n, :n], Vt[:n, :]),
)
output_image = np.clip(reconstructed, 0, 255).astype(np.uint8)
# 단계별 이미지 저장
if n % 10 == 0:
output_path = os.path.join(output_dir, f"{n}.png")
Image.fromarray(output_image).save(output_path)
"""Singular value 시각화"""
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
image = Image.open("object4.jpg").convert("L")
image = np.array(image, dtype=np.float64)
U, S, Vt = np.linalg.svd(image, full_matrices=False)
cumulative_sum = np.cumsum(S)
total_sum = np.sum(S)
threshold_percentage_1 = 0.5
threshold_percentage_2 = 0.8
threshold_1 = total_sum * threshold_percentage_1
threshold_2 = total_sum * threshold_percentage_2
threshold_index_1 = np.argmax(cumulative_sum >= threshold_1)
threshold_index_2 = np.argmax(cumulative_sum >= threshold_2)
plt.figure(figsize=(14, 6))
plt.plot(range(len(S)), S, label="Values")
plt.axvline(
x=threshold_index_1,
color="lightcoral",
linestyle="--",
label=f"{threshold_percentage_1 * 100}% Threshold (Index: {threshold_index_1})",
)
plt.axvline(
x=threshold_index_2,
color="red",
linestyle="--",
label=f"{threshold_percentage_2 * 100}% Threshold (Index: {threshold_index_2})",
)
plt.xlabel("Index")
plt.ylabel("Singular value")
plt.ylim(0, S[0] + 1)
plt.legend()
plt.grid(True)
plt.show()
참고자료