차원 축소란?
SVD(Singular Value Decompostion, 특이값 분해)
[PCA vs SVD]
- PCA: 정방행렬만을 고유벡터로 분해
- SVD: 정방행렬 뿐 아니라 행과 열의 크기가 다른 행렬에도 적용 가능
여기에서 각 행렬은 다음과 같은 성질을 가진다.
- U는 m × m 크기를 가지는 유니터리 행렬이다.
- Σ는 m × n 크기를 가지며, 대각선상에 있는 원소의 값은 음수가 아니며 나머지 원소의 값이 모두 0인 대각행렬이다.
- V∗ 는 V의 전치 행렬로, n × n 유니터리 행렬이다.
- 행렬 M을 이와 같은 세 행렬의 곱으로 나타내는 것을 M의 특잇값 분해라고 한다.
★ 유니터리 행렬이란?
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에 가깝다고 볼 수 있다.
다음 내용
[출처]
파이썬 머신러닝 완벽 가이드
위키백과
수학 용어를 알면 개념이 보인다
'[파이썬 Projects] > <파이썬 머신러닝>' 카테고리의 다른 글
[머신러닝] 군집화: k-평균 (0) | 2024.10.25 |
---|---|
[머신러닝] 군집화 (Clustering) (0) | 2024.10.25 |
[머신러닝] 차원 축소 - NMF (1) | 2024.10.24 |
[머신러닝] 차원 축소 - LDA (0) | 2024.10.24 |
[머신러닝] 차원 축소 - PCA (0) | 2024.10.24 |