TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 머신러닝>

[머신러닝] 차원 축소 - SVD

by 기록자_Recordian 2024. 10. 24.
728x90
반응형
차원 축소란?
 

[머신러닝] 차원 축소(Dimension Reduction)

차원 축소(Dimension Reduction)차원 축소의 중요한 의미는차원 축소를 통해 좀 더 데이터를 잘 설명할 수 있는잠재적인 요소를 추출하는 데 있다.차원 축소: 매우 많은 피처로 구성된 다차원 데이터

puppy-foot-it.tistory.com


SVD(Singular Value Decompostion, 특이값 분해)

 

[PCA vs SVD]

  • PCA: 정방행렬만을 고유벡터로 분해
  • SVD: 정방행렬 뿐 아니라 행과 열의 크기가 다른 행렬에도 적용 가능

여기에서 각 행렬은 다음과 같은 성질을 가진다.

  • U는 m × m 크기를 가지는 유니터리 행렬이다.
  • Σ는 m × n 크기를 가지며, 대각선상에 있는 원소의 값은 음수가 아니며 나머지 원소의 값이 모두 0인 대각행렬이다.
  • V∗ 는 V의 전치 행렬로, n × n 유니터리 행렬이다.
  • 행렬 M을 이와 같은 세 행렬의 곱으로 나타내는 것을 M의 특잇값 분해라고 한다.

★ 유니터리 행렬이란?

 

