
차원축소 알고리즘 PCA, LDA, SVD, NMF에 대해
일반적으로
💡 데이터포인트 : 특정 측정치 집계 기간 동안 측정치의 값,
다차원 공간에서 위치로 표현되는 벡터
http://taewan.kim/post/sample_example/
다중 공선성 문제 : 회귀 분석에서 독립변수들간 상관관계가 높아 분석시 부정적 영향이 미치는 문제
다차원 피처를 차원 축소해 피처 수를 줄였을 때의 장점
1. 직관적으로 데이터 해석 가능
수 십개 이상의 피처가 있는 데이터의 경우 시각화를 통한 데이터 특성 파악 불가능
but 3차원 이하의 차원 축소를 통해 시각적으로 데이터를 압축헤 표현 가능
2. 학습 데이터 크기가 줄어 학습에 필요한 처리 능력 감소
피처 선택 (Feature Selection)
특정 피처에 종속성이 강한 불필요한 피처는 제거 + 데이터의 특징이 잘 나타나 있는 주요 피처만 선택
피처 추출(Feature Extraction)
기존 피처를 차원의 중요 피처로 압축해 추출
새롭게 추출된 중요 특성은 기존 피처와 완전히 다른 값
단순 압축이 아닌 피처를 함축적으로 더 잘 설명할 수 있는 또 다른 공간으로 매핑해 추출

함축적 특성 추출 :
기존 피처가 전혀 인지하기 어려웟던 잠재적인 요소(Latent Factor)를 추출하는 것
❗️ 차원축소는 단순히 데이터의 압축을 의미하는 것이 아닌
차원 축소를 통해 좀 더 데이터를 잘 설명할 수 있는 잠재적인 요소를 추출하는데 있음.
PCA, SVD, NMF : 잠재적 요소를 찾는 대표적인 차원 축소 알고리즘,
많은 차원을 가지고 있는 이미지나 텍스트에서 차원축소를 통한 잠재적 의미를 찾는 과정에 잘 활용됨
활용 예시 :
1. 이미지 데이터
잠재된 특성을 피처로 도출해 함축적 형태의 이미지 변환, 압축 수행 가능
변환 후 원본에 비해 작은 차원이 되었기에 이미지 분류 등 분류 수행시에 과적합 영향력이 작아져
원본 데이터로 예측 하는 것 보다 예측 성능을 더 끌어 올릴 수 있음
❗️이미지 자체가 가지고 있는 차원의 수가 너무 크기에 비슷한 이미지라도 조그만 픽셀의 차이가 잘못된 예측으로 이어질 수 있음
2.텍스트 문서의 숨겨진 의미 추출
어떠한 의미, 의도를 가지고 있는 단어들을 알고리즘이 시맨틱이나 토픽을 잠재 요소로 간주해 이를 찾아냄
SVD, NMF은 이러한 시맨틱 토픽 모델링을 위한 기반 알고리즘으로 사용됨
가장 대표적인 차원 축소 기법, 주성분 분석
원본 데이터의 피처 개수에 비해 매우 작은 주성분으로 원본 데이터의 총 변동성을 대부분 설명할 수 있는 분석법
ex. 키와 몸무게 2개의 피처를 가지고 있는 데이터 세트 가정

2개의 피처를 한 개의 주성분을 가진 데이터 세트로 차원 축소
데이터 변동성이 가장 큰 방향으로 축을 생성 > 새롭게 생성된 축으로 데이터 투영

이렇게 생성된 벡터 축에 원본 데이터를 투영하면 벡터 축의 개수 만큼 원본 데이터가 차원 축소됨

