聚类算法性能对比:K-means vs DBSCAN vs 层次聚类

聚类算法性能对比:K-means vs DBSCAN vs 层次聚类

关键词:聚类算法、K-means、DBSCAN、层次聚类、性能对比、机器学习、无监督学习

摘要:聚类是无监督学习的核心任务之一,广泛应用于用户分群、图像分割、异常检测等场景。本文将用“分水果”“找朋友”“建家谱”等生活化比喻,从原理、优缺点到实战场景,一步一步对比K-means、DBSCAN、层次聚类三种主流算法。无论你是刚入门的机器学习爱好者,还是需要为项目选择聚类方案的开发者,读完本文都能清晰掌握三种算法的差异与适用场景。


背景介绍

目的和范围

聚类的本质是“物以类聚”:从无标签数据中发现隐含的分组规律。本文聚焦三种最常用的聚类算法——K-means(最经典)、DBSCAN(抗噪声)、层次聚类(可视化强),通过原理拆解、代码实战、场景对比,帮你解决“选哪个”的核心问题。

预期读者

  • 机器学习初学者:想理解聚类算法的底层逻辑
  • 项目开发者:需要为实际数据选择合适的聚类方案
  • 数据分析师:希望优化用户分群、异常检测等任务效果

文档结构概述

本文将按“原理→对比→实战→选型”的逻辑展开:先通过生活化故事讲清每个算法的核心思想,再用数学公式和代码揭示细节,最后通过真实数据集实战对比效果,最终总结选型指南。

术语表

  • 无监督学习:从无标签数据中发现规律(类似“没有老师教,自己找规律”)
  • 簇(Cluster):聚类结果中的“组”(类似“分好的水果篮”)
  • 质心(Centroid):K-means中簇的“中心代表”(类似班级的“平均身高”)
  • 密度(Density):DBSCAN中数据点的“拥挤程度”(类似地铁早高峰的人群密度)
  • 树状图(Dendrogram):层次聚类的“家谱图”(类似家族的代际关系树)

核心概念与联系:用“分水果”“找朋友”“建家谱”理解聚类

故事引入:水果摊老板的分货难题

假设你是水果摊老板,需要把一车混装的苹果、橘子、香蕉分开。不同的分法对应不同的聚类算法:

  • K-means:先猜三个筐的位置(质心),把每个水果放进最近的筐,再根据筐里的水果调整筐的位置,重复直到筐不动(收敛)。
  • DBSCAN:找“拥挤”的水果堆——如果一个苹果周围50cm内有至少3个水果(核心点),就把附近所有能连到它的水果(密度可达)归为一堆,剩下的单独放(噪声)。
  • 层次聚类:先把每个水果当独立堆,然后不断合并最近的两堆(或最远的、平均距离),直到只剩一堆,最后切一刀分成想要的簇数(类似画家谱,从单个成员合并成家族)。

核心概念解释(像给小学生讲故事)

核心概念一:K-means(质心驱动的“分筐游戏”)

K-means的核心是“选代表→分组→调整代表”。
比如分班级:老师先猜三个“平均身高代表”(质心),让每个同学站到离自己身高最近的代表旁边;然后重新计算这三组的平均身高(新质心),再让同学重新站队;重复直到代表位置不再变化,就分好了组。
关键规则:需要提前定簇数(K),每个点属于最近的质心,簇是圆形/球形(因为用欧氏距离)。

核心概念二:DBSCAN(密度驱动的“找朋友圈”)

DBSCAN的核心是“找密集区域,忽略稀疏点”。
比如在操场找朋友:设定“好朋友”条件——你周围2米内至少有3个同学(核心点),那么你和这3个同学是朋友;这3个同学周围2米内的其他同学(密度可达)也是朋友,最终形成一个“朋友圈”(簇)。剩下的落单同学(周围2米内不足3人)是“独行侠”(噪声点)。
关键规则:不需要提前定簇数,能发现任意形状的簇(比如月牙形、环形),但怕密度不均匀的数据。

核心概念三:层次聚类(结构驱动的“建家谱”)

