机器学习之K-Means聚类算法详解

摘要

K-Means是机器学习中最经典、最广泛使用的聚类算法之一,属于无监督学习范畴。其核心思想是通过迭代优化,将n个数据点划分为k个簇,使得每个簇内的数据点尽可能紧凑相似,而不同簇之间的数据点尽可能远离。本文将从聚类基础概念出发,深入讲解K-Means的算法原理、K值选择策略、初始化方法优化,并结合scikit-learn提供完整的Python实战代码。读者通过本文可以全面掌握K-Means的理论基础与工程实践技巧。

关键词:K-Means聚类、无监督学习、机器学习、scikit-learn、肘部法则、轮廓系数


一、聚类基础

1.1 无监督学习概念

无监督学习是机器学习的一个重要分支,与监督学习不同,无监督学习不依赖带标签的训练数据。在无监督学习中,算法需要从无标签的数据集中发现隐藏的模式和结构。常见无监督学习任务包括聚类(Clustering)、降维(Dimensionality Reduction)和异常检测(Anomaly Detection)。

聚类的目标是将数据集中的样本划分为若干个组(簇),使得同一个簇内的样本相似度较高,不同簇之间的样本相似度较低。聚类算法本身不知道数据的真实标签,只是根据数据特征自动发现分组结构。

1.2 聚类与分类的区别

对比维度 聚类(无监督) 分类(监督)
训练数据 无标签 有标签
学习目标 发现数据内在结构 预测未知样本类别
输出结果 簇标签(簇编号) 预定义类别标签
评估方式 内部指标(如SSE) 准确率、召回率等
典型算法 K-Means、DBSCAN、层次聚类 SVM、随机森林、神经网络

1.3 聚类评估指标

聚类算法的评估通常使用内部评估指标,以下是几个常用指标:

簇内平方和(Within-Cluster Sum of Squares, SSE):也称为惯性(Inertia),衡量每个簇内数据点到簇中心的距离平方和,SSE越小表示簇越紧凑。

复制代码
# 手动计算SSE的示例
import numpy as np
​
def calculate_sse(X, labels, centers):
    """
    计算簇内平方和(SSE)
    
    参数:
        X: 数据集,形状为 (n_samples, n_features)
        labels: 每个样本所属簇的标签,形状为 (n_samples,)
        centers: 簇中心,形状为 (n_clusters, n_features)
    
    返回:
        sse: 簇内平方和
    """
    sse = 0
    for i in range(len(centers)):
        # 获取属于第i个簇的所有样本
        cluster_points = X[labels == i]
        if len(cluster_points) > 0:
            # 计算该簇内所有点到簇中心的距离平方和
            sse += np.sum((cluster_points - centers[i]) ** 2)
    return sse
​
# 示例使用
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
labels = np.array([0, 0, 0, 1, 1, 1])
centers = np.array([[1, 2], [10, 2]])
sse = calculate_sse(X, labels, centers)
print(f"SSE = {sse}")  # 输出应为 0

轮廓系数(Silhouette Score):取值范围[-1, 1],值越大表示聚类效果越好。对于单个样本,轮廓系数定义为:

s = (b - a) / max(a, b)

其中a是样本与同簇其他样本的平均距离,b是样本与最近邻簇内样本的平均距离。

复制代码
from sklearn.metrics import silhouette_score
import numpy as np
​
# 生成示例数据
from sklearn.datasets import make_blobs
X, _ = make_blobs(n_samples=200, centers=3, random_state=42)
​
# 计算不同K值的轮廓系数
for k in [2, 3, 4, 5]:
    from sklearn.cluster import KMeans
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = kmeans.fit_predict(X)
    score = silhouette_score(X, labels)
    print(f"K={k}, 轮廓系数={score:.4f}")

调整兰德指数(Adjusted Rand Index, ARI):当有真实标签时使用,衡量聚类结果与真实标签的匹配程度,取值范围[-1, 1],值越大越好。

复制代码
from sklearn.metrics import adjusted_rand_score
​
# 假设我们有真实标签和预测标签
y_true = np.array([0, 0, 1, 1, 2, 2])
y_pred = np.array([0, 0, 1, 1, 1, 2])
​
ari = adjusted_rand_score(y_true, y_pred)
print(f"调整兰德指数 (ARI) = {ari:.4f}")

二、K-Means算法原理

2.1 算法概述

K-Means算法由Stuart Lloyd于1957年提出,是一种基于距离的聚类算法。其核心思想是:将数据集划分为K个簇,每个簇由一个中心点(质心)代表,样本被分配到距离最近的中心点所在的簇,通过迭代优化使得所有样本到其所属簇中心的距离平方和最小。

2.2 算法步骤

K-Means算法的完整流程如下:

步骤1 - 初始化(Initialization):随机选择K个数据点作为初始簇中心,或者使用K-Means++方法进行更智能的初始化。

步骤2 - 分配(Assignment):将每个数据点分配给距离最近的簇中心,形成K个簇。

步骤3 - 更新(Update):重新计算每个簇的中心,即该簇内所有数据点的均值。

步骤4 - 迭代(Iteration):重复步骤2和步骤3,直到满足收敛条件(通常是中心点不再发生变化,或达到最大迭代次数,或目标函数变化小于阈值)。

步骤5 - 输出(Output):返回K个簇的标签和最终的簇中心。