유니터리 행렬 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 선형대수학에서 유니터리 행렬(영어: unitary matrix)는 켤레 전치가 역행렬과 같은 복소수 정사각 행렬이다. 복소수 n × n {\displaystyle n\times n} 정사각 행렬 U {\display

ko.wikipedia.org

 

SVD의 행렬에 속한 벡터는 특이벡터(singular vector) 이며, 모든 특이벡터는 서로 직교하는 성질을 갖는다.

일반적인 SVD는 보통 넘파이나 사이파이 라이브러리를 이용해 수행한다.

 

[넘파이 SVD 모듈을 로딩하고 랜덤한 넘파이 행렬 생성]

넘파이의 SVD 모듈인 numpy.linalg.svd를 로딩하고, 랜덤한 4*4 넘파이 행렬을 생성한다.

※ 랜덤 행렬을 생성하는 이유는 행렬의 개별 로우끼리의 의존성을 없애기 위함

# 넘파이의 svd 모듈 임포트
import numpy as np
from numpy.linalg import svd

# 4*4 랜덤 행렬 a 생성
np.random.seed(121)
a = np.random.randn(4, 4)
print(np.round(a, 3))

 

[생성된 a 행렬에 SVD 적용]

생성된 a 행렬에 SVD를 적용해 U, Sigma, Vt를 도출한다.

SVD 분해는 numpy.linalg.svd 에 파라미터로 원본 행렬을 입력하면 U 행렬, Sigma 행렬, V 전치 행렬을 반환한다.

U, Sigma, Vt = svd(a)
print(U.shape, Sigma.shape, Vt.shape)
print('U matrix:\n', np.round(U, 3))
print('Sigma Value:\n', np.round(Sigma, 3))
print('V transpose matrix:\n', np.round(Vt, 3))

▶ U 행렬이 4*4, Vt 행렬이 4*4, Sigma의 경우는 1차원 행렬인 (4, )로 반환되었다.

 

[분해된 행렬을 원본 행렬로 복원]

분해된 U, Sigma, Vt 를 이용해 다시 원본 행렬로 정확히 복원되는지 확인해본다. 원본 행렬로의 복원은 이들을 내적하면 되는데, 한 가지 유의할 것은 Sigma의 경우 0이 아닌 값만 1차원으로 추출했으므로 다시 0을 포함한 대칭행렬로 변환한 뒤에 내적을 수행해야 한다는 점이다.

내적 | 內積 | inner product
적은 '쌓는다'는 뜻의 한자이고, 여기서는 '곱한다'는 뜻이다. 벡터의 곱하기는 두 가지 정의가 있는데, 내적은 벡터를 마치 수처럼 곱하는 개념이다. 벡터에는 방향이 있으므로, 방향이 일치하는 만큼만 곱한다. 예를 들어 두 벡터의 방향이 같으면, 두 벡터의 크기를 그냥 곱한다. 두 벡터가 이루는 각이 90도일 땐, 일치하는 정도가 전혀 없기 때문에 내적의 값은 0이다. 내적은 한 벡터를 다른 벡터로 정사영 시켜서, 그 벡터의 크기를 곱한다.
# Sigma를 다시 0을 포함한 대칭행렬로 변환
Sigma_mat = np.diag(Sigma)
a_ = np.dot(np.dot(U, Sigma_mat), Vt)
print(np.round(a_, 3))

▶ a_가 원본 행렬 a와 동일하게 복원되었다.

 

[데이터 세트가 로우 간 의존성이 있을 때의 Sigma 값 변화와 차원 축소 진행 여부 알아보기]

의존성을 부여하기 위해 a 행렬의 3번째 로우를 '첫 번째 로우 + 두 번째 로우'로 업데이트하고, 4번째 로우는 첫 번째 로우와 같다고 업데이트 한다.

a[2] = a[0] + a[1]
a[3] = a[0]
print(np.round(a, 3))

▶ a 행렬은 로우 간 관계가 매우 높아졌다.

 

[로우 간 관계가 높아진 데이터를 SVD로 재분해]

# 다시 SVD를 수행해 Sigma 값 확인
U, Sigma, Vt = svd(a)
print(U.shape, Sigma.shape, Vt.shape)
print('Sigma Value:\n', np.round(Sigma, 3))

▶ 이전과 차원은 같으나 Sigma 값 중 2개가 0으로 변했고, 이는 선형 독립인 로우 벡터의 개수가 2개라는 의미이다.

 

[해당 데이터를 다시 원본 행렬로 복원]

이번에는 U, Sigma, Vt의 전체 데이터를 이용하지 않고 Sigma의 0에 대응되는 U, Sigma, Vt의 데이터를 제외하고 복원해 본다. 즉, Sigma의 경우 앞의 2개 요소만 0이 아니므로 U 행렬 중 선행 두 개의 열만 추출하고, Vt의 경우는 선행 두 개의 행만 추출해 복원한다.

# U 행렬의 경우는 Sigma와 내적을 수행하므로 Sigma의 앞 2행에 대응되는 앞 2열만 추출
U_ = U[:, :2]
Sigma_ = np.diag(Sigma[:2])
# V 전치 행렬의 경우는 앞 2행만 추출
Vt_ = Vt[:2]
print(U.shape, Sigma_.shape, Vt_.shape)
# U, Sigma, Vt의 내적을 수행하며, 다시 원본 행렬 복원
a_ = np.dot(np.dot(U_, Sigma_), Vt_)
print(np.round(a_, 3))

 

[Truncated SVD 를 이용한 행렬 분해]

Truncated SVD는 대각원소 중에 상위 몇 개만 추출해서 여기에 대응하는 U와 V의 원소도 함께 제거해 더욱 차원을 줄인 형태로 분해하는 것이다. 이렇게 분해하면 인위적으로 더 작은 차원의 U, Sigma, Vt로 분해하기 때문에 원본 행렬을 정확하게 다시 원복할 수는 없다.

그러나 데이터 정보가 압축되어 분해됨에도 불구하고 상당한 수준으로 원본 행렬을 근사할 수 있으며, 원래 차원의 차수에 가깝게 잘라낼수록 원본 행렬에 더 가깝게 복원할 수 있다.

 

Truncated SVD는 넘파이가 아닌 사이파이에서만 지원되며, 사아파이는 SVD 뿐 아니라 Truncated SVD도 지원한다.

Truncated SVD는 희소 행렬로만 지원돼서 scipy.sparse.linalg.svds를 이용해야 한다.

 

임의의 원본 행렬 6*6 을 Normal SVD로 분해해 분해된 행렬의 차원과 Sigma 행렬 내의 특이값을 확인한 뒤 다시 Truncated SVD 로 분해해 분해된 행렬의 차원, Sigma 행렬 내의 특이값, Truncated SVD 로 분해된 행렬의 내적을 계산하여 다시 복원된 데이터와 원본 데이터를 비교해 본다.

import numpy as np
from scipy.sparse.linalg import svds
from scipy.linalg import svd

# 원본 행렬을 출력하고 SVD를 적용할 경우, U, Sigma, Vt의 차원 확인
np.random.seed(121)
matrix = np.random.randn(6, 6)
print('원본 행렬:\n', matrix)
U, Sigma, Vt = svd(matrix, full_matrices=False)
print('\n분해 행렬 차원:', U.shape, Sigma.shape, Vt.shape)
print('\nSigma값 행렬:', Sigma)

# Truncated SVD로 Sigma 행렬의 특이값을 4개로 하여 Truncated SVD 수행
num_components = 4
U_tr, Sigma_tr, Vt_tr = svds(matrix, k=num_components)
print('\nTruncated SVD 분해 행렬 차원:', U_tr.shape, Sigma_tr.shape, Vt_tr.shape)
print('\nTruncated SVD Sigma 값 행렬:', Sigma_tr)
matrix_tr = np.dot(np.dot(U_tr, np.diag(Sigma_tr)), Vt_tr) # output of Truncated SVD

print('\nTruncated SVD로 분해 후 복원 행렬:\n', matrix_tr)

▶ 6*6 행렬을 SVD 분해하면 U, Sigma, Vt 가 각각 (6, 6) (6, )(6, 6) 차원이지만, Truncated SVD의 n_components를 4로 설정해 각각 (6, 4) (4, ) (4, 6)으로 분해하였다. Truncated SVD로 분해된 행렬로 다시 복원할 경우 완벽하게 복원되지 않고 근사적으로 복원됨을 알 수 있다.

 

[사이킷런 TruncatedSVD 클래스를 이용한 변환]

사이킷런의 TruncatedSVD 클래스는 사아파이의 svds와 같이 Truncated SVD 연산을 수행해 원본 행렬을 분해한 U, Sigma, Vt 행렬을 반환하지는 않는다. 사이킷런의 TruncatedSVD 클래스는 PCA 클래스와 유사하게 fit()와 transform() 을 호출해 원본 데이터를 몇 개의 주요 컴포넌트(K 컴포넌트 수)로 차원을 축소해 변환한다.

 

붓꽃 데이터 세트를 TruncatedSVD를 이용해 변환해본다.

from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
%matplotlib inline

iris = load_iris()
iris_ftrs = iris.data
# 2개의 주요 컴포넌트로 Truncated SVD 반환
tsvd = TruncatedSVD(n_components=2)
tsvd.fit(iris_ftrs)
iris_tsvd = tsvd.transform(iris_ftrs)

# 2개의 주요 컴포넌트로 PCA 반환
pca = PCA(n_components=2)
pca.fit(iris_ftrs)
iris_pca = pca.transform(iris_ftrs)

plt.scatter(x=iris_tsvd[:, 0], y=iris_tsvd[:, 1], c=iris.target)
plt.xlabel('TruncatedSVD Component 1')
plt.ylabel('TruncatedSVD Component 2')

# 산점도 2차원으로 TruncatedSVD 와 PCA 변환된 데이터 표현. 품종은 색깔로 구분
# TruncatedSVD 변환된 데이터는 왼쪽, PCA 변환 데이터는 오른쪽
fig, (ax1, ax2) = plt.subplots(figsize=(9, 4), ncols=2)
ax1.scatter(x=iris_tsvd[:, 0], y=iris_tsvd[:, 1], c=iris.target)
ax2.scatter(x=iris_pca[:, 0], y=iris_pca[:, 1], c=iris.target)
ax1.set_title('Truncated SVD')
ax2.set_title('PCA')

▶ 비교를 위해 PCA로 변환된 붓꽃 데이터 세트도 출력하였다. TruncatedSVD 변환 역시 PCA 와 유사하게 변환 후에 품종별로 어느 정도 클러스터링(군집화)이 가능할 정도로 각 변환 속성으로 뛰어난 고유성을 가지고 있음을 알 수 있다.

 

[붓꽃 데이터를 스케일링으로 변환한 뒤 TruncatedSVD, PCA 변환 수행]

from sklearn.preprocessing import StandardScaler

# 붓꽃 데이터를 StandardScaler로 변환
scaler = StandardScaler()
iris_scaled = scaler.fit_transform(iris_ftrs)

# 스케일링된 데이터를 기반으로 TruncatedSVD 변환 수행
tsvd = TruncatedSVD(n_components=2)
tsvd.fit(iris_scaled)
iris_tsvd = tsvd.transform(iris_scaled)

# 스케일링된 데이터를 기반으로 PCA 변환 수행
pca = PCA(n_components=2)
pca.fit(iris_scaled)
iris_pca = pca.transform(iris_scaled)

# TruncatedSVD 변환된 데이터는 왼쪽, PCA 변환 데이터는 오른쪽
fig, (ax1, ax2) = plt.subplots(figsize=(9, 4), ncols=2)
ax1.scatter(x=iris_tsvd[:, 0], y=iris_tsvd[:, 1], c=iris.target)
ax2.scatter(x=iris_pca[:, 0], y=iris_pca[:, 1], c=iris.target)
ax1.set_title('Truncated SVD Transformed')
ax2.set_title('PCA Transformed')

 

[두 개의 변환 행렬 값과 원본 속성별 컴포넌트 비율값 비교]

print((iris_pca - iris_tsvd).mean())
print((pca.components_ - tsvd.components_).mean())

※ 두 개의 숫자를 실수로 변환하면 다음과 같다:
2.341136543885606e-15 -> 0.000000000000002341136543885606
1.5959455978986625e-16 -> 0.00000000000000015959455978986625
이 두 숫자는 소수점 이하의 자릿수가 많고, 정수로 변환할 때 0에 가깝기 때문에 결과는 모두 0이 된다.

두 개의 소수를 실수로 변환했을 때 결과가 0인 이유는 다음과 같다:

1. 소수의 값이 매우 작음: 두 숫자는 모두 굉장히 작은 값이다.
   - ( 2.341136543885606 \times 10^{-15} )
   - ( 1.5959455978986625 \times 10^{-16} )

2. 지수로 표현된 소수점 이하 위치: 이 숫자들은 모두 1/1,000조 또는 그보다 작은 값이기 때문에, 0에 가깝다고 볼 수 있다.

다음 내용

 

[머신러닝] 차원 축소 - NMF

차원 축소란? [머신러닝] 차원 축소(Dimension Reduction)차원 축소(Dimension Reduction)차원 축소의 중요한 의미는차원 축소를 통해 좀 더 데이터를 잘 설명할 수 있는잠재적인 요소를 추출하는 데 있다.

puppy-foot-it.tistory.com


[출처]

파이썬 머신러닝 완벽 가이드

위키백과

수학 용어를 알면 개념이 보인다

728x90
반응형