콘텐츠로 이동
Data Prep
상세

DBSCAN

논문 정보

항목 내용
제목 A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise
저자 Martin Ester, Hans-Peter Kriegel, Jörg Sander, Xiaowei Xu
연도 1996
학회 KDD (International Conference on Knowledge Discovery and Data Mining)
링크 AAAI

핵심 아이디어

기존 클러스터링(K-Means, Hierarchical)의 한계: - 클러스터 수 K를 미리 지정해야 함 - 구형 클러스터만 잘 찾음 - 노이즈/이상치를 클러스터에 강제 할당

DBSCAN의 접근: - 밀도(density)가 높은 영역을 클러스터로 정의 - 밀도가 낮은 영역은 노이즈로 분류 - 클러스터 수를 자동으로 결정 - 임의의 형태의 클러스터 발견 가능

알고리즘

핵심 정의

  • ε (eps): 이웃 반경 (neighborhood radius)
  • MinPts: 핵심점이 되기 위한 최소 이웃 수
  • ε-이웃: 점 p로부터 거리 ε 이내의 모든 점 $\(N_\epsilon(p) = \{q \in D : d(p, q) \leq \epsilon\}\)$

점의 분류

유형 조건 설명
Core point \(\|N_\epsilon(p)\| \geq MinPts\) 밀집 영역의 내부
Border point Core point의 ε-이웃이지만, 자신은 Core가 아님 클러스터 경계
Noise point 어떤 Core point의 이웃도 아님 이상치

밀도 연결성 (Density Connectivity)

  • 직접 밀도 도달 (Directly density-reachable): p가 Core이고 q ∈ N_ε(p)
  • 밀도 도달 (Density-reachable): 직접 밀도 도달의 체인
  • 밀도 연결 (Density-connected): 공통 점 o를 통해 밀도 도달 가능

클러스터 = 밀도 연결된 점들의 최대 집합

알고리즘 절차

입력: 데이터 D, ε, MinPts
출력: 클러스터 집합 C, 노이즈 집합 N

1. 모든 점을 UNVISITED로 표시
2. 각 점 p ∈ D에 대해:
   a. p가 VISITED면 건너뜀
   b. p를 VISITED로 표시
   c. N_ε(p) 계산
   d. |N_ε(p)| < MinPts면:
      - p를 NOISE로 표시
   e. 아니면 (p는 Core point):
      - 새 클러스터 C 생성
      - p를 C에 추가
      - Seeds = N_ε(p) - {p}
      - Seeds의 각 점 q에 대해:
        * q가 NOISE면 C에 추가
        * q가 UNVISITED면:
          - VISITED로 표시
          - N_ε(q) 계산
          - |N_ε(q)| ≥ MinPts면:
            Seeds에 N_ε(q) 추가
          - q가 어느 클러스터에도 없으면 C에 추가

시간 복잡도

구현 복잡도
기본 O(n²)
공간 인덱스 (KD-Tree, Ball-Tree) O(n log n)

장단점

장점

장점 설명
K 불필요 클러스터 수 자동 결정
임의 형태 비구형 클러스터 발견
노이즈 처리 이상치 자동 식별
강건성 클러스터 순서/초기화 무관

단점

단점 설명
파라미터 민감 ε, MinPts 선택이 중요
가변 밀도 밀도가 다른 클러스터 어려움
고차원 차원의 저주로 거리 무의미해짐
메모리 거리 계산 시 O(n²) 가능

파라미터 선택

ε 선택: k-distance graph

from sklearn.neighbors import NearestNeighbors
import numpy as np
import matplotlib.pyplot as plt

# k-최근접 이웃 거리 계산
k = 4  # MinPts와 동일하게 설정
nn = NearestNeighbors(n_neighbors=k)
nn.fit(X)
distances, _ = nn.kneighbors(X)

# k번째 이웃까지의 거리를 정렬
k_distances = np.sort(distances[:, k-1])[::-1]

plt.figure(figsize=(10, 5))
plt.plot(k_distances)
plt.xlabel('Points sorted by distance')
plt.ylabel(f'{k}-th nearest neighbor distance')
plt.title('k-Distance Graph (Elbow Method for ε)')
plt.axhline(y=0.5, color='r', linestyle='--', label='Suggested ε')
plt.legend()
plt.show()

: 그래프의 "엘보우" 지점을 ε로 선택

