[파이썬 머신러닝 완벽 가이드] 6장. 차원 축소
# 22.01.30 머신러닝 스터디 6장. 차원 축소
<파이썬 머신러닝 완벽 가이드>¶
차원축소 ( p377 ~ p408 )¶
01. 차원 축소 ( Dimension Reduction ) 개요¶
차원 축소란? : 매우 많은 피처로 구성된 다차원 데이터 세트의 차원을 축소해 새로운 차원의 데이터 세트를 생성하는 것
일반적으로 차원이 증가할수록 데이터 포인트 간의 거리가 기하급수적으로 멀어지게 되고 희소한 구조를 가지게 된다. 또한 피처가 많아지면 상대적으로 적은 차원에서 학습된 모델보다 예측 신뢰도가 떨어지고 피처가 많을 경우 개별 피처간 상관관계가 높을 가능성이 있어 다중 공선성 문제가 발생할 수 있습니다. 따라서 차원 축소를 통해 피처 수를 줄이면 더 직관적으로 데이터를 해석할 수 있고 3차원 이하의 차원 축소를 통해 시각적으로 데이터를 압축해서 표현할 수도 있다. 또한 차원 축소를 할 경우 학습 데이터의 크기가 줄어들어서 학습에 필요한 처리 능력도 줄일 수 있다.
- 피처 선택 ( Feature Selection ) : 종속성이 강한 불필요한 피처는 아예 제거하고 데이터의 특징을 잘 나타내는 주요 피처만 선택하는 것
- 피처 추출 ( Feature Extraction ) : 기존 피처를 저차원의 중요 피처로 압축해서 추출하는 것
매우 많은 차원을 가지고 있는 이미지나 텍스트에서 잠재적인 의미를 찾아주기 위해 차원 축소 알고리즘을 많이 사용한다.
02. PCA ( Principal Component Analysis )¶
여러 변수 간에 존재하는 상관관계를 이용해 이를 대표하는 주성분 ( Principal Component )을 추출해 차원을 축소하는 기법이다.
- 데이터 변동성이 가장 큰 방향으로 축 생성
- 새로운 축으로 데이터 투영
- 새로운 축 기준으로 데이터 표현
선형대수 관점에서 해석할 경우 입력 데이터의 공분산 행렬을 고유값 분해하고, 이렇게 구한 고유벡터에 입력 데이터를 선형 변환하는 것으로 볼 수 있다. 공분산 Cov(X, Y) > 0은 X가 증가할 때 Y도 증가한다는 의미이다. 고유벡터는 행렬 A를 곱하더라도 방향이 변하지 않고 그 크기만 변하는 벡터를 의미한다. 고유값은 고유벡터의 크기를 나타내며 동시에 입력 데이터의 분산을 나타냅니다.
입력데이터의 공분산 행렬이 고유벡터와 고유값으로 분해될 수 있으며, 이렇게 분해된 고유벡터를 이용해 입력 데이터를 선형 변환하는 방식이 PCA이다.
- 입력 데이터 세트의 공분산 행렬을 생성합니다.
- 공분산 행렬의 고유벡터와 고유값을 계산합니다.
- 고유값이 가장 큰 순으로 K개(PCA 변환 차수만큼)만큼 고유벡터를 추출합니다.
- 고유값이 가장 큰 순으로 추출된 고유벡터를 이용해 새롭게 입력 데이터를 변환합니다.
from sklearn.datasets import load_iris
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
iris = load_iris()
# 넘파이 데이터 세트를 판다스 DataFrame
columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
irisDF = pd.DataFrame(iris.data, columns = columns)
irisDF['target'] = iris.target
irisDF.head(3)
sepal_length | sepal_width | petal_length | petal_width | target | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | 0 |
1 | 4.9 | 3.0 | 1.4 | 0.2 | 0 |
2 | 4.7 | 3.2 | 1.3 | 0.2 | 0 |
#setosa는 세모, versicolor는 네모, virginica는 동그라미로 표현
markers=['^', 's', 'o']
#setosa의 target 값은 0, versicolor는 1, virginica는 2. 각 target 별로 다른 shape으로 scatter plot
for i, marker in enumerate(markers):
x_axis_data = irisDF[irisDF['target']==i]['sepal_length']
y_axis_data = irisDF[irisDF['target']==i]['sepal_width']
plt.scatter(x_axis_data, y_axis_data, marker=marker,label=iris.target_names[i])
plt.legend()
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.show()
Setosa의 경우 sepal width가 3보다 크고 sepal length가 6 이하인 곳에 일정하게 분포되어있다.
Versicolor와 virginica의 경우에는 sepal width와 sepal length 조건만으로는 분류가 어렵다.
따라서 PCA로 4개 속성을 2개로 압축한 뒤 앞의 예제와 비슷하게 2개의 PCA 속성으로 2차원 시각화를 진행해보겠다.
from sklearn.preprocessing import StandardScaler
# Target 값을 제외한 모든 속성 값을 StandardScaler를 이용하여 표준 정규 분포를 가지는 값들로 변환
iris_scaled = StandardScaler().fit_transform(irisDF.iloc[:, :-1])
스케일링이 적용된 데이터 세트에 PCA를 적용
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
#fit( )과 transform( ) 을 호출하여 PCA 변환 데이터 반환
pca.fit(iris_scaled)
iris_pca = pca.transform(iris_scaled)
print(iris_pca.shape)
(150, 2)
# 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 # target 추가
irisDF_pca.head(3)
pca_component_1 | pca_component_2 | target | |
---|---|---|---|
0 | -2.264703 | 0.480027 | 0 |
1 | -2.080961 | -0.674134 | 0 |
2 | -2.364229 | -0.341908 | 0 |
#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']
plt.scatter(x_axis_data, y_axis_data, marker=marker,label=iris.target_names[i])
plt.legend()
plt.xlabel('pca_component_1')
plt.ylabel('pca_component_2')
plt.show()
앞에 임의의 피처 2개로 구분하는 것보다 PCA를 통해 구분을 하니 Versicolor와 Virginica까지 명확하게 구분이 되는 모습이다.
# 전체 변동성에서 개별 PCA 컴포넌트별로 차지하는 변동성 비율
print(pca.explained_variance_ratio_)
# 2개의 요소로 원본 데이터의 변동성을 95% 설명할 수 있다. ( 오히려 많으면 과적합이 날 수 있는 걸 생각!! )
[0.72962445 0.22850762]
# 원본 데이터에 랜덤 포레스트 적용
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.target,scoring='accuracy',cv=3)
print('원본 데이터 교차 검증 개별 정확도:',scores)
print('원본 데이터 평균 정확도:', np.mean(scores))
원본 데이터 교차 검증 개별 정확도: [0.98 0.94 0.96] 원본 데이터 평균 정확도: 0.96
# PCA 결과에 랜덤 포레스트
pca_X = irisDF_pca[['pca_component_1', 'pca_component_2']]
scores_pca = cross_val_score(rcf, 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
속성 개수가 50% 감소한 것을 고려할 경우 PCA 변환 후 10%정도의 정확도 하락은 원본 데이터의 특성을 상당 부분 유지하고 있음을 알 수 있다.
Credit card 데이터
import pandas as pd
df = pd.read_excel('credit_card.xls', sheet_name='Data')
print(df.shape)
df.head(3)
(30001, 25)
Unnamed: 0 | X1 | X2 | X3 | X4 | X5 | X6 | X7 | X8 | X9 | ... | X15 | X16 | X17 | X18 | X19 | X20 | X21 | X22 | X23 | Y | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | ID | LIMIT_BAL | SEX | EDUCATION | MARRIAGE | AGE | PAY_0 | PAY_2 | PAY_3 | PAY_4 | ... | BILL_AMT4 | BILL_AMT5 | BILL_AMT6 | PAY_AMT1 | PAY_AMT2 | PAY_AMT3 | PAY_AMT4 | PAY_AMT5 | PAY_AMT6 | default payment next month |
1 | 1 | 20000 | 2 | 2 | 1 | 24 | 2 | 2 | -1 | -1 | ... | 0 | 0 | 0 | 0 | 689 | 0 | 0 | 0 | 0 | 1 |
2 | 2 | 120000 | 2 | 2 | 2 | 26 | -1 | 2 | 0 | 0 | ... | 3272 | 3455 | 3261 | 0 | 1000 | 1000 | 1000 | 0 | 2000 | 1 |
3 rows × 25 columns
# header로 의미없는 첫 행 제거, iloc로 기존 id 제거
import pandas as pd
df = pd.read_excel('credit_card.xls', header=1, sheet_name='Data').iloc[0:,1:]
print(df.shape)
df.head(3)
(30000, 24)
LIMIT_BAL | SEX | EDUCATION | MARRIAGE | AGE | PAY_0 | PAY_2 | PAY_3 | PAY_4 | PAY_5 | ... | BILL_AMT4 | BILL_AMT5 | BILL_AMT6 | PAY_AMT1 | PAY_AMT2 | PAY_AMT3 | PAY_AMT4 | PAY_AMT5 | PAY_AMT6 | default payment next month | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 20000 | 2 | 2 | 1 | 24 | 2 | 2 | -1 | -1 | -2 | ... | 0 | 0 | 0 | 0 | 689 | 0 | 0 | 0 | 0 | 1 |
1 | 120000 | 2 | 2 | 2 | 26 | -1 | 2 | 0 | 0 | 0 | ... | 3272 | 3455 | 3261 | 0 | 1000 | 1000 | 1000 | 0 | 2000 | 1 |
2 | 90000 | 2 | 2 | 2 | 34 | 0 | 0 | 0 | 0 | 0 | ... | 14331 | 14948 | 15549 | 1518 | 1500 | 1000 | 1000 | 1000 | 5000 | 0 |
3 rows × 24 columns
# Pay_0 열의 이름을 Pay_1로 바꿈
df.rename(columns={'PAY_0':'PAY_1','default payment next month':'default'}, inplace=True)
y_target = df['default']
X_features = df.drop('default', axis=1)
# 데이터 사이의 상관도 확인
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
corr = X_features.corr()
plt.figure(figsize=(14,14))
sns.heatmap(corr, annot=True, fmt='.1g')
plt.show()
BILL_AMT1 ~ BILL_AMT6 6개 속성끼리의 상관도가 대부분 0.9 이상으로 매우 높다. 또한 Pay_1 ~ Pay_6까지의 상관도도 서로 높은 것을 확인할 수 있다. 이렇게 높은 상관도를 가진 속성들은 소수의 PCA 만으로도 자연스럽게 이 속성들의 변동성을 수용할 수 있다.
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
#BILL_AMT1 ~ BILL_AMT6 까지 6개의 속성명 생성
cols_bill = ['BILL_AMT'+str(i) for i in range(1,7)]
print('대상 속성명:',cols_bill)
# 2개의 PCA 속성을 가진 PCA 객체 생성하고, explained_variance_ratio_ 계산 위해 fit( ) 호출
scaler = StandardScaler()
df_cols_scaled = scaler.fit_transform(X_features[cols_bill])
X_features.loc[:, cols_bill] = df_cols_scaled
pca = PCA(n_components=2)
pca.fit(df_cols_scaled)
print('PCA Component별 변동성:', pca.explained_variance_ratio_)
대상 속성명: ['BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6'] PCA Component별 변동성: [0.90555253 0.0509867 ]
2개의 PCA 컴포넌트만으로도 6개 속성의 변동성을 95% 이상 설명할 수 있다.
# 원본 데이터와 6개의 PCA 컴포넌트
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
rcf = RandomForestClassifier(n_estimators=300, random_state=156)
scores = cross_val_score(rcf, X_features, y_target, scoring='accuracy', cv=3 )
print('CV=3 인 경우의 개별 Fold세트별 정확도:',scores)
print('평균 정확도:{0:.4f}'.format(np.mean(scores)))
CV=3 인 경우의 개별 Fold세트별 정확도: [0.8083 0.8196 0.8232] 평균 정확도:0.8170
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 원본 데이터셋에 먼저 StandardScaler적용
scaler = StandardScaler()
df_scaled = scaler.fit_transform(X_features)
# 6개의 Component를 가진 PCA 변환을 수행하고 cross_val_score( )로 분류 예측 수행.
pca = PCA(n_components=6)
df_pca = pca.fit_transform(df_scaled)
scores_pca = cross_val_score(rcf, df_pca, y_target, scoring='accuracy', cv=3)
print('CV=3 인 경우의 PCA 변환된 개별 Fold세트별 정확도:',scores_pca)
print('PCA 변환 데이터 셋 평균 정확도:{0:.4f}'.format(np.mean(scores_pca)))
CV=3 인 경우의 PCA 변환된 개별 Fold세트별 정확도: [0.7928 0.7972 0.8033] PCA 변환 데이터 셋 평균 정확도:0.7978
전체 23개 속성의 4분의 1인 6개의 PCA 컴포넌트만으로도 원본 데이터를 기반으로 한 분류 예측 결과보다 약 1 ~ 2% 정도의 예측 성능 저하가 발생했다. PCA는 차원 축소를 통해 데이터를 쉽게 인지하는 데 활용할 수 있지만 이보다 더 활발하게 사용되는 영역은 컴퓨터 비전 분야이다.
03. LDA ( Linear Discriminant Analysis )¶
LDA란? : 선형 판별 분석법으로 불리며 PCA와 매우 유사하게 입력 데이터 세트를 저차원 공간에 투영해 차원을 축소하는 기법이지만, 중요한 차이는 LDA는 지도학습의 분류에서 사용하기 쉽도록 개별 클래스를 분별할 수 있는 기준을 최대한 유지하면서 차원을 축소한다. PCA에서는 입력 데이터의 변동성이 가장 큰 축을 찾았다면 LDA는 입력 데이터의 결정 값 클래스를 최대한으로 분리할 수 있는 축을 찾는다.
LDA는 특정 공간상에서 클래스 분리를 최대화하는 축을 찾기 위해 클래스 간 분산과 클래스 내부 분산의 비율을 최대화하는 방식으로 차원을 축소한다. 즉, 클래스 간 분산은 최대한 크게 가져가고, 클래스 내부의 분산은 최대한 작게 가져가는 방식이다.
일반적으로 LDA를 구하는 스텝은 PCA와 유사하나 가장 큰 차이점은 PCA에서 사용하는 공분산 행렬이 아닌 클래스 간 분산과 클래스 내부 분산 행렬을 생성하고, 고유벡터를 사용해 입력 데이터를 투여한다는 점이다.
- 클래스 내부와 클래스 간 분산 행렬을 구합니다. 이 두 개의 행렬은 입력 데이터의 결정 값 클래스별로 개별 피처의 평균 벡터를 기반으로 구한다.
- 클래스 내부 분산 행렬, 클래스 간 분산 행렬 두 행렬을 고유벡터로 분해한다.
- 고유값이 가장 큰 순으로 K개 추출한다.
- 고유값이 가장 큰 순으로 추출된 고유벡터를 이용해 새롭게 입력 데이터를 변환한다.
붓꽃 데이터 세트에 LDA 적용하기¶
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에서 주의해야할 점은 LDA는 PCA와 다르게 비지도학습이 아닌 지도학습이라는 것이다. 따라서 클래스의 결정 값이 변환 시에 필요하다.
lda = LinearDiscriminantAnalysis(n_components=2)
lda.fit(iris_scaled, iris.target)
iris_lda = lda.transform(iris_scaled)
print(iris_lda.shape)
(150, 2)
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
lda_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 별로 다른 shape으로 scatter plot
for i, 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=marker,label=iris.target_names[i])
plt.legend(loc='upper right')
plt.xlabel('lda_component_1')
plt.ylabel('lda_component_2')
plt.show()
04. SVD ( Singular Value Decomposition )¶
SVD란? : PCA와 유사한 행렬 분해 기법을 사용합니다. PCA의 경우 정방행렬만을 고유벡터로 분해할 수 있지만 SVD는 정방행렬뿐만 아니라 행과 열의 크기가 다른 행렬에도 적용할 수 있습니다. 일반적으로 SVD는 m x n 크기의 행렬 A를 다음과 같이 분해한다.
$$ A = U\sum V^T $$행렬 U와 V에 속한 벡터는 특이벡터이며, 모든 특이벡터는 서로 직교하는 성질을 가진다. 시그마는 대각행렬이며, 행렬의 대각에 위치한 값만 0이 아니고 나머지 위치의 값은 모두 0이다. 시그마가 sum의 의미가 아님!! 시그마가 위치한 0이 아닌 값이 행렬 A의 특이값이고 SVD는 A의 차원이 m x n일 때 U를 m x m, 시그마를 m x n, V^T를 n x n로 분해한다.
하지만 일반적으로는 시그마의 비대각인 부분과 대각원소 중에 특이값이 0인 부분도 모두 제거하고 제거된 시그마에 대응되는 U와 V 원소도 함께 제거해 차원을 줄인 형태로 SVD를 적용한다. 이렇게 되면 SVD는 A의 차원이 m x n일 때 U를 m x p, 시그마를 p x p, V^T를 p x n로 분해한다. 이러한 Truncated SVD는 시그마의 대각원소 중에 상위 몇 개만 추출해서 여기에 대응하는 U와 V의 원소도 함께 제거해 더욱 차원을 줄인 형태로 분해하는 것이다.
# 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]]
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))
(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]]
분해된 U, Sigma, Vt가 원본 행렬로 정확히 복원되는지 확인
# Sima를 다시 0 을 포함한 대칭행렬로 변환
Sigma_mat = np.diag(Sigma)
a_ = np.dot(np.dot(U, Sigma_mat), Vt)
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]]
데이터 세트가 로우 간 의존성이 있을 경우 어떻게 Sigma가 변하고, 이에 따른 차원 축소가 진행될 수 있는지
# 의존성을 부여하기 위해 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를 수행하여 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. ]
Sigma의 값 중 2개가 0으로 변했다 == 선형 독립인 로우 벡터의 개수가 2개이다. ( 랭크가 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))
(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 활용 -> Sigma 행렬에 있는 대각원소 중 상위 일부 데이터만 추출해 분해하는 방식
이 경우 더 적은 차원의 U, Sigma, Vt로 분해되기 때문에 원본 행렬을 정확하게 다시 복원할 수는 없다. 하지만 데이터 정보가 압축되어 분해됨에도 불구하고 상당한 수준으로 원본 행렬을 근사할 수 있다.
scipy 모듈을 사용 ( SVD, 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.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]]
사이킷런 TruncatedSVD 클래스를 이용한 변환¶
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)
# Scatter plot 2차원으로 TruncatedSVD 변환 된 데이터 표현. 품종은 색깔로 구분
plt.scatter(x=iris_tsvd[:,0], y= iris_tsvd[:,1], c= iris.target)
plt.xlabel('TruncatedSVD Component 1')
plt.ylabel('TruncatedSVD Component 2')
plt.show()
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')
Text(0.5, 1.0, 'PCA Transformed')
Truncated SVD와 PCA가 비슷하게 변환된 것을 확인할 수 있다.
print((iris_pca - iris_tsvd).mean())
print((pca.components_ - tsvd.components_).mean())
2.350723782296171e-15 -1.3877787807814457e-17
두 개의 변환 행렬 값과 원복 속성별 컴포넌트 비율값을 실제로 서로 비교해보니 모두 0에 가까운 값으로 2개의 변환이 서로 동일함을 알 수 있다. 즉, 데이터 세트가 스케일링으로 데이터 중심이 동일해지면 사이킷런의 SVD와 PCA는 동일한 변환을 수행한다. ( PCA가 SVD 알고리즘으로 구현된다 ) 하지만 PCA는 밀집 행렬에 대한 변환만 가능하며 SVD는 희소 행렬에 대한 변환도 가능하다. SVD는 이미지 압축을 통한 패턴 인식과 신호 처리 분야에 사용되고, 텍스트의 토픽 모델링 기법인 LSA ( Latent Semantic Analysis )의 기반 알고리즘이다.
05. NMF ( Non-Negative Matrix Factorization )¶
NMF란? : Truncated SVD와 같이 낮은 랭크를 통한 행렬 근사 방식의 변형. NMF는 원본 행렬 내의 모든 원소 값이 모두 양수라는 게 보장되면 다음과 같이 좀 더 간단하게 두 개의 기반 양수 행렬로 분해될 수 있는 기법을 지칭한다. 원본 행렬 V를 행렬 W와 H로 근사해 분해할 수 있고 이 때 W는 일반적으로 길고 가는 행렬( 원본 행렬의 행 크기와 같다 ), H는 작고 넓은 행렬 ( 원본 행렬의 열 크기와 같다 )로 분해된다. 이렇게 분해된 행렬은 잠재 요소를 특성으로 가지게 되고 w는 원본 행에 대해서 이 잠재 요소의 값이 얼마나 되는지에 대응하고, H는 이 잠재 요소가 원본 열로 어떻게 구성됐는지를 나타내는 행렬이다.
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')
C:\ProgramData\Anaconda3\lib\site-packages\sklearn\decomposition\_nmf.py:312: FutureWarning: The 'init' value, when 'init=None' and n_components is less than n_samples and n_features, will be changed from 'nndsvd' to 'nndsvda' in 1.1 (renaming of 0.26). warnings.warn(("The 'init' value, when 'init=None' and " C:\ProgramData\Anaconda3\lib\site-packages\sklearn\decomposition\_nmf.py:1090: ConvergenceWarning: Maximum number of iterations 200 reached. Increase it to improve convergence. warnings.warn("Maximum number of iterations %d reached. Increase it to"
Text(0, 0.5, 'NMF Component 2')
NMF도 SVD와 유사하게 이미지 압축을 통한 패턴 인식, 텍스트의 토픽 모델링 기법, 문서 유사도 및 클러스터링에 잘 사용된다. 또한 영화 추천과 같은 추천 영역에 활발하게 적용된다. 사용자의 상품 평가 데이터 세트인 사용자 평가 순위 데이터 세트를 행렬 분해 기법을 통해 분해하면서 사용자가 평가하지 않은 상품에 대한 잠재적인 요소를 추출해 이를 통해 평가 순위를 예측하고, 높은 순위로 예측된 상품을 추천해주는 방식이다. ( 잠재 요소 기반의 추천 방식 )
정리
- 차원 축소 알고리즘은 많은 차원을 가지는 이미지나 텍스트에서 활발하게 사용된다.
- PCA는 입력 데이터의 변동성이 가장 큰 축을 구하고, 다시 이 축에 직각인 축을 반복적으로 축소하려는 차원 개수만큼 구한 뒤 입력 데이터를 이 축들에 투여해 차원을 축소하는 방식이다.
- LDA는 입력 데이터의 결정 값 클래스를 최대한으로 분리할 수 있는 축을 찾는 방식으로 차원을 축소한다.
- SVD와 NMF는 매우 많은 피처 데이터를 가진 고차원 행렬을 두 개의 저차원 행렬로 분리하는 행렬 분해 기법이다.