将数据聚类成组(第一部分)

151 阅读10分钟

聚类是将数据点组织成概念上有意义的群体的过程。是什么让一个给定的组 "在概念上有意义"?这个问题没有简单的答案。任何聚类输出的有用性都取决于我们被分配的任务。

想象一下,我们被要求对一个宠物照片的集合进行聚类。我们是否将鱼和蜥蜴归为一组,将毛茸茸的宠物(如仓鼠、猫和狗)归为另一组?或者,仓鼠、猫和狗应该被分配到它们自己的三个独立的群组中?如果是这样,也许我们应该考虑按品种对宠物进行分组。因此,吉娃娃和大丹犬属于分歧的群组。区分狗的品种将是不容易的。然而,我们可以很容易地根据品种的大小来区分吉娃娃和大丹犬。也许我们应该妥协:我们将在蓬松度和尺寸上进行聚类,从而绕过凯恩梗和长相相似的诺里奇梗之间的区别。

这种妥协值得吗?这取决于我们的数据科学任务。假设我们为一家宠物食品公司工作,我们的目的是估计狗粮、猫粮和蜥蜴粮的需求。在这些条件下,我们必须区分毛茸茸的狗、毛茸茸的猫和有鳞的蜥蜴。然而,我们不需要解决独立狗种之间的差异。另外,想象一下兽医办公室的分析员,他试图将宠物病人按其品种分组。这第二个任务需要更细化的分组解析。

不同的情况取决于不同的聚类技术。作为数据科学家,我们必须选择正确的聚类解决方案。在我们的职业生涯中,我们将使用各种聚类技术对数以千计(如果不是数以万计)的数据集进行聚类。最常用的算法是依靠一些中心度的概念来区分聚类。

使用中心度来发现聚类

数据的中心性可以用平均值来表示。想象一下,你已经计算了两组鱼的平均长度,并通过分析它们的平均值之间的差异进行比较。我们可以利用这个差值来确定所有的鱼是否属于同一个组。直观地说,一个组中的所有数据点都应该围绕一个中心值聚集。同时,两个分歧组中的测量值应该围绕两个不同的平均值聚集。因此,我们可以利用中心性来区分两个不同的组。让我们具体探讨一下这个概念。

假设我们去一个热闹的当地酒吧实地考察,看到两个并排挂着的镖靶。每个镖靶上都挂满了飞镖,飞镖也从墙上伸出来。酒馆里喝醉了的选手们瞄准了一个镖靶的靶心或另一个。他们经常错过,这导致了观察到的以两个牛眼为中心的飞镖散落。

让我们用数字来模拟散射情况。我们将把每个牛眼的位置当作一个二维坐标。飞镖在这个坐标上被随机地投掷。因此,飞镖的2D位置是随机分布的。由于以下原因,对飞镖位置建模最合适的分布是正态分布:

  • 一个典型的投镖者的目标是牛眼,而不是镖靶的边缘。因此,每个飞镖更有可能击中靠近镖靶中心的地方。这种行为与随机的正常样本是一致的,在这种情况下,接近平均值的数值比其他更远的数值出现的频率更高。
  • 我们希望飞镖能相对于中心对称地击中镖靶。飞镖将以相同的频率击中中心的左边3英寸和右边3英寸的位置。这种对称性被钟形正态曲线所捕获。

假设第一个牛眼位于坐标[0, 0] 。在这个坐标上投掷一个飞镖。我们将使用两个正态分布来模拟飞镖的X和Y的位置。这些分布的平均值为0,我们还假设它们的方差为2。以下代码生成飞镖的随机坐标。

清单1.使用两个正态分布对飞镖坐标进行建模

 import numpy as np
 np.random.seed(0)
 mean = 0
 variance = 2
 x = np.random.normal(mean, variance ** 0.5)
 y = np.random.normal(mean, variance ** 0.5)
 print(f"The x coordinate of a randomly thrown dart is {x:.2f}")
 print(f"The y coordinate of a randomly thrown dart is {y:.2f}")
 The x coordinate of a randomly thrown dart is 2.49
 The y coordinate of a randomly thrown dart is 0.57

我们可以使用np.random.multivariate_normal 方法更有效地模拟飞镖位置。这个方法是从多变量正态分布中选择一个随机点*。*多变量正态曲线只是一个扩展到一个以上维度的正态曲线。我们的二维多元正态分布将类似于一座圆山,其山顶的位置是[0, 0]

让我们模拟5,000个随机飞镖投掷到位于[0, 0] 的靶心。我们还模拟了5000个随机飞镖投掷到第二个牛眼上,这个牛眼的位置是[0, 6] 。然后我们生成一个所有随机飞镖坐标的散点图(图10.1)。

清单2.模拟随机投掷的飞镖

 import matplotlib.pyplot as plt
 np.random.seed(1)
 bulls_eye1 = [0, 0]
 bulls_eye2 = [6, 0]
 bulls_eyes = [bulls_eye1, bulls_eye2]
 x_coordinates, y_coordinates = [], []
 for bulls_eye in bulls_eyes:
     for _ in range(5000):
         x = np.random.normal(bulls_eye[0], variance ** 0.5)
         y = np.random.normal(bulls_eye[1], variance ** 0.5)
         x_coordinates.append(x)
         y_coordinates.append(y)
  
 plt.scatter(x_coordinates, y_coordinates)
 plt.show()

清单2包括一个嵌套的五行for 循环,从for _ in range(5000) 开始。可以使用NumPy来执行这个循环,只需要一行代码:运行x_coordinates, y_coordinates = np.random.multivariate_normal(bulls_eye, np.diag(2 * [variance]), 5000).T ,返回5000个从多变量正态分布中抽样的x和y坐标。


