삶의 공유

정답 없는 데이터에서 보물을 찾아라! 머신러닝 군집 분석 (Cluster Analysis) 완벽 가이드 🗺️ 본문

Data Scientist/ML

정답 없는 데이터에서 보물을 찾아라! 머신러닝 군집 분석 (Cluster Analysis) 완벽 가이드 🗺️

dkrehd 2025. 12. 9. 22:56
728x90
반응형

오늘은 머신러닝의 비지도 학습(Unsupervised Learning) 분야 중 가장 매력적인 주제인 **군집 분석(Cluster Analysis)**에 대해 다뤄보겠습니다. 정답(레이블)이 없는 데이터 안에서 숨겨진 구조를 찾아내고, 비슷한 데이터끼리 그룹을 묶는 과정은 마치 미지의 땅에서 보물 지도를 그리는 것과 같습니다.

머신러닝 교과서 11장의 핵심 내용을 바탕으로, K-평균부터 계층적 군집, 그리고 DBSCAN까지 코드를 한 줄 한 줄 뜯어보며 완벽하게 이해해 봅시다.


1. K-평균(K-Means) 알고리즘: 가장 대중적인 그룹 찾기

군집 분석은 데이터들 사이의 '유사도'를 기반으로 자연스러운 그룹을 찾는 기법입니다. 마케팅에서 고객을 그룹화하거나, 뉴스 기사를 주제별로 묶을 때 자주 사용되죠. 그중 가장 널리 알려진 것이 K-평균 알고리즘입니다.

💡 프로토타입(Prototype) 기반 군집이란?

K-평균은 프로토타입 기반 군집에 속합니다. 여기서 프로토타입이란 각 클러스터를 대표하는 '중심점'을 의미합니다.

  • 센트로이드(Centroid): 데이터가 연속적인 값일 때(예: 키, 몸무게), 클러스터 내 데이터들의 평균을 중심점으로 삼습니다. (K-Means가 여기 해당)
  • 메도이드(Medoid): 데이터가 범주형이거나 특이값이 많을 때, 클러스터 내에서 가장 대표적인 실제 데이터 포인트를 중심점으로 삼습니다.

먼저 실습에 사용할 가상의 2차원 데이터를 만들어보겠습니다.

Python
 
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt

# 150개의 샘플, 2개의 특성, 3개의 중심점을 가진 데이터 생성
X, y = make_blobs(n_samples=150, 
                  n_features=2,
                  centers=3, 
                  cluster_std=0.50, 
                  shuffle=True,
                  random_state=0)

# 데이터 시각화
plt.scatter(X[:,0], X[:,1], c='white', marker='o', s=50, edgecolors='black')
plt.grid()
plt.show()

🚀 K-평균 알고리즘의 4단계 동작 원리

이 알고리즘은 복잡해 보이지만 사실 아주 직관적인 4단계로 동작합니다.

  1. 초기화: 데이터 중에서 무작위로 $k$개의 점을 골라 초기 센트로이드(중심)로 삼습니다.
  2. 할당: 모든 데이터를 가장 가까운 센트로이드에 할당합니다. 이때 거리는 주로 유클리디안 거리의 제곱을 사용합니다.
  3. 이동: 할당된 데이터들의 평균 위치로 센트로이드를 이동시킵니다.
  4. 반복: 센트로이드가 더 이상 움직이지 않거나, 사용자가 지정한 허용 오차/최대 반복 횟수에 도달할 때까지 2~3번을 반복합니다.

❓ 클러스터 내 제곱 오차(SSE)란?

알고리즘은 SSE(Sum of Squared Errors), 즉 **관성(Inertia)**을 최소화하는 방향으로 학습합니다. 이는 **"각 데이터 포인트가 자신이 속한 클러스터의 중심점과 얼마나 가깝게 뭉쳐있는가"**를 나타냅니다. 거리의 제곱 합이 작을수록 군집이 오밀조밀하게 잘 형성되었다는 뜻입니다.

⚠️ 주의할 점: 빈 클러스터 문제

