news 2026/9/3 6:51:29

从零实现KMeans聚类算法:Python源码详解与实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现KMeans聚类算法:Python源码详解与实战应用

简介:本资源是一套面向机器学习初学者与数据挖掘实践者的Python版KMeans聚类算法完整实现,聚焦于无监督学习中的核心聚类任务,适用于课程设计、算法复现及科研预研等场景。压缩包共246个文件,总大小35.02MB,涵盖141个CSV格式的多源时序分解数据集(如EEMD、CEEMDAN系列)、43张PNG可视化图表(含聚类过程迭代图、簇中心演化与轮廓系数分析)、16个结构清晰的Python脚本(含数据标准化、肘部法则计算、KMeans主循环及评估模块),另有辅助配置与版本控制文件。已有60人学习下载,资源目录按功能分层组织,从原始数据→预处理→建模→评估→可视化形成闭环流程,所有代码可直接运行,配套图表直观呈现不同K值下的聚类效果差异,为理解算法原理与调参逻辑提供了扎实的实践支撑。

1. 项目概述:从理论到实践的KMeans之旅

如果你正在学习机器学习,或者手头有一堆数据想看看能不能分个类,那KMeans聚类算法几乎是你绕不开的第一个实战点。它不像那些需要你提前打好标签的分类算法,KMeans属于“无监督学习”,意思就是你只管把数据扔给它,它自己就能摸索着把相似的数据点归到一堆。这个“一堆”,在算法里就叫“簇”。我最早接触它是在处理用户行为数据的时候,面对几万条没有任何分类标记的点击流记录,KMeans帮我快速勾勒出了几个典型的用户群体画像,为后续的精准运营省下了大量人工标注的成本。今天,我就把手写Python实现KMeans源码的整个过程,连同如何准备和处理数据集的经验,一次性讲透。无论你是刚学完Python语法想找个项目练手,还是需要在工作中快速应用聚类分析,这篇内容都能让你不仅“跑通代码”,更能“吃透原理”,避开我当年踩过的那些坑。

简单来说,这个项目要干两件事:一是用纯Python(不直接调sklearnKMeans类)从头实现KMeans算法的核心逻辑;二是使用真实或模拟的数据集来验证我们写的算法对不对。通过这个实践,你会彻底明白“质心”是怎么移动的,“距离”是怎么计算的,以及为什么KMeans有时会给出不太理想的结果。我们会用最流行的numpy来处理数据计算,用matplotlib来可视化聚类过程,整个代码结构清晰,每一步都有对应的原理讲解。你会发现,看似神秘的机器学习算法,拆解开来都是扎实的数学和编程基本功。

2. KMeans核心原理与我们的实现思路

在动手写代码之前,我们必须把KMeans的“大脑”搞清楚。很多教程直接上公式,容易让人懵。我用一个最生活的例子来比喻:假设你是一个快递站老板,手下有几个快递员(质心),负责一片居民区(数据点)。你的目标是划分送货区域,让每个快递员负责的住户尽可能离他的站点近,同时各个区域住户数量均衡。

KMeans做的就是类似的事,它通过迭代来优化这个划分,过程可以概括为四个步骤:

  1. 初始化:随机在居民区里选几个点作为快递站的初始位置(初始质心)。
  2. 分配阶段:计算每一个住户(数据点)到所有快递站(质心)的距离,然后把这个住户分配给离他最近的那个快递员。
  3. 更新阶段:所有住户分配完毕后,每个快递员根据自己负责的所有住户的位置,重新计算一个新的站点位置(通常是这些住户位置的平均值,也就是“均值点”),并搬过去。
  4. 迭代:重复第2步和第3步,直到快递站的位置不再发生显著变化(或者说,每个住户所属的快递员不再改变),算法就收敛了。

这里有几个关键点决定了我们代码怎么写:

  • 距离度量:我们怎么定义“远近”?最常用的是欧几里得距离,也就是我们中学学的两点间直线距离。在代码里,这就是numpy向量运算的优势所在。
  • 质心初始化:随机选虽然简单,但可能导致结果不好或收敛慢。我们实现时会采用更常用的“K-Means++”初始化方法,它能让初始质心彼此远离,效果更好更稳定。这是第一个要动手实现的重要改进。
  • 停止条件:通常有两种:一是质心的移动距离小于一个很小的阈值(比如1e-4);二是迭代次数达到了预设的最大值。我们的代码里会把这两个都加上。