입력 데이터의 공분산 행렬을 고유값 분해 후 구한 고유벡터에 입력데이터를 선형 변환하는 것
고유벡터 : PCA의 주성분 벡터로서 입력 데이터의 분산이 큰 방향을 나타냄
고윳값 : 고유 벡터의 크기를 나타내며 동시에 입력 데이터의 분산을 나타냄
💡 공분산과 공분산 행렬 : https://kh-mo.github.io/notation/2021/01/02/covariance/
선형 변환 : 특정 벡터에 행렬 A를 곱해 새로운 벡터로 변환하는 것
특정 벡터를 하나의 공간에서 다른 공간으로 투영하는 개념으로도 볼 수 있음 이 경우 행렬을 곧바로 공간으로 가정
분산 : 한 개의 특정한 변수의 데이터 변동
공분산 : 두 변수 간의 변동
사람 키 변수를 X, 몸무게 변수 Y라고 하면 공분산 Cov(X,Y) > 0은 X가 증가할 때 Y도 증가한다는 의미
고유 벡터 : 행렬 A를 곱하더라도 방향이 변하지 않고 크기만 변하는 벡터
Ax = ax(A는 행렬, 호는 고유벡터, a는 스칼라값), 고유 벡터 다수 존재
정방 행렬 > 최대 그 차원 수 만큼 고유벡터를 가질 수 있음
ex. 2x2 행렬 = 고유벡터 2개 3x3 행렬 = 고유벡터 3개
위와 같이 고유벡터는 행렬이 작용하는 힘의 방향과 관계가 있어서 행렬을 분해하는 데 사용됨
공분산 행렬 :
1. 여러 변수와 관련된 공분산을 포함하는 정방형 행렬
각 변수(X,Y,Z)의 분산 = 대각선 원소 > X,Y,Z의 분산 : 3.0, 5.5, 0.91
변수 쌍 간의 공분산 = 대각선 이외의 원소 > X,Y의 공분산 : -0.71/ Y,Z 공분산 : 0.28

정방행렬(Square Matrix)이자 대칭행렬(Symmetric Matrix)
정방행렬: 열과 행이 같은 행렬
대칭행렬 : 정방행렬 중에서 대각 원소를 중심으로 원소 값이 대칭(A^t = A)
개별 분산값을 대각 원소로 하는 대칭행렬
대칭행렬은 항상 고유벡터를 직교행렬(orthogonal matrix), 고유값을 정방 행렬로 대각화할 수 있음
대각화 : 고유값과 고유벡터를 활용하기 위한 하나의 방법, 고유값 분해라고도 함
직교행렬 : 행벡터와 열벡터가 유클리드 공간의 정규 직교 기저를 이루는 실수 행렬
입력 데이터의 공분산 행렬을 C라고 하면 공분산 행렬의 특성으로 인해 다음과 같이 분해할 수 있다

고유벡터와 고유값 행렬

