发挥UMAP 0.4的新功能(绘图,嵌入非欧几里德空间,逆变换)


UMAP已升级,并且v0.4已发布。

2020/02/10开始,您可以使用pip install --pre umap-learn升级版本。

看来您可以按原样输入稀疏矩阵,并且已经添加了各种函数,但是在这里,我将尝试绘图函数,嵌入非欧几里得空间并进行逆转换。

仅更改数据,并且文档中编写的代码示例几乎不变,因此有关每个示例的详细信息,请转到UMAP文档。

数据

在PARC存储库中使用scRNA-seq数据集和注释进行实验(Zheng等,2017,10X PBMC)。 68,579个单元格,使用PCA预压缩到50个尺寸。数据太大了,不能随便做,因此将其减少到约10,000个单元并使用。

1
2
3
import numpy as np
import pandas as pd
import umap

读取数据。 10,000个细胞,50个主要成分。

1
2
dat = np.loadtxt('./data/pca50_pbmc10k.txt', delimiter=',')
dat.shape
1
(10000, 50)

批注如下所示。

1
2
3
4
5
6
7
labels = []
for line in open('./data/zheng17_annotations_10k.txt'):
    labels.append(line.rstrip())

import collections
import pprint
pprint.PrettyPrinter(indent=4).pprint(collections.Counter(labels))
1
2
3
4
5
6
7
8
9
10
11
Counter({   'CD4+/CD45RA+/CD25- Naive T': 1853,
            'CD8+ Cytotoxic T': 1694,
            'CD8+/CD45RA+ Naive Cytotoxic': 1599,
            'CD4+/CD45RO+ Memory': 1425,
            'CD56+ NK': 1211,
            'CD4+/CD25 T Reg': 828,
            'CD14+ Monocyte': 595,
            'CD19+ B': 583,
            'Dendritic': 127,
            'CD4+ T Helper2': 55,
            'CD34+': 30})

暂时,UMAP正常。

1
2
model = umap.UMAP(verbose=True)
model.fit(dat)

绘图功能

umap.plot现在可以单独使用umap进行绘图。

与使用matplotlib进行绘图几乎相同,但是很容易。

请注意,使用

umap.plot时,需要与umap分开使用datashader,bokeh和holoview。安装每个。

1
import umap.plot

如果提供训练模型的实例,它将绘制散点图。

1
umap.plot.points(model)

output_14_2.png

另外,如果将每个点的标签数据赋予labels参数,则会以不同的颜色绘制。

1
umap.plot.points(model, labels=labels)

output_16_1.png

要提供的数据可以是连续值。由于此处没有合适的数据,因此将给出原始数据的平均值。

准备了背景图和颜色图的组合,因此可以通过使用theme参数选择所需的一种来使用它。

有9种主题可供选择。

1
umap.plot._themes.keys()
1
dict_keys(['fire', 'viridis', 'inferno', 'blue', 'red', 'green', 'darkblue', 'darkred', 'darkgreen'])
1
2
3
umap.plot.points(model,
                 values=dat.mean(axis=1),
                 theme='viridis')

output_19_1.png

您也可以使用Bokeh绘制交互式绘图。

1
umap.plot.output_notebook()

预先以pandas.dataframe的形式准备要在鼠标悬停时显示的信息。

1
2
3
4
df_labels = pd.DataFrame(labels, columns=['celltype'])
p = umap.plot.interactive(model, labels=labels,
                          hover_data=df_labels, point_size=2)
umap.plot.show(p)

output_19_2.png

另外,有一个功能可以可视化嵌入UMAP时使用的邻域图。边缘权重也以等级显示。验证已学习了哪些连通性时可能很有用。

1
2
umap.plot.connectivity(model, show_points=True,
                      edge_cmap='viridis')

output_26_2.png

另外,似乎还有一个诊断绘图功能,用于诊断带有各种指示器的嵌入。

嵌入非欧几里德空间

默认情况下,UMAP嵌入在欧几里德空间中(为目标优化了低维空间中的欧几里得距离),但是它似乎可以嵌入到其他类型的空间中,例如球体。

通过使用output_metric参数在较低尺寸侧指定距离计算方法,这是可能的。

首先,尝试将其嵌入球形表面。对于球形嵌入,请指定Haversine公式。

1
2
sphere_mapper = umap.UMAP(output_metric='haversine')
sphere_mapper.fit(dat)

生成的坐标以球坐标系显示,因此我不确定是否按原样绘制它们。因此,根据教科书将其转换为笛卡尔坐标系,然后进行绘制。

1
2
3
x = np.sin(sphere_mapper.embedding_[:, 0]) * np.cos(sphere_mapper.embedding_[:, 1])
y = np.sin(sphere_mapper.embedding_[:, 0]) * np.sin(sphere_mapper.embedding_[:, 1])
z = np.cos(sphere_mapper.embedding_[:, 0])

