콘텐츠로 이동
Data Prep
상세

Silhouette Score (실루엣 계수)

논문 정보

항목 내용
제목 Silhouettes: A Graphical Aid to the Interpretation and Validation of Cluster Analysis
저자 Peter J. Rousseeuw
연도 1987
학회 Journal of Computational and Applied Mathematics
링크 ScienceDirect

핵심 아이디어

클러스터링 평가의 두 가지 관점: - 응집도 (Cohesion): 클러스터 내 점들이 얼마나 가까운가? - 분리도 (Separation): 클러스터 간 점들이 얼마나 먼가?

실루엣 계수는 이 두 가지를 단일 지표로 결합함: - 높은 값: 점이 자기 클러스터에 잘 맞고, 다른 클러스터와 멀리 떨어짐 - 낮은 값: 점이 클러스터 경계에 있거나 잘못 할당됨 - 음수 값: 점이 다른 클러스터에 더 가까움 (잘못된 할당)

수식

단일 점의 실루엣 계수

데이터 포인트 \(i\)에 대해:

\[s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}\]
  • \(a(i)\): 점 \(i\)같은 클러스터 내 다른 점들과의 평균 거리 (응집도) $\(a(i) = \frac{1}{|C_i| - 1} \sum_{j \in C_i, j \neq i} d(i, j)\)$

  • \(b(i)\): 점 \(i\)가장 가까운 다른 클러스터 내 점들과의 평균 거리 (분리도) $\(b(i) = \min_{k \neq i's cluster} \frac{1}{|C_k|} \sum_{j \in C_k} d(i, j)\)$

해석

값 범위 해석
0.71 ~ 1.00 강한 구조 (excellent)
0.51 ~ 0.70 합리적 구조 (good)
0.26 ~ 0.50 약한/인공적 구조 (fair)
≤ 0.25 구조 없음 (poor)

전체 실루엣 점수

모든 점의 실루엣 계수 평균:

\[S = \frac{1}{n} \sum_{i=1}^{n} s(i)\]

시간 복잡도

  • \(O(n^2)\) (모든 점 쌍 거리 계산)

장단점

장점

장점 설명
직관적 응집도와 분리도의 균형
범위 한정 [-1, 1]로 해석 용이
레이블 불필요 내부 평가 지표
시각화 실루엣 플롯으로 개별 점 분석

단점

단점 설명
볼록 클러스터 편향 DBSCAN 결과에 부적합할 수 있음
밀도 가정 밀도가 다른 클러스터 비교 어려움
계산 비용 O(n²)
노이즈 민감 이상치에 영향받음

언제 쓰는가

적합한 경우

  • 최적 K 선택 (K-Means, GMM)
  • 클러스터링 결과 품질 비교
  • 개별 점 분석 (잘못된 할당 탐지)
  • 볼록/구형 클러스터

부적합한 경우

  • 비볼록 클러스터 (DBSCAN 결과)
  • 매우 큰 데이터셋
  • 노이즈가 많은 경우

Python 코드

import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import matplotlib.cm as cm

# 데이터 생성
X, y_true = make_blobs(n_samples=500, centers=4, cluster_std=0.6, random_state=42)

# 다양한 K에 대해 실루엣 점수 계산
K_range = range(2, 10)
silhouette_scores = []

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    labels = kmeans.fit_predict(X)
    score = silhouette_score(X, labels)
    silhouette_scores.append(score)

# 시각화
plt.figure(figsize=(10, 5))
plt.plot(K_range, silhouette_scores, 'bo-')
plt.axvline(x=K_range[np.argmax(silhouette_scores)], color='r', linestyle='--', 
            label=f'Optimal K = {K_range[np.argmax(silhouette_scores)]}')
plt.xlabel('Number of Clusters (K)')
plt.ylabel('Silhouette Score')
plt.title('Silhouette Analysis for Optimal K')
plt.legend()
plt.show()

print(f"Optimal K: {K_range[np.argmax(silhouette_scores)]}")
print(f"Best Silhouette Score: {max(silhouette_scores):.3f}")

실루엣 플롯 (개별 점 분석)

def plot_silhouette(X, labels, n_clusters):
    """실루엣 플롯: 클러스터별 개별 점의 실루엣 계수"""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

    # 실루엣 계수
    silhouette_vals = silhouette_samples(X, labels)
    silhouette_avg = silhouette_score(X, labels)

    y_lower = 10
    for i in range(n_clusters):
        # i번째 클러스터의 실루엣 값
        ith_cluster_silhouette_values = silhouette_vals[labels == i]
        ith_cluster_silhouette_values.sort()

        size_cluster_i = ith_cluster_silhouette_values.shape[0]
        y_upper = y_lower + size_cluster_i

        color = cm.nipy_spectral(float(i) / n_clusters)
        ax1.fill_betweenx(np.arange(y_lower, y_upper),
                          0, ith_cluster_silhouette_values,
                          facecolor=color, edgecolor=color, alpha=0.7)

        # 클러스터 라벨
        ax1.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
        y_lower = y_upper + 10

    ax1.set_title(f'Silhouette Plot (Avg: {silhouette_avg:.3f})')
    ax1.set_xlabel('Silhouette Coefficient')
    ax1.set_ylabel('Cluster')
    ax1.axvline(x=silhouette_avg, color="red", linestyle="--", label='Average')
    ax1.set_xlim([-0.1, 1])
    ax1.legend()

    # 클러스터 시각화
    colors = cm.nipy_spectral(labels.astype(float) / n_clusters)
    ax2.scatter(X[:, 0], X[:, 1], c=colors, s=30, alpha=0.7)
    ax2.set_title('Cluster Visualization')

    plt.tight_layout()
    plt.show()

# K=4로 클러스터링
kmeans = KMeans(n_clusters=4, random_state=42)
labels = kmeans.fit_predict(X)
plot_silhouette(X, labels, 4)

잘못된 K의 실루엣 플롯

# K=2 (너무 적음)
kmeans_2 = KMeans(n_clusters=2, random_state=42)
labels_2 = kmeans_2.fit_predict(X)
plot_silhouette(X, labels_2, 2)

# K=6 (너무 많음)
kmeans_6 = KMeans(n_clusters=6, random_state=42)
labels_6 = kmeans_6.fit_predict(X)
plot_silhouette(X, labels_6, 6)

잘못 할당된 점 탐지

kmeans = KMeans(n_clusters=4, random_state=42)
labels = kmeans.fit_predict(X)

# 개별 실루엣 계수
silhouette_vals = silhouette_samples(X, labels)

# 음수 실루엣 계수 = 잘못 할당된 점
misassigned = silhouette_vals < 0
print(f"잘못 할당 가능성 있는 점: {misassigned.sum()} ({misassigned.mean()*100:.1f}%)")

# 시각화
plt.figure(figsize=(10, 6))
scatter = plt.scatter(X[:, 0], X[:, 1], c=silhouette_vals, cmap='RdYlGn', s=30)
plt.scatter(X[misassigned, 0], X[misassigned, 1], c='red', marker='x', s=100, label='Potentially misassigned')
plt.colorbar(scatter, label='Silhouette Coefficient')
plt.legend()
plt.title('Individual Silhouette Coefficients')
plt.show()

다양한 알고리즘 비교

from sklearn.cluster import DBSCAN, AgglomerativeClustering
from sklearn.mixture import GaussianMixture

algorithms = {
    'K-Means': KMeans(n_clusters=4, random_state=42),
    'GMM': GaussianMixture(n_components=4, random_state=42),
    'Agglomerative': AgglomerativeClustering(n_clusters=4),
}

results = []
for name, algo in algorithms.items():
    if hasattr(algo, 'fit_predict'):
        labels = algo.fit_predict(X)
    else:
        algo.fit(X)
        labels = algo.predict(X)

    score = silhouette_score(X, labels)
    results.append({'Algorithm': name, 'Silhouette': score})

import pandas as pd
df = pd.DataFrame(results)
print(df.sort_values('Silhouette', ascending=False))

관련 기법

참고 문헌

  1. Rousseeuw, P. J. (1987). Silhouettes: A Graphical Aid to the Interpretation and Validation of Cluster Analysis. Journal of Computational and Applied Mathematics, 20, 53-65.
  2. Kaufman, L., & Rousseeuw, P. J. (1990). Finding Groups in Data: An Introduction to Cluster Analysis. Wiley.