news 2026/9/3 14:07:47

从零手写KMeans聚类算法:原理、实现与实战优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写KMeans聚类算法:原理、实现与实战优化

简介:本资源是一套面向机器学习初学者与数据挖掘实践者的Python版KMeans聚类算法完整实现方案,聚焦于无监督学习中的典型聚类任务,适用于课程设计、算法复现及科研预研等场景。压缩包共246个文件,总大小35.02MB,其中141个CSV数据集涵盖EEMD、CEEMDAN等多种信号分解衍生特征(如data_EEMDV2_.csv、data_CEEMDAN.csv等),43张PNG可视化图表直观呈现聚类过程与结果,16个Python脚本覆盖数据标准化、肘部法确定K值、迭代更新、轮廓系数评估等核心环节,另有辅助配置与版本控制文件。已有60人下载学习,资源结构清晰、流程闭环——从原始数据加载、特征工程、算法实现到评估与可视化一应俱全,所有代码可直接运行,无需额外依赖配置,是理解KMeans原理与动手实践的高实用性参考范例。

1. 从零到一:手搓KMeans算法的动机与价值

最近在整理硬盘里的老项目,翻出来一堆当年为了应付课程作业写的机器学习代码,其中就有一个用Python实现的KMeans聚类。现在回头看,那些代码虽然能跑出结果,但注释混乱、结构松散,完全就是“一次性”的产物。这让我意识到,很多朋友在学习算法时,可能也止步于“调包跑通”,对算法内部的运作机制、代码实现的优雅性以及那些教科书上不会写的“坑”知之甚少。所以,我决定重新梳理一遍,用一篇长文,把KMeans算法从核心原理、数学推导,到手写实现、性能优化,再到实战应用和数据集处理,完完整整地讲透。这不仅仅是给你一份能运行的源码,更是给你一套理解、实现并驾驭这个经典无监督学习算法的完整方法论。

KMeans算法堪称聚类领域的“Hello World”,它思想直观、实现相对简单,是入门机器学习的绝佳起点。但简单并不意味着肤浅。你是否想过,为什么初始中心点的选择如此重要,以至于催生了KMeans++算法?算法迭代停止的条件,除了最大迭代次数,那个看似简单的“中心点不再变化”背后,隐藏着怎样的数值稳定性问题?当我们用sklearnKMeans三行代码搞定一切时,是否错过了理解数据与距离度量、聚类与评估之间微妙关系的机会?手写实现的意义,就在于强迫我们直面这些细节。通过亲手构建,你会对距离计算、向量化操作、循环优化有更深的体会,这些经验在你未来处理更复杂的模型或进行算法优化时,将是无价的。

本文将围绕“Python实现KMeans聚类算法源码及数据集”这个核心,展开一次深度之旅。我们会从最基础的欧氏距离计算开始,一步步搭建起完整的KMeans类。过程中,我会穿插大量我在实际项目中踩过的坑和总结的技巧,比如如何高效计算样本到所有质心的距离、如何处理空簇这个令人头疼的问题、如何选择合适的数据集来验证你的算法,以及如何用可视化的方式直观地理解聚类的过程和结果。无论你是刚接触机器学习的新手,想通过一个具体项目巩固Python和算法基础,还是有一定经验的开发者,希望深入理解聚类算法的底层逻辑以便进行定制化开发,这篇文章都将提供充足的“干货”。我们将使用纯NumPy来实现核心算法,这不仅是为了理解原理,也是为了获得对计算过程的最大控制权,并体验从零构建一个机器学习组件的成就感。

2. KMeans算法核心原理与数学拆解

在动手写代码之前,我们必须把KMeans算法的“骨架”和“肌肉”看清楚。很多人对KMeans的印象停留在“迭代求平均”上,这没错,但过于笼统。让我们深入到数学和流程的细节中。

2.1 算法目标与数学模型

KMeans算法的终极目标,是将一个包含N个数据点的数据集X= {x₁,x₂, ...,x_N} 划分成K个簇(Cluster)。每个簇有一个中心点,称为质心(Centroid)。算法追求的是:让同一个簇内的数据点彼此尽可能相似(距离近),而不同簇的数据点尽可能不同(距离远)

这个目标被形式化为最小化一个目标函数,即误差平方和(Sum of Squared Errors, SSE),有时也叫“畸变”(Distortion)或“簇内平方和”(Within-Cluster Sum of Squares, WCSS)。其数学表达式如下:

SSE = Σ_{i=1}^{K} Σ_{x ∈ C_i} ||x - μ_i||²

这里,C_i表示第i个簇的集合,μ_i是第i个簇的质心,||x - μ_i||表示数据点x到其所属质心μ_i的欧氏距离(默认情况下)。SSE衡量的是所有数据点到其所属簇质心的总距离的平方和。KMeans算法的迭代过程,本质上就是在寻找能够使SSE最小化的那组质心{μ₁, μ₂, ..., μ_K}和簇划分{C₁, C₂, ..., C_K}

注意:SSE是评估聚类效果的一个重要内部指标。通常,SSE越小,说明簇内聚合度越高。但要注意,随着簇数K的增加,SSE必然会下降(极端情况每个点都是一个簇,SSE为0),因此不能单纯追求SSE最小化来选择K,还需要结合肘部法则(Elbow Method)或轮廓系数(Silhouette Score)等方法。

2.2 经典迭代流程:期望最大化(EM)思想的体现

KMeans的迭代过程完美体现了期望最大化(Expectation-Maximization, EM)算法的思想,具体分为两个交替进行的步骤:

步骤一:分配(Assignment / E-Step)固定当前的所有质心{μ₁, ..., μ_K},遍历数据集中的每一个数据点x_j,计算它到K个质心中每一个的距离。然后,将x_j分配给距离最近的那个质心所在的簇。用公式表示就是:C_i = { x_j : ||x_j - μ_i||² ≤ ||x_j - μ_l||², ∀ l, 1 ≤ l ≤ K }这一步是“期望”步,为每个数据点确定其当前的“归属”(期望的簇标签)。

步骤二:更新(Update / M-Step)在完成了所有数据点的分配之后,每个簇C_i都包含了一组数据点。接着,重新计算每个簇的质心。新的质心就是该簇内所有数据点的均值向量。计算公式为:μ_i = (1 / |C_i|) * Σ_{x ∈ C_i} x这一步是“最大化”步,在给定当前分配的情况下,找到能使SSE最小化的新质心(即求均值)。

这两个步骤不断循环,直到满足某个停止条件,比如:

  1. 质心不再发生变化:新计算出的质心与上一轮的质心完全相同(在数值精度内)。
  2. 达到最大迭代次数:防止算法在某些情况下无限循环。
  3. SSE的变化小于某个阈值:目标函数的优化已趋于稳定。

2.3 关键细节与常见误解

理解了主干流程,我们还需要关注几个极易出错的细节,这些正是手写代码时需要特别注意的地方。

1. 初始质心的选择:KMeans的“阿喀琉斯之踵”KMeans算法对初始质心的位置非常敏感。糟糕的初始化可能导致算法收敛到局部最优解,甚至产生空簇。经典的随机初始化方法是从数据集中随机选择K个点作为初始质心。但这种方法效果不稳定。因此,实践中更推荐使用KMeans++初始化策略,它通过一种概率方法来选择彼此距离较远的点作为初始质心,能显著提高聚类效果和收敛速度。我们会在后续的代码实现中详细对比这两种初始化方式。

2. 距离度量:不仅仅是欧氏距离虽然默认使用欧氏距离,但KMeans的核心思想并不局限于它。任何满足三角不等式的距离度量都可以使用,例如曼哈顿距离。但需要注意的是,如果你更换了距离度量,那么更新质心的“均值”计算也需要相应改变。对于欧氏距离,质心就是算术平均;对于曼哈顿距离,质心应该是中位数(因此有时被称为K-Medians)。在大部分通用场景下,欧氏距离是标准选择。

3. 空簇问题:一个必须处理的边界情况在迭代过程中,有可能出现某个簇在分配步骤后没有分配到任何数据点,即|C_i| = 0,这就是空簇。如果不对空簇进行处理,在更新步骤计算均值时就会除以零,导致程序崩溃。常见的处理策略有:

  • 随机重启:将这个空簇的质心重新初始化为数据集中的一个随机点。
  • 选择最远点:将空簇的质心设置为距离当前任何质心最远的数据点。
  • 选择SSE贡献最大的点:从最大的那个簇中,选择一个距离其当前质心最远的点,“分配”给空簇作为新质心。 在我们的实现中,必须包含对空簇的鲁棒性处理。

