콘텐츠로 이동

Hierarchical Clustering (계층적 클러스터링)

개요

문제 정의

데이터를 계층적 트리 구조(덴드로그램)로 클러스터링:

  • 클러스터 수를 미리 지정할 필요 없음
  • 다양한 수준에서 클러스터 구조 파악 가능
  • 클러스터 간 관계 시각화

핵심 아이디어

두 가지 접근법:

병합적 (Agglomerative): Bottom-up - 각 포인트가 하나의 클러스터로 시작 - 가장 가까운 클러스터 쌍을 반복적으로 병합 - 하나의 클러스터가 될 때까지 진행

분할적 (Divisive): Top-down - 전체 데이터가 하나의 클러스터로 시작 - 가장 이질적인 클러스터를 반복적으로 분할 - 각 포인트가 개별 클러스터가 될 때까지 진행

실제로는 병합적 방법이 주로 사용됨 (계산 효율성).

알고리즘/수식

병합적 클러스터링 (Agglomerative)

Algorithm: Agglomerative Clustering

Input: 데이터 X, 연결 방식 linkage
Output: 덴드로그램 (계층 구조)

1. 초기화: 각 포인트를 개별 클러스터로
   clusters = {{x_1}, {x_2}, ..., {x_n}}

2. 거리 행렬 계산
   D[i,j] = distance(cluster_i, cluster_j)

3. while |clusters| > 1:
     # 가장 가까운 클러스터 쌍 찾기
     (i, j) = argmin D[i,j]

     # 병합
     new_cluster = cluster_i union cluster_j

     # 거리 행렬 업데이트
     update D using linkage method

     # 병합 기록 (덴드로그램용)
     record merge(cluster_i, cluster_j, distance)

4. return dendrogram

연결 방식 (Linkage Methods)

클러스터 간 거리 정의:

방식 수식 특징
Single min{d(a,b) : a in A, b in B} 체인 효과 위험, 긴 클러스터
Complete max{d(a,b) : a in A, b in B} 조밀한 클러스터
Average (UPGMA) mean{d(a,b) : a in A, b in B} 균형 잡힌 방법
Ward argmin 병합 후 분산 증가량 최소 분산, 구형 클러스터
Centroid d(centroid(A), centroid(B)) 역전 가능

Ward's Method 상세

병합으로 인한 분산 증가량 최소화:

Delta(A, B) = (n_A * n_B) / (n_A + n_B) * ||c_A - c_B||^2

여기서: - n_A, n_B: 클러스터 크기 - c_A, c_B: 클러스터 중심

Ward's method는 k-means와 유사한 목적 함수를 계층적으로 최적화.

Lance-Williams 업데이트 공식

새 클러스터 (A union B)와 C 간 거리:

d(A union B, C) = alpha_A * d(A, C) + alpha_B * d(B, C) 
                 + beta * d(A, B) + gamma * |d(A, C) - d(B, C)|
Linkage alpha_A alpha_B beta gamma
Single 0.5 0.5 0 -0.5
Complete 0.5 0.5 0 0.5
Average n_A/(n_A+n_B) n_B/(n_A+n_B) 0 0
Ward (n_A+n_C)/(n_A+n_B+n_C) (n_B+n_C)/(n_A+n_B+n_C) -n_C/(n_A+n_B+n_C) 0

시간 복잡도

알고리즘 시간 공간
기본 O(n^3) O(n^2)
Single linkage (MST) O(n^2) O(n^2)
최적화 (sklearn) O(n^2) ~ O(n^2 log n) O(n^2)

하이퍼파라미터 가이드

파라미터 설명 옵션
linkage 연결 방식 'ward', 'complete', 'average', 'single'
metric 거리 측정 'euclidean', 'manhattan', 'cosine'
n_clusters 최종 클러스터 수 None (덴드로그램만) 또는 정수
distance_threshold 클러스터 병합 임계값 None 또는 float

Linkage 선택 가이드

Ward (기본 권장):
- 구형 클러스터에 적합
- euclidean 거리만 사용
- k-means와 유사한 결과

Complete:
- 조밀한 클러스터 원할 때
- 이상치에 민감