基于这个思路,我们的代码骨架就出来了:一个KMeans类,包含初始化方法、一个拟合方法(包含分配和更新的循环)、以及一个预测方法。我们会把计算距离、分配簇、更新质心这些核心操作都拆成独立的函数,这样代码既好读,也方便调试。

注意:自己实现算法最大的好处是“可控”。你用sklearn的黑盒子,如果聚类效果不好,可能很难定位问题。而自己写的代码,你可以在每一轮迭代后打印质心位置、每个簇的样本数,甚至画图看看,问题出在哪一目了然。

3. 手把手实现KMeans核心源码

接下来,我们进入实战环节。我会分模块讲解代码,并解释每一行背后的意图。建议你打开编辑器跟着敲一遍。

3.1 环境准备与工具导入

首先,确保你的Python环境里安装了必要的科学计算库。打开终端,用pip安装:

pip install numpy matplotlib scikit-learn
  • numpy:核心,用于高效的数组和矩阵运算,所有距离计算、均值计算都靠它。
  • matplotlib:用于可视化,我们可以把聚类过程动态地画出来,非常直观。
  • scikit-learn:我们不用它的KMeans,但会用它来生成模拟数据集,以及最后和我们自己实现的算法结果做对比验证,这是检验我们代码正确性的好方法。

然后,在Python文件的开头,导入这些库:

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs # 用于生成模拟聚类数据 from sklearn.metrics import pairwise_distances_argmin # 用于高效计算最近质心,我们后面会自己实现类似功能 import random

3.2 KMeans类骨架与初始化

我们定义一个KMeans类。__init__方法用于接收算法的主要参数。

class MyKMeans: def __init__(self, n_clusters=3, max_iter=300, tol=1e-4, init='k-means++', random_state=None): """ 初始化KMeans参数。 :param n_clusters: 要形成的簇数,也是质心数量。必须事先指定,这是KMeans的主要缺点之一。 :param max_iter: 最大迭代次数,防止不收敛时无限循环。 :param tol: 容忍度,当质心移动距离小于此值时,认为已收敛,停止迭代。 :param init: 质心初始化方法,'random'为随机选择,'k-means++'为改进方法。 :param random_state: 随机种子,固定后可使每次运行结果一致,便于调试。 """ self.n_clusters = n_clusters self.max_iter = max_iter self.tol = tol self.init = init self.random_state = random_state self.centroids = None # 质心坐标,形状为 (n_clusters, n_features) self.labels = None # 每个样本点所属的簇标签 self.inertia_ = None # 所有样本到其所属质心的距离平方和,用于衡量聚类效果,越小越好

这里inertia_(惯性)是聚类效果的一个内部指标,就是所有样本点到其所属质心距离的平方和。算法优化的目标其实就是最小化这个值。

3.3 核心方法一:质心初始化(K-Means++)

随机初始化质心可能导致糟糕的局部最优解。K-Means++的思路是让初始质心尽可能彼此远离。

  1. 第一个质心从数据点中随机选取。
  2. 对于每个数据点,计算它到已选质心的最短距离(即离已选质心中最近的那个的距离)。
  3. 将这些最短距离的平方作为概率权重,随机选择下一个质心。距离越远的点,被选为下一个质心的概率越大。
  4. 重复步骤2和3,直到选够K个质心。
def _initialize_centroids(self, X): """使用K-Means++方法初始化质心。""" n_samples, n_features = X.shape centroids = np.zeros((self.n_clusters, n_features)) # 设置随机种子以保证可复现性 if self.random_state is not None: np.random.seed(self.random_state) # 1. 随机选择第一个质心 first_idx = np.random.randint(n_samples) centroids[0] = X[first_idx] # 2. 选择剩余的 K-1 个质心 for i in range(1, self.n_clusters): # 计算每个样本点到已有质心的最小距离平方 distances = np.array([min([np.linalg.norm(x - c) ** 2 for c in centroids[:i]]) for x in X]) # 将距离平方转换为概率(距离越远,概率越大) probabilities = distances / distances.sum() # 根据概率分布随机选择一个样本点作为下一个质心 next_idx = np.random.choice(n_samples, p=probabilities) centroids[i] = X[next_idx] return centroids

