聚类算法

本节定位
聚类是无监督学习中最常用的任务——在没有标签的情况下,自动把相似的数据分到一组。客户分群、文档归类、图像分割等场景都离不开聚类。
学习目标
- 掌握 K-Means 聚类的原理与实现
- 理解 K-Means++ 初 始化策略
- 了解层次聚类(凝聚与分裂)
- 掌握 DBSCAN 密度聚类
- 掌握 K 值选择方法与聚类评估指标
先说一个很重要的学习预期
这一节很容易让新人一开始发虚,因为它和前面的监督学习不一样:
- 没有标签
- 没有标准答案
- 看起来像“分出来了”,但又不知道这样分算不算好
更适合第一遍先学会的不是把所有聚类算法都背熟,而是先接受这件事:
聚类是在没有标签时,对数据结构提出一个可检验的假设。
只要这条线先立住,你就不会把聚类误会成“自动得到唯一真相”。
先建立一张地图
聚类这一节最容易让新人发虚的地方在于:
- 没有标签,不知道“学到了什么”
- 算法很多,不知道应该先学哪一个
- 画出来好像分组了,但不知道分得好不好
更稳的理解顺序是:
所以聚类不是“让机器自动分组”这么简单,它本质上是在做:
没有标签时,如何给数据找结构。
一、聚类的直觉
1.1 什么是聚类?
聚类 = 把"相似的"放在一起,把"不同的"分开。
| 应用场景 | 数据 | 聚类目标 |
|---|---|---|
| 客户分群 | 消费行为数据 | 找出高价值/低频/流失客户群 |
| 文档归类 | 文本向量 | 按主题自动分类 |
| 图像分割 | 像素颜色值 | 把图像分成前景/背景 |
| 基因分析 | 基因表达数据 | 找出功能相似的基因组 |
1.2 聚类和分类真正差在哪?
这两个词很像,但它们解决的是两种完全不同的问题:
- 分类:标签已知,目标是学会“怎么判”
- 聚类:标签未知,目标是发现“可能存在什么组”
所以第一次学聚类时,一定要接受一件事:
- 聚类结果不是唯一标准答案
- 它更像是一种“数据结构假设”
- 你需要用指标和业务解释去判断这个假设是否有价值
1.2.1 一个更适合新人的类比
你可以先把聚类想成:
- 第一次整理一大堆没有标签的杂物
你会先按“看起来像一类”的直觉去分:
- 常用的放一堆
- 不常用的放一堆
- 特别杂乱的单独拿出来
这时候你并不是在找唯一正确答案,
而是在找一种:
- 方便理解
- 方便后续行动
- 能被验证是否有用
的分组方式。
1.3 生成演示数据
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
# 生成 3 个簇的数据
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)
plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 1], s=30, alpha=0.7, color='gray')
plt.title('未标注的数据——你能看出几个群组?')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.grid(True, alpha=0.3)
plt.show()
二、K-Means 聚类
2.1 算法原理
K-Means 是最经典的聚类算法,步骤非常简单:
2.2 从零实现 K-Means
def kmeans_simple(X, k, max_iters=100):
"""简易 K-Means 实现"""
np.random.seed(42)
# 1. 随机初始化质心
idx = np.random.choice(len(X), k, replace=False)
centroids = X[idx].copy()
for iteration in range(max_iters):
# 2. 分配每个点到最近的质心
distances = np.sqrt(((X[:, np.newaxis] - centroids) ** 2).sum(axis=2))
labels = distances.argmin(axis=1)
# 3. 更新质心
new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(k)])
# 检查收敛
if np.allclose(centroids, new_centroids):
print(f"在第 {iteration+1} 轮收敛")
break
centroids = new_centroids
return labels, centroids
# 运行
labels, centroids = kmeans_simple(X, k=3)
# 可视化
plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', s=30, alpha=0.7)
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200,
edgecolors='black', linewidth=2, label='质心')
plt.title('K-Means 聚类结果(手动实现)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
2.3 用 sklearn 实 现
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)
print(f"簇标签: {np.unique(kmeans.labels_)}")
print(f"质心:\n{kmeans.cluster_centers_}")
print(f"总惯性(SSE): {kmeans.inertia_:.2f}")
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 聚类结果
axes[0].scatter(X[:, 0], X[:, 1], c=kmeans.labels_, cmap='viridis', s=30, alpha=0.7)
axes[0].scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
c='red', marker='X', s=200, edgecolors='black', linewidth=2)
axes[0].set_title('K-Means 聚类结果')
# 与真实标签对比
axes[1].scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', s=30, alpha=0.7)
axes[1].set_title('真实标签(用于对比)')
for ax in axes:
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
2.4 K-Means 的迭代过程可视化
fig, axes = plt.subplots(2, 3, figsize=(15, 9))
np.random.seed(42)
idx = np.random.choice(len(X), 3, replace=False)
centroids = X[idx].copy()
for i, ax in enumerate(axes.ravel()):
# 分配
distances = np.sqrt(((X[:, np.newaxis] - centroids) ** 2).sum(axis=2))
labels = distances.argmin(axis=1)
ax.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', s=20, alpha=0.6)
ax.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200,
edgecolors='black', linewidth=2)
ax.set_title(f'第 {i+1} 轮迭代')
ax.grid(True, alpha=0.3)
# 更新质心
centroids = np.array([X[labels == j].mean(axis=0) for j in range(3)])
plt.suptitle('K-Means 迭代过程', fontsize=13)
plt.tight_layout()
plt.show()