4. 收敛性判断:浮点数比较的陷阱判断质心“不再变化”时,不能直接使用==进行浮点数比较。由于计算精度问题,两个理论上相等的浮点数在计算机中可能略有差异。正确的做法是计算新旧质心之间的欧氏距离(或绝对差),如果这个距离小于一个极小的阈值(如1e-4),则认为已经收敛。

3. 纯NumPy手写KMeans:从骨架到健壮实现

现在,我们进入实战环节,抛开sklearn,仅使用NumPy来构建一个功能完整、鲁棒的KMeans类。我会分模块讲解,并解释每一行关键代码背后的意图。

3.1 类结构与初始化

首先,我们定义类的骨架和初始化方法。这里我们会实现两种初始化策略:随机初始化和KMeans++初始化。

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs # 用于生成演示数据 class MyKMeans: def __init__(self, n_clusters=8, max_iter=300, tol=1e-4, init='k-means++', random_state=None): """ 初始化KMeans聚类器。 参数: n_clusters : int, 默认=8 要形成的簇数,也是要生成的质心数。 max_iter : int, 默认=300 单次运行的最大迭代次数。 tol : float, 默认=1e-4 关于质心变化的收敛容忍度。如果所有质心的变化都小于tol,则停止迭代。 init : {'k-means++', 'random'}, 默认='k-means++' 初始化方法: - 'k-means++' : 使用KMeans++算法选择初始质心,通常能加速收敛。 - 'random' : 从数据中随机选择n_clusters个样本作为初始质心。 random_state : int, 默认=None 确定随机数生成器的种子。用于可重现的结果。 """ 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 # 每个样本的簇标签,形状 (n_samples,) self.inertia_ = None # 最终的SSE值 self.n_iter_ = 0 # 实际运行的迭代次数 if random_state is not None: np.random.seed(random_state) def _initialize_centroids(self, X): """初始化质心。""" n_samples, n_features = X.shape centroids = np.zeros((self.n_clusters, n_features)) if self.init == 'random': # 方法1:随机选择K个样本点作为初始质心 indices = np.random.choice(n_samples, self.n_clusters, replace=False) centroids = X[indices] elif self.init == 'k-means++': # 方法2:KMeans++初始化 (更优) # 步骤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 = self._calc_distances(X, centroids[:i]) # (n_samples, i) min_distances = np.min(distances, axis=1) # (n_samples,) # 将距离平方作为概率,进行加权随机选择 probabilities = min_distances / np.sum(min_distances) next_idx = np.random.choice(n_samples, p=probabilities) centroids[i] = X[next_idx] else: raise ValueError(f"不支持的初始化方法: {self.init}") return centroids

_initialize_centroids方法中,我重点实现了KMeans++。它的核心思想是让初始质心彼此远离。选择概率与“该点到已选质心的最短距离的平方”成正比,这保证了新质心有较大概率出现在距离已选质心较远的区域。虽然比随机初始化多了一个循环,但能极大改善聚类效果,是绝对值得的投入。

3.2 核心迭代:分配与更新

这是算法的引擎部分。我们需要高效地计算所有样本点到所有质心的距离,并完成分配。

