마케팅 데이터를 분석하다가 처음으로 K-means를 썼을 때 당황했습니다. 정답 레이블이 없는데 모델이 알아서 그룹을 나눠버렸기 때문입니다. 지도 학습에 익숙해 있던 상태라 "기준도 없는데 어떻게 분류하지?"라는 의문이 들었습니다. 알고 보니 K-means는 정답을 맞히는 게 아니라, 데이터 안에 숨어 있는 구조를 찾아내는 알고리즘이었습니다. 그걸 알고 나서야 비지도 학습이 왜 필요한지 납득이 됐습니다.
오늘은 K-means가 어떤 수학적 기준으로 데이터를 나누는지, 반복 과정이 왜 수렴하는지, 그리고 K-means의 한계를 확률적으로 해결한 EM 알고리즘까지 실제 숫자로 따라가 보겠습니다.

비지도 학습이란 무엇인가
지도 학습은 정답 레이블이 있는 데이터로 학습합니다. 스팸 메일 분류, 주가 예측, 이미지 인식 모두 정답이 주어집니다. 비지도 학습은 정답 레이블 없이 데이터의 구조 자체를 찾아냅니다. 고객 구매 패턴에서 자연스럽게 형성된 그룹, 유전자 발현 데이터에서 비슷한 유형끼리의 묶음이 대표적입니다.
예) 이메일 → 스팸/정상 (레이블 있음)
비지도 학습: (x) → 모델 학습 → 데이터 구조 발견
예) 고객 데이터 → 비슷한 고객끼리 그룹화 (레이블 없음)
비지도 학습의 주요 종류:
군집화(Clustering): 비슷한 데이터끼리 묶기 ← 오늘 주제
차원 축소: 고차원 데이터를 저차원으로 압축 (PCA, SVD)
밀도 추정: 데이터 분포 자체를 모델링
군집화의 핵심 질문은 "비슷하다"를 어떻게 수학적으로 정의하느냐입니다. K-means는 유클리드 거리로 유사성을 측정합니다. 협업 필터링 글에서 코사인 유사도로 취향의 거리를 쟀던 것과 같은 아이디어입니다. 거리로 유사성을 정의한다는 원리가 여기서도 그대로 쓰입니다.