复制代码
# K-Means算法手动实现(帮助理解原理)
import numpy as np
​
def kmeans_manual(X, k, max_iter=300, tol=1e-4):
    """
    K-Means算法手动实现
    
    参数:
        X: 数据集,形状为 (n_samples, n_features)
        k: 簇的数量
        max_iter: 最大迭代次数
        tol: 收敛阈值(中心点变化小于此值时停止)
    
    返回:
        labels: 每个样本的簇标签
        centers: 簇中心
        n_iter: 实际迭代次数
    """
    n_samples, n_features = X.shape
    
    # ============ 步骤1: 初始化 ============
    # 随机选择k个样本作为初始中心
    np.random.seed(42)
    idx = np.random.choice(n_samples, k, replace=False)
    centers = X[idx].copy()
    
    for iteration in range(max_iter):
        # ============ 步骤2: 分配 ============
        # 计算每个样本到各中心的欧氏距离
        distances = np.zeros((n_samples, k))
        for i in range(k):
            distances[:, i] = np.sqrt(np.sum((X - centers[i]) ** 2, axis=1))
        
        # 将每个样本分配给距离最近的中心
        labels = np.argmin(distances, axis=1)
        
        # ============ 步骤3: 更新 ============
        new_centers = np.zeros((k, n_features))
        for i in range(k):
            cluster_points = X[labels == i]
            if len(cluster_points) > 0:
                new_centers[i] = cluster_points.mean(axis=0)
            else:
                # 如果某个簇为空,保留原中心
                new_centers[i] = centers[i]
        
        # ============ 步骤4: 收敛检查 ============
        center_shift = np.sqrt(np.sum((new_centers - centers) ** 2))
        centers = new_centers
        
        if center_shift < tol:
            print(f"在第 {iteration + 1} 次迭代后收敛")
            break
    
    return labels, centers, iteration + 1
​
# 测试手动实现
np.random.seed(42)
X = np.vstack([
    np.random.randn(50, 2) + [0, 0],
    np.random.randn(50, 2) + [5, 5],
    np.random.randn(50, 2) + [0, 5]
])
​
labels, centers, n_iter = kmeans_manual(X, k=3)
print(f"迭代次数: {n_iter}")
print(f"簇中心:\n{centers}")
print(f"各簇样本数: {np.bincount(labels)}")

2.3 目标函数

K-Means的优化目标是最小化簇内平方和(Sum of Squared Errors, SSE),也称为惯性(Inertia)或失真度(Distortion):

J = Σ(i=1 to k) Σ(x ∈ C_i) ||x - μ_i||²

其中:

  • k 是簇的数量

  • C_i 是第i个簇

  • μ_i 是第i个簇的中心点

  • ||x - μi||² 是样本x到簇中心μi的欧氏距离平方

这个目标函数是非凸的,可能收敛到局部最优解而非全局最优解。这也是K-Means对初始化敏感的原因。

2.4 收敛条件

K-Means算法的收敛条件通常有以下几种:

  1. 中心点不再变化:所有簇中心的位置在连续两次迭代之间的变化小于阈值ε

  2. 达到最大迭代次数:防止无限循环

  3. 目标函数变化小于阈值:SSE的变化量小于预设阈值

  4. 样本分配不再变化:所有样本的簇标签在连续两次迭代中保持不变

复制代码
from sklearn.cluster import KMeans
import numpy as np
​
# 生成测试数据
np.random.seed(42)
X = np.vstack([
    np.random.randn(100, 2) + [0, 0],
    np.random.randn(100, 2) + [5, 5],
    np.random.randn(100, 2) + [0, 5]
])
​
# 使用sklearn的KMeans,设置不同的收敛条件
kmeans = KMeans(
    n_clusters=3,
    init='k-means++',      # 使用K-Means++初始化
    n_init=10,             # 运行10次,选择最优结果
    max_iter=300,          # 最大迭代次数
    tol=1e-4,              # 收敛阈值
    random_state=42
)
​
kmeans.fit(X)
print(f"收敛迭代次数: {kmeans.n_iter_}")
print(f"最终SSE (Inertia): {kmeans.inertia_:.4f}")
print(f"簇中心:\n{kmeans.cluster_centers_}")
print(f"各簇样本数: {np.bincount(kmeans.labels_)}")

三、K值选择

K值的选择是K-Means算法应用中最为关键的问题之一。K值选择过小会导致欠拟合,无法捕捉数据的真实结构;K值过大会导致过拟合,将本来应该属于同一簇的数据强行分开。

3.1 肘部法则(Elbow Method)

肘部法则是最常用的K值选择方法。其核心思想是:随着K值增大,SSE会逐渐减小(因为簇越多,每个簇越小,越紧凑);当K达到真实簇数时,继续增大会导致SSE下降速度明显变缓,在K-SSE图中形成一个"肘点"。

复制代码
import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 生成带有3个真实簇的数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
​
# 计算不同K值的SSE
k_range = range(1, 11)
sse_values = []
​
for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    sse_values.append(kmeans.inertia_)
​
# 绘制肘部法则图
plt.figure(figsize=(10, 6))
plt.plot(k_range, sse_values, 'bo-', linewidth=2, markersize=8)
plt.xlabel('簇数量 (K)', fontsize=12)
plt.ylabel('簇内平方和 (SSE / Inertia)', fontsize=12)
plt.title('肘部法则确定最优K值', fontsize=14)
plt.xticks(k_range)
plt.grid(True, alpha=0.3)
​
# 标注肘点(通常是3)
plt.axvline(x=3, color='r', linestyle='--', alpha=0.7, label='最优K值=3')
plt.legend()
​
plt.tight_layout()
plt.savefig('elbow_method.png', dpi=150)
plt.show()
​
# 打印具体数值
print("K值选择参考:")
print("-" * 30)
for k, sse in zip(k_range, sse_values):
    print(f"K={k:2d}: SSE={sse:8.2f}")

