聚类算法之推导流程
1 课程概览
本课讲解KMeans聚类算法的实现流程。包括确定K值、随机选择初始质心、计算距离、标记类别、重新计算质心、迭代直到收敛。
2 核心概念与定义
- K值:簇的数量(几个簇)。
- 质心:簇的中心点,不一定是真实存在的点。
- 距离计算:每个样本到K个质心的距离。
- 迭代:重复计算质心和分类。
- 收敛:新的中心点和上次的中心点一样,停止迭代。
3 算法与模型详解
3.1 KMeans实现流程
步骤:
- 确定K值:事先确定常数K(几个簇)
- 随机选择质心:随机选择K个样本点作为初始质心
- 计算距离:计算每个样本到K个质心的距离
- 标记类别:选择最近的质心作为标记类别
- 重新计算质心:根据每个类别的样本点重新计算新的质心
- 迭代:重复步骤3-5
- 收敛判断:如果新的中心点和上次的中心点一样,停止
3.2 流程详解
步骤1:确定K值
K = 簇的数量
- K=3:分3组
- K=5:分5组
步骤2:随机选择初始质心
随机选择K个样本点作为初始质心
- 选哪儿都无所谓
- 因为质心会一直变化
步骤3:计算距离
计算每个样本到K个质心的距离
- 每个样本都会算K次(有几个质心算几次)
- 看哪个距离最近
- 距离越短,相似性越高
步骤4:标记类别
选择最近的质心作为标记类别
- 每个样本分到距离最近的质心所在的簇
- 这是临时的,只是第一次
步骤5:重新计算质心
根据每个类别的样本点重新计算新的质心
- 重新计算质心位置
- 质心不一定是真实存在的点
- 坐标算对就可以
步骤6:迭代
所有样本围绕新的质心重新分类
- 刚才绿色组的,可能这次跑到其他组
- 会重新分组
步骤7:收敛判断
如果新的中心点和上次的中心点一样,停止
- 无可划分
- 程序结束
3.3 通俗理解
类比:分组长
- 假设要分7个组
- 选7个组长站在旁边
- 剩下的人自由选择去哪一组
- 选择关系好的、离得近的组长
KMeans流程:
- 每个点都会去找质心
- 算一遍距离
- 离谁近就分到哪个组
3.4 结束条件
结束条件:当这一次计算的中心点和上一次计算的中心点是一样的
说明:
- 无可划分
- 程序结束
- 停止迭代
4 代码示例
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
# 1. 生成数据
print("=== 1. 生成数据 ===")
X, y_true = make_blobs(
n_samples=300,
centers=3,
cluster_std=1.0,
random_state=42
)
print(f"数据形状: {X.shape}")
# 2. 手动实现KMeans流程
print("\n=== 2. 手动实现KMeans流程 ===")
def kmeans_manual(X, k, max_iters=100, random_state=42):
"""手动实现KMeans算法"""
np.random.seed(random_state)
# 步骤1: 确定K值
print(f"\n步骤1: 确定K值 = {k}")
# 步骤2: 随机选择初始质心
n_samples = X.shape[0]
initial_indices = np.random.choice(n_samples, k, replace=False)
centroids = X[initial_indices].copy()
print(f"步骤2: 随机选择初始质心")
print(f" 初始质心坐标:")
for i, c in enumerate(centroids):
print(f" 质心{i}: ({c[0]:.4f}, {c[1]:.4f})")
# 记录每次迭代的结果
history = [centroids.copy()]
labels_history = []
for iteration in range(max_iters):
# 步骤3: 计算每个样本到K个质心的距离
distances = np.zeros((n_samples, k))
for i in range(k):
distances[:, i] = np.sqrt(np.sum((X - centroids[i]) ** 2, axis=1))
# 步骤4: 标记类别(选择最近的质心)
labels = np.argmin(distances, axis=1)
labels_history.append(labels.copy())
# 步骤5: 重新计算质心
new_centroids = np.zeros_like(centroids)
for i in range(k):
if np.sum(labels == i) > 0:
new_centroids[i] = np.mean(X[labels == i], axis=0)
else:
new_centroids[i] = centroids[i]
# 步骤7: 收敛判断
if np.allclose(centroids, new_centroids):
print(f"\n步骤7: 第{iteration+1}次迭代后收敛")
print(f" 新的中心点和上一次一样,停止迭代")
break
centroids = new_centroids
history.append(centroids.copy())
if iteration < 5 or iteration % 10 == 0:
print(f"\n迭代{iteration + 1}:")
for i, c in enumerate(centroids):
print(f" 质心{i}: ({c[0]:.4f}, {c[1]:.4f})")
return centroids, labels, history, labels_history
# 运行手动KMeans
centroids, labels, history, labels_history = kmeans_manual(X, k=3)
# 3. 可视化迭代过程
print("\n=== 3. 可视化迭代过程 ===")
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
# 显示前5次迭代和最终结果
iterations_to_show = [0, 1, 2, 3, 4, len(history)-1]
for idx, iter_num in enumerate(iterations_to_show):
ax = axes[idx // 3, idx % 3]
if iter_num < len(labels_history):
labels = labels_history[iter_num]
else:
labels = labels_history[-1]
centroids = history[min(iter_num, len(history)-1)]
ax.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.6)
ax.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='x', s=200, linewidths=3)
ax.set_title(f'迭代{iter_num + 1}')
plt.tight_layout()
plt.savefig('kmeans_iterations.png', dpi=100)
print("迭代过程图已保存")
# 4. 最终结果
print("\n=== 4. 最终结果 ===")
print(f"最终质心坐标:")
for i, c in enumerate(centroids):
print(f" 质心{i}: ({c[0]:.4f}, {c[1]:.4f})")
# 5. 与sklearn对比
print("\n=== 5. 与sklearn对比 ===")
kmeans_sklearn = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_sklearn = kmeans_sklearn.fit_predict(X)
print(f"sklearn质心:")
for i, c in enumerate(kmeans_sklearn.cluster_centers_):
print(f" 质心{i}: ({c[0]:.4f}, {c[1]:.4f})")
# 6. 可视化对比
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.6)
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='x', s=200, linewidths=3)
plt.title('手动实现KMeans')
plt.subplot(1, 2, 2)
plt.scatter(X[:, 0], X[:, 1], c=labels_sklearn, cmap='viridis', alpha=0.6)
plt.scatter(kmeans_sklearn.cluster_centers_[:, 0],
kmeans_sklearn.cluster_centers_[:, 1],
c='red', marker='x', s=200, linewidths=3)
plt.title('sklearn KMeans')
plt.tight_layout()
plt.savefig('kmeans_comparison.png', dpi=100)
print("对比图已保存")
# 7. KMeans流程总结
print("\n=== 7. KMeans流程总结 ===")
print("""
KMeans算法流程:
1. 确定K值(簇的数量)
2. 随机选择K个样本点作为初始质心
3. 计算每个样本到K个质心的距离
4. 选择最近的质心作为标记类别
5. 根据每个类别的样本点重新计算新的质心
6. 重复步骤3-5
7. 如果新的中心点和上次的中心点一样,停止
""")
# 8. 逐步演示
print("\n=== 8. 逐步演示 ===")
def kmeans_step_by_step(X, k=3, random_state=42):
"""逐步演示KMeans"""
np.random.seed(random_state)
n_samples = X.shape[0]
print("步骤1: 确定K值")
print(f" K = {k}")
print("\n步骤2: 随机选择初始质心")
initial_indices = np.random.choice(n_samples, k, replace=False)
centroids = X[initial_indices].copy()
for i, c in enumerate(centroids):
print(f" 初始质心{i}: ({c[0]:.4f}, {c[1]:.4f})")
for iteration in range(3): # 只演示前3次
print(f"\n--- 迭代{iteration + 1} ---")
print("步骤3: 计算每个样本到K个质心的距离")
distances = np.zeros((n_samples, k))
for i in range(k):
distances[:, i] = np.sqrt(np.sum((X - centroids[i]) ** 2, axis=1))
print(f" 距离矩阵形状: {distances.shape}")
print("步骤4: 标记类别(选择最近的质心)")
labels = np.argmin(distances, axis=1)
for i in range(k):
count = np.sum(labels == i)
print(f" 簇{i}: {count}个样本")
print("步骤5: 重新计算质心")
new_centroids = np.zeros_like(centroids)
for i in range(k):
new_centroids[i] = np.mean(X[labels == i], axis=0)
print(f" 新质心{i}: ({new_centroids[i][0]:.4f}, {new_centroids[i][1]:.4f})")
print("步骤7: 收敛判断")
if np.allclose(centroids, new_centroids):
print(" 收敛!停止迭代")
break
else:
print(" 未收敛,继续迭代")
centroids = new_centroids
kmeans_step_by_step(X, k=3)
输出示例:
=== 1. 生成数据 ===
数据形状: (300, 2)
=== 2. 手动实现KMeans流程 ===
步骤1: 确定K值 = 3
步骤2: 随机选择初始质心
初始质心坐标:
质心0: (1.2345, 2.3456)
质心1: (-3.4567, 4.5678)
质心2: (5.6789, -1.2345)
迭代1:
质心0: (1.1234, 2.2345)
质心1: (-3.3456, 4.4567)
质心2: (5.5678, -1.1234)
步骤7: 第5次迭代后收敛
新的中心点和上一次一样,停止迭代
=== 4. 最终结果 ===
最终质心坐标:
质心0: (1.1234, 2.2345)
质心1: (-3.3456, 4.4567)
质心2: (5.5678, -1.1234)
=== 5. 与sklearn对比 ===
sklearn质心:
质心0: (1.1234, 2.2345)
质心1: (-3.3456, 4.4567)
质心2: (5.5678, -1.1234)
5 重难点与易错提醒
- ❗重点:K值是事先确定的簇的数量。
- ❗重点:质心不一定是真实存在的点。
- ❗重点:每次迭代都要重新计算质心。
- ❗重点:收敛条件是新中心点和上次一样。
- ⚠️易错:忘记收敛判断导致死循环。
- ⚠️易错:质心计算错误。
- 💡深入理解:KMeans是通过迭代优化质心位置。
6 课堂问答精选
Q: KMeans的收敛条件是什么?
A: 收敛条件:当这一次计算的中心点和上一次计算的中心点是一样的,说明无可划分,程序结束。
Q: 质心一定是真实存在的点吗?
A: 不一定。质心是簇内所有点的平均值,坐标算对就可以,不一定是真实存在的样本点。
Q: 为什么需要迭代?
A: 因为第一次分类后,质心位置会变化,所有样本需要围绕新的质心重新分类。只有当质心不再变化时,才说明聚类稳定。
7 本课小结
- 步骤1:确定K值。
- 步骤2:随机选择初始质心。
- 步骤3:计算距离。
- 步骤4:标记类别。
- 步骤5:重新计算质心。
- 步骤6:迭代。
- 步骤7:收敛判断。
8 延伸思考与实践
- 实践:手动实现KMeans算法。
- 预习:聚类算法评估指标。
- 思考:初始质心的选择对结果有什么影响?