MinPts 선택

  • 경험적 규칙: MinPts ≥ dim + 1 (dim = 차원)
  • 2D 데이터: MinPts = 4가 일반적
  • 노이즈가 많으면: MinPts 증가
  • 작은 클러스터를 찾으려면: MinPts 감소

언제 쓰는가

적합한 경우

  • 클러스터 수를 모를 때
  • 비정형 클러스터 (초승달, 동심원 등)
  • 이상치 탐지가 필요할 때
  • 밀도가 비교적 균일할 때
  • 공간 데이터 (지리, 이미지)

부적합한 경우

  • 밀도가 매우 다른 클러스터들
  • 고차원 데이터 (> 20차원)
  • 클러스터 간 밀도 차이가 클 때 → HDBSCAN 사용

Python 코드

import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons, make_blobs
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

# 초승달 데이터 (K-Means로는 어려움)
X, y = make_moons(n_samples=300, noise=0.05, random_state=42)
X = StandardScaler().fit_transform(X)

# DBSCAN 적용
dbscan = DBSCAN(
    eps=0.3,              # 이웃 반경
    min_samples=5,        # MinPts
    metric='euclidean',   # 거리 척도
    algorithm='auto',     # 'ball_tree', 'kd_tree', 'brute'
    n_jobs=-1             # 병렬 처리
)
labels = dbscan.fit_predict(X)

# 결과 분석
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = list(labels).count(-1)

print(f"클러스터 수: {n_clusters}")
print(f"노이즈 포인트: {n_noise} ({n_noise/len(X)*100:.1f}%)")

# 시각화
plt.figure(figsize=(10, 6))
unique_labels = set(labels)
colors = plt.cm.viridis(np.linspace(0, 1, len(unique_labels)))

for k, col in zip(unique_labels, colors):
    if k == -1:
        col = 'red'  # 노이즈는 빨간색
        marker = 'x'
        label = 'Noise'
    else:
        marker = 'o'
        label = f'Cluster {k}'

    class_member_mask = (labels == k)
    xy = X[class_member_mask]
    plt.scatter(xy[:, 0], xy[:, 1], c=[col], marker=marker, s=50, label=label, alpha=0.6)

plt.title('DBSCAN Clustering (Moon Dataset)')
plt.legend()
plt.show()

K-Means와 비교

from sklearn.cluster import KMeans

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# 원본 데이터
axes[0].scatter(X[:, 0], X[:, 1], c=y, cmap='viridis', s=30)
axes[0].set_title('Ground Truth')

# K-Means
kmeans = KMeans(n_clusters=2, random_state=42)
kmeans_labels = kmeans.fit_predict(X)
axes[1].scatter(X[:, 0], X[:, 1], c=kmeans_labels, cmap='viridis', s=30)
axes[1].set_title('K-Means (Fails!)')

# DBSCAN
axes[2].scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', s=30)
axes[2].scatter(X[labels == -1, 0], X[labels == -1, 1], c='red', marker='x', s=50)
axes[2].set_title('DBSCAN (Success!)')

plt.tight_layout()
plt.show()

파라미터 그리드 탐색

from sklearn.metrics import silhouette_score

eps_values = [0.1, 0.2, 0.3, 0.4, 0.5]
min_samples_values = [3, 5, 7, 10]

results = []
for eps in eps_values:
    for min_samples in min_samples_values:
        db = DBSCAN(eps=eps, min_samples=min_samples)
        labels = db.fit_predict(X)

        n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
        n_noise = list(labels).count(-1)

        # 클러스터가 2개 이상이고 노이즈가 전체가 아닐 때만 실루엣 계산
        if n_clusters >= 2 and n_noise < len(X):
            score = silhouette_score(X[labels != -1], labels[labels != -1])
        else:
            score = -1

        results.append({
            'eps': eps,
            'min_samples': min_samples,
            'n_clusters': n_clusters,
            'n_noise': n_noise,
            'silhouette': score
        })

import pandas as pd
df = pd.DataFrame(results)
print(df.sort_values('silhouette', ascending=False).head(10))

관련 기법

참고 문헌

  1. Ester, M., Kriegel, H. P., Sander, J., & Xu, X. (1996). A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise. KDD-96 Proceedings, 226-231.
  2. Schubert, E., Sander, J., Ester, M., Kriegel, H. P., & Xu, X. (2017). DBSCAN Revisited, Revisited: Why and How You Should (Still) Use DBSCAN. ACM Transactions on Database Systems, 42(3).