Search
Duplicate

머신 러닝 교과서/ 레이블되지 않은 데이터 다루기: 군집 분석

k-평균 알고리즘을 사용하여 유사한 객체 그룹핑

이 절에서는 가장 잘 알려준 군집(clustering) 알고리즘 중 하나인 k-평균(k-means)을 다루겠다. k-평균은 산업 현장은 물론 학계에서도 널리 사용되는 방법으로 비슷한 객체로 이루어진 그룹을 찾는 기법이다.

사이킷런을 사용한 k-평균 군집

k-평균 알고리즘은 구현하기 쉽고 다른 군집 알고리즘에 비해 계산 효율성이 높기 때문에 인기가 많다.
k-평균 알고리즘은 프로토타입 기반 군집(prototype-based clustering)에 속한다.
프로토타입 기반 군집은 각 클러스터가 하나의 프로토타입으로 표현된다는 뜻이다.
프로토타입은 연속적인 특성에서는 비슷한 데이터 포인트의 센트로이드(centroid, 평균)이거나 범주형 특성에서는 메도이드(medoid, 가장 대표되는 포인트나 가장 자주 등장하는 포인트)가 된다.
k-평균 알고리즘이 원형 클러스터를 구분하는데 뛰어나지만, 이 알고리즘의 단점은 사전에 클러스터 개수 k를 지정해야 한다는 것이다. 적절하지 않은 k를 고르면 군집 성능이 좋지 않다.
다음 코드는 k-평균 군집 알고리즘에 사용할 간단한 2차원 데이터셋 예제를 만드는 것이다.
from sklearn.datasets import make_blobs import matplotlib.pyplot as plt X, y = make_blobs(n_samples=150, n_features=2, centers=3, cluster_std=0.5, shuffle=True, random_state=0) plt.scatter(X[:,0], X[:,1], c='white', marker='o', edgecolor='black', s=50) plt.grid() plt.tight_layout() plt.show()
Python
실제 군집 애플리케이션에서는 샘플에 대한 진짜 카테고리 정보가 전혀 없다. (그렇지 않으면 지도 학습에 해당한다) 여기서 목표는 특성의 유사도에 기초하여 샘플을 그룹으로 모으는 것이다.
이런 문제에 사용할 수 있는 k-평균 알고리즘은 다음 네 단계로 요약된다.
1.
샘플 포인트에서 랜덤하게 k개의 센트로이드를 초기 클러스터 중심으로 선택한다.
2.
각 샘플을 가장 가까운 센트로이드  에 할당한다. μ(j),j{l,...,k}\mu^{(j)}, j \in \{ l, ..., k \}
3.
할당된 샘플들의 중심으로 센트로이드를 이동한다.
4.
클러스터 할당이 변하지 않거나 사용자가 지정한 허용 오차나 최대 반복 횟수에 도달할 때까지 2-3 단계를 반복한다.
각 샘플간의 유사도는 거리의 반대 개념으로 정의할 수 있다.
연속적인 특성을 가진 샘플을 클러스터로 묶는데 널리 사용되는 거리는 m-차원 공간에 있는 두 포인트 x,yx, y 사이의 유클리디안 거리의 제곱(squared Euclidean distance)이다
d(x,y)2=j=1m(xjyj)2=xy22d(x, y)^{2} = \sum_{j=1}^{m} (x_{j} - y_{j})^{2} = \| x- y \|_{2}^{2}
위 식에서 인덱스 jj는 샘플 포인트 x,yx, y의 jj번째 차원(특성 열)을 나타낸다.
유클리디안 거리 지표를 기반으로 간단한 최적화 문제로 k-평균 알고리즘을 기술할 수 있다. 클러스터 내 제곱 오차합(SSE) 또는 클러스터 관성(cluster inertia)를 반복적으로 최소화하는 방법이다.
SSE=i=1nj=1kw(i,j)x(i)μ(j)22SSE = \sum_{i=1}^{n} \sum_{j=1}^{k} w^{(i, j)} \| x^{(i)} - \mu^{(j)} \|_{2}^{2}
여기서 μ(j)\mu^{(j)}는 클러스터 jj의 대표 포인트(센트로이드)이다. 샘플 x(i)x^{(i)}가 클러스터 jj 안에 있다면 w(i,j)=1w^{(i, j)} = 1이고 아니면  w(i,j)=0w^{(i, j)} = 0이다.
준비된 샘플 데이터셋에 사이킷런 cluster 모듈의 KMeans 클래스를 적용해 보자.
from sklearn.cluster import KMeans km = KMeans(n_clusters=3, init='random', n_init=10, max_iter=300, tol=1e-04, random_state=0) y_km = km.fit_predict(X)
Python
이 코드에서 클러스터 개수를 3으로 지정했다. 클러스터를 사전에 지정해야 하는 것은 k-평균 알고리즘의 한계 중 하나이다.
n_init = 10으로 설정하면 k-평균 군집 알고리즘을 각기 다른 랜덤한 센트로이드에서 독립적으로 열 번 실행하여 가장 낮은 SSE를 만드는 하나를 최종 모델로 선택한다.
max_iter 매개변수는 한 번의 실행에서 수행할 최대 반복 횟수를 지정한다. 사이킷런의 k-평균 구현은 최대 반복 횟수에 도달하기 전에 수렴하면 일찍 종료한다.
수렴에 문제가 있다면 tol 매개변수 값을 늘리는 것이 한 가지 방법이다. 이 매개변수는 수렴을 결정한느 클러스터 내 제곱 오차 합의 변화량에 대한 허용 오차를 조정한다. 위 코드에서는 1e-04(=0.0001)로 설정하였다.
k-평균의 한 가지 문제는 하나 이상의 클러스터가 비어 있으 ㄹ수 있다는 점이다. 이런 문제는 k-메도이드(k-medoid)나 C-평균(C-means)에는 나타나지 않는다.
사이킷런의 k-평균에는 이 문제가 고려되어 있다. 한 클러스터가 비어있다면 알고리즘이 빈 클러스터의 센트로이드에서 가장 멀리 떨어진 샘플을 찾고 그 다음 가장 먼 포인트에 센트로이드를 다시 할당한다.
앞선 코드를 시각화 하면 다음과 같다.
plt.scatter(X[y_km==0, 0], X[y_km==0, 1], s=50, c='lightgreen', marker='s', edgecolor='black', label='cluster 1') plt.scatter(X[y_km==1, 0], X[y_km==1, 1], s=50, c='orange', marker='o', edgecolor='black', label='cluster 2') plt.scatter(X[y_km==2, 0], X[y_km==2, 1], s=50, c='lightblue', marker='v', edgecolor='black', label='cluster 3') plt.scatter(km.cluster_centers_[:, 0], km.cluster_centers_[:, 1], s=250, c='red', marker='*', edgecolor='black', label='centroids') plt.legend(scatterpoints=1) plt.grid() plt.tight_layout() plt.show()
Python