K-평균은 랜덤하게 시작하다 보니, 운이 나쁘면 어떤 센트로이드에는 데이터가 하나도 할당되지 않는 빈 클러스터가 생길 수 있습니다. 이 경우 알고리즘 동작에 문제가 생길 수 있습니다. (반면 K-메도이드 방식 등은 이 문제에 더 강합니다.)

이제 사이킷런으로 K-평균을 직접 돌려보고 시각화해 보겠습니다.

Python
 
from sklearn.cluster import KMeans

# 클러스터 개수(k)를 3으로 설정하여 모델 생성
km = KMeans(n_clusters=3,
            init='random',
            n_init=10,
            max_iter=300,
            tol=1e-04,
            random_state=0)

# 학습 및 예측 (각 데이터가 몇 번 클러스터인지 레이블 반환)
y_km = km.fit_predict(X)

# 시각화 코드
# ? 설명: X[y_km==0, 0]의 의미
# y_km은 각 데이터의 클러스터 번호(0, 1, 2)를 담고 있습니다.
# y_km==0은 클러스터가 0번인 데이터만 True로 표시하는 '불리언 마스크'입니다.
# X[y_km==0, 0]은 "클러스터 0에 속하는 데이터들 중(행 선택), 첫 번째 특성(열 선택, x축)"을 의미합니다.
plt.scatter(X[y_km==0,0], X[y_km==0,1], c='lightgreen', marker='s', s=50, edgecolors='black', label='Cluster 1')
plt.scatter(X[y_km==1,0], X[y_km==1,1], c='orange', marker='o', s=50, edgecolors='black', label='Cluster 2')
plt.scatter(X[y_km==2,0], X[y_km==2,1], c='lightblue', marker='v', s=50, edgecolors='black', label='Cluster 3')

# 센트로이드(중심점) 표시
plt.scatter(km.cluster_centers_[:,0], km.cluster_centers_[:,1], 
            s=250, marker='*', c='red', edgecolors='black', label='Centroids')

plt.legend(scatterpoints=1)
plt.grid()
plt.tight_layout()
plt.show()


2. 더 똑똑한 시작: K-Means++

기존 K-평균의 가장 큰 단점은 초기 중심점을 랜덤하게 잡는다는 것입니다. 운이 나빠 중심점들이 서로 너무 가까이 붙어서 시작하면 결과가 엉망이 될 수 있습니다.

이를 해결하기 위해 등장한 것이 **K-Means++**입니다.

  • 핵심 원리: 초기 중심점을 선택할 때, 이미 선택된 중심점들로부터 최대한 먼 곳에 있는 데이터를 다음 중심점으로 선택할 확률을 높입니다.
  • 장점: 군집 결과가 훨씬 안정적이고 좋게 나옵니다.
  • 사용법: 사이킷런에서는 init='k-means++' 옵션을 사용하면 됩니다. (사실 이게 기본값이라 평소엔 신경 안 써도 됩니다!)

3. 몇 개의 그룹이 최적일까? (Elbow & Silhouette)

비지도 학습의 난제는 정답 $k$(그룹 개수)를 모른다는 점입니다. 최적의 $k$를 찾는 두 가지 방법을 소개합니다.

1️⃣ 엘보우 방법 (Elbow Method)

클러스터 개수 $k$를 1부터 늘려가며 왜곡(Distortion, SSE) 값이 어떻게 변하는지 그립니다. $k$가 늘어나면 오차(SSE)는 줄어들지만, 어느 순간 감소 폭이 팍 줄어들며 팔꿈치처럼 꺾이는 지점이 나타납니다. 그곳이 효율적인 $k$입니다.

Python
 
# 왜곡(SSE) 값을 저장할 리스트
distortions = []

for i in range(1, 11):
    km = KMeans(n_clusters=i,
                init='k-means++',
                n_init=10,
                max_iter=300,
                tol=1e-04,
                random_state=0)
    km.fit(X)
    # km.inertia_ 속성에 SSE 값이 저장되어 있습니다.
    distortions.append(km.inertia_)

plt.plot(range(1,11), distortions, marker='o')
plt.xlabel('Number of clusters')
plt.ylabel('Distortion')
plt.tight_layout()
plt.show()

