: PCA와 유사. 정방 행렬뿐만 아니라 행과 열의 크기가 다른 행렬에도 적용 가능
Full SVD
Compact SVD (일반적)
Truncated SVD
SVD 사용: 보통 넘파이나 사이파이 라이브러리를 이용
from numpy.linalg import svd
# or
from scipy.linalg import svd
# numpy의 svd 모듈 import
import numpy as np
from numpy.linalg import svd
# 4X4 Random 행렬 a 생성
np.random.seed(121)
a = np.random.randn(4,4)
print(np.round(a, 3))
> [[-0.212 -0.285 -0.574 -0.44 ]
> [-0.33 1.184 1.615 0.367]
> [-0.014 0.63 1.71 -1.327]
> [ 0.402 -0.191 1.404 -1.969]]
U, Sigma, Vt = svd(a) # a = 원본 행렬
print(U.shape, Sigma.shape, Vt.shape)
print('U matrix:\n',np.round(U, 3))
print('Sigma Value:\n',np.round(Sigma, 3)) # 대각행렬 => 대각에 위치한 값 == 1, 외에 나머지 값 0
print('V transpose matrix:\n',np.round(Vt, 3))
> (4, 4) (4,) (4, 4)
> U matrix:
> [[-0.079 -0.318 0.867 0.376]
> [ 0.383 0.787 0.12 0.469]
> [ 0.656 0.022 0.357 -0.664]
> [ 0.645 -0.529 -0.328 0.444]]
> Sigma Value:
> [3.423 2.023 0.463 0.079]
> V transpose matrix:
> [[ 0.041 0.224 0.786 -0.574]
> [-0.2 0.562 0.37 0.712]
> [-0.778 0.395 -0.333 -0.357]
> [-0.593 -0.692 0.366 0.189]]
다시 행렬 A로 복원해보기 ⇒
단, 의 경우 0이 아닌 값만 1차원으로 추출했으므로, 다시 0을 포함한 대칭행렬로 변환 뒤 내적() 수행
# Sima를 다시 0 을 포함한 대칭행렬로 변환
Sigma_mat = np.diag(Sigma)
a_ = np.dot(np.dot(U, Sigma_mat), Vt)
print(np.round(a_, 3))
데이터 세트가 로 우간 의존성이 있을 경우, 어떻게 값이 변하고, 이에 따른 차원 축소 진행되는지 알아보기
행렬 ⇒ 3번째 로우 = 1번째 로우 + 2번째 로우 & 4번째 로우 = 1번째 로우
a[2] = a[0] + a[1]
a[3] = a[0]
print(np.round(a,3))
> [[-0.212 -0.285 -0.574 -0.44 ]
> [-0.33 1.184 1.615 0.367]
> [-0.542 0.899 1.041 -0.073]
> [-0.212 -0.285 -0.574 -0.44 ]]
이 행렬을 다시 SVD 분해
# 다시 SVD를 수행하여 Sigma 값 확인
U, Sigma, Vt = svd(a)
print(U.shape, Sigma.shape, Vt.shape)
print('Sigma Value:\n',np.round(Sigma,3))
> (4, 4) (4,) (4, 4)
> Sigma Value:
> [2.663 0.807 0. 0. ]
이제 다시 행렬 A를 복원해볼 것
⇒ 전체 데이터 사용하지 않고, 의 0에 대응되는 를 제외하고 복원해보겠음
⇒ 즉, 행렬 중 선행 두 개의 열만 추출 하고, 의 경우는 선행 두 개의 행만 추출 복원해보는 것
```python
# 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))
> (4, 2) (2, 2) (2, 4)
> [[-0.212 -0.285 -0.574 -0.44 ]
> [-0.33 1.184 1.615 0.367]
> [-0.542 0.899 1.041 -0.073]
> [-0.212 -0.285 -0.574 -0.44 ]]
```
Truncated SVD : 행렬에 있는 대각원소, 즉 특이값 중 상위 일부 데이터만 추출해 분해하는 방식
Truncated SVD 사용 : 사이파이에서만 지원됨
from scipy.sparse.linalg import svds
검증 수행 순서
1. 임의의 원본 행렬 6x6을 Normal SVD로 분해해 ⇒ 행렬의 차원, 행렬 내 특이값 확인
2. 다시 Truncated SVD로 분해해 ⇒ 행렬의 차원, 행렬 내 특이값 확인
3. Truncated SVD로 분해된 행렬의 내적을 계산해서 ⇒ 원상 복구하여 원본데이터와 비교
import numpy as np
from scipy.sparse.linalg import svds # Truncated SVD
from scipy.linalg import svd # Nomarl SVD
# 원본 행렬을 출력하고, SVD를 적용할 경우 U, Sigma, Vt 의 차원 확인
np.random.seed(121)
matrix = np.random.random((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 TruncatedSVD
print('\nTruncated SVD로 분해 후 복원 행렬:\n', matrix_tr)
> 원본 행렬:
> [[0.11133083 0.21076757 0.23296249 0.15194456 0.83017814 0.40791941]
> [0.5557906 0.74552394 0.24849976 0.9686594 0.95268418 0.48984885]
> [0.01829731 0.85760612 0.40493829 0.62247394 0.29537149 0.92958852]
> [0.4056155 0.56730065 0.24575605 0.22573721 0.03827786 0.58098021]
> [0.82925331 0.77326256 0.94693849 0.73632338 0.67328275 0.74517176]
> [0.51161442 0.46920965 0.6439515 0.82081228 0.14548493 0.01806415]]
> 분해 행렬 차원: (6, 6) (6,) (6, 6)
> Sigma값 행렬: [3.2535007 0.88116505 0.83865238 0.55463089 0.35834824 0.0349925 ]
> Truncated SVD 분해 행렬 차원: (6, 4) (4,) (4, 6)
> Truncated SVD Sigma값 행렬: [0.55463089 0.83865238 0.88116505 3.2535007 ]
> Truncated SVD로 분해 후 복원 행렬:
> [[0.19222941 0.21792946 0.15951023 0.14084013 0.81641405 0.42533093]
> [0.44874275 0.72204422 0.34594106 0.99148577 0.96866325 0.4754868 ]
> [0.12656662 0.88860729 0.30625735 0.59517439 0.28036734 0.93961948]
> [0.23989012 0.51026588 0.39697353 0.27308905 0.05971563 0.57156395]
> [0.83806144 0.78847467 0.93868685 0.72673231 0.6740867 0.73812389]
> [0.59726589 0.47953891 0.56613544 0.80746028 0.13135039 0.03479656]]
6 ⇒ 4 차원 & Truncated SVD 적용 후 원상 복구 하면, 완벽하진 않지만 근사하게 복원됨
사이킷런 TruncatedSVD 클래스를 이용한 변환
사이파이의 SVDs와 같이 행렬을 반환하지는 않음.
사이킷런의 PCA 클래스와 유사하게, fit()
, transform()
으로 원본 데이터를 몇 개의 주요 컴포넌트로 차원축소해 변환
즉, 원본 데이터를 Truncated SVD 방식으로 분해된 행렬에 선형변환하여 생성
from sklearn.decomposition import TruncatedSVD, PCA
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
%matplotlib inline
iris = load_iris()
iris_ftrs = iris.data
# 2개의 주요 component로 TruncatedSVD 변환
tsvd = TruncatedSVD(n_components=2)
tsvd.fit(iris_ftrs)
iris_tsvd = tsvd.transform(iris_ftrs)
# 2개의 주요 component로 TruncatedSVD 변환 (비교를 위해)
pca = PCA(n_components=2)
pca.fit(iris_ftrs)
iris_pca = pca.transform(iris_ftrs)
# TruncatedSVD 변환 데이터를 왼쪽에, PCA변환 데이터를 오른쪽에 표현
fig, (ax1, ax2) = plt.subplots(figsize=(18,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')
ax1.set_xlabel('TruncatedSVD Component 1')
ax1.set_ylabel('TruncatedSVD Component 2')
ax2.set_xlabel('PCA Component 1')
ax2.set_ylabel('PCA Component 2')
TruncatedSVD 역시 PCA와 유사하게 변환 후 품종별로 어느정도 클러스터링이 가능할정도로 고유성 가지고 있음
사실 두 클래스를 모두 뜨덩보면, 모두 SVD를 이용해 행렬을 분해함 ⇒ 원본 데이터를 스케일링으로 변환 후에 적용해보면 거의 동일함
from sklearn.preprocessing import StandardScaler
# iris 데이터를 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.3419865583888347e-15
> 6.245004513516506e-17
: 원본 행렬 내의 모든 원소값이 모두 양수(0 이상)라는 게 보장되면, 두 개의 기반 양수 행렬로 분해될 수 있는 기법
: Truncated SVD와 같이 낮은 랭크를 통한 행렬 근사 방식의 변형
from sklearn.decomposition import NMF
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
%matplotlib inline
iris = load_iris()
iris_ftrs = iris.data
nmf = NMF(n_components=2)
nmf.fit(iris_ftrs)
iris_nmf = nmf.transform(iris_ftrs)
plt.scatter(x=iris_nmf[:,0], y= iris_nmf[:,1], c= iris.target)
plt.xlabel('NMF Component 1')
plt.ylabel('NMF Component 2')