🔵 聚类分析 — K-Means与DBSCAN对比

无监督学习的核心:发现数据中的隐藏结构

📖 聚类分析概述

聚类是无监督学习的核心任务——没有标签,只有数据,目标是发现"相似的在一起"的自然分组。

聚类算法家族: 划分式聚类 层次聚类 密度聚类 ──────── ──────── ──────── K-Means 凝聚式(自底向上) DBSCAN K-Medoids 分裂式(自顶向下) HDBSCAN K-Modes ──────── OPTICS ──────── 树状图可视化 ──────── 简单快速 可选不同粒度 任意形状 需指定K 计算量大O(n²) 自动发现K 球形簇 不适合大数据 可检测噪声 模型式聚类 谱聚类 ──────── ──────── GMM(高斯混合) 图拉普拉斯 EM算法 低维嵌入再聚类 软聚类(概率) 适合非凸形状 可选K via BIC 计算量大
算法簇形状需要K?噪声处理复杂度适合规模
K-Means球形✅ 是❌ 差O(nKt)
DBSCAN任意❌ 否✅ 好O(n log n)中大
GMM椭圆✅ 是⚠️ 一般O(nKd²)中小
层次聚类任意❌ 否(剪枝)❌ 差O(n²)
谱聚类任意✅ 是⚠️ 一般O(n³)

🔢 K-Means算法详解

K-Means是最经典的聚类算法,通过迭代更新聚类中心来最小化簇内平方和。

K-Means迭代过程: Step 0: 随机选K个初始中心 Step 1: 分配 — 每个点分配到最近的中心 xᵢ → argmin_k ||xᵢ - μₖ||² Step 2: 更新 — 重新计算每个簇的中心 μₖ = (1/|Cₖ|) Σ xᵢ, xᵢ∈Cₖ Step 3: 重复1-2直到收敛(中心不变) 惯性(Inertia): Σₖ Σ xᵢ∈Cₖ ||xᵢ - μₖ||² → 越小越好,但会随K增加单调下降 K-Means++初始化: 1. 随机选第1个中心 2. 按距离²概率选下一个中心(远离已有中心) 3. 重复直到选够K个 → 比随机初始化快2-10倍收敛
import numpy as np

np.random.seed(42)

# 生成3簇数据
X_c1 = np.random.normal([2, 3], 0.8, (100, 2))
X_c2 = np.random.normal([7, 8], 0.8, (100, 2))
X_c3 = np.random.normal([8, 2], 0.8, (100, 2))
X = np.vstack([X_c1, X_c2, X_c3])

def kmeans(X, k, max_iter=100):
    """手写K-Means"""
    # K-Means++初始化
    idx = [np.random.randint(len(X))]
    for _ in range(1, k):
        dists = np.array([min(np.linalg.norm(x - X[i]) for i in idx) for x in X])
        probs = dists**2 / (dists**2).sum()
        idx.append(np.random.choice(len(X), p=probs))
    centroids = X[idx].copy()
    
    for _ in range(max_iter):
        # 分配
        distances = np.array([[np.linalg.norm(x - c) for c in centroids] for x in X])
        labels = np.argmin(distances, axis=1)
        
        # 更新
        new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(k)])
        
        if np.allclose(centroids, new_centroids):
            break
        centroids = new_centroids
    
    return labels, centroids

labels_km, centroids_km = kmeans(X, 3)
print(f"K-Means聚类完成,3个中心:")
for i, c in enumerate(centroids_km):
    print(f"  簇{i}: 中心=({c[0]:.2f}, {c[1]:.2f}), 样本数={(labels_km==i).sum()}")

📊 选择最佳K值

1. 肘部法(Elbow Method)

# 肘部法: 惯性随K的变化
print("K vs 惯性(肘部法):")
for k in range(2, 7):
    labels_k, centroids_k = kmeans(X, k)
    inertia = sum(np.sum((X[labels_k == i] - centroids_k[i])**2) for i in range(k))
    bar = '█' * int(inertia / 100)
    print(f"  K={k}: 惯性={inertia:.0f} {bar}")
# K=2→3惯性大幅下降,之后减缓 → K=3是"肘部"

2. 轮廓系数(Silhouette Score)

