UMAP 用于监督降维和度量学习

虽然 UMAP 可以用于标准的无监督降维,但该算法提供了显著的灵活性,使其可以扩展到执行其他任务,包括利用分类标签信息进行监督降维,甚至度量学习。我们将在下面看一些如何做到这一点的示例。

首先,我们需要加载一些基础库——显然是 numpy,还需要 mnist 来读取 Fashion-MNIST 数据,以及 matplotlib 和 seaborn 用于绘图。

import numpy as np
from mnist.loader import MNIST
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set(style='white', context='poster')

用于本次探索的示例数据集是 Zalando Research 的 Fashion-MNIST 数据集。它被设计为经典 MNIST 数字数据集的直接替代品,但使用时尚物品(连衣裙、外套、鞋子、包等)的图像代替手写数字。由于图像更复杂,它比 MNIST 数字提供了更大的挑战。我们可以使用 mnist 库(下载数据集后)加载它。然后我们可以将训练集和测试集打包成一个大数据集,标准化值(使其在 [0,1] 范围内),并为 10 个类别设置标签。

mndata = MNIST('fashion-mnist/data/fashion')
train, train_labels = mndata.load_training()
test, test_labels = mndata.load_testing()
data = np.array(np.vstack([train, test]), dtype=np.float64) / 255.0
target = np.hstack([train_labels, test_labels])
classes = [
    'T-shirt/top',
    'Trouser',
    'Pullover',
    'Dress',
    'Coat',
    'Sandal',
    'Shirt',
    'Sneaker',
    'Bag',
    'Ankle boot']

接下来我们将加载 umap 库,以便我们可以对此数据集执行降维。

import umap

在 Fashion MNIST 上使用 UMAP

首先,我们将仅使用 UMAP 进行标准的无监督降维,以便我们有一个结果基线,用于后续比较。这很简单,只需实例化一个 UMAP 对象(在此例中将 n_neighbors 参数设置为 5 – 我们主要对非常局部的信信息感兴趣),然后调用 fit_transform() 方法,传入我们希望降维的数据。默认情况下,UMAP 会降维到二维,因此我们将能够以散点图的形式查看结果。

%%time
embedding = umap.UMAP(n_neighbors=5).fit_transform(data)
CPU times: user 1min 45s, sys: 7.22 s, total: 1min 52s
Wall time: 1min 26s

这花了一点时间,但考虑到这是 784 维空间中的 70,000 个数据点,时间并不算太长。我们可以简单地将结果绘制成散点图,并根据时尚物品的类别进行着色。我们可以使用 matplotlib 的颜色条以及适当的刻度标签来提供颜色键。

fig, ax = plt.subplots(1, figsize=(14, 10))
plt.scatter(*embedding.T, s=0.3, c=target, cmap='Spectral', alpha=1.0)
plt.setp(ax, xticks=[], yticks=[])
cbar = plt.colorbar(boundaries=np.arange(11)-0.5)
cbar.set_ticks(np.arange(10))
cbar.set_ticklabels(classes)
plt.title('Fashion MNIST Embedded via UMAP');
_images/SupervisedUMAP_10_1.png

结果相当不错。我们成功地分离了许多类别,并且全局结构(将裤子和鞋类与衬衫、外套和连衣裙分开)也得到了很好的保留。然而,与 MNIST 数字的结果不同,有一些类别并没有完全干净地分离。特别是 T 恤、衬衫、连衣裙、套头衫和外套都有一些混合。至少连衣裙基本分离,T 恤大多集中在一个大团块中,但它们与其他类别区分不明显。更糟的是外套、衬衫和套头衫(这在一定程度上并不令人惊讶,因为它们看起来确实非常相似),它们之间都有显著的重叠。理想情况下,我们希望类别分离得更好。由于我们有标签信息,我们可以将其提供给 UMAP 使用!

使用标签分离类别 (监督式 UMAP)

我们如何强制 UMAP 使用目标标签?如果您熟悉 sklearn API,您会知道 fit() 方法接受一个目标参数 y,该参数指定监督目标信息(例如,在训练监督分类模型时)。我们只需在拟合时将这些目标数据传递给 UMAP 模型,它就会利用这些信息执行监督降维!