图1.围绕两个牛眼目标随机散布的飞镖模拟图


图中出现了两个重叠的飞镖组。这两组代表10,000个飞镖。一半的飞镖是瞄准左边的牛眼,其余的是瞄准右边的牛眼。每个飞镖都有一个预定的目标,我们可以通过看图来估计这个目标。靠近[0, 0] 的飞镖可能是瞄准左边的靶心。我们将把这个假设纳入我们的飞镖图谱中。

让我们把每个飞镖分配到离它最近的靶心。我们首先定义一个nearest_bulls_eye 函数,该函数的输入是一个dart 列表,其中有一个飞镖的X和Y的位置。该函数返回与dart 最接近的牛眼的索引。我们用欧几里得距离来衡量飞镖的接近程度,这是两点之间的标准直线距离。

欧氏距离产生于毕达哥拉斯定理。假设我们检查一个飞镖在位置[x_dart, y_dart] ,相对于牛眼在位置[x_bull, y_bull] 。根据勾股定理,distance2= (x_dart - x_bull)2 + (y_dart - y_bull)2。我们可以用一个自定义的欧氏函数解决距离问题。或者,我们也可以使用SciPy提供的scipy.spatial.distance.euclidean 函数。

下面的代码定义了nearest_bulls_eye ,并把它应用于飞镖[0, 1][6, 1]

清单3.将飞镖分配给最近的靶心

 from scipy.spatial.distance import euclidean
 def nearest_bulls_eye(dart):
     distances = [euclidean(dart, bulls_e) for bulls_e in bulls_eyes] 

使用从 SciPy 导入的 euclidean 函数获得飞镖和每个牛眼之间的欧氏距离

返回与最短牛眼距离相匹配的索引

现在我们把nearest_bulls_eye 应用到我们所有计算出来的飞镖坐标。每个镖点都用两种颜色中的一种绘制,以区分两个牛眼的分配(图10.2)。

清单4.根据最近的牛眼给飞镖着色

 def color_by_cluster(darts): 

绘制输入的飞镖列表中的彩色元素的辅助函数。darts 中的每个飞镖都可以作为 nearest_bulls_eye 的输入。

选择最接近bulls_eyes[bs_index]的飞镖

通过转置所选飞镖的数组来分离每个飞镖的x-和y-坐标。转置是用一个二维数据结构交换行和列的位置。

将每个飞镖的独立坐标合并成一个单一的x-和y-坐标的列表。


图2.根据距离最近的牛眼的远近对飞镖进行着色。群组A代表所有最接近左牛眼的点,而群组B代表所有最接近右牛眼的点。


彩色的飞镖合理地分成了两个偶数的集群。如果没有提供中心坐标,我们将如何识别这样的集群?那么,一个原始的策略是简单地猜测牛眼的位置。我们可以选择两个随机的飞镖,并希望这些飞镖在某种程度上相对接近每个牛眼,尽管这种情况发生的可能性非常低。在大多数情况下,根据两个随机选择的中心给飞镖着色不会产生好的结果(图3)。

清单5将飞镖分配给随机选择的中心

 bulls_eyes = np.array(darts[:2]) 

随机选择前两个飞镖作为我们的代表牛眼


图3.根据与随机选择的中心的接近程度给飞镖着色。集群B向左拉得太长了。


我们不加选择的中心在质量上感觉不对。例如,右边的B群似乎向左延伸得太远了。我们所指定的任意中心似乎与它的实际靶心点不一致。但是有一个方法可以弥补我们的错误:我们可以计算被拉伸的右侧聚类中所有点的平均坐标,然后利用这些坐标来调整我们对该组中心的估计。在把群组的平均坐标分配给靶心后,我们可以重新应用我们基于距离的分组技术来调整最右边群组的边界。事实上,为了达到最大的效果,我们在重新运行基于中心性的聚类之前,也会将最左边聚类的中心重置为其平均值(图10.4)。

当我们计算一个一维数组的平均值时,我们返回一个单一的值。我们现在将这个定义扩展到包括多个维度。当我们计算一个二维数组的平均值时,我们返回所有x坐标的平均值和所有y坐标的平均值。最终的输出是一个二维数组,包含了x轴和y轴上的平均值。

清单6.根据平均值将飞镖分配到中心

 def update_bulls_eyes(darts):
     updated_bulls_eyes = []
     nearest_bulls_eyes = [nearest_bulls_eye(dart) for dart in darts]
     for bs_index in range(len(bulls_eyes)):
         selected_darts = [darts[i] for i in range(len(darts))
                           if bs_index == nearest_bulls_eyes[i]]
         x_coordinates, y_coordinates = np.array(selected_darts).T
         mean_center = [np.mean(x_coordinates), np.mean(y_coordinates)] 

取所有分配到给定牛眼的飞镖的x和y坐标的平均值。然后这些平均坐标被用来更新我们估计的牛眼位置。我们可以通过执行np.mean(selected_darts, axis=0)更有效地运行这个计算。


图4.根据与重新计算的中心的接近程度给飞镖着色。现在这两个集群看起来更均匀了。


结果已经看起来更好了,尽管它们还没有达到应有的效果。集群的中心仍然显得有些偏离。让我们通过重复基于均值的中心性调整,再进行10次迭代来补救这个结果(图5)。

清单7.在10次迭代中调整牛眼的位置

 
 for i in range(10):
     bulls_eyes = update_bulls_eyes(darts)
  
 color_by_cluster(darts)
  


图5.根据与迭代重新计算的中心的接近程度给飞镖着色


这两组飞镖现在已经完美地聚在一起了我们基本上复制了K-means聚类算法,该算法使用中心性来组织数据。