聚类算法
聚类是无监督学习的核心任务之一。与分类不同,聚类不需要预先定义的标签,而是自动发现数据中的自然分组。聚类的目标是将相似的样本归为同一组,使组内样本尽量相似,组间样本尽量不同。
聚类算法广泛应用于客户细分、图像分割、异常检测、文档归类等场景。本章将介绍 sklearn 中常用的聚类算法及其应用。
聚类问题基础
什么是好的聚类?
评估聚类质量比评估分类更困难,因为没有"正确答案"。好的聚类通常满足:
- 高簇内相似度:同一簇内的样本应该尽量相似
- 低簇间相似度:不同簇的样本应该尽量不同
- 合理的簇数量:簇的数量应该符合数据的真实结构
聚类算法的分类
聚类算法大致可以分为以下几类:
| 类型 | 代表算法 | 特点 |
|---|---|---|
| 基于原型 | K-Means、K-Medoids | 每个簇由中心点表示 |
| 基于密度 | DBSCAN、OPTICS | 可以发现任意形状的簇 |
| 基于层次 | 层次聚类 | 构建树状的簇层次结构 |
| 基于分布 | 高斯混合模型 | 假设数据由多个分布生成 |
K-Means 聚类
K-Means 是最经典、最常用的聚类算法。它简单、高效、易于理解,是聚类问题的首选基线算法。
原理详解
K-Means 通过迭代优化来最小化簇内样本到簇中心的距离平方和。算法流程如下:
初始化:随机选择 K 个样本作为初始簇中心(质心)
迭代:
- 分配步骤:将每个样本分配给最近的簇中心
- 更新步骤:重新计算每个簇的中心(簇内样本的均值)
终止条件:簇中心不再变化或达到最大迭代次数
目标函数:最小化簇内平方和(Inertia)
其中 是第 个簇, 是该簇的中心。
为什么 K-Means 有效?
从优化角度看,K-Means 的目标函数是非凸的,可能存在多个局部最优。但实践证明,K-Means 通常能找到不错的解。每次迭代保证目标函数不增加,最终收敛到局部最优。
基本用法
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
# 生成示例数据
X, y_true = make_blobs(
n_samples=300,
centers=4,
cluster_std=0.8,
random_state=42
)
# K-Means 聚类
kmeans = KMeans(
n_clusters=4, # 簇数量
init='k-means++', # 初始化方法
n_init=10, # 运行次数
max_iter=300, # 最大迭代次数
random_state=42
)
y_pred = kmeans.fit_predict(X)
print(f"簇内平方和 (Inertia): {kmeans.inertia_:.2f}")
print(f"迭代次数: {kmeans.n_iter_}")
# 可视化结果
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', s=50)
plt.title('真实标签')
plt.subplot(1, 2, 2)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap='viridis', s=50)
plt.scatter(kmeans.cluster_centers_[:, 0],
kmeans.cluster_centers_[:, 1],
c='red', marker='x', s=200, linewidths=3)
plt.title('K-Means 结果')
plt.tight_layout()
plt.show()
重要参数
| 参数 | 说明 | 默认值 |
|---|---|---|
n_clusters | 簇数量 K | 8 |
init | 初始化方法:'k-means++'、'random' | 'k-means++' |
n_init | 使用不同初始化运行的次数 | 'auto' |
max_iter | 单次运行的最大迭代次数 | 300 |
tol | 收敛阈值 | 1e-4 |
algorithm | 实现算法:'lloyd'、'elkan' | 'lloyd' |
k-means++ 初始化:
标准 K-Means 随机选择初始质心,可能导致收敛到较差的局部最优。k-means++ 通过以下步骤改进初始化:
- 随机选择第一个质心
- 对于每个样本,计算与已选质心的最短距离
- 按照距离平方的比例概率选择下一个质心
- 重复直到选出 K 个质心
这种方法使初始质心尽量分散,提高找到全局最优的概率。
确定最佳 K 值
K-Means 需要预先指定簇数量 K,但实际应用中 K 往往未知。以下是几种常用的确定方法:
肘部法则(Elbow Method):
绘制不同 K 值对应的 Inertia 曲线,选择曲线开始变平缓的点("肘部")。
inertias = []
K_range = range(1, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X)
inertias.append(kmeans.inertia_)
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('簇数量 K')
plt.ylabel('Inertia')
plt.title('肘部法则')
plt.grid(True)
# 标注肘部位置
plt.axvline(x=4, color='r', linestyle='--', label='肘部 (K=4)')
plt.legend()
plt.show()
轮廓系数(Silhouette Score):
轮廓系数综合考虑簇内紧密度和簇间分离度,取值范围 [-1, 1],值越大表示聚类效果越好。
from sklearn.metrics import silhouette_score, silhouette_samples
silhouette_scores = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
score = silhouette_score(X, labels)
silhouette_scores.append(score)
plt.figure(figsize=(8, 5))
plt.plot(K_range, silhouette_scores, 'ro-')
plt.xlabel('簇数量 K')
plt.ylabel('轮廓系数')
plt.title('轮廓系数法')
plt.grid(True)
plt.show()
# 最佳 K 值
best_k = K_range[silhouette_scores.index(max(silhouette_scores))]
print(f"最佳 K 值: {best_k}")
轮廓系数的数学定义:
对于样本 :
- = 样本 到同簇其他样本的平均距离(簇内距离)
- = 样本 到最近其他簇的所有样本的平均距离(簇间距离)
轮廓系数:
K-Means 的局限性
假设簇是球形的:K-Means 使用欧氏距离,隐含假设簇是凸的、各向同性的。对于非球形簇效果不好。
对初始值敏感:不同的初始化可能收敛到不同的局部最优。解决方案是使用 k-means++ 和多次运行(n_init)。
对异常值敏感:异常值会显著影响簇中心的计算。可以使用 K-Medoids 或先进行异常检测。
需要预先指定 K:虽然可以用肘部法则等方法估计,但仍需要主观判断。
Mini-Batch K-Means
对于大规模数据,Mini-Batch K-Means 是更高效的选择。它每次只使用一小批数据进行更新,大大提高了速度。
from sklearn.cluster import MiniBatchKMeans
import time
# 生成大规模数据
X_large, _ = make_blobs(n_samples=100000, centers=10, random_state=42)
# 标准K-Means
start = time.time()
kmeans = KMeans(n_clusters=10, random_state=42, n_init=10)
kmeans.fit(X_large)
print(f"标准 K-Means: {time.time() - start:.2f}秒")
# Mini-Batch K-Means
start = time.time()
mbk = MiniBatchKMeans(n_clusters=10, batch_size=1000, random_state=42)
mbk.fit(X_large)
print(f"Mini-Batch K-Means: {time.time() - start:.2f}秒")
print(f"\nInertia 差异: {abs(kmeans.inertia_ - mbk.inertia_):.0f}")
DBSCAN 聚类
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法,能够发现任意形状的簇,并自动识别噪声点。
原理详解
DBSCAN 的核心概念:
核心点(Core Point):如果一个点的 邻域内至少有 min_samples 个点,则该点为核心点。
边界点(Border Point):不是核心点,但在某个核心点的 邻域内。
噪声点(Noise Point):既不是核心点也不是边界点。
算法流程:
- 找出所有核心点
- 将相互可达的核心点连接成簇(密度相连)
- 将边界点分配给对应的核心点簇
- 标记噪声点
为什么 DBSCAN 能发现任意形状的簇?
与 K-Means 基于距离不同,DBSCAN 基于密度连接。只要两个点之间可以通过一系列核心点连接,它们就属于同一个簇,不受簇形状限制。
基本用法
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
import numpy as np
# 生成月牙形数据(K-Means 效果不好)
X, y_true = make_moons(n_samples=300, noise=0.05, random_state=42)
# DBSCAN 聚类
dbscan = DBSCAN(eps=0.3, min_samples=5)
y_pred = dbscan.fit_predict(X)
# 统计结果
n_clusters = len(set(y_pred)) - (1 if -1 in y_pred else 0)
n_noise = list(y_pred).count(-1)
print(f"发现簇数量: {n_clusters}")
print(f"噪声点数量: {n_noise}")
# 可视化
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', s=50)
plt.title('真实标签')
plt.subplot(1, 3, 2)
kmeans = KMeans(n_clusters=2, random_state=42)
y_kmeans = kmeans.fit_predict(X)
plt.scatter(X[:, 0], X[:, 1], c=y_kmeans, cmap='viridis', s=50)
plt.title('K-Means')
plt.subplot(1, 3, 3)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap='viridis', s=50)
plt.title('DBSCAN')
plt.tight_layout()
plt.show()
重要参数
| 参数 | 说明 | 默认值 |
|---|---|---|
eps | 邻域半径 | 0.5 |
min_samples | 核心点的最小邻居数 | 5 |
metric | 距离度量 | 'euclidean' |
参数选择:
eps的选择可以使用 k-距离图:计算每个点到第 k 近邻的距离,排序后绘制曲线,选择"膝点"作为 eps。
from sklearn.neighbors import NearestNeighbors
# 计算每个点到第 min_samples 近邻的距离
neighbors = NearestNeighbors(n_neighbors=5)
neighbors.fit(X)
distances, indices = neighbors.kneighbors(X)
# 取第 5 近邻的距离并排序
k_distances = np.sort(distances[:, 4])
plt.figure(figsize=(8, 5))
plt.plot(k_distances)
plt.xlabel('样本索引(排序后)')
plt.ylabel('第5近邻距离')
plt.title('k-距离图(用于选择 eps)')
plt.grid(True)
plt.axhline(y=0.15, color='r', linestyle='--', label='建议 eps')
plt.legend()
plt.show()
DBSCAN 的优缺点
优点:
- 不需要预先指定簇数量
- 可以发现任意形状的簇
- 能够识别噪声点
- 对异常值不敏感
缺点:
- 对参数 eps 和 min_samples 敏感
- 对密度不均匀的数据效果不好
- 高维数据中距离度量效果下降
层次聚类
层次聚类通过构建树状的层次结构来组织数据,可以生成树状图(Dendrogram)直观展示聚类过程。
原理详解
层次聚类有两种主要方式:
凝聚式(Agglomerative,自底向上):
- 初始时每个样本是一个簇
- 迭代合并最相似的两个簇
- 直到只剩一个簇或达到指定簇数量
分裂式(Divisive,自顶向下):
- 初始时所有样本在一个簇
- 迭代分裂簇
- 直到每个样本成为一个簇或达到指定簇数量
sklearn 实现的是凝聚式方法。
簇间距离度量(链接方式):
- Ward:最小化合并后的簇内方差增加(默认)
- Complete:使用两个簇的最远点距离
- Average:使用两个簇的平均距离
- Single:使用两个簇的最近点距离
基本用法
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt
# 生成数据
X, y_true = make_blobs(n_samples=100, centers=3, cluster_std=1.0, random_state=42)
# 层次聚类
hc = AgglomerativeClustering(
n_clusters=3,
linkage='ward'
)
y_pred = hc.fit_predict(X)
print(f"聚类结果: {y_pred[:20]}")
# 绘制树状图
plt.figure(figsize=(12, 6))
Z = linkage(X, method='ward')
dendrogram(Z, truncate_mode='level', p=5)
plt.title('层次聚类树状图')
plt.xlabel('样本索引或簇大小')
plt.ylabel('距离')
plt.show()
不同链接方式的对比
from sklearn.cluster import AgglomerativeClustering
linkage_methods = ['ward', 'complete', 'average', 'single']
plt.figure(figsize=(14, 10))
for i, method in enumerate(linkage_methods, 1):
hc = AgglomerativeClustering(n_clusters=3, linkage=method)
y_pred = hc.fit_predict(X)
plt.subplot(2, 2, i)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap='viridis', s=50)
plt.title(f'链接方式: {method}')
plt.tight_layout()
plt.show()
链接方式选择建议:
- Ward:适用于大多数情况,产生大小相近的簇
- Complete:产生紧凑的簇,对异常值敏感
- Average:平衡的选择,对异常值相对稳健
- Single:能发现链状簇,但可能产生"链式效应"
层次聚类的优缺点
优点:
- 不需要预先指定簇数量(可以从树状图选择)
- 可以生成层次结构,便于分析
- 簇的粒度可以灵活调整
缺点:
- 计算复杂度高,不适合大数据集
- 一旦合并或分裂就不能撤销
- 对噪声和异常值敏感
高斯混合模型(GMM)
高斯混合模型假设数据由多个高斯分布混合生成,是一种基于概率模型的聚类方法。
原理详解
GMM 假设数据由 K 个高斯分布混合生成:
其中 是第 k 个分量的混合系数, 是高斯分布。
EM 算法:
GMM 使用期望最大化(EM)算法估计参数:
- E 步:计算每个样本属于各分量的后验概率
- M 步:根据后验概率更新各分量的参数
基本用法
from sklearn.mixture import GaussianMixture
# GMM 聚类
gmm = GaussianMixture(
n_components=3, # 分量数量
covariance_type='full', # 协方差类型
random_state=42
)
gmm.fit(X)
y_pred = gmm.predict(X)
# 概率输出
y_proba = gmm.predict_proba(X)
print(f"聚类结果: {y_pred[:10]}")
print(f"样本属于各簇的概率:\n{y_proba[:5]}")
# 使用 BIC 选择最佳分量数
bic_scores = []
n_components_range = range(1, 11)
for n in n_components_range:
gmm = GaussianMixture(n_components=n, random_state=42)
gmm.fit(X)
bic_scores.append(gmm.bic(X))
plt.figure(figsize=(8, 5))
plt.plot(n_components_range, bic_scores, 'bo-')
plt.xlabel('分量数量')
plt.ylabel('BIC')
plt.title('BIC 准则选择最佳分量数')
plt.grid(True)
plt.show()
重要参数
| 参数 | 说明 | 默认值 |
|---|---|---|
n_components | 高斯分量数量 | 1 |
covariance_type | 协方差类型:'full'、'tied'、'diag'、'spherical' | 'full' |
init_params | 初始化方法:'kmeans'、'random' | 'kmeans' |
GMM 的优缺点
优点:
- 可以输出概率,便于评估不确定性
- 可以拟合椭圆形簇(不同的协方差结构)
- 软聚类,样本可以属于多个簇
缺点:
- 对初始化敏感
- 假设数据服从高斯分布
- 对异常值敏感
聚类评估
由于没有真实标签,聚类评估比分类更复杂。以下是常用的评估指标。
内部评估指标
不需要真实标签,基于数据本身评估聚类质量。
轮廓系数(Silhouette Score):
from sklearn.metrics import silhouette_score
score = silhouette_score(X, y_pred)
print(f"轮廓系数: {score:.3f}")
# 解释:
# 0.71 - 1.00: 强
# 0.51 - 0.70: 合理
# 0.26 - 0.50: 弱
# < 0.25: 差
Calinski-Harabasz 指数:
簇间方差与簇内方差的比值,值越大越好。
from sklearn.metrics import calinski_harabasz_score
score = calinski_harabasz_score(X, y_pred)
print(f"Calinski-Harabasz 指数: {score:.2f}")
Davies-Bouldin 指数:
衡量簇内的紧密度和簇间的分离度,值越小越好。
from sklearn.metrics import davies_bouldin_score
score = davies_bouldin_score(X, y_pred)
print(f"Davies-Bouldin 指数: {score:.3f}")
外部评估指标
当有真实标签时,可以评估聚类结果与真实标签的匹配程度。
调整兰德指数(Adjusted Rand Index, ARI):
from sklearn.metrics import adjusted_rand_score
ari = adjusted_rand_score(y_true, y_pred)
print(f"调整兰德指数: {ari:.3f}")
# ARI 范围 [-1, 1],1 表示完美匹配,0 表示随机
归一化互信息(Normalized Mutual Information, NMI):
from sklearn.metrics import normalized_mutual_info_score
nmi = normalized_mutual_info_score(y_true, y_pred)
print(f"归一化互信息: {nmi:.3f}")
# NMI 范围 [0, 1],1 表示完美匹配
指标对比示例
from sklearn.metrics import (
silhouette_score, calinski_harabasz_score, davies_bouldin_score,
adjusted_rand_score, normalized_mutual_info_score
)
# 生成数据
X, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.8, random_state=42)
# 不同聚类算法
algorithms = {
'K-Means': KMeans(n_clusters=4, random_state=42, n_init=10),
'DBSCAN': DBSCAN(eps=0.8, min_samples=5),
'层次聚类': AgglomerativeClustering(n_clusters=4),
'GMM': GaussianMixture(n_components=4, random_state=42)
}
results = []
for name, algo in algorithms.items():
y_pred = algo.fit_predict(X)
# 跳过 DBSCAN 的噪声点
if name == 'DBSCAN':
mask = y_pred != -1
if mask.sum() == 0:
continue
result = {
'算法': name,
'轮廓系数': silhouette_score(X, y_pred),
'CH指数': calinski_harabasz_score(X, y_pred),
'DB指数': davies_bouldin_score(X, y_pred),
'ARI': adjusted_rand_score(y_true, y_pred),
'NMI': normalized_mutual_info_score(y_true, y_pred)
}
results.append(result)
import pandas as pd
df = pd.DataFrame(results)
print(df.to_string(index=False))
算法选择指南
| 数据特点 | 推荐算法 |
|---|---|
| 球形簇、大小相近 | K-Means |
| 任意形状簇 | DBSCAN |
| 需要层次结构 | 层次聚类 |
| 需要概率输出 | GMM |
| 密度不均匀 | OPTICS |
| 大规模数据 | Mini-Batch K-Means |
| 已知簇数量 | K-Means、GMM |
| 未知簇数量 | DBSCAN、层次聚类 |
选择流程:
- 数据规模:大数据用 Mini-Batch K-Means,中小数据可以尝试多种方法
- 簇形状:球形用 K-Means,复杂形状用 DBSCAN
- 是否有噪声:有噪声用 DBSCAN
- 是否需要概率:需要概率用 GMM
- 是否知道簇数量:已知用 K-Means/GMM,未知用 DBSCAN/层次聚类
完整示例
下面是一个完整的聚类分析示例,展示从数据生成到算法选择和评估的全过程:
from sklearn.datasets import make_blobs, make_moons, make_circles
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score, adjusted_rand_score
import matplotlib.pyplot as plt
import numpy as np
# 生成三种不同结构的数据
datasets = {
'球形簇': make_blobs(n_samples=300, centers=4, cluster_std=0.6, random_state=42)[0],
'月牙形': make_moons(n_samples=300, noise=0.05, random_state=42)[0],
'环形': make_circles(n_samples=300, noise=0.05, factor=0.5, random_state=42)[0]
}
# 定义聚类算法
def get_algorithms(name):
if name == '球形簇':
return {
'K-Means': KMeans(n_clusters=4, random_state=42, n_init=10),
'DBSCAN': DBSCAN(eps=0.5, min_samples=5),
'层次聚类': AgglomerativeClustering(n_clusters=4)
}
else:
return {
'K-Means': KMeans(n_clusters=2, random_state=42, n_init=10),
'DBSCAN': DBSCAN(eps=0.2, min_samples=5),
'层次聚类': AgglomerativeClustering(n_clusters=2, linkage='single')
}
# 绘制结果
fig, axes = plt.subplots(3, 4, figsize=(16, 12))
for row, (data_name, X) in enumerate(datasets.items()):
# 标准化
X = StandardScaler().fit_transform(X)
# 绘制原始数据
axes[row, 0].scatter(X[:, 0], X[:, 1], s=30, c='gray')
axes[row, 0].set_title(f'{data_name} - 原始数据')
# 应用聚类算法
algorithms = get_algorithms(data_name)
for col, (algo_name, algo) in enumerate(algorithms.items(), 1):
y_pred = algo.fit_predict(X)
# 计算轮廓系数(排除噪声点)
if algo_name == 'DBSCAN' and -1 in y_pred:
mask = y_pred != -1
if mask.sum() > 1:
score = silhouette_score(X[mask], y_pred[mask])
else:
score = 0
else:
score = silhouette_score(X, y_pred)
axes[row, col].scatter(X[:, 0], X[:, 1], c=y_pred, cmap='viridis', s=30)
axes[row, col].set_title(f'{algo_name}\n轮廓系数: {score:.2f}')
plt.tight_layout()
plt.show()
这个示例展示了不同聚类算法在不同数据结构上的表现,帮助你理解如何选择合适的聚类算法。
小结
聚类是无监督学习的重要工具,但使用时需要注意:
- 预处理很重要:大多数聚类算法对特征尺度敏感,建议标准化
- 选择合适的算法:根据数据特点选择算法,没有一种算法适用于所有场景
- 多角度评估:使用多种评估指标,从不同角度评估聚类质量
- 参数调优:DBSCAN 的 eps、K-Means 的 K 都需要调优
- 结合业务理解:聚类结果需要结合业务场景解释和应用