K-means 알고리즘 — 반복으로 수렴하는 구조
K-means는 단순한 반복 구조입니다. K개의 중심점(centroid)을 임의로 잡고, 각 데이터를 가장 가까운 중심점에 배정하고, 중심점을 배정된 데이터의 평균으로 업데이트합니다. 이 두 단계를 중심점이 더 이상 움직이지 않을 때까지 반복합니다.
초기화: K개의 중심점 μ₁, μ₂, ..., μₖ를 임의로 선택
배정 단계 (Assignment Step):
각 데이터 xᵢ를 가장 가까운 중심점의 클러스터에 배정
cᵢ = argmin_k ||xᵢ - μₖ||²
업데이트 단계 (Update Step):
각 클러스터의 중심점을 소속 데이터들의 평균으로 갱신
μₖ = (1/|Cₖ|) Σ_{xᵢ∈Cₖ} xᵢ
수렴 확인: 중심점 변화가 없으면 종료, 있으면 배정 단계로
→ 배정과 업데이트를 반복할수록 클러스터가 안정화됨
왜 이 반복이 반드시 수렴하는지 궁금했습니다. 처음엔 그냥 멈추지 않으면 어떡하지 싶었는데, 각 단계에서 목적함수(WCSS)가 절대 증가하지 않는다는 게 핵심이었습니다. 배정 단계에서 더 가까운 중심에 재배정하면 WCSS가 줄거나 같고, 업데이트 단계에서 평균으로 중심을 옮기면 WCSS가 줄거나 같습니다. 경사하강법이 손실을 줄이는 방향으로만 움직이는 것과 같은 원리입니다.
실제 숫자로 K-means 계산해보기
6개의 2차원 데이터로 K=2 군집화를 직접 계산해 보겠습니다. 고객의 구매 횟수(x)와 평균 구매금액(y, 만 원)이라고 생각하면 됩니다.
A=(1,1), B=(1,2), C=(2,1), D=(8,8), E=(9,8), F=(8,9)
초기 중심점: μ₁=(1,1), μ₂=(8,8)
──────────────────────────────────────────
1회차 배정 단계:
A=(1,1): ||A-μ₁||²=0, ||A-μ₂||²=98 → 클러스터 1
B=(1,2): ||B-μ₁||²=1, ||B-μ₂||²=85 → 클러스터 1
C=(2,1): ||C-μ₁||²=1, ||C-μ₂||²=85 → 클러스터 1
D=(8,8): ||D-μ₁||²=98, ||D-μ₂||²=0 → 클러스터 2
E=(9,8): ||E-μ₁||²=113, ||E-μ₂||²=1 → 클러스터 2
F=(8,9): ||F-μ₁||²=113, ||F-μ₂||²=1 → 클러스터 2
1회차 업데이트 단계:
μ₁ = ((1+1+2)/3, (1+2+1)/3) = (1.33, 1.33)
μ₂ = ((8+9+8)/3, (8+8+9)/3) = (8.33, 8.33)
──────────────────────────────────────────
2회차 배정 단계:
A=(1,1): ||A-μ₁||²=0.22, ||A-μ₂||²=108.6 → 클러스터 1
B=(1,2): ||B-μ₁||²=0.55, ||B-μ₂||²=95.4 → 클러스터 1
C=(2,1): ||C-μ₁||²=0.55, ||C-μ₂||²=95.4 → 클러스터 1
D=(8,8): ||D-μ₁||²=90.9, ||D-μ₂||²=0.22 → 클러스터 2
E=(9,8): ||E-μ₁||²=106.2, ||E-μ₂||²=0.55 → 클러스터 2
F=(8,9): ||F-μ₁||²=106.2, ||F-μ₂||²=0.55 → 클러스터 2
배정 결과 변화 없음 → 수렴 종료
최종 결과:
클러스터 1: A, B, C (저빈도·저금액 고객)
클러스터 2: D, E, F (고빈도·고금액 고객)
단 2회 반복으로 수렴했습니다. 데이터가 워낙 명확하게 두 덩어리로 나뉘어 있었기 때문입니다. 현실 데이터는 경계가 모호해서 수렴까지 수십 번 반복하는 경우도 많습니다. 직접 고객 데이터에 돌려보면 레이블 없이도 "이 그룹은 VIP 고객 같다"는 패턴이 나오는 게 신기했습니다.

