关键词:聚类算法、K-means、DBSCAN、层次聚类、性能对比、机器学习、无监督学习
摘要:聚类是无监督学习的核心任务之一,广泛应用于用户分群、图像分割、异常检测等场景。本文将用“分水果”“找朋友”“建家谱”等生活化比喻,从原理、优缺点到实战场景,一步一步对比K-means、DBSCAN、层次聚类三种主流算法。无论你是刚入门的机器学习爱好者,还是需要为项目选择聚类方案的开发者,读完本文都能清晰掌握三种算法的差异与适用场景。
聚类的本质是“物以类聚”:从无标签数据中发现隐含的分组规律。本文聚焦三种最常用的聚类算法——K-means(最经典)、DBSCAN(抗噪声)、层次聚类(可视化强),通过原理拆解、代码实战、场景对比,帮你解决“选哪个”的核心问题。
本文将按“原理→对比→实战→选型”的逻辑展开:先通过生活化故事讲清每个算法的核心思想,再用数学公式和代码揭示细节,最后通过真实数据集实战对比效果,最终总结选型指南。
假设你是水果摊老板,需要把一车混装的苹果、橘子、香蕉分开。不同的分法对应不同的聚类算法:
K-means的核心是“选代表→分组→调整代表”。
比如分班级:老师先猜三个“平均身高代表”(质心),让每个同学站到离自己身高最近的代表旁边;然后重新计算这三组的平均身高(新质心),再让同学重新站队;重复直到代表位置不再变化,就分好了组。
关键规则:需要提前定簇数(K),每个点属于最近的质心,簇是圆形/球形(因为用欧氏距离)。
DBSCAN的核心是“找密集区域,忽略稀疏点”。
比如在操场找朋友:设定“好朋友”条件——你周围2米内至少有3个同学(核心点),那么你和这3个同学是朋友;这3个同学周围2米内的其他同学(密度可达)也是朋友,最终形成一个“朋友圈”(簇)。剩下的落单同学(周围2米内不足3人)是“独行侠”(噪声点)。
关键规则:不需要提前定簇数,能发现任意形状的簇(比如月牙形、环形),但怕密度不均匀的数据。
层次聚类的核心是“从分到合,建家族树”。
比如家族聚会:一开始每个客人是独立的“小家庭”(簇);然后找到关系最近的两家人(距离最小),合并成一个“大家庭”;重复合并直到所有人在一个家族里,形成一棵“家谱树”(树状图)。最后在树的某个位置切一刀,得到想要的簇数。
关键规则:不需要提前定簇数(可以后期切分),但计算量大(适合小数据),能展示簇间层次关系。
三种算法像三种不同的分水果策略,区别在于“分组依据”和“适应场景”:
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[画树状图,切分得到簇]
目标是最小化所有点到其所属簇质心的平方距离和(误差平方和,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=1∑nj=1∑KIij⋅∣∣xi−μj∣∣2
其中:
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
:必须提前指定的簇数Kinit
:质心初始化方式(默认k-means++
,避免随机初始化的坏结果)max_iter
:最大迭代次数(默认300)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
:距离度量(默认欧氏距离,也可用曼哈顿、余弦等)层次聚类的关键是定义“簇间距离”,常见方式:
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³)),怕大数据 |
某电商想将用户分为“高价值”“潜力”“普通”“流失”四组,用以下特征:
数据特点:
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[['年消费金额', '年购买次数', '最近一次购买距今天数']])
# 用手肘法选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_
结果分析:
# 调参:用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可能被标记为噪声?)
结果分析:
# 层次聚类适合小数据,取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()
结果分析:
指标 | K-means | DBSCAN | 层次聚类 |
---|---|---|---|
计算速度 | 快(O(nKIt)) | 中(O(n²)优化后) | 慢(O(n³)) |
需提前定K | 是(关键参数) | 否 | 否(可后期切分) |
处理噪声 | 敏感(噪声影响质心) | 鲁棒(标记噪声为-1) | 敏感(所有点必须成簇) |
簇形状 | 仅球形/椭球形 | 任意形状(月牙形、环形) | 取决于链接方式(单链接易狭长) |
适用数据量 | 大(10万+) | 中(1万-10万) | 小(1千-1万) |
可视化 | 仅簇中心(无层次) | 无层次 | 树状图(层次清晰) |
scikit-learn
(集成三种算法)、scipy.cluster.hierarchy
(层次聚类增强)、hdbscan
(DBSCAN改进版,自动调参)。matplotlib
(画簇分布)、seaborn
(画轮廓系数图)、d3.js
(交互式树状图)。三种算法的差异本质是“分组依据”的不同:K-means看“到中心的距离”,DBSCAN看“周围的密度”,层次聚类看“簇间的距离”。选择时需结合数据量、噪声、簇形状、是否需要层次结构四大因素。
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:单链接用两簇的最近点距离,容易被“桥梁点”(连接两个簇的孤立点)误导,导致合并本不相关的簇,形成长链。