결과 그래프를 보면 $k=3$에서 팔꿈치처럼 꺾이는 것을 확인할 수 있습니다.

2️⃣ 실루엣 분석 (Silhouette Analysis)

군집이 얼마나 잘 되었는지 정량적으로 평가하는 방법입니다.

  • 응집력 ($a$): 내 클러스터 친구들과 얼마나 가까운가? (작을수록 좋음)
  • 분리도 ($b$): 옆 클러스터 친구들과 얼마나 먼가? (클수록 좋음)
  • 실루엣 계수 ($s$): $\frac{b - a}{\max(a, b)}$
    • 1에 가까움: 완벽한 군집 (끼리끼리 잘 뭉치고, 남과는 멀다)
    • 0: 애매함 (두 클러스터 경계에 있음)
    • -1: 잘못 분류됨
Python
 
import numpy as np
from matplotlib import cm
from sklearn.metrics import silhouette_samples

# 모델 학습
km = KMeans(n_clusters=3,
            init='k-means++',
            n_init=10,
            max_iter=300,
            tol=1e-04,
            random_state=0)
y_km = km.fit_predict(X)

# 실루엣 값 계산
cluster_labels = np.unique(y_km)
n_clusters = cluster_labels.shape[0]
silhouette_vals = silhouette_samples(X, y_km, metric='euclidean')

y_ax_lower, y_ax_upper = 0, 0
yticks = []

for i, c in enumerate(cluster_labels):
    c_silhouette_vals = silhouette_vals[y_km == c]
    c_silhouette_vals.sort()
    y_ax_upper += len(c_silhouette_vals)
    color = cm.jet(i / n_clusters)
    plt.barh(range(y_ax_lower, y_ax_upper),
             c_silhouette_vals,
             height=1.0,
             edgecolor='none',
             color=color)
    yticks.append((y_ax_lower + y_ax_upper) / 2)
    y_ax_lower += len(c_silhouette_vals)

silhouette_avg = np.mean(silhouette_vals)
plt.axvline(silhouette_avg, color='red', linestyle='--')
plt.yticks(yticks, cluster_labels + 1)
plt.ylabel('Cluster')
plt.xlabel('Silhouette coefficient')
plt.tight_layout()
plt.show()


4. 계층적 군집 분석 (Hierarchical Clustering)

$k$를 미리 정하기 힘들 때, 데이터를 계층적으로 묶어 나가는 방식입니다. 이를 시각화한 **덴드로그램(Dendrogram)**을 통해 데이터의 구조를 한눈에 볼 수 있습니다.

상향식 접근: 병합 계층 군집

처음엔 모든 데이터가 각각 하나의 클러스터였다가, 가장 가까운 것끼리 합치면서 커지는 방식입니다. 이때 '두 클러스터 사이의 거리'를 재는 방식(연결 방식)이 중요합니다.

  • 완전 연결 (Complete Linkage): 두 클러스터에서 가장 먼 데이터끼리의 거리를 사용합니다. (지름이 작은 둥근 군집을 찾는 데 유리)
  • 단일 연결 (Single Linkage): 가장 가까운 데이터끼리의 거리를 사용합니다.
  • 와드 연결 (Ward Linkage): 합쳤을 때 **분산(SSE)**이 가장 적게 증가하는 쪽을 선택합니다.

데이터를 만들고 거리 행렬을 계산해 보겠습니다.

Python
 
import pandas as pd
import numpy as np
from scipy.spatial.distance import pdist, squareform

np.random.seed(123)
variables = ['X', 'Y', 'Z']
labels = ['ID_0', 'ID_1', 'ID_2', 'ID_3', 'ID_4']

# 5개의 샘플, 3개의 특성 생성
X = np.random.random_sample([5,3]) * 10
df = pd.DataFrame(X, columns=variables, index=labels)
print(df)

# 유클리디안 거리 행렬 계산
# pdist: pairwise distance (쌍별 거리) 계산
# squareform: 보기 좋게 정사각형 행렬로 변환
row_dist = pd.DataFrame(squareform(pdist(df, metric='euclidean')),
                        columns=labels,
                        index=labels)

