别再死记硬背KMeans公式了!用Python从零实现,带你搞懂聚类算法的‘质心’到底怎么动
·
从零实现KMeans聚类:用Python动态可视化质心迁移之谜
当你第一次接触KMeans算法时,是否曾被那些数学符号和公式吓到?随机初始化的质心如何在迭代中逐渐找到最佳位置?簇内平方和(Inertia)的下降过程究竟隐藏着什么规律?本文将带你用Python从零实现KMeans核心算法,并通过动态可视化揭开聚类过程中最关键的质心移动机制。不同于单纯记忆公式,我们将通过代码直观感受算法如何自主发现数据中的自然分组。
1. 理解KMeans的舞蹈:质心与数据点的互动艺术
想象一场精心编排的舞蹈,质心就像领舞者,数据点则是跟随者。每一轮迭代都是舞蹈动作的调整过程:
- 初始站位 :随机选择的质心就像不熟悉舞步的领舞者,站在舞池的任意位置
- 第一支舞 :每位跟随者(数据点)选择距离最近的领舞者(质心)组成临时舞群
- 调整队形 :领舞者移动到当前舞群的中心位置,形成更协调的队形
- 循环优化 :重复选择与调整,直到领舞者位置不再显著变化
用Python实现这个过程时,我们需要关注三个核心变量:
# 关键变量初始化示例
import numpy as np
np.random.seed(42) # 确保可重复性
# 生成模拟数据:300个二维点,明显分为3个簇
data = np.vstack([
np.random.normal(loc=[0,0], scale=0.5, size=(100,2)),
np.random.normal(loc=[5,5], scale=0.8, size=(100,2)),
np.random.normal(loc=[8,1], scale=0.3, size=(100,2))
])
k = 3 # 预设簇数量
max_iter = 100 # 最大迭代次数
tolerance = 1e-4 # 收敛阈值
提示:在实际应用中,k值的选择需要结合业务需求或肘部法则确定,这里我们假设已知最佳簇数为3
2. 算法核心实现:拆解KMeans的引擎部件
2.1 初始化阶段的策略选择
随机初始化质心看似简单,却直接影响算法收敛速度:
def initialize_centroids(data, k):
"""改进的初始化方法:避免质心过于接近"""
centroids = [data[np.random.randint(len(data))]]
for _ in range(1, k):
# 计算每个点到最近质心的距离
dists = np.array([min([np.linalg.norm(x-c) for c in centroids]) for x in data])
# 按距离加权概率选择下一个质心
probs = dists / dists.sum()
next_centroid = data[np.random.choice(len(data), p=probs)]
centroids.append(next_centroid)
return np.array(centroids)
这种方法相比完全随机初始化,能显著减少后续迭代次数。
2.2 分配阶段的距离计算优化
传统实现中,距离计算可能成为性能瓶颈。我们使用矩阵运算加速:
def assign_clusters(data, centroids):
"""向量化计算距离矩阵"""
# 扩展维度以便广播计算
expanded_data = data[:, np.newaxis, :]
expanded_centroids = centroids[np.newaxis, :, :]
# 计算欧式距离平方(避免开方运算)
distances = np.sum((expanded_data - expanded_centroids)**2, axis=2)
# 返回每个点的最近质心索引
return np.argmin(distances, axis=1)
2.3 更新阶段的质心重计算
质心更新需要处理可能的空簇情况:
def update_centroids(data, labels, k):
"""安全更新质心,处理空簇"""
new_centroids = []
for i in range(k):
# 获取当前簇所有点
cluster_points = data[labels == i]
if len(cluster_points) > 0:
new_centroids.append(cluster_points.mean(axis=0))
else:
# 若出现空簇,随机重新初始化该质心
new_centroids.append(data[np.random.randint(len(data))])
return np.array(new_centroids)
3. 可视化呈现:让算法过程一目了然
3.1 静态多帧对比法
展示关键迭代步骤的质心位置变化:
import matplotlib.pyplot as plt
def plot_kmeans_steps(data, all_centroids, labels_history):
plt.figure(figsize=(15,10))
for i, (centroids, labels) in enumerate(zip(all_centroids, labels_history)):
plt.subplot(2, 3, i+1)
# 绘制数据点按簇着色
plt.scatter(data[:,0], data[:,1], c=labels, cmap='viridis', alpha=0.5)
# 绘制质心轨迹
plt.scatter(centroids[:,0], centroids[:,1], c='red', marker='X', s=200)
plt.title(f'Iteration {i+1}')
plt.tight_layout()
plt.show()
3.2 动态实时演示
使用matplotlib动画功能展示质心移动过程:
from matplotlib.animation import FuncAnimation
def animate_kmeans(data, all_centroids, labels_history):
fig, ax = plt.subplots(figsize=(8,6))
def update(frame):
ax.clear()
centroids = all_centroids[frame]
labels = labels_history[frame]
# 绘制当前状态
scat = ax.scatter(data[:,0], data[:,1], c=labels, cmap='viridis', alpha=0.5)
centroids_plot = ax.scatter(centroids[:,0], centroids[:,1], c='red',
marker='X', s=200, edgecolor='black')
# 绘制质心移动轨迹
for i in range(len(centroids)):
path = np.array([c[i] for c in all_centroids[:frame+1]])
ax.plot(path[:,0], path[:,1], 'r--', alpha=0.3)
ax.set_title(f'Iteration {frame+1}')
return scat, centroids_plot
ani = FuncAnimation(fig, update, frames=len(all_centroids), interval=800, blit=False)
plt.close()
return ani
4. 算法调优与实战技巧
4.1 评估指标实现
除了观察Inertia下降,还需实现轮廓系数等评估指标:
from sklearn.metrics import silhouette_samples
def calculate_metrics(data, labels, centroids):
"""计算多种评估指标"""
# 计算Inertia
inertia = sum(np.linalg.norm(data[i]-centroids[labels[i]])**2
for i in range(len(data)))
# 计算轮廓系数
sil_samples = silhouette_samples(data, labels)
avg_silhouette = np.mean(sil_samples)
return {
'inertia': inertia,
'silhouette': avg_silhouette,
'cluster_sizes': np.bincount(labels)
}
4.2 常见问题解决方案
实际实现中可能遇到的典型问题及对策:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 质心震荡不收敛 | 学习率过高/数据尺度不一 | 数据标准化/设置收敛阈值 |
| 空簇频繁出现 | K值过大/初始化不当 | 改进初始化方法/合并相近簇 |
| 局部最优解 | 随机初始化敏感 | 多次运行取最优解 |
| 维度灾难 | 高维数据距离失效 | 特征选择/PCA降维 |
4.3 进阶优化方向
对于追求更高性能的场景,可以考虑:
# 使用Numba加速距离计算
from numba import njit
@njit
def euclidean_distance(x, y):
return np.sqrt(np.sum((x - y)**2))
# GPU加速版本示例
import cupy as cp
def gpu_kmeans(data, k, max_iter):
data_gpu = cp.asarray(data)
centroids = data_gpu[cp.random.choice(len(data), k, replace=False)]
for _ in range(max_iter):
# 在GPU上计算距离
distances = cp.linalg.norm(data_gpu[:, None] - centroids, axis=2)
labels = cp.argmin(distances, axis=1)
new_centroids = cp.array([data_gpu[labels==i].mean(axis=0)
for i in range(k)])
if cp.allclose(centroids, new_centroids):
break
centroids = new_centroids
return cp.asnumpy(centroids), cp.asnumpy(labels)
在完成基础实现后,尝试用不同分布的数据集测试算法表现。例如创建非球形分布数据,观察KMeans的局限性,这会自然引出对DBSCAN等密度聚类算法的学习需求。
更多推荐
所有评论(0)