Average:
- 균형 잡힌 방법
- 다양한 거리 측정 가능

Single:
- 긴/불규칙 형태 클러스터
- 체인 효과 주의

Python 코드 예시

기본 사용법

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import make_blobs
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist

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

# 병합적 클러스터링
agg_clustering = AgglomerativeClustering(
    n_clusters=4,
    linkage='ward'
)
labels = agg_clustering.fit_predict(X)

# 결과 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', alpha=0.6)
axes[0].set_title('True Labels')

axes[1].scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.6)
axes[1].set_title('Hierarchical Clustering')

plt.tight_layout()
plt.savefig('hierarchical_result.png', dpi=150)
plt.show()

덴드로그램 시각화

from scipy.cluster.hierarchy import dendrogram, linkage

# linkage 계산
Z = linkage(X, method='ward')

# 덴드로그램
plt.figure(figsize=(14, 7))
dendrogram(
    Z,
    truncate_mode='lastp',  # 마지막 p 병합만 표시
    p=30,
    leaf_rotation=90,
    leaf_font_size=10,
    show_contracted=True
)
plt.xlabel('Sample index')
plt.ylabel('Distance')
plt.title('Hierarchical Clustering Dendrogram (Ward)')
plt.axhline(y=15, color='r', linestyle='--', label='Cut threshold')
plt.legend()
plt.tight_layout()
plt.savefig('dendrogram.png', dpi=150)
plt.show()

덴드로그램에서 클러스터 수 결정

# 거리 임계값으로 클러스터 수 결정
from scipy.cluster.hierarchy import fcluster

# 다양한 임계값 테스트
thresholds = [5, 10, 15, 20, 25]

for t in thresholds:
    labels_t = fcluster(Z, t=t, criterion='distance')
    n_clusters = len(set(labels_t))
    print(f"Threshold {t}: {n_clusters} clusters")

# 최적 임계값으로 클러스터 할당
optimal_threshold = 15
final_labels = fcluster(Z, t=optimal_threshold, criterion='distance')

# 또는 클러스터 수로 직접 지정
labels_k = fcluster(Z, t=4, criterion='maxclust')

연결 방식 비교

from sklearn.cluster import AgglomerativeClustering
from sklearn.metrics import silhouette_score

linkages = ['ward', 'complete', 'average', 'single']

fig, axes = plt.subplots(2, 2, figsize=(12, 12))

for ax, linkage_type in zip(axes.flatten(), linkages):
    if linkage_type == 'ward':
        agg = AgglomerativeClustering(n_clusters=4, linkage=linkage_type)
    else:
        agg = AgglomerativeClustering(n_clusters=4, linkage=linkage_type, 
                                       metric='euclidean')

    labels = agg.fit_predict(X)
    score = silhouette_score(X, labels)

    ax.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.6)
    ax.set_title(f'{linkage_type.capitalize()} (Silhouette: {score:.3f})')

plt.tight_layout()
plt.savefig('linkage_comparison.png', dpi=150)
plt.show()

연결 행렬 활용

# AgglomerativeClustering의 연결 행렬 접근
agg = AgglomerativeClustering(n_clusters=4, linkage='ward')
agg.fit(X)

# children_: 각 병합 단계의 두 자식 인덱스
# n_leaves_: 리프 수
# distances_: 병합 거리 (compute_distances=True 필요)

agg_with_dist = AgglomerativeClustering(
    n_clusters=None,
    distance_threshold=0,  # 전체 트리 계산
    linkage='ward',
    compute_distances=True
)
agg_with_dist.fit(X)

print(f"Number of leaves: {agg_with_dist.n_leaves_}")
print(f"Number of merges: {len(agg_with_dist.children_)}")
print(f"Distance range: {agg_with_dist.distances_.min():.2f} - {agg_with_dist.distances_.max():.2f}")