k-평균++로 초기 클러스터 센트로이드를 똑똑하게 할당

k-평균 알고리즘에서 초기 센트로이드가 좋지 않게 선택되면 이따금 나쁜 군집 결과를 만들거나 수렴이 느려진다.
이 문제를 해결하는 한 가지 방법은 같은 데이터셋에서 k-평균 알고리즘을 여러 번 실행하여 SSE 입장에서 가장 성능이 좋은 모델을 선택하는 것이다.
또 다른 방법은 k-평균++ 알고리즘을 통해 초기 센트로이드가 서로 멀리 떨어지도록 위치하는 것이다. 이는 기본 k-평균보다 일관되고 훌륭한 결과를 만든다.
k-평균++ 초기화는 다음과 같이 정리할 수 있다.
선택한 k개의 센트로이드를 저장할 빈 집합 MM을 초기화 한다.
입력 샘플에서 첫 번째 센트로이드 μ(i)\mu^{(i)}를 랜덤하게 선택하고 MM에 할당한다.
MM에 있지 않은 각 샘플 x(i)x^{(i)}에 대해 MM에 있는 센트로이드까지 최소 제곱 거리 d(x(i),M)2d(x^{(i)}, M)^{2}를 찾는다.
다음 식과 같은 가중치가 적용된 확률 분포를 사용하여 다음 센트로이드 μ(p)\mu^{(p)}를 랜덤하게 선택한다.
d(μ(p),M)2id(x(i),M)2{d(\mu^{(p)}, M)^{2} \over \sum_{i} d(x^{(i)}, M)^{2}}
k개의 센트로이드를 선택할 때까지 단계 2-3을 반복한다.
그 다음 기본 k-평균 알고리즘을 수행한다.
사이킷런의 Kmeans 클래스로 k-평균++를 사용하려면 init 매개변수를 ‘k-means++’로 지정하기만 하면 된다.
사실 ‘k-means++’가 실전에서 매우 권장되기 때문에 init 매개변수의 기본값이다.

