Python 中的 1D K-means 聚类

2025年3月17日 | 阅读13分钟

在本文中,1D K-均值聚类将是主要主题。为了介绍该技术并阐明其思想,将使用一个基本的1D 实现。该概念将在后续文章中扩展到 N 维。本文将不仅关注所讨论的技术,还将关注底层代码——在本例中是 Python。也就是说,本文既是关于 K-均值 1D 聚类,也是关于 Python,甚至可能是关于一般编程的。为了实现这一点,我们将逐行阅读相关代码,以理解它不仅在做什么,还在如何以及为什么这样做。

  • 创建 K 个质心,并为每个簇分配一个。
  • 每个数据点应与其最近的质心匹配。
  • 在每个点根据其与簇质心的接近程度被分配到簇后,重新计算每个簇的质心
  • 重复步骤 2-3,直到达到预定迭代次数,或者直到质心停止变化。

尽管一维数据通常不用于 K 均值聚类,但它是一个非常简单的示例,可以展示该过程如何运作。正如标题所示,本文的目的是演示如何使用 Python 显示一维 K 均值聚类

该程序

这个特定的脚本旨在清晰而非简洁,因此预计需要一些 Python 或至少编程知识,尽管大部分信息将在下面详细介绍。该脚本应与 Python 2.xPython 3.x 兼容,并且还需要安装 numpymatplotlib 包。

好的,让我们开始吧。在您喜欢的 IDE 或文本编辑器中创建一个名为 kmeans1D_demo.py 的文件,然后按照说明开始。

我们将实际创建的簇的数量,每个簇包含 pntsPerCluster 个点,是 nmbrClustersxCntrBounds 提供的 (min, max) 值将用于定位这些簇的中心(在我们的这个一维示例中,“x”是我们的一个维度)。“K 均值”中的“K”代表我们希望算法寻找的质心数量,或簇数量。请记住,算法寻找的簇数量与我们实际生成的簇数量不同。这实际上是一个关键点;在这种情况下,我们主动创建数据簇,然后让 K 均值算法对其进行处理。然而,在现实世界中,我们通常不会生成数据。由于我们不确切知道存在多少个簇(如果有的话),因此由我们和/或我们的 K 均值算法来决定数据需要分成多少个簇,即选择 K 的哪个值。可以使用多种技术找到 K 的最佳值;但是,我们将在稍后更详细地介绍这些技术。

为了创建在 [0.0, 1.0] 范围内从连续均匀分布中选择的随机浮点数数组(与正态分布相反),它首先使用 Numpy 的 random 模块的 random sample 函数。下一行将这些元素从 [0.0, 1.0] 范围转换为 xCntrBounds 范围。这些变量用作我们将创建的点簇的中心,这在代码的以下部分完成(此处的循环的每次迭代都在生成一个点簇并将其存储在不同的点中)。为此,我们使用 numpy 函数 randn(),它从正态分布中提取,并构造一个标准差 stDev。随着 stDev 值的增加,点将散开,而其值的减少将导致簇更紧密地聚集在一起。尝试修改它以观察它如何影响算法区分簇的能力。

有多种策略可以确定我们的 K 个质心的初始位置。这很重要,因为结果取决于我们选择的起始位置。任何给定数据集都可以以多种方式进行聚类。K 均值方法仅仅确保我们将得到一个解决方案(“局部最优”),而不一定是最佳答案(“全局最优”)。然而,作为一般规则,从我们的数据集中随机选择 K 个不同的点并将它们用作初始质心位置是建立质心位置的一种不错的方法。通过从 points 数组中选择 K 个不同的索引并将这些索引处的值保存在 centroids 数组中,可以实现这一点。每次算法迭代后,我们将用更新后的质心位置更新 centroids

最后,使用函数 "assignPntsToCentroids()",我们将集合中的每个点分配到其最近的质心所在的簇。换句话说,我们将遍历 points,对于每个点,找到它与 centroidsK 个质心中的哪个质心最接近,然后将与该质心/簇相关的索引保存在 classifications 数组中(因为我们正在将该点“分类”为属于 K 个簇之一)。points 中的一个点相当于每个分类元素。这意味着如果 classifications[3] = 0,则第四个点,即 points[3],最接近质心 0,其位置由 centroids[0] 指示。由于此示例只涉及一维,因此可以使用内置的绝对值方法 abs() 来计算给定点和特定质心之间的路径。一旦我们定义了 assignPntsToCentroids() 方法,我们就调用它一次,根据 centroids 中已有的初始质心坐标对点进行聚类。

该过程的另一个关键组成部分是重新计算每个簇的质心位置,程序的这一部分定义了一个名为 reCalcCentroids() 的函数来完成此操作。如果 sum(classifications == k) > 0,我们首先确定是否已将任何点实际分配给每个簇。如果组件之和至少为 1,则至少有一个点分配给该簇。比较语句 classifiers == k 返回一个数组,其中分类代表 k 的索引处为 1,不代表 k 的索引处为 0。下一句除以簇中的点总数来计算质心。通过确保簇至少包含一个点来避免除以零