层次聚类的核心是“从分到合,建家族树”。
比如家族聚会:一开始每个客人是独立的“小家庭”(簇);然后找到关系最近的两家人(距离最小),合并成一个“大家庭”;重复合并直到所有人在一个家族里,形成一棵“家谱树”(树状图)。最后在树的某个位置切一刀,得到想要的簇数。
关键规则:不需要提前定簇数(可以后期切分),但计算量大(适合小数据),能展示簇间层次关系。

核心概念之间的关系(用“分水果”比喻)

三种算法像三种不同的分水果策略,区别在于“分组依据”和“适应场景”:

  • K-means vs DBSCAN:一个按“到中心的距离”分组(分筐),一个按“周围拥挤程度”分组(找朋友圈)。前者适合“均匀分布的圆堆水果”,后者适合“奇形怪状的水果堆”。
  • K-means vs 层次聚类:一个是“动态调整筐位置”的快速分法,一个是“先分后合建家谱”的慢分法。前者适合大数据,后者适合需要“看簇间关系”的小数据。
  • DBSCAN vs 层次聚类:一个“忽略落单水果”(抗噪声),一个“必须把所有水果放进家族”(无噪声处理)。前者适合数据有噪声的场景,后者适合需要完整结构的场景。

核心原理的文本示意图

  • K-means:初始化K个质心→分配点到最近质心→更新质心→重复直到收敛。
  • DBSCAN:遍历每个点→判断是否是核心点(半径eps内有≥min_samples点)→用BFS找所有密度可达点→剩余点为噪声。
  • 层次聚类:计算所有点间距离→合并最近的两个簇→更新簇间距离→重复直到只剩一个簇→画树状图切分。

Mermaid 流程图

graph TD
    A[K-means流程] --> B[初始化K个质心]
    B --> C[每个点分配到最近质心的簇]
    C --> D[重新计算每个簇的质心]
    D --> E{质心是否变化?}
    E -->|是| C
    E -->|否| F[输出簇]

    G[DBSCAN流程] --> H[设定eps和min_samples]
    H --> I[遍历每个点,判断是否是核心点]
    I --> J[用BFS找所有密度可达的核心点和边界点]
    J --> K[剩余点标记为噪声]
    K --> L[输出簇和噪声]

    M[层次聚类流程] --> N[计算所有点间距离矩阵]
    N --> O[合并距离最小的两个簇]
    O --> P[更新距离矩阵(单/全/平均链接)]
    P --> Q{是否剩1个簇?}
    Q -->|否| O
    Q -->|是| R[画树状图,切分得到簇]

核心算法原理 & 具体操作步骤

1. K-means:用“质心迭代”找簇

数学模型

目标是最小化所有点到其所属簇质心的平方距离和(误差平方和,SSE):
S S E = ∑ i = 1 n ∑ j = 1 K I i j ⋅ ∣ ∣ x i − μ j ∣ ∣ 2 SSE = \sum_{i=1}^{n} \sum_{j=1}^{K} I_{ij} \cdot ||x_i - \mu_j||^2 SSE=i=1nj=1KIij∣∣xiμj2
其中:

  • ( I_{ij} ) 是指示函数(若点( x_i )属于簇( j )则为1,否则为0)
  • ( \mu_j ) 是簇( j )的质心(均值)
具体步骤(Python代码示例)
from sklearn.cluster import KMeans
import numpy as np

# 模拟数据:50个点,2维,分3簇
X = np.concatenate([
    np.random.normal(0, 0.5, (20, 2)),   # 簇1:均值(0,0),标准差0.5
    np.random.normal(3, 0.5, (20, 2)),   # 簇2:均值(3,0),标准差0.5
    np.random.normal(1.5, 0.5, (10, 2))  # 簇3:均值(1.5,3),标准差0.5
])

# 初始化K-means(K=3)
kmeans = KMeans(n_clusters=3, random_state=42)
kmeans.fit(X)  # 训练

# 输出结果
print("质心坐标:\n", kmeans.cluster_centers_)
print("每个点的簇标签:", kmeans.labels_)

