본문 바로가기
통계/머신러닝

14. 클러스터링(군집화) 평가지표 Silhouette(실루엣) 지수(계수)에 대해서 알아보자 with Python

by 부자 꽁냥이 2022. 5. 1.

이번 포스팅에서는 클러스터링(군집화)이 잘되었는지 정량적으로 확인할 수 있는 Silhouette(실루엣) 지수(계수)에 대해서 알아보고 파이썬(Python)으로 구현하는 방법에 대해서 알아보려고 한다. 또한 실제 샘플용 데이터를 이용하여 클러스터링(군집화) 결과를 평가해보려고 한다.

 

클러스터링(군집화) 결과를 평가할 수 있는 또 다른 유명한 지표로 Dunn Index가 있는데 이는 이전 포스팅에서 소개했는데 여기에 클러스터링(군집화)에 대한 필요성도 같이 설명하고 있으니 읽고 오면 좋다.

 

이번 포스팅에서 다룰 내용은 다음과 같다.

 

1. Silhouette(실루엣) 지수(계수) 란?

2. Silhouette(실루엣) 지수(계수) 파이썬(Python) 구현



이 곳은 꽁냥이가 머신러닝을 공부한 내용을 정리하는 곳입니다.

이 포스팅에서는 수식을 포함하고 있습니다. 티스토리 피드에서는 수식이 제대로 표시되지 않을 수 있으니 웹브라우저 또는 모바일 웹브라우저로 보시길 바랍니다.



   1. Silhouette(실루엣) 지수(계수) 란?

1) 원리

Silhouette(실루엣) 지수(계수)는 클러스터링(군집화) 결과를 평가하는 지표로써 각 데이터별로 그 데이터가 속한 군 내의 (거리 기반) 유사도와 인접한 군의 유사도를 비교하는 지표이다.

 

[그림 1]은 Silhouette(실루엣) 지수(계수)의 원리를 나타낸 것이다. 먼저 각 데이터(주황색 점)에 대하여 군 내(Intra Cluster : 파란색 원) 유사도를 계산한다. 그리고 인접한 군 간(Inter Cluster : 빨간 원)의 유사도를 계산하여 비교하게 된다. 

 

이때 군 내 유사도는 크고 인접한 군 간 유사도는 작게 나온다면(Silhouette Index 값은 커진다) 그 클러스터링(군집화)는 잘되었다고 판단한다. Dunn Index는 모든 군간의 거리를 고려하는 반면 Silhouette는 인접한 군만을 군 간 유사도 계산에 활용한다.

 

[그림 1] Silhouette Index의 원리

 


2) 정의

Silhouette(실루엣) 지수(계수)는 각 데이터별로 그 데이터가 속한 군 내의 (거리 기반) 유사도와 인접한 군의 유사도를 비교하는 지표이다. 이제 수학적 기호를 사용하여 Silhouette Index를 정의하려고 한다.

 

먼저 주어진 데이터가 클러스터링 알고리즘(예 : K-Means, Gaussian Mixture Model 클러스터링)을 통하여 군집화가 되어 있고 클러스터 개수는 $K$라고 하자. 먼저  $I$ 번째 클러스터 $C_I$에 속하는 $i$번째 데이터에 대하여 다음을 정의한다.

$$a(i) = \frac{1}{|C_I|-1}\sum_{j\in C_I, j\neq i} d(i, j)\tag{1}$$

이때 $C_I$는 클러스터 $C_I$에 속하는 데이터 개수이고 $d(i, j)$는 $C_I$에 속하는 $i, j$번째 데이터 간의 거리이다. $a(i)$는 자신이 속하는 군의 모든 데이터(물론 자기 자신을 제외하고)와의 평균 거리로 이는 군 내 유사도를 측정하는 것이다. 참고로 $i$번째 자기 자신과의 거리는 제외하므로 분모가 $|C_I|-1$이 된 것이다.

 

이번엔 $i \in C_I$에 대하여 $b(i)$를 다음과 같이 정의한다.

$$b(i) = \min_{J\neq I}\frac{1}{|C_J|}\sum_{j\in C_J}d(i, j)$$

 

$b(i)$의 의미는 클러스터 $C_I$와는 다른 클러스터 간의 유사도를 측정하는 것인데 $\min$이 붙어 있어서 결국 인접한 클러스터와의 유사도를 측정하게 되는 것이다.

 

이제 Silhouette(실루엣) 값(Value)을 정의할 수 있다.

