前两篇文章我们学习了监督学习中的回归和分类算法。但真实世界中,大量数据是没有标签的。当数据没有标准答案时,我们如何从中发现规律?这就是无监督学习的用武之地,而聚类分析是无监督学习中最核心的任务之一。

什么是聚类分析?

聚类分析的目标是将数据划分为若干个组(簇),使得同一簇内的样本尽可能相似,不同簇的样本尽可能不同。聚类与分类的根本区别在于:分类有标签可学,聚类完全靠数据自身的结构

聚类的主要应用

  • 客户分群:将用户按消费行为分组,制定差异化营销策略
  • 图像压缩:将相似颜色聚为一类,减少颜色数量
  • 异常检测:远离所有簇的样本可能是异常值
  • 文档分类:自动将新闻文章按主题分组
  • 生物信息学:基因表达数据分析,发现功能相似的基因

K-Means 聚类

K-Means 是最经典、最常用的聚类算法,简单高效。

算法原理

K-Means 的迭代过程如下:

  1. 随机选择 K 个点作为初始簇中心
  2. 计算每个样本到 K 个中心的距离,分配到最近的中心
  3. 对每个簇,重新计算中心(簇内所有点的均值)
  4. 重复 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++ 是更聪明的初始化方法:

  1. 随机选择第一个中心点
  2. 对于每个样本点,计算它到最近已选中心的距离
  3. 以正比于距离平方的概率选择下一个中心
  4. 重复直到选够 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

总结

本文深入介绍了三种最常用的聚类算法:

  1. K-Means:简单高效,适合球形簇和大规模数据
  2. 层次聚类:树状图提供丰富的层次信息,适合探索性分析
  3. DBSCAN:基于密度,发现任意形状的簇,自动识别噪声

我们还学习了如何评估聚类质量、如何选择合适的参数和算法。

聚类分析是无监督学习的核心,它与分类、回归等监督学习方法共同构成了机器学习的基石。掌握聚类,你就拥有了在无标签数据中发现隐藏模式的能力。

下一篇文章我们将讨论特征工程——机器学习中"好特征胜过好模型"的关键环节。