关键参数

  • n_clusters:必须提前指定的簇数K
  • init:质心初始化方式(默认k-means++,避免随机初始化的坏结果)
  • max_iter:最大迭代次数(默认300)

2. DBSCAN:用“密度可达”找簇

核心定义
  • 核心点(Core Point):在半径( \epsilon )(eps)内至少有( min_samples )个邻居的点。
  • 边界点(Border Point):在半径( \epsilon )内邻居数<( min_samples ),但能被某个核心点密度可达的点。
  • 噪声点(Noise Point):既不是核心点也不是边界点的点。
具体步骤(Python代码示例)
from sklearn.cluster import DBSCAN
import matplotlib.pyplot as plt

# 模拟数据:包含月牙形簇和噪声
from sklearn.datasets import make_moons
X, _ = make_moons(n_samples=200, noise=0.05, random_state=42)
# 添加10%噪声点(坐标在[-2, 5]随机)
noise = np.random.uniform(-2, 5, (20, 2))
X = np.concatenate([X, noise])

# 初始化DBSCAN(eps=0.3,min_samples=5)
dbscan = DBSCAN(eps=0.3, min_samples=5)
dbscan.fit(X)

# 输出结果
print("簇标签(-1为噪声):", dbscan.labels_)
print("核心点索引:", dbscan.core_sample_indices_)

# 可视化(噪声点用灰色)
plt.scatter(X[:,0], X[:,1], c=dbscan.labels_, cmap='tab20', 
            edgecolor='k', s=50)
plt.title("DBSCAN聚类结果(噪声为灰色)")
plt.show()

关键参数

  • eps:邻域半径(类似“朋友圈”的范围)
  • min_samples:邻域内最少点数(类似“至少需要几个朋友才算核心”)
  • metric:距离度量(默认欧氏距离,也可用曼哈顿、余弦等)

3. 层次聚类:用“树状图”建簇

核心距离度量

层次聚类的关键是定义“簇间距离”,常见方式:

  • 单链接(Single Linkage):两簇中最近两点的距离(易受噪声影响)
  • 全链接(Complete Linkage):两簇中最远两点的距离(易割裂大簇)
  • 平均链接(Average Linkage):两簇中所有点对的平均距离(平衡)
具体步骤(Python代码示例)
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt

# 模拟数据:3个簇,2维
X = np.concatenate([
    np.random.normal(0, 0.5, (20, 2)),   # 簇1
    np.random.normal(3, 0.5, (20, 2)),   # 簇2
    np.random.normal(6, 0.5, (20, 2))    # 簇3
])

# 计算链接矩阵(全链接)
Z = linkage(X, method='complete', metric='euclidean')

# 画树状图
plt.figure(figsize=(10, 5))
dendrogram(Z, truncate_mode='lastp', p=10, show_contracted=True)
plt.title("层次聚类树状图(全链接)")
plt.xlabel("数据点索引")
plt.ylabel("簇间距离")
plt.show()

# 用AgglomerativeClustering直接聚类(指定簇数3)
agg = AgglomerativeClustering(n_clusters=3, linkage='complete')
agg.fit(X)
print("簇标签:", agg.labels_)

关键参数

  • linkage:簇间距离计算方式(单/全/平均链接)
  • n_clusters:可选(也可通过树状图动态选择)
  • distance_threshold:替代n_clusters,指定合并停止的距离阈值

数学模型对比:谁更“聪明”?

算法 核心数学目标 优点 缺点
K-means 最小化SSE(点到质心距离平方和) 计算快,适合大数据 需提前定K,只能找球形簇,怕噪声
DBSCAN 基于密度可达性划分区域 无需定K,抗噪声,任意形状簇 需调eps和min_samples,怕密度不均
层次聚类 基于簇间距离合并的树状结构 无需定K(可后期切分),展示层次关系 计算量大(O(n³)),怕大数据

项目实战:客户分群场景对比

背景与数据

某电商想将用户分为“高价值”“潜力”“普通”“流失”四组,用以下特征:

  • 年消费金额(元)
  • 年购买次数(次)
  • 最近一次购买距今天数(R值)