공분산C = 고유벡터 직교 행렬 x 고유값 정방 행렬 x 고유벡터 직교 행렬의 전치 행렬
ei > i번 째 고유벡터, e1 > 가장 분산이 큰 방향을 가진 고유 벡터,
e2 > e1에 수직이면서 다음으로 가장 분산이 큰 방향을 가진 고유벡터
PCA는 많은 속성으로 구성된 원본 데이터를 그 핵심을 구성하는 데이터로 압축한 것
sepal length, sepal width, petal length, petal width 이 4개의 속성을
2개의 PCA 차원으로 압축해 원래 데이터 세트와 압축된 데이터 세트가 어떻게 달라졌는지 확인
사이킷런의 붓꽃 데이터를 load_iris() API를 이용해 로딩 후 편하게 시각화하기 위해 DataFrame으로 변환
from sklearn.datasets import load_iris
import pandas as pd
import matplotlib.pyplot as pit
%matplotlib inline
iris = load_iris()
# 넘파이 데이터 세트를 판다스 DataFrame으로 변환
columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
irisDF = pd.DataFramedris.data, columns=columns)
irisDF['target']=iris.target
irisDF.head(3)
각 품종에 따라 원본 붓꽃 데이터 세트가 어떻게 분포돼 있는지 2차원으로 시각화
sepal length, sepal width를 각 X축, Y축으로 품종 데이터 분포를 나타냄
#setosa는 세모, versicolor는 네모, virginica는 동그라미로 표현
markers=['^', 's', 'o']
'''
setosa의 target 값은 0, versicolor는 1, virginica는 2,
각 target별로 다른 모양으로 산점도로 표시
'''
for i, marker in enumerate(markers):
x_axis_data = irisDF[irisDF['target']=i]['sepal_length']
y_axis_data = irisDF[irisDF['target']=i]['sepal_width']
pit.scatter(x_axis_data, y_axis_data, marker=marker,
label=iris.target_names[i])
plt.legend()
pit.xlabel('sepal length')
pit.ylabel('sepal width')
plt.show()
Setosa 품종 : sepal width가 3.0보다 크고, sepal length가 6.0 이하인 곳에 일정하게 분포돼 있음 Versicolor, virginica : sepal width와 sepal length 조건만으로는 분류가 어려운 복잡한 조건임을 알 수 있다
PCA는 여러 속성의 값을 연산해야 하므로 속성의 스케일에 영향을 받기에 여러 속성을 PCA로 압축 하기 전에 각 속성값을 동일한 스케일로 변환하는 과정 필요
사이킷런의 StandardScaler를 이용해 평균이 0, 분산이 1인 표준 정규 분포로 iris 데이터 세트의 속성값들을 변환
from sklearn.preprocessing import StandardScaler
'''
Target 값을 제외한 모든 속성 값을 StandardScaler를 이용해 표준 정규 분포를 가지는 값들로 변환
iris_scaled = StandardScaler().fit_transform(irisDF.iloc[:, :-1])
'''
스케일링 적용된 데이터 세트에 PCA를 적용해 붓꽃 데이터를 2차원 PCA 데이터로 변환
from sklearn.decomposition import PCA # PCA import
pca = PCA(n_components=2)
'''
PCA 클래스는 생성 파라미터로 n_components를 입력받음
n_components > PCA로 변환 할 차원의 수를 의미
'''
# fit()과 transformO을 호출해 PCA 변환 데이터 반환
pca.fit(iris_scaled)iris_pca = pea.transform(iris_scaled)
print(iris_pca.shape)
> (150, 2)
DataFrame 으로 변환 후 데이터 값 확인
#PCA 변환된 데이터의 칼럼명을 각각 pca_component_1, pca_component_2로 명명
pca_columns=['pca_component_1','pca_component_2']
irisDF_pca = pd.DataFrame(iris_pca, columns=pca_columns)
irisDF_pca['target']=iris.target irisDF_pca.head(3)
pca_component_1 속성을 X축, pca_component_2 속성을 Y축으로 해 2차원 상에서 시각화
#setosa를 세모, versicolor를 네모, virginica를 동그라미로 표시
markers=['^', 's', 'o']
#pca_component_1 을 x축, pc_component_2를 y축으로 scatter plot 수행
for i, marker in enumerate(markers):
x_axis_data = irisDF_pca[irisDF_pca['target']=i]['pca_component_1']
y_axis_data = irisDF_pca[irisDF_pca['target']=i]['pca_component_2']
pit.scatter(x_axis_data, y_axis_dataz marker=marker,label=iris.target_names[i])
pit.legend()
plt.xlabel('pca_component_1')
pit.ylabel('pca_component_2')
plt.show()
pca_component_1이 원본 데이터의 변동성을 잘 반영 했기에 PCA 변환 후에도
pca_component_1 축을 기반으로 setosa 품종을 명확하게 구분 가능 하며
Versicolor, Viriginica는 pca_component_1 축을 기반으로 서로 겹치는 부분이 일부 존재 했지만 비교적 잘 구분 됨
PCA 변환을 수행한 PCA 객체의 explainedvariance_ratio 속성은 전체 변동성에서 개별 PCA 컴포넌트별 차지하는 변동성 비율 제공
print(pca.explained_variance_ratio_)
> [0.72962445 0.22850762]
첫 번째 PCA 변환 요소인 pca_component_l이 전체 변동성의 약 72.9%를 차지
두 번째인 pca_component_2가 약 22.8%를 차지
그렇기에 PCA를 2개 요소로만 변환해도 원본 데이터 의 변동성을 95% 설명할 수 있음
랜덤 포레스트(Random Forest) 적용 결과
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
import numpy as np
rcf = RandomForestClassifier(random_state=156)
scores = cross_val_score(rcf, iris.data,
iris.targetzscoring='accuracy', cv=3)
print('원본 데이터 교차 검증 개별 정확도:',scores)
print('원본 데이터 평균 정확도:', np.mean(scores))
> 원본 데이터 교차 검증 개별 정확도:[0.98 0.94 0.96]
원본 데이터 평균 정확도:0.96
pca_X = irisDF_pca[['pca_component_1', 'pca_component_2']]
scores_pca = cross_val_score(ref, pca_X, iris.target,
scoring='accuracy', cv=3 )
print('PCA 변환 데이터 교차 검증 개별 정확도:',scores_pca)
print('PCA 변환 데이터 평균 정확도:', np.mean(scores_pca))
> PCA 변환 데이터 교차 검증 개별 정확도:[0.88 0.88 0.88]
PCA 변환 데이터 평균 정확도:0.88
원본 데이터 세트 대비 예측 정확도는 PCA 변환 차원 개수에 따라 예측 성능이 떨어질 수 밖에 없음
8%의 정확도 하락은 비교적 큰 성능 수치의 감소지만 속성 개수가 50% 감소한 것을 고려한다면
PCA 변환 후에도 원본 데이터의 특성을 상당 부분 유지 하고 있음
선형 판별 분석법
특정 공간상에서 클래스 분리를 최대화하는 축을 찾기 위해 클래스 간 분산과 클래스 내부 분산의 비율 최대화
클래스 간의 분산은 최대한 크게 가져가고 클래스 내부의 분산은 최대한 작게 가져감
PCA와 차이점 :