这个初始化方法比纯随机多了一些计算,但能显著提升最终聚类效果和收敛速度,是实践中几乎必用的技巧。

3.4 核心方法二:分配样本点到最近质心

这一步需要计算每个样本点到所有质心的距离,并找到最近的质心索引。我们使用欧氏距离。为了提高效率,我们利用numpy的广播机制进行向量化计算,避免写低效的Python循环。

def _assign_clusters(self, X, centroids): """ 将每个样本点分配到最近的质心。 :return: labels - 每个样本点所属簇的索引数组。 """ # 计算样本点与所有质心的距离矩阵,形状为 (n_samples, n_clusters) # np.linalg.norm用于计算范数,axis=1指定按行计算,即每个样本点与一个质心的距离 # 这里利用广播,X[:, np.newaxis, :] 形状 (n_samples, 1, n_features) # centroids[np.newaxis, :, :] 形状 (1, n_clusters, n_features) # 相减后形状 (n_samples, n_clusters, n_features),再求范数得到距离 distances = np.linalg.norm(X[:, np.newaxis, :] - centroids[np.newaxis, :, :], axis=2) # 找到每个样本点距离最小的那个质心的索引 labels = np.argmin(distances, axis=1) return labels

向量化运算是numpy的精华,它底层用C实现,比用Python写for循环快成百上千倍。理解这里的广播机制对写好科学计算代码至关重要。

3.5 核心方法三:更新质心位置

根据当前分配结果,重新计算每个簇的质心(即该簇所有样本点的均值)。

def _update_centroids(self, X, labels): """ 根据当前样本点的分配,重新计算每个簇的质心。 :return: new_centroids - 新的质心坐标。 """ new_centroids = np.zeros((self.n_clusters, X.shape[1])) for i in range(self.n_clusters): # 找出所有属于簇i的样本点 cluster_points = X[labels == i] # 防止空簇出现(如果某个簇没有点,则保持原质心不变,或重新初始化) if len(cluster_points) > 0: new_centroids[i] = cluster_points.mean(axis=0) else: # 如果出现空簇,一种处理方法是将其质心设置为一个随机数据点 new_centroids[i] = X[np.random.randint(X.shape[0])] return new_centroids

这里有一个重要的边界情况处理:空簇。在迭代过程中,有可能某个质心分配不到任何样本点。如果不处理,计算均值时会出错。我们的策略是,如果遇到空簇,就随机选择一个数据点作为该簇的新质心。还有其他策略,比如将空簇的质心设置为离它最远的那个样本点,或者直接忽略它(但这样簇数就减少了)。

3.6 核心方法四:拟合主循环

现在,我们把初始化、分配、更新串起来,形成完整的训练流程。

def fit(self, X): """ 在数据X上拟合KMeans模型。 :param X: 训练数据,形状为 (n_samples, n_features)。 :return: self - 返回实例自身,符合scikit-learn API风格。 """ n_samples, n_features = X.shape # 1. 初始化质心 if self.init == 'k-means++': self.centroids = self._initialize_centroids(X) else: # random random_indices = np.random.choice(n_samples, self.n_clusters, replace=False) self.centroids = X[random_indices] # 迭代优化 for iteration in range(self.max_iter): # 2. 分配簇 self.labels = self._assign_clusters(X, self.centroids) # 3. 更新质心 new_centroids = self._update_centroids(X, self.labels) # 4. 检查收敛条件:质心移动的平均距离是否小于容忍度 centroid_shift = np.linalg.norm(new_centroids - self.centroids, axis=1).mean() if centroid_shift < self.tol: print(f"迭代在第 {iteration + 1} 轮收敛。") break self.centroids = new_centroids else: # 如果for循环正常结束(未break),说明达到了最大迭代次数 print(f"达到最大迭代次数 {self.max_iter},可能未完全收敛。") # 计算最终的惯性(距离平方和) self.inertia_ = 0 for i in range(self.n_clusters): cluster_points = X[self.labels == i] if len(cluster_points) > 0: distances = np.linalg.norm(cluster_points - self.centroids[i], axis=1) self.inertia_ += np.sum(distances ** 2) return self def predict(self, X): """预测新数据点所属的簇。""" # 本质上就是分配步骤 return self._assign_clusters(X, self.centroids)