由于本教程的目的是展示该算法,因此每个簇应由不同的颜色表示。利用 HSV 颜色空间是一种巧妙的方法,它根据色调、饱和度和亮度值组织颜色,适用于任意数量的簇。锥形/圆柱体可以用来象征这些属性。当我们绕着圆柱体旋转时,颜色会发生变化。红色由 0° 表示,绿色由 120° 表示,蓝色由 240° 表示,并在 360° 处返回红色。Python colorsys 模块中,0°-360° 的范围由 0 到 1 的浮点数表示。然后,我们使用除最后一个数字之外的所有数字,将色调光谱 [0, 1] 分成 K+1 个等距值,以获得 K 种不同的颜色。这是因为第一个和最后一个数字都将为 0,这在 HSV 颜色空间中代表红色

然后,我们使用 pyplot 函数 subplots() 生成一个带轴的图形。此函数将返回图形和轴对象的句柄,允许我们随后获取或更改图形和轴的属性。我们希望以不同的颜色绘制每个簇及其质心,以便可视化该过程。此外,我们应该能够在算法迭代后更新每个簇。为此,我们将使用一个 for 循环对每个簇和质心执行轴的 plot() 函数。这将为每个簇和质心初始化一个空的 matplotlib 线对象。本质上,我们使用 plot() 为每个 K 个簇向轴 axe 添加一个“线对象”。我们提供给 plot() 的参数决定了我们的线对象的特性。

我们想要描绘的点的 xy 坐标是 plot() 函数的前两个参数。通过 ax.plot([], [],...),我们通过提供空数组来初始化一条没有任何 xy 数据的线。linestyle 由关键字参数 ls 决定;ls='None' 通知 matplotlib 我们希望绘制点而不连接线。标记样式由关键字参数 marker 决定。任何 matplotlib 颜色规范都可以用作关键字参数 color。在这里,我们使用 colorsys 方法 hsv_to_rgb 从我们的 HSV 颜色创建 RGB 元组(请注意,使用 colorsys 是为了演示 Python 已经有一个内置包来处理此功能;matplotlib 还包含一个颜色模块 matplotlib.colors,它也可以将 HSV 转换为 RGB)。为了以后更改上述任何属性,plot() 输出它刚刚构建的线对象的句柄。当然,每当我们希望更新我们的绘图时,我们必须能够在之后访问此线对象句柄。为了实现这一点,我们将簇和质心线对象句柄添加到相应的列表 clusterPntsListcentroidPntsList 中。这会在循环结束后跟踪我们的 for 循环中的项目。

然后,在绘图的左下角用空文本行进行标注。此文本将用于显示算法已进行的迭代次数,我们将使用其句柄(我将其命名为 iterText)来更新它。

算法的每次迭代都需要一个简单的函数来更新绘图。我们的 updatePlot() 函数使用一个输入(当前迭代)来设置 iterText 注释的值。该函数首先确定哪些点是每个簇 k 的一部分,然后使用该信息设置属于该簇的线对象的 x 数据(它从 clusterPntsList 获取)。set data() 函数需要两个输入:一个 x 坐标数组和另一个 y 坐标数组。由于这是一个 1D 示例y 值无关紧要,因此我们将它们都设置为 0。请注意,由于我们在生成线对象时已经指定了线对象的颜色和标记样式,因此我们现在无需担心这样做。我们只向质心线对象的 set data() 方法提供一个 x 值和一个 y 值,因为每个簇只有一个质心值。然后,使用 savefig() 方法将绘图保存为图像。这将在我们用于动画算法的 while 循环的每次迭代中发生。然后,动画将使用这些照片转换为视频。如果您不想保存动画照片,可以注释掉此行。

注意:不建议使用 WHILE 循环在 MATPLOTLIB 中使用动画或制作视频动画。

虽然可以使用 while 循环Matplotlib 中为对象制作动画,但不推荐这样做。使用 matplotlib 中的动画模块可以更好地完成此操作。此外,手动制作视频和保存单个帧也是多余的,matplotlib.animation 模块已经执行了这项任务。此外,我们希望向您展示“困难的方式”是如何完成的,然后再向您展示正确的方式。

回到当前代码;为了确保所有数据点都将显示,我们接下来指定绘图的轴边界。然后,使用之前定义的 updatePlot() 函数初始化绘图。之后,我们激活 Plt.ion() 方法的交互式图表。具有讽刺意味的是,通过允许代码的其余部分在绘图打开时继续运行,这使我们能够在无需用户干预的情况下持续更新绘图。最后,要实际显示绘图,需要调用 plt.show()

最后,我们启动算法。当质心位置停止移动时,结束条件就满足了。为了确保 while 循环至少运行一次,我们构建了数组 last_Centroids 并用一组与 centroids 不同的随机值对其进行初始化。换句话说,如果我们使用 last_Centroids = centroids,则 last_Centroidscentroids 将确实指向同一个对象,修改一个会修改另一个,并且由于它们都指向同一个对象,它们将始终相等,这意味着循环只会执行一次。请注意,我们使用 np.copy() 而不是 last_Centroids = centroids,因为后者会指向它而不是创建它的副本。了解 numpy 数组的这个特性至关重要。

现在我们可以在运行脚本后检查结果。尝试多次执行它以观察不同质心初始化对结果的影响。通过试验不同的 K 值、簇密度和簇标准差来玩转设置。

完整代码

输出

K-means 1D Clustering in Python