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

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()

  • 실제 군집 애플리케이션에서는 샘플에 대한 진짜 카테고리 정보가 전혀 없다. (그렇지 않으면 지도 학습에 해당한다) 여기서 목표는 특성의 유사도에 기초하여 샘플을 그룹으로 모으는 것이다.
  • 이런 문제에 사용할 수 있는 k-평균 알고리즘은 다음 네 단계로 요약된다.
    1. 샘플 포인트에서 랜덤하게 k개의 센트로이드를 초기 클러스터 중심으로 선택한다.
    2. 각 샘플을 가장 가까운 센트로이드 \mu^{(j)}, j \in \{ l, ..., k \} 에 할당한다.
    3. 할당된 샘플들의 중심으로 센트로이드를 이동한다.
    4. 클러스터 할당이 변하지 않거나 사용자가 지정한 허용 오차나 최대 반복 횟수에 도달할 때까지 2-3 단계를 반복한다.
  • 각 샘플간의 유사도는 거리의 반대 개념으로 정의할 수 있다.
    • 연속적인 특성을 가진 샘플을 클러스터로 묶는데 널리 사용되는 거리는 m-차원 공간에 있는 두 포인트 x, y 사이의 유클리디안 거리의 제곱(squared Euclidean distance)이다

d(x, y)^{2} = \sum_{j=1}^{m} (x_{j} - y_{j})^{2} = \| x- y \|_{2}^{2}

  • 위 식에서 인덱스 j 는 샘플 포인트 x, y j 번째 차원(특성 열)을 나타낸다.
  • 유클리디안 거리 지표를 기반으로 간단한 최적화 문제로 k-평균 알고리즘을 기술할 수 있다. 클러스터 내 제곱 오차합(SSE) 또는 클러스터 관성(cluster inertia)를 반복적으로 최소화하는 방법이다.