fit方法是核心,它控制着迭代的节奏。我们添加了详细的打印信息,方便观察收敛过程。predict方法很简单,就是对新的数据点,用训练好的质心去分配簇标签。

实操心得:在调试自己实现的算法时,我强烈建议在fit循环里加入可视化代码,把每一轮迭代的质心和数据点分布画出来。这能让你直观地看到质心是如何“追逐”数据点并最终稳定的,对理解算法有巨大帮助。这也是使用matplotlib动态绘图的好机会。

4. 数据集准备、实验与结果可视化

算法写好了,我们需要数据来测试它。这里介绍两种常用的数据来源:模拟数据和真实数据。

4.1 使用模拟数据集进行验证

scikit-learnmake_blobs函数可以生成适合聚类算法的数据集,它会产生几个高斯分布的“斑点”。

# 生成模拟数据 def generate_sample_data(n_samples=300, centers=4, random_state=42): """ 生成用于聚类的模拟数据。 :param n_samples: 总样本数。 :param centers: 簇的中心数量(即真实的K)。 :param random_state: 随机种子。 :return: 特征数据X,以及真实标签(仅用于评估,算法本身不知道)。 """ X, y_true = make_blobs(n_samples=n_samples, centers=centers, cluster_std=0.8, # 每个簇的标准差,控制簇的紧密程度 random_state=random_state) return X, y_true # 生成数据 X, y_true = generate_sample_data(n_samples=300, centers=4) # 可视化原始数据 plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.scatter(X[:, 0], X[:, 1], c=y_true, s=30, cmap='viridis', edgecolors='k') plt.title("原始数据(真实标签)") plt.xlabel("特征 1") plt.ylabel("特征 2")

这段代码生成了300个样本点,它们实际上来自4个不同的分布(centers=4)。我们用颜色y_true区分它们,这是“上帝视角”的真实分类。我们的KMeans算法在训练时是看不到y_true的。

4.2 运行自实现KMeans并可视化过程

为了更生动,我们修改一下fit方法,让它能记录每一轮迭代的质心位置,然后我们画一个动态过程图。这里为了简化,我们写一个专门用于演示的拟合函数。

def fit_and_plot(X, n_clusters=4, max_iter=10): """运行KMeans并绘制每次迭代的结果。""" km = MyKMeans(n_clusters=n_clusters, max_iter=max_iter, init='k-means++', random_state=42) # 手动执行迭代并记录 centroids_history = [] labels_history = [] # 初始化 centroids = km._initialize_centroids(X) if km.init == 'k-means++' else X[np.random.choice(X.shape[0], n_clusters, replace=False)] centroids_history.append(centroids.copy()) for it in range(max_iter): # 分配 labels = km._assign_clusters(X, centroids) labels_history.append(labels.copy()) # 更新 new_centroids = km._update_centroids(X, labels) centroids_history.append(new_centroids.copy()) # 检查收敛(简化版) if np.linalg.norm(new_centroids - centroids) < 1e-4: print(f"提前收敛于迭代 {it+1}") break centroids = new_centroids # 绘制迭代过程 fig, axes = plt.subplots(2, (max_iter+1)//2, figsize=(15, 8)) axes = axes.flatten() for i in range(len(centroids_history)-1): ax = axes[i] ax.scatter(X[:, 0], X[:, 1], c=labels_history[i], s=30, cmap='viridis', alpha=0.6) ax.scatter(centroids_history[i][:, 0], centroids_history[i][:, 1], c='red', s=200, marker='X', label=f'质心-迭代{i}') # 用线连接新旧质心,显示移动轨迹 for j in range(n_clusters): ax.plot([centroids_history[i][j, 0], centroids_history[i+1][j, 0]], [centroids_history[i][j, 1], centroids_history[i+1][j, 1]], 'r--', linewidth=1) ax.set_title(f'迭代 {i+1}') ax.legend() for j in range(i+1, len(axes)): axes[j].axis('off') plt.tight_layout() plt.show() return km # 运行并绘图 km_model = fit_and_plot(X, n_clusters=4, max_iter=6)

运行这段代码,你会看到一系列子图,展示了质心(红色大叉)如何一步步移动到簇的中心,以及数据点的颜色(所属簇)如何随之变化。这个可视化是理解KMeans动态过程的无价之宝。