붓꽃 데이터 세트 로드 + 정규 분포로 스케일링
from sklearn,discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
iris = load_iris()
iris_scaled = StandardScaler().fit_transform(iris.data)
2개의 컴포넌트로 붓꽃 데이터를 LDA 변환
❗️ 비지도학습 PCA와 달리 지도학습이기에 클래스 결정 값이 변환 시 필요
그렇기에 lda 객체의 fit() 매서드를 호출 할 때 결정값이 입력됨
lda = LinearDiscriminantAnalysis(n_components=2)
lda.fit(iris_scaled, iris.target)
iris.1da = lda.transform(iris_scaled)
print(iris_lda.shape)
> (150,2)
LDA 변환된 입력 데이터 값을 2차원 평면에 품종별 표현
import pandas as pdimport
matplotlib.pyplot as pit
%matplotlib
inlinelda_columns=['lda_component_1', 'lda_component_2']
irisDF_lda = pd.DataFrame(iris_lda, columns=lda_columns)
irisDF_lda['target']=iris.target
#setosa는 세모, versicolor는 네모, virginica는 동그라미로 표현
markers=['^', 's', 'o']
'''
setosa의 target 값은 0, versicolor는 1, virginica는 2,
각 target별로 다른 모양으로 산점도로 표시
'''
for iz marker in enumerate(markers):
x_axis_data = irisDF_lda [irisDF_lda['target']=i]['lda_component_1']
y_axis_data = irisDF_lda[irisDF_lda['target']=i]['lda_component_2']
plt.scatter(x_axis_data, y_axis_data, marker=jnarker,
label=iris.target_names[i])
pit.legend(loc='upper right')
pit.xlabel('lda_component_1')
pit.ylabel('lda_component_2')
plt.show()
특이값 분해, mxn 크기의 행렬 A를 다음과 같이 분해하는 것