K-means의 수학적 목표 — WCSS 최소화
K-means가 최소화하려는 목적함수는 WCSS(Within-Cluster Sum of Squares), 즉 클러스터 내 분산의 합입니다. 각 데이터와 소속 클러스터 중심 사이의 거리 제곱을 전부 더한 값입니다.
앞선 예시로 최종 WCSS 계산:
클러스터 1 (μ₁ = (1.33, 1.33)):
A: (1-1.33)²+(1-1.33)² = 0.11+0.11 = 0.22
B: (1-1.33)²+(2-1.33)² = 0.11+0.45 = 0.56
C: (2-1.33)²+(1-1.33)² = 0.45+0.11 = 0.56
소계 = 1.34
클러스터 2 (μ₂ = (8.33, 8.33)):
D: (8-8.33)²+(8-8.33)² = 0.11+0.11 = 0.22
E: (9-8.33)²+(8-8.33)² = 0.45+0.11 = 0.56
F: (8-8.33)²+(9-8.33)² = 0.11+0.45 = 0.56
소계 = 1.34
WCSS = 1.34 + 1.34 = 2.68
WCSS를 0으로 만들려면 K를 데이터 수만큼 늘리면 됩니다. 데이터 하나당 클러스터 하나면 거리가 항상 0이 됩니다. 하지만 그건 아무 의미가 없습니다. 과적합 글에서 다룬 것과 똑같은 함정입니다. 훈련 데이터를 완벽하게 외워버리면 새 데이터에서 쓸모가 없어지는 것처럼, K가 너무 크면 군집화 자체가 의미를 잃습니다.
K-means의 한계 — 언제 실패하는가
K-means는 빠르고 직관적이지만 구조적 한계가 있습니다.
첫째는 초기 중심점 의존성입니다. 초기 중심점을 어디에 놓느냐에 따라 결과가 달라집니다. 운 나쁘게 두 중심이 같은 클러스터 안에 놓이면 잘못된 결과로 수렴합니다. 이를 해결하기 위해 K-means++가 등장했습니다. 첫 중심은 임의로, 이후 중심들은 기존 중심과 최대한 멀리 떨어진 곳에서 확률적으로 선택합니다. scikit-learn의 기본값이 init='k-means++'인 이유입니다.
둘째는 구형 클러스터 가정입니다. K-means는 유클리드 거리를 쓰기 때문에 클러스터가 원형일 때 잘 작동합니다. 달 모양, 나선형처럼 비구형 클러스터는 제대로 나누지 못합니다. 실제로 달 모양 데이터에 K-means를 돌려보면 완전히 엉뚱하게 나뉘는 걸 볼 수 있습니다. 이 경우 DBSCAN 같은 밀도 기반 알고리즘이 필요합니다.
셋째는 클러스터 크기와 밀도 차이입니다. 한 클러스터가 다른 클러스터보다 훨씬 크거나 밀도가 낮으면 경계가 잘못 나뉩니다. K-means는 모든 클러스터의 분산이 비슷하다고 가정합니다. 이 가정이 깨지면 아래에서 설명할 GMM과 EM 알고리즘이 더 적합합니다.
| 문제 상황 | 실패 원인 | 대안 |
|---|---|---|
| 나쁜 초기값 | 지역 최솟값 수렴 | K-means++ |
| 비구형 클러스터 | 유클리드 거리 한계 | DBSCAN, 스펙트럴 |
| 크기·밀도 불균형 | 등분산 가정 위반 | GMM + EM 알고리즘 |
| K를 모를 때 | WCSS만으로 판단 불가 | 엘보우 기법, 실루엣 |
EM 알고리즘 — 확률로 군집을 나누는 방법
K-means는 각 데이터를 하나의 클러스터에 딱 잘라 배정합니다. 경계에 있는 데이터도 무조건 한쪽으로 넣습니다. 처음엔 이게 당연하다고 생각했는데, 현실에서는 "이 고객은 70%는 그룹 A, 30%는 그룹 B 같다"는 식의 소속 확률이 더 자연스러운 경우가 많습니다. 이걸 구현한 게 GMM(Gaussian Mixture Model)과 EM(Expectation-Maximization) 알고리즘입니다.
데이터가 K개의 정규분포가 섞인 분포에서 생성됐다고 가정
각 클러스터 k는 평균 μₖ, 공분산 Σₖ, 혼합 비율 πₖ로 정의
p(x) = Σₖ πₖ · N(x | μₖ, Σₖ)
EM 알고리즘 구조:
E 단계 (Expectation): 소속 확률 계산
γᵢₖ = πₖ·N(xᵢ|μₖ,Σₖ) / Σⱼ πⱼ·N(xᵢ|μⱼ,Σⱼ)
→ 데이터 xᵢ가 클러스터 k에 속할 확률 (0~1)
M 단계 (Maximization): 파라미터 업데이트
Nₖ = Σᵢ γᵢₖ (클러스터 k의 유효 데이터 수)
μₖ = (1/Nₖ) Σᵢ γᵢₖ·xᵢ (가중 평균)
πₖ = Nₖ / n
K-means와의 비교:
K-means: γᵢₖ ∈ {0, 1} → 딱 잘라 배정
GMM/EM: γᵢₖ ∈ [0, 1] → 확률적 소속
→ K-means는 EM의 특수한 경우 (하드 배정 버전)
처음엔 E단계, M단계가 뭔지 감이 안 잡혔습니다. 결국 K-means랑 구조가 같은데 '딱 잘라 배정' 대신 '확률로 배정'하는 차이였습니다. M단계에서 파라미터를 업데이트하는 방식이 이 연재에서 다룬 최대우도추정(MLE)과 정확히 같습니다. 정규분포 개념도 각 클러스터를 가우시안으로 모델링하는 데 그대로 적용됩니다.
K 값은 어떻게 정하는가 — 엘보우 기법
K-means에서 가장 현실적인 문제는 K를 어떻게 정하냐는 겁니다. 비지도 학습이라 외부 기준이 없습니다. 처음에 이게 제일 막막했습니다. 가장 많이 쓰이는 방법이 엘보우 기법(Elbow Method)입니다.
K를 1부터 늘려가며 WCSS를 계산하고 그래프로 그림
K=1: WCSS 매우 큼 (모든 데이터가 한 덩어리)
K=2: WCSS 급감
K=3: WCSS 감소 (완만해짐)
K=4: WCSS 소폭 감소
K=5: WCSS 거의 변화 없음
→ 감소폭이 꺾이는 "팔꿈치" 지점이 최적 K
보완 지표 — 실루엣 점수(Silhouette Score):
s(i) = (b(i) - a(i)) / max(a(i), b(i))
a(i): 같은 클러스터 내 평균 거리 (낮을수록 좋음)
b(i): 가장 가까운 다른 클러스터까지 평균 거리 (높을수록 좋음)
s(i) 범위: -1 ~ 1 (1에 가까울수록 좋은 군집화)
→ 엘보우가 불명확할 때 실루엣 점수로 보완
엘보우 기법이 항상 명확한 답을 주지는 않습니다. 데이터에 뚜렷한 군집이 없으면 그래프가 완만하게 내려가기만 해서 꺾이는 지점이 안 보입니다. PCA 글에서 누적 분산으로 주성분 수를 정했던 것과 구조가 같습니다. 얼마나 줄여도 핵심이 남아있는 가를 찾는 문제라는 점에서 같은 고민입니다.
scikit-learn으로 K-means 구현하기
이론을 이해했으면 직접 돌려보는 게 빠릅니다. scikit-learn의 KMeans는 K-means++를 기본값으로 씁니다.
from sklearn.metrics import silhouette_score
import numpy as np
X = np.array([[1,1],[1,2],[2,1],[8,8],[9,8],[8,9]])
# K-means 학습 (K-means++ 초기화)
kmeans = KMeans(n_clusters=2, init='k-means++', random_state=42)
kmeans.fit(X)
print(f"클러스터 레이블: {kmeans.labels_}")
# 출력: [0 0 0 1 1 1]
print(f"중심점: {kmeans.cluster_centers_}")
# 출력: [[1.33 1.33] [8.33 8.33]]
print(f"WCSS: {kmeans.inertia_:.2f}")
# 출력: 2.67
# 실루엣 점수
score = silhouette_score(X, kmeans.labels_)
print(f"실루엣 점수: {score:.3f}") # 0.9 이상이면 우수
# 엘보우 기법으로 최적 K 탐색
wcss_list = []
for k in range(1, 6):
km = KMeans(n_clusters=k, random_state=42)
km.fit(X)
wcss_list.append(km.inertia_)
# wcss_list를 그래프로 그려서 꺾이는 지점 확인
레이블 없는 데이터에 K-means를 돌렸을 때 의미 있는 그룹이 나오는 걸 처음 봤을 때 신기했습니다. 수학적으로는 거리 계산과 평균을 반복하는 단순한 구조인데, 고객 데이터에서는 "이 그룹은 VIP 고객", "이 그룹은 이탈 위험 고객" 같은 패턴이 보입니다. 거리 계산, 정규분포, MLE를 따로따로 공부했는데 K-means 하나 안에 다 들어가 있었습니다.
'데이터 과학 수학' 카테고리의 다른 글
| 머신러닝의 시작, 선형 회귀(Linear Regression)와 최소제곱법·정규방정식의 수학 (0) | 2026.05.04 |
|---|---|
| 스팸 필터는 어떻게 작동하는가, 로지스틱 회귀(Logistic Regression)의 수학과 확률적 분류 (0) | 2026.05.03 |
| 주가와 날씨는 어떻게 예측하는가, 시계열 데이터와 LSTM의 수학적 원리 (0) | 2026.05.02 |
| 딥러닝 학습을 안정시키는 두 가지 기법, 배치 정규화(Batch Normalization)와 드롭아웃의 수학 (0) | 2026.05.01 |
| torch.optim.Adam 한 줄 뒤에 숨겨진 수학, 모멘텀·RMSProp·Adam 최적화 알고리즘의 원리 (0) | 2026.04.30 |