직접 군집 vs 간접 군집

직접 군집(hard clustering)은 데이터셋의 샘플이 정확히 하나의 클러스터에 할당되는 알고리즘 종류를 말한다. 이전 절세 설명한 k-평균 알고리즘이 이에 해당한다.
반면 간접 군집(soft clustering) 또는 퍼지 군집(fuzzy clustering) 알고리즘은 샘플을 하나 이상의 클러스터에 할당한다.
간접 군집의 대표적인 예는 퍼지 C-평균(Fuzzy C-Means, FCM) 알고리즘이다.
원래 아이디어는 조셉 던(Joseph C. Dunn)이 k-평균을 개선하기 위해 퍼지 군집의 초기 버전을 처음 제안한 1970년대로 거슬러 올라간다. 약 10년 정도 지난 후에 제임스 베즈덱(James C. Bezdek)이 퍼지 군집 알고리즘의 개선 버전을 공개했고, 이것이 FCM 알고리즘이라 불리게 되었다.
FCM 처리 단계는 k-평균과 매우비슷하다. 다만 포인트가 직접적으로 클러스터에 할당되는 것을 각 클러스터에 속할 확률로 바꾼다.
k-평균에서는 샘플 x의 소속을 이진 희소벡터로 표현할 수 있다.
[μ(1)0μ(2)1μ(3)0]\left[ \begin{array}{rrr} \mu^{(1)} \to 0 \\ \mu^{(2)} \to 1 \\ \mu^{(3)} \to 0 \end{array} \right]
여기서 값이 1인 인덱스 위치가 이 샘플이 할당된 클러스터 센트로이드를 나타낸다.
이와 다르게 FCM의 클래스 소속 벡터는 다음과 같이 표현할 수 있다.
[μ(1)0.10μ(2)0.85μ(3)0.05]\left[ \begin{array}{rrr} \mu^{(1)} \to 0.10 \\ \mu^{(2)} \to 0.85 \\ \mu^{(3)} \to 0.05 \end{array} \right]
여기서 각 값은 [0, 1] 범위 안에 있으며 각 클러스터 센트로이드의 확률을 나타낸다. 한 샘플에 대한 클래스 소속 확률의 합은 1이다.
k-평균 알고리즘과 비슷하게 FCM 알고리즘은 네 단계로 요약할 수 있다.
센트로이드 개수 k를 지정하고 랜덤하게 각 포인트에 대해 클러스터 확률을 할당한다.
클러스터 센트로이드 를 계산한다. μ(j),j{1,...,k}\mu^{(j)}, j \in \{ 1, ... , k \}
각 샘플에 대해 클러스터 소속 확률을 업데이트 한다.
클러스터 확률이 변하지 않거나 사용자가 지정한 허용 오차나 최대 반복 횟수에 도달할 때까지 2-3을 반복한다.
FCM의 목적 함수 JmJ_{m}sms k-평균에서 최소화하는 클러스터 내 제곱 오차합과 매우 비슷하다.
Jm=i=1nj=1kwm(i,j)x(i)μ(j)22J_{m} = \sum_{i=1}^{n} \sum_{j=1}^{k} w^{m(i, j)} \| x^{(i)} - \mu^{(j)} \|_{2}^{2}
클러스터 소속 가중치 w(i,j)w^{(i, j)}는 k-평균처럼 이진값이 아니라 실수값이다.
w(i,j)w^{(i, j)}는 추가적인 지수를 포함한다.
퍼지 계수(fuzziness coefficient) 또는 퍼지 지수(fuzzifier)라고 하는 지수 mm은 1보다 크거나 같으며(일반적으로 m=2m = 2) 퍼지의 정도를 제어한다.
mm이 클수록 클러스터 소속 확률 w(i,j)w^{(i, j)}가 작아져 더 복잡한(fuzzier) 클러스터를 만든다.
클러스터 소속 확률은 다음과 같이 계산한다.
w(i,j)=[p=1k(x(i)μ(j)2x(i)μ(p)2)2m1]1w^{(i, j)} = [ \sum_{p=1}^{k} ({\| x^{(i)} - \mu^{(j)} \|_{2} \over \| x^{(i)} - \mu^{(p)} \|_{2}})^{{2 \over m-1}} ]^{-1}
예컨대 세 개의 클러스터 중심을 선택한다면 샘플 x(i)x^{(i)}가 μ(j)\mu^{(j)}클러스터에 속할 확률은 다음과 같이 계산할 수 있다.
w(i,j)=[(x(i)μ(j)2x(i)μ(1)2)2m1+(x(i)μ(j)2x(i)μ(2)2)2m1+(x(i)μ(j)2x(i)μ(3)2)2m1]1w^{(i, j)} = [ ({\| x^{(i)} - \mu^{(j)} \|_{2} \over \| x^{(i)} - \mu^{(1)} \|_{2}})^{{2 \over m-1}} + ({\| x^{(i)} - \mu^{(j)} \|_{2} \over \| x^{(i)} - \mu^{(2)} \|_{2}})^{{2 \over m-1}} + ({\| x^{(i)} - \mu^{(j)} \|_{2} \over \| x^{(i)} - \mu^{(3)} \|_{2}})^{{2 \over m-1}} ]^{-1}
클러스터 중심 μ(j)\mu^{(j)}는 샘플의 소속 확률 wm(i,j)w^{m(i, j)}을 가중치로 주어 클러스터에 속한 모든 샘플의 평균으로 계산된다.
μ(j)=i=1nwm(i,j)x(i)i=1nwm(i,j)\mu^{(j)} = {\sum_{i=1}^{n} w^{m(i, j)} x^{(i)} \over \sum_{i=1}^{n} w^{m(i, j)}}
클러스터 소속 확률을 계산하는 공식을 보면 FCM의 각 반복이 k-평균 반복보다 비용이 더 많이 든다는 것을 알 수 있다. 하지만 FCM은 전형적으로 수렴에 도달하기까지 반복 횟수가 적게 든다.
안타깝지만 FCM 알고리즘은 아직 사이킷런에 구현되어 있지 않다. 실제로는 k-평균과 FCM이 매우 비슷한 군집 결과를 만든다고 알려져 있다.

