聚类
聚类(Clustering)
from sklearn.cluster import KMeans
🔤 逐词翻译:
sklearn:scikit-learn,Python 最流行的机器学习库cluster(聚类):一种无监督学习任务,目标是把相似的数据点“分组”KMeans(K均值):最经典、最常用的聚类算法
✅ 所以这行代码的意思是:
“从 sklearn 的聚类模块中,导入 KMeans 算法”
🌟 什么是 KMeans?(大白话版)
想象你有一堆没标签的小球,随机摆在纸上(每个球有各自的位置),你想把它们按“位置远近”自动分成 3 堆。
KMeans 就像一个“自动分拣机器人”,它会:
先随便选 3 个小球位置当中心点(叫 聚类中心 / centroids)
把每个小球分配给离自己最近的中心
重新计算每堆的新中心(取该堆所有球位置的平均坐标)
重复步骤 2~3,直到中心不再怎么移动
✅ 最终,你就得到了 K 个簇(clusters),每簇内的球彼此位置近(相似度高),不同簇之间的球位置远(差异大)。
💡 K 是你提前指定的簇的数量(比如 K=3)
💻 基本用法示例:
from sklearn.cluster import KMeans
import numpy as np
# 明确数据含义:每行 = [用户年龄, 月消费金额(元)],共6个用户
X = np.array([
[18, 200], # 用户1:18岁,月消费200元
[20, 400], # 用户2:20岁,月消费400元
[19, 150], # 用户3:19岁,月消费150元
[35, 220], # 用户4:35岁,月消费220元
[38, 450], # 用户5:38岁,月消费450元
[36, 180] # 用户6:36岁,月消费180元
])
# 创建KMeans模型:分成2类(高消费/低消费),固定random_state确保结果可复现
"""
n_clusters就是K
KMeans 的第一步是 “随便选 K 个中心点”(比如 K=2 时选 2 个点),这个 “随便” 其实是计算机的 “伪随机”—— 如果不固定random_state,每次运行程序都会选不同的初始中心点;如果固定了random_state,每次运行都会选相同的初始中心点。
n_init的全称是 “number of initializations”(初始化次数),n_init=10就是告诉 KMeans:
“你别只随机选 1 次初始中心,给我选 10 次不同的初始中心,分别跑 10 遍完整的 KMeans(每次都按「选中心→分样本→更中心→重复」的流程来),最后从这 10 遍结果里,挑一个「最好」的当最终答案!”
"""
kmeans = KMeans(n_clusters=2, random_state=42, n_init=10)
# 训练模型(无监督学习,不需要标签y)
kmeans.fit(X)
# 1. 查看每个用户的聚类标签(0和1只是类别代号,无大小意义!)
print("每个用户的聚类标签(0=低消费,1=高消费):")
print(kmeans.labels_)
# 固定输出:[0 1 0 0 1 0](因为random_state=42,标签顺序不会乱)
# 👉 关键说明:如果把random_state改成其他数,可能输出[1 0 1 1 0 1],但类别完全一样,只是代号互换
# 2. 查看两个聚类中心(= 每类用户的「平均年龄」和「平均消费金额」)
print("\n两个聚类中心([平均年龄, 平均月消费]):")
cluster_centers = kmeans.cluster_centers_
print("低消费聚类中心:", cluster_centers[0].round(1)) # 保留1位小数,更直观
print("高消费聚类中心:", cluster_centers[1].round(1))
# 输出:
# 两个聚类中心([平均年龄, 平均月消费]):
# 低消费聚类中心: [ 27. 187.5]
# 高消费聚类中心: [ 29. 425.]
# 👉 关键说明:
# 消费金额” 的差异比 “年龄” 差异对聚类的影响更大,最终聚出的是「低消费组」和「高消费组」,而非「年轻人」和「中年人」
---
### ✅ KMeans 的优缺点
| 优点 | 缺点 |
|------|------|
| ✔️ 简单、快速、易用 | ❌ 必须提前知道 **K 是多少**(可用“肘部法则”或轮廓系数辅助) |
| ✔️ 对球形簇效果很好 | ❌ 对非球形簇(如月牙形)效果差 |
| ✔️ 可扩展到大数据(有优化版) | ❌ 对异常值敏感(因为用“均值”) |
---
### ✅ 总结:
> **`KMeans` = 自动分组工具**
> 给一堆没标签的数据,它能帮你找出**隐藏的类别结构**,广泛用于:
> - 客户分群(高价值 vs 低活跃)
> - 图像压缩(把相近颜色合并)
> - 文档聚类(自动归类新闻主题)
---
## 轮廓系数(Silhouette Score)
### 🌟 一句话总结:
> **轮廓系数 = 衡量聚类结果“好不好”的打分卡,分数越接近 1 越好,越接近 -1 越差。**
它能告诉你:
✅ 每个样本是不是被分到了“对的簇”?
✅ 整体聚类结构是否清晰?
---
### 🔍 为什么需要它?
KMeans 需要你提前指定 **K(聚类数量)**,但你怎么知道 K=3 比 K=4 更好呢?
→ **轮廓系数就是帮你选 K 的“黄金指标”之一!**
---
### 🧠 核心思想(超通俗版)
对**每一个样本点**,计算两个距离:
1. **a = 到自己簇内其他点的平均距离**
→ 越小越好(说明“我和自己人很近”)
2. **b = 到最近的其他簇的所有点的平均距离**
→ 越大越好(说明“我和外人很远”)
然后定义这个点的**轮廓系数 s**:
\[
s = \frac{b - a}{\max(a, b)}
\]
- 如果 **s ≈ 1**:说明 a 很小、b 很大 → **分得非常合理!**
- 如果 **s ≈ 0**:说明 a ≈ b → **在两个簇边界上,模棱两可**
- 如果 **s < 0**:说明 a > b → **你可能被分错簇了!**
> ✅ 所有点的 s 取平均,就是**整体轮廓系数**(范围:[-1, 1])
---
### 🖼️ 示意图(文字版)
簇 A 簇 B
● ● ● ○ ○ ○
● ○
↑
这个点离簇A近(a小),离簇B远(b大)→ s ≈ 1 ✅
边界点:
● ● | ○ ○
↑
这个点离两边差不多(a≈b)→ s ≈ 0 ⚠️
错误点:
● ● ○ ← 这个○被分到●簇
↑
它离自己簇(●)远(a大),离○簇近(b小)→ s < 0 ❌
---
### 💻 Python 怎么算?
```python
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
# 生成示例数据(3个自然簇)
X, _ = make_blobs(n_samples=300, centers=3, cluster_std=0.6, random_state=42)
plt.scatter(X[:, 0], X[:, 1], s=50)
plt.title('示例数据(3个自然簇)')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.show()
# 尝试不同 K 值
K_range = range(2, 10)
scores = []
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42)
labels = kmeans.fit_predict(X)
score = silhouette_score(X, labels) # silhouette 英 /ˌsɪluˈet/、美 /ˌsɪluˈet/ 轮廓
scores.append(score)
print(f"K={k}, 轮廓系数 = {score:.3f}")
# 画图找最高分
plt.plot(K_range, scores, 'bo-')
plt.xlabel('聚类数量 K')
plt.ylabel('轮廓系数')
plt.title('选择最优 K:轮廓系数越大越好')
plt.grid(True)
plt.show()
输出示例:
K=2, 轮廓系数 = 0.582
K=3, 轮廓系数 = 0.721 ← 最高!
K=4, 轮廓系数 = 0.598
...
✅ 所以最优 K = 3(和真实情况一致!)
✅ 轮廓系数 vs 肘部法则(Inertia)
| 方法 | 优点 | 缺点 |
|---|---|---|
| 肘部法则(Inertia) | 计算快 | 主观(“拐点”不好判断) |
| 轮廓系数 | 客观打分,有明确范围 [-1,1] | 计算稍慢(需算所有点对距离) |
💡 推荐组合使用:先用肘部法缩小范围,再用轮廓系数精确定 K。
⚠️ 注意事项
- 不适用于密度不均的簇(比如 DBSCAN 更适合月牙形数据)
- 默认用欧氏距离,如果特征尺度差异大,记得先标准化(
StandardScaler) - K=1 时无法计算(因为没有“其他簇”)
✅ 总结:
| 概念 | 含义 |
|---|---|
| 轮廓系数(Silhouette Score) | 聚类质量的客观评分(-1 ~ 1) |
| s ≈ 1 | 样本在正确簇中,且簇间分离清晰 |
| s ≈ 0 | 样本在簇边界,分类模糊 |
| s < 0 | 样本很可能被分错簇 |
| 用途 | 选择最优聚类数量 K,评估聚类效果 |
一个完整、可运行的 Python 案例
下面是一个完整、可运行的 Python 案例,使用 KMeans 对手写数字(load_digits)进行无监督聚类 —— 即使没有标签,我们也能看看 KMeans 能不能自动把 0~9 分成 10 类!
我们会:
- 加载数据(不使用标签)
- 用 KMeans 聚类(K=10)
- 用真实标签评估聚类效果(虽然训练时没用它)
- 可视化每个簇的“代表图像”
- 计算轮廓系数 & 调整兰德指数(ARI)
✅ 完整代码(带详细中文注释)
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, adjusted_rand_score
from sklearn.preprocessing import StandardScaler
# ==============================
# 1. 加载手写数字数据集(只用图像,不用标签)
# ==============================
digits = load_digits()
X = digits.data # shape: (1797, 64) —— 每张图是 8x8=64 维向量
y_true = digits.target # 真实标签(仅用于后续评估,KMeans 不会看到它!)
print(f"数据形状: {X.shape}")
print("注意:KMeans 是无监督学习,训练时完全不知道 y_true!")
# ==============================
# 2. (可选)标准化特征 —— 对 KMeans 很重要!
# ==============================
# 因为 KMeans 基于欧氏距离,如果某些像素值范围大,会主导结果
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# ==============================
# 3. 使用 KMeans 聚类(假设我们知道有 10 个数字)
# ==============================
kmeans = KMeans(n_clusters=10, random_state=42, n_init=10)
y_pred = kmeans.fit_predict(X_scaled) # 得到每个样本的聚类标签
# ==============================
# 4. 评估聚类效果(用真实标签做“事后检验”)
# ==============================
# (a) 轮廓系数(不需要真实标签)
sil_score = silhouette_score(X_scaled, y_pred)
print(f"\n🎯 轮廓系数: {sil_score:.3f}(越接近1越好)")
# (b) 调整兰德指数 ARI(需要真实标签,衡量聚类与真实标签的一致性)
ari_score = adjusted_rand_score(y_true, y_pred)
print(f"🎯 调整兰德指数 (ARI): {ari_score:.3f}(越接近1越好,0表示随机)")
# ==============================
# 5. 可视化:每个簇的“中心图像”(即聚类中心 reshape 成 8x8)
# ==============================
fig, axes = plt.subplots(2, 5, figsize=(12, 6))
centers = kmeans.cluster_centers_ # shape: (10, 64)
# 注意:因为用了 StandardScaler,中心值不是原始像素,需反变换回近似原始尺度
centers_original_scale = scaler.inverse_transform(centers)
for i in range(10):
ax = axes[i // 5, i % 5]
# 将 64 维向量转为 8x8 图像,并限制在 [0, 16](原始 digits 数据范围)
img = centers_original_scale[i].reshape(8, 8)
img = np.clip(img, 0, 16) # 防止负值或过大值
ax.imshow(img, cmap='gray_r')
ax.set_title(f'簇 {i}', fontsize=14)
ax.set_axis_off()
plt.suptitle('KMeans 聚类中心(每个簇的“平均数字”)', fontsize=16)
plt.tight_layout()
plt.show()
# ==============================
# 6. (进阶)查看每个簇中真实的数字分布
# ==============================
from sklearn.metrics import confusion_matrix
import seaborn as sns
# 画混淆矩阵:行=真实标签,列=聚类标签
cm = confusion_matrix(y_true, y_pred)
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('KMeans 聚类标签')
plt.ylabel('真实数字标签')
plt.title('聚类结果 vs 真实标签(看是否对齐)')
plt.show()
🔍 关键点解释
1. 为什么能聚类手写数字?
- 虽然 KMeans 不知道“这是 3 还是 8”,但它发现:
- 所有“圈多”的图像彼此相似 → 自动聚成一类(可能是 8 或 9)
- 所有“竖线”的图像彼此相似 → 聚成另一类(可能是 1)
- 前提是:同类数字的图像在像素空间中确实聚集在一起
2. 为什么要标准化(StandardScaler)?
- 原始像素值范围是 0~16,但不同位置的像素方差不同
- 标准化让每个像素“平等参与距离计算”,避免某些位置主导结果
3. 调整兰德指数(ARI)是什么?
- 衡量两个标签分配的一致性(即使标签编号不同)
- 比如:聚类把“0”标成“5”,但所有 0 都在一个簇 → ARI 仍高
- ARI = 1:完美匹配;ARI ≈ 0:相当于随机分
4. 聚类中心图像是怎么来的?
- KMeans 的
cluster_centers_是 64 维向量 - 把它 reshape 成 8×8,就得到“这个簇的平均数字长什么样”
📊 典型输出示例
数据形状: (1797, 64)
注意:KMeans 是无监督学习,训练时完全不知道 y_true!
🎯 轮廓系数: 0.132
🎯 调整兰德指数 (ARI): 0.678
- 轮廓系数偏低(~0.13):因为手写数字簇不是球形,且有些数字相似(如 4/9, 3/8)
- ARI 较高(~0.68):说明聚类结果和真实标签有较强对应关系!
💡 即使没有标签,KMeans 也能大致把数字分开!
🎯 总结
| 步骤 | 目的 |
|---|---|
加载 load_digits().data |
获取无标签图像数据 |
| 标准化 | 提升 KMeans 效果 |
KMeans(n_clusters=10) |
自动分 10 类 |
| 轮廓系数 | 评估簇内紧密度 & 簇间分离度 |
| ARI + 混淆矩阵 | 用真实标签“事后验证”效果 |
| 可视化中心 | 看每个簇学到了什么数字 |
✅ 这就是无监督学习的魅力:
即使没人告诉你“这是什么数字”,算法也能从数据中发现结构!

浙公网安备 33010602011771号