특이 벡터 : U(mxm), V(nxn) 안의 벡터, 모두 서로 직교
Σ(m x n) : 대각 행렬, 대각 원소를 제외한 나머지 값 = 0인 행렬
특이값 : Σ의 대각에 위치한 값
일반적으로 Σ의 비대각인 부분인 대각 원소 중에 특이값이 0인 부분을 모두 제거하고
제거된 Σ에 해당하는 U, V의 원소도 제거해 차원을 줄인 형태로 적용
(mxp, pxp, pxn)
분해한 행렬을 다시 내적하여 곱하면 원본 행렬로 복원 가능함
#넘파이의 svd 모듈 임포트
import numpy as np from numpy.linalg
import svd
#4X4 랜덤 행렬 a 생성
np.random.seed(121)
a = np.random.randn(4, 4)
print(np.round(a, 3))
Uz 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))
행렬의 개별 행 벡터간 의존성을 없애기 위해 랜덤으로 생성한 행렬 a에 SVD를 적용해 U, Sigma, Vt 도출
U행렬 4x4, Vt 행렬 4x4,
Sigma 1차원 행렬 (4, ) 반환 : 대각에 위치한 값 만을 가지기에 1차원 행렬로 표현
#Sigma를 다시 0을 포함한 대칭행렬로 변환
Sigmajnat = np.diag(Sigma)
a_ = np.dot(np.dot(U, Sigma_mat), Vt)
print(np.round(a_, 3))
a[2] = a[0] + a[1]
a[3] = a[0]
#다시 SVD를 수행해 Sigma 값 확인
U, Sigma, Vt = svd(a)
print(np.round(a, 3))
print(U.shape, Sigma.shape, Vt.shape)
print('Sigma Value:\n'z np.round(Sigma, 3))
이전과 차원을 같지만 Sigma 값 중 2개가 0으로 나타났으며 이는 선형 독립인 로우 벡터의 개수가 2개라는 의미
#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))
import numpy as npfrom scipy.sparse.linalg
import svds from scipy.linalg
import 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_trz Sigma_tr, Vt_tr = svds(matrix, k=num_components)
print('XnTruncated SVD 분해 행렬 차원:', U_tr.shape, Sigma_tr.shape, Vt_tr.shape)
print('XnTruncated SVD Sigma값 행렬:', Sigma_tr)
matrix_tr = np.dot(np.dot(U_tr, np.diag(Sigma_tr)), Vt_tr)
# output of TruncatedSVD
print('XnTruncated SVD로 분해 후 복원 행렬:\n'z matrix_tr)
n_components의 설정에 따라 차원수가 다르게 분해 됨
Truncated SVD로 분해할 경우 완벽하지 않고 근사적으로 복원 됨을 볼 수 있음
from sklearn.decomposition import TruncatedSVD, PCA
from sklearn.datasets import load_iris
import matplotlib.pyplot as pit
%matplotlib inline
iris = load_iris()
iris_ftrs = iris.data
#2개의 주요 컴포넌트로 TruncatedSVD 변환
tsvd = TruncatedSVD(n_components=2)
tsvd.fit(iris_ftrs)
iris_tsvd = tsvd.transform(iris_ftrs)
#산점도 2차원으로 TruncatedSVD 변환된 데이터 표현, 품종은 색깔로 구분
pit.scatter(x=iris_tsvd[:, 0], y= iris_tsvd[:, 1 ], c= iris.target)
plt.xlabel('TruncatedSVD Component 1')
pit.ylabel('TruncatedSVD Component 2')

left : TruncatedSVD 변환된 붓꽃 데이터 세트 / right : 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')

두개의 변환이 서로 동일
데이터 세트가 스케일링으로 데이터 중심이 동일해지면 사이킷런의 SVD, PDA는 동일한 변환 수행
but PCA는 밀집 행렬에 대한 변환만 가능하며 SVD는 희소 행렬에 대한 변환도 가능
낮은 랭크를 통한 행렬 근사 방식의 변형, 원본 행렬 내의 모든 원소 값이 모두 양수 (0 이상)라는게 보장 되면 두 개의 양수 행렬로 분해될 수 있는 기법

1. 일반적으로 행렬 분해를 하면 길고 가는 행렬 W, 작고 넓은 행렬 H로 분해됨
이러한 행렬을 잠재 요소 특성으로 가진다
2.
W : 원본 행에 대해 잠재 요소의 값이 얼마나 되는지
H : 잠재 요소가 원본 열로 어떻게 구성 되었는지
3. 이미지 변환 및 압축, 텍스트 토픽 도출 등 사용
from sklearn.decomposition import NMF
from sklearn.datasets import load_iris
import matplotlib.pyplot as pit
%matplotlib inline
iris = load_iris()
iris_ftrs = iris.data
nmf = NMF(n_components=2)
nmf.fit(iris_ftrs)
iris_nmf = nmf.transform(iris_ftrs)
pit.scatter(x=iris_nmf[:, 0], y= iris_nmf[:, 1], c= iris.target)
plt.xlabel('NMF Component 1')
plt.ylabel('NMF Component 2')