# scipy 형식으로 변환하여 덴드로그램 생성
def create_linkage_matrix(model):
    counts = np.zeros(model.children_.shape[0])
    n_samples = len(model.labels_) if hasattr(model, 'labels_') else model.n_leaves_

    for i, merge in enumerate(model.children_):
        current_count = 0
        for child_idx in merge:
            if child_idx < n_samples:
                current_count += 1
            else:
                current_count += counts[child_idx - n_samples]
        counts[i] = current_count

    linkage_matrix = np.column_stack([
        model.children_,
        model.distances_,
        counts
    ]).astype(float)

    return linkage_matrix

Z_sklearn = create_linkage_matrix(agg_with_dist)

비구형 클러스터에서의 성능

from sklearn.datasets import make_moons

# 비구형 데이터
X_moons, y_moons = make_moons(n_samples=200, noise=0.05, random_state=42)

# 다양한 연결 방식 비교
fig, axes = plt.subplots(2, 3, figsize=(15, 10))

axes[0, 0].scatter(X_moons[:, 0], X_moons[:, 1], c=y_moons, cmap='viridis', alpha=0.6)
axes[0, 0].set_title('True Labels')

linkages = ['ward', 'complete', 'average', 'single']
for ax, linkage_type in zip(axes.flatten()[1:5], linkages):
    agg = AgglomerativeClustering(n_clusters=2, linkage=linkage_type)
    labels = agg.fit_predict(X_moons)

    ax.scatter(X_moons[:, 0], X_moons[:, 1], c=labels, cmap='viridis', alpha=0.6)
    ax.set_title(f'{linkage_type.capitalize()}')

# DBSCAN과 비교
from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.2, min_samples=5)
labels_dbscan = dbscan.fit_predict(X_moons)
axes[1, 2].scatter(X_moons[:, 0], X_moons[:, 1], c=labels_dbscan, cmap='viridis', alpha=0.6)
axes[1, 2].set_title('DBSCAN')

plt.tight_layout()
plt.savefig('hierarchical_moons.png', dpi=150)
plt.show()

클러스터 안정성 분석

from sklearn.model_selection import train_test_split
from sklearn.metrics import adjusted_rand_score

# 부트스트랩으로 클러스터 안정성 평가
n_bootstraps = 100
stability_scores = []

for i in range(n_bootstraps):
    # 부트스트랩 샘플
    indices = np.random.choice(len(X), size=len(X), replace=True)
    X_boot = X[indices]
    y_boot = y_true[indices]

    # 클러스터링
    agg = AgglomerativeClustering(n_clusters=4, linkage='ward')
    labels_boot = agg.fit_predict(X_boot)

    # ARI 계산
    ari = adjusted_rand_score(y_boot, labels_boot)
    stability_scores.append(ari)

print(f"Clustering Stability (ARI):")
print(f"  Mean: {np.mean(stability_scores):.4f}")
print(f"  Std: {np.std(stability_scores):.4f}")
print(f"  Min-Max: {np.min(stability_scores):.4f} - {np.max(stability_scores):.4f}")

언제 쓰나?

적합한 상황: - 클러스터 수를 모를 때 (덴드로그램 분석) - 계층적 구조 파악이 필요할 때 (분류 체계) - 시각화가 중요할 때 - 중소규모 데이터셋 (n < 10,000) - 생물학적 분류, 문서 분류

부적합한 상황: - 대규모 데이터셋 (O(n^2) 메모리) - 실시간 클러스터링 - 새 데이터 추가 시 재계산 필요 - 노이즈가 많은 데이터 (single linkage 특히)

장단점

장점 단점
클러스터 수 미리 지정 불필요 O(n^2) 시간/공간 복잡도
덴드로그램으로 구조 파악 대규모 데이터에 부적합
다양한 연결 방식 선택 병합 후 취소 불가
재현 가능 (결정적) 체인 효과 (single linkage)
다양한 거리 측정 가능 이상치에 민감할 수 있음

관련 논문/참고

  • Ward, J.H. (1963). "Hierarchical Grouping to Optimize an Objective Function". Journal of the American Statistical Association.
  • Murtagh, F., & Contreras, P. (2012). "Algorithms for hierarchical clustering: an overview". WIREs Data Mining and Knowledge Discovery.
  • scikit-learn documentation: https://scikit-learn.org/stable/modules/clustering.html#hierarchical-clustering