엘보우 방법을 사용하여 최적의 클러스터 개수 찾기

비지도 학습에서 가장 어려운 점 하나는 최종 답을 모른다는 것이다. 데이터셋에 진짜 클래스 레이블이 없기 때문에 지도 학습의 성능 평가를 위해 사용한 기법들을 적용할 수 없다.
군집 품질을 평가하려면 알고리즘 자체의 지표를 사용해야 한다. 예컨대 k-평균 군집의 성능을 비교하기 위해 앞서 언급한 클래스 내 SSE(왜곡) 를 사용한다.
사이킷런을 사용하면 클래스 내 SSE를 직접 계산할 필요가 없는데, inertia_ 속성에 이미 계산되어 있기 때문이다.
클래스 내 SSE를 바탕으로 엘보우 방법이라 하는 그래프를 사용하여 문제에 최적인 클러스터 개수 k를 추정할 수 있다.
직관적으로 k가 증가하면 왜곡은 줄어들 것이다. 샘플이 할당된 센트로이드에 더 가까워지기 때문이다. 엘보우 방법 이면에 있는 아이디어는 왜곡이 빠르게 증가하는 지점의 k값을 찾는 것이다.
k값을 바꾸어 가며 왜곡 값을 그래프로 그리면 명확하게 알 수 있다.
distortions = [] for i in range(1, 11): km = KMeans(n_clusters=i, init='k-means++', n_init=10, max_iter=300, random_state=0) km.fit(X) distortions.append(km.inertia_) plt.plot(range(1, 11), distortions, marker='o') plt.xlabel('Number of clusters') plt.ylabel('Distortion') plt.tight_layout() plt.show()
Python
위 코드의 결과를 보면 k=3k = 3에서 엘보우가 나타난다는 것을 볼 수 있다. 그러므로 이 데이터셋에서는 k=3k = 3이 좋은 선택이 된다.

실루엣 그래프로 군집 품질을 정량화