3.2 轮廓系数法(Silhouette Score)

轮廓系数综合考虑了簇内紧密度和簇间分离度,取值范围[-1, 1],值越大表示聚类效果越好。我们可以遍历不同的K值,选择轮廓系数最大的K。

复制代码
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
​
# 生成测试数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
​
# 计算不同K值的轮廓系数
k_range = range(2, 11)
silhouette_scores = []
​
for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = kmeans.fit_predict(X)
    score = silhouette_score(X, labels)
    silhouette_scores.append(score)
    print(f"K={k}, 轮廓系数={score:.4f}")
​
# 找到最优K值
optimal_k = k_range[np.argmax(silhouette_scores)]
print(f"\n根据轮廓系数,最优K值为: {optimal_k}")
​
# 绘制轮廓系数图
plt.figure(figsize=(10, 6))
plt.plot(k_range, silhouette_scores, 'go-', linewidth=2, markersize=8)
plt.xlabel('簇数量 (K)', fontsize=12)
plt.ylabel('轮廓系数 (Silhouette Score)', fontsize=12)
plt.title('轮廓系数法确定最优K值', fontsize=14)
plt.xticks(k_range)
plt.grid(True, alpha=0.3)
plt.axvline(x=optimal_k, color='r', linestyle='--', alpha=0.7, label=f'最优K={optimal_k}')
plt.legend()
​
plt.tight_layout()
plt.savefig('silhouette_method.png', dpi=150)
plt.show()

3.3 轮廓系数可视化

为了更直观地理解轮廓系数,我们可以绘制每个样本的轮廓系数图,显示哪些样本被很好地聚类,哪些样本可能位于簇边界上。

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_samples, silhouette_score
​
# 准备数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
​
# 执行K-Means聚类
k = 3
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
​
# 计算轮廓系数
silhouette_avg = silhouette_score(X, labels)
sample_silhouette_values = silhouette_samples(X, labels)
​
# 绘制轮廓系数图
fig, ax = plt.subplots(figsize=(10, 7))
​
y_lower = 10
for i in range(k):
    # 获取第i个簇的轮廓系数
    ith_cluster_silhouette_values = sample_silhouette_values[labels == i]
    ith_cluster_silhouette_values.sort()
    
    size_cluster_i = ith_cluster_silhouette_values.shape[0]
    y_upper = y_lower + size_cluster_i
    
    # 使用不同颜色填充
    color = plt.cm.nipy_spectral(float(i) / k)
    ax.fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_silhouette_values,
                     facecolor=color, edgecolor=color, alpha=0.7)
    
    # 在图上标注簇编号
    ax.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
    
    y_lower = y_upper + 10
​
ax.set_title(f'轮廓系数分析图 (K={k})', fontsize=14)
ax.set_xlabel('轮廓系数', fontsize=12)
ax.set_ylabel('簇标签', fontsize=12)
​
# 添加平均值线
ax.axvline(x=silhouette_avg, color="red", linestyle="--", linewidth=2,
           label=f'平均轮廓系数={silhouette_avg:.3f}')
ax.legend(loc='upper right')
​
ax.set_xlim([-0.1, 1])
​
plt.tight_layout()
plt.savefig('silhouette_analysis.png', dpi=150)
plt.show()

四、初始化问题与优化

4.1 随机初始化的缺陷

K-Means算法对初始化非常敏感。随机选择K个样本作为初始中心时,不同的初始化可能导致完全不同的聚类结果。随机初始化的问题在于:

  1. 如果初始中心恰好落在同一个簇内,会导致某个簇过大,其他簇过小

  2. 可能收敛到局部最优而非全局最优

  3. 收敛速度可能较慢

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 生成测试数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
​
# 演示不同随机初始化导致的不同结果
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
​
# 不同的随机种子会导致不同的聚类结果
for idx, seed in enumerate([0, 1, 2, 42, 100, 200]):
    ax = axes[idx // 3, idx % 3]
    
    # 使用随机初始化(init='random')
    kmeans = KMeans(n_clusters=3, init='random', n_init=1, 
                    random_state=seed, max_iter=100)
    labels = kmeans.fit_predict(X)
    
    # 绘制聚类结果
    colors = ['#FF6B6B', '#4ECDC4', '#45B7D1']
    for i in range(3):
        ax.scatter(X[labels == i, 0], X[labels == i, 1], 
                  c=colors[i], s=30, alpha=0.7, label=f'簇{i}')
    
    # 绘制簇中心
    ax.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
              c='black', marker='X', s=200, edgecolors='white', linewidths=2,
              label='中心')
    
    ax.set_title(f'Random Init (seed={seed})\nSSE={kmeans.inertia_:.2f}', fontsize=11)
    ax.legend(loc='upper right', fontsize=8)
​
plt.suptitle('不同随机初始化导致的聚类结果差异', fontsize=14)
plt.tight_layout()
plt.savefig('random_init_comparison.png', dpi=150)
plt.show()

4.2 K-Means++初始化

