CH 07. 비지도 학습
주성분분석(PCA; Principal Components Analysis)
- 흔히 변수들은 함께 변하기 때문에(공변), 어느 한 변수에서의 일부 변화는 실제로 다른 변수에서의 변화에 의해 중복되기도 한다(예를 들어 식당의 음식 값과 팁). 주성분분석은 수치형 변수가 어떤 식으로 공변하는지 알아내는 기법이다.
- 전체 변수들의 변동성을 거의 대부분 설명할 수 있는 적은 수의 변수들의 집합을 주성분이라고 하며, 이를 이용해 데이터의 차원을 줄일 수 있다. 주성분을 만드는 데 사용되는 가중치는 결국 새로운 주성분을 만드는 데 기존의 변수들이 어느 정도 기여하는지를 보여준다.
주성분 계산
- 주성분은 예측변수(수치형)들의 선형결합으로, 수치형 변수에 적용되며 범주형 변수에는 적용할 수 없다.
주성분 해석
- 주성분들은 서로 간의 상관관계가 최소화되며 중복성이 줄어들도록 한다.
- 주성분에 대한 이해를 돕기 위해 사용되는 표준화된 시각화 방법으로 주성분의 상대적인 중요도를 표시해주는 스크리그래프가 있다. 사이킷런 결과에서 이러한 그래프를 만드는 데 필요한 정보는 explain_variance_에서 얻을 수 있다.
대응분석
- PCA는 범주형 데이터에 사용할 수 없지만 그래도 어느 정도 관련 있는 기술은 대응분석이다. 이 분석의 목적은 범주간 혹은 범주형 피쳐 간의 연관성을 인식하는 것이다. 대응분석과 주성분분석은 주로 차원 스케일링을 위한 행렬 대수라는 기본 원리에 공통점이 있다. 사이킷런 API를 사용하여 대응분석을 구현한 prince 패키지를 사용할 수 있다.
- 대응분석은 범주형 데이터에 대해 표면적으로 유사한 기술이지만, 빅데이터에서는 유용하지 않다.
k-평균 클러스터링
- k-평균은 최초로 개발된 클러스터링 기법이다. 알고리즘이 상대적으로 간단하고 데이터 크기가 커져도 손쉽게 사용할 수 있다는 점에서 아직도 널리 사용되고 있다.
- k-평균은 데이터를 k개의 클러스터로 나눈다. 이때 할당된 클러스터의 평균과 포함된 데이터들의 거리 제곱합이 최소가 되도록 한다. 이를 클러스터 내 제곱합 또는 클러스터 내 SS라고 한다. k-평균은 각 클러스터의 크기가 동일하다는 보장은 없지만, 클러스터들끼리는 최대한 멀리 떨어지도록 한다.
클러스터 평균
: 여러 변수가 존재하는 레코드들을 클러스터링할 때, 클러스터 평균이라는 것은 하나의 값(스칼라)이 아닌 각 변수들의 평균으로 이루어진 벡터를 의미한다.
df = sp500_px.loc[sp500_px.index >= '2011-01-01', ['XOM', 'CVX']]
kmeans = KMeans(n_clusters=4, n_init='auto').fit(df)
df['cluster'] = kmeans.labels_
centers = pd.DataFrame(kmeans.cluster_centers_, columns=['XOM', 'CVX'])
fig, ax = plt.subplots(figsize=(4, 4))
ax = sns.scatterplot(x='XOM', y='CVX', hue='cluster', style='cluster',
ax=ax, data=df)
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
centers.plot.scatter(x='XOM', y='CVX', ax=ax, s=50, color='black')
plt.tight_layout()
plt.show()
k-평균 알고리즘
- 사용자가 미리 정해준 k 값과 클러스터 평균의 초깃값을 가지고 알고리즘을 시작하며, 아래 과정을 반복한다.
- 각 레코드들 거리가 가장 가까운 평균을 갖는 클러스터에 할당한다.
- 새로 할당된 레코드들을 가지고 새로운 클러스터 평균을 계산한다.
- 각 레코드에 대한 클러스터 할당이 더는 변화하지 않을 때 알고리즘이 수렴했다고 볼 수 있다.
클러스터 해석
- kmeans 함수에서 가장 중요한 두 출력은 바로 클러스터의 크기와 클러스터 평균이다.
- 파이썬에서는 표준 라이브러리의 collections.Counter 클래스를 사용하여 이 정보를 얻을 수 있다. 구현상의 차이와 알고리즘 내부에 존재하는 무작위성 때문에 서로 다른 여러 가지 결과가 나올 수 있다.
클러스터 개수 선정
- 실무 혹은 관리상의 고려에 따라 클러스터 개수를 미리 결정하기가 어려울 경우, 통계적 접근 방식을 사용할 수 있다. 다만 '최상의' 클러스터 개수를 찾는 딱 한 가지 표준화된 방법은 없다.
- elbow method는 언제 클러스터 세트가 데이터의 분산 '대부분'을 설명하는지를 알려준다. 여기에 새로운 클러스터를 더 추가하면 분산에 대한 기여도가 상대적으로 작아진다. 즉 이는 누적 분산이 가파르게 상승한 다음 어느 순간 평평하게 되는 지점을 말하며, 이러한 성질 때문에 elbow라는 이름이 붙었다.
inertia = []
for n_clusters in range(2, 15):
kmeans = KMeans(n_clusters=n_clusters, random_state=0, n_init='auto').fit(top_sp)
inertia.append(kmeans.inertia_ / n_clusters)
inertias = pd.DataFrame({'n_clusters': range(2, 15), 'inertia': inertia})
ax = inertias.plot(x='n_clusters', y='inertia')
plt.xlabel('Number of clusters(k)')
plt.ylabel('Average Within-Cluster Squared Distances')
plt.ylim((0, 1.1 * inertias.inertia.max()))
ax.legend().set_visible(False)
plt.tight_layout()
plt.show()
계층적 클러스터링
- 계층적 클러스터링은 k-평균 대신 사용하는 클러스터링 방법으로 k-평균과는 아주 다른 결과를 보여준다. 특이점이나 비정상적인 그룹이나 레코드를 발견하는 데 더 민감하다. 계층적 클러스터링은 또한 직관적인 시각화가 가능하여 클러스터를 해석하기가 쉽다.
- 계층적 클러스터링의 유연성에는 비용이 따르기 때문에 수백만 개의 레코드가 있는 대규모 데이터에는 적용할 수 없다. 수만 개의 레코드로 이루어진 적당한 크기의 데이터의 경우에도 계층적 클러스터링을 위해서는 상대적으로 많은 컴퓨팅 리소스가 필요할 수 있다. 대부분 상대적으로 데이터 크기가 작은 문제에 주로 사용된다.
간단한 예제
n개의 레코드와 p개의 변수가 있는 일반적인 데이터에 대해 계층적 클러스터링을 사용할 수 있으며, 아래 두 가지 기본 구성 요소를 기반으로 한다.
- 두 개의 레코드 i와 j 사이의 거리를 측정하기 위한 거리 측정 지표
- 두 개의 클러스터 A와 B 사이의 차이를 측정하기 위한, 각 클러스터 구성원 간의 거리를 기반으로 한 비유사도 측정 지표
syms1 = ['AAPL', 'AMZN', 'AXP', 'COP', 'COST', 'CSCO', 'CVX', 'GOOGL', 'HD',
'INTC', 'JPM', 'MSFT', 'SLB', 'TGT', 'USB', 'WFC', 'WMT', 'XOM']
df = sp500_px.loc[sp500_px.index >= '2011-01-01', syms1].transpose()
Z = linkage(df, method='complete')
# print(Z.shape)
# (17, 4)
fig, ax = plt.subplots(figsize=(5, 5))
dendrogram(Z, labels=list(df.index), color_threshold=0)
plt.xticks(rotation=90)
ax.set_ylabel('distance')
plt.tight_layout()
plt.show()
- scipy 패키지의 scipy.cluster.hierarchy 모듈에는 계층적 클러스터링을 위한 다양한 메서드를 제공한다.
- 계층적 클러스터링에서 linkage 함수는 데이터 포인트 간의 거리 행렬을 기반으로 클러스터를 병합하여 계층적 클러스터링 트리를 생성한다.
- method: 클러스터 간의 거리 측정 방법을 지정. single, complete, average, ward 등이 있다. complete는 클러스터 간의 최대 거리를 사용한다.
- metric: 거리 계산에 사용할 거리 측정 방법을 지정. 기본값은 euclidean
- 계층적 클러스터링은 트리 모델과 같이 자연스러운 시각적 표현이 가능하며, 이를 덴드로그램이라고 한다. 파이썬에서는 linkage 함수의 결과를 시각화하기 위해 dendrogram 메서드를 사용할 수 있다.
- k-평균과 달리 클러스터의 수를 미리 지정할 필요는 없다. 그래프상에서 위 또는 아래로 이동하는 수평선을 이용해 서로 다른 수의 클러스터를 식별할 수 있다. 수평선이 수직선과 교차하는 곳에서 클러스터를 정의할 수 있다. 파이썬에서는 fcluster 메서드를 사용하여 원하는 개수의 클러스터를 추출할 수 있다.
memb = fcluster(Z, 4, criterion='maxclust')
memb = pd.Series(memb, index=df.index)
for key, item in memb.groupby(memb):
print(f"{key} : {', '.join(item.index)}")
# 1 : COP, CVX, SLB, XOM
# 2 : AAPL, AXP, COST, CSCO, HD, INTC, JPM, MSFT, TGT, USB, WFC, WMT
# 3 : AMZN
# 4 : GOOGL
병합 알고리즘
- 계층적 클러스터링에서 가장 중요한 알고리즘은 바로 병합 알고리즘인데 이는 유사한 클러스터들을 반복적으로 병합하는 역할을 한다. 병합 알고리즘은 단일 레코드로 구성된 클러스터에서 시작하여 점점 더 큰 클러스터들을 만든다.
- 병합 과정은 내역이 남고 시각화할 수 있으며, 사용자가 미리 클러스터 수를 지정하지 않더라도 여러 단계에서 클러스터의 수와 구조를 시각화할 수 있다.
- 병합 알고리즘의 주요 단계는 다음과 같다.
- 데이터의 모든 레코드에 대해, 단일 레코드로만 구성된 클러스터들로 초기 클러스터 집합을 만든다.
- 모든 쌍의 클러스터 k, l 사이의 비유사도 D(C_k, C_l)을 계산한다.
- D(C_k, C_l)에 따라 가장 두꺼운 두 클러스터 C_k, C_l을 병합한다.
- 둘 이상의 클러스터가 남아 있으면 2단계로 다시 돌아간다. 그렇지 않고 클러스터가 하나 남는다면 알고리즘을 멈춘다.
비유사도 측정
- 비유사도를 측정하는 네 가지 일반적인 지표는 완전연결, 단일연결, 평균연결, 최소분산이다.
- 완전연결: 두 클러스터 간의 거리를 그 클러스터 간의 가장 먼 데이터 포인트 간의 거리로 정의
- 단일연결: 두 클러스터의 레코드 간 최소 거리를 사용하는 방식
- 평균연결: 모든 거리 상의 평균을 사용하는 방법으로 이는 단일연결과 완전연결법 사이를 절충한 방법
- 최소분산(=워드 기법): 클러스터 내의 제곱합을 최소화
df = sp500_px.loc[sp500_px.index >= '2011-01-01', ['XOM', 'CVX']]
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(5, 5))
for i, method in enumerate(['single', 'average', 'complete', 'ward']):
ax = axes[i // 2, i % 2]
Z = linkage(df, method=method)
colors = [f'C{c+1}' for c in fcluster(Z, 4, criterion='maxclust')]
ax = sns.scatterplot(x='XOM', y='CVX', hue=colors, style=colors,
size=0.5, ax=ax, data=df, legend=False)
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
ax.set_title(method)
plt.tight_layout()
plt.show()
모델 기반 클러스터링
- 계층적 클러스터링과 k-평균 같은 클러스터링 방법들은 모두 휴리스틱한 방법이라고 할 수 있으며, 직접 관측한(즉 확률모형에 기반하지 않고) 데이터들이 서로 가깝게 있는 클러스터를 찾는 데 주로 사용된다.
- 모델 기반 클러스터링의 목적은 데이터를 가장 살 설명하는 다변량정규분포를 찾는 것이다.
다변량정규분포
- 가장 널리 사용되는 대부분의 모델 기반 클러스터링 방법은 모두 다변량정규분포를 따른다.
- 다변량정규분포는 p개의 변수 집합 X_1, X_2, ..., X_p 에 대해 정규분포를 일반화한 것이다.
정규혼합
- 모델 기반 클러스터링의 핵심 아이디어는 각 레코드가 k개의 다변량정규분포 중 하나로부터 발생했다고 가정하는 것이다. 여기서 k가 클러스터의 개수를 의미한다.
- 사이킷런에는 모델 기반 클러스터링을 위한 sklearn.mixture.GaussianMixture 클래스가 있다.
df = sp500_px.loc[sp500_px.index >= '2011-01-01', ['XOM', 'CVX']]
mclust = GaussianMixture(n_components=2).fit(df) # 데이터에 대한 가우시안 혼합 모델을 피팅
# print(mclust.bic(df))
# 4589.382473953898
fig, ax = plt.subplots(figsize=(4, 4))
colors = [f'C{c}' for c in mclust.predict(df)]
df.plot.scatter(x='XOM', y='CVX', c=colors, alpha=0.5, ax=ax)
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
plt.tight_layout()
plt.show()
클러스터 개수 결정하기
- k-평균이나 계층적 클러스터링과 달리 R의 mcluster는 클러스터 수를 자동으로 선택한다. 바로 베이즈 정보기준 값이 가장 큰 클러스터의 개수를 선택하도록 동작하기 때문이다(BIC는 AIC와 유사하다).
- BIC는 모델의 파라미터 개수에 대해 벌점을 주는 방식으로 가장 적합한 모델을 선택한다.
- 모델 기반 클러스터링의 경우 클러스터를 추가하면 할수록 모델의 파라미터 개수가 증가되는 대신 항상 적합도는 좋아진다.
모델 기반 클러스터링의 한계
- 기본적으로 데이터들이 모델을 따른다는 가정이 필요하며, 클러스터링 결과는 이 가정에 따라 매우 다르다.
- 필요한 계산량 역시 계층적 클러스터링보다 높으므로 대용량 데이터로 확장하기가 어렵다.
스케일링과 범주형 변수
- 비지도 학습 기술을 이용할 때는 일반적으로 데이터를 적절하게 스케일해야 한다. 스케일링이 중요하지 않았던 회귀나 분류 방법들과는 차이가 있다(물론 k-최근접 이웃 알고리즘은 예외이다).
- 범주형 데이터는 일부 클러스터링 과정에서 특별한 문제를 일으킬 수 있다. KNN에서와 마찬가지로, 순서가 없는 요인변수는 일반적으로 원-핫 인코딩을 사용하여 이진 변수 집합으로 변환한다. 이러한 이진변수는 다른 데이터와 스케일이 다를 뿐만 아니라 PCA나 k-평균 같은 기법을 사용할 때 이진변수가 두 가지의 값만 가질 수 있다는 것 때문에 문제가 될 수 있다.
변수 스케일링
scaler = preprocessing.StandardScaler()
df0 = scaler.fit_transform(df * 1.0)
kmeans = KMeans(n_clusters=4, random_state=1, n_init='auto').fit(df0)
counts = Counter(kmeans.labels_)
centers = pd.DataFrame(scaler.inverse_transform(kmeans.cluster_centers_),
columns=columns)
centers['size'] = [counts[i] for i in range(4)]
print(centers)
- n_init='auto' → 초기 중심을 선택할 때 실행할 알고리즘을 자동으로 선택
- 사이킷런의 inverse_transform 메서드를 사용하면 클러스터 중심을 다시 원래 스케일로 변환할 수 있다.
- 클러스터 중심을 원래 단위로 재조정하지 않으면, z 점수로 표시되므로 해석하기가 어려워진다.
지배 변수
- 변수들이 서로 동일한 규무로 측정되고 상대적 중요성을 정확하게 반영하는 경우(예를 들어 주가 변동)조차도 변수의 스케일을 재조정하는 것이 유용할 수 있다.
syms = ['GOOGL', 'AMZN', 'AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM',
'SLB', 'COP', 'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST']
top_sp1 = sp500_px.loc[sp500_px.index >= '2005-01-01', syms]
sp_pca1 = PCA()
sp_pca1.fit(top_sp1)
explained_variance = pd.DataFrame(sp_pca1.explained_variance_)
ax = explained_variance.head(10).plot.bar(legend=False, figsize=(4, 4))
ax.set_xlabel('Component')
plt.tight_layout()
plt.show()
loadings = pd.DataFrame(sp_pca1.components_[0:2, :],
columns=top_sp1.columns)
print(loadings.transpose())
# 0 1
# GOOGL 0.857310 -0.477873
# AMZN 0.444728 0.874149
# AAPL 0.071627 0.020802
# MSFT 0.036002 0.006204
# CSCO 0.029205 0.003045
# INTC 0.026666 0.006069
# CVX 0.089548 0.037420
# XOM 0.080336 0.020511
# SLB 0.110218 0.030356
# COP 0.057739 0.024117
# JPM 0.071228 0.009244
# WFC 0.053228 0.008597
# USB 0.041670 0.005952
# AXP 0.078907 0.024027
# WMT 0.040346 0.007141
# TGT 0.063659 0.024662
# HD 0.051412 0.032922
# COST 0.071403 0.033826
- 처음 두 가지 주성분은 GOOGL과 AMZN에 의해 거의 완전히 지배되고 있다. 이는 GOOGL과 AMZN의 주가 움직임이 전체 변동성의 대부분을 지배하기 때문이다. 이러한 상황에서는 변수를 스케일링해서 포함하거나, 이러한 지배 변수를 전체 분석에서 제외하고도 별도로 처리할 수도 있다. 이러한 방법이 항상 옳다고는 할 수 없으며 응용 분야에 따라 달라지낟.
범주형 데이터와 고워 거리
- 범주형 데이터가 있는 경우에는 순서형(정렬된 요인) 변수 또는 이진형(더미) 변수를 사용하여 수치형 데이터로 변환해야 한다. 데이터를 구성하는 변수들에 연속형과 이진형 변수가 섞여 있는 경우 비슷한 스케일이 되도록 변수의 크기를 조정해야 한다. 이를 위한 대표적인 방법은 고워 거리를 사용하는 것이다.
- 고워 거리의 기본 아이디어는 각 변수의 데이터 유형에 따라 거리 지표를 다르게 적용하는 것이다.
- 수치형 변수나 순서형 요소에는 두 레코드 간의 거리는 차이의 절댓값(맨해튼 거리)으로 계산한다.
- 범주형 변수의 경우 두 레코드 사이의 범주가 서로 다르면 거리가 1이고 범주가 동일하면 거리는 0이다.
- 고워 거리는 다음과 같이 계산한다.
- 각 레코드의 변수 i와 j의 모든 쌍에 대해 거리 d_ij를 계산한다.
- 각 d_ij의 크기를 최솟값이 0이고 최댓값이 1이 되도록 스케일을 조정한다.
- 거리 행렬을 구하기 위해 변수 간에 스케일된 거리를 모두 더한 후 평균 혹은 가중평균을 계산한다.
혼합 데이터의 클러스터링 문제
- k-평균과 PCA는 연속형 변수에 가장 적합하다.
- 데이터 집합의 크기가 더 작아질수록 고워 거리를 사용하여 계층적 클러스터링을 하는 것이 좋다.
- 원칙적으로는 이진형 또는 범주형 데이터에도 k-평균을 적용할 수 있다. 범주형 데이터의 경우에는 일반적으로 원-핫 인코딩 방법을 이용해 수치형으로 변환할 수 있다. 하지만 실무에서는 k-평균과 PCA를 이진형 데이터와 함께 사용하는 것이 어려울 수 있다.
- 표준 z 점수를 사용할 경우, 이진형 변수가 클러스터 결과에 지대한 영향을 미치게 되는데, 0/1 변수의 경우 0 또는 1의 값인 레코드가 모두 한 클러스터에 포함되므로 k-평균에서 클러스터 내 제곱합이 작아지기 때문이다.
- 이러한 문제를 피하기 위해 이진형 변수의 크기를 다른 변수들보다 작은 값으로 조정할 수 있다. 아니면 데이터의 크기가 아주 큰 경우에는 특정 범주 값들에 따라 서로 다른 하위 집합에 클러스터링을 적용할 수도 있다.