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

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

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研究的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

UMAP在Fashion MNIST上的应用

%%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
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

关键点在于:数据的重要结构特性得以保留,同时已知类别被清晰地分开和隔离。如果你有已知类别的数据,并希望在分离它们的同时仍然保留单个点的有意义嵌入,那么监督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

正如你所看到的,这与之前的工作类似,成功地嵌入了单独的类,同时保留了内部结构和整体全局结构。我们现在可以看看测试集是如何通过transform()方法嵌入的,我们没有提供标签信息。

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 networks相比。

在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数据集上使用这种方法。