군집 품질을 평가하는 또 다른 방법은 실루엣 분석(silhouette analysis)이다. 이 방법은 k-평균 이외에 이 장 뒤에서 설명할 다른 군집 알고리즘에도 적용할 수 있다.
실루엣 분석은 클러스터 내 샘플들이 얼마나 조밀하게 모여있는지를 측정하는 그래프 도구이다. 데이터셋 샘플 하나에 대한 실루엣 계수(silhouette coeffcient)를 계산하려면 다음 세 단계를 적용한다.
샘플 x(i)x^{(i)}와 동일한 클러스터 내 모든 다른 포인트 사이의 거리를 평균하여 클러스터 응집력(cluster cohesion) a(i)a^{(i)}를 계산한다.
샘플 x(i)x^{(i)}와 가장 가까운 클러스터의 모든 샘플 간 평균 거리고 최근접 클러스터의 클러스터 분리도(cluster separation) b(i)b^{(i)}를 계산한다.
클러스터 응집력과 분리도 사이의 차이를 둘 중 큰 값으로 나누어 실루엣 s(i)s^{(i)}를 다음과 같이 계산한다.
s(i)=b(i)a(i)max{b(i),a(i)}s^{(i)} = {b^{(i)} - a^{(i)} \over \max \{ b^{(i)}, a^{(i)} \} }
실루엣 계수는 -1과 1 사이의 값을 갖는다.
앞 공식을 보면 클러스터 응집력과 분리도가 같으면(b(i)=a(i))(b^{(i)} = a^{(i)}) 실루엣 계수가 0이 된다.
또 (b(i)>a(i))(b^{(i)} > a^{(i)})이면 이상적인 실루엣 계수 1에 가깝게 된다.
b(i)b^{(i)}는 샘플이 다른 클러스터와 얼마나 다른지 나타내고, a(i)a^{(i)}는 클러스터 내 다른 샘플과 얼마나 비슷한지 나타내기 때문이다.
실루엣 계수는 사이킷런의 metric 모델 아래 silhouette_samples 함수로 계산할 수 있다. 또 편의를 위해 silhouette_score 함수를 임포트 할 수 있다.
silhouette_scores 함수는 모든 샘플에 걸쳐 평균 실루엣 계수를 계산한다.
다음 코드는 k=3k = 3인 k-평균 군집의 실루엣 계수 그래프이다.
import matplotlib.pyplot as plt from matplotlib import cm import numpy as np km = KMeans(n_clusters=3, init='k-means++', n_init=10, max_iter=300, tol=1e-04, random_state=0) y_km = km.fit_predict(X) cluster_labels = np.unique(y_km) n_clusters = cluster_labels.shape[0] silhouette_vals = silhouette_samples(X, y_km, metric='euclidean') y_ax_lower, y_ax_upper = 0, 0 yticks = [] for i, c in enumerate(cluster_labels): c_silhouette_vals = silhouette_vals[y_km == c] c_silhouette_vals.sort() y_ax_upper += len(c_silhouette_vals) color = cm.jet(float(i)/n_clusters) plt.barh(range(y_ax_lower, y_ax_upper), c_silhouette_vals, height=1.0, edgecolor='none', color=color) yticks.append((y_ax_lower + y_ax_upper) / 2.) y_ax_lower += len(c_silhouette_vals) silhouette_avg = np.mean(silhouette_vals) plt.axvline(silhouette_avg, color="red", linestyle="--") plt.yticks(yticks, cluster_labels + 1) plt.xlabel('Silhouette coefficient') plt.ylabel('Cluster') plt.tight_layout() plt.show()
Python
위 그래프에서 보이듯이 실루엣 계수의 값이 0에서 멀리 떨어져 있다. 이는 군집이 잘 되었다는 것을 나타낸다.
나쁜 군집에 대해 실루엣 그래프가 어떻게 보이는지 알아보기 위해 2개의 센트로이드로 k-평균 알고리즘을 적용해 보자.
(앞선 내용과 동일하므로 코드 생략)
이때의 실루엣 그래프는 아래와 같다. 이는 군집 결과가 나쁘거나 최적이 아니라는 뜻이 된다.
(앞선 내용과 동일하므로 코드 생략)

계층적인 트리로 클러스터 조직화

