聚类

聚类(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确保结果可复现
kmeans = KMeans(n_clusters=2, random_state=42, n_init=10)  # n_init=10避免初始中心选得太差

# 训练模型(无监督学习,不需要标签y)
kmeans.fit(X)

# 1. 查看每个用户的聚类标签(0和1只是类别代号,无大小意义!)
print("每个用户的聚类标签(0=年轻人,1=中年人):")
print(kmeans.labels_)  
# 固定输出:[0 0 0 1 1 1](因为random_state=42,标签顺序不会乱)
# 👉 关键说明:如果把random_state改成其他数,可能输出[1 1 1 0 0 0],但类别完全一样,只是代号互换

# 2. 查看两个聚类中心(= 每类用户的「平均年龄」和「平均消费金额」)
print("\n两个聚类中心([平均年龄, 平均月消费]):")
cluster_centers = kmeans.cluster_centers_
print("年轻人聚类中心:", cluster_centers[0].round(1))  # 保留1位小数,更直观
print("中年人聚类中心:", cluster_centers[1].round(1))
# 输出:
# 年轻人聚类中心: [19.  250. ] (平均19岁,月均消费250元)
# 中年人聚类中心: [36.3 283.3] (平均36.3岁,月均消费283.3元)

"""
KMeans 的第一步是 “随便选 K 个中心点”(比如 K=2 时选 2 个点),这个 “随便” 其实是计算机的 “伪随机”—— 如果不固定random_state,每次运行程序都会选不同的初始中心点;如果固定了random_state,每次运行都会选相同的初始中心点。
"""

⚙️ 常用参数解释:

参数 中文 说明
n_clusters=8 聚类数量 默认分 8 类,你通常要自己指定(比如 2、3、5)
random_state 随机种子 保证结果可复现(就像“固定随机数”)
init='k-means++' 初始化方法 默认用 k-means++(比随机选中心更稳)
max_iter=300 最大迭代次数 防止无限循环

✅ KMeans 的优缺点

优点 缺点
✔️ 简单、快速、易用 ❌ 必须提前知道 K 是多少(可用“肘部法则”或轮廓系数辅助)
✔️ 对球形簇效果很好 ❌ 对非球形簇(如月牙形)效果差
✔️ 可扩展到大数据(有优化版) ❌ 对异常值敏感(因为用“均值”)

📈 如何选择 K?—— 肘部法则(Elbow Method)

import matplotlib.pyplot as plt
from sklearn.cluster import KMeans

# 计算不同 K 值下的误差平方和(inertia)
inertias = []
K_range = range(1, 10)
for k in K_range:
    kmeans = KMeans(n_clusters=k)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)

# 画图
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('K (聚类数量)')
plt.ylabel('簇内误差平方和 (Inertia)')
plt.title('肘部法则')
plt.show()

👉 找“拐点”(elbow point)——之后下降变缓的地方,就是合适的 K。


✅ 总结:

KMeans = 自动分组工具
给一堆没标签的数据,它能帮你找出隐藏的类别结构,广泛用于:

  • 客户分群(高价值 vs 低活跃)
  • 图像压缩(把相近颜色合并)
  • 文档聚类(自动归类新闻主题)

当然可以!我们用大白话 + 图解思维 + 例子,彻底搞懂 轮廓系数(Silhouette Score) 是什么、为什么重要、怎么用。