K-Means++是由Arthur和Vassilvitskii于2007年提出的初始化方法,是对随机初始化的重大改进。K-Means++的初始化策略是:

  1. 随机选择一个样本作为第一个簇中心

  2. 对于每个样本,计算其到最近簇中心的距离D(x)

  3. 以概率 D(x)² / Σ D(x)² 选择下一个簇中心(距离越远的样本越可能被选中)

  4. 重复步骤2-3,直到选出K个簇中心

  5. 使用标准K-Means算法进行迭代

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 生成测试数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
​
# 比较K-Means++和随机初始化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
​
# K-Means++初始化
kmeans_pp = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42)
labels_pp = kmeans_pp.fit_predict(X)
​
axes[0].scatter(X[:, 0], X[:, 1], c=labels_pp, cmap='viridis', s=30, alpha=0.7)
axes[0].scatter(kmeans_pp.cluster_centers_[:, 0], kmeans_pp.cluster_centers_[:, 1],
               c='red', marker='X', s=300, edgecolors='white', linewidths=2)
axes[0].set_title(f'K-Means++ 初始化\nSSE={kmeans_pp.inertia_:.2f}', fontsize=12)
​
# 随机初始化
kmeans_random = KMeans(n_clusters=3, init='random', n_init=1, random_state=42)
labels_random = kmeans_random.fit_predict(X)
​
axes[1].scatter(X[:, 0], X[:, 1], c=labels_random, cmap='viridis', s=30, alpha=0.7)
axes[1].scatter(kmeans_random.cluster_centers_[:, 0], kmeans_random.cluster_centers_[:, 1],
               c='red', marker='X', s=300, edgecolors='white', linewidths=2)
axes[1].set_title(f'Random 初始化\nSSE={kmeans_random.inertia_:.2f}', fontsize=12)
​
plt.suptitle('K-Means++ vs Random 初始化对比', fontsize=14)
plt.tight_layout()
plt.savefig('init_comparison.png', dpi=150)
plt.show()
​
print(f"K-Means++ SSE: {kmeans_pp.inertia_:.4f}")
print(f"Random SSE: {kmeans_random.inertia_:.4f}")

4.3 多次运行取最优

scikit-learn的KMeans默认使用n_init=10,即运行10次不同的初始化,选择SSE最小的结果作为最终模型。这有效降低了陷入局部最优的风险。

复制代码
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 生成较难聚类的数据(簇有重叠)
np.random.seed(42)
X, y_true = make_blobs(n_samples=500, centers=3, cluster_std=2.5, random_state=42)
​
# 多次运行对比
print("单次运行(可能陷入局部最优):")
kmeans_single = KMeans(n_clusters=3, n_init=1, random_state=0)
kmeans_single.fit(X)
print(f"  SSE = {kmeans_single.inertia_:.4f}")
​
print("\n10次运行取最优:")
kmeans_multi = KMeans(n_clusters=3, n_init=10, random_state=0)
kmeans_multi.fit(X)
print(f"  SSE = {kmeans_multi.inertia_:.4f}")
​
print(f"\nSSE改善: {kmeans_single.inertia_ - kmeans_multi.inertia_:.4f}")
print(f"改善比例: {(kmeans_single.inertia_ - kmeans_multi.inertia_) / kmeans_single.inertia_ * 100:.2f}%")

五、使用场景

K-Means聚类算法在实际中有广泛的应用,以下是几个典型的使用场景。

5.1 客户分群

在市场营销中,K-Means常用于客户细分。通过分析客户的消费金额、购买频率、活跃度等特征,将客户划分为不同群体,制定针对性的营销策略。

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
​
# 模拟客户数据:消费金额、购买频率、活跃天数
np.random.seed(42)
n_customers = 300
​
# 生成三类客户:高价值、沉睡、潜力
high_value = np.random.multivariate_normal(
    mean=[10000, 50, 300], cov=[[2000, 50, 30], [50, 100, 20], [30, 20, 100]], 
    size=int(n_customers * 0.2)
)
sleeping = np.random.multivariate_normal(
    mean=[500, 5, 50], cov=[[500, 10, 30], [10, 20, 10], [30, 10, 200]], 
    size=int(n_customers * 0.3)
)
potential = np.random.multivariate_normal(
    mean=[3000, 20, 150], cov=[[1000, 30, 50], [30, 50, 30], [50, 30, 150]], 
    size=int(n_customers * 0.5)
)
​
X_customers = np.vstack([high_value, sleeping, potential])
feature_names = ['消费金额(元)', '购买频率(次)', '活跃天数(天)']
​
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_customers)
​
# 使用肘部法则确定最优K
inertias = []
for k in range(1, 8):
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X_scaled)
    inertias.append(km.inertia_)
​
# 根据业务理解,我们知道有3类客户
k = 3
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_scaled)
centers_scaled = kmeans.cluster_centers_
centers = scaler.inverse_transform(centers_scaled)
​
# 可视化(使用前两个特征)
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(range(1, 8), inertias, 'bo-')
plt.xlabel('簇数量 K')
plt.ylabel('SSE')
plt.title('肘部法则')
plt.axvline(x=3, color='r', linestyle='--', alpha=0.7)
​
plt.subplot(1, 2, 2)
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1']
cluster_names = ['高价值客户', '沉睡客户', '潜力客户']
for i in range(k):
    mask = labels == i
    plt.scatter(X_customers[mask, 0], X_customers[mask, 1], 
               c=colors[i], s=40, alpha=0.7, label=f'{cluster_names[i]}({mask.sum()}人)')
