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))
관련 기법¶
- Calinski-Harabasz Index - 분산 비율 기반
- Davies-Bouldin Index - 클러스터 유사도 기반
- Elbow Method - WCSS 기반 K 선택
참고 문헌¶
- 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.
- Kaufman, L., & Rousseeuw, P. J. (1990). Finding Groups in Data: An Introduction to Cluster Analysis. Wiley.