배운 내용
- PCA
- StandardScaler
- sklearn.decomposition.PCA
- PCA.explained_variance_ratio_
- RandomForestClassifier
- cross_val_score
- DataFrame.rename(columns)
- DataFrame.corr()
- sns.heatmap()
- LDA
- LinearDiscriminantAnalysis
- SVD
- numpy.linalg.svd
- np.diag()
- np.dot()
- scipy.sparse.linalg.svds
- scipy.linalg.svd
- sklearn.decomposition.TruncatedSVD
- NMF
본문
차원 축소는 피처가 매우 많은 다차원 데이터 세트의 차원을 더 적은 피처(차원)를 갖도록 축소하는 것입니다. 차원이 증가할 수록 데이터 포인트 간 거리가 더 멀어질 수 밖에 없으므로, 희소한(sparse) 구조를 갖게 됩니다. 더 적은 차원에서 학습된 모델보다 예측 신뢰도가 떨어집니다.
다차원 피처를 시각화해서 데이터의 특성을 파악하기는 불가능합니다. 이 경우 3차원 이하의 축소를 통해 데이터를 압축해 시각화할 수 있습니다.
일반적으로 차원 축소는 피처 선택(feature selection)과 피처 추출(feature extraction)로 나눌 수 있습니다.
피처 선택(feature selection)은 데이터의 특징을 잘 나타내는 주요 피처만 선택하고, 불필요한 피처는 제거하는 것입니다.
피처 추출(feature extraction)은 기존 피처들을 함축적으로 더 잘 설명할 수 있는 피처로 압축해서 추출하는 것입니다.
피처 추출의 예시를 들면, 한 학생을 평가하는 피처로 모의고사 성적, 내신 성적, 수상 경력, 봉사활동, 대외 활동 등이 있다고 합시다. 그러면 이러한 피처를 학업 성취도, 커뮤니케이션 능력 등 더 함축적인 피처로 요약하는 것입니다. 피처 추출은 단순히 데이터를 압축하는 것이 아니라 데이터를 더 잘 설명할 수 있는 잠재적인 요소(Latent Factor)를 추출하는 것을 의미합니다. PCA, SVD, NMF는 이처럼 잠재적인 요소를 찾는 대표적인 차원 축소 알고리즘입니다. 매우 많은 차원을 가지는 이미지, 텍스트의 경우에 자주 쓰입니다.
이미지의 경우, 이 차원 축소 알고리즘은 이미지를 더 적은 차원으로 바꿔주어서 이미지 분류 수행 시, 과적합 문제를 해결해줍니다. 또한, 이미지가 가지고 있는 큰 픽셀 때문에 발생하는 비슷한 이미지 간에 적은 픽셀의 차이가 잘못된 예측을 일으키는 문제를 방지해줍니다.
텍스트의 경우, 이 차원 축소 알고리즘이 문서 속 많은 단어들의 구성에 숨겨져 있는 Semantic(시맨틱) 의미나 Topic(토픽)을 잠재 요소로 간주하고 찾아낼 수 있습니다. SVD와 NMF는 이러한 시맨틱 토픽(Semantic Topic) 모델링을 위한 기반 알고리즘으로 사용됩니다.
PCA(Principal component Analysis)
PCA는 여러 변수 간에 존재하는 상관관계를 이용해 이를 대표하는 주성분(Principal Component)을 추출해 차원을 축소하는 기법입니다. PCA는 차원을 축소할 때, 기존 데이터의 유실을 최소화하기 위해 가장 높은 분산을 가지는 데이터 축을 찾아 이 축으로 차원을 축소합니다. 이것이 이 PCA의 주성분이 됩니다.(즉, 분산이 데이터의 특성을 가장 잘 나타내는 것으로 간주합니다.) 분산이 가장 높은 축을 잡는 다는 것은 그 축으로 데이터를 투영시 점들이 더 멀리 퍼져있다는 걸 의미한다. 이렇게 되면, 점들을 투영해도 가능한 적은 점들이 겹쳐진다. 즉, 많은 정보가 남아있다. 아래 그림을 보면, PC1이 PC2보다 많은 점(정보)이 있다는 걸 알 수 있다.
PCA는 첫 번째 축을 분산이 가장 큰 벡터 축으로 생성하고, 두 번째 축은 이 벡터 축에 직각이 되는 벡터를 축으로 합니다. 세 번째는 다시 두 번째 축과 직각이 되는 벡터를 축으로 생성합니다. 이렇게 생성된 벡터에 축을 투영하는데, PCA는 이러한 축에 투영된 정보 가령 x, y, 색깔의 3가지 정보를 가지고 있다고 가정하면, 3가지 축을 모두 이용하면 원래 데이터를 복원할 수 있다. 반대로 축들 중에서 필요한 정보를 가진 축만 선택해 사용할 수도 있다.
PCA, 즉 주성분 분석은 원본 데이터가 가지는 변동성(분산)을 매우 작은 주성분(축)으로 설명할 수 있는 분석법입니다.
PCA를 선형대수 관점에서 해석해보면, 입력 데이터의 공분산 행렬(Covariance Matrix)을 고유값 분해하고, 이렇게 구한 고유벡터에 입력 데이터를 선형 변환하는 것입니다. 이 고유벡터가 PCA의 주성분 벡터로서 입력 데이터의 분산이 큰 방향을 나타냅니다. 고유값(eigenvalue)은 바로 이 고유벡터의 크기를 나타내며, 동시에 입력 데이터의 분산을 나타냅니다.
일반적으로 선형 변환은 특정 벡터에 행렬 A를 곱해 새로운 백터로 변환하는 것을 의미합니다. 이를 특정 벡터를 하나의 공간에서 다른 공간으로 투영하는 개념으로 볼 수 있으며, 이 경우 이 행렬을 바로 공간으로 가정하는 것입니다.
보통 분산은 한 개의 특정한 변수의 데이터 변동을 의미하나, 공분산은 두 변수 간의 변동을 의미합니다. 가령 사람의 키 변수를 X, 몸무게 변수를 Y라고 한다면 공분산 Cov(X, Y) > 0은 X(키)가 증가할 때 Y(몸무게)도 증가한다는 의미입니다. 공분산 행렬은 여러 변수와 관련된 공분산을 포함하는 정방형 행렬입니다.
X | Y | Z | |
X | 3.0 | -0.71 | -0.24 |
Y | -0.71 | 4.5 | 0.28 |
Z | -0.24 | 0.28 | 0.91 |
위 표에서 보면 공분산 행렬에서 대각선 원소는 각 변수(X, Y, Z)의 분산을 의미하며, 대각선 이외의 원소는 가능한 모든 변수 쌍 간의 공분산을 의미합니다. X, Y, Z의 분산은 각각 3.0, 4.5, 0.91입니다. X와 Y의 공분산은 -0.71, X와 Z의 공분산은 -0.24, Y와 Z의 공분산은 0.28입니다.
고유벡터는 행렬 A를 곱하더라도 방향이 변하지 않고 그 크기만 변하는 벡터를 지징합니다.
즉, Ax = ax (A는 행렬, x는 고유 벡터, a는 스칼라값)입니다. 이 고유벡터는 여러 개가 존재하며, 정방 행렬은 그 차원 수만큼 고유벡터를 가질 수 있습니다. 예를 들면, 3x3 행렬은 3개의 고유 백터를 가질 수 있습니다. 이렇게 고유벡터는 행렬이 작용하는 힘의 방향과 관계가 있어서 행렬을 분해하는 데 사용됩니다. (Ax = ax)
공분산 행렬은 정방 행렬(Dagonal Matrix)이며 대칭 행렬(Symmetric Matrix)입니다. 대칭 행렬이란 정방 행렬 중에서 대각 원소를 중심으로 원소값이 대칭되는 행렬을 말합니다. 즉, A.T = A 입니다. 대칭 행렬은 고유값 분해와 관련해 좋은 특성을 가지고 있습니다. 고유 벡터를 직교행렬(orthogonal matrix)로, 고유값을 정방 행렬로 대각화할 수 있습니다.
입력 데이터의 공분산 행렬을 C라고 하면 공분산 행렬의 특성으로 인해 다음과 같이 분해할 수 있습니다. P는 n x n 직교 행렬, Q는 n x n 정방 행렬, P.T는 행렬 P의 전치 행렬입니다.
즉, 공분산 C = (고유벡터 직교 행렬) * (고유값 정방 행렬) * (고유벡터 직교 행렬의 전치 행렬) 입니다. ei는 i 번째 고유 벡터를, λi는 i번째 고유벡터의 크기를 의미합니다. e1는 가장 분산이 큰 방향을 가진 고유 벡터이고, e2는 e1에 수직이면서 다음으로 가장 분산이 큰 방향을 가진 고유벡터입니다.
결론적으로 입력 데이터의 공분산 행렬이 고유벡터와 고유값으로 분해될 수 있으며, 이렇게 분해된 고유벡터를 이용해 입력 데이터를 선형 변환하는 방식이 PCA라는 것입니다.
PCA를 수행하는 단계는 아래와 같습니다.
- 입력 데이터 세트의 공분산 행렬을 생성합니다.
- 공분산 행렬의 고유벡터와 고유값을 계산합니다.
- 고유값이 가장 큰 순으로 K개(PCA로 변환하고 싶은 차수 만큼) 고유벡터를 추출합니다.
- 고유값이 가장 큰 순으로 추출된 고유벡터를 이용해 새롭게 입력 데이터를 변환합니다.
PCA는 많은 속성으로 구성된 원본 데이터를 그 핵심을 구성하는 데이터로 압축한 것입니다. 붓꽃(Iris) 데이터 세트는 sepal length, sepal width, petal length, petal width의 4개 속성으로 돼있는 걸 2개의 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)
'''
결과1
'''
각 품종에 따라 sepal length와 sepal width를 X축, Y축으로 해 품종 데이터 분포를 2차원으로 나타냅니다.
# 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']
plt.scatter(x=x_axis_data, y=y_axis_data, marker=marker, label=iris.target_names[i])
plt.legend()
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.show()
'''
결과2
'''
setosa의 경우는 혼자 독립적으로 잘 분포돼있습니다. versicolor, virginica의 경우 혼합돼있어 구별하기 어려운 조건입니다. PCA로 4개의 피처 sepal length, width 그리고 petal length, width를 PCA로 2개의 피처로 압축하겠습니다.
PCA를 적용하기 전 개별 피처를 스케일링 해야합니다. PCA는 여러 피처 값을 연산해야해서 스케일에 영향을 받습니다.
StandardScaler
데이터를 분산 1, 표준 0의 표준 정규 분포 형태로 만듭니다.
from sklearn.preprocessing import StandardScaler
# Target 값을 제외한 모든 속성 값을 StandardScaler를 이용해 표준 정규 분포를 가지는 값들로 변환
iris_scaled = StandardScaler().fit_transform(irisDF.iloc[:, :-1])
PCA
사이킷런은 PCA 변환을 위해 PCA 클래스를 제공합니다. PCA 클래스는 생성 파라미터로 n_components를 입력 받습니다. 이는 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)
'''
피처의 차원이 2개로 줄었음을 알 수 있습니다. iris_pca는 (150, 2) 넘파이 행렬을 가지고 있으므로 이를 DataFrame으로 바꾸어줍니다.
# PCA 변환된 데이터의 칼럼 명을 각각 pca_component_1, pca_component2로 명명
pca_columns=['pca_component_1', 'pca_component_2']
irisDF_pca = pd.DataFrame(data=iris_pca, columns=pca_columns)
irisDF_pca['target'] = iris.target
irisDF_pca.head(3)
'''
결과3
'''
이렇게 PCA를 통해 압축된 피처를 2차원상에서 시각화해보겠습니다. pca_component_1 속성을 X축으로, pca_component_2 속성을 Y축으로 해서 붓꽃 품종 마다 어떻게 분포됐는지 확인합니다.
# 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=x_axis_data, y=y_axis_data, marker=marker, label=iris.target_names[i])
plt.legend()
plt.xlabel('pca_component_1')
plt.ylabel('pca_component_2')
plt.show()
'''
결과4
'''
Setosa는 명확히 구분이 가능하고, versicolor와 virginica는 겹치는 부분이 아직 있지만 매우 잘 구분됐습니다. PCA로 압축한 component 별로 원본 데이터의 변동성을 얼마나 반영하고 있는지 알아보겠습니다.
PCA.explained_variance_ratio_
PCA.explained_variance_ratio_속성은 전체 변동성에서 개별 PCA component가 차지하는 변동성 비율을 제공합니다.
print(pca.explained_variance_ratio_)
'''
[0.72962445 0.22850762]
'''
pca_component_1이 전체 변동성의 약 72.9%를 차지하며, pca_component_2가 약 22.8%를 차지합니다. 따라서 PCA를 2개 요소로만 변환해도 원본 데이터의 변동성을 95%나 설명할 수 있습니다.
이제 원본 붓꽃 데이터 세트와 PCA로 피처 추출한 데이터 세트에 분류를 적용한 후 결과를 비교하겠습니다. Estimator는 RandomForestClassifier를 쓰고, cross_val_score()를 통해 3개의 교차 검증 세트로 정확도 결과를 비교합니다.
RandomForestClassifier & cross_val_score
원본 데이터 세트 정확도
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 변환 후에도 원본 데이터의 특성을 상당 부분 유지하고 있음을 알 수 있습니다.
신용 카드 고객 데이터 세트
이번엔 조금더 피처가 많은 UCI Machine Learning Repository에 있는 신용카드 고객 데이터 세트를 PCA로 피처 데이터 세트를 추출해서 원본 데이터와 예측 성능을 비교 평가해보겠습니다.
데이터는 https://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients에서 내려받을 수 있습니다.
# 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)
'''
'''
결과1
'''
30,000개의 레코드와 24개의 피처를 가지고 있고, Target값은 'default payment next month' 피처로 '연체'의 경우 1, '정상납부'가 0입니다.
Target인 'default payment next mondth'를 'default'로 이름을 바꾸고 'PAY_0'을 자연스럽게 'PAY_1'로 바꾸겠습니다. 그리고 피처 데이터와 Target 데이터를 DataFrame으로 만들겠습니다.
DataFrame.rename(columns)
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)
해당 데이터 세트는 23개의 피처를 가지고 있으나, 피처끼리 상관도가 매우 높습니다. DataFrame.corr()를 이용해 피처 간 상관도를 구한 뒤 Seaborn의 heatmap으로 시각화하겠습니다.
DataFrame.corr() & sns.heatmap()
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()
'''
결과2
'''
BILL_AMT1 ~ BILL_AMT6 6개 속성끼리의 상관도가 0.9 이상으로 매우 높음을 알 수 있습니다. 이보다는 낮지만 PAY_1 ~ PAY6_도 역시 상관도가 높습니다. 높은 상관도를 가진 피처들은 소수의 PCA 만으로도 자연스럽게 속성들의 변동성을 수용할 수 있습니다. BILL_AMT1~6 피처를 2개의 component로 PCA 변환해 개별 component의 변동성을 explained_variance_ratio_속성으로 알아보겠습니다.
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])
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 component만으로도 6개 속성의 변동성을 약 95% 이상 설명할 수 있습니다. 특히, 첫 번째 PCA 축으로 90%의 변동성을 수용할 정도로 6개 속성의 상관도가 높습니다.
이번엔, 원본 데이터 세트와, 이를 PCA component 6개로 압축한 데이터 세트의 예측 결과를 비교해보겠습니다. 이번에도 RandomForestClassifier를 이용해 3개의 교차 검증 세트로 분류하겠습니다.
원본 데이터 세트
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
'''
PCA component 6개로 압축한 데이터 세트
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 원본 데이터 세트에 먼저 StandardScaler 적용
scaler = StandardScaler()
df_scaled = scaler.fit_transform(X_features)
# 6개의 컴포넌트를 가진 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인 경우의 개별 Fold 세트 별 정확도:', scores_pca)
print('평균 정확도:{0:.4f}'.format(np.mean(scores)))
'''
CV=3인 경우의 개별 Fold 세트 별 정확도: [0.7907 0.7987 0.8019]
평균 정확도:0.7971
'''
정확도가 0.02정도 낮아졌지만, 전체 피처 중 1/4만으로도 이정도 예측 성능을 유지할 수 있다는 건 PCA의 뛰어난 압축 능력을 잘 보여주는 것이라고 생각됩니다.
PCA는 차원 축소를 통해 데이터를 쉽게 인지하는 데 활용할 수 있습니다. 이보다 더 활발하게 사용되는 영역은 컴퓨터 비전(Computer Vision) 분야입니다. 얼굴 인식의 경우 Eigen-face라고 불리는 PCA 변환으로 원본 얼굴 이미지를 변환해 사용하는 경우가 많습니다.
LDA(Linear Discriminant Analysis)
LDA(Linear Discriminant Analysis)는 선형 판별 분석법으로 PCA와 매우 유사합니다. LDA는 PCA와 매우 유사하게 입력 데이터 세트를 저차원 공간에 투영해 차원을 축소하는 기법입니다. 하지만, LDA는 분류(Classification)에서 사용하기 쉽도록 개별 클래스를 분별할 수 있는 기준을 최대한 유지하면서 차원을 축소합니다.
PCA는 입력 데이터의 변동성이 가장 큰 축을 찾았지만, LDA는 입력 데이터의 결정값 클래스를 최대한으로 분리할 수 있는 축을 찾습니다.
LDA는 특정 공간 상에서 클래스 간 분산(between - class scatter)을 최대한 크게 가져가고, 클래스 내부 분산(within - class scatter)은 최대한 작게 가져가는 방식입니다. 아래 그림은 두 분산을 설명하는 그림입니다.
수식에서 가장 큰 차이점은 PCA는 공분산 행렬을 통해 고유 벡터를 구했지만, LDA는 between - class scatter 행렬과 within - class scatter 행렬을 생성한 뒤, 이 행렬에 기반해 고유벡터를 구하고 입력 데이터를 투영한다는 점입니다. LDA를 구하는 과정은 다음과 같습니다.
- 클래스 내부와 클래스 간 분산 행렬을 구합니다. 이 두 개의 행렬은 입력 데이터의 결정값 클래스 별로 개별 피처의 평균 벡터(mean vector)를 기반으로 구합니다.
- 클래스 내부 분산 행렬을 Sw, 클래스 간 분산 행렬은 Sb 라고 하면, 다음 식으로 두 행렬을 고유벡터로 분해할 수 있습니다. $$S_W^TS_B=\left[e_1\ ...\ e_n\right]\begin{bmatrix}\lambda _1&0&0\\0&..&0\\0&0&\lambda _n\end{bmatrix}\begin{bmatrix}e_1^T\\...\\e_n^T\end{bmatrix}$$
- 고유값이 가장 큰 순으로 K개 (LDA 변환 차수 만큼) 추출합니다.
- 고유값이 가장 큰 순으로 추출된 고유벡터를 이용해 새롭게 입력 데이터를 변환합니다.
붓꽃 데이터 세트에 LDA 적용하기
사이킷런은 LDA를 LinearDiscriminantAnalysis 클래스로 제공합니다. 붓꽃 데이터 세트를 로드하고, 표준 정규 분포로 스케일링합니다.
LinearDiscriminantAnalysis
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개의 component로 Iris 데이터를 LDA 변환하겠습니다. 주의해야할 점은 LDA는 PCA와 다르게 비지도 학습이 아닌 지도학습이라는 점입니다. 즉, 클래스의 결정값이 변환 시에 필요하기 떄문에 LDA.fit()에 결정값 레이블이 입력됐음에 유의합시다.
lda = LinearDiscriminantAnalysis(n_components=2)
lda.fit(iris_scaled, iris.target)
iris_lda = lda.transform(iris_scaled)
print(iris_lda.shape)
'''
(150, 2)
'''
LDA 변환된 입력 데이터 값을 2차원 평면에 품종 별로 시각화해보겠습니다.
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
lda_columns=['lda_component_1', 'lda_component_2']
irisDF_lda = pd.DataFrame(data=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 i, marker in enumerate(markers):
x_axis_data = pd.DataFrame(irisDF_lda[irisDF_lda['target']==i])['lda_component_1']
y_axis_data = pd.DataFrame(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()
'''
결과3
'''
LDA 변환이 데이터의 변동성을 잘 반영함을 알 수 있습니다.
SVD(Singular Value Decomposition)
SVD 역시 PCA와 유사한 행렬 분해 기법을 이용합니다. PCA의 경우 정방행렬(즉, 행과 열의 크기가 같은 행렬)만 고유벡터로 분해가능했지만, SVD는 정방행렬뿐만 아니라 m x n 크기의 행렬 A를 다음과 같이 분해하는 것을 의미합니다. $$A=U\sum _{\ }^{\ }V^T$$ 행렬 U와 V는 특이벡터(singular vector)이며, 모든 특이벡터는 서로 직교하는 성질을 가집니다. ∑는 대각행렬이며, 대각에 위치한 값만 0이 아니며, 대각에 위치한 값은 행렬 A의 특이값입니다. SVD는 A의 차원이 m x n일 때, U의 차원이 m x n, ∑의 차원이 m x n, V.T의 차원이 n x n으로 분해합니다.
위 그림과 같이, ∑가 비대각인 부분이나, 특이값이 0인 부분을 제거했을 때 k x k가 된다고 가정하면, U와 V.T에 ∑에 대응되는 원소를 함께 제거해서 U가 m x k, V.T가 k x n으로 분해해 차원을 줄인 형태로 SVD를 적용합니다. 이를 Truncated SVD라고 합니다. 일반적으로는 Truncated SVD를 적용합니다.
SVD는 일반적으로 Numpy나 Scipy 라이브러리를 이용해 수행합니다. 넘파이의 SVD를 이용해 SVD 연산을 수행하고, SVD로 분해가 어떻게 되는지 간단한 예제를 통해 살펴보겠습니다.
numpy.linalg.svd
랜덤한 4 x 4 넘파이 행렬을 생성합니다. 랜덤 행렬을 생성하는 이유는 행렬의 개별 로우끼리의 의존성을 없애기 위함입니다.
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))
'''
[[-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]]
'''
numpy.linalg.svd에 파라미터로 원본 행렬을 넣으면 U, sigma, Vt를 반환합니다. Sigma의 경우 대각에 위치한 값을 1차원 행렬로 반환합니다.
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를 이용해 다시 원본 행렬로 정확히 복원되는지 확인해보겠습니다. 원본 행렬로의 복원은 단지 이 U, sigma, Vt를 내적하면 됩니다. 단, sigma의 경우 대칭 행렬로 만들어야 합니다.
np.diag() & np.dot()
# Sigma를 다시 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]]
'''
U, sigma, Vt를 통해 복원한 a_와 a가 같음을 알 수 있습니다.
이번엔 데이터 세트의 로우 간에 의존성이 있는 경우 어떻게 Sigma가 변하고, 이에 따른 차원 축소가 진행되는지 알아보겠습니다.
# 행렬 a의 3번째, 4번째 행에 의존성(선형 종속) 부여
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개라는 의미입니다.(즉, 행렬의 Rank가 2입니다.) 이번에도 U, Sigma, Vt를 이용해 원본 행렬을 복원해보겠습니다. 단, Sigma의 0에 대응되는 U, Sigma, 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))
'''
(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는 ∑행렬에 있는 대각원소, 즉 특이값 중 상위 일부 데이터만 추출해 분해하는 방식입니다. Truncated SVD는 인위적으로 더 작은 차원의 U, ∑, V.T로 분해하기 때문에 원본 행렬을 정확하게 다시 원복할 수는 없습니다. 하지만, 데이터 정보가 압축되어도 상당 수준 원본 행렬에 근사할 수 있습니다.
당연한 얘기지만, 원래 차원의 차수에 가깝게 잘라낼 수록(Truncate) 원본 행렬에 더 가깝게 복원할 수 있습니다.
Truncated SVD는 scipy 모듈에서만 지원됩니다. scipy는 SVD, Truncated SVD를 모두 지원합니다. scipy의 SVD는 scipy.linalg.svd를 이용하면 되지만, Truncated SVD는 희소 행렬로만 지원돼서 scipy.sparse.linalg.svds를 이용해야 합니다.
scipy.sparse.linalg.svds & scipy.linalg.svd
임의의 6 x 6 행렬을 만들고 Normal SVD와 Truncated SVD의 특이값, Sigma의 차원, 복원한 원본 행렬을 비교해보겠습니다.
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]]
'''
Truncated SVD의 차원을 4로 설정하고, TruncatedSVD로 분해된 행렬로 다시 복원했을 때, 완벽하게 복원되지 않고 근사적으로 복원됨을 알 수 있습니다.
사이킷런 TruncatedSVD 클래스를 이용한 변환
사이킷런의 TruncatedSVD 클래스는 scipy의 svds와 같이 truncated SVD 연산을 수행해 원본 행렬을 분해한 U, ∑ 등을 반환하지 않습니다. 사이킷런의 TruncatedSVD 클래스는 PCA 클래스와 유사하게 fit()과 transform()을 호출해 원본 데이터를 K개의 컴포넌트로 차원을 축소해 반환합니다. 원본 데이터를 Truncated SVD 방식으로 분해된 U*Sigma 행렬에 선형 변환해 생성합니다.
sklearn.decomposition.TruncatedSVD
Iris 데이터 세트를 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개의 주요 컴포넌트로 TruncatedSVD 변환
tsvd = TruncatedSVD(n_components=2)
tsvd.fit(iris_ftrs)
iris_tsvd = tsvd.transform(iris_ftrs)
# 산점도 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')
'''
결과1
'''
TruncatedSVD 변환 역시 변환 후 품종 별로 어느 정도 클러스터링이 가능할 정도로 각 변환 속성으로 뛰어난 고유성을 가지고 있음을 알 수 있습니다.
사실, 사이킷런의 TruncatedSVD와 PCA 클래스 구현을 조금 더 자세히 들여다 보면 두 개 클래스 모두 SVD를 이용해 행렬을 분해합니다. 붗꽃 데이터를 스케일링으로 변환한 뒤에 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')
'''
결과2
'''
두 개의 변환 행렬과 원복 피처별 component 비율값을 실제로 비교해 보면 거의 같음을 알 수 있습니다.
print((iris_pca - iris_tsvd).mean())
print((pca.components_ - tsvd.components_).mean())
'''
2.3538231549065834e-15
7.632783294297951e-17
'''
모두 0에 가까운 값이므로 2개의 변환이 서로 동일함을 알 수 있습니다. 즉, 데이터 세트가 스케일링으로 데이터 중심이 동일해지면 사이킷런의 SVD와 PCA는 동일한 변환을 수행합니다. 이는 PCA가 SVD 알고리즘으로 구현됐음을 의미합니다. 하지만 PCA는 밀집 행렬(Dense Matrix)에 대한 변환만 가능하며 SVD는 희소 행렬(Sparse Matrix)에 대한 변환도 가능합니다.
SVD는 PCA와 유사하게 Computer Vision 영역에서 이미지 압축을 통한 패턴 인식과 신호 처리 분야에 사용됩니다. 또한, 텍스트의 토픽 모델링 기법인 LSA(Latent Semantic Analysis)의 기반 알고리즘입니다.
NMF(Non-Negative Matrix Factorization)
NMF는 Truncated SVD와 같이 낮은 랭크를 통한 행렬 근사(Low-Rank Approximation) 방식의 변현입니다. NMF는 원본 행렬 내 모든 원소값이 양수라는게 보장되면 아래와 같이 좀 더 간단하게 두개의 기반 양수 행렬로 분해될 수 있는 기법을 지칭합니다.
4 x 5 원본 행렬 V는 4 x 2 행렬 W와 2 x 5 행렬 H로 근사해 분해될 수 있습니다. 행렬 분해(Matrix Factorization)은 일반적으로 SVD와 같은 행렬 분해 기법을 통칭하는 것입니다. 이처럼 행렬 분해를 하게 되면, 행렬 W처럼 길고 가는 행렬과 행렬 H처럼 짧고 넓은 행렬로 분해됩니다. 이렇게 분해된 행렬은 잠재 요소를 특성으로 가지게 됩니다.
분해 행렬 W는 원본 행에 대해서 이 잠재 요소의 값이 얼마나 되는지에 대응하며, 분해 행렬 H는 이 잠재 요소가 원본 열(즉 원본 피처)로 어떻게 구성됐는지를 나타내는 행렬입니다.
NMF는 SVD와 유사하게 차원 축소를 통한 잠재 요소 도출로 이미지 변환 및 압축, 텍스트의 토픽 도출 등의 영역에서 사용되고 있습니다. 사이킷런에서 NMF는 NMF 클래스를 이용해 지원됩니다.
NMF를 이용해 2개의 컴포넌트로 변환하고 이를 시각화해보겠습니다.
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')
'''
결과3
'''
NMF는 문서 유사도 및 클러스터링, 영화 추천과 같은 추천(Recommendations) 영역에서 활발하게 적용됩니다. 예를 들면, 사용자의 상품(영화) 평가 데이터 세트인 사용자 - 평가 순위(user - Rating) 데이터 세트를 행렬 분해 기법을 통해 분해하면서 사용자가 아직 평가하지 않은 상품에 대한 잠재적인 요소를 추출해 이를 통해 평가 순위(Rating)을 예측하고, 높은 순서대로 상품을 추천해주는 방식입니다.(이를 잠재 요소(Latent Factoring) 기반의 추천 방식이라고 합니다.)
정리
PCA, LDA, SVD, NMF는 차원을 축소하는 대표적인 알고리즘이지만, 알고리즘을 축소하는 개념보다는 이를 통해 데이터를 더 잘 설명할 수 있는 잠재적인 요소를 추출하는데 큰 의미가 있습니다. 때문에 이미지나 텍스트 에서 PCA, SVD 등의 차원 축소 알고리즘이 활발하게 사용됩니다.
PCA는 입력 데이터의 변동성이 가장 큰 축을 구하고, 이 축에 직각인 축을 반복적으로 지정한 차원의 갯수만큼 만들어 이 축들에 투영해 차원을 축소하는 방식입니다. 이를 위해 공분산 행렬을 기반으로 고유 벡터(Eigenvector)를 생성하고, 이 고유벡터에 입력 데이터를 선형 변환하는 방식입니다. LDA는 PCA와 매우 유사한데, 다른 점은 입력 데이터의 결정값 클래스를 최대한으로 분리할 수 있는 축을 찾는 방식으로 구합니다.
SVD와 NMF는 매우 많은 피처 데이터를 가진 고차원 행렬을 두 개의 저차원 행렬로 분리하는 행렬 분해 기법입니다. 이러한 행렬 분해를 수행하면서 원본 행렬에서 잠재된 요소를 추출하기 때문에 토픽 모델링이나 추천 시스템에서 활발하게 사용됩니다.
궁금한 점
- Sematic한 의미가 무엇인가?: 의미가 있다는 뜻
- 분산이 가장 높은 축을 잡는 것
-
irisDF.iloc[:, :-1]는 마지막 칼럼은 포함하지 않는다.
- plt.legend()는 label에 적은 데이터의 종류를 알려주는 범례를 표시해준다. 속성값으로 위치 등을 조정할 수 있다.
- PCA 변환을 통해 pca_comp_1 - pca_comp2 데이터로 target들이 더 잘 구분됐는데 왜 예측 정확도가 떨어지는가? 이는 petal length - petal width 데이터가 target을 잘 구분해서 정확하게 예측하는데 도움을 주었기 때문인 것 같다.
- sns.heatmap()의 파라미터 annot, fmt : annot은 셀 안에 값을 표기할 것인가? fmt는 .2f면 소숫점 2자리, d면 정수 형 등 데이터 값의 타입을 나타낸다.
- PCA component의 변동성이 왜 피처들 간의 상관도가 비슷하다는 걸 말해주는 걸까? explained_variance_ratio_ 속성의 함수식을 들여다봐야겠다.
- 입력 데이터 결정값 클래스 별로 개별 피처의 평균 벡터를 구한다는게 무슨 말이지? X가 피처 벡터일 때, E(X)가 피처 평균 벡터이다. $$X=\begin{bmatrix}X_1\\X_2\\..\\X_n\end{bmatrix}\ \ \ \ E\left(X\right)=\begin{bmatrix}E\left(X_1\right)\\E\left(X_2\right)\\\ \ \ \ ...\\E\left(X_n\right)\end{bmatrix}$$ 예를 들면, 클래스 0에 속하는 데이터의 개별 피처들(X1, X2 .. Xn)의 평균 벡터(E(X1), E(X2) .. E(Xn)) 간의 공분산을 구하는게 클래스 내부 분산인 것 같다. 또, 레이블 클레스 값이 0인 데이터의 피처 평균 벡터(E(X))와 레이블 클레스 값이 1인 피처 평균 벡터(E(X)) 간의 공분산을 구하는 게 클래스 간 분산을 의미하는 것 같다.
- 벡터가 직교한다는 의미가 뭘까? 두 벡터의 내적이 0이라는 뜻, 즉 수직
- 희소 행렬이란 무엇일까? 밀집 행렬이란 무엇일까? 희소 행렬: 항들에 0이 많이 있는 행렬, 밀집 행렬은: 항들에 0이 거의 없거나 없는 행렬
- 원본 데이터를 Truncated SVD 방식으로 분해된 U*Sigma 행렬에 선형 변환해 생성합니다 . 축에 투영한다는 의미인 것 같다.
- plt.scatter(x=iris_tsvd[:, 0], y=iris_tsvd[:, 1], c=iris.target)에서 color를 이렇게 줄 수도 있구나.
- 원복 피처별 component 비율값이 무엇인가? PCA의 경우 분산이 가장 큰 방향으로 축이 만들어 지는데, 이 축 방향을 의미하는 것 같은데, Eigen Vector 즉, 고유벡터입니다.
- NMF에서 분해 행렬 W와 H에 대한 더 자세한 이해: https://bcho.tistory.com/1216에서 보면 알 수 있듯이, NMF_component 들은 각 행으로 들어온 데이터 들의 특성을 파악하고, H는 피처들의 특성을 파악한다. 위의 링크에서는 책제목의 단어를 통해 비슷한 정도를 예측하고 있다.
- 사용자 - 평가 순위(user - Rating) 데이터 세트를 행렬 분해 기법을 통해 분해하면서 사용자가 아직 평가하지 않은 상품에 대한 잠재적인 요소를 추출해 이를 통해 평가 순위(Rating)을 예측한다는 말에서 이미 평가한 데이터 세트의 분해 행렬과 아직 평가하지 않은 상품의 분해 행렬이 비슷한 순서대로 추천해준다는 뜻인 것 같다.
출처: 파이썬 머신러닝 완벽 가이드(권철민)
사진 출처:
https://link.springer.com/article/10.1007/s12652-019-01550-5
http://jun.hansung.ac.kr/PR/12%20LDA.pdf
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=hancury&logNo=221215245092
'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글
군집화 실습 - 고객 세그먼테이션 (0) | 2021.12.02 |
---|---|
군집화 (0) | 2021.11.28 |
회귀 실습 - 캐글 주택 가격: 고급 회귀 기법 (0) | 2021.11.24 |
회귀 실습 - 자전거 대여 수요 예측 (0) | 2021.11.23 |
회귀 (0) | 2021.11.19 |