plt.xlabel(feature_names[0])
plt.ylabel(feature_names[1])
plt.legend()
plt.title('客户分群结果')
​
plt.tight_layout()
plt.savefig('customer_segmentation.png', dpi=150)
plt.show()
​
# 输出各簇统计信息
print("客户分群统计:")
for i in range(k):
    mask = labels == i
    print(f"\n{cluster_names[i]} (簇{i}):")
    print(f"  人数: {mask.sum()} ({mask.sum()/len(labels)*100:.1f}%)")
    print(f"  平均消费: {X_customers[mask, 0].mean():.2f}元")
    print(f"  平均购买频率: {X_customers[mask, 1].mean():.1f}次")
    print(f"  平均活跃天数: {X_customers[mask, 2].mean():.0f}天")

5.2 图像压缩

K-Means可以用于图像压缩,通过将图像中相近的颜色聚类,用簇中心代替原颜色来达到压缩目的。

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import load_sample_image
​
# 加载示例图像(scikit-learn自带的中国古建筑图像)
china = load_sample_image('china.jpg')
print(f"原始图像形状: {china.shape}")
print(f"原始颜色数: {len(np.unique(china.reshape(-1, 3), axis=0))}")
​
# 将图像转换为二维数组 (像素数, 颜色通道)
X_image = china.reshape(-1, 3)
​
# 使用K-Means进行颜色聚类
n_colors = 64  # 压缩到64种颜色
kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_image)
​
# 用簇中心替换每个像素的颜色
compressed_colors = kmeans.cluster_centers_[labels]
compressed_image = compressed_colors.reshape(china.shape).astype(np.uint8)
​
# 计算压缩率
original_size = china.shape[0] * china.shape[1] * 3  # RGB三个通道
compressed_size = n_colors * 3 + len(labels) * np.log2(n_colors) / 8
compression_ratio = original_size / (compressed_size)
print(f"\n压缩到 {n_colors} 种颜色")
print(f"压缩比: {compression_ratio:.2f}x")
​
# 显示对比图
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].imshow(china)
axes[0].set_title('原始图像 (256²³ colors)', fontsize=12)
axes[0].axis('off')
​
axes[1].imshow(compressed_image)
axes[1].set_title(f'压缩图像 ({n_colors} colors)', fontsize=12)
axes[1].axis('off')
​
plt.tight_layout()
plt.savefig('image_compression.png', dpi=150)
plt.show()

5.3 异常检测

K-Means可以用于异常检测,正常数据点通常聚集在某些区域,而异常点则远离这些区域。通过计算每个点到其所属簇中心的距离,可以识别出异常点。

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 生成正常数据
np.random.seed(42)
X_normal, _ = make_blobs(n_samples=300, centers=1, cluster_std=1.5, random_state=42)
​
# 添加异常点
outliers = np.random.uniform(low=-8, high=8, size=(20, 2))
X_with_outliers = np.vstack([X_normal, outliers])
​
# K-Means聚类
kmeans = KMeans(n_clusters=1, random_state=42, n_init=10)
kmeans.fit(X_normal)  # 仅使用正常数据训练
​
# 计算所有点到中心的距离
distances = np.sqrt(np.sum((X_with_outliers - kmeans.cluster_centers_) ** 2, axis=1))
​
# 设置阈值(距离大于2倍标准差为异常)
threshold = 2.5 * np.std(distances[:len(X_normal)])
outlier_mask = distances > threshold
​
print(f"检测到的异常点数量: {outlier_mask.sum()} (真实异常: {len(outliers)})")
​
# 可视化
plt.figure(figsize=(10, 8))
plt.scatter(X_normal[:, 0], X_normal[:, 1], c='blue', s=40, alpha=0.7, label='正常数据')
plt.scatter(outliers[:, 0], outliers[:, 1], c='red', s=100, marker='x', linewidths=2, label='真实异常点')
​
# 标记检测到的异常
detected_outliers = X_with_outliers[outlier_mask]
plt.scatter(detected_outliers[:, 0], detected_outliers[:, 1], 
           facecolors='none', edgecolors='red', s=200, linewidths=2, label='检测到的异常')
​
# 绘制簇中心和决策边界
center = kmeans.cluster_centers_[0]
theta = np.linspace(0, 2 * np.pi, 100)
circle_x = center[0] + threshold * np.cos(theta)
circle_y = center[1] + threshold * np.sin(theta)
plt.plot(circle_x, circle_y, 'g--', linewidth=2, label=f'异常阈值 (距离>{threshold:.2f})')
plt.scatter(center[0], center[1], c='green', marker='*', s=500, edgecolors='white', linewidths=2)
​
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('基于K-Means的异常检测')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('anomaly_detection.png', dpi=150)
plt.show()

5.4 新闻聚类

K-Means可以对新闻文章进行主题聚类,帮助用户快速浏览和筛选感兴趣的内容。

