4 minutes
聚类分析
前两篇文章我们学习了监督学习中的回归和分类算法。但真实世界中,大量数据是没有标签的。当数据没有标准答案时,我们如何从中发现规律?这就是无监督学习的用武之地,而聚类分析是无监督学习中最核心的任务之一。
什么是聚类分析?
聚类分析的目标是将数据划分为若干个组(簇),使得同一簇内的样本尽可能相似,不同簇的样本尽可能不同。聚类与分类的根本区别在于:分类有标签可学,聚类完全靠数据自身的结构。
聚类的主要应用
- 客户分群:将用户按消费行为分组,制定差异化营销策略
- 图像压缩:将相似颜色聚为一类,减少颜色数量
- 异常检测:远离所有簇的样本可能是异常值
- 文档分类:自动将新闻文章按主题分组
- 生物信息学:基因表达数据分析,发现功能相似的基因
K-Means 聚类
K-Means 是最经典、最常用的聚类算法,简单高效。
算法原理
K-Means 的迭代过程如下:
- 随机选择 K 个点作为初始簇中心
- 计算每个样本到 K 个中心的距离,分配到最近的中心
- 对每个簇,重新计算中心(簇内所有点的均值)
- 重复 2-3 步直到中心不再变化或达到最大迭代次数
Python 实现
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
# 生成模拟数据:3 个簇
X, y_true = make_blobs(n_samples=300, centers=3,
cluster_std=1.0, random_state=42)
# 可视化数据
plt.figure(figsize=(10, 6))
plt.scatter(X[:, 0], X[:, 1], s=50, alpha=0.7)
plt.title('原始数据(无标签)')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.show()
# 应用 K-Means
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)
# 获取结果
labels = kmeans.labels_
centers = kmeans.cluster_centers_
print(f"簇中心:\n{centers}")
print(f"每个簇的样本数: {np.bincount(labels)}")
# 可视化聚类结果
plt.figure(figsize=(10, 6))
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', s=50, alpha=0.7)
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='X',
s=200, linewidths=3, label='簇中心')
plt.title('K-Means 聚类结果')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.legend()
plt.show()
选择合适的 K
K 值是 K-Means 最重要的参数。常用的选择方法有肘部法则和轮廓系数。
肘部法则(Elbow Method):绘制 K 值与总距离(惯性)的关系图,选择曲线"拐弯"处的 K。
# 肘部法则
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=(10, 6))
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('K 值')
plt.ylabel('惯性 (Inertia)')
plt.title('肘部法则')
plt.xticks(K_range)
plt.grid(alpha=0.3)
plt.show()
# 选择曲线拐点处的 K
轮廓系数(Silhouette Score):衡量每个样本与其所属簇的紧密程度。
from sklearn.metrics import silhouette_score
silhouette_scores = []
for k in range(2, 11):
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=(10, 6))
plt.plot(range(2, 11), silhouette_scores, 'bo-')
plt.xlabel('K 值')
plt.ylabel('轮廓系数')
plt.title('不同 K 值的轮廓系数')
plt.grid(alpha=0.3)
plt.show()
best_k = range(2, 11)[np.argmax(silhouette_scores)]
print(f"轮廓系数最大的 K: {best_k}")
# 轮廓系数范围 [-1, 1],越大越好
K-Means 的初始化:k-means++
K-Means 对初始中心的选择很敏感。k-means++ 是更聪明的初始化方法:
- 随机选择第一个中心点
- 对于每个样本点,计算它到最近已选中心的距离
- 以正比于距离平方的概率选择下一个中心
- 重复直到选够 K 个初始中心
scikit-learn 的 KMeans(n_init=10) 默认使用 k-means++ 初始化,并运行多次取最优结果。
# 显式指定初始化方法
kmeans_plus = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42)
kmeans_random = KMeans(n_clusters=3, init='random', n_init=1, random_state=42)
kmeans_plus.fit(X)
kmeans_random.fit(X)
print(f"k-means++ 惯性: {kmeans_plus.inertia_:.2f}")
print(f"random 初始化惯性: {kmeans_random.inertia_:.2f}")
K-Means 的局限性
- 需要预先指定 K
- 对异常值敏感
- 仅适用于球形簇(不能发现非凸形状的簇)
- 对初始值敏感
层次聚类
层次聚类不需要预先指定簇的数量,结果以树状图(树状图)形式呈现。
凝聚式(自底向上)
每个样本初始为一个簇,然后逐步合并最近的两个簇,直到所有样本属于一个簇。
分裂式(自顶向下)
所有样本初始为一个簇,然后递归分裂。
scikit-learn 实现的是凝聚式层次聚类。
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
# 层次聚类
agglo = AgglomerativeClustering(n_clusters=3, linkage='ward')
agglo_labels = agglo.fit_predict(X)
# 绘制树状图
plt.figure(figsize=(12, 6))
Z = linkage(X, method='ward')
dendrogram(Z, truncate_mode='level', p=10)
plt.title('层次聚类树状图')
plt.xlabel('样本')
plt.ylabel('距离')
plt.axhline(y=20, color='r', linestyle='--', label='切割线 (k=3)')
plt.legend()
plt.show()
连接准则
| 准则 | 距离定义 | 特点 |
|---|---|---|
| ward | 簇内方差增量 | 产生大小相近的簇,最常用 |
| complete | 两个簇最远点距离 | 产生紧凑的簇 |
| average | 两个簇所有点平均距离 | 折中方案 |
| single | 两个簇最近点距离 | 容易产生链状簇 |
树状图的解读方法:纵轴是合并时的距离,横轴是样本或子簇。在纵轴上选择一个截断高度,水平线穿过的竖线数量就是簇的数量。
DBSCAN
DBSCAN(基于密度的空间聚类)可以发现任意形状的簇,并能自动识别噪声点。
核心概念
DBSCAN 有两个关键参数:
- epsilon (eps):邻域半径
- minPts (min_samples):成为核心点所需的最小点数
DBSCAN 将点分为三类:
- 核心点:eps 半径内至少有 minPts 个点
- 边界点:eps 半径内点数少于 minPts,但属于某个核心点的邻域
- 噪声点:不属于任何簇的点
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
# 生成非球形数据
X_moons, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
# K-Means 表现(无法处理非球形簇)
kmeans_moons = KMeans(n_clusters=2, random_state=42, n_init=10)
kmeans_labels = kmeans_moons.fit_predict(X_moons)
# DBSCAN 表现
dbscan = DBSCAN(eps=0.3, min_samples=5)
dbscan_labels = dbscan.fit_predict(X_moons)
# 可视化对比
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].scatter(X_moons[:, 0], X_moons[:, 1], c='gray', s=50, alpha=0.7)
axes[0].set_title('原始数据')
axes[1].scatter(X_moons[:, 0], X_moons[:, 1], c=kmeans_labels,
cmap='viridis', s=50, alpha=0.7)
axes[1].set_title('K-Means (失败)')
axes[2].scatter(X_moons[:, 0], X_moons[:, 1], c=dbscan_labels,
cmap='viridis', s=50, alpha=0.7)
axes[2].set_title('DBSCAN (成功)')
plt.tight_layout()
plt.show()
n_noise = np.sum(dbscan_labels == -1)
n_clusters_db = len(set(dbscan_labels) - {-1})
print(f"DBSCAN 发现 {n_clusters_db} 个簇, {n_noise} 个噪声点")
选择 eps 和 minPts
k-distance 图可以帮助选择合适的 eps:计算每个点到其第 k 近邻的距离,排序后绘图,拐点处就是合适的 eps 值。
from sklearn.neighbors import NearestNeighbors
# k-distance 图
k = 5 # minPts
neigh = NearestNeighbors(n_neighbors=k)
neigh.fit(X_moons)
distances, _ = neigh.kneighbors(X_moons)
k_distances = np.sort(distances[:, k-1])
plt.figure(figsize=(10, 6))
plt.plot(k_distances)
plt.xlabel('样本排序')
plt.ylabel(f'第 {k} 近邻距离')
plt.title('k-distance 图')
plt.grid(alpha=0.3)
plt.axhline(y=0.3, color='r', linestyle='--', label='epsilon=0.3')
plt.legend()
plt.show()
DBSCAN 的优缺点
优点:
- 不需要预先指定 K
- 可以发现任意形状的簇
- 能够识别噪声点
- 对异常值不敏感
缺点:
- 对 eps 参数敏感
- 密度变化大的数据效果不好
- 高维数据距离度量失效(维度诅咒)
聚类评估指标
内部评估(不需要真实标签)
from sklearn.metrics import silhouette_score, davies_bouldin_score, calinski_harabasz_score
# 三种内部评估指标
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
sil = silhouette_score(X, labels)
dbi = davies_bouldin_score(X, labels)
ch = calinski_harabasz_score(X, labels)
print(f"轮廓系数 (Silhouette): {sil:.4f}")
print(f"Davies-Bouldin 指数: {dbi:.4f}")
print(f"Calinski-Harabasz 指数: {ch:.4f}")
| 指标 | 范围 | 越大越好? | 说明 |
|---|---|---|---|
| 轮廓系数 | [-1, 1] | ↑ | 同时衡量簇内凝聚度和簇间分离度 |
| Davies-Bouldin | [0, +∞) | ↓ | 簇之间相似度的平均值,越小越好 |
| Calinski-Harabasz | [0, +∞) | ↑ | 组间离散与组内离散的比值 |
外部评估(需要真实标签)
当有真实标签时,可以用调整兰德指数(ARI)和互信息(NMI)等指标。
from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score
# 有真实标签时
ari = adjusted_rand_score(y_true, labels)
nmi = normalized_mutual_info_score(y_true, labels)
print(f"调整兰德指数 (ARI): {ari:.4f}")
print(f"归一化互信息 (NMI): {nmi:.4f}")
实战:客户分群
让我们用真实的场景来实战聚类分析——对零售客户进行分群。
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
# 生成模拟客户数据
np.random.seed(42)
n_customers = 500
customer_data = pd.DataFrame({
'age': np.random.randint(18, 70, n_customers),
'annual_income': np.random.normal(60000, 25000, n_customers).astype(int),
'spending_score': np.random.randint(1, 100, n_customers),
'membership_years': np.random.uniform(0.5, 10, n_customers).round(1),
'avg_order_value': np.random.normal(150, 80, n_customers).astype(int),
'purchase_frequency': np.random.poisson(5, n_customers),
})
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(customer_data)
# 肘部法则确定最佳 K
inertias = []
for k in range(1, 11):
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X_scaled)
inertias.append(kmeans.inertia_)
plt.figure(figsize=(10, 5))
plt.plot(range(1, 11), inertias, 'bo-')
plt.xlabel('K')
plt.ylabel('惯性')
plt.title('客户数据肘部法则')
plt.grid(alpha=0.3)
plt.show()
# 选择 K=4(假设肘部在 4)
kmeans_final = KMeans(n_clusters=4, random_state=42, n_init=10)
customer_data['cluster'] = kmeans_final.fit_predict(X_scaled)
# 分析每个簇的特点
cluster_profile = customer_data.groupby('cluster').agg({
'age': 'mean',
'annual_income': 'mean',
'spending_score': 'mean',
'membership_years': 'mean',
'avg_order_value': 'mean',
'purchase_frequency': 'mean',
'cluster': 'count'
}).rename(columns={'cluster': 'count'})
print("客户分群画像:")
print(cluster_profile.round(1))
算法选择指南
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 数据量大、簇近似球形 | K-Means | 快、简单、可扩展 |
| 需要查看层次结构 | 层次聚类 | 树状图直观展示嵌套结构 |
| 簇形状不规则 | DBSCAN | 基于密度,发现任意形状 |
| 数据包含大量噪声 | DBSCAN | 自动识别噪声点 |
| 需要指定簇数量 | K-Means / 层次聚类 | 设定 n_clusters |
| 不知道簇数量 | DBSCAN / 层次聚类 | K-Means 需要预设 K |
总结
本文深入介绍了三种最常用的聚类算法:
- K-Means:简单高效,适合球形簇和大规模数据
- 层次聚类:树状图提供丰富的层次信息,适合探索性分析
- DBSCAN:基于密度,发现任意形状的簇,自动识别噪声
我们还学习了如何评估聚类质量、如何选择合适的参数和算法。
聚类分析是无监督学习的核心,它与分类、回归等监督学习方法共同构成了机器学习的基石。掌握聚类,你就拥有了在无标签数据中发现隐藏模式的能力。
下一篇文章我们将讨论特征工程——机器学习中"好特征胜过好模型"的关键环节。