该图似乎无法单独工作,因此我用matplotlib自己绘制了它。

1
2
3
4
5
6
7
8
9
10
11
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
sns.set(style='white')

categories = sorted(list(set(labels)))
label_ids = [categories.index(l) for l in labels]

fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(x, y, z, c=label_ids, cmap='Spectral')

output_36_1.png

每种细胞类型都组织在球形表面上。

在3D中很难理解,因此我们将其扩展为2D。

1
2
3
4
x = np.arctan2(x, y)
y = np.arccos(z)
fig = plt.figure(figsize=(12, 12))
plt.scatter(x, y, c=label_ids, cmap='Spectral')

output_39_1.png

左侧和右侧最初已连接。

我不确定何时使用它,但是当数据具有固有的周期性特性时,它是否有效?

另外,还介绍了在双曲空间中的嵌入。我对此不太确定,因此我将仅遵循文档的流程。看来Poincare磁盘模型本身很难优化,因此似乎是通过针对双曲面模型进行学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
hyperbolic_mapper = umap.UMAP(output_metric='hyperboloid')
hyperbolic_mapper.fit(dat)

x = hyperbolic_mapper.embedding_[:, 0]
y = hyperbolic_mapper.embedding_[:, 1]
z = np.sqrt(1 + np.sum(hyperbolic_mapper.embedding_**2, axis=1))

disk_x = x / (1 + z)
disk_y = y / (1 + z)

fig = plt.figure(figsize=(12,12))
ax = fig.add_subplot(111)
ax.scatter(disk_x, disk_y, c=label_ids, cmap='Spectral')
boundary = plt.Circle((0,0), 1, fc='none', ec='k')
ax.add_artist(boundary)
ax.axis('off');

output_45_0.png

逆转换

一种从已学习嵌入的低维一侧的坐标生成相应高维样本的方法。

可能难以像VAE这样的生成模型那样使用,但快速了解嵌入式低维度是什么可能很有用。

对于高维样本生成,单细胞数据不是很有趣,因此在这里我将尝试使用Kuzushiji-MNIST数据(随机选择10,000个)。

1
2
dat = np.load('./data/kmnist-train-imgs_10k.npy')
dat.shape
1
(10000, 784)
1
2
labels = np.load('./data/kmnist-train-labels_10k.npy')
labels
1
array([4, 5, 0, ..., 6, 9, 0], dtype=uint8)

首先,尝试正常计算UMAP。

1
2
3
model = umap.UMAP(n_epochs=500, verbose=True).fit(dat)

umap.plot.points(model, labels=labels)

output_52_1.png

我想通过从该空间进行插值来对似乎有趣的区域进行逆变换,所以

选择聚类8左上,聚类9的左下,聚类0的右上和聚类3的右下附近的点,并进行100个测试点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
x = model.embedding_

top_left = x[labels == 8, :][x[labels == 8, 0].argmin()]
btm_left = x[labels == 9, :][x[labels == 9, 1].argmin()]
top_right = x[labels == 0, :][x[labels == 0, 0].argmax()]
btm_right = x[labels == 3, :][x[labels == 3, 1].argmin()]

test_pts = np.array([
    (top_left*(1-x) + top_right*x)*(1-y) +
    (btm_left*(1-x) + btm_right*x)*y
    for y in np.linspace(0, 1, 10)
    for x in np.linspace(0, 1, 10)
])

print(top_left)
print(btm_left)
print(top_right)
print(btm_right)
1
2
3
4
[-3.0056033  9.982167 ]
[2.0912035 2.2021403]
[14.147088  10.8581085]
[10.669375   2.6710818]

执行反向转换。为inverse_transform函数提供要查找的点的坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
inv_transformed_points = model.inverse_transform(test_pts)

from matplotlib.gridspec import GridSpec

fig = plt.figure(figsize=(12,6))
gs = GridSpec(10, 20, fig)
scatter_ax = fig.add_subplot(gs[:, :10])
kuzushiji_axes = np.zeros((10, 10), dtype=object)
for i in range(10):
    for j in range(10):
        kuzushiji_axes[i, j] = fig.add_subplot(gs[i, 10 + j])

scatter_ax.scatter(model.embedding_[:, 0], model.embedding_[:, 1],
                   c=labels.astype(np.int32), cmap='Spectral', s=0.1)
scatter_ax.set(xticks=[], yticks=[])

scatter_ax.scatter(test_pts[:, 0], test_pts[:, 1], marker='x', c='k', s=15)

for i in range(10):
    for j in range(10):
        kuzushiji_axes[i, j].imshow(inv_transformed_points[i*10 + j].reshape(28, 28), cmap='viridis')
        kuzushiji_axes[i, j].set(xticks=[], yticks=[])

output_60_0.png

有点粗糙,但是我可以猜出它是如何沿轴过渡的,以及为什么簇如此靠近。