2 분 소요

PCA를 이용해서 이미지 압축을 진행해보자.

Code

[Notice] download here

이번 프로젝트는 PCA를 활용해서 이미지 압축 문제를 해결한다.

이론적으로 PCA의 개념을 이해하고 있다는 전제로 프로젝트를 수행한다.

PCA 개념 숙지는 여기!

데이터 불러오기

from scipy.linalg import eigh
import numpy as np
import matplotlib.pyplot as plt
def load_and_center_dataset(filename):
    f = np.load(filename)
    dc = f - np.mean(f, axis=0) # 정규화
    return dc

공분산 행렬 구하기

image

공분산데이터 분포에 대한 특징을 포함하는 행렬이다.

보다 자세한 정보는 여기를 참조하자.

def get_covariance(dataset):
    return np.dot(np.transpose(dataset), dataset) / (len(dataset) - 1)

고유값 분해(Eigendecomposition)

고유값 분해는 정방행렬을 대상으로 적용할 수 있는 기법이다.

만약 우리가 다루는 행렬이 정방행렬이 아닌 직각행렬이라면 고유값 분해대신 특이값 분해(singular value decomposition)를 사용하면 된다.

이 또한 보다 자세한 정보는 여기를 참조하자.

def get_eig(S, m):
    d = len(S)
    w, v = eigh(S, subset_by_index=[d-m, d-1]) # get largest m eigen-values/vectors
    eigh_dict = dict()
    for i in range(m):
        eigh_dict[w[i]] = v[:,i] # save columns of eigenvectors
    wsrt = np.sort(w)[::-1] # eigenvalues in decreasing order
    vsrt = v.copy()
    for i in range(m):
        vsrt[:,i] = eigh_dict[wsrt[i]] # rearrangement
    return np.diag(wsrt), vsrt

파이썬에서는 ‘scipy.linalg import eigh‘를 활용해서 손쉽게 고유값 분해를 처리할 수 있다.

해당 라이브러리의 ‘eigh’ 함수를 통해 고유값과 고유벡터를 얻을 수 있다.

  • subset_by_index’: 불러오는 고유값과 고유벡터 수를 지정한다.

공분산 행렬에 고유값 분해를 취하여 얻는 고유벡터들 중에서 그 크기가 가장 큰 벡터들은 데이터 분포에서 분산을 가장 크게 만드는 축에 대한 주성분 벡터이다.

차원축소 과정에서, 분산을 가장 크게 만드는 주성분들 위로 데이터를 투영시키면, 차원이 줄어듦에 따라 소실되면 정보를 최소화할 수 있다.

보다 자세한 정보는 여기를 참조하자.

따라서, 우리는 m(압축하고자 하는 이미지 차원 개수)개의 고유벡터를 가져와서 축소를 진행한다.

‘get_eig’의 출력값은 각 고유벡터의 크기가 큰 순서로 고유값을 정렬한 대각행렬과 이에 상응하는 고유벡터를 return한다.

추가적으로, 하기 ‘get_eig_prop’는 압축하고자 하는 이미지 해상도 크기를 입력으로 받는 것이 아닌 어느 정도 비율로 이미지를 압축할 것인지를 인풋으로 받는 함수이다.

def get_eig_prop(S, perc):
    w, v = eigh(S)
    wsum = sum(w)
    w, v = eigh(S, subset_by_value=[perc * wsum, wsum])
    m = len(w)
    eigh_dict = dict()
    for i in range(m):
        eigh_dict[w[i]] = v[:,i]
    wsrt = np.sort(w)[::-1] # eigenvalues in decreasing order
    vsrt = v.copy()
    for i in range(m):
        vsrt[:,i] = eigh_dict[wsrt[i]] # rearrangement
    return np.diag(wsrt), vsrt

차원축소: 투영(projection)

위에서 투영시켜야할 축들을 구해냈으니, 실질적으로 데이터를 그 축들에 투영시키면서 차원을 축소해보자.

def project_image(img, U):
    sum = np.zeros(img.shape[0]) # img.shape: (1024,)
    for i in range(U.shape[1]): # U.shape: (1024, 2)
        alpha = np.dot(U[:,i], img)
        sum += np.dot(alpha, U[:,i])
    return sum

‘np.zeros(img.shape[0])’는 원본 이미지의 크기만큼의 배열을 생성하고, 그 값들은 전부 0으로 초기화한다.

‘U.shape[1]’는 우리가 투영시킬 차원(= 주성분)들에 대한 벡터의 개수이다.

  • ‘U.shape[0]’는 원본 이미지의 해상도인 1024(32x32)의 값을 갖는다.

[투영(Projection) 공식]

image image

  • u들은 고유벡터들 말한다.
  • x들은 원본 이미지 데이터이다.
  • m: 투영시킬 차원 개수(= 최종 해상도 크기)

시각화: 원본 이미지와 비교

def display_image(orig, proj):
    # reshape the images to be 32 x 32
    org = np.reshape(orig, (32,32), order = 'F')
    prj = np.reshape(proj, (32,32), order = 'F')
    # create a figure with one row of two subplots
    fig, ax = plt.subplots(1, 2)
    # title the subplots
    ax[0].set_title('Original')
    ax[1].set_title('Projection')
    # adjust aspect ratio
    ax0 = ax[0].imshow(org,aspect = 'equal')
    ax1 = ax[1].imshow(prj,aspect = 'equal')
    # create a colorbar for each image
    fig.colorbar(ax0, ax=ax[0])
    fig.colorbar(ax1, ax=ax[1])
    plt.show()

결과

x = load_and_center_dataset('<이미지 파일>.npy')
S = get_covariance(x)
Lambda, U = get_eig(S, 2)
projection = project_image(x[0], U)
display_image(x[0], projection)

상기 코드를 통해 손쉽게 원본 이미지와 압축된 이미지를 비교해보자.

image

댓글남기기