지난 포스팅에서는 차원 축소란 무엇이고 왜 하는지, 무슨 방법이 있는지에 대해서 알아보았습니다.

(https://rfriend.tistory.com/736)  차원축소하는 방법에는 크게 Projection-based dimensionality reduction, Manifold Learning 의 두가지 방법이 있다고 했습니다. 

 

이번 포스팅에서는 투사를 통한 차원축소 방법(dimensionality reduction via projection approach) 으로서 주성분분석을 통한 차원축소(dimensionality reduction using PCA, Principal Component Analysis)에 대해서 소개하겠습니다. 

 

(1) 주성분분석(PCA, Principal Component Analysis)을 통한 차원 축소

(2) 특이값 분해 (SVD, Singular Value Decomposition)을 통한 차원 축소

 

 

 

(1) 주성분 분석(PCA, Principal Component Analysis)을 통한 차원 축소

 

주성분 분석(PCA)의 핵심 아이디어만 간략하게 소개하자면요, 피쳐 공간(Feature Space)에서 데이터의 분산을 최대로 잡아낼 수 있는 축을 제1 주성분 축으로 잡고, 이 제1 주성분 축과 직교(orthogonal)하는 축을 제2 주성분 축으로 잡고, ..., 이렇게 최대 변수의 개수 p 개 만큼 주성분 축을 잡아줍니다. (물론, 차원축소를 하는 목적이면 주성분 개수 m 이 변수 개수 p 보다는 작아야 겠지요). 그리고 축을 회전시켜주면 돼요. 

 

아래의 예시 도면을 보면 파란색 제 1 주성분 축 (1st principal component axis)이 데이터 분산을 가장 많이 설명하고 있는 것을 알 수 있습니다. 빨간색 점선의 제 2 주성분 축(2nd principal component axis) 은 제1 주성분 축과 직교하구요. 

 

 

Principal Component Analysis

 

이제 Python 을 가지고 실습을 해볼께요. 

(R로 주성분 분석 하는 것은 https://rfriend.tistory.com/61 를 참고하세요.)

 

먼저 예제로 사용할 iris 데이터셋을 가져오겠습니다. sepal_length, sepal_width, petal_length, petal_width 의 4개 변수를 가진 데이터셋인데요, 4개 변수 간 상관관계 분석을 해보니 상관계수가 0.8 이상으로 꽤 높게 나온 게 있네요. 주성분분석으로 차원축소 해보면 이쁘게 나올거 같아요. 

 

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

## loading IRIS dataset
from sklearn.datasets import load_iris
data = load_iris()


data['data'][:10]
# array([[5.1, 3.5, 1.4, 0.2],
#        [4.9, 3. , 1.4, 0.2],
#        [4.7, 3.2, 1.3, 0.2],
#        [4.6, 3.1, 1.5, 0.2],
#        [5. , 3.6, 1.4, 0.2],
#        [5.4, 3.9, 1.7, 0.4],
#        [4.6, 3.4, 1.4, 0.3],
#        [5. , 3.4, 1.5, 0.2],
#        [4.4, 2.9, 1.4, 0.2],
#        [4.9, 3.1, 1.5, 0.1]])


## converting into pandas DataFrame
iris_df = pd.DataFrame(
    data['data'], 
    columns=['sepal_length', 'sepal_width', 
             'petal_length', 'petal_width'])

iris_df.head()
# 	sepal_length	sepal_width	petal_length	petal_width
# 0	5.1	3.5	1.4	0.2
# 1	4.9	3.0	1.4	0.2
# 2	4.7	3.2	1.3	0.2
# 3	4.6	3.1	1.5	0.2
# 4	5.0	3.6	1.4	0.2


## correlation matrix
iris_df.corr()
# 	sepal_length	sepal_width	petal_length	petal_width
# sepal_length	1.000000	-0.117570	0.871754	0.817941
# sepal_width	-0.117570	1.000000	-0.428440	-0.366126
# petal_length	0.871754	-0.428440	1.000000	0.962865
# petal_width	0.817941	-0.366126	0.962865	1.000000

 

 

주성분 분석은 비지도 학습 (Unsupervised Learning) 이다보니 정답이라는게 없습니다. 그래서 분석가가 주성분의 개수를 지정해주어야 하는데요, 주성분의 개수가 적을 수록 차원 축소가 많이 되는 반면 정보 손실(information loss)가 발생하게 되며, 반면 주성분 개수가 많을 수록 정보 손실은 적겠지만 차원 축소하는 의미가 퇴색됩니다. 그래서 적절한 주성분 개수를 선택(hot to decide the number of principal components)하는게 중요한데요, 주성분의 개수별로 설명 가능한 분산의 비율 (percentage of explained variance by principal components) 을 많이 사용합니다. 

 

아래의 예에서는 첫번째 주성분이 분산의 92.4%를 설명하고, 두번째 주성분이 분산의 5.3%를 설명하므로, 주성분 1 & 2 까지 사용하면 전체 분산의 97.7%를 설명할 수 있게 됩니다. (즉, 원래 4개 변수를 2개의 차원으로 축소하더라도 분산의 97.7%를 설명 가능하다는 뜻) 

 

참고로, 만약 주성분분석 결과를 지도학습(가령, 회귀분석)의 설명변수 인풋으로 사용한다면, cross validation을 사용해서 주성분 개수별로 모델의 성능을 평가(가령, 회귀분석의 경우 MSE)해서, 모델 성능지표가 가장 좋은 주성분 개수를 선택하는 것도 좋은 방법입니다. 

 

## how to decide the number of Principal Components
from sklearn.decomposition import PCA

pca = PCA(random_state=1004)
pca.fit_transform(iris_df)


## percentage of variance explained
print(pca.explained_variance_ratio_)
# [0.92461872 0.05306648 0.01710261 0.00521218]


## Principal 1 & 2 explain about 97.8% of variance
plt.rcParams['figure.figsize'] = (7, 7)
plt.plot(range(1, iris_df.shape[1]+1), pca.explained_variance_ratio_)
plt.xlabel("number of Principal Components", fontsize=12)
plt.ylabel("% of Variance Explained", fontsize=12)
plt.show()

Explained Variance by Principal Components

 

 

이제 주성분 개수를 2개로 지정(n_components=2)해서 주성분 분석을 실행해보겠습니다. Python의 sklearn 모듈의 decomposition.PCA 메소드를 사용하겠습니다. 

 

## Dimensionality Reduction with n_components=2
pca = PCA(n_components=2, random_state=1004)
iris_pca = pca.fit_transform(iris_df)


iris_pca[:10]
# array([[-2.68412563,  0.31939725],
#        [-2.71414169, -0.17700123],
#        [-2.88899057, -0.14494943],
#        [-2.74534286, -0.31829898],
#        [-2.72871654,  0.32675451],
#        [-2.28085963,  0.74133045],
#        [-2.82053775, -0.08946138],
#        [-2.62614497,  0.16338496],
#        [-2.88638273, -0.57831175],
#        [-2.6727558 , -0.11377425]])

 

 

 

위에서 실행한 주성분분석 결과를 가지고 시각화를 해보겠습니다. 4개 변수를 2개의 차원으로 축소를 했기 때문에 2차원의 산점도로 시각화를 할 수 있습니다. 이때 iris 데이터셋의 target 속성정보를 이용해서 붓꽃의 품종별로 색깔과 모양을 달리해서 산점도로 시각화해보겠습니다. 

 

## Visualization

## target
data['target'][:5]
# array([0, 0, 0, 0, 0])


## mapping target name using numpy vectorization
species_map_dict = {
    0: 'setosa', 
    1: 'versicolor', 
    2: 'virginica'
}

iris_pca_df = pd.DataFrame({
    'pc_1': iris_pca[:, 0], 
    'pc_2': iris_pca[:, 1], 
    'species': np.vectorize(species_map_dict.get)(data['target']) # numpy broadcasting
})


iris_pca_df.head()
# pc_1	pc_2	species
# 0	-2.684126	0.319397	setosa
# 1	-2.714142	-0.177001	setosa
# 2	-2.888991	-0.144949	setosa
# 3	-2.745343	-0.318299	setosa
# 4	-2.728717	0.326755	setosa


import seaborn as sns
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (7, 7)
sns.scatterplot(
    x='pc_1', 
    y='pc_2',
    hue='species', 
    style='species',
    s=100,
    data=iris_pca_df
)

plt.title('PCA result of IRIS dataset')
plt.xlabel('Principal Component 1', fontsize=14)
plt.ylabel('Principal Component 2', fontsize=14)
plt.show()

PCA result of iris dataset

 

 

 

(2) 특이값 분해 (SVD, Singular Value Decomposition)을 통한 차원 축소

 

선형대수의 특이값 분해의 결과로 나오는 U, sigma, V 에서 V 가 주성분 분석의 주성분에 해당합니다.  

특이값 분해(SVD, Singular Value Decomposition)에 대한 이론적인 소개는 https://rfriend.tistory.com/185 를 참고하세요. 

 

numpy 모듈의 linalg.svd 메소드를 사용하여 특이값 분해를 하려고 할 때 먼저 데이터 표준화(standardization)을 수작업으로 진행해 줍니다. (sklearn 으로 주성분분석을 할 때 sklearn 모듈이 내부적으로 알아서 표준화해서 진행해줌). 

 

## Standardization first
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(data['data'])


## PCA assumes that the dataset is centered around the origin.
X_centered = data['data'] - data['data'].mean(axis=0)
X_centered[:5]
# array([[-0.74333333,  0.44266667, -2.358     , -0.99933333],
#        [-0.94333333, -0.05733333, -2.358     , -0.99933333],
#        [-1.14333333,  0.14266667, -2.458     , -0.99933333],
#        [-1.24333333,  0.04266667, -2.258     , -0.99933333],
#        [-0.84333333,  0.54266667, -2.358     , -0.99933333]])

 

 

웨에서 표준화한 데이터를 numpy 모듈의 linalg.svd 메소드를 사용하여 특이값 분해를 해준 후에, V 를 transpose (T) 해주어서 첫번째와 두번째 열의 값을 가져오면 제1 주성분, 제2 주성분을 얻을 수 있습니다. 

 

## standard matrix factorization using SVD
U, s, V = np.linalg.svd(X_scaled.T)


## V contains all the principal components
pc_1 = V.T[:, 0]
pc_2 = V.T[:, 1]


## check pc_1, pc_2
pc_1[:10]
# array([0.10823953, 0.09945776, 0.1129963 , 0.1098971 , 0.11422046,
#        0.099203  , 0.11681027, 0.10671702, 0.11158214, 0.10439809])


pc_2[:10]
# array([-0.0409958 ,  0.05757315,  0.02920003,  0.05101939, -0.0552418 ,
#        -0.12718049, -0.00406897, -0.01905755,  0.09525253,  0.04005525])

 

 

 

위에서 특이값분해(SVD)로 구한 제1 주성분, 제2 주성분을 가지고 산점도를 그려보겠습니다. 이때 iris 의 target 별로 색깔과 모양을 달리해서 시각화를 해보겠습니다. 

 

## Visualization

iris_svd_df = pd.DataFrame({
    'pc_1': pc_1, 
    'pc_2': pc_2, 
    'species': np.vectorize(species_map_dict.get)(data['target']) # numpy broadcasting
})


import seaborn as sns
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (7, 7)
sns.scatterplot(
    x='pc_1', 
    y='pc_2',
    hue='species', 
    style='species',
    s=100,
    data=iris_svd_df
)

plt.title('SVD result of IRIS dataset')
plt.xlabel('Principal Component 1', fontsize=14)
plt.ylabel('Principal Component 2', fontsize=14)
plt.show()

dimensionality reduction by SVD

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요. 

 

728x90
반응형
Posted by Rfriend
,

학교 다닐 때 행렬로 연립방정식 풀었던 기억이 날 듯 합니다. 선형대수(Linear Algebra)는 통계, 기계학습, 공학, 영상/이미지 처리 등 여러 분야에서 활용이 됩니다. 선형대수를 전부 다루려면 너무나 방대하므로, 이번 포스팅에서는 Python의 NumPy에 있는 선형대수(Linear Algebra) 함수들 중에서 자주 사용하는 함수에 대해서만 선별적으로 소개하겠습니다. 그리고 선형대수의 이론적인 부분은 별도로 참고할 수 있는 링크를 달도록 하겠습니다. 


  • 단위행렬 (Unit matrix): np.eye(n)
  • 대각행렬 (Diagonal matrix): np.diag(x)
  • 내적 (Dot product, Inner product): np.dot(a, b)
  • 대각합 (Trace): np.trace(x)
  • 행렬식 (Matrix Determinant): np.linalg.det(x)
  • 역행렬 (Inverse of a matrix): np.linalg.inv(x)
  • 고유값 (Eigenvalue), 고유벡터 (Eigenvector): w, v = np.linalg.eig(x)
  • 특이값 분해 (Singular Value Decomposition): u, s, vh = np.linalg.svd(A)
  • 연립방정식 해 풀기 (Solve a linear matrix equation): np.linalg.solve(a, b)
  • 최소자승 해 풀기 (Compute the Least-squares solution): m, c = np.linalg.lstsq(A, y, rcond=None)[0]




 1. 단위행렬 혹은 항등행렬 (Unit matrix, Identity matrix): np.eye(n)


단위행렬은 대각원소가 1이고, 나머지는 모두 0인 n차 정방행렬을 말하며, numpy의 eye() 함수를 사용해서 만들 수 있습니다. 


* 참고 링크 : https://rfriend.tistory.com/141



import numpy as np


unit_mat_4 = np.eye(4)


print(unit_mat_4)

[[1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]]

 




  1. 대각행렬 (Diagonal matrix): np.diag(x)


대각행렬은 대각성분 이외의 모든 성분이 모두 '0'인 n차 정방행렬을 말합니다. 아래 예시의 행렬에서 빨간색으로 표시한 원소를 '0'으로 바꾼 행렬이 대각행렬입니다. 


* 참고 링크 : http://rfriend.tistory.com/141


 

In [1]: import numpy as np


In [2]: x = np.arange(9).reshape(3, 3)


In [3]: print(x)

[[0 1 2]

 [3 4 5]

 [6 7 8]]


In [4]: np.diag(x)

Out[4]: array([0, 4, 8])


In [5]: np.diag(np.diag(x))

Out[5]:

array([[0, 0, 0],

        [0, 4, 0],

        [0, 0, 8]])





  2. 내적 (Dot product, Inner product): np.dot(a, b), a.dot(b)


matrix dot product, inner product, scalar product, projection product

Python에서 '*' 를 사용한 두 행렬 간 곱은 원소 간 곱(element-wise product)을 반환하며, 선형대수에서 말하는 행렬 간 내적 곱을 위해서는 np.dot() 함수를 이용해야 합니다. 


원소 간 곱 (element-wise product)

: a*b

내적 (dot product, inner product)

: np.dot(a, b)

 

In [6]: a = np.arange(4).reshape(2, 2)


In [7]: print(a)

[[0 1]

 [2 3]]


In [8]: a*a

Out[8]:

array([[0, 1],

        [4, 9]])


In [6]: a = np.arange(4).reshape(2, 2)


In [7]: print(a)

[[0 1]

 [2 3]]


In [9]: np.dot(a, a)

Out[9]:

array([[ 2, 3],

        [ 6, 11]])



np.dot(a, b) NumPy 함수와 a.dot(b)의 배열 메소드의 결과는 동일합니다. 



In [10]: a.dot(a)

Out[10]:

array([[ 2, 3],

        [ 6, 11]])

 




  3. 대각합 (Trace): np.trace(x)


정방행렬의 대각에 위치한 원소를 전부 더해줍니다. 

아래의 2차 정방행렬 예의 대각합은 0+5+10+15 = 30 이 됩니다. (파란색으로 표시함)



In [12]: b = np.arange(16).reshape(4, 4)


In [13]: print(b)

[[ 0 1 2 3]

 [ 4 5 6 7]

 [ 8 9 10 11]

 [12 13 14 15]]


In [14]: np.trace(b)

Out[14]: 30

 



3차원 행렬에 대해서도 대각합을 구할 수 있습니다. 2차원은 대각선 부분의 원소 값을 전부 더하면 되지만 3차원 행렬에서는 대각(diagonal)이 어떻게 되나 좀 헷갈릴 수 있겠습니다. 아래의 3차원 행렬의 대각합을 구하는 예를 살펴보면, [0+12+24, 1+13+25, 2+14+26] = [36, 39, 42] 가 됩니다. 



In [15]: c = np.arange(27).reshape(3, 3, 3)


In [16]: print(c)

[[[ 0 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]]]


In [17]: np.trace(c)

Out[17]: array([36, 39, 42])

 




  4. 행렬식 (Matrix Determinant): np.linalg.det(x)


역행렬이 존재하는지 여부를 확인하는 방법으로 행렬식(determinant, 줄여서 det)이라는 지표를 사용합니다. 이 행렬식이 '0'이 아니면 역행렬이 존재하고, 이 행렬식이 '0'이면 역행렬이 존재하지 않습니다. 


* 참고 링크 : http://rfriend.tistory.com/142


아래의 예에서 array([[1, 2], [3, 4]]) 의 행렬식이 '-2.0'으로서, '0'이 아니므로 역행렬이 존재한다고 판단할 수 있습니다. 



In [18]: d = np.array([[1, 2], [3, 4]])


In [19]: np.linalg.det(a)

Out[19]: -2.0

 




  5. 역행렬 (Inverse of a matrix): np.linalg.inv(x)


역행렬은 n차정방행렬 Amn과의 곱이 항등행렬 또는 단위행렬 In이 되는 n차정방행렬을 말합니다. A*B 와 B*A 모두 순서에 상관없이 곱했을 때 단위행렬이 나오는 n차정방행렬이 있다면 역행렬이 존재하는 것입니다.

역행렬은 가우스 소거법(Gauss-Jordan elimination method), 혹은 여인수(cofactor method)로 풀 수 있습니다. 




In [20]: a = np.array(range(4)).reshape(2, 2)


In [21]: print(a)

[[0 1]

 [2 3]]


In [22]: a_inv = np.linalg.inv(a)


In [23]: a_inv

Out[23]:

array([[-1.5, 0.5],

        [ 1. , 0. ]])




위의 예제에서 np.linalg.inv() 함수를 사용하여 푼 역행렬이 제대로 푼 것인지 확인을 해보겠습니다. 역행렬의 정의에 따라서 원래의 행렬에 역행렬을 곱하면, 즉, a.dot(a_inv) 또는 np.dot(a, a_inv) 를 하면 단위행렬(unit matrix)가 되는지 확인해보겠습니다. 



In [24]: a.dot(a_inv)

Out[24]:

array([[1., 0.],

         [0., 1.]])

 




  6. 고유값 (Eigenvalue), 고유벡터 (Eigenvector): w, v = np.linalg.eig(x)


정방행렬 A에 대하여 Ax = λx  (상수 λ) 가 성립하는 0이 아닌 벡터 x가 존재할 때 상수 λ 를 행렬 A의 고유값 (eigenvalue), x 를 이에 대응하는 고유벡터 (eigenvector) 라고 합니다. 


np.linalg.eig() 함수는 고유값(eigenvalue) w, 고유벡터(eigenvector) v 의 두 개의 객체를 반환합니다. 


In [25]: e = np.array([[4, 2],[3, 5]])


In [26]: print(e)

[[4 2]

[3 5]]


In [27]: w, v = np.linalg.eig(e)


#  w: the eigenvalues lambda

In [28]: print(w)

[2. 7.]


# v: the corresponding eigenvectors, one eigenvector per column

In [29]: print(v)

[[-0.70710678 -0.5547002 ]

[ 0.70710678 -0.83205029]]

 



고유벡터는 배열 인덱싱하는 방법을 사용해서 각 고유값에 대응하는 고유벡터를 선택할 수 있습니다. 



# eigenvector of eigenvalue lambda 2

In [30]: print(v[:, 0]

[-0.70710678 0.70710678]


# eigenvector of eigenvalue labmda 7

In [31]: print(v[:, 1]

[-0.5547002 -0.83205029]

 




  7. 특이값 분해 (Singular Value Decomposition): u, s, vh = np.linalg.svd(A)


특이값 분해는 고유값 분해(eigen decomposition)처럼 행렬을 대각화하는 한 방법으로서, 정방행렬뿐만 아니라 모든 m x n 행렬에 대해 적용 가능합니다. 특이값 분해는 차원축소, 데이터 압축 등에 사용할 수 있습니다. 이론적인 부분은 설명하자면 너무 길기 때문에 이 포스팅에서는 설명하지 않겠으며, 아래의 링크를 참고하시기 바랍니다. 


* 참고 링크 : http://rfriend.tistory.com/185


아래의 np.linalg.svd(A) 예제는 위의 참고 링크에서 사용했던 예제와 동일한 것을 사용하였습니다. 


In [32]: A = np.array([[3,6], [2,3], [0,0], [0,0]])


In [33]: print(A)

[[3 6]

 [2 3]

 [0 0]

 [0 0]]


In [34]: u, s, vh = np.linalg.svd(A)


In [35]: print(u)

[[-0.8816746 -0.47185793 0. 0. ]

 [-0.47185793 0.8816746 0. 0. ]

 [ 0. 0. 1. 0. ]

 [ 0. 0. 0. 1. ]]


In [36]: print(s)

[7.60555128 0.39444872]


In [37]: print(vh)

[[-0.47185793 -0.8816746 ]

 [ 0.8816746 -0.47185793]]

 




  8. 연립방정식 해 풀기 (Solve a linear matrix equation): np.linalg.solve(a, b)


아래의 두 개 연립방정식의 해(x0, x1)를 np.linalg.solve(a, b) 함수를 사용하여 풀어보겠습니다. 



위의 연립방정식을 어떻게 행렬로 입력하고, np.linalg.solve(a, b)에 입력하는지 유심히 살펴보시기 바랍니다. 



In [38]: a = np.array([[4, 3], [3, 2]])


In [39]: b = np.array([23, 16])


In [40]: x = np.linalg.solve(a, b)


In [41]: print(x)

[2. 5.]

 



NumPy 가 제대로 x0, x1의 해를 풀었는지 확인해보겠습니다. x0=2, x1=5 가 해 맞네요!



In [42]: np.allclose(np.dot(a, x), b)

Out[42]: True

 




  9. 최소자승 해 풀기 (Compute the Least-squares solution)

     : m, c = np.linalg.lstsq(A, y, rcond=None)[0]


회귀모형 적합할 때 최소자승법(Least-squares method)으로 잔차 제곱합을 최소화하는 회귀계수를 추정합니다. 


* 참고 링크 : https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.lstsq.html



아래의 예에서는 회귀계수 m, y절편 c를 최소자승법을 사용해서 구해보겠습니다. 



In [43]: x = np.array([0, 1, 2, 3])


In [44]: y = np.array([-1, 0.2, 0.9, 2.1])


In [45]: A = np.vstack([x, np.ones(len(x))]).T


In [46]: A

Out[46]:

array([[0., 1.],

        [1., 1.],

        [2., 1.],

        [3., 1.]])


In [47]: m, c = np.linalg.lstsq(A, y, rcond=None)[0]


In [48]: print(m, c)

0.9999999999999999 -0.9499999999999997

 



아래의 그래프에서 점은 원래의 데이터이며, 빨간색 선은 최소자승법으로 추정한 회귀식의 적합선이 되겠습니다. 



In [49]: import matplotlib.pyplot as plt

    ...: plt.plot(x, y, 'o', label='Original data', markersize=10)

    ...: plt.plot(x, m*x + c, 'r', label='Fitted line')

    ...: plt.legend()

    ...: plt.show()




많은 도움이 되었기를 바랍니다. 


728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 행과 열의 개수가 같은 정방행렬(square matrix)에 대해 고유값 분해(eigenvalue decompositon)를 활용한 대각화(diagonalization)와, 이를 마아코프 과정(Markov Process)의 안정상태확률 계산에 적용한 사례에 대해서 소개하였습니다.

 

복습하는 차원에서 고유값 분해에 대해서 정리해보면 아래와 같습니다. 

 

  • 고유값 분해 (eigenvalue decomposition)

 

고유값 분해는 n*n 정방행렬 (n by n square matrix)에 대해서만 적용 가능하다는 점과 가운데에 D 행렬에 고유값이 들어가 있다는 점은 다시 한번 상기하시기 바랍니다.  m*n 직사각행렬(m by n rectangular matrix)에 대해서는 고유값 분해는 사용할 수 없다는 뜻입니다.

 

 

 

 

이번 포스팅에서는 실수(real number)나 복소수(complex number)로 이루어진 공간의 원소로 이루어진 m개의 행과 n개의 열을 가진 모든 직사각행렬 (rectangular matrix)에 폭넓게 사용 가능한 특이값 분해 (SVD, singular value decomposition)에 대해서 알아보겠습니다.

 

특이값 분해는 행렬의 스펙트럼 이론을 임의의 직사각행렬에 대해 일반화한 것으로 볼 수 있습니다. 스펙트럼 이론을 이용하면 직교 정사각행렬을 고유값을 기저로 하여 대각행렬로 분해할 수 있습니다. (* 위키피디아) 

  • 스펙트럼 분해 (spectral decomposition)

p*p 대칭행렬 A에 대한 스펙트럼 분해(spectral decomposition)는 다음과 같습니다.  p*p 대칭행렬 A는 직교행렬 P에 의해 대각화(diagonalization)된다고 합니다.

 

 

이때 를 만족하는 직교행렬 P는 로 이루어지며, (람다, lambda)는 A의 고유값(eigenvalue)들로만 이루어진 대각행렬(diagonal matrix)인

 입니다.  대각행렬 이 성립합니다.

 

 

 

위의 스펙트럼 분해 (혹은 분광분해)를 일반화한 특이값 분해는 아래와 같습니다. 

 

 

 

  • 특이값 분해 (SVD, Singular Value Decomposition)

m*n 직사각행렬 A에 대한 특이값 분해(SVD, Singular Value Decomposition)는 아래와 같이 나타낼 수 있습니다.

 

 

 

행렬 A의 계수(rank)가 k 라고 할 때,

 

를 고유값분해(eigenvalue decomposition)로 직교대각화하여 얻은 m*m 직교행렬 (orthogonal matrix)이며, 특히를 좌특이벡터(left singular vectors, gene coefficient vectors) 라고 합니다.

 

 

는  를 고유값분해로 직교대각화하여 얻은 n*n 직교행렬이며, 특히 를 우특이벡터(right singular vectors, expression level vectors)라고 합니다.

 

 

는 (의 0이 아닌 고유값이 일 때)  를 대각성분으로 가지고 나머지 성분은 0을 가지는 m*n 직사각 대각행렬(diagonal matrix) 입니다.

 

 

 

 

m*n 직사각행렬 A의 특이값 분해 를 다시 한번 풀어서 쓰면 아래와 같습니다.

 

 

 

 

위의 식에서 특이값(singular value)는 가 됩니다.

 

 

참고로,

U, V가 직교행렬(orthogonal matrix)이면 가 성립합니다. 

직교행렬(orthogonal matrix) Q는 다음을 만족하는 정방행렬이기 때문입니다.

 

 

 

 

서두에서 정방행렬에 국한된 고유값 분해보다 모든 m*n 행렬에 적용가능한 특이값 분해가 일반화면에서 활용성이 더 넓다고 했는데요, 이 둘이 사실은 서로 관련이 되어 있습니다.

 

 

 

  • 특이값 분해와 고유값 분해의 관계

아래의 수식 전개를 보면 확인할 수 있는데요, 서두에서 소개했던 고유값 분해 형식()과 같아졌습니다.  m*n 행렬 A의 특이값 분해의 U는  의 고유벡터(eigenvector)이고, V는의 고유벡터(eigenvector) 이며, A의 0이 아닌 특이값들의 제곱() 은 , 의 고유값과 같음을 알 수 있습니다.

 

결국 SVD를 계산한다는 것은 의 고유벡터와 고유값을 구하는 것이라는 것을 알 수 있습니다.

 

 

 

다음으로, 특이값 분해의 기하학적인 의미를 살펴보겠습니다.

  • 특이값 분해의 기하학적인 의미 (visualization of SVD)

아래의 그림을 가지고 의 특이값 분해가 가지는 선형변환의 의미를 기하학적으로 설명하자면, 먼저 직교행렬 에 의해서 원 행렬 M이 회전(방향 변환)을 하게 되며, 에 의해서 크기가 달라졌고 (scale 변환), 다시 직교행렬 에 의해서 에 의한 회전과는 반대로 회전(방향 변환)하였습니다.

 

* 그림 출처 : https://en.wikipedia.org/wiki/Singular_value_decomposition

 

위의 설명을 애니메이션을 넣어서 설명해주는 그림은 아래와 같습니다.

 

Singular value decomposition

* 출처 : By Kieff (Own work) [Public domain], via Wikimedia Commons

 

 

고유값 분해를 통한 대각화의 경우 고유벡터의 방향은 변화가 없고, 크기(scale 변환)만 고유값(eigenvalue) 만큼 변한다고 설명드렸었습니다.  반면, 특이값 분해는 위의 그림 결과를 보면 처음의 행렬 U, V^T에 의해 M이 방향이 변하고, Σ 특이값(singular values)들 만큼의 크기(scale)가 변했음을 알 수 있습니다.

 

 

  • Redeced SVD (Singular Value Decomposition)

위에서 SVD(Singular Value Decomposition)를 설명할 때 full SVD를 설명해 드렸는데요차원 축소할 때 아래 그림에서 소개드린 것처럼 reduced SVD를 합니다. full SVD 대비 reduced SVD는 특이값(singular value) 들 중에서 0인 것들을 제외하고 SVD를 한다는 점이 서로 다릅니다.

 

 

 

아래의 그림을 보면 조금 더 이해하기가 쉬울텐데요, 빨간색 점선으로 표시한 부분을 제외하고 행렬 A의 계수(rank) k 개 만큼의 특이값들을 가지고 SVD를 진행하는 것이 reduced SVD 입니다.

 

 

  • 특이값 분해 예제 (example of full SVD)

이해를 돕기 위해서 4 by 2 직사각행렬 (rectacgular matrix) A를 가지고 (full) SVD 계산 예를 들어보겠습니다.  아래 예에서의 고유값과 고유벡터 계산은 R 분석툴을 사용했습니다.  손으로 푸는 방법은 ☞ [선형대수] 고유값, 고유벡터 구하기 (calculation of eigenvalue and eigenvector) 를 참조하시기 바랍니다.

 

 

 

4 by 2 행렬 을 가지고 해보겠습니다.

 

특이값 분해가  라고 했는데요,

 

(1) 먼저 의 고유벡터(eigenvectors)인 U를 구해보겠습니다.

 

 

 

 

위의 풀이에서 사용한, R로 의 고유벡터를 구해서 U를 구하는 방법은 아래와 같습니다.

 

> A <- matrix(c(3, 2, 0, 0,  6, 3, 0, 0), nc=2, byrow = FALSE)
> A
     [,1] [,2]
[1,]    3    6
[2,]    2    3
[3,]    0    0
[4,]    0    0
> t(A)
     [,1] [,2] [,3] [,4]
[1,]    3    2    0    0
[2,]    6    3    0    0
> ##--- (1) calculation of U
> # A%*%t(A)
> W_1 <- A%*%t(A)
> W_1
     [,1] [,2] [,3] [,4]
[1,]   45   24    0    0
[2,]   24   13    0    0
[3,]    0    0    0    0
[4,]    0    0    0    0
> 
> 
> # eigenvalue, eigenvector of W
> eigen(W_1)
$values
[1] 57.8444102  0.1555898  0.0000000  0.0000000

$vectors
          [,1]       [,2] [,3] [,4]
[1,] 0.8816746 -0.4718579    0    0
[2,] 0.4718579  0.8816746    0    0
[3,] 0.0000000  0.0000000    0    1
[4,] 0.0000000  0.0000000    1    0

> 
> # U
> U <- eigen(W_1)[[2]] # eigenvectors
> U
          [,1]       [,2] [,3] [,4]
[1,] 0.8816746 -0.4718579    0    0
[2,] 0.4718579  0.8816746    0    0
[3,] 0.0000000  0.0000000    0    1
[4,] 0.0000000  0.0000000    1    0

 

 

 

 

(2) 다음으로,  의 고유벡터(eigenvectors of A^T*A)  를 구해보겠습니다.  위의 (1)번 풀이 과정과 동일합니다.

 

 

R로 풀이한 것은 아래와 같습니다.

 

> > ##---- (2) calculation of V^T > # t(A)%*%A > W_2 <- t(A)%*%A > W_2 [,1] [,2] [1,] 13 24 [2,] 24 45 > > # eigenvalue of W > eigen(W_2) $values [1] 57.8444102 0.1555898 $vectors [,1] [,2] [1,] 0.4718579 -0.8816746 [2,] 0.8816746 0.4718579 > > # V > V <- eigen(W_2)[[2]] # eigenvectors > V [,1] [,2] [1,] 0.4718579 -0.8816746 [2,] 0.8816746 0.4718579 

 

 

 

(3) 다음으로, 의 고유값(eigenvalue)의 제곱근(square root)을 특이값(singular value) 대각원소로 가지고 나머지는 '0'인 대각행렬 Σ 를 구해보겠습니다.

 

 

R로 고유값에 square root를 취해서 특이값(singular value) 구하는 절차는 아래와 같습니다.

 

> ##--- (3) calculation of Σ
> # square root of eigenvalues
> W_2_eigenvalue_sqrt <- sqrt(eigen(W_2)[[1]])
> W_2_eigenvalue_sqrt
[1] 7.6055513 0.3944487
> 
> S <- matrix(rep(0, 8), nc=2, byrow=F) # all zeros, temp matrix
> S
     [,1] [,2]
[1,]    0    0
[2,]    0    0
[3,]    0    0
[4,]    0    0
> 
> S[1,1] <- W_2_eigenvalue_sqrt[1] 
> S[2,2] <- W_2_eigenvalue_sqrt[2]
> S
         [,1]      [,2]
[1,] 7.605551 0.0000000
[2,] 0.000000 0.3944487
[3,] 0.000000 0.0000000
[4,] 0.000000 0.0000000 

 

 

 

(4) 위에서 구한 U, V^T, Σ 를 종합하면 끝이네요.

 

 

 

위에서 R로 풀었던 것을 다시 한번 불러와 정리해보면 아래와 같습니다.

 

> # overall (aggregation)

> > A # 4 by 2 rectacgular matrix [,1] [,2] [1,] 3 6 [2,] 2 3 [3,] 0 0 [4,] 0 0 >

> U # eigenvectors of A*t(A) [,1] [,2] [,3] [,4] [1,] 0.8816746 -0.4718579 0 0 [2,] 0.4718579 0.8816746 0 0 [3,] 0.0000000 0.0000000 0 1 [4,] 0.0000000 0.0000000 1 0 >

> S # square root of eigenvalues of t(A)*A [,1] [,2] [1,] 7.605551 0.0000000 [2,] 0.000000 0.3944487 [3,] 0.000000 0.0000000 [4,] 0.000000 0.0000000 >

> V # eigenvectors of t(A)*A [,1] [,2] [1,] 0.4718579 -0.8816746 [2,] 0.8816746 0.4718579 > > SVD_of_A <- U %*% S %*% t(V) > SVD_of_A [,1] [,2] [1,] 3.328201 5.824352 [2,] 1.386750 3.328201 [3,] 0.000000 0.000000 [4,] 0.000000 0.000000 

 

 


많은 도움이 되었기를 바랍니다.

 

도움이 되었다면 아래의 '공감 ♡'를 꾸욱~ 눌러주세요. ^^

 

728x90
반응형
Posted by Rfriend
,