def _calc_distances(self, X, centroids): """ 计算所有样本X到所有质心centroids的欧氏距离平方。 使用向量化操作避免循环,大幅提升性能。 公式:dist^2 = (X - centroids)^2 在特征维度求和。 利用广播机制。 """ # X形状: (n_samples, n_features) # centroids形状: (n_centroids, n_features) # 我们需要得到形状为 (n_samples, n_centroids) 的距离矩阵 # 技巧: 利用 (a-b)^2 = a^2 + b^2 - 2ab # 这里我们直接使用更直观的广播计算差值的平方和 # 另一种高效写法是:np.sum((X[:, np.newaxis, :] - centroids[np.newaxis, :, :]) ** 2, axis=2) # 但为了清晰,我们分步计算: n_samples = X.shape[0] n_centroids = centroids.shape[0] distances = np.zeros((n_samples, n_centroids)) for i in range(n_centroids): # 计算样本与第i个质心的差值,平方,然后沿特征轴求和 distances[:, i] = np.sum((X - centroids[i]) ** 2, axis=1) return distances def fit(self, X): """ 在数据X上拟合KMeans模型。 参数: X : array-like, 形状 (n_samples, n_features) 训练数据。 返回: self : object 返回实例本身。 """ X = np.array(X) # 确保是NumPy数组 n_samples, n_features = X.shape # 1. 初始化质心 self.centroids = self._initialize_centroids(X) centroids_old = np.zeros_like(self.centroids) # 2. 开始迭代 for i in range(self.max_iter): self.n_iter_ = i + 1 centroids_old[:] = self.centroids # 保存旧质心用于比较 # E-Step: 分配样本到最近的质心 distances = self._calc_distances(X, self.centroids) # (n_samples, n_clusters) self.labels_ = np.argmin(distances, axis=1) # 每个样本最近的质心索引 # M-Step: 更新质心 for k in range(self.n_clusters): # 获取属于簇k的所有样本 cluster_k = X[self.labels_ == k] if len(cluster_k) > 0: # 计算新质心(均值) self.centroids[k] = np.mean(cluster_k, axis=0) else: # 处理空簇:这里采用随机重启策略 # 从整个数据集中随机选择一个点作为新质心 random_idx = np.random.randint(n_samples) self.centroids[k] = X[random_idx] print(f"迭代 {i+1}: 簇 {k} 为空,已重新初始化其质心。") # 检查收敛条件:质心变化是否小于容忍度 centroid_shift = np.sqrt(np.sum((self.centroids - centroids_old) ** 2, axis=1)).max() if centroid_shift < self.tol: print(f"迭代 {i+1} 后收敛。") break # 3. 计算最终的SSE (inertia_) # 计算每个样本到其所属质心的距离平方 final_distances = self._calc_distances(X, self.centroids) # 取每个样本到其最近质心的距离 min_distances = np.min(final_distances, axis=1) self.inertia_ = np.sum(min_distances) return self def predict(self, X): """ 预测X中每个样本所属的簇。 参数: X : array-like, 形状 (n_samples, n_features) 返回: labels : array, 形状 (n_samples,) 每个样本的簇索引。 """ X = np.array(X) distances = self._calc_distances(X, self.centroids) return np.argmin(distances, axis=1)

fit方法中,有几个关键点值得强调:

  1. 向量化距离计算_calc_distances方法虽然用了循环遍历质心,但在样本维度上利用了NumPy的广播机制进行向量化计算,这比用双重循环遍历每个样本和每个质心要快几个数量级。对于大规模数据,可以考虑使用scipy.spatial.distance.cdist函数,它经过高度优化。
  2. 空簇处理:在更新质心的循环中,我们检查len(cluster_k) > 0。如果为空簇,我们执行随机重启。这是一个简单有效的策略。在生产环境中,你可能需要根据业务逻辑选择更复杂的策略。
  3. 收敛判断:我们计算了所有质心变化的最大范数(这里用了欧氏距离)。只有当所有质心的移动都小于阈值tol时,才认为收敛。centroid_shift的计算也使用了向量化操作。
  4. 计算SSE:在迭代结束后,我们重新计算了所有样本到其最终归属质心的距离平方和,作为inertia_属性。这个值对于后续评估模型和选择K值非常重要。

3.3 性能优化与小技巧

上面的实现是清晰的教学版本。在实际追求效率的场景下,我们可以进行一些优化:

  • 距离计算优化:如前所述,使用scipy.spatial.distance.cdist(X, self.centroids, 'euclidean')可以极大提升距离计算速度,尤其是当特征维度不高时。
  • 避免重复计算:在迭代中,_calc_distances被调用了两次(分配和最终SSE计算)。在最终SSE计算时,其实可以直接使用最后一轮迭代中计算出的distanceslabels_来求和,避免重复计算。
  • 使用np.einsum进行张量运算:对于高级用户,可以使用np.einsum函数来更优雅、有时也更高效地实现距离矩阵的计算。例如,计算欧氏距离平方可以写成:np.einsum('ij,ij->i', X, X)[:, None] + np.einsum('ij,ij->i', centroids, centroids) - 2 * X.dot(centroids.T)。这利用了数学恒等式,在特定条件下更快。

实操心得:在开发初期,优先保证代码的清晰性和正确性,使用容易理解的实现方式。在功能稳定后,再通过性能分析工具(如cProfileline_profiler)定位瓶颈,进行有针对性的优化。盲目优化往往会引入难以调试的Bug。

4. 算法验证与评估:用数据和可视化说话

代码写完了,但它真的对吗?效果如何?我们需要用数据来验证。这里我介绍几种常用的方法,并展示如何用我们的MyKMeans类来实现。

4.1 生成与使用合成数据集