数据特点:

  • 10万用户(大数据)
  • 存在少量异常用户(如年消费100万的超级VIP,或半年未购买的流失用户)
  • 簇形状可能不规则(如“高价值”用户可能分布在“高消费+高频+近期购买”的狭长区域)

步骤1:数据预处理

import pandas as pd
from sklearn.preprocessing import StandardScaler

# 读取数据(模拟)
data = pd.read_csv("customer_data.csv")
# 特征标准化(K-means对尺度敏感)
scaler = StandardScaler()
X = scaler.fit_transform(data[['年消费金额', '年购买次数', '最近一次购买距今天数']])

步骤2:K-means实战

# 用手肘法选K(SSE最小化的拐点)
sse = []
for k in range(2, 10):
    kmeans = KMeans(n_clusters=k)
    kmeans.fit(X)
    sse.append(kmeans.inertia_)  # inertia_是SSE

# 画手肘图
plt.plot(range(2, 10), sse, 'bo-')
plt.xlabel("K值")
plt.ylabel("SSE")
plt.title("手肘法选K值")
plt.show()  # 假设K=4是拐点

# 训练K=4的模型
kmeans = KMeans(n_clusters=4)
kmeans.fit(X)
data['kmeans_cluster'] = kmeans.labels_

结果分析

  • 优点:5分钟内处理10万数据,速度快
  • 缺点:将“高价值”和“潜力”用户误分为同一簇(因簇是球形,无法拟合狭长区域),且将超级VIP标记为普通簇(质心被拉偏)

步骤3:DBSCAN实战

# 调参:用k-距离图找eps(k=min_samples-1)
from sklearn.neighbors import NearestNeighbors

neighbors = NearestNeighbors(n_neighbors=5)  # min_samples=5
neighbors_fit = neighbors.fit(X)
distances, _ = neighbors_fit.kneighbors(X)
distances = np.sort(distances, axis=0)[:, 4]  # 第5近的距离(索引4)

plt.plot(distances)
plt.xlabel("数据点排序")
plt.ylabel("第5近邻距离")
plt.title("k-距离图找eps")
plt.show()  # 假设eps=0.8是拐点

# 训练DBSCAN(min_samples=5,eps=0.8)
dbscan = DBSCAN(eps=0.8, min_samples=5)
dbscan.fit(X)
data['dbscan_cluster'] = dbscan.labels_  # -1是噪声(超级VIP可能被标记为噪声?)

结果分析

  • 优点:正确识别出狭长的“高价值”簇,将超级VIP标记为噪声(需人工确认是否为异常)
  • 缺点:调参耗时(k-距离图需要经验),10万数据计算时间是K-means的10倍

步骤4:层次聚类实战

# 层次聚类适合小数据,取1000个样本(否则计算太慢)
sample_data = X[:1000]

# 训练(全链接,自动选簇数)
agg = AgglomerativeClustering(linkage='complete', distance_threshold=2, n_clusters=None)
agg.fit(sample_data)
data['agg_cluster'] = agg.labels_

# 画树状图辅助分析
Z = linkage(sample_data, method='complete')
plt.figure(figsize=(15, 5))
dendrogram(Z, truncate_mode='level', p=5)
plt.title("用户分群树状图")
plt.show()

结果分析

  • 优点:树状图清晰展示“流失用户→普通用户→潜力用户→高价值用户”的层次关系
  • 缺点:1000样本计算耗时20分钟,无法处理10万全量数据

综合对比表

指标 K-means DBSCAN 层次聚类
计算速度 快(O(nKIt)) 中(O(n²)优化后) 慢(O(n³))
需提前定K 是(关键参数) 否(可后期切分)
处理噪声 敏感(噪声影响质心) 鲁棒(标记噪声为-1) 敏感(所有点必须成簇)
簇形状 仅球形/椭球形 任意形状(月牙形、环形) 取决于链接方式(单链接易狭长)
适用数据量 大(10万+) 中(1万-10万) 小(1千-1万)
可视化 仅簇中心(无层次) 无层次 树状图(层次清晰)

