K-Means 聚类实战:Python 手写数字识别与 3 维可视化

当我们需要从无标签数据中发现隐藏结构时,K-Means 聚类算法就像一位善于发现群体共性的侦探。今天我们将用 MNIST 手写数字数据集,展示如何从原始像素到三维空间中的清晰分类。这个实战项目不仅能帮你理解聚类算法的核心思想,还能掌握数据降维与可视化的关键技巧。

1. 环境准备与数据加载

在开始之前,确保你的 Python 环境已安装以下关键库:

import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline

加载 MNIST 数据集并做基础预处理:

mnist = fetch_openml('mnist_784', version=1, as_frame=False)
X = mnist.data / 255.0  # 像素值归一化
y = mnist.target.astype(int)

print(f"数据集形状: {X.shape}")  # (70000, 784)
print(f"标签示例: {y[:10]}")  # [5 0 4 ... 4 5 6]

注意:MNIST 每张图片是 28x28 像素,展平后成为 784 维向量。归一化到 [0,1] 区间有助于提升聚类效果。

2. K-Means 模型训练与 K 值选择

确定最佳聚类数量是 K-Means 成功的关键。我们使用肘部法则和轮廓系数双重验证:

# 肘部法则计算不同K值的惯性
inertias = []
silhouettes = []
K_range = range(5, 15)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)
    
    # 轮廓系数评估(抽样计算避免内存问题)
    sample_idx = np.random.choice(len(X), 5000, replace=False)
    silhouette = silhouette_score(X[sample_idx], kmeans.predict(X[sample_idx]))
    silhouettes.append(silhouette)

# 绘制双指标曲线
plt.figure(figsize=(12,4))
plt.subplot(121)
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('K值'), plt.ylabel('惯性值')
plt.title('肘部法则')

plt.subplot(122)
plt.plot(K_range, silhouettes, 'r*-')
plt.xlabel('K值'), plt.ylabel('轮廓系数')
plt.title('轮廓分析')
plt.tight_layout()

典型输出会显示在 K=10 附近出现拐点(对应 10 个数字类别),同时轮廓系数在 K=10 时达到峰值。确定 K=10 后训练最终模型:

optimal_k = 10
kmeans = KMeans(n_clusters=optimal_k, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(X)

3. 高维数据可视化技巧

784 维数据难以直接观察,我们使用 PCA 进行降维:

pca = PCA(n_components=3)
X_3d = pca.fit_transform(X)

# 创建3D散点图
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111, projection='3d')

scatter = ax.scatter(X_3d[:,0], X_3d[:,1], X_3d[:,2], 
                    c=cluster_labels, cmap='tab10', 
                    alpha=0.5, s=5)

# 添加颜色条和图例
plt.colorbar(scatter, ax=ax, label='聚类标签')
ax.set_xlabel('PCA 1'), ax.set_ylabel('PCA 2'), ax.set_zlabel('PCA 3')
plt.title('MNIST 数据的3D聚类可视化')

为了更直观理解聚类效果,我们可以查看每个簇的中心图像:

fig, axes = plt.subplots(2, 5, figsize=(12,5))
centers = kmeans.cluster_centers_

for i, ax in enumerate(axes.flat):
    ax.imshow(centers[i].reshape(28,28), cmap='gray')
    ax.set_title(f'簇 {i} 中心')
    ax.axis('off')

4. 聚类结果评估与调优

评估聚类质量需要综合多个指标:

评估指标 计算公式 理想值
惯性 (Inertia) Σ(样本到其簇中心的距离平方) 越小越好
轮廓系数 (b-a)/max(a,b) (a:簇内距离, b:最近簇距离) [-1,1] 接近1最佳
纯度 (Purity) Σ(每个簇中最多数类别的样本数)/总样本数 接近1最佳

计算这些指标:

from sklearn.metrics import confusion_matrix

# 计算纯度
conf_mat = confusion_matrix(y, cluster_labels)
purity = np.sum(np.amax(conf_mat, axis=0)) / len(y)

print(f"惯性值: {kmeans.inertia_:.2f}")
print(f"轮廓系数: {silhouette_score(X, cluster_labels, sample_size=5000):.3f}")
print(f"聚类纯度: {purity:.3f}")

如果发现某些数字被错误聚类(如4和9混淆),可以尝试以下优化策略:

  1. 特征工程 :使用方向梯度直方图(HOG)替代原始像素
  2. 算法改进 :采用K-Means++初始化或MiniBatch K-Means
  3. 后处理 :合并相似簇或拆分异质簇

5. 完整项目集成与扩展

将整个流程封装为可复用的Jupyter notebook,添加以下实用功能:

def plot_cluster_samples(X, labels, n_samples=5):
    """展示每个簇的典型样本"""
    unique_labels = np.unique(labels)
    plt.figure(figsize=(12,8))
    
    for i, label in enumerate(unique_labels):
        samples = X[labels == label][:n_samples]
        for j, sample in enumerate(samples):
            plt.subplot(len(unique_labels), n_samples, i*n_samples + j + 1)
            plt.imshow(sample.reshape(28,28), cmap='gray')
            plt.axis('off')
            if j == 0:
                plt.ylabel(f'簇 {label}')

# 调用示例
plot_cluster_samples(X, cluster_labels)

对于想要进一步探索的开发者,可以考虑以下扩展方向:

  • 结合t-SNE进行非线性降维可视化
  • 实现二分K-Means算法解决局部最优问题
  • 将聚类结果作为特征输入到监督学习模型

这个项目展示了如何将理论算法转化为实际解决方案。当你在三维空间中看到那些逐渐分离的数字簇时,就能直观感受到无监督学习发现数据内在结构的魔力。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