4.3 与Scikit-learn官方实现对比

为了验证我们代码的正确性,最好的方法是用同样的数据和参数,运行sklearn的KMeans,对比结果。

from sklearn.cluster import KMeans as SKLearnKMeans # 使用我们自实现的模型 my_km = MyKMeans(n_clusters=4, random_state=42).fit(X) my_labels = my_km.labels my_centroids = my_km.centroids # 使用sklearn的模型 sk_km = SKLearnKMeans(n_clusters=4, init='k-means++', random_state=42, n_init='auto').fit(X) sk_labels = sk_km.labels_ sk_centroids = sk_km.cluster_centers_ # 比较质心位置 print("自实现质心:\n", my_centroids) print("\nSklearn质心:\n", sk_centroids) print("\n质心间平均差异:", np.linalg.norm(my_centroids - sk_centroids, axis=1).mean()) # 比较惯性(距离平方和) print("\n自实现惯性:", my_km.inertia_) print("Sklearn惯性:", sk_km.inertia_) # 可视化对比结果 fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # 真实分布 axes[0].scatter(X[:, 0], X[:, 1], c=y_true, s=30, cmap='viridis', edgecolors='k') axes[0].set_title("真实分布") axes[0].set_xlabel("特征 1") axes[0].set_ylabel("特征 2") # 自实现结果 axes[1].scatter(X[:, 0], X[:, 1], c=my_labels, s=30, cmap='viridis', edgecolors='k') axes[1].scatter(my_centroids[:, 0], my_centroids[:, 1], c='red', s=200, marker='X') axes[1].set_title("自实现 KMeans") axes[1].set_xlabel("特征 1") # Sklearn结果 axes[2].scatter(X[:, 0], X[:, 1], c=sk_labels, s=30, cmap='viridis', edgecolors='k') axes[2].scatter(sk_centroids[:, 0], sk_centroids[:, 1], c='red', s=200, marker='X') axes[2].set_title("Sklearn KMeans") axes[2].set_xlabel("特征 1") plt.tight_layout() plt.show()

如果我们的实现正确,两个模型的质心位置应该非常接近,惯性值也相差无几。由于KMeans对初始质心敏感(即使都用K-Means++,随机性也不同),结果可能在小数点后几位有细微差别,但聚类的大格局应该是一致的。通过这个对比,你能极大增强对自己代码的信心。

4.4 尝试真实数据集:鸢尾花数据集

模拟数据太完美,我们试试经典的真实数据集——鸢尾花数据集。它包含150个样本,4个特征(花萼和花瓣的长宽),3个类别(山鸢尾、变色鸢尾、维吉尼亚鸢尾)。我们只用前两个特征以便可视化。

from sklearn.datasets import load_iris # 加载数据 iris = load_iris() X_iris = iris.data[:, :2] # 只取前两个特征用于二维可视化 y_iris_true = iris.target # 运行自实现KMeans,假设我们不知道是3类,先尝试分为3类 km_iris = MyKMeans(n_clusters=3, random_state=42).fit(X_iris) labels_iris = km_iris.labels # 可视化 plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.scatter(X_iris[:, 0], X_iris[:, 1], c=y_iris_true, s=50, cmap='viridis') plt.title("鸢尾花真实类别") plt.xlabel("花萼长度 (cm)") plt.ylabel("花萼宽度 (cm)") plt.subplot(1, 2, 2) plt.scatter(X_iris[:, 0], X_iris[:, 1], c=labels_iris, s=50, cmap='viridis') plt.scatter(km_iris.centroids[:, 0], km_iris.centroids[:, 1], c='red', s=200, marker='X', label='质心') plt.title("KMeans聚类结果 (K=3)") plt.xlabel("花萼长度 (cm)") plt.legend() plt.show()

你会发现,仅用两个特征,KMeans的结果与真实类别已经有较高的吻合度,但也存在一些错分的点。这引出了KMeans的两个关键问题:1)K值需要预先指定,我们这里知道是3类,但现实中往往不知道;2)特征选择和尺度对结果影响很大,如果我们用了全部4个特征,或者特征之间的量纲差异巨大,结果可能不同。

5. 深入探讨:K值选择、局限性与优化

自己实现了一遍之后,你对KMeans的优缺点会有更深刻的认识。这里分享几个必须知道的进阶话题。