이제 완전 연결 방식으로 연결 행렬을 구하고 덴드로그램을 그려봅시다.

Python
 
from scipy.cluster.hierarchy import linkage, dendrogram
import matplotlib.pyplot as plt

# 연결 행렬 계산 (완전 연결 방식)
# method='complete'가 핵심입니다.
row_clusters = linkage(pdist(df, metric='euclidean'), method='complete')

# 결과를 데이터프레임으로 확인 (필수는 아님)
pd.DataFrame(row_clusters, 
             columns=['row label 1', 'row label 2', 'distance', 'no. of items in clust.'],
             index=['cluster %d' % (i + 1) for i in range(row_clusters.shape[0])])

# 덴드로그램 그리기
# 이 트리를 보면 어떤 데이터끼리 먼저 뭉쳤는지(가까운지) 알 수 있습니다.
row_dendr = dendrogram(row_clusters, labels=labels)
plt.tight_layout()
plt.show()

🌟 고급 시각화: 히트맵에 덴드로그램 붙이기

데이터의 값(히트맵)과 군집 구조(덴드로그램)를 함께 보면 데이터 패턴 파악에 아주 유리합니다. 코드가 조금 복잡하니 단계별로 설명해 드릴게요.

Python
 
# 1. Figure 초기화
fig = plt.figure(figsize=(8, 8), facecolor='white')

# 2. 덴드로그램 위치 설정 및 그리기
# add_axes([left, bottom, width, height]) 좌표로 위치를 잡습니다.
axd = fig.add_axes([0.09, 0.1, 0.2, 0.6]) 
row_dendr = dendrogram(row_clusters, orientation='left') # 왼쪽에 눕혀서 그림

# 미관을 위해 덴드로그램의 축 눈금 제거
axd.set_xticks([])
axd.set_yticks([])
for i in axd.spines.values():
    i.set_visible(False)

# 3. 데이터 재정렬 (매우 중요!)
# 덴드로그램이 군집화한 순서대로 원본 데이터의 행 순서를 바꿔야
# 히트맵에서 비슷한 것끼리 모여 보입니다.
# row_dendr['leaves']에 그 순서가 들어있고, [::-1]로 순서를 맞춥니다.
df_rowclust = df.iloc[row_dendr['leaves'][::-1]]

# 4. 히트맵 그리기
# 덴드로그램 바로 오른쪽에 위치시킵니다.
axm = fig.add_axes([0.23, 0.1, 0.6, 0.6]) 
cax = axm.matshow(df_rowclust, interpolation='nearest', cmap='hot_r', aspect='auto')

# 5. 축 및 레이블 설정
axm.set_xticks(range(len(df_rowclust.columns)))
axm.set_xticklabels(df_rowclust.columns)
axm.set_yticks(range(len(df_rowclust.index)))
axm.set_yticklabels(df_rowclust.index)

# 6. 컬러바 추가
fig.colorbar(cax)

plt.show()

 

사이킷런의 AgglomerativeClustering을 사용하면 클러스터 개수를 지정해서 바로 결과를 얻을 수도 있습니다.

 

Python
 
from sklearn.cluster import AgglomerativeClustering

# 3개의 클러스터로 나누기, 유클리디안 거리, 완전 연결 사용
ac = AgglomerativeClustering(n_clusters=3, metric='euclidean', linkage='complete')
labels = ac.fit_predict(X)
print('클러스터 레이블: %s' % labels)

5. DBSCAN: 모양이 이상해도 괜찮아! 🌙

K-평균이나 계층적 군집은 둥근 형태의 군집을 가정합니다. 만약 데이터가 반달 모양처럼 복잡하게 생겼다면? 이때는 **밀도 기반 군집(DBSCAN)**이 정답입니다.

DBSCAN의 핵심 개념

