K-均值(K-means)聚类算法的原理、实现与优化
字数 1533 2025-12-08 12:44:57
K-均值(K-means)聚类算法的原理、实现与优化
1. 题目/知识点描述
K-均值(K-means)是一种经典的无监督学习聚类算法,其目标是将n个数据点划分为k个簇(cluster),使得每个数据点都属于离其最近的簇中心(centroid)对应的簇,并且簇内点的距离尽可能小。这个算法广泛应用于数据挖掘、图像分割、客户分群等领域。
2. 算法原理详解
K-means算法的核心思想很简单:
- 初始化k个簇中心(通常是随机选择k个数据点作为初始中心)
- 重复以下两个步骤直到收敛:
- 分配阶段:将每个数据点分配到距离最近的簇中心所属的簇
- 更新阶段:重新计算每个簇中所有点的平均值,作为新的簇中心
算法收敛的条件通常是簇中心不再发生明显变化,或者达到了预设的最大迭代次数。
3. 算法步骤分解
步骤1:初始化
- 输入:数据集X = {x₁, x₂, ..., xₙ},簇的数量k
- 输出:k个簇C = {C₁, C₂, ..., Cₖ}
- 方法:从n个数据点中随机选择k个点作为初始簇中心μ₁, μ₂, ..., μₖ
步骤2:分配数据点到最近的簇
对于每个数据点xᵢ:
- 计算xᵢ到每个簇中心μⱼ的距离(通常使用欧几里得距离)
- 将xᵢ分配到距离最近的簇中心对应的簇
公式:cᵢ = argminⱼ ||xᵢ - μⱼ||²
其中cᵢ表示xᵢ被分配到的簇的索引
步骤3:更新簇中心
对于每个簇Cⱼ:
- 计算簇Cⱼ中所有数据点的平均值
- 将该平均值作为新的簇中心μⱼ
公式:μⱼ = (1/|Cⱼ|) Σ_{x∈Cⱼ} x
步骤4:判断收敛
检查是否满足以下任一收敛条件:
- 簇中心的变化小于预设阈值
- 分配结果不再变化
- 达到最大迭代次数
如果不收敛,返回步骤2继续迭代
4. 距离度量
K-means最常用的距离度量是欧几里得距离:
- 对于点x = (x₁, x₂, ..., xₘ)和y = (y₁, y₂, ..., yₘ)
- 欧几里得距离:dist(x, y) = √[Σᵢ(xᵢ - yᵢ)²]
- 实际计算时通常使用平方距离,因为比较距离大小时开方不影响结果
5. 算法实现(Python)
import numpy as np
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
class KMeans:
def __init__(self, n_clusters=3, max_iter=300, tol=1e-4, random_state=None):
"""
初始化K-means参数
n_clusters: 簇的数量k
max_iter: 最大迭代次数
tol: 收敛阈值
random_state: 随机种子
"""
self.n_clusters = n_clusters
self.max_iter = max_iter
self.tol = tol
self.random_state = random_state
self.centroids = None
self.labels = None
def _initialize_centroids(self, X):
"""随机初始化簇中心"""
np.random.seed(self.random_state)
n_samples = X.shape[0]
# 随机选择k个不同的点作为初始中心
indices = np.random.choice(n_samples, self.n_clusters, replace=False)
return X[indices]
def _assign_clusters(self, X):
"""将数据点分配到最近的簇"""
distances = np.zeros((X.shape[0], self.n_clusters))
for i, centroid in enumerate(self.centroids):
# 计算每个点到每个簇中心的距离
distances[:, i] = np.linalg.norm(X - centroid, axis=1) ** 2
# 每个点分配到距离最近的簇
return np.argmin(distances, axis=1)
def _update_centroids(self, X, labels):
"""更新簇中心"""
new_centroids = np.zeros((self.n_clusters, X.shape[1]))
for i in range(self.n_clusters):
# 获取属于第i簇的所有点
points_in_cluster = X[labels == i]
if len(points_in_cluster) > 0:
# 计算均值作为新的簇中心
new_centroids[i] = points_in_cluster.mean(axis=0)
else:
# 如果簇为空,重新随机初始化
new_centroids[i] = X[np.random.choice(X.shape[0])]
return new_centroids
def fit(self, X):
"""训练K-means模型"""
# 1. 初始化簇中心
self.centroids = self._initialize_centroids(X)
for iteration in range(self.max_iter):
# 2. 分配簇标签
self.labels = self._assign_clusters(X)
# 3. 保存旧的簇中心
old_centroids = self.centroids.copy()
# 4. 更新簇中心
self.centroids = self._update_centroids(X, self.labels)
# 5. 检查收敛
centroid_shift = np.linalg.norm(self.centroids - old_centroids)
if centroid_shift < self.tol:
print(f"在迭代 {iteration+1} 次后收敛")
break
return self
def predict(self, X):
"""预测新数据点的簇标签"""
return self._assign_clusters(X)
6. 算法的优缺点
优点:
- 简单易懂,实现相对容易
- 计算效率高,适用于大规模数据集
- 结果容易解释
缺点:
- 需要预先指定k值
- 对初始值敏感,可能收敛到局部最优
- 对异常值敏感
- 假设簇是凸形和球状,对非凸形状效果不好
- 不适合簇大小差异很大的情况
7. 优化策略
1. K-means++初始化
- 改进随机初始化的方法
- 步骤:
a) 随机选择第一个簇中心
b) 对于每个点,计算到最近簇中心的距离D(x)
c) 以概率D(x)²/ΣD(x)²选择下一个簇中心
d) 重复直到选择k个中心
2. 肘部法则(Elbow Method)确定k值
- 计算不同k值下的簇内误差平方和(SSE)
- SSE = Σᵢ Σ_{x∈Cᵢ} ||x - μᵢ||²
- 绘制k-SSE曲线,选择"肘点"作为最优k值
3. Mini-Batch K-means
- 每次迭代只使用部分数据
- 减少计算量,适用于大规模数据
8. 应用示例
# 生成示例数据
X, y_true = make_blobs(n_samples=300, centers=4,
cluster_std=0.60, random_state=0)
# 创建K-means模型
kmeans = KMeans(n_clusters=4, max_iter=300, random_state=42)
# 训练模型
kmeans.fit(X)
# 可视化结果
plt.figure(figsize=(12, 5))
# 真实标签
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis')
plt.title("真实聚类")
# K-means聚类结果
plt.subplot(1, 2, 2)
plt.scatter(X[:, 0], X[:, 1], c=kmeans.labels, cmap='viridis')
plt.scatter(kmeans.centroids[:, 0], kmeans.centroids[:, 1],
c='red', s=200, alpha=0.8, marker='X')
plt.title("K-means聚类结果")
plt.show()
9. 时间复杂度分析
- 分配阶段:O(nkd),其中n是样本数,k是簇数,d是维度
- 更新阶段:O(nd)
- 每次迭代:O(nkd)
- 总时间复杂度:O(I×nkd),其中I是迭代次数
10. 常见面试问题
- K-means的收敛性如何保证?
- 如何选择合适的k值?
- K-means对异常值敏感,如何改进?
- K-means和KNN的区别是什么?
- 如何处理非球形分布的数据?
通过以上详细讲解,你应该对K-means聚类算法有了全面的理解,包括其原理、实现细节、优化方法以及实际应用中的注意事项。