5.1 如何确定最佳的K值?

KMeans最大的痛点就是需要人工指定K(簇数)。有几种常用的方法:

  • 肘部法则:绘制不同K值对应的惯性(inertia_)曲线。惯性会随着K增大而减小,但减小的幅度会变缓。那个拐点(像手肘一样)对应的K值通常是一个好的选择。
  • 轮廓系数:它结合了簇内的凝聚度和簇间的分离度。轮廓系数越接近1,说明聚类效果越好。可以计算不同K值下的平均轮廓系数,取最大的那个K。

我们可以用自实现的模型来计算肘部法则曲线:

def plot_elbow_method(X, max_k=10): """绘制肘部法则图以帮助选择K值。""" inertias = [] K_range = range(1, max_k+1) for k in K_range: km = MyKMeans(n_clusters=k, random_state=42) km.fit(X) inertias.append(km.inertia_) plt.figure(figsize=(8, 5)) plt.plot(K_range, inertias, 'bo-') plt.xlabel('簇的数量 K') plt.ylabel('惯性 (Inertia)') plt.title('肘部法则 (Elbow Method)') plt.xticks(K_range) plt.grid(True) plt.show() # 对模拟数据使用肘部法则 plot_elbow_method(X, max_k=10)

对于之前生成的4个簇的模拟数据,你会在K=4附近看到一个明显的拐点。而对于更复杂的数据,拐点可能不明显,这就需要结合轮廓系数和业务理解来综合判断。

5.2 KMeans的主要局限性

通过实践,你应该能感受到KMeans的“脾气”:

  1. 需要预先指定K:这是最常被诟病的一点。
  2. 对初始值敏感:虽然K-Means++改善了,但多次运行结果仍可能略有不同。一个实践技巧是:多次运行(比如10次),选择惯性最小的那次结果作为最终模型
  3. 对异常值敏感:质心是均值,异常值会把它“拉偏”。
  4. 假设簇是凸形且大小相近:KMeans使用欧氏距离,它倾向于发现球状或凸形的簇。对于流形、环形或不规则形状的簇,效果会很差。
  5. 特征尺度影响大:如果特征A的范围是0-100,特征B的范围是0-1,那么距离计算会被特征A主导。因此,在应用KMeans前,对特征进行标准化(如Z-score标准化)是必不可少的预处理步骤

5.3 针对局限性的优化实践

针对上述问题,有一些常见的应对策略:

  • 数据预处理
    from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 标准化,使每个特征均值为0,方差为1 km.fit(X_scaled) # 在标准化后的数据上拟合
  • 多次初始化:在我们的MyKMeans类中,可以很容易地添加一个n_init参数,在fit方法中运行多次,保留最佳结果。
    class MyKMeansEnhanced(MyKMeans): def __init__(self, n_clusters=3, max_iter=300, tol=1e-4, init='k-means++', n_init=10, random_state=None): super().__init__(n_clusters, max_iter, tol, init, random_state) self.n_init = n_init # 初始化次数 def fit(self, X): best_inertia = float('inf') best_centroids = None best_labels = None for init_idx in range(self.n_init): # 调用父类的fit,但每次用不同的随机种子 current_seed = self.random_state + init_idx if self.random_state is not None else None super().__init__(self.n_clusters, self.max_iter, self.tol, self.init, current_seed) super().fit(X) if self.inertia_ < best_inertia: best_inertia = self.inertia_ best_centroids = self.centroids.copy() best_labels = self.labels.copy() self.centroids = best_centroids self.labels = best_labels self.inertia_ = best_inertia return self
  • 处理非凸簇:如果数据形状复杂,可能需要考虑其他算法,如DBSCAN(基于密度)或谱聚类。

6. 常见问题与调试技巧实录

在自己实现和使用的过程中,我遇到过不少坑。这里总结一下,希望能帮你节省时间。

6.1 空簇问题及其处理

_update_centroids方法中我们已经提到了空簇。除了随机重置,另一种策略是选择距离当前所有质心最远的那个数据点作为新质心,这有助于探索新的区域。你可以根据实际情况选择策略。在调试时,如果发现某个簇的样本数突然变为0,可以打印出当前迭代信息来检查。

6.2 算法不收敛或震荡