$$s(i) = \begin{cases} \frac{b(i)-a(i)}{\max \{ a(i), b(i)\}}, & \text{ if } |C_I| > 1 \\ 0, & \text { if } |C_I|=1\end{cases}$$

$|C_I|$인 경우 $s(i)$는 다음과 같이 바꿔 쓸 수 있다.

$$s(i) = \begin{cases} 1-a(i)/b(i), & \text{ if } a(i) < b(i) \\ 0, & \text{ if } a(i) = b(i) \\ b(i)/a(i) -1, &\text{ if } a(i)>b(i) \end{cases}\tag{2}$$

위 식 (2)를 통해 $-1 \leq s(i) \leq 1$임을 알 수 있고 1에 가까울수록 군 내 유사도가 군 간 유사도보다 큰 상황이기 때문에($a(i) < b(i)$) 데이터의 군집화가 잘되었다는 것을 알 수 있다.

 

이제 개별 Silhouette(실루엣) 값(Value)을 이용하여 Silhouette(실루엣) 지수(계수 : Coefficient) $SC$를 다음과 같이 정의한다.

$$SC = \max_{1\leq J \leq K} \tilde{s}(J) \tag{3}$$

여기서 $\tilde{s}(J)$는 $J$번째 클러스터의 Silhouette(실루엣) 값(Value)들의 평균이다.


3) 장단점

- 장점 -

클러스터의 개수를 판단하는데 활용될 수 있다.

여러 클러스터 개수 후보에 대하여 Silhouette(실루엣) 지수를 계산하고 이를 최대화하는 클러스터 개수를 최적 클러스터 개수로 정할 수 있다.

 

클러스터링의 평가 결과를 시각적으로 나타낼 수 있다. 

클러스터 별로 색상을 정하고 각 데이터에 대한 Sihouette 값(Value)을 x 축으로 하여 막대그래프 형식으로 나타낼 수 있다(아래 그림 참고). 이를 통하여 개별 데이터들이 1에 가까운 값을 갖는지 눈으로 확인할 수 있고 그에 따라 클러스터링 평가 결과를 시각적으로 이해할 수 있다.

 

 

Silhouette Index를 시각적으로 나타낸 그림(출처 위키피디아)

 

- 단점 -

계산량이 많다.

Silhouette(실루엣) 지수(계수)도 개별 데이터 포인트마다 계산해야 하므로 데이터 개수가 많아지면 자연스럽게 계산량이 많아진다.


   2. Silhouette(실루엣) 지수(계수) 파이썬(Python) 구현

먼저 테스트용 데이터를 만들어 준다.

 

import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
from sklearn.cluster import KMeans

np.random.seed(100)
num_data = 50

x11 = np.linspace(0.3,0.7,20)
x12 = np.linspace(1.3,1.8,15)
x13 = np.linspace(2.4,3,15)
x1 = np.concatenate((x11,x12,x13),axis=None)
error = np.random.normal(1,0.5,num_data)
x2 = 1.5*x1+2+error

 

데이터를 만들어 줬으니 예의상 시각화를 한번 해주자.

 

fig = plt.figure(figsize=(7,7))
fig.set_facecolor('white')
plt.scatter(x1, x2, color='k')
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()

 

클러스터링 평가용 데이터

이제 K-Means를 이용하여 클러스터링을 수행해보자.

 

X = np.stack([x1, x2], axis=1)
init = np.array([[2. , 4. ], [1. , 5. ], [2.5, 6. ]])

kmeans = KMeans(n_clusters=3, init=init)
kmeans.fit(X)
labels = kmeans.labels_

 

fig = plt.figure(figsize=(7,7))
fig.set_facecolor('white')
for i, label in enumerate(labels):
    if label == 0:
        color = 'blue'
    elif label ==1:
        color = 'red'
    else:
        color = 'green'
    plt.scatter(X[i,0],X[i,1], color=color)
    
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()

 

K Means 클러스터링 결과

이제 Silhouette(실루엣) 값과 계수를 계산하는 함수를 구현해보자.

1. 직접 구현

먼저 아래와 같이 Silhouette(실루엣) 값과 계수를 계산하는 함수(get_silhouette_results)를 만들었다. 이 함수는 2차원 행렬 형식의 데이터와 클러스터 라벨을 인자로 받으며 각 데이터별 Silhouette(실루엣) 값 배열과 Silhouette(실루엣) 계수를 리턴한다.

 