%%time
embedding = umap.UMAP().fit_transform(data, y=target)
CPU times: user 3min 28s, sys: 9.17 s, total: 3min 37s
Wall time: 2min 45s

这花了一点更长的时间——原因有二:一是我们使用了更大的 n_neighbors 值(这在进行监督降维时建议这样做;此处我们使用默认值 15),二是我们还需要依赖于标签数据进行条件判断。和之前一样,我们将数据降维到了二维,因此我们可以再次通过散点图来可视化数据,并按类别着色。

fig, ax = plt.subplots(1, figsize=(14, 10))
plt.scatter(*embedding.T, s=0.1, c=target, cmap='Spectral', alpha=1.0)
plt.setp(ax, xticks=[], yticks=[])
cbar = plt.colorbar(boundaries=np.arange(11)-0.5)
cbar.set_ticks(np.arange(10))
cbar.set_ticklabels(classes)
plt.title('Fashion MNIST Embedded via UMAP using Labels');
_images/SupervisedUMAP_15_1.png

结果是一组清晰分离的类别(以及一些零散的噪声点——与它们的类别足够不同,以至于没有与其余点归为一组)。然而,除了清晰的类别分离(这是预期中的——我们向算法提供了所有的类别信息)之外,还有几个重要点需要注意。第一个要注意的点是,我们保留了各个类别的内部结构。衬衫和套头衫都仍然具有在原始无监督情况下可见的独特条纹图案;裤子、T 恤和包都保留了它们的形状和内部结构等。第二个要注意的点是,我们也保留了全局结构。虽然各个类别已彼此清晰分离,但类别之间的相互关系得到了保留:鞋类类别都彼此靠近;裤子和包位于图的相对两侧;套头衫、衬衫、T 恤和连衣裙的弧形结构仍然存在。

关键点在于:数据的重要的结构属性得到了保留,而已知类别被干净地分开和隔离。如果您的数据具有已知类别,并且希望在分离它们的同时仍然获得有意义的单个点嵌入,那么监督式 UMAP 正好能满足您的需求。

使用部分标注 (半监督式 UMAP)

然而,如果我们只有部分数据被标注,并且许多项目没有标签怎么办?我们还能利用已有的标签信息吗?这现在是一个半监督学习问题,是的,我们也可以处理这些情况。为了设置示例,我们将遮盖一些目标信息——我们将通过使用 sklearn 的标准方法来实现这一点,即给未标注点一个 -1 的标签(例如,来自 DBSCAN 聚类的噪声点)。

masked_target = target.copy().astype(np.int8)
masked_target[np.random.choice(70000, size=10000, replace=False)] = -1

现在我们已经随机遮盖了一些标签,我们可以尝试再次进行监督学习。一切都像以前一样工作,但 UMAP 会将 -1 标签解释为未标注点并相应地学习。

%%time
fitter = umap.UMAP().fit(data, y=masked_target)
embedding = fitter.embedding_
CPU times: user 3min 8s, sys: 7.85 s, total: 3min 16s
Wall time: 2min 40s

同样,我们可以查看按类别着色的数据散点图。

fig, ax = plt.subplots(1, figsize=(14, 10))
plt.scatter(*embedding.T, s=0.1, c=target, cmap='Spectral', alpha=1.0)
plt.setp(ax, xticks=[], yticks=[])
cbar = plt.colorbar(boundaries=np.arange(11)-0.5)
cbar.set_ticks(np.arange(10))
cbar.set_ticklabels(classes)
plt.title('Fashion MNIST Embedded via UMAP using Partial Labels');
_images/SupervisedUMAP_22_1.png

结果与我们预期的一样——虽然我们没有像在完全监督情况下那样清晰地分离数据,但类别变得更清晰、更分明。这种半监督方法在标注成本可能很高,或者数据量多于标签量,但希望利用额外数据时,提供了一个强大的工具。

使用标签训练并嵌入未标注测试数据 (使用 UMAP 进行度量学习)

