지난 글에서 랭킹 모델을 평가하는 NDCG·MAP·MRR을 배웠다. 그 지표들은 모두 "정답 순서"라는 기준이 있었다. 이번에는 그 기준이 아예 없는 자리, 비지도 학습인 클러스터링의 평가로 넘어간다. 정답이 없으니 "잘 묶었다"를 무엇으로 말할지부터 정해야 한다. 이 글에서는 결과물의 모양만 보고 매기는 세 지표 — 실루엣, Davies-Bouldin, Calinski-Harabász — 를 식과 손계산으로 뜯어보고, 정답이 있을 때 쓰는 ARI·NMI가 무엇을 보정하는지, 그리고 세 지표가 서로 다른 K를 가리킬 때 어떻게 판단하는지를 실제 실행 결과로 따라간다.
응집도와 분리도
두 조건
잘 묶인 군집을 말로 적으면 두 조건이다. 같은 군집에 든 점들은 서로 가까워야 하고, 다른 군집의 점들과는 멀어야 한다. 앞의 것을 응집도(cohesion), 뒤의 것을 분리도(separation)라 부른다. 응집도는 군집 하나 안에서 재는 값이고, 분리도는 군집과 군집 사이에서 재는 값이다. 그래서 응집도는 군집마다 따로 매길 수 있지만 분리도는 늘 상대가 있어야 한다. 둘은 한쪽만으로는 안 된다. 점마다 군집을 하나씩 주면 응집도는 완벽하지만 쓸모없는 결과이고, 전체를 한 군집으로 두면 응집도를 따질 것도 없이 아무것도 나누지 않은 것이다.
내부 평가 지표는 전부 이 둘을 숫자 하나로 합치는 서로 다른 방법이다. 실루엣은 점 하나하나에서 둘을 견주고, DB 지수는 군집 쌍마다 견주고, CH 지수는 전체 분산을 둘로 쪼개 비를 낸다. 합치는 방식이 다르니 같은 결과를 두고도 점수가 다르게 움직이고, 뒤에서 보겠지만 가리키는 K가 갈리기도 한다.
내부 지표와 외부 지표
지표를 가르는 첫 기준은 정답 레이블을 쓰느냐다. 군집화 결과와 데이터만 보고 매기는 것이 내부 지표(internal index), 정답 레이블과 견줘 매기는 것이 외부 지표(external index)다. 실무에서 군집화를 하는 이유가 정답이 없어서이므로 실제로 쓸 수 있는 것은 대개 내부 지표다.
그럼 외부 지표는 어디에 쓰는가. 알고리즘을 고르거나 검증할 때다. 정답이 알려진 벤치마크 데이터에서 여러 군집화 방법을 돌려 외부 지표로 순위를 매기고, 거기서 믿을 만하다고 확인된 방법을 정답 없는 실제 데이터에 쓴다. 또 하나의 쓰임은 두 군집화 결과끼리의 비교다. 한쪽을 정답인 양 두면 "시드를 바꿔도 같은 군집이 나오는가"를 외부 지표로 잴 수 있다.
거리의 어긋남
내부 지표는 모두 거리를 쓴다. 그런데 군집화 알고리즘이 쓴 거리와 지표가 쓰는 거리가 다르면 점수가 결과를 공정하게 재지 못한다. 코사인 유사도로 문서를 묶어 놓고 유클리드 거리로 실루엣을 재면, 길이가 다른 두 문서가 같은 방향이라 한 군집에 들어갔는데 지표는 그 둘을 멀다고 판단한다.
scikit-learn의 silhouette_score는 metric 인자로 거리를 바꿀 수 있으니 군집화에 쓴 것과 맞춘다. 스케일링도 같은 문제다. 표준화한 데이터로 군집화했으면 지표도 표준화한 데이터에서 재야 한다. 원본 단위로 재면 범위가 큰 특성 하나가 지표를 좌우한다.
실루엣 계수
한 점의 손계산
실루엣 계수(silhouette coefficient)는 점 하나에서 두 거리를 잰다. 는 같은 군집의 다른 점들까지의 평균 거리로 응집도를, 는 가장 가까운 다른 군집의 점들까지의 평균 거리로 분리도를 나타낸다. 둘을 이렇게 합친다.
여섯 점으로 직접 계산해 보자. 군집 A는 (0,0)·(0,2)·(2,0), 군집 B는 (6,0)·(8,0)·(6,2)다. A의 점 (2,0)을 보면, 같은 군집의 두 점까지 거리는 2와 이라 이다. B의 세 점까지는 4, 6, 이라 다. 그러면 이다. 같은 방식으로 여섯 점을 모두 계산하면 0.50~0.71 사이가 나오고, (2,0)이 가장 낮다. B 쪽으로 튀어나와 있어 분리도가 가장 약한 점이기 때문이다.
값의 해석
식의 모양에서 범위가 −1~+1로 정해진다. 가 보다 훨씬 작으면 분자가 에 가까워져 +1에 다가간다 — 제 군집에 잘 박혀 있다. 와 가 같으면 0이다. 두 군집의 경계에 서 있어 어느 쪽에 넣어도 비슷하다는 뜻이다. 가 보다 크면 음수가 된다. 제 군집보다 옆 군집이 더 가깝다는 것이니, 잘못 배정된 점이다.
데이터 전체의 실루엣 점수는 모든 점의 평균이다. 경험적으로 0.7을 넘으면 뚜렷한 구조, 0.50.7이면 합리적인 구조, 0.250.5면 약한 구조로 읽는다. 다만 이 구간은 관례일 뿐이고, 차원이 높아질수록 모든 점 사이 거리가 비슷해지는 현상 때문에 같은 구조라도 점수가 낮게 나온다. 실루엣은 같은 데이터에서 K나 알고리즘을 견줄 때 쓰고, 서로 다른 데이터의 점수를 견주는 데는 쓰지 않는다.
군집별 평균
전체 평균 하나만 보면 문제가 숨는다. silhouette_samples로 점마다의 값을 받아 군집별로 평균을 내면 어느 군집이 약한지가 보인다.
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples
from sklearn.datasets import make_blobs
X, y_true = make_blobs(n_samples=300, centers=4,
cluster_std=0.8, random_state=42)
labels = KMeans(n_clusters=3, random_state=42, n_init=10).fit_predict(X)
s = silhouette_samples(X, labels)
for c in range(3):
print(c, (labels == c).sum(), s[labels == c].mean().round(3))
# 0 150 0.662 · 1 75 0.906 · 2 75 0.884
실제로는 네 무리인 데이터를 K=3으로 묶었다. 두 군집은 75개씩에 평균 0.9 안팎인데, 한 군집만 150개에 0.662다. 개수가 두 배이고 점수가 뚝 떨어진 이 군집이 두 무리를 억지로 합친 자리다. K=4로 다시 돌리면 네 군집이 75개씩 0.78~0.90으로 고르게 선다. 군집 하나의 점수만 낮고 크기가 크면 쪼갤 후보, 군집 안에 음수 점이 몰려 있으면 경계를 잘못 그은 후보로 읽는다.
DB 지수
가장 닮은 이웃
Davies-Bouldin 지수(DB 지수)는 군집 단위로 본다. 군집 i의 퍼짐을 — 군집 안 점들이 중심에서 떨어진 평균 거리 — 로 두고, 두 군집 i, j의 닮음을 이렇게 정의한다.
분자는 두 군집의 퍼짐을 더한 것이고 분모는 두 중심 사이 거리다. 둘 다 넓게 퍼져 있는데 중심이 가까우면 이 커진다 — 두 군집이 겹쳐 보인다는 뜻이다. DB 지수는 군집마다 가장 닮은 상대 하나와의 만 골라 평균 낸다.
"가장 닮은 상대 하나"만 보는 것이 이 지표의 성격을 정한다. 다른 군집들과 아무리 멀리 떨어져 있어도 바로 옆에 헷갈리는 짝 하나가 있으면 그 군집의 점수는 나쁘다. 최악의 경우를 기준으로 삼는 보수적인 지표다.
세 군집 계산
한 직선 위에 세 군집을 놓아 보자. 중심은 0, 4, 10이고 퍼짐은 , , 이다. 쌍마다 을 구하면 , , 이다.
이제 군집마다 가장 큰 값을 고른다. 군집 1은 , 군집 2는 , 군집 3은 이다. 평균하면 DB = 0.61이다. 이 값을 끌어올린 것은 넓게 퍼진 군집 3과 그 옆의 군집 2 한 쌍이고, 군집 2와 3 둘 다 그 한 쌍 때문에 0.67을 받았다. 점수를 낮추려면 이 쌍을 더 떼어 놓거나 군집 3을 쪼개야 한다는 것까지 계산에서 바로 읽힌다.
지표의 방향
DB 지수는 낮을수록 좋다. 0이면 모든 군집의 퍼짐이 0이거나 중심이 무한히 멀다는 이상적인 상태다. 실루엣과 CH는 높을수록 좋으므로, 세 지표를 한 표에 놓으면 한 열만 방향이 반대다. 표를 훑다가 "DB가 가장 큰 K"를 고르는 실수가 흔하니, 표 머리에 방향을 적어 두는 편이 안전하다.
계산 비용은 가볍다. 군집마다 중심과 퍼짐을 한 번 구하고, 군집 쌍 개만 견주면 되므로 점 수 n에 대해 선형이다. 대신 중심과 평균 거리로 군집을 요약하기 때문에, 중심이 뜻을 갖지 않는 초승달이나 고리 모양 군집에서는 실제 모양을 반영하지 못한다.
CH 지수
분산비와 자유도
Calinski-Harabász 지수(CH 지수)는 분산 분석의 F 통계량과 같은 발상이다. 전체 데이터의 흩어짐을 둘로 쪼갠다. 군집 중심들이 전체 중심에서 얼마나 떨어져 있는지가 군집 간 분산 B이고, 각 점이 제 군집 중심에서 얼마나 떨어져 있는지가 군집 내 분산 W다. 좋은 군집화는 B가 크고 W가 작다.
분모의 과 는 자유도다. K를 늘리기만 해도 W는 저절로 줄어든다 — 극단적으로 점마다 군집을 주면 W가 0이다. 그래서 B와 W를 그대로 나누면 K가 클수록 유리해진다. 자유도로 나눠 주면 군집을 하나 늘릴 때 B 쪽은 로 나누는 수가 커지는 벌을, W 쪽은 가 줄어드는 벌을 받아, K를 늘린 만큼 실제로 좋아졌는지를 따지게 된다.
CH에는 위쪽 한계가 없다. 앞의 네 무리 데이터에서 K=2일 때 342였던 값이 K=4에서 5,286으로 열다섯 배가 되는데, 이 숫자 자체에는 "좋다"의 기준이 없다. 점이 많고 무리가 멀수록 얼마든지 커지므로 CH는 같은 데이터 안에서 K끼리 견줄 때만 뜻이 있다. 봉우리가 어디에 서는지를 보는 지표이지, 값을 보고 품질을 말하는 지표가 아니다.
계산 비용
CH는 세 지표 중 가장 싸다. 전체 중심 하나, 군집 중심 k개를 구하고 점마다 제 중심까지의 거리 제곱을 한 번씩 더하면 끝이다. 점 수에 대해 선형이라 수십만 건에서도 K를 2부터 30까지 훑는 데 부담이 없다.
반면 실루엣은 점마다 다른 모든 점까지의 거리가 필요해 이다. 10만 건이면 거리 계산이 50억 번에 가깝다. 그래서 큰 데이터에서는 CH로 K 후보를 먼저 좁히고, 실루엣은 silhouette_score(X, labels, sample_size=10000)처럼 표본을 뽑아 확인하는 식으로 나눠 쓴다. 표본으로 잰 실루엣은 뽑을 때마다 조금씩 달라지므로 random_state를 고정하고, K끼리 견줄 때는 같은 표본을 쓰도록 맞춘다. 표본이 달라서 생긴 차이를 K의 차이로 잘못 읽지 않기 위해서다.
볼록 군집 전제
CH와 DB는 둘 다 군집을 "중심과 그 주변의 퍼짐"으로 요약한다. 이 요약이 맞으려면 군집이 중심을 둘러싼 둥근 덩어리, 곧 볼록한 모양이어야 한다. 초승달 두 개가 맞물린 데이터에서 확인해 보면 이 전제가 얼마나 강한지 보인다.
| 방법 | 실루엣 | CH | ARI (정답 대비) |
|---|---|---|---|
| K-Means (K=2) | 0.489 | 586 | 0.274 |
| DBSCAN | 0.330 | 261 | 1.000 |
DBSCAN은 두 초승달을 정확히 가려냈고(ARI 1.0), K-Means는 초승달을 가로로 잘라 엉뚱하게 나눴다. 그런데 두 내부 지표는 모두 K-Means 쪽이 낫다고 말한다. 가로로 자른 두 덩어리가 중심 기준으로는 더 둥글고 더 떨어져 있기 때문이다. 이 지점에서는 실루엣도 예외가 아니다. 모양이 둥글지 않은 군집을 찾는 알고리즘의 결과를 이 지표들로 채점하면 좋은 결과가 나쁜 점수를 받는다.
외부 지표
ARI
정답 레이블이 있으면 두 분할이 얼마나 일치하는지를 잴 수 있다. 출발점은 랜드 지수(Rand Index)다. 모든 점 쌍을 보고, 두 분할이 그 쌍을 "같은 군집"이나 "다른 군집"으로 똑같이 판정한 비율을 센다. 문제는 무작위로 묶어도 이 값이 꽤 높게 나온다는 것이다. 네 부류 300개 점에 군집 번호를 아무렇게나 붙였더니 랜드 지수가 0.626이었다. 대부분의 쌍이 원래 다른 부류라서, 무작위 분할도 "다르다"는 판정을 많이 맞히기 때문이다.
ARI(Adjusted Rand Index)는 이 기준선을 걷어낸다. 무작위 분할에서 기대되는 랜드 지수를 빼고, 가능한 최댓값과의 차이로 나눈다. 그러면 완전 일치가 1, 무작위가 0 근처가 된다. 같은 무작위 분할의 ARI는 −0.001이었다. 음수는 무작위보다도 일치하지 않는다는 뜻이지만, 실무에서는 0 근처와 같게 읽는다.
ARI가 쓸모 있는 또 하나의 자리는 안정성 확인이다. 같은 데이터를 시드만 바꿔 두 번 군집화하고, 한쪽을 정답인 양 두어 ARI를 잰다. 0.9를 넘으면 군집이 데이터에 실제로 있는 구조를 잡은 것이고, 0.5 근처에서 흔들리면 초기값에 따라 경계가 이리저리 옮겨 다니는 것이다. 정답이 없어도 쓸 수 있는 외부 지표 활용법이다.
동질성과 완전성
정보 이론 쪽 지표는 두 가지 질문을 따로 묻는다. 동질성(homogeneity)은 "각 군집에 한 부류만 들어 있는가"다. 부류를 여러 군집으로 잘게 쪼개도 각 군집이 순수하면 동질성은 1이다. 완전성(completeness)은 반대로 "각 부류가 한 군집에 다 모여 있는가"다. 전체를 한 군집으로 묶으면 모든 부류가 한곳에 모였으니 완전성은 1이다.
숫자로 보면 분명하다. 두 부류 50개씩인 데이터에서 한 부류를 25개씩 두 군집으로 쪼개면 동질성은 1.0 그대로이고 완전성이 0.67로 떨어진다. 반대로 100개를 한 군집으로 몰면 완전성은 1.0인데 동질성이 0이다. 둘은 서로 당기는 관계라 한쪽만 높이는 쉬운 방법이 늘 있다. 그래서 둘의 조화평균인 V-measure를 함께 보고, 이것은 정규화 방식에 따라 NMI(Normalized Mutual Information)와 같은 값이 된다. 다만 NMI는 무작위 기준선을 보정하지 않아, 군집 수가 많을수록 무작위 분할에서도 값이 올라간다. 군집 수가 크게 다른 결과끼리 견줄 때는 보정판인 AMI를 쓴다.
from sklearn.metrics import (adjusted_rand_score,
normalized_mutual_info_score,
homogeneity_score, completeness_score)
labels = KMeans(n_clusters=4, random_state=42, n_init=10).fit_predict(X)
print(adjusted_rand_score(y_true, labels), # 1.0
normalized_mutual_info_score(y_true, labels), # 1.0
homogeneity_score(y_true, labels), # 1.0
completeness_score(y_true, labels)) # 1.0
순열 불변
군집 번호에는 뜻이 없다. 정답이 [0,0,1,1,2,2]이고 결과가 [2,2,0,0,1,1]이면 번호만 바뀌었을 뿐 완벽한 일치다. 그래서 외부 지표는 모두 번호를 어떻게 바꿔 붙여도 같은 값을 내도록 만들어져 있다. 이 성질을 순열 불변이라 한다. 위 두 분할의 ARI는 1.0이다.
이 성질 때문에 정확도(accuracy_score)를 군집화 평가에 쓰면 안 된다. 정확도는 번호가 같아야 맞았다고 치므로, 완벽한 군집화도 번호가 어긋나 있으면 0점이 나온다. 굳이 정확도로 보고 싶으면 군집 번호와 부류를 가장 잘 짝짓는 대응을 헝가리안 알고리즘으로 먼저 찾아야 하는데, 그럴 바에는 ARI를 쓰는 편이 간단하다.
K 선택과 검증
세 지표의 봉우리
K를 고르는 기본 절차는 K를 2부터 10쯤까지 바꿔 가며 세 내부 지표를 한 표에 놓는 것이다. 네 무리가 깔끔히 떨어진 앞의 데이터에서는 세 지표가 모두 K=4를 가리킨다 — 실루엣 0.834, CH 5,286, DB 0.232로 각각 최고(DB는 최저)다. 이런 데이터에서는 무엇을 봐도 답이 같다.
어긋나는 경우가 더 배울 것이 많다. 다섯 무리 중 둘이 1.5만큼만 떨어져 거의 붙어 있는 데이터를 만들어 보자.
| K | 실루엣 ↑ | CH ↑ | DB ↓ |
|---|---|---|---|
| 3 | 0.668 | 780 | 0.564 |
| 4 | 0.836 | 5,306 | 0.222 |
| 5 | 0.688 | 5,560 | 0.463 |
| 6 | 0.594 | 4,900 | 0.702 |
실루엣과 DB는 K=4를, CH는 K=5를 가리킨다. 실루엣과 DB는 붙어 있는 두 무리를 한 군집으로 보는 쪽이 더 깔끔하다고 판단했다 — 쪼개면 그 두 군집이 서로 너무 가까워 분리도가 무너진다. CH는 두 무리를 가르면 군집 내 분산이 크게 줄어드는 것을 더 높이 샀다. 둘 다 틀린 게 아니라 다른 질문에 답한 것이다. 이럴 때는 "가까이 붙은 두 무리를 다르게 다룰 이유가 도메인에 있는가"를 물어 정한다. 이유가 있으면 5, 없으면 4다.
DBSCAN의 잡음
DBSCAN은 어느 군집에도 속하지 않는 점에 −1을 붙인다. 그런데 silhouette_score는 −1을 특별히 다루지 않고 다른 번호와 똑같은 군집 하나로 본다. 잡음 점들은 여기저기 흩어져 있으니 이 "잡음 군집"의 응집도는 최악이고, 그 점들이 전체 평균을 끌어내린다.
세 무리에 흩어진 잡음 30개를 섞은 데이터에서 DBSCAN(eps=0.5)을 돌렸더니 63개가 잡음으로 빠졌다. −1을 포함해 잰 실루엣은 0.552, 잡음을 빼고 잰 실루엣은 0.832였다. 같은 결과인데 재는 방식만으로 "약한 구조"와 "뚜렷한 구조"가 갈린다. 그래서 DBSCAN 결과는 잡음을 뺀 점으로 지표를 재고, 잡음 비율을 따로 보고한다. 잡음 비율이 크게 나오면 지표가 아무리 좋아도 eps가 너무 작지 않은지부터 의심한다 — 까다로운 점을 전부 잡음으로 버리면 남은 점의 점수는 얼마든지 좋아진다.
도메인 검증
지표가 좋다고 쓸모 있는 군집은 아니다. 고객 데이터를 군집화했더니 실루엣 0.8로 깔끔한 네 군집이 나왔는데, 열어 보니 가입 연도별로 나뉘어 있었다면 그 군집은 이미 아는 정보를 되풀이한 것이다. 지표는 "이 데이터에 뚜렷한 덩어리가 있다"까지만 말해 주고, 그 덩어리가 무엇인지는 말해 주지 않는다.
그래서 마지막 단계는 사람이 한다. 군집마다 중심 근처의 대표 샘플 몇 개와 경계의 애매한 샘플 몇 개를 뽑아, 도메인을 아는 사람이 각 군집에 이름을 붙일 수 있는지 본다. 이름이 붙으면 쓸 만한 군집이고, 두 군집에 같은 이름이 붙으면 합칠 후보다. 여기에 시드나 표본을 바꿔 다시 돌렸을 때 같은 군집이 나오는지를 ARI로 재는 안정성 확인까지 더하면, 지표 하나에 기대는 것보다 훨씬 튼튼한 결론이 된다.
이것으로 분류·회귀·랭킹·군집화의 평가를 모두 돌아봤다. 다음 글부터는 모델 쪽으로 방향을 돌려, 딥러닝의 출발점인 퍼셉트론이 입력에 가중치를 곱해 더하는 단순한 계산으로 무엇을 할 수 있고 무엇을 못 하는지를 본다.
읽어주셔서 감사합니다. 😊

