使用UMAP进行聚类
UMAP 可以作为有效的预处理步骤来提高基于密度的聚类性能。这一点有些争议,应谨慎尝试。有关此问题的一些讨论,请参阅这个stackoverflow线程中关于t-SNE结果聚类的各种回答。其中提出的许多关注点对于UMAP结果的聚类也同样重要。最值得注意的是,UMAP与t-SNE一样,不能完全保留密度。UMAP与t-SNE一样,也可能在聚类中产生虚假的分裂,导致比数据中实际存在的更细的聚类。尽管存在这些担忧,仍然有合理的理由将UMAP用作聚类的预处理步骤。与任何聚类方法一样,人们需要对生成的聚类进行一些探索和评估,以尽可能验证它们。
综上所述,让我们通过一个例子来演示聚类方法可能面临的困难,以及UMAP如何提供一个强大的工具来帮助克服这些困难。
首先,我们需要加载一些库。显然,我们需要数据,我们可以使用sklearn的fetch_openml来获取数据。我们还需要常用的numpy工具和绘图工具。接下来,我们需要umap和一些聚类选项。最后,由于我们将处理带有标签的数据,我们可以使用强大的聚类评估指标Adjusted Rand Index和Adjusted Mutual Information。
from sklearn.datasets import fetch_openml
from sklearn.decomposition import PCA
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# Dimension reduction and clustering libraries
import umap
import hdbscan
import sklearn.cluster as cluster
from sklearn.metrics import adjusted_rand_score, adjusted_mutual_info_score
现在让我们设置绘图并获取我们将要使用的数据——在这种情况下是MNIST手写数字数据集。MNIST由28x28像素的灰度图像组成,这些图像是手写数字(0到9)。这些可以被展开,使得每个数字由一个784维向量描述(图像中每个像素的灰度值)。理想情况下,我们希望聚类能够恢复数字结构。
mnist = fetch_openml('mnist_784', version=1)
mnist.target = mnist.target.astype(int)
为了可视化目的,我们可以使用UMAP将数据降维到2维。当我们在高维空间中对数据进行聚类时,我们可以可视化该聚类的结果。然而,首先,我们将查看按每个数据点代表的数字着色的数据——我们将为每个数字使用不同的颜色。这将有助于理解接下来的内容。
standard_embedding = umap.UMAP(random_state=42).fit_transform(mnist.data)
plt.scatter(standard_embedding[:, 0], standard_embedding[:, 1], c=mnist.target.astype(int), s=0.1, cmap='Spectral');
传统聚类
现在我们想要对数据进行聚类。作为第一次尝试,让我们尝试传统的方法:K-Means。在这种情况下,我们可以解决K-Means聚类中的一个难题——选择正确的k值,即我们寻找的聚类数量。在这种情况下,我们知道答案正好是10。我们将使用sklearn的K-Means实现,在原始的784维数据中寻找10个聚类。
kmeans_labels = cluster.KMeans(n_clusters=10).fit_predict(mnist.data)
聚类效果如何?我们可以通过按聚类成员资格为UMAP嵌入数据着色来查看结果。
plt.scatter(standard_embedding[:, 0], standard_embedding[:, 1], c=kmeans_labels, s=0.1, cmap='Spectral');
这并不是我们真正想要的结果(尽管它确实揭示了K-Means在高维空间中选择聚类的方式以及UMAP通过找到流形边界来展开流形的有趣特性)。虽然K-Means在某些情况下是正确的,比如右侧的两个聚类大部分是正确的,但其余的数据看起来在剩余的聚类中被有些随意地分割。我们可以通过评估调整后的兰德分数和调整后的互信息来验证这一印象,与真实标签进行比较。
(
adjusted_rand_score(mnist.target, kmeans_labels),
adjusted_mutual_info_score(mnist.target, kmeans_labels)
)
(0.36675295135972552, 0.49614118437750965)
正如预期的那样,我们并没有做得特别好——两个分数都在0到1的范围内,0表示聚类效果差(基本上是随机的),1表示完美地恢复了真实的标签。K-Means绝对不是随机的,但它也远未完美地恢复真实的标签。问题的一部分在于K-Means的工作方式,它基于质心并假设大部分是球形的聚类——这导致了K-Means在数字类别之间产生了一些明显的分割。我们有可能通过使用更智能的基于密度的算法来改进这一点。在这种情况下,我们选择尝试HDBSCAN,我们认为它是最先进的基于密度的技术之一。为了性能考虑,我们将通过PCA将数据的维度降低到50维(这恢复了大部分的方差),因为HDBSCAN在处理数据的维度时表现较差。
lowd_mnist = PCA(n_components=50).fit_transform(mnist.data)
hdbscan_labels = hdbscan.HDBSCAN(min_samples=10, min_cluster_size=500).fit_predict(lowd_mnist)
我们现在可以检查结果了。然而,在此之前,应该注意到HDBSCAN的一个特点是它可以拒绝聚类某些点并将它们分类为“噪声”。为了可视化这一方面,我们将把被分类为噪声的点着色为灰色,然后根据聚类成员资格为其余的点着色。
clustered = (hdbscan_labels >= 0)
plt.scatter(standard_embedding[~clustered, 0],
standard_embedding[~clustered, 1],
color=(0.5, 0.5, 0.5),
s=0.1,
alpha=0.5)
plt.scatter(standard_embedding[clustered, 0],
standard_embedding[clustered, 1],
c=hdbscan_labels[clustered],
s=0.1,
cmap='Spectral');
这看起来有些令人失望。它通过简单地拒绝分类大部分数据来满足HDBSCAN的“不出错”方法。结果是聚类几乎肯定无法恢复所有标签。我们可以通过查看聚类验证分数来验证这一点。
(
adjusted_rand_score(mnist.target, hdbscan_labels),
adjusted_mutual_info_score(mnist.target, hdbscan_labels)
)
(0.053830107882840102, 0.19756104096566332)
clustered = (hdbscan_labels >= 0)
(
adjusted_rand_score(mnist.target[clustered], hdbscan_labels[clustered]),
adjusted_mutual_info_score(mnist.target[clustered], hdbscan_labels[clustered])
)
(0.99843407988303912, 0.99405521087764015)
在这里我们看到,HDBSCAN 愿意进行聚类的地方,它几乎完全正确。这正是它设计的目的——在它能做到的地方是正确的,而对于它没有足够信心的任何事物则选择推迟。当然,这里的问题是它推迟了对大量数据的聚类。HDBSCAN 实际上将多少数据分配给了聚类?我们可以很容易地计算出来。
np.sum(clustered) / mnist.data.shape[0]
0.17081428571428572
似乎只有不到18%的数据被聚类。虽然HDBSCAN在能够聚类的数据上表现很好,但在实际管理聚类数据方面表现不佳。这里的问题是,作为一种基于密度的聚类算法,HDBSCAN往往会受到维度诅咒的影响:高维数据需要更多的观察样本来产生足够的密度。如果我们能进一步降低数据的维度,我们将使密度更加明显,并使HDBSCAN更容易对数据进行聚类。问题是,尝试使用PCA来实现这一点将会变得有问题。虽然将50个维度减少仍然解释了数据的很多方差,但进一步减少将会迅速变得更糟。这是由于PCA的线性特性。我们需要的是强大的流形学习,而这正是UMAP可以发挥作用的地方。
UMAP增强聚类
我们的目标是利用UMAP执行非线性流形感知的降维,以便我们可以将数据集降至足够小的维度,以便基于密度的聚类算法能够取得进展。UMAP的一个优势是它不需要你只降到两个维度——你可以降到10个维度,因为目标是聚类而不是可视化,而且UMAP的性能成本非常低。碰巧MNIST是一个如此简单的数据集,我们真的可以将其降至仅两个维度,但一般来说,你应该探索不同的嵌入维度选项。
接下来需要注意的是,当使用UMAP进行降维时,您需要选择与用于可视化时不同的参数。首先,我们需要一个更大的n_neighbors值——较小的值会更关注非常局部的结构,并且更容易产生细粒度的聚类结构,这可能是数据中噪声模式的结果,而不是实际的聚类。在这种情况下,我们将默认值从15增加到30。其次,将min_dist设置为一个非常低的值是有益的。由于我们实际上希望将点密集地聚集在一起(毕竟密度是我们想要的),低值将有助于此,并且可以在聚类之间产生更清晰的分离。在这种情况下,我们将简单地将min_dist设置为0。
clusterable_embedding = umap.UMAP(
n_neighbors=30,
min_dist=0.0,
n_components=2,
random_state=42,
).fit_transform(mnist.data)
我们可以将这些结果可视化,以查看它与更多适合可视化的参数相比如何:
plt.scatter(clusterable_embedding[:, 0], clusterable_embedding[:, 1],
c=mnist.target, s=0.1, cmap='Spectral');
正如你所见,我们仍然保留了整体的全局结构,但我们在集群内将点更紧密地打包在一起,因此我们可以看到集群之间有更大的间隙。最终,这个嵌入仅用于聚类目的,从现在开始,我们将回到原始嵌入以进行可视化。
下一步是对这些数据进行聚类。我们将再次使用HDBSCAN,并使用与之前相同的参数设置。
labels = hdbscan.HDBSCAN(
min_samples=10,
min_cluster_size=500,
).fit_predict(clusterable_embedding)
clustered = (labels >= 0)
plt.scatter(standard_embedding[~clustered, 0],
standard_embedding[~clustered, 1],
color=(0.5, 0.5, 0.5),
s=0.1,
alpha=0.5)
plt.scatter(standard_embedding[clustered, 0],
standard_embedding[clustered, 1],
c=labels[clustered],
s=0.1,
cmap='Spectral');
我们也可以通过使用之前的聚类质量度量来进行定量评估。
adjusted_rand_score(mnist.target, labels), adjusted_mutual_info_score(mnist.target, labels)
(0.9239306564265013, 0.90302671641133736)
以前HDBSCAN表现非常差的地方,现在我们得到了0.9或更高的分数。这是因为我们实际上对更多的数据进行了聚类。和以前一样,我们也可以看看HDBSCAN在它确信能聚类的数据上的表现如何。
clustered = (labels >= 0)
(
adjusted_rand_score(mnist.target[clustered], labels[clustered]),
adjusted_mutual_info_score(mnist.target[clustered], labels[clustered])
)
(0.93240371696811541, 0.91912906363537572)
这比原始的HDBSCAN稍差一些,但如果你做出更多的预测,错误率更高也就不足为奇了。问题是HDBSCAN实际上对多少数据进行了聚类?之前我们只对17%的数据进行了聚类。
np.sum(clustered) / mnist.data.shape[0]
0.99164285714285716
现在我们正在对超过99%的数据进行聚类!我们在调整兰德分数和调整互信息方面的结果与当前使用卷积自编码器技术的最新技术相符。对于一个仅仅将数据视为任意784维向量的方法来说,这并不算差。
希望这已经概述了UMAP如何对聚类有益。 与所有事情一样,必须小心谨慎,但显然,当明智地使用时,UMAP可以提供显著更好的聚类结果。