复制代码
import numpy as np
from sklearn.cluster import KMeans
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
​
# 模拟新闻数据(实际应用中应从API或数据库获取)
news_titles = [
    "央行降准释放流动性,支持实体经济发展",
    "股市大幅上涨,沪指突破3500点",
    "房地产市场调控政策持续收紧",
    "人工智能技术获突破,AI概念股大涨",
    "特斯拉电动汽车销量创新高",
    "新能源汽车补贴政策出台",
    "华为发布最新款5G手机",
    "苹果公司季度财报超预期",
    "芯片短缺影响汽车行业产能",
    "半导体行业投资热潮持续",
    "北京冬奥会参赛名单公布",
    "中国女排获得世界冠军",
    "欧冠决赛皇马夺冠",
    "NBA总决赛湖人队夺冠",
    "世界杯预选赛中国国家队获胜",
]
​
# 实际新闻标题已经是中文了,这里演示向量化的方式
# 使用TF-IDF向量化(实际中文需先分词)
vectorizer = TfidfVectorizer(max_features=100, token_pattern=r'(?u)\b\w+\b')
tfidf_matrix = vectorizer.fit_transform(news_titles)
​
# 使用SVD降维(可选,提高聚类效果)
svd = TruncatedSVD(n_components=5, random_state=42)
X_reduced = svd.fit_transform(tfidf_matrix)
​
# K-Means聚类
n_clusters = 4
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_reduced)
​
# 按簇分组显示
cluster_names = {
    0: "财经/金融",
    1: "科技/互联网", 
    2: "汽车/新能源",
    3: "体育"
}
​
print("新闻聚类结果:")
print("=" * 60)
for i in range(n_clusters):
    cluster_news = [news_titles[j] for j in range(len(labels)) if labels[j] == i]
    print(f"\n【{cluster_names[i]}】({len(cluster_news)}篇)")
    for title in cluster_news:
        print(f"  • {title}")

5.5 基因表达数据

在生物信息学中,K-Means常用于基因表达数据分析,识别具有相似表达模式的基因。

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 模拟基因表达数据:100个基因,6个样本
np.random.seed(42)
n_genes = 100
n_samples = 6
​
# 生成3类具有不同表达模式的基因
pattern1 = np.random.randn(30, n_samples) + np.array([5, 5, 3, 1, 0, 0])
pattern2 = np.random.randn(40, n_samples) + np.array([0, 0, 2, 4, 4, 3])
pattern3 = np.random.randn(30, n_samples) + np.array([2, 1, 0, 0, 1, 2])
​
X_genes = np.vstack([pattern1, pattern2, pattern3])
​
# K-Means聚类
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_genes)
​
# 可视化基因表达热图
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
​
# 左图:原始数据热图(按真实类别排序)
im1 = axes[0].imshow(X_genes, aspect='auto', cmap='RdBu_r')
axes[0].set_xlabel('样本')
axes[0].set_ylabel('基因')
axes[0].set_title('基因表达矩阵(原始)')
plt.colorbar(im1, ax=axes[0], label='表达量')
​
# 右图:按聚类结果排序
sorted_idx = np.argsort(labels)
X_sorted = X_genes[sorted_idx]
im2 = axes[1].imshow(X_sorted, aspect='auto', cmap='RdBu_r')
axes[1].set_xlabel('样本')
axes[1].set_ylabel('基因(按聚类排序)')
axes[1].set_title('基因表达矩阵(按K-Means聚类排序)')
plt.colorbar(im2, ax=axes[1], label='表达量')
​
# 添加簇分隔线
cumsum = np.cumsum(np.bincount(labels))
for i in range(len(cumsum) - 1):
    axes[1].axhline(y=cumsum[i], color='white', linewidth=2)
​
plt.tight_layout()
plt.savefig('gene_clustering.png', dpi=150)
plt.show()
​
# 统计各簇的基因数量
print("基因聚类结果:")
for i in range(3):
    print(f"  簇{i}: {np.sum(labels == i)} 个基因")

六、实战代码:完整的K-Means工作流

6.1 模拟数据聚类

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, adjusted_rand_score
​
# ============ 步骤1: 生成模拟数据 ============
np.random.seed(42)
X, y_true = make_blobs(
    n_samples=500, 
    centers=4, 
    cluster_std=1.2, 
    random_state=42
)
​
print(f"数据集大小: {X.shape}")
print(f"真实簇数: {len(np.unique(y_true))}")
​
# ============ 步骤2: 使用肘部法则确定最优K ============
k_range = range(2, 10)
inertias = []
silhouette_scores = []
​
for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X, kmeans.labels_))
​
# 找到最优K(肘部法则)
# 简单方法:计算SSE下降率的拐点
diffs = np.diff(inertias)
diff2 = np.diff(diffs)
optimal_k_elbow = np.argmax(diff2) + 2  # +2 因为两次diff
​
# 轮廓系数法
optimal_k_silhouette = list(k_range)[np.argmax(silhouette_scores)]
​
print(f"\n最优K值(肘部法则): {optimal_k_elbow}")
print(f"最优K值(轮廓系数): {optimal_k_silhouette}")
​
# ============ 步骤3: 使用最优K进行聚类 ============
# 这里我们使用轮廓系数的结果
best_k = optimal_k_silhouette
kmeans_final = KMeans(n_clusters=best_k, random_state=42, n_init=10)
labels = kmeans_final.fit_predict(X)
​
# ============ 步骤4: 评估聚类效果 ============
ari = adjusted_rand_score(y_true, labels)
silhouette = silhouette_score(X, labels)
​
print(f"\n聚类评估指标:")
print(f"  调整兰德指数 (ARI): {ari:.4f}")
print(f"  轮廓系数: {silhouette:.4f}")
​
# ============ 步骤5: 可视化结果 ============
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
​
# 左图:肘部法则
axes[0].plot(k_range, inertias, 'bo-', linewidth=2, markersize=8)
axes[0].axvline(x=best_k, color='r', linestyle='--', alpha=0.7, label=f'选择K={best_k}')
axes[0].set_xlabel('簇数量 (K)')
axes[0].set_ylabel('SSE (Inertia)')
axes[0].set_title('肘部法则')
axes[0].legend()
​
# 中图:轮廓系数
axes[1].plot(k_range, silhouette_scores, 'go-', linewidth=2, markersize=8)
axes[1].axvline(x=best_k, color='r', linestyle='--', alpha=0.7, label=f'选择K={best_k}')
axes[1].set_xlabel('簇数量 (K)')
axes[1].set_ylabel('轮廓系数')
axes[1].set_title('轮廓系数法')
axes[1].legend()
​
# 右图:聚类结果
colors = plt.cm.viridis(np.linspace(0, 1, best_k))
for i in range(best_k):
    mask = labels == i
    axes[2].scatter(X[mask, 0], X[mask, 1], c=[colors[i]], s=40, alpha=0.7, label=f'簇{i}')
