聚类

聚类(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。


⚠️ 注意事项

  1. 不适用于密度不均的簇(比如 DBSCAN 更适合月牙形数据)
  2. 默认用欧氏距离,如果特征尺度差异大,记得先标准化(StandardScaler
  3. K=1 时无法计算(因为没有“其他簇”)

✅ 总结:

概念 含义
轮廓系数(Silhouette Score) 聚类质量的客观评分(-1 ~ 1)
s ≈ 1 样本在正确簇中,且簇间分离清晰
s ≈ 0 样本在簇边界,分类模糊
s < 0 样本很可能被分错簇
用途 选择最优聚类数量 K,评估聚类效果

一个完整、可运行的 Python 案例

下面是一个完整、可运行的 Python 案例,使用 KMeans 对手写数字(load_digits)进行无监督聚类 —— 即使没有标签,我们也能看看 KMeans 能不能自动把 0~9 分成 10 类!

我们会:

  1. 加载数据(不使用标签)
  2. 用 KMeans 聚类(K=10)
  3. 用真实标签评估聚类效果(虽然训练时没用它)
  4. 可视化每个簇的“代表图像”
  5. 计算轮廓系数 & 调整兰德指数(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 + 混淆矩阵 用真实标签“事后验证”效果
可视化中心 看每个簇学到了什么数字

这就是无监督学习的魅力
即使没人告诉你“这是什么数字”,算法也能从数据中发现结构!


posted @ 2025-12-01 09:58  wangya216  阅读(210)  评论(0)    收藏  举报