实际应用场景推荐

  • 选K-means:数据量极大(10万+),簇是球形/椭球形,无明显噪声,且能通过手肘法确定K(如用户分群中的“高/中/低价值”简单分组)。
  • 选DBSCAN:数据有噪声,簇形状不规则(如地理定位中的“商圈”划分),或需要自动识别异常点(如工业传感器的异常检测)。
  • 选层次聚类:数据量小(1千-1万),需要展示簇间层次关系(如生物分类学的“界门纲目科属种”),或需要动态调整簇数(如市场细分的“大类→子类”分析)。

工具和资源推荐

  • Python库scikit-learn(集成三种算法)、scipy.cluster.hierarchy(层次聚类增强)、hdbscan(DBSCAN改进版,自动调参)。
  • 可视化工具matplotlib(画簇分布)、seaborn(画轮廓系数图)、d3.js(交互式树状图)。
  • 学习资源
    • 论文:《k-means++: The Advantages of Careful Seeding》(K-means优化)
    • 教程:Scikit-learn官方文档“Clustering”章节(链接)
    • 书籍:《统计学习方法》(李航)第14章“聚类方法”。

未来发展趋势与挑战

  • 高维数据聚类:传统算法在高维(如1000维的文本特征)下效果差,需结合降维(如PCA、t-SNE)或子空间聚类。
  • 流数据聚类:实时数据流(如电商实时用户行为)需要增量更新的聚类算法(如CluStream、DenStream)。
  • 与深度学习结合:深度聚类(Deep Clustering)用神经网络提取特征后再聚类(如DEC、IDECC),提升复杂数据的聚类效果。

总结:学到了什么?

核心概念回顾

  • K-means:质心驱动,适合球形簇、大数据,需提前定K。
  • DBSCAN:密度驱动,适合任意形状、有噪声的数据,需调eps和min_samples。
  • 层次聚类:结构驱动,适合小数据,展示层次关系,计算量大。

概念关系回顾

三种算法的差异本质是“分组依据”的不同:K-means看“到中心的距离”,DBSCAN看“周围的密度”,层次聚类看“簇间的距离”。选择时需结合数据量、噪声、簇形状、是否需要层次结构四大因素。


思考题:动动小脑筋

  1. 如果你要对“电商用户的购买行为”聚类,数据包含100万用户,且存在大量“只浏览不购买”的噪声用户,你会选哪种算法?为什么?
  2. 假设你有一组二维数据,簇形状是两个嵌套的圆环(类似靶心),K-means和DBSCAN谁能更好地识别?为什么?
  3. 层次聚类的树状图中,“合并距离”越大说明什么?如果想得到更多的簇,应该在树状图的“高层”还是“低层”切分?

附录:常见问题与解答

Q1:K-means为什么用平方误差(SSE)而不是绝对误差?
A:平方误差是凸函数,容易用梯度下降优化;绝对误差的导数不连续,优化更难。此外,平方误差对离群点更敏感(这也是K-means怕噪声的原因)。

Q2:DBSCAN的eps和min_samples如何调参?
A:常用方法是画k-距离图(k=min_samples-1),找曲线的“拐点”作为eps。例如min_samples=5时,计算每个点的第4近邻距离,排序后找突然上升的位置,对应eps。

Q3:层次聚类的“单链接”为什么容易形成链状簇?
A:单链接用两簇的最近点距离,容易被“桥梁点”(连接两个簇的孤立点)误导,导致合并本不相关的簇,形成长链。


扩展阅读 & 参考资料

  1. 《Pattern Recognition and Machine Learning》(Christopher M. Bishop)第9章“Mixture Models and EM”(K-means理论基础)。
  2. 《A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise》(DBSCAN原始论文)。
  3. 《Hierarchical Grouping to Optimize an Objective Function》(层次聚类经典论文)。
  4. Scikit-learn官方文档:Clustering Guide。

你可能感兴趣的:(算法,聚类,kmeans,ai)