​
# 绘制簇中心
axes[2].scatter(kmeans_final.cluster_centers_[:, 0], kmeans_final.cluster_centers_[:, 1],
               c='red', marker='X', s=300, edgecolors='white', linewidths=2, label='中心')
axes[2].set_xlabel('特征1')
axes[2].set_ylabel('特征2')
axes[2].set_title(f'K-Means聚类结果 (K={best_k})')
axes[2].legend()
​
plt.tight_layout()
plt.savefig('kmeans_workflow.png', dpi=150)
plt.show()

6.2 K-Means++ vs 随机初始化对比实验

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
​
# 生成具有挑战性的数据(簇有重叠)
np.random.seed(42)
X, y_true = make_blobs(n_samples=600, centers=3, cluster_std=2.5, random_state=42)
​
# 多次实验比较
n_experiments = 20
kmeanspp_sses = []
random_sses = []
​
for seed in range(n_experiments):
    # K-Means++
    kmpp = KMeans(n_clusters=3, init='k-means++', n_init=1, random_state=seed)
    kmpp.fit(X)
    kmeanspp_sses.append(kmpp.inertia_)
    
    # 随机初始化
    kr = KMeans(n_clusters=3, init='random', n_init=1, random_state=seed)
    kr.fit(X)
    random_sses.append(kr.inertia_)
​
# 统计分析
print("K-Means++ 初始化:")
print(f"  平均SSE: {np.mean(kmeanspp_sses):.4f}")
print(f"  标准差: {np.std(kmeanspp_sses):.4f}")
print(f"  最小SSE: {np.min(kmeanspp_sses):.4f}")
​
print("\nRandom 初始化:")
print(f"  平均SSE: {np.mean(random_sses):.4f}")
print(f"  标准差: {np.std(random_sses):.4f}")
print(f"  最小SSE: {np.min(random_sses):.4f}")
​
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
​
# 左图:SSE分布对比
axes[0].boxplot([kmeanspp_sses, random_sses], labels=['K-Means++', 'Random'])
axes[0].set_ylabel('SSE (Inertia)')
axes[0].set_title('不同初始化方法的SSE分布')
axes[0].grid(True, alpha=0.3)
​
# 右图:20次实验的SSE变化
x = range(n_experiments)
axes[1].plot(x, kmeanspp_sses, 'go-', label='K-Means++', alpha=0.7)
axes[1].plot(x, random_sses, 'ro-', label='Random', alpha=0.7)
axes[1].set_xlabel('实验序号')
axes[1].set_ylabel('SSE (Inertia)')
axes[1].set_title('20次随机实验的SSE对比')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
​
plt.tight_layout()
plt.savefig('initialization_comparison.png', dpi=150)
plt.show()

6.3 图像压缩实战

复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from skimage import data
from skimage import img_as_float
​
# 加载宇航员图像
image = img_as_float(data.astronaut())
print(f"图像形状: {image.shape}")
print(f"像素数: {image.shape[0]} x {image.shape[1]} = {image.shape[0] * image.shape[1]}")
​
# 将图像数据转换为二维数组
original_shape = image.shape
X = image.reshape(-1, 3)
​
# 不同压缩质量的对比
compression_results = []
​
for n_colors in [2, 5, 10, 20, 50, 100]:
    kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init=10)
    kmeans.fit(X)
    
    # 压缩图像
    labels = kmeans.predict(X)
    compressed = kmeans.cluster_centers_[labels].reshape(original_shape)
    
    # 计算均方误差
    mse = np.mean((X - kmeans.cluster_centers_[labels]) ** 2)
    compression_results.append({
        'n_colors': n_colors,
        'mse': mse,
        'compressed': compressed
    })
    
    print(f"颜色数: {n_colors:3d}, MSE: {mse:.6f}")
​
# 可视化
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()
​
# 显示不同压缩级别的图像
for idx, result in enumerate(compression_results):
    axes[idx].imshow(result['compressed'])
    axes[idx].set_title(f'{result["n_colors"]} colors, MSE={result["mse"]:.4f}')
    axes[idx].axis('off')
​
# 最后一幅显示原始图像
axes[-1].imshow(image)
axes[-1].set_title(f'Original ({len(np.unique(X, axis=0))} colors)')
axes[-1].axis('off')
​
plt.suptitle('K-Means图像压缩效果对比', fontsize=14)
plt.tight_layout()
plt.savefig('image_compression_comparison.png', dpi=150)
plt.show()

七、K-Means算法总结

7.1 算法优缺点

优点

  • 原理简单,易于理解和实现

  • 计算效率高,时间复杂度为O(nKI),其中n为样本数,K为簇数,I为迭代次数

  • 对大数据集具有良好的可扩展性

  • 收敛速度快(通常10-20次迭代即可收敛)

  • 聚类结果易于解释