여기서는 프로토타입 기반 군집의 또 다른 방법인 계층 군집(hierarchical clustering)을 알아보겠다.
계층 군집 알고리즘의 한 가지 장점은 덴드로그램(dendrogram) (이진 트리 형태로 계층 군집을 시각화 할 수 있는 도구)을 그릴 수 있다는 것이다. 또 다른 장점은 클러스터 개수를 미리 지정할 필요가 없다는 것이다.
계층 군집의 두 가지 방법은 병합 계층 군집(agglomerative hierarchical clustering)과 분할 계층 군집(divisive hierarchical clustering)이다.
분할 군집에서는 전체 샘플을 포함하는 하나의 클러스터에서 시작하여 더 작은 클러스터로 반복적으로 나누고, 이를 클러스터 안에 샘플이 하나만 남을 때까지 한다.
병합 군집은 이와 반대인데, 먼저 각 샘플이 독립적인 클러스터가 되고 하나의 클러스터가 남을 때까지 가장 가까운 클러스터를 합친다.

상향식으로 클러스터 묶기

병합 계층 군집의 두 가지 기본 알고리즘은 단일 연결(single linkage)와 완전 연결(complete linkage)이다.
단일 연결을 사용하면 클러스터 쌍에서 가장 비슷한 샘플 간 거리를 계산한다. 그 다음 거리가 가장 작은 두 클러스터를 합친다.
완전 연결 방식은 단일 연결과 비슷하지만 클러스터 쌍에서 가장 비슷한 샘플을 비교하는게 아니라 가장 비슷하지 않은 샘플을 비교하여 병합을 수행한다.
병합 계층 군집에서 널리 사용하는 다른 알고리즘은 평균 연결(average linkage)와 와드 연결(Ward’s linkage)이다.
평균 연결은 두 클러스터에 있는 모든 샘플 사이의 평균 거리가 가장 작은 클러스터 쌍을 합친다.
와드 연결은 클러스터 내 SSE가 가장 작게 증가하는 두 클러스터를 합친다.
완전 연결 계층 군집은 반복적인 과정이며, 다음 단계로 요약할 수 있다.
1.
모든 샘플의 거리 행렬을 계산한다.
2.
모든 데이터 포인트를 단일 클러스터로 표현한다.
3.
가장 비슷하지 않은 (멀리 떨어진) 샘플 사이 거리에 기초하여 가장 가까운 두 클러스터를 합친다.
4.
유사도 행렬을 업데이트 한다.
5.
하나의 클러스터가 남을 때까지 2-4단계를 반복한다.
거리 행렬을 계산하는 바법을 알아보기 위해 샘플 데이터를 만들어 보자.
import pandas as pd import numpy as np np.random.seed(123) variables = ['X', 'Y', 'Z'] labels = ['ID_0', 'ID_1', 'ID_2', 'ID_3', 'ID_4'] X = np.random.random_sample([5,3]) * 10 df = pd.DataFrame(X, columns=variables, index=labels)
Python

거리 행렬에서 계층 군집 수행

계층 군집 알고리즘의 입력에 사용할 거리 행렬을 계산하기 위해 사이파이의 spatial.distance 모듈에서 pdist 함수를 사용하겠다.
from scipy.spatial.distance import pdist, squareform row_dist = pd.DataFrame(squareform(pdist(df, metric='euclidean')), columns=labels, index=labels)
Python
위 코드에서 특성 X, Y, Z를 기반으로 데이터셋 모든 샘플 쌍의 유클리디안 거리를 계산한다.
pdist 함수는 축약된 거리 행렬을 반환한다. 이를 squareform 함수에 넣어 샘플간 거리 대칭 행렬을 만든다.
그 다음 사이파이 cluster.hierarchy 모듈의 linkage 함수를 사용해서 완전 연결 병합을 적용해 보자. 이 함수는 연결 행렬을 반환한다.
from scipy.cluster.hierarchy import linkage row_clusters = linkage(df.values, method='complete', metric='euclidean')
Python
군집 결과를 자세히 보기 위해 판다스의 DataFrame으로 변환하자.
pd.DataFrame(row_clusters, columns=['row label 1', 'row label 2', 'distance', 'no. of items in clust.'], index=['cluster %d' %(i+1) for i in range(row_clusters.shape[0])])
Python
연결 행렬을 계산했으므로 이 결과를 덴드로그램으로 그릴 수 있다.
from scipy.cluster.hierarchy import dendrogram row_dendr = dendrogram(row_clusters, labels=labels) plt.tight_layout() plt.ylabel('Euclidean distance') plt.show()
Python

히트맵에 덴드로그램 연결