我们首先使用sklearnmake_blobs函数生成一个易于理解的合成数据集。它能够生成各向同性的高斯斑点簇,非常适合演示聚类算法。

def test_and_visualize(): # 1. 生成合成数据 n_samples = 500 n_features = 2 n_clusters = 4 random_state = 42 # 生成数据,X是特征,y_true是真实的标签(用于评估,但聚类算法本身不知道) X, y_true = make_blobs(n_samples=n_samples, n_features=n_features, centers=n_clusters, cluster_std=0.8, # 控制簇的紧密度 random_state=random_state) # 2. 使用我们的MyKMeans进行聚类 kmeans = MyKMeans(n_clusters=n_clusters, init='k-means++', max_iter=300, random_state=random_state) kmeans.fit(X) y_pred = kmeans.labels_ centroids = kmeans.centroids print(f"算法运行了 {kmeans.n_iter_} 次迭代后收敛。") print(f"最终的簇内平方和(SSE/inertia)为: {kmeans.inertia_:.2f}") print(f"最终质心坐标:\n{centroids}") # 3. 可视化结果 fig, axes = plt.subplots(1, 2, figsize=(12, 5)) # 子图1:真实分布 scatter1 = axes[0].scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', s=30, edgecolor='k', alpha=0.7) axes[0].set_title('Ground Truth Clusters') axes[0].set_xlabel('Feature 1') axes[0].set_ylabel('Feature 2') # 为真实中心添加标记(这里我们不知道真实中心,用类别均值近似演示) for i in range(n_clusters): cluster_mean = X[y_true == i].mean(axis=0) axes[0].scatter(cluster_mean[0], cluster_mean[1], c='red', marker='X', s=200, linewidths=2) # 子图2:KMeans聚类结果 scatter2 = axes[1].scatter(X[:, 0], X[:, 1], c=y_pred, cmap='viridis', s=30, edgecolor='k', alpha=0.7) axes[1].scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, linewidths=2, label='Centroids') axes[1].set_title('MyKMeans Clustering Result') axes[1].set_xlabel('Feature 1') axes[1].set_ylabel('Feature 2') axes[1].legend() plt.tight_layout() plt.show() # 4. 简单评估:调整兰德指数 (ARI) - 需要真实标签 from sklearn.metrics import adjusted_rand_score ari = adjusted_rand_score(y_true, y_pred) print(f"\n调整兰德指数(ARI): {ari:.3f}") # ARI取值范围[-1,1],越接近1表示聚类结果与真实情况越一致。 # 注意:聚类是无监督学习,通常没有真实标签,ARI仅在有标签数据验证时使用。 if __name__ == "__main__": test_and_visualize()

运行这段代码,你会看到两幅并排的散点图。左边是数据真实的分布(我们生成时知道的),右边是我们的MyKMeans算法的聚类结果。红色的“X”表示质心。通过对比,你可以直观地判断算法是否成功识别出了数据的自然分组。调整兰德指数(ARI)则给出了一个量化的评估。

4.2 如何选择最佳的K值?——肘部法则实战

KMeans需要预先指定簇数K,但这往往是未知的。肘部法则(Elbow Method)是一种常用的启发式方法。其原理是:随着K值的增加,SSE会下降。当K增加到真实簇数附近时,SSE的下降幅度会突然变缓,这个拐点就像“肘部”,对应的K值就是较好的选择。

def elbow_method(X, max_k=10): """绘制肘部法则图以帮助选择K值。""" inertias = [] K_range = range(1, max_k + 1) for k in K_range: kmeans = MyKMeans(n_clusters=k, init='k-means++', max_iter=300, random_state=42) kmeans.fit(X) inertias.append(kmeans.inertia_) plt.figure(figsize=(8, 5)) plt.plot(K_range, inertias, 'bo-') plt.xlabel('Number of clusters (K)') plt.ylabel('Inertia (SSE)') plt.title('Elbow Method For Optimal K') plt.xticks(K_range) plt.grid(True, linestyle='--', alpha=0.7) plt.show() # 使用之前生成的数据X elbow_method(X, max_k=10)

运行后,你会看到一条曲线。理想情况下,曲线会在某个K值处出现明显的拐点(肘部)。在拐点之后,增加K带来的SSE下降收益变小。这个拐点对应的K就是建议值。但要注意,肘部有时并不明显,需要结合业务理解和后续的轮廓系数等指标综合判断。