轮廓系数(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 怎么算?

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt

# 生成示例数据(3个自然簇)
X, _ = make_blobs(n_samples=300, centers=3, cluster_std=0.6, random_state=42)

# 尝试不同 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)
    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,评估聚类效果

✅ 它就像给每个样本发一张“满意度调查表”:
“你对自己所在的群满意吗?和隔壁群比呢?”
最后取全班平均分!


可视化单个样本的轮廓图

轮廓图(Silhouette Plot) 是一种非常直观的可视化方法,能让你一眼看出:

  • 每个簇的内部紧密程度
  • 是否有分错的样本(轮廓系数为负)
  • 聚类数量 K 是否合适

下面我给你一个 完整、可运行的 Python 示例,并用手写数字数据集演示如何画出漂亮的轮廓图!


✅ 完整代码:可视化单个样本的轮廓图

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_samples, silhouette_score
from sklearn.preprocessing import StandardScaler

# ----------------------------
# 1. 加载数据
# ----------------------------
digits = load_digits()
X = digits.data
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# ----------------------------
# 2. 设置聚类数量 K(尝试 K=8, 9, 10, 11 看效果)
# ----------------------------
K = 10  # 你可以改成 8, 9, 11 对比

kmeans = KMeans(n_clusters=K, random_state=42)
cluster_labels = kmeans.fit_predict(X_scaled)

# ----------------------------
# 3. 计算轮廓系数(每个样本 + 整体)
# ----------------------------
silhouette_avg = silhouette_score(X_scaled, cluster_labels)
sample_silhouette_values = silhouette_samples(X_scaled, cluster_labels)

print(f"✅ K = {K} 时,平均轮廓系数 = {silhouette_avg:.3f}")

# ----------------------------
# 4. 画轮廓图
# ----------------------------
plt.figure(figsize=(10, 8))

y_lower = 10  # 起始 y 坐标(留白)
colors = plt.cm.tab10.colors  # 使用 10 种颜色(足够 K≤10)

for i in range(K):
    # 获取第 i 个簇的所有样本的轮廓系数
    ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]
    ith_cluster_silhouette_values.sort()  # 排序,让图形更整齐

    size_cluster_i = ith_cluster_silhouette_values.shape[0]
    y_upper = y_lower + size_cluster_i

    # 填充颜色区域
    color = colors[i % len(colors)]
    plt.fill_betweenx(
        np.arange(y_lower, y_upper),
        0,
        ith_cluster_silhouette_values,
        facecolor=color,
        edgecolor=color,
        alpha=0.7
    )

    # 在图上标注簇编号
    plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
    
    # 更新下一起始位置
    y_lower = y_upper + 10  # 每个簇之间留一点空隙

# 画平均轮廓系数的竖线
plt.axvline(x=silhouette_avg, color="red", linestyle="--", linewidth=2, label=f'平均轮廓系数 = {silhouette_avg:.3f}')

# 设置坐标轴
plt.xlim([-0.1, 1])
plt.ylim([0, y_lower])

plt.xlabel('轮廓系数值')
plt.ylabel('簇标签')
plt.title(f'K={K} 的轮廓图(Silhouette Plot)')
plt.legend(loc='upper right')
plt.tight_layout()
plt.show()

🔍 图形解读(关键!)

📌 X 轴:轮廓系数 s ∈ [-1, 1]

  • 越靠右越好(接近 1)
  • 红线:所有样本的平均轮廓系数

📌 Y 轴:每个簇的样本(按簇分组)

  • 每个彩色块 = 一个簇
  • 块的高度 = 该簇的样本数量
  • 块的宽度 = 该簇样本的轮廓系数分布

✅ 好的聚类应满足:

  1. 所有块都 > 0(没有负值)
  2. 各块宽度相近(说明各簇质量均衡)
  3. 红线靠右(平均分高)
  4. 各块高度差不多(说明没有特别小的“垃圾簇”)

❌ 坏的信号:

  • 某个块延伸到负值区 → 有样本被分错
  • 某个块非常窄 → 这个簇内部松散
  • 某个块特别矮 → 可能是噪声或 K 设多了

🖼️ 示例对比(想象图)

K=10(合理) K=12(可能过聚类)
所有块在 0.0~0.4 之间,无负值 有几个块很窄,甚至部分样本 s < 0
平均 s ≈ 0.13 平均 s 反而下降

💡 手写数字本身不是“球形簇”,所以轮廓系数不会很高(通常 0.1~0.2),但相对比较仍有效!


🧪 小实验:试试不同 K

K = 10 改成 K = 8K = 12,运行几次,观察:

  • 平均轮廓系数如何变化?
  • 是否出现负值区域?
  • 哪个 K 的图看起来最“整齐”?

你会发现:K=10 通常是最合理的(因为真实有 10 个数字)!


✅ 总结:轮廓图的作用

功能 说明
评估单个样本 看它是否被合理分配
诊断问题簇 找出松散或错误的簇
辅助选 K 选平均轮廓系数最高、且无负值的 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  阅读(1)  评论(0)    收藏  举报