실전 애플리케이션에서는 계층 군집 덴드로그램이 히트맵(heat map)과 함께 자주 사용된다. 히트맵을 사용하면 샘플 행렬의 개별 값을 색으로 표현할 수 있다.
덴드로그램을 히트맵에 추가하는 것은 조금 까다롭다.
새로운 figure 객체를 만들고 add_axes 메서드를 사용해서 덴드로그램의 x, y, width, height를 지정한다. 그 다음 덴드로그램을 반시계 방향으로 90도 회전시킨다.
fig = plt.figure(figsize=(8,8), facecolor='white') axd = fig.add_axes([0.09, 0.1, 0.2, 0.6]) # matbplolib 버전이 1.5.1 보다 낮을 때는 'right'를 사용할 것 row_dendr = dendrogram(row_clusters, orientation='left')
Python
그 다음 파이썬 딕셔너리인 덴드로그램 객체의 leaves 키에서 얻은 클러스터 레이블을 따라 원본 DataFrame에 있는 데이터를 재정렬한다.
df_rowclust = df.iloc[row_dendr['leaves'][::-1]]
Python
이제 재정렬된 DataFrame에서 히트맵을 만들고 덴드로그램 다음에 위치시킨다.
axm = fig.add_axes([0.23, 0.1, 0.6, 0.6]) cax = axm.matshow(df_rowclust, interpolation='nearest', cmap='hot_r')
Python
마지막으로 미려하게 만들기 위해 축 눈금을 제거하고 그래프 테두리를 감춘다. 컬러 막대를 추가하고 특성과 샘플 이름을 각각 x축과 y축 눈금의 레이블로 할당한다.
axd.set_xticks([]) axd.set_yticks([]) for i in axd.spines.values(): i.set_visible(False) axm.set_xticklabels([''] + list(df_rowclust.columns)) axm.set_yticklabels([''] + list(df_rowclust.index)) plt.show()
Python

사이킷런에서 병합 군집 적용

앞서 사이파이를 사용하여 병합 계층 군집을 수행하는 방법을 배웠는데, 사이킷런에도 AgglomerativeClustering 클래스가 구현되어 있으며 원하는 클러스터 개수를 지정할 수 있다.
이 클래스를 사용하면 계층 군집의 트리 성장을 일찍 멈추게 할 수 있다.
from sklearn.cluster import AgglomerativeClustering ac = AgglomerativeClustering(n_clusters=2, affinity='euclidean', linkage='complete') labels = ac.fit_predict(X)
Python

DBSCAN을 사용하여 밀집도가 높은 지역 찾기

