K-均值(K-means)聚类算法的原理、实现与优化
题目描述
K-means聚类是一种经典的无监督机器学习算法,用于将数据点划分为K个不同的簇。该算法通过迭代计算各个簇的中心点(质心),并将每个数据点分配给距离其最近的质心所属的簇,最终使得所有数据点到其所属簇质心的距离平方和最小化。
知识背景
聚类分析的目标是将相似的数据点归为一类,而将不相似的数据点分开。K-means算法需要预先指定簇的数量K,适用于球形、大小相近且密度均匀的簇。它在图像分割、客户细分、文档分类等领域广泛应用。
算法原理详解
核心思想
K-means算法的目标是找到K个簇的质心,并分配每个数据点到最近的质心,从而最小化簇内平方误差(Within-Cluster Sum of Squares, WCSS):
\[WCSS = \sum_{i=1}^{K} \sum_{\mathbf{x} \in C_i} \|\mathbf{x} - \boldsymbol{\mu}_i\|^2 \]
其中,\(C_i\) 是第 \(i\) 个簇,\(\boldsymbol{\mu}_i\) 是该簇的质心。
基本步骤
- 初始化:随机选择K个数据点作为初始质心(或其他初始化方法)。
- 分配:将每个数据点分配到与其欧氏距离最近的质心所属的簇。
- 更新:重新计算每个簇的质心(取该簇所有数据点的平均值)。
- 迭代:重复步骤2和步骤3,直到质心不再变化(或变化小于阈值),或达到最大迭代次数。
逐步实现
假设我们有数据集 \(X = \{\mathbf{x}_1, \mathbf{x}_2, ..., \mathbf{x}_n\}\),每个数据点是一个d维向量。
步骤1:初始化质心
随机选择K个数据点作为初始质心。这可能导致局部最优解,因此通常需要多次运行并选择最佳结果。
import numpy as np
def initialize_centroids(X, k):
indices = np.random.choice(len(X), k, replace=False)
return X[indices]
步骤2:分配数据点到最近质心
对于每个数据点,计算其与所有质心的距离,并分配到距离最小的质心所在的簇。
def assign_clusters(X, centroids):
distances = np.sqrt(((X - centroids[:, np.newaxis])**2).sum(axis=2)) # 形状 (k, n)
return np.argmin(distances, axis=0) # 每个数据点的簇标签
步骤3:更新质心
计算每个簇中所有数据点的平均值作为新质心。
def update_centroids(X, labels, k):
new_centroids = np.zeros((k, X.shape[1]))
for i in range(k):
new_centroids[i] = X[labels == i].mean(axis=0)
return new_centroids
步骤4:迭代直至收敛
重复分配和更新步骤,直到质心变化小于阈值或达到最大迭代次数。
def kmeans(X, k, max_iters=100, tol=1e-4):
centroids = initialize_centroids(X, k)
for _ in range(max_iters):
labels = assign_clusters(X, centroids)
new_centroids = update_centroids(X, labels, k)
if np.linalg.norm(new_centroids - centroids) < tol:
break
centroids = new_centroids
return centroids, labels
算法优化与变体
1. 初始化优化:K-means++
- 原理:选择第一个质心随机,后续质心从剩余点中选择,概率正比于到已选质心的最小距离平方。
- 目的:使初始质心更分散,减少局部最优解,加速收敛。
def initialize_centroids_plus(X, k):
centroids = [X[np.random.randint(len(X))]]
for _ in range(k - 1):
distances = np.min([np.linalg.norm(X - c, axis=1)**2 for c in centroids], axis=0)
prob = distances / distances.sum()
centroids.append(X[np.random.choice(len(X), p=prob)])
return np.array(centroids)
2. 距离计算的加速:Elkan K-means
- 原理:利用三角不等式避免不必要的距离计算,特别适用于高维数据。
- 做法:维护数据点与质心之间距离的下界,当可以确定最近质心时跳过计算。
3. 处理非球形簇:K-medoids(PAM)
- 原理:质心必须是实际数据点(medoid),使用曼哈顿距离或其他距离度量,对异常值更鲁棒。
- 步骤:类似K-means,但更新质心时选择簇内到其他点距离总和最小的点。
4. 确定K值:肘部法则(Elbow Method)
- 原理:绘制不同K值对应的WCSS曲线,选择曲线拐点(肘部)作为最佳K值。
def elbow_method(X, max_k=10):
wcss = []
for k in range(1, max_k + 1):
centroids, labels = kmeans(X, k)
wcss.append(sum(np.linalg.norm(X[i] - centroids[labels[i]])**2 for i in range(len(X))))
# 绘制wcss vs k,寻找肘部
算法复杂度分析
- 时间复杂度:每次迭代 \(O(n \cdot k \cdot d)\),其中n为样本数,k为簇数,d为维度。
- 空间复杂度:\(O((n + k) \cdot d)\),存储数据和质心。
应用场景与局限性
应用场景
- 客户细分:根据购买行为将客户分组。
- 图像压缩:将相似颜色的像素聚类,用质心颜色代替。
- 异常检测:远离所有簇的点可能为异常值。
局限性
- 需要预先指定K值。
- 对初始质心敏感,容易陷入局部最优。
- 假设簇为凸形且大小相近,对非球形簇效果差。
- 对异常值敏感。
总结
K-means是一种简单高效的聚类算法,通过迭代优化最小化簇内误差。优化方法如K-means++和Elkan算法可提升性能。在实际应用中,需结合肘部法则确定K值,并根据数据特性选择变体算法。掌握K-means的核心原理、实现细节和优化策略,是应对相关面试问题的关键。