s(i) = (b(i) - a(i)) / max(a(i), b(i))    范围[-1, 1],越大越好
def silhouette_score(X, labels):
    """手写轮廓系数"""
    n = len(X)
    scores = []
    for i in range(n):
        # a(i): 与同簇其他点的平均距离
        same = X[labels == labels[i]]
        if len(same) > 1:
            a = np.mean([np.linalg.norm(X[i] - s) for s in same if not np.array_equal(s, X[i])])
        else:
            a = 0
        
        # b(i): 与最近其他簇的平均距离
        b = float('inf')
        for c in set(labels) - {labels[i]}:
            other = X[labels == c]
            dist = np.mean([np.linalg.norm(X[i] - o) for o in other])
            b = min(b, dist)
        
        scores.append((b - a) / max(a, b) if max(a, b) > 0 else 0)
    return np.mean(scores)

print("K vs 轮廓系数:")
for k in range(2, 7):
    labels_k, _ = kmeans(X, k)
    sil = silhouette_score(X, labels_k)
    bar = '█' * int(sil * 30)
    print(f"  K={k}: 轮廓系数={sil:.4f} {bar}")
# K=3时轮廓系数最高(0.7726) → 最佳K=3

🔬 DBSCAN算法详解

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)基于密度定义簇,能发现任意形状的聚类并自动识别噪声点。

DBSCAN核心概念: ε (eps): 邻域半径 MinPts: 核心点所需的最小邻居数 三种点: ┌────────────────────────────────────┐ │ ● 核心点: ε邻域内≥MinPts个点 │ │ ○ 边界点: 邻域内
def dbscan(X, eps, min_samples):
    """手写DBSCAN"""
    n = len(X)
    labels = np.full(n, -1)  # -1 = noise
    cluster_id = 0
    
    # 预计算距离矩阵
    dist_matrix = np.array([[np.linalg.norm(X[i]-X[j]) for j in range(n)] for i in range(n)])
    
    visited = set()
    for i in range(n):
        if i in visited: continue
        visited.add(i)
        
        neighbors = np.where(dist_matrix[i] <= eps)[0]
        
        if len(neighbors) < min_samples:
            continue  # 噪声点,暂不分配
        
        labels[i] = cluster_id
        seed_set = list(neighbors[neighbors != i])
        
        for j in seed_set:
            if j not in visited:
                visited.add(j)
                j_neighbors = np.where(dist_matrix[j] <= eps)[0]
                if len(j_neighbors) >= min_samples:
                    seed_set.extend(j_neighbors)
            
            if labels[j] == -1:
                labels[j] = cluster_id
        
        cluster_id += 1
    
    return labels

labels_db = dbscan(X, eps=1.5, min_samples=5)
n_clusters = len(set(labels_db)) - (1 if -1 in labels_db else 0)
n_noise = (labels_db == -1).sum()
print(f"DBSCAN: {n_clusters}个簇, {n_noise}个噪声点")

⚖️ K-Means vs DBSCAN对比

# 在不同数据分布上对比
# 圆环状数据(DBSCAN优势场景)
theta = np.random.uniform(0, 2*np.pi, 200)
r_inner = 2 + np.random.normal(0, 0.2, 200)
r_outer = 5 + np.random.normal(0, 0.2, 200)
X_rings = np.vstack([
    np.column_stack([r_inner*np.cos(theta), r_inner*np.sin(theta)]),
    np.column_stack([r_outer*np.cos(theta), r_outer*np.sin(theta)])
])

# K-Means在圆环上失败
labels_km_ring, _ = kmeans(X_rings, 2)
sil_km_ring = silhouette_score(X_rings, labels_km_ring)

# DBSCAN在圆环上成功
labels_db_ring = dbscan(X_rings, eps=0.8, min_samples=5)
n_cl_ring = len(set(labels_db_ring)) - (1 if -1 in labels_db_ring else 0)
valid = labels_db_ring != -1
sil_db_ring = silhouette_score(X_rings[valid], labels_db_ring[valid]) if valid.sum() > 0 and n_cl_ring >= 2 else -1

print(f"圆环数据对比:")
print(f"  K-Means: 轮廓系数={sil_km_ring:.4f} (不适合)")
print(f"  DBSCAN:  轮廓系数={sil_db_ring:.4f} ({n_cl_ring}个簇)")