DBSCAN(Density-Based Spatial Clustering of Applications with Noise) 군집 알고리즘은 k-평균처럼 원형 클러스터를 가정하지 않는다. 또 임계치를 수동으로 지정해야 하는 계층적인 방식으로 데이터셋을 나누지 않는다.
이름이 의미하듯 밀집도 기반 군집 알고리즘은 샘플이 조밀하게 모인 지역에 클러스터 레이블을 할당한다.
DBSCAN에서 밀집도란 특정 반경 ε\varepsilon 안에 있는 샘플 개수로 정의한다.
DBSCAN 알고리즘에는 다음 조건에 따라 샘플에 특별한 레이블이 할당된다.
어떤 샘플의 특정 반경 ε\varepsilon 안에 있는 이웃 샘플이 지정된 개수(MinPts) 이상이면 핵심 샘플(core point)이 된다.
ε\varepsilon 이내에 MinPts 보다 이웃이 적지만 다른 핵심 샘플의 반경 ε\varepsilon 안에 있으면 경계 샘플(border point)이 된다.
핵심 샘플과 경계 샘플이 아닌 다른 모든 샘플은 잡음 샘플(noise point)이 된다.
핵심 샘플, 경계 샘플, 잡음 샘플로 레이블을 할당한 후에는 DBSCAN 알고리즘을 다음 두 단계로 요약할 수 있다.
1.
개별 핵심 샘플이나 (ε\varepsilon 이내에 있는 핵심 샘플을 연결한) 핵심 샘플의 그룹을 클러스터로 만든다.
2.
경계 샘플을 해당 핵심 샘플의 클러스터에 할당한다.
DBSCAN의 핵심 샘플, 경계 샘플, 잡음 샘플은 아래 그림과 같다.
DBSCAN의 대표적인 장점 중 하나는 k-평균처럼 클러스터 모양을 원형으로 가정하지 않는다는 것이다. 또 DBSCAN은 k-평균이나 계층 군집과는 달리 모든 샘플을 클러스터에 할당하지 않고 잡음 샘플을 구분하는 능력이 있다.
반달 모양의 데이터셋을 만들어 k-평균 군집, 계층 군집, DBSCAN을 비교해 보자.
from sklearn.datasets import make_moons X, y = make_moons(n_samples=200, noise=0.05, random_stat=0) plt.scatter(X[:, 0], X[:, 1]) plt.tight_layout() plt.show()
Python
k-평균 알고리즘과 완전 연결 병합 군집 알고리즘이 반달 모양을 별도의 클러스터로 구분할 수 있는지 확인해 보자
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(8,3)) km = KMeans(n_clusters=2, random_state=0) y_km = km.fit_predict(X) ax1.scatter(X[y_km==0, 0], X[y_km==0, 1], c='lightblue', edgecolor='black', marker='o', s=40, label='cluster 1') ax1.scatter(X[y_km==1, 0], X[y_km==1, 1], c='red', edgecolor='black', marker='s', s=40, label='cluster 2') ax1.set_title('K-means clustering') ac = AgglomerativeClustering(n_clusters=2, affinity='euclidean', linkage='complete') y_ac = ac.fit_predict(X) ax2.scatter(X[y_ac==0, 0], X[y_ac==0, 1], c='lightblue', edgecolor='black', marker='o', s=40, label='cluster 1') ax2.scatter(X[y_ac==1, 0], X[y_ac==1, 1], c='red', edgecolor='black', marker='s', s=40, label='cluster 2') ax2.set_title('K-Agglomerative clustering') plt.legend() plt.tight_layout() plt.show()
Python
두 알고리즘 모두 두 클러스터를 구분하는 결과가 좋지 못하다.
이 데이터셋에 DBSCAN 알고리즘을 적용해서 밀집도 기반 방식이 어떤 결과를 보여주는지 살펴보자.
from sklearn.cluster import DBSCAN db = DBSCAN(eps=0.2, min_samples=5, metric='euclidean') y_db = db.fit_predict(X) plt.scatter(X[y_db==0, 0], X[y_db==0, 1], c='lightblue', edgecolor='black', marker='o', s=40, label='cluster 1') plt.scatter(X[y_db==1, 0], X[y_db==1, 1], c='red', edgecolor='black', marker='s', s=40, label='cluster 2') plt.legend() plt.tight_layout() plt.show()
Python
DBSCAN은 성공적으로 반달 모양을 감지했다. 이 예제는 DBSCAN 장점 중 하낭니 임의 형태의 데이터를 처리할 수 있는 능력을 잘 보여준다.
DBSCAN의 단점도 이야기해보면 다음과 같다.
데이터셋에서 훈련 샘플 개수가 고정되어 있다 가정하고, 특성 개수가 늘어나면 차원의 저주로 인한 역효과가 증가한다. 특히 유클리디안 거리 측정을 사용할 때 문제가 된다.
차원의 저주(curse of dimensionality)가 DBSCAN 만의 문제는 아니고 유클리디안 거리 측정을 사용하는 다른 군집 알고리즘에도 영향을 미친다. 예컨대 k-평균과 계층 군집 알고리즘도 해당한다.
DBSCAN이 좋은 군집 결과를 만들려면 두 개의 하이퍼파라미터(MinPts와 ε\varepsilon)를 최적화 해야 한다. 데이터셋에 있는 밀집 영역의 크기가 많이 차이나면 알맞은 MinPts와 ε\varepsilon 조합을 찾는 일이 어렵다.
실전에서는 어떤 군집 알고리즘이 주어진 데이터셋에서 최상일지 확실하지 않다. 특히 시각화가 어렵거나 불가능한 고차원 데이터일 때 그렇다.
성공적인 군집 알고리즘은 알고리즘이나 하이퍼파라미터에만 의존하지 않는다. 오히려 적절한 거리 지표를 선택하고 실험 환경을 구성하는데 도움을 줄 수 있는 도메인(domain) 지식이 더 중요할 수 있다.
차원의 저주를 고려하면 군집을 수행하기 전 차원 축소 기법을 적용하는 것이 일반적이다.