4.3 使用真实数据集:以鸢尾花数据集为例

合成数据太完美,我们用一个经典的现实世界数据集——鸢尾花(Iris)数据集来测试。它包含3种鸢尾花(Setosa, Versicolour, Virginica)的4个特征(萼片和花瓣的长度与宽度)。我们知道有3类,但算法不知道。

from sklearn import datasets def test_on_iris(): # 加载数据 iris = datasets.load_iris() X = iris.data # (150, 4) y_true = iris.target # 真实标签 # 使用我们的KMeans,设定K=3 kmeans = MyKMeans(n_clusters=3, init='k-means++', max_iter=300, random_state=42) kmeans.fit(X) y_pred = kmeans.labels_ # 评估 from sklearn.metrics import confusion_matrix, classification_report, adjusted_rand_score # 注意:聚类标签是任意的,可能与真实标签编号不对应,需要映射 # 这里我们使用混淆矩阵查看对应关系,并计算ARI(不受标签排列影响) print("混淆矩阵 (行:真实标签, 列:预测标签):") print(confusion_matrix(y_true, y_pred)) print(f"\n调整兰德指数(ARI): {adjusted_rand_score(y_true, y_pred):.3f}") # 由于有4个特征,我们选取前两个主成分进行可视化 from sklearn.decomposition import PCA pca = PCA(n_components=2) X_pca = pca.fit_transform(X) plt.figure(figsize=(8,6)) scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_pred, cmap='Set2', s=50, edgecolor='k') plt.scatter(kmeans.centroids.dot(pca.components_.T)[:, 0], kmeans.centroids.dot(pca.components_.T)[:, 1], c='red', marker='X', s=200, linewidths=3, label='Centroids (PCA)') plt.xlabel('Principal Component 1') plt.ylabel('Principal Component 2') plt.title('MyKMeans on Iris Dataset (PCA Projection)') plt.legend() plt.colorbar(scatter, label='Cluster Label') plt.show() test_on_iris()

在这个例子中,你会看到混淆矩阵和ARI分数。由于KMeans是基于距离的,而鸢尾花数据中Setosa类别与其他两类分离得很好,Versicolour和Virginica有部分重叠,所以聚类结果可能会将后两类部分混淆。通过PCA降维可视化,可以直观看到聚类结果在二维空间上的分布。这个练习告诉我们,对于高维或重叠的数据,KMeans的局限性会显现出来。

5. 超越基础:常见问题、优化与进阶思考

一个健壮的算法实现不仅要能处理标准情况,还要能应对各种边界条件和性能需求。同时,理解算法的局限性才能更好地应用它。

5.1 处理非球形簇与不同尺度特征

KMeans的一个核心局限是它假设簇是凸形的(通常是球形的)且大小相似。对于流形、环形或大小差异巨大的簇,KMeans效果会很差。例如,考虑两个同心圆分布的数据,KMeans无法正确聚类。

解决方案与进阶算法

  • 数据预处理:如果特征尺度差异巨大(如一个特征范围是0-1,另一个是1000-10000),欧氏距离会被大尺度特征主导。务必进行特征标准化(如Z-score标准化)或归一化。
  • 使用其他聚类算法:对于复杂形状的簇,可以考虑:
    • DBSCAN:基于密度的算法,能发现任意形状的簇,并能识别噪声点。这在热词中也被提到,是一个重要的对比算法。
    • 谱聚类:利用数据相似度矩阵的特征向量进行聚类,对簇的形状没有凸性要求。
    • 高斯混合模型:假设数据由多个高斯分布生成,是KMeans的概率扩展,能提供软分配(一个点属于各个簇的概率)。

5.2 大数据集下的挑战与优化

当数据量极大时,我们手写的循环版本会非常慢。此外,KMeans需要多次扫描整个数据集。

优化策略

  • 使用更快的距离计算库:如前所述,用scipy.spatial.distance.cdistsklearn.metrics.pairwise_distances
  • Mini-Batch KMeans:这是KMeans的一种变体,每次迭代只使用数据集的一个随机子集(mini-batch)来更新质心。虽然可能略微降低精度,但能极大减少计算时间,特别适合海量数据。sklearn.cluster.MiniBatchKMeans提供了现成实现。
  • 利用三角不等式加速:Elkan KMeans算法利用距离的三角不等式来避免不必要的距离计算,可以显著提升速度。sklearnKMeans默认使用这种算法(algorithm='elkan')。