如果我们已经学习了一个监督嵌入,我们能否用它将以前从未见过的新(现在未标注的)点嵌入到该空间中?这将提供一种度量学习算法,我们可以使用一组带标签的点来学习数据的度量,然后使用该学习到的度量作为新未标注点之间的距离度量。这在机器学习流程中特别有用,我们可以学习监督嵌入作为一种监督特征工程形式,然后在新的空间上构建分类器——只要我们可以将新数据传递给嵌入模型以转换到新空间,这是可行的。

为了用 UMAP 尝试这一点,我们使用 Fashion MNIST 提供的训练/测试划分

train_data = np.array(train)
test_data = np.array(test)

现在我们可以使用训练数据拟合模型,利用训练标签学习监督嵌入。

%%time
mapper = umap.UMAP(n_neighbors=10).fit(train_data, np.array(train_labels))
CPU times: user 2min 18s, sys: 7.53 s, total: 2min 26s
Wall time: 1min 52s

接下来我们可以使用该模型的 transform() 方法将测试集转换到学习到的空间中。这次我们将不传递标签信息,让模型尝试正确地放置数据。

%%time
test_embedding = mapper.transform(test_data)
CPU times: user 17.3 s, sys: 986 ms, total: 18.3 s
Wall time: 15.4 s

UMAP 转换不像某些方法那样快,但正如您所见,它仍然相当高效。重要的问题是我们如何将测试数据成功嵌入到现有学习空间中。首先,让我们可视化训练数据的嵌入,以便我们了解数据 应该 去哪里。

fig, ax = plt.subplots(1, figsize=(14, 10))
plt.scatter(*mapper.embedding_.T, s=0.3, c=np.array(train_labels), cmap='Spectral', alpha=1.0)
plt.setp(ax, xticks=[], yticks=[])
cbar = plt.colorbar(boundaries=np.arange(11)-0.5)
cbar.set_ticks(np.arange(10))
cbar.set_ticklabels(classes)
plt.title('Fashion MNIST Train Digits Embedded via UMAP Transform');
_images/SupervisedUMAP_31_0.png

正如您所见,我们复制了训练数据的布局,包括类别的大部分内部结构。在很大程度上,新点的分配很好地遵循了类别。最大的混淆来源是一些 T 恤与衬衫混在一起,以及一些套头衫与外套混淆。考虑到问题的难度,这是一个很好的结果,特别是与当前的最新方法(如 siamese 和 triplet 网络)相比。

fig, ax = plt.subplots(1, figsize=(14, 10))
plt.scatter(*test_embedding.T, s=2, c=np.array(test_labels), cmap='Spectral', alpha=1.0)
plt.setp(ax, xticks=[], yticks=[])
cbar = plt.colorbar(boundaries=np.arange(11)-0.5)
cbar.set_ticks(np.arange(10))
cbar.set_ticklabels(classes)
plt.title('Fashion MNIST Test Digits Embedded via UMAP');
_images/SupervisedUMAP_33_0.png

正如您所见,我们复制了训练数据的布局,包括类别的大部分内部结构。在很大程度上,新点的分配很好地遵循了类别。最大的混淆来源是一些 T 恤与衬衫混在一起,以及一些套头衫与外套混淆。考虑到问题的难度,这是一个很好的结果,特别是与当前的最新方法(如 siamese 和 triplet 网络)相比。

在 Galaxy10SDSS 数据集上使用监督式 UMAP

Galaxy10SDSS 数据集 是一个众包的人工标注的星系图像数据集,这些图像被分成了十个类别。Umap 可以学习一个能够部分分离数据的嵌入。为了保持运行时间较短,UMAP 应用于数据集的一个子集。

import numpy as np
import h5py
import matplotlib.pyplot as plt
import umap
import os
import math
import requests

if not os.path.isfile("Galaxy10.h5"):
    url = "http://astro.utoronto.ca/~bovy/Galaxy10/Galaxy10.h5"
    r = requests.get(url, allow_redirects=True)
    open("Galaxy10.h5", "wb").write(r.content)