缺点

  • 需要预先指定K值

  • 对噪声和异常值敏感

  • 只能发现球形簇,无法处理非凸形簇

  • 对初始化敏感,容易陷入局部最优

  • 簇大小差异较大时效果不佳

  • 特征尺度不同会影响聚类结果

7.2 算法参数说明

scikit-learn中KMeans的主要参数:

参数 说明 常用值
n_clusters 簇的数量K 根据业务或肘部法则确定
init 初始化方法 'k-means++'(默认推荐)或 'random'
n_init 初始化运行次数 10(默认,推荐)
max_iter 最大迭代次数 300(默认)
tol 收敛阈值 1e-4(默认)
random_state 随机种子 整数,保证可复现

7.3 使用注意事项

  1. 数据预处理:K-Means基于距离计算,特征尺度不同时会影响结果。建议使用StandardScaler进行标准化。

  2. K值选择:没有绝对的最佳方法,通常结合肘部法则和轮廓系数,并考虑业务意义。

  3. 多次运行 :建议使用n_init=10或更多,以获得更稳定的聚类结果。

  4. 异常值处理:K-Means对异常值敏感,可考虑先进行异常值检测和处理。

  5. 适用场景:K-Means最适合各簇大小相近、簇为球形、特征维度相近的数据。


八、扩展阅读

8.1 相关算法

  • K-Means++:K-Means的改进初始化方法,显著提升聚类质量

  • Mini-Batch K-Means:适合大规模数据的增量学习

  • DBSCAN:基于密度的聚类,无需指定K值,可发现任意形状簇

  • 层次聚类:通过构建树形结构进行聚类

  • GMM(高斯混合模型):软聚类方法,给出每个样本属于各簇的概率

8.2 sklearn官方文档

sklearn.cluster.KMeans官方文档地址: https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html


九、完整代码仓库

以下是本文所有代码的汇总,可直接复制运行:

复制代码
# 安装必要的库
# pip install numpy matplotlib scikit-learn scikit-image
​
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score, silhouette_samples
from sklearn.preprocessing import StandardScaler
import warnings
warnings.filterwarnings('ignore')
​
print("=" * 60)
print("K-Means聚类算法完整示例")
print("=" * 60)
​
# 1. 生成数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
print(f"\n1. 数据生成: {X.shape[0]} 样本, {X.shape[1]} 特征")
​
# 2. 肘部法则确定K
inertias = []
for k in range(1, 10):
    km = KMeans(k, random_state=42, n_init=10)
    km.fit(X)
    inertias.append(km.inertia_)
​
# 3. 轮廓系数确定K
sil_scores = []
for k in range(2, 10):
    km = KMeans(k, random_state=42, n_init=10)
    labels = km.fit_predict(X)
    sil_scores.append(silhouette_score(X, labels))
​
optimal_k = list(range(2, 10))[np.argmax(sil_scores)]
print(f"2. 最优K值(轮廓系数): {optimal_k}")
​
# 4. 最终聚类
kmeans = KMeans(optimal_k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
print(f"3. 聚类完成,轮廓系数: {silhouette_score(X, labels):.4f}")
print(f"   各簇样本数: {np.bincount(labels)}")
print(f"   簇中心:\n{kmeans.cluster_centers_}")
​
# 5. 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].plot(range(1, 10), inertias, 'bo-')
axes[0].axvline(x=optimal_k, color='r', linestyle='--')
axes[0].set_title('肘部法则')
axes[0].set_xlabel('K')
​
axes[1].plot(range(2, 10), sil_scores, 'go-')
axes[1].axvline(x=optimal_k, color='r', linestyle='--')
axes[1].set_title('轮廓系数')
axes[1].set_xlabel('K')
​
colors = plt.cm.viridis(np.linspace(0, 1, optimal_k))
for i in range(optimal_k):
    axes[2].scatter(X[labels==i, 0], X[labels==i, 1], c=[colors[i]], s=40, alpha=0.7)
axes[2].scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], 
               c='red', marker='X', s=200)
axes[2].set_title(f'聚类结果 (K={optimal_k})')
​
plt.tight_layout()
plt.savefig('kmeans_complete_example.png', dpi=150)
plt.show()
​
print("\n" + "=" * 60)
print("运行完成!图像已保存。")
print("=" * 60)
相关推荐
yugi9878381 小时前
主动噪声控制中的 FXLMS 算法研究与 MATLAB 实现
开发语言·算法·matlab
三维重建-光栅投影1 小时前
最小二乘中的矩阵求导基础总结
线性代数·机器学习·矩阵
Liangwei Lin1 小时前
LeetCode 394. 字符串解码
数据结构·算法
YuanDaima20481 小时前
动态规划基础原理与题目说明
数据结构·人工智能·python·算法·动态规划·手撕代码
大志出奇迹1 小时前
传输协议为大端,STM32为小端,数据传输的字节序问题
c语言·stm32·单片机·mcu·算法·rtos
龙侠九重天1 小时前
C# 调用 TensorFlow:迁移学习与模型推理实战指南
人工智能·深度学习·机器学习·c#·tensorflow·迁移学习·tensorflow.net
我爱cope1 小时前
【滑动窗口:力扣438找到字符串中所有字母异位词】
算法·leetcode·职场和发展
happyprince1 小时前
06-FlagEmbedding 核心算法详解
算法
洛水水1 小时前
【力扣100题】27. 二叉树的最大深度
算法·leetcode·图论