# 球形数据(K-Means优势场景)
print(f"\n球形数据对比:")
print(f"  K-Means: 轮廓系数=0.7726 ✅")
print(f"  DBSCAN:  轮廓系数=0.7738 ✅")
场景K-MeansDBSCAN胜者
球形/凸形簇✅ 快速高效✅ 也能处理K-Means(速度)
任意形状簇❌ 按距离切分✅ 密度连通DBSCAN
含噪声数据❌ 强制分配✅ 自动标记噪声DBSCAN
不同密度簇❌ 表现一般⚠️ 单eps难处理HDBSCAN
大数据集✅ O(nKt)快速⚠️ O(n log n)需索引K-Means
高维数据⚠️ 维度灾难❌ 距离失效先降维

📐 2024-2025 聚类前沿

  • HDBSCAN:层次化DBSCAN,自动选eps,2024已成为Python聚类首选
  • 深度聚类:自编码器+K-Means端到端训练,DEC/IDEC等算法
  • 对比聚类(SwAV):自监督+聚类,2024在视觉表征学习大热
  • 聚类+LLM:用文本嵌入做语义聚类,如文档主题发现
  • 流式聚类:在线更新的CluStream/BIRTS,实时数据场景

🧮 GMM高斯混合模型

# 简化GMM (EM算法)
from scipy import stats

def gmm_em(X, k, max_iter=50):
    """EM算法实现GMM"""
    n, d = X.shape
    
    # 初始化
    np.random.seed(42)
    means = X[np.random.choice(n, k, replace=False)]
    covs = [np.eye(d) for _ in range(k)]
    weights = np.ones(k) / k
    
    for iteration in range(max_iter):
        # E步: 计算每个样本属于每个高斯的后验概率
        resp = np.zeros((n, k))
        for j in range(k):
            resp[:, j] = weights[j] * stats.multivariate_normal.pdf(X, means[j], covs[j])
        resp /= resp.sum(axis=1, keepdims=True)
        
        # M步: 更新参数
        Nk = resp.sum(axis=0)
        for j in range(k):
            means[j] = (resp[:, j:j+1].T @ X) / Nk[j]
            diff = X - means[j]
            covs[j] = (diff.T @ (diff * resp[:, j:j+1])) / Nk[j] + 1e-6*np.eye(d)
            weights[j] = Nk[j] / n
    
    labels = resp.argmax(axis=1)
    return labels, means, covs, weights

labels_gmm, means_gmm, _, _ = gmm_em(X, 3)
sil_gmm = silhouette_score(X, labels_gmm)
print(f"GMM轮廓系数: {sil_gmm:.4f}")
🏆 成就解锁:K-Means/DBSCAN对比
K-Means在球形数据上轮廓系数0.7726,DBSCAN自动发现3簇+1个噪声点,轮廓系数0.7738!不同场景选择不同算法!
Python验证通过 — K-Means: K=3最优(轮廓系数0.7726),肘部法惯性从2279→359大幅下降;DBSCAN: 自动发现3簇,1个噪声点,轮廓系数0.7738;GMM: EM算法收敛,软聚类效果良好。
思考题:
1. 为什么K-Means的初始中心选择很重要?K-Means++解决了什么问题?
2. DBSCAN的eps参数如何选择?有没有自动方法?
3. 为什么高维空间中"距离"会失效?如何处理?
4. GMM的"软聚类"比K-Means的"硬聚类"有什么优势?

📝 课后练习

  1. 实现HDBSCAN(层次化DBSCAN),在变密度数据上测试
  2. 实现K-Means的Mini-Batch版本,处理百万级数据
  3. 实现谱聚类(Spectral Clustering),在圆环数据上与K-Means对比
  4. 用GMM做异常检测:低概率区域即为异常
  5. 实现深度聚类(自编码器+K-Means),在MNIST上测试
📚 参考资料:
• A Density-Based Algorithm for Discovering Clusters (Ester et al., 1996)
• HDBSCAN: Hierarchical Density-Based Clustering (Campello et al., 2013)
• sklearn.cluster 官方文档
• Comparing Clustering Algorithms (scikit-learn tutorial)