有时你会发现质心在两个位置之间来回跳,无法稳定。这通常是因为:

  • 数据中有重复点或非常近的点,导致质心计算时在两个相同位置来回切换。检查数据去重。
  • 容忍度tol设置得太小,而迭代次数max_iter不够。可以适当增大tol(比如到1e-3)或增加max_iter
  • K值设置不合理,远大于或小于真实簇数。用肘部法则重新评估K。

一个实用的调试方法是:在fit循环中,打印每一轮迭代的质心移动距离centroid_shift和惯性inertia_,观察它们的变化趋势。正常情况下,两者都应该单调递减并趋于平缓。

6.3 结果不一致问题

即使固定了random_state,你的结果也可能和sklearn的结果在标签编号上不一致。比如,你的算法把A簇标为0,B簇标为1,而sklearn可能把A簇标为2,B簇标为0。这通常是正常的,因为KMeans的簇标签本身没有语义,只是索引。你需要关注的是聚类本身的划分是否一致。可以通过构建混淆矩阵(需要知道真实标签)或计算调整互信息等指标来评估,这些指标与标签编号无关。

6.4 性能优化技巧

我们实现的版本为了清晰,在有些地方用了Python循环(如更新质心时遍历每个簇)。对于大数据集,这可能会成为瓶颈。完全向量化的更新方式更高效,但代码稍复杂。一个折中的优化是,在_update_centroids中,可以使用numpybincountnp.add.at等高级操作来避免显式循环。不过对于学习和中等规模数据,我们当前的实现已经足够清晰和高效。

最后,把完整的MyKMeans类、数据生成、实验对比和可视化代码整合到一个脚本里,就是一个非常扎实的KMeans聚类算法实践项目。它不仅是一段可运行的代码,更是一个理解无监督学习核心思想的窗口。当你下次需要做客户分群、图像颜色量化、文档主题发现时,你会感谢自己曾经亲手实现过这个经典的算法。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 6:50:14

ThinkPHP6+小程序课程表源码工程化实践

简介&#xff1a;这是一套基于ThinkPHP框架开发的课程表小程序全开源源码&#xff0c;面向高校学生、情侣用户及教务系统轻量级对接场景&#xff0c;解决个人课表管理、跨设备同步、社交化课程共享等实际需求。资源包共4968个文件&#xff0c;含1814个PHP后端逻辑文件、1666个J…

作者头像 李华
网站建设 2026/9/3 6:48:02

LLM全栈实战:从Prompt设计到RAG与Agent系统集成

上周帮一个朋友排查他们团队用 RAG 搭建的内部知识库&#xff0c;发现一个挺有意思的现象&#xff1a;他们花了两周时间调通了流程&#xff0c;单次查询效果不错&#xff0c;但一到批量处理就频繁超时&#xff0c;团队里有人开始怀疑是不是模型选错了&#xff0c;甚至有人提议换…

作者头像 李华
网站建设 2026/9/3 6:47:40

AI编程助手:非程序员如何从零开发可上线项目

那天下午&#xff0c;朋友发来一个链接&#xff0c;是他用 AI 生成的一个小型网站原型。他完全不会写代码&#xff0c;但网站有基础页面、简单交互&#xff0c;甚至能提交表单。他问我&#xff1a;“这样搞出来的东西&#xff0c;真能上线用吗&#xff1f;”这个问题背后&#…

作者头像 李华
网站建设 2026/9/3 6:47:18

上海中央空调铜管漏氟维修 欧米到家查漏补焊加氟一站式服务

核心导读上海中央空调维修找欧米到家&#xff0c;全品牌全品类覆盖&#xff0c;持证师傅上门&#xff0c;标准化服务流程&#xff0c;先检测后报价&#xff0c;修不好不收费。服务热线&#xff1a;400-996-9791&#xff0c;官网&#xff1a;https://www.oumidj.com/。无论您家是…

作者头像 李华
网站建设 2026/9/3 6:46:51

VB6网页自动化填表:基于WebBrowser控件实现DOM操作与流程控制

简介&#xff1a;本资源是一套基于Visual Basic实现网页自动填表功能的完整开发实践包&#xff0c;面向VB初学者、Windows桌面应用开发者及需要自动化处理网页表单&#xff08;如登录、注册、数据录入&#xff09;的技术人员。资源聚焦WebBrowser控件调用、HTML DOM元素定位、表…

作者头像 李华