跳到主要内容

聚类算法

聚类是无监督学习的核心任务之一。与分类不同,聚类不需要预先定义的标签,而是自动发现数据中的自然分组。聚类的目标是将相似的样本归为同一组,使组内样本尽量相似,组间样本尽量不同。

聚类算法广泛应用于客户细分、图像分割、异常检测、文档归类等场景。本章将介绍 sklearn 中常用的聚类算法及其应用。

聚类问题基础

什么是好的聚类?

评估聚类质量比评估分类更困难,因为没有"正确答案"。好的聚类通常满足:

  • 高簇内相似度:同一簇内的样本应该尽量相似
  • 低簇间相似度:不同簇的样本应该尽量不同
  • 合理的簇数量:簇的数量应该符合数据的真实结构

聚类算法的分类

聚类算法大致可以分为以下几类:

类型代表算法特点
基于原型K-Means、K-Medoids每个簇由中心点表示
基于密度DBSCAN、OPTICS可以发现任意形状的簇
基于层次层次聚类构建树状的簇层次结构
基于分布高斯混合模型假设数据由多个分布生成

K-Means 聚类

K-Means 是最经典、最常用的聚类算法。它简单、高效、易于理解,是聚类问题的首选基线算法。

原理详解

K-Means 通过迭代优化来最小化簇内样本到簇中心的距离平方和。算法流程如下:

初始化:随机选择 K 个样本作为初始簇中心(质心)

迭代

  1. 分配步骤:将每个样本分配给最近的簇中心
  2. 更新步骤:重新计算每个簇的中心(簇内样本的均值)

终止条件:簇中心不再变化或达到最大迭代次数

目标函数:最小化簇内平方和(Inertia)

J=i=1KxCixμi2J = \sum_{i=1}^{K} \sum_{x \in C_i} \|x - \mu_i\|^2

其中 CiC_i 是第 ii 个簇,μi\mu_i 是该簇的中心。

为什么 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簇数量 K8
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++ 通过以下步骤改进初始化:

  1. 随机选择第一个质心
  2. 对于每个样本,计算与已选质心的最短距离
  3. 按照距离平方的比例概率选择下一个质心
  4. 重复直到选出 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}")

轮廓系数的数学定义

对于样本 ii

  • a(i)a(i) = 样本 ii 到同簇其他样本的平均距离(簇内距离)
  • b(i)b(i) = 样本 ii 到最近其他簇的所有样本的平均距离(簇间距离)

轮廓系数:

s(i)=b(i)a(i)max(a(i),b(i))s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}

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):如果一个点的 ϵ\epsilon 邻域内至少有 min_samples 个点,则该点为核心点。

边界点(Border Point):不是核心点,但在某个核心点的 ϵ\epsilon 邻域内。

噪声点(Noise Point):既不是核心点也不是边界点。

算法流程

  1. 找出所有核心点
  2. 将相互可达的核心点连接成簇(密度相连)
  3. 将边界点分配给对应的核心点簇
  4. 标记噪声点

为什么 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邻域半径 ϵ\epsilon0.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,自底向上)

  1. 初始时每个样本是一个簇
  2. 迭代合并最相似的两个簇
  3. 直到只剩一个簇或达到指定簇数量

分裂式(Divisive,自顶向下)

  1. 初始时所有样本在一个簇
  2. 迭代分裂簇
  3. 直到每个样本成为一个簇或达到指定簇数量

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 个高斯分布混合生成:

P(x)=k=1KπkN(xμk,Σk)P(x) = \sum_{k=1}^{K} \pi_k \mathcal{N}(x | \mu_k, \Sigma_k)

其中 πk\pi_k 是第 k 个分量的混合系数,N(xμk,Σk)\mathcal{N}(x | \mu_k, \Sigma_k) 是高斯分布。

EM 算法

GMM 使用期望最大化(EM)算法估计参数:

  1. E 步:计算每个样本属于各分量的后验概率
  2. 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、层次聚类

选择流程

  1. 数据规模:大数据用 Mini-Batch K-Means,中小数据可以尝试多种方法
  2. 簇形状:球形用 K-Means,复杂形状用 DBSCAN
  3. 是否有噪声:有噪声用 DBSCAN
  4. 是否需要概率:需要概率用 GMM
  5. 是否知道簇数量:已知用 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()

这个示例展示了不同聚类算法在不同数据结构上的表现,帮助你理解如何选择合适的聚类算法。

小结

聚类是无监督学习的重要工具,但使用时需要注意:

  1. 预处理很重要:大多数聚类算法对特征尺度敏感,建议标准化
  2. 选择合适的算法:根据数据特点选择算法,没有一种算法适用于所有场景
  3. 多角度评估:使用多种评估指标,从不同角度评估聚类质量
  4. 参数调优:DBSCAN 的 eps、K-Means 的 K 都需要调优
  5. 结合业务理解:聚类结果需要结合业务场景解释和应用