데이터가 빽빽하게 모여있는 '밀도'를 기준으로 군집을 만듭니다.

  • 핵심 샘플 (Core Point): 내 주변(반경 $\epsilon$)에 친구들이 $MinPts$개 이상 있으면 난 '인싸(핵심)'!
  • 경계 샘플 (Border Point): 난 친구가 적지만, '핵심' 친구 옆에 붙어 있어서 껴줌.
  • 잡음 샘플 (Noise Point): 이도 저도 아닌 외톨이들. (이상치로 취급)

반달 모양 데이터(make_moons)를 사용하여 K-평균과 비교해 봅시다.

Python
 
from sklearn.datasets import make_moons

# 반달 모양 데이터 생성
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 3))

# 1. K-Means 결과
km = KMeans(n_clusters=2, n_init=10, random_state=0)
y_km = km.fit_predict(X)

ax1.scatter(X[y_km == 0, 0], X[y_km == 0, 1],
            edgecolor='black', c='lightblue', marker='o', s=40, label='cluster 1')
ax1.scatter(X[y_km == 1, 0], X[y_km == 1, 1],
            edgecolor='black', c='red', marker='s', s=40, label='cluster 2')
ax1.set_title('K-means clustering')

# 2. 병합 군집 결과
ac = AgglomerativeClustering(n_clusters=2, metric='euclidean', linkage='complete')
y_ac = ac.fit_predict(X)

ax2.scatter(X[y_ac == 0, 0], X[y_ac == 0, 1], c='lightblue',
            edgecolor='black', marker='o', s=40, label='Cluster 1')
ax2.scatter(X[y_ac == 1, 0], X[y_ac == 1, 1], c='red',
            edgecolor='black', marker='s', s=40, label='Cluster 2')
ax2.set_title('Agglomerative clustering')

plt.legend()
plt.tight_layout()
plt.show()

K-평균과 병합 군집은 반달 모양을 제대로 구분하지 못하고 반으로 뚝 잘라버립니다. 😭

 

이제 DBSCAN을 적용해 보겠습니다.

Python
 
from sklearn.cluster import DBSCAN

# eps: 반경, min_samples: 최소 이웃 개수
db = DBSCAN(eps=0.2, min_samples=5, metric='euclidean')
y_db = db.fit_predict(X)

plt.scatter(X[y_db == 0, 0], X[y_db == 0, 1],
            c='lightblue', marker='o', s=40,
            edgecolor='black', label='Cluster 1')
plt.scatter(X[y_db == 1, 0], X[y_db == 1, 1],
            c='red', marker='s', s=40,
            edgecolor='black', label='Cluster 2')
plt.legend()
plt.tight_layout()
plt.show()

와우! DBSCAN은 복잡한 반달 모양을 완벽하게 찾아냈습니다. 🎉

🧐 차원의 저주를 피하는 법: 차원 축소

DBSCAN은 데이터의 특성(차원)이 너무 많으면 거리 계산이 무의미해져 성능이 떨어지는 차원의 저주에 취약합니다. 이때는 군집 분석 전에 차원 축소를 먼저 하는 것이 좋습니다.

  • 주성분 분석 (PCA): 데이터를 가장 잘 표현하는 직선(축)을 찾아 투영합니다. (선형적 데이터에 적합)
  • 커널 PCA (RBF Kernel PCA): 데이터가 롤케이크처럼 비선형적으로 말려있을 때, 이를 펴서 차원을 축소하는 강력한 기법입니다.

📝 요약: 군집 분석 마스터하기

  1. K-평균 (K-Means): 가장 기본적이고 빠름. 원형 군집에 강함. 초기화는 k-means++ 필수!
  2. 최적의 K 찾기: 엘보우 기법실루엣 분석을 활용하세요.
  3. 계층적 군집: 덴드로그램히트맵으로 데이터 구조를 시각화할 때 강력합니다.
  4. DBSCAN: 밀도 기반이라 복잡한 모양의 군집도 잘 찾고, 이상치(Noise) 제거에 탁월합니다.

정답이 없는 데이터 속에서 숨겨진 보물을 찾는 군집 분석, 이제 여러분도 자신 있게 활용해 보세요! 😎

 

이 글은 [머신러닝 교과서 with 파이썬, 사이킷런, 텐서플로]의 내용을 바탕으로 학습하여 작성되었습니다.

반응형