5.3 项目集成与源码管理

当你把MyKMeans类写好后,如何将它变成一个可复用的项目?

  1. 模块化:将代码保存为一个独立的.py文件,例如my_kmeans.py。通过if __name__ == '__main__':来包含测试代码。
  2. 编写文档字符串:我们已经在每个函数和类中写了基本的docstring。一个好的docstring应该说明参数、返回值、可能抛出的异常以及简单的示例。
  3. 创建setup.py:如果你想让别人方便地安装使用,可以创建一个setup.py文件,使用setuptools打包你的代码。
  4. 版本控制:使用Git管理你的源码。为不同的功能(如基础实现、KMeans++、性能优化)创建分支。
  5. 单元测试:编写测试用例来验证你的算法在各种情况下的正确性(如空簇处理、收敛性、与sklearn结果的一致性等)。可以使用pytestunittest框架。

踩坑实录:在一次项目中,我直接使用了未标准化的数据跑KMeans,结果聚类完全被某个量纲大的特征带偏。调试了很久才发现是特征尺度的问题。从此以后,“数据预处理,尤其是标准化,是运行任何距离相关算法的标配”成了我的铁律。另外,在实现KMeans++时,概率计算probabilities = min_distances / np.sum(min_distances)要确保min_distances没有负数,且np.sum(min_distances)不为零(在数据点都重合的极端情况下可能为零,需要额外判断)。

手写KMeans的过程,是一次对经典算法的深度对话。它强迫你思考每一个细节,从数学公式到代码边界,从效率优化到异常处理。这份完整的源码和配套的数据集处理方法,不仅是一个可运行的工具,更是一个理解聚类算法乃至机器学习基础思想的绝佳模板。当你下次再调用sklearn.cluster.KMeans时,你看到的将不再是一个黑盒,而是一个由清晰逻辑和精妙细节构成的透明结构。这才是学习算法最扎实的方式。

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

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

BLARM:潜在刚体运动原语驱动视频生成3D动画的新思路

如果你正在做 3D 生成、NeRF、3D 高斯泼溅或者角色动画方向&#xff0c;最近会明显感到一个趋势&#xff1a;大家已经不满足于“生成一个静态 3D 物体”&#xff0c;而是想让物体真的动起来。问题在于&#xff0c;视频里的物体运动和 3D 几何不完全是一回事。从一段普通视频里恢…

作者头像 李华
网站建设 2026/9/3 14:06:22

AI聊天机器人安全防护:从原理到企业级部署实践

1. AI聊天机器人技术背景与安全挑战 近年来&#xff0c;AI聊天机器人技术取得了突破性进展&#xff0c;从简单的问答工具发展到能够理解复杂上下文、生成专业内容的大语言模型。这类技术基于Transformer架构&#xff0c;通过海量数据训练获得知识表示能力&#xff0c;在编程辅助…

作者头像 李华
网站建设 2026/9/3 14:05:45

VR设备告别千元时代:技术升级与体验闭环驱动行业价值重塑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 14:03:55

Awesome Privacy 项目风险管理演讲:专家分享与案例

Awesome Privacy 项目风险管理演讲&#xff1a;专家分享与案例 在当今数字化时代&#xff0c;隐私与安全已成为用户和企业关注的核心议题。Awesome Privacy 作为一个专注于隐私和安全的开源项目&#xff0c;其风险管理至关重要。本演讲将从项目架构、风险识别、应对策略等方面…

作者头像 李华
网站建设 2026/9/3 14:03:49

Awesome Privacy 企业隐私框架文档:组织的指导文件

Awesome Privacy 企业隐私框架文档&#xff1a;组织的指导文件 在当今数据驱动的商业环境中&#xff0c;企业面临着日益严峻的隐私保护挑战。客户数据泄露、监管处罚和品牌声誉受损等风险促使组织必须建立完善的隐私保护体系。Awesome Privacy 作为一个专注于隐私和安全的开源…

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

【AI大模型】量化部署:GPTQ/AWQ量化模型部署指南

【AI大模型】量化部署:GPTQ/AWQ量化模型部署指南(含实操代码) 在AI大模型本地化、私有化落地场景中,显存不足、硬件门槛高、推理成本昂贵是绝大多数开发者的核心痛点。7B、13B原生FP16模型显存占用大,低配消费级显卡无法直接部署,34B、70B超大模型更是需要高端算力集群支…

作者头像 李华