def get_silhouette_results(X, labels):
    def get_sum_distance(target_x, target_cluster):
        res = np.sum([np.linalg.norm(target_x-x) for x in target_cluster])
        return res
    
    '''
    각 데이터 포인트를 돌면서 a(i), b(i)를 계산
    그리고 s(i)를 계산한다.
    
    마지막으로 Silhouette(실루엣) Coefficient를 계산한다.
    '''
    uniq_labels = np.unique(labels)
    silhouette_val_list = []
    for i in range(len(labels)):
        target_data = X[i]

        ## calculate a(i)
        target_label = labels[i]
        target_cluster_data_idx = np.where(labels==target_label)[0]
        if len(target_cluster_data_idx) == 1:
            silhouette_val_list.append(0)
            continue
        else:
            target_cluster_data = X[target_cluster_data_idx]
            temp1 = get_sum_distance(target_data, target_cluster_data)
            a_i = temp1/(target_cluster_data.shape[0]-1)

        ## calculate b(i)
        b_i_list = []
        label_list = uniq_labels[np.unique(labels) != target_label]
        for ll in label_list:
            other_cluster_data_idx = np.where(labels==ll)[0]
            other_cluster_data = X[other_cluster_data_idx]
            temp2 = get_sum_distance(target_data, other_cluster_data)
            temp_b_i = temp2/other_cluster_data.shape[0]
            b_i_list.append(temp_b_i)

        b_i = min(b_i_list)
        s_i = (b_i-a_i)/max(a_i, b_i)
        silhouette_val_list.append(s_i)

    silhouette_coef_list = []
    for ul in uniq_labels:
        temp3 = np.mean([s for s, l in zip(silhouette_val_list, labels) if l == ul])
        silhouette_coef_list.append(temp3)

    silhouette_coef = max(silhouette_coef_list)
    return (silhouette_coef, np.array(silhouette_val_list))

 

이제 클러스터링 결과를 평가해보자.

 

silhouette_coef, silhouette_val_list = get_silhouette_results(X, labels)
print(silhouette_coef)

 

Silhouette(실루엣) 계수

Silhouette(실루엣) 계수는 0.74로 꽤나 1에 가까웠다. 어느 정도 클러스터링이 잘되었다고 볼 수 있다.

 

이번엔 Silhouette(실루엣) 값을 이용하여 시각화해보자. 시각화를 할 때에는 각 클러스터링 별로 값을 정렬해줘야 한다.

 

import seaborn as sns

## 각 클러스터별로 Silhouette(실루엣) 값을 정렬한다.
uniq_labels = np.unique(labels)
sorted_cluster_svl = []
rearr_labels = []
for ul in uniq_labels:
    labels_idx = np.where(labels==ul)[0]
    target_svl = silhouette_val_list[labels_idx]
    sorted_cluster_svl += sorted(target_svl)
    rearr_labels += [ul]*len(target_svl)

colors = sns.color_palette('hls', len(uniq_labels))
color_labels = [colors[i] for i in rearr_labels]

fig = plt.figure(figsize=(6, 10))
fig.set_facecolor('white')
plt.barh(range(len(sorted_cluster_svl)), sorted_cluster_svl, color=color_labels)
plt.ylabel('Data Index')
plt.xlabel('Silhouette Value')
plt.show()

 

Silhouette(실루엣) 값을 이용한 시각화 결과

각 색상은 클러스터를 의미하며 같은 색상 내에서, 즉 같은 클러스터 내에서 Silhouette(실루엣) 값이 1에 가까운 데이터가 많다면 클러스터링(군집화)이 잘되었다고 볼 수 있다.

2. Scikit-Learn 모듈 이용

Scikit-Learn을 이용하여 Silhouette(실루엣) 계수(Coefficient)를 계산할 수도 있다. 대신 여기서는 식 (3)과 같은 클러스터별 평균의 최대값이 아닌 모든 Silhouette(실루엣) 값들의 평균을 구한다는 차이점이 있다.

from sklearn.metrics import silhouette_score

silhouette_score(X, labels, metric='euclidean')

 

Silhouette(실루엣) 계수

직접 구현한 함수에서도 이 값을 얻을 수 있다.

 

np.mean(silhouette_val_list)

Silhouette(실루엣) 계수


이번 포스팅에서는 클러스터링 결과를 평가하는 지표 중에 하나로 Silhouette(실루엣) 계수에 대하여 알아보았다. 직접 구현도 해보고 그 의미를 더 심도 있게 이해할 수 있었던 좋은 시간이었다.

 

참고자료

Silhouette (clustering) - https://en.wikipedia.org/wiki/Silhouette_(clustering)


댓글


맨 위로