SSE = \sum_{i=1}^{n} \sum_{j=1}^{k} w^{(i, j)} \| x^{(i)} - \mu^{(j)} \|_{2}^{2}

  • 여기서 \mu^{(j)} 는 클러스터 j 의 대표 포인트(센트로이드)이다. 샘플 x^{(i)} 가 클러스터 j 안에 있다면 w^{(i, j)} = 1 이고 아니면 w^{(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)
  • 이 코드에서 클러스터 개수를 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()

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

  • k-평균 알고리즘에서 초기 센트로이드가 좋지 않게 선택되면 이따금 나쁜 군집 결과를 만들거나 수렴이 느려진다.
    • 이 문제를 해결하는 한 가지 방법은 같은 데이터셋에서 k-평균 알고리즘을 여러 번 실행하여 SSE 입장에서 가장 성능이 좋은 모델을 선택하는 것이다.
    • 또 다른 방법은 k-평균++ 알고리즘을 통해 초기 센트로이드가 서로 멀리 떨어지도록 위치하는 것이다. 이는 기본 k-평균보다 일관되고 훌륭한 결과를 만든다.
  • k-평균++ 초기화는 다음과 같이 정리할 수 있다.
    • 선택한 k개의 센트로이드를 저장할 빈 집합 M 을 초기화 한다.
    • 입력 샘플에서 첫 번째 센트로이드 \mu^{(i)} 를 랜덤하게 선택하고 M 에 할당한다.
    • M 에 있지 않은 각 샘플 x^{(i)} 에 대해 M 에 있는 센트로이드까지 최소 제곱 거리 d(x^{(i)}, M)^{2} 를 찾는다.
    • 다음 식과 같은 가중치가 적용된 확률 분포를 사용하여 다음 센트로이드 \mu^{(p)} 를 랜덤하게 선택한다.
      • {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의 소속을 이진 희소벡터로 표현할 수 있다.

\left[ \begin{array}{rrr} \mu^{(1)} \to 0 \\ \mu^{(2)} \to 1 \\ \mu^{(3)} \to 0 \end{array} \right]

  • 여기서 값이 1인 인덱스 위치가 이 샘플이 할당된 클러스터 센트로이드를 나타낸다.
  • 이와 다르게 FCM의 클래스 소속 벡터는 다음과 같이 표현할 수 있다.

\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를 지정하고 랜덤하게 각 포인트에 대해 클러스터 확률을 할당한다.
    • 클러스터 센트로이드 \mu^{(j)}, j \in \{ 1, ... , k \} 를 계산한다.
    • 각 샘플에 대해 클러스터 소속 확률을 업데이트 한다.
    • 클러스터 확률이 변하지 않거나 사용자가 지정한 허용 오차나 최대 반복 횟수에 도달할 때까지 2-3을 반복한다.
  • FCM의 목적 함수 J_{m} sms k-평균에서 최소화하는 클러스터 내 제곱 오차합과 매우 비슷하다.

J_{m} = \sum_{i=1}^{n} \sum_{j=1}^{k} w^{m(i, j)} \| x^{(i)} - \mu^{(j)} \|_{2}^{2}

  • 클러스터 소속 가중치 w^{(i, j)} 는 k-평균처럼 이진값이 아니라 실수값이다.
  • w^{(i, j)} 는 추가적인 지수를 포함한다.
    • 퍼지 계수(fuzziness coefficient) 또는 퍼지 지수(fuzzifier)라고 하는 지수 m 은 1보다 크거나 같으며(일반적으로 m=2 ) 퍼지의 정도를 제어한다.
    • m 이 클수록 클러스터 소속 확률 w^{(i, j)} 가 작아져 더 복잡한(fuzzier) 클러스터를 만든다.
  • 클러스터 소속 확률은 다음과 같이 계산한다.

w^{(i, j)} = [ \sum_{p=1}^{k} ({\| x^{(i)} - \mu^{(j)} \|_{2} \over \| x^{(i)} - \mu^{(p)} \|_{2}})^{{2 \over m-1}} ]^{-1}

  • 예컨대 세 개의 클러스터 중심을 선택한다면 샘플 x^{(i)} \mu^{(j)} 클러스터에 속할 확률은 다음과 같이 계산할 수 있다.

w^{(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}

  • 클러스터 중심 \mu^{(j)} 는 샘플의 소속 확률 (w^{m(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()

  • 위 코드의 결과를 보면 k=3에서 엘보우가 나타난다는 것을 볼 수 있다. 그러므로 이 데이터셋에서는 k=3이 좋은 선택이 된다.

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

  • 군집 품질을 평가하는 또 다른 방법은 실루엣 분석(silhouette analysis)이다. 이 방법은 k-평균 이외에 이 장 뒤에서 설명할 다른 군집 알고리즘에도 적용할 수 있다.
  • 실루엣 분석은 클러스터 내 샘플들이 얼마나 조밀하게 모여있는지를 측정하는 그래프 도구이다. 데이터셋 샘플 하나에 대한 실루엣 계수(silhouette coeffcient)를 계산하려면 다음 세 단계를 적용한다.
    • 샘플 x^{(i)} 와 동일한 클러스터 내 모든 다른 포인트 사이의 거리를 평균하여 클러스터 응집력(cluster cohesion) a^{(i)} 를 계산한다.
    • 샘플 x^{(i)} 와 가장 가까운 클러스터의 모든 샘플 간 평균 거리고 최근접 클러스터의 클러스터 분리도(cluster separation) b^{(i)} 를 계산한다.
    • 클러스터 응집력과 분리도 사이의 차이를 둘 중 큰 값으로 나누어 실루엣 s^{(i)} 를 다음과 같이 계산한다.
      • s^{(i)} = {b^{(i)} - a^{(i)} \over max \{ b^{(i)}, a^{(i)} \} }
  • 실루엣 계수는 -1과 1 사이의 값을 갖는다.
    • 앞 공식을 보면 클러스터 응집력과 분리도가 같으면(b^{(i)} = a^{(i)} ) 실루엣 계수가 0이 된다.
    • b^{(i)} >> a^{(i)} 이면 이상적인 실루엣 계수 1에 가깝게 된다.
    • b^{(i)} 는 샘플이 다른 클러스터와 얼마나 다른지 나타내고, a^{(i)} 는 클러스터 내 다른 샘플과 얼마나 비슷한지 나타내기 때문이다.
  • 실루엣 계수는 사이킷런의 metric 모델 아래 silhouette_samples 함수로 계산할 수 있다. 또 편의를 위해 silhouette_score 함수를 임포트 할 수 있다.
    • silhouette_scores 함수는 모든 샘플에 걸쳐 평균 실루엣 계수를 계산한다.
  • 다음 코드는 k=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()

  • 위 그래프에서 보이듯이 실루엣 계수의 값이 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)

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

  • 계층 군집 알고리즘의 입력에 사용할 거리 행렬을 계산하기 위해 사이파이의 spatial.distance 모듈에서 pdist 함수를 사용하겠다.
from scipy.spatial.distance import pdist, squareform

row_dist = pd.DataFrame(squareform(pdist(df, metric='euclidean')), columns=labels, index=labels)
  • 위 코드에서 특성 X, Y, Z를 기반으로 데이터셋 모든 샘플 쌍의 유클리디안 거리를 계산한다.
  • pdist 함수는 축약된 거리 행렬을 반환한다. 이를 squareform 함수에 넣어 샘플간 거리 대칭 행렬을 만든다.
  • 그 다음 사이파이 cluster.hierarchy 모듈의 linkage 함수를 사용해서 완전 연결 병합을 적용해 보자. 이 함수는 연결 행렬을 반환한다.
from scipy.cluster.hierarchy import linkage

row_clusters = linkage(df.values, method='complete', metric='euclidean')
  • 군집 결과를 자세히 보기 위해 판다스의 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])])
  • 연결 행렬을 계산했으므로 이 결과를 덴드로그램으로 그릴 수 있다.
from scipy.cluster.hierarchy import dendrogram

row_dendr = dendrogram(row_clusters, labels=labels)

plt.tight_layout()
plt.ylabel('Euclidean distance')
plt.show()

히트맵에 덴드로그램 연결

  • 실전 애플리케이션에서는 계층 군집 덴드로그램이 히트맵(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')
  • 그 다음 파이썬 딕셔너리인 덴드로그램 객체의 leaves 키에서 얻은 클러스터 레이블을 따라 원본 DataFrame에 있는 데이터를 재정렬한다.
df_rowclust = df.iloc[row_dendr['leaves'][::-1]]
  • 이제 재정렬된 DataFrame에서 히트맵을 만들고 덴드로그램 다음에 위치시킨다.
axm = fig.add_axes([0.23, 0.1, 0.6, 0.6])
cax = axm.matshow(df_rowclust, interpolation='nearest', cmap='hot_r')
  • 마지막으로 미려하게 만들기 위해 축 눈금을 제거하고 그래프 테두리를 감춘다. 컬러 막대를 추가하고 특성과 샘플 이름을 각각 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()

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

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

ac = AgglomerativeClustering(n_clusters=2, affinity='euclidean', linkage='complete')

labels = ac.fit_predict(X)

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()

  • 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()

  • 두 알고리즘 모두 두 클러스터를 구분하는 결과가 좋지 못하다.
  • 이 데이터셋에 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()

  • DBSCAN은 성공적으로 반달 모양을 감지했다. 이 예제는 DBSCAN 장점 중 하낭니 임의 형태의 데이터를 처리할 수 있는 능력을 잘 보여준다.
  • DBSCAN의 단점도 이야기해보면 다음과 같다.
    • 데이터셋에서 훈련 샘플 개수가 고정되어 있다 가정하고, 특성 개수가 늘어나면 차원의 저주로 인한 역효과가 증가한다. 특히 유클리디안 거리 측정을 사용할 때 문제가 된다.
    • 차원의 저주(curse of dimensionality)가 DBSCAN 만의 문제는 아니고 유클리디안 거리 측정을 사용하는 다른 군집 알고리즘에도 영향을 미친다. 예컨대 k-평균과 계층 군집 알고리즘도 해당한다.
    • DBSCAN이 좋은 군집 결과를 만들려면 두 개의 하이퍼파라미터(MinPts와 \varepsilon )를 최적화 해야 한다. 데이터셋에 있는 밀집 영역의 크기가 많이 차이나면 알맞은 MinPts와 \varepsilon 조합을 찾는 일이 어렵다.
  • 실전에서는 어떤 군집 알고리즘이 주어진 데이터셋에서 최상일지 확실하지 않다. 특히 시각화가 어렵거나 불가능한 고차원 데이터일 때 그렇다.
    • 성공적인 군집 알고리즘은 알고리즘이나 하이퍼파라미터에만 의존하지 않는다. 오히려 적절한 거리 지표를 선택하고 실험 환경을 구성하는데 도움을 줄 수 있는 도메인(domain) 지식이 더 중요할 수 있다.
  • 차원의 저주를 고려하면 군집을 수행하기 전 차원 축소 기법을 적용하는 것이 일반적이다.
[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.