# To get the images and labels from file
with h5py.File("Galaxy10.h5", "r") as F:
    images = np.array(F["images"])
    labels = np.array(F["ans"])

X_train = np.empty([math.floor(len(labels) / 100), 14283], dtype=np.float64)
y_train = np.empty([math.floor(len(labels) / 100)], dtype=np.float64)
X_test = X_train
y_test = y_train
# Get a subset of the data
for i in range(math.floor(len(labels) / 100)):
    X_train[i, :] = np.array(np.ndarray.flatten(images[i, :, :, :]), dtype=np.float64)
    y_train[i] = labels[i]
    X_test[i, :] = np.array(
        np.ndarray.flatten(images[i + math.floor(len(labels) / 100), :, :, :]),
        dtype=np.float64,
    )
    y_test[i] = labels[i + math.floor(len(labels) / 100)]

# Plot distribution
classes, frequency = np.unique(y_train, return_counts=True)
fig = plt.figure(1, figsize=(4, 4))
plt.clf()
plt.bar(classes, frequency)
plt.xlabel("Class")
plt.ylabel("Frequency")
plt.title("Data Subset")
plt.savefig("galaxy10_subset.svg")
_images/galaxy10_subset.svg

图中显示所选的数据集子集是不平衡的,但整个数据集也是不平衡的,因此本次实验仍将使用此子集。下一步是检查标准 UMAP 算法的输出。

reducer = umap.UMAP(
    n_components=2, n_neighbors=5, random_state=42, transform_seed=42, verbose=False
)
reducer.fit(X_train)

galaxy10_umap = reducer.transform(X_train)
fig = plt.figure(1, figsize=(4, 4))
plt.clf()
plt.scatter(
    galaxy10_umap[:, 0],
    galaxy10_umap[:, 1],
    c=y_train,
    cmap=plt.cm.nipy_spectral,
    edgecolor="k",
    label=y_train,
)
plt.colorbar(boundaries=np.arange(11) - 0.5).set_ticks(np.arange(10))
plt.savefig("galaxy10_2D_umap.svg")
_images/galaxy10_2D_umap.svg

标准 UMAP 算法不能根据星系的类型来分离它们。监督式 UMAP 可以做得更好。

reducer = umap.UMAP(
    n_components=2, n_neighbors=15, random_state=42, transform_seed=42, verbose=False
)
reducer.fit(X_train, y_train)

galaxy10_umap_supervised = reducer.transform(X_train)
fig = plt.figure(1, figsize=(4, 4))
plt.clf()
plt.scatter(
    galaxy10_umap_supervised[:, 0],
    galaxy10_umap_supervised[:, 1],
    c=y_train,
    cmap=plt.cm.nipy_spectral,
    edgecolor="k",
    label=y_train,
)
plt.colorbar(boundaries=np.arange(11) - 0.5).set_ticks(np.arange(10))
plt.savefig("galaxy10_2D_umap_supervised.svg")
_images/galaxy10_2D_umap_supervised.svg

监督式 UMAP 确实做得更好。有些类别之间有一些重叠,但原始数据集在分类上也存在一些歧义。检查这种方法的最佳方式是将测试数据投影到学习到的嵌入空间上。

galaxy10_umap_supervised_prediction = reducer.transform(X_test)
fig = plt.figure(1, figsize=(4, 4))
plt.clf()
plt.scatter(
    galaxy10_umap_supervised_prediction[:, 0],
    galaxy10_umap_supervised_prediction[:, 1],
    c=y_test,
    cmap=plt.cm.nipy_spectral,
    edgecolor="k",
    label=y_test,
)
plt.colorbar(boundaries=np.arange(11) - 0.5).set_ticks(np.arange(10))
plt.savefig("galaxy10_2D_umap_supervised_prediction.svg")
_images/galaxy10_2D_umap_supervised_prediction.svg

这表明学习到的嵌入可以用于新的数据集,因此这种方法可能有助于检查星系图像。请在完整的 200 Mb 数据集以及更新的 2.54 Gb Galaxy 10 DECals 数据集上试用此方法。