SVD(Singular Value Decomposition) 특이값 분해는 대체 어떤 행렬이 튀어나오는가? SVD 파이썬 치트코드

PCA와 다르게 SVD는 여러 행렬이 튀어나온다. 사실 PCA는 내부적으로 SVD를 이용하는데, SVD는 상황에 따라서 원본 행렬 크기 그대로 행렬을 만들수도 있고, 축소된 정보를 지닌 Truncated된 행렬을 가질수도 있다.

svd

Singular Value Decomposition 을 통한 행렬분해

In [93]:
import numpy as np

array = np.random.randn(10, 3)
array
Out[93]:
array([[-0.18907607,  1.40526823, -0.82144299],
       [-1.33705953,  0.22458166, -0.45066011],
       [-0.46262648,  0.9600491 ,  0.3166489 ],
       [ 0.48785771,  1.89255158,  0.29655123],
       [ 1.30803128, -1.19769358, -0.51642088],
       [-0.2947286 ,  0.12356438, -1.51307636],
       [ 0.12637999,  2.22853341, -0.12660306],
       [ 1.71416151,  1.06121283, -1.36887409],
       [ 0.79649616,  0.79553079,  0.95338132],
       [ 1.32529062, -0.83703984, -1.02962078]])

linalg의 svd 메서드를 사용하면 다음과 같은 행렬로 나눠진다.

In [94]:
u, s, vh = np.linalg.svd(array, full_matrices=True)
In [96]:
print("u:" + str(u.shape))
print("s:" + str(s.shape))
print("vh:" + str(vh.shape))
u:(10, 10)
s:(3,)
vh:(3, 3)

행렬의 shape는 full_matrices를 False로 주면 곱이 가능하다.

In [28]:
u, s, vh = np.linalg.svd(array, full_matrices=False)
In [29]:
print("u:" + str(u.shape))
print("s:" + str(s.shape))
print("vh:" + str(vh.shape))
u:(10, 3)
s:(3,)
vh:(3, 3)

s는 항상 내림차순으로 정렬되어있다.

In [47]:
s
Out[47]:
array([4.23326592, 2.51669151, 2.31782445])

s는 대각행렬로써, 행렬곱이 가능하도록 변환해서 곱한다.

곱한 결과는 사영된 행렬로, 앞 열부터 차례대로 유용하게 쓸수있다.

In [38]:
us = np.matmul(u, np.diag(s))
us
Out[38]:
array([[ 1.51963748, -0.8077354 ,  1.62888869],
       [ 0.64488374, -0.31030758,  0.06249265],
       [-1.05292703,  0.23856862,  0.02940617],
       [-0.72474885, -0.21693963, -0.90644839],
       [ 2.53048111,  0.34441689, -0.24767838],
       [-2.63193182, -0.27475976,  0.80104604],
       [-0.18431127,  1.93005545,  0.82372555],
       [-0.32413449,  0.26396689,  0.58599602],
       [ 0.21262484,  0.75181067,  0.09943803],
       [ 0.216352  ,  0.96275821, -0.39722875]])

이 세개의 행렬을 곱한 결과로 오리지널 행렬이 다시 튀어나왔다.

In [40]:
a = np.matmul(us, vh)
a
Out[40]:
array([[-0.53172916,  0.22982883, -2.29770662],
       [ 0.18205132,  0.37601693, -0.58441401],
       [-0.19235255, -0.77583138,  0.72630655],
       [ 0.78063326, -0.29379353,  0.8356252 ],
       [-0.08406369,  2.30346499, -1.12706901],
       [-0.34609288, -2.59225961,  0.89701874],
       [-1.96914115,  0.26764137,  0.69889891],
       [-0.59362303, -0.40695266, -0.01158666],
       [-0.61551677,  0.42957374,  0.23858336],
       [-0.42829527,  0.72611618,  0.64870693]])

사실 이대로 행렬을 복원시키는 거라면, 그냥 수학적 삽질이나 다름없다.

왜 이런짓을 하는지는 차원축소에 답이 있다.

In [43]:
from sklearn.decomposition import TruncatedSVD

10*3 의 행렬은, 3개의 Feature를 가진 데이터셋으로 볼수도 있다.

이를 2개의 주요 Feature를 가진 행렬로 데이터셋으로 변환해보자

이번에는 TruncatedSVD를 통해 데이터를 중요순서대로 끊어내는 프로세스를 해볼 수 있다.

In [74]:
svd = TruncatedSVD(n_components=2)
svd.fit(array)
Out[74]:
TruncatedSVD(algorithm='randomized', n_components=2, n_iter=5,
       random_state=None, tol=0.0)

사실 특이값들은 위에서 구한 대각행렬의 첫번째, 두번째 원소를 나열한것이나 다름없다!

In [75]:
print(svd.singular_values_)
[4.23326592 2.51669151]

차원축소가 된 행렬을 한번 살펴보자. 어디선가 많이 보던 행렬의 일부분이다.

In [76]:
svd.transform(array)
Out[76]:
array([[-1.51963748, -0.8077354 ],
       [-0.64488374, -0.31030758],
       [ 1.05292703,  0.23856862],
       [ 0.72474885, -0.21693963],
       [-2.53048111,  0.34441689],
       [ 2.63193182, -0.27475976],
       [ 0.18431127,  1.93005545],
       [ 0.32413449,  0.26396689],
       [-0.21262484,  0.75181067],
       [-0.216352  ,  0.96275821]])

이는 아까 위에서 봤던 us 의 앞 두번째 열과 의미가 같다.

In [77]:
np.matmul(u, np.diag(s))[:,:2]
Out[77]:
array([[ 1.51963748, -0.8077354 ],
       [ 0.64488374, -0.31030758],
       [-1.05292703,  0.23856862],
       [-0.72474885, -0.21693963],
       [ 2.53048111,  0.34441689],
       [-2.63193182, -0.27475976],
       [-0.18431127,  1.93005545],
       [-0.32413449,  0.26396689],
       [ 0.21262484,  0.75181067],
       [ 0.216352  ,  0.96275821]])

오리지널 행렬을 vh의 전치행렬과 곱한 결과와도 같다.

In [78]:
np.matmul(array, vh.T)[:,:2]
Out[78]:
array([[ 1.51963748, -0.8077354 ],
       [ 0.64488374, -0.31030758],
       [-1.05292703,  0.23856862],
       [-0.72474885, -0.21693963],
       [ 2.53048111,  0.34441689],
       [-2.63193182, -0.27475976],
       [-0.18431127,  1.93005545],
       [-0.32413449,  0.26396689],
       [ 0.21262484,  0.75181067],
       [ 0.216352  ,  0.96275821]])

분산을 얼마나 설명하는지도 확인 가능하다.

In [79]:
svd.explained_variance_ratio_
Out[79]:
array([0.63584863, 0.1953097 ])

답글 남기기