深入浅出 K-means:从第一性原理到 Python 实战
·
文章目录
一篇涵盖算法本质、实现细节与视觉化解读的完整指南
引言
K-means 是一种经典的无监督学习算法,自 20 世纪 60 年代提出以来,已成为数据挖掘、图像分割、市场细分等领域的基石工具。本教程将从第一性原理推导出算法,逐步剖析其运作机制,并用 Python 实现全流程可视化。
算法核心应用场景
- 客户分群:将消费者按购买行为自动分类
- 图像压缩:减少颜色数量,用代表性色彩代替
- 文档聚类:将相似主题的文档归为一组
- 异常检测:识别远离簇中心的异常点
一、K-means 核心思想:从原理到算法
1.1 自上而下的目标分解
让我们从顶层目标开始理解 K-means:

SSE 的精确定义:
SSE = Σ_{k=1}^{K} Σ_{x∈C_k} ||x - μ_k||²
其中:
- K: 簇的数量
- C_k: 第 k 个簇的所有点
- μ_k: 第 k 个簇的质心
- ||x - μ_k||: 点 x 到其质心的欧氏距离
1.2 从第一性原理推导算法
如果我们将 K-means 拆解为最基本的假设,会得到这样一条推理链:
# 第一性原理推导过程
1. 基本假设:同一组的点应该“靠近”一个代表点
2. 量化“靠近”:使用欧氏距离的平方(易优化处理)
3. 寻找“代表点”:使组内距离平方和最小的点 → 均值点
4. 破解循环依赖:用“初始化→分配→更新”迭代逼近
5. 算法涌现:自然推导出 K-means 的交替优化框架
物理类比:
想象在平面上有 N 个村庄,要建立 K 个邮局。最优方案是:
- 每个村民都去最近的邮局
- 邮局设在所服务村民的中心位置
- 不断调整这两点,直到方案稳定
二、Python 实现:手把手构建 K-means
2.1 完整算法实现
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
class KMeans:
"""K-means 算法的完整实现"""
def __init__(self, n_clusters=4, max_iters=300, random_state=42):
self.n_clusters = n_clusters
self.max_iters = max_iters
self.random_state = random_state
self.centroids = None
self.labels = None
def fit(self, X):
"""训练 K-means 模型"""
np.random.seed(self.random_state)
n_samples, n_features = X.shape
# 1. 初始化:随机选择 K 个点作为初始质心
random_idx = np.random.choice(n_samples, self.n_clusters, replace=False)
self.centroids = X[random_idx]
# 迭代优化
for iteration in range(self.max_iters):
# 2. 分配步骤:将每个点分配到最近质心
distances = self._compute_distances(X)
self.labels = np.argmin(distances, axis=1)
# 3. 更新步骤:重新计算质心位置
new_centroids = self._compute_centroids(X)
# 4. 收敛判断:质心是否不再变化
if np.allclose(self.centroids, new_centroids):
print(f"算法在 {iteration+1} 次迭代后收敛")
break
self.centroids = new_centroids
return self
def _compute_distances(self, X):
"""计算每个点到所有质心的距离"""
# 使用向量化加速计算
return np.linalg.norm(X[:, np.newaxis, :] - self.centroids[np.newaxis, :, :], axis=2)
def _compute_centroids(self, X):
"""计算每个簇的新质心"""
new_centroids = []
for k in range(self.n_clusters):
cluster_points = X[self.labels == k]
if len(cluster_points) > 0:
new_centroids.append(cluster_points.mean(axis=0))
else: # 处理空簇
new_centroids.append(self.centroids[k])
return np.array(new_centroids)
def calculate_sse(self, X):
"""计算误差平方和 (SSE)"""
sse = 0
for k in range(self.n_clusters):
cluster_points = X[self.labels == k]
if len(cluster_points) > 0:
sse += np.sum(np.linalg.norm(cluster_points - self.centroids[k], axis=1) ** 2)
return sse
2.2 可视化演示
# 生成示例数据
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.8, random_state=42)
# 运行 K-means
kmeans = KMeans(n_clusters=4, random_state=42)
kmeans.fit(X)
sse = kmeans.calculate_sse(X)
# 创建可视化对比
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# 1. 原始数据
axes[0].scatter(X[:, 0], X[:, 1], s=20, c='gray', alpha=0.6)
axes[0].set_title("原始数据 (未聚类)")
axes[0].grid(True, alpha=0.3)
# 2. 聚类结果
colors = ['red', 'blue', 'green', 'purple']
for k in range(4):
cluster_points = X[kmeans.labels == k]
axes[1].scatter(cluster_points[:, 0], cluster_points[:, 1],
s=20, c=colors[k], alpha=0.6, label=f'簇{k}')
axes[1].scatter(kmeans.centroids[:, 0], kmeans.centroids[:, 1],
s=200, c='yellow', marker='*', edgecolors='black',
linewidths=1.5, label='质心')
axes[1].set_title(f"K-means 聚类结果 (SSE={sse:.2f})")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 3. 与真实标签对比
axes[2].scatter(X[:, 0], X[:, 1], c=kmeans.labels, s=20, alpha=0.6, cmap='tab20')
axes[2].set_title("算法聚类 vs 真实分布")
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
执行结果会显示三幅对比图:
- 左:原始未标记数据
- 中:K-means 聚类结果(不同颜色表示不同簇,星号为质心)
- 右:聚类结果与真实分布的对比
三、核心概念深入解析
3.1 K 的含义:算法的最关键参数
| 概念 | 解释 | 类比 |
|---|---|---|
| K 值 | 要划分的簇的数量 | 图书馆要设置多少个分类区域 |
| 质心 | 每个簇的中心点 | 每个分类区域的中心位置 |
| 分配规则 | 点属于最近的质心 | 每本书放到最近的中心区域 |
关键事实:K 是必须预先指定的超参数,它直接决定算法的输出结构。
3.2 SSE:评估聚类质量的核心指标
SSE(误差平方和)的计算过程可以用下图直观展示:

SSE 的数学计算过程:
对于簇 k:
SSE_k = Σ_{x∈C_k} (x₁ - μ_k₁)² + (x₂ - μ_k₂)² + ... + (x_d - μ_kd)²
总 SSE:
SSE_total = SSE_1 + SSE_2 + ... + SSE_K
SSE 的现实意义:
- SSE 越小 → 簇内点越紧凑 → 聚类效果越好
- SSE 随 K 增大而减小,但 K 过大时会导致过拟合
- 理想状态是找到 SSE 较小且 K 值合理的平衡点
四、K-means 算法的详细步骤
4.1 完整算法流程图
算法步骤:
1. 初始化 → 2. 分配 → 3. 更新 → 4. 判断 → 5. 重复/结束
↓ ↓ ↓ ↓
随机选择 计算距离 计算均值 质心变化
K个质心 分配最近 新质心 < 阈值?
4.2 每一步的时间复杂度
| 步骤 | 操作 | 时间复杂度 | 说明 |
|---|---|---|---|
| 1. 初始化 | 随机选择 K 个点 | O(K) | 通常可忽略 |
| 2. 分配 | 计算所有点到所有质心的距离 | O(n×K×d) | 主要开销 |
| 3. 更新 | 计算每个簇的均值 | O(n×d) | 需遍历所有点 |
| 4. 判断 | 检查质心变化 | O(K×d) | 通常可忽略 |
| 5. 总复杂度 | I 次迭代 | O(I×n×K×d) | 实际应用中的主要因素 |
符号说明:
- n: 样本数量
- K: 簇的数量
- d: 特征维度
- I: 迭代次数
五、如何确定最佳 K 值?
5.1 肘部法则(Elbow Method)
def elbow_method_visualization(X, max_K=10):
"""肘部法则可视化"""
sse_values = []
K_range = range(1, max_K+1)
for K in K_range:
kmeans = KMeans(n_clusters=K, random_state=42)
kmeans.fit(X)
sse_values.append(kmeans.calculate_sse(X))
# 绘制肘部曲线
plt.figure(figsize=(10, 6))
plt.plot(K_range, sse_values, 'bo-', linewidth=2, markersize=8)
plt.xlabel('簇数量 (K)', fontsize=12)
plt.ylabel('SSE (误差平方和)', fontsize=12)
plt.title('肘部法则:寻找最佳K值', fontsize=14)
plt.grid(True, alpha=0.3)
# 标记可能的肘点
for i in range(1, len(sse_values)-1):
reduction_rate = (sse_values[i-1] - sse_values[i]) / sse_values[i-1]
if reduction_rate < 0.1: # 变化率小于10%
plt.axvline(x=K_range[i], color='r', linestyle='--', alpha=0.5)
plt.text(K_range[i], sse_values[i]*0.8, f'K={K_range[i]}',
fontsize=10, ha='center', bbox=dict(boxstyle="round,pad=0.3",
facecolor="white", alpha=0.8))
print(f"建议K值: {K_range[i]} (SSE下降率: {reduction_rate:.2%})")
break
plt.show()
return sse_values
# 使用示例
sse_history = elbow_method_visualization(X, max_K=8)
肘部法则的直观解释:
- 随着 K 增大,SSE 会逐渐减小
- 当 K 增加到真实簇数时,SSE 下降速度会突然变缓
- 这个"拐点"就像手肘的弯曲处,因此得名
5.2 轮廓系数法
from sklearn.metrics import silhouette_score
def silhouette_analysis(X, max_K=10):
"""轮廓系数分析"""
silhouette_scores = []
K_range = range(2, max_K+1) # 轮廓系数需要至少2个簇
for K in K_range:
kmeans = KMeans(n_clusters=K, random_state=42)
kmeans.fit(X)
score = silhouette_score(X, kmeans.labels)
silhouette_scores.append(score)
# 可视化
plt.figure(figsize=(10, 6))
plt.plot(K_range, silhouette_scores, 'go-', linewidth=2, markersize=8)
plt.xlabel('簇数量 (K)', fontsize=12)
plt.ylabel('轮廓系数', fontsize=12)
plt.title('轮廓系数分析', fontsize=14)
plt.grid(True, alpha=0.3)
# 标记最佳K值
best_K = K_range[np.argmax(silhouette_scores)]
plt.axvline(x=best_K, color='r', linestyle='--', alpha=0.5)
plt.text(best_K, max(silhouette_scores), f'最佳K={best_K}',
fontsize=12, ha='center', va='bottom')
plt.show()
return best_K, silhouette_scores
轮廓系数取值范围为 [-1, 1]:
- 接近 1:聚类效果好
- 接近 0:簇重叠
- 接近 -1:点被分配到错误簇
六、总结与扩展
6.1 K-means 的核心要点
- 算法本质:通过交替执行"分配"和"更新"两步,最小化簇内误差平方和
- 时间复杂度:O(I×n×K×d),适合处理大规模数据
- 空间复杂度:O(n×K + K×d),内存友好
- 收敛性:保证收敛到局部最优,但不一定是全局最优
6.2 何时使用 K-means?
适用场景:
- 数据量较大,需要高效算法
- 数据集呈球形或凸形分布
- 簇的大小和密度相似
- 需要简单可解释的结果
不适用场景:
- 数据包含复杂形状的簇
- 簇的大小和密度差异很大
- 数据有大量噪声和异常值
- 不知道簇的数量 K
6.3 扩展学习方向
- K-medoids:使用实际数据点作为中心,对异常值更鲁棒
- Fuzzy C-means:允许点以概率形式属于多个簇
- DBSCAN:基于密度的聚类,无需指定 K
- 层次聚类:构建聚类树,可探索不同粒度
- 谱聚类:基于图论的聚类方法,可发现复杂形状簇
K-means 是聚类领域的入门算法,其简洁性和高效性使其成为实际应用中的首选工具。掌握 K-means 不仅学习了一个具体算法,更是理解了聚类问题的本质思考方式——在紧凑性与简洁性之间寻找平衡,在不确定中寻找结构。
欢迎阅读专栏《开源AI图像算法实战演练》:
专栏阅读指南
更多推荐


所有评论(0)