vlambda博客
学习文章列表

单细胞转录组聚类算法: scDeepCluster

今天继续介绍一种基于自编码网络的单细胞转录组聚类算法: scDeepCluster.

本文将涉及上次介绍的 DCA 算法,错过的同学请回顾上一篇推文:

此外立个 Flag,考虑到大家背景的不同,计划写2篇推文详细介绍:

  1. 自编码网络及其变体(去噪自编码、变分自编码、深度嵌入聚类)
  2. 聚类、填补、整合等算法的性能评价指标

还请各位读者耐心等待!


前言

本文是聚类(Clustering)系列的开篇之作,后续将持续更新该系列.

近几年单细胞转录组聚类算法的发展特别快,目前主流的算法主要分为以下几类[1]:

  1. 基于划分(Partition)的:
    • pcaReduce
    • SC3
    • SAIC
    • RaceID系列
  2. 基于层次聚类的:
    • CIDR
    • BackSPIN
    • CellTree
    • SINCERA
  3. 基于图的:
    • SIMLR
    • SNN-Cliq
    • Seurat/Scanpy(Louvain)
  4. 基于模型的:
    • BISCUIT
    • DTWScore
    • TSCAN
  5. 基于密度的:
    • GiniClust系列
    • Monocle
  6. 基于神经网络的:
    • scDeepCluster
    • scDCC(scDeepCluster的后续工作)

需要说明的是,聚类的效果也和特征、距离度量等因素有关,例如通过质量控制、标准化特征选择以及降维得到的新特征,较原特征通常可以取得更好的聚类结果.

scDeepCluster 于 19 年发表在 Nature Machine Intelligence,paper 名为 Clustering single-cell RNA-seq data with a model-based deep learning approach,单位为新泽西理工学院计算机科学系,作者为田天等人.

  • 论文链接:https://www.nature.com/articles/s42256-019-0037-0
  • 代码链接:https://github.com/ttgump/scDeepCluster
  • 教程:https://github.com/ttgump/scDeepCluster/blob/master/Tutorial.ipynb

背景

我们知道,在 scRNA-seq 的分析中,聚类分析是非常关键的一步,通过聚类可以识别细胞簇,进而并利用已知的细胞类型标记基因确定细胞簇的身份、发现罕见细胞类型等.

然而,对 scRNA-seq 数据做聚类是一个在统计和计算上很有挑战性的问题,主要有几点原因:

  1. 高度稀疏性,特别是 dropout 事件
  2. 基因表达水平的差异性很大,即使是同组的细胞也是如此
  3. 待聚类的细胞数量多,计算和存储复杂度高

前文已经提到过,针对 scRNA-seq 数据,目前已经有了很多很成熟的聚类算法. 然而这些算法要么是没有针对 scRNA-seq 数据的特点(例如超散度(over-dispersion)和零膨胀)建模,要么是计算和存储复杂度高(例如谱聚类).

另外一个研究是,通过对 scRNA-seq 数据作填补/去噪,来改善聚类等下游分析的结果,例如上篇提到的 DCA(Deep Count Autoencoder) 算法.

DCA 对 scRNA-seq 数据建模为零膨胀负二项分布(Zero-inflated Negative Binomial, ZINB),该分布可以有效的拟合 scRNA-seq 数据. 各类实验结果表明,用 DCA 填补数据后可以有效的提升下游分析的结果.

DCA 算法示意图

但是这类填补的方法并没有针对聚类作特别的设计或优化,而是先填补,再用一些简单的聚类算法实现聚类. 事实上,这种分开的策略并不是最优的,更好的方法应该是将这两点结合起来(也就是 scDeepCluster),作者在后文中通过与 DCA 对比实验证明了这点.

此外,基于深度学习的方法可以学到一个非线性变换,将原始高维数据映射到较小的潜空间中,从而获得一个很好的低维表示,这类方法在图像和文本数据的聚类上已经取得了成功的应用. 同时,深度学习类方法也可以应对很大的数据集,通过将数据分为数个 mini-batch 进行训练和处理,可以有效地降低计算和存储复杂度.

综合上述这些考虑,作者提出了一种基于深度学习的聚类方法,将 ZINB 模型与聚类损失相结合. 目的是在执行降维时显式地优化聚类,其核心是深度嵌入聚类(Deep Embedded Clustering, DEC)[2]和改进深度嵌入聚类(Improved Deep Embedded Clustering, DEC)[3]算法.

深度嵌入聚类

深度嵌入式聚类是一种使用深度神经网络(一般为自编码网络),同时学习特征表示和聚类分配的方法. DEC 学习了从数据空间到低维特征空间的映射,并在该特征空间中迭代地优化聚类目标. 在图像和文本语料库上的实验评估表明,与现有技术相比,该方法在性能上有着显著提升.

Hinton 等人[4]提出了 t-SNE 算法,证明了最小化数据分布和嵌入分布之间的 Kullback-Leibler(KL) 散度,可用于数据可视化和降维,而 t-SNE 的参数变体[5]可使用深度神经网络对嵌入进行参数化.

KL 散度是一种衡量两个概率分布的匹配程度的指标,两个分布差异越大,KL散度越大.

假设对于同一个随机变量  有两个单独的概率分布  和  ,则 KL 散度的定义为:

DEC 是从 t-SNE 中获得启发,该算法定义了基于质心的概率分布,并迭代优化该分布与一个辅助目标分布的 KL 散度,从而同时改善了聚类和特征表示.

算法概述

考虑将  个点  的集合聚类为  个类的问题,每个聚类由质心  表示.

DEC 首先使用非线性映射  变换数据,而不是直接在数据空间  中进行聚类.

其中  是可学习的参数, 是潜在特征空间.  的维度通常比  小得多,以避免"维度诅咒".

为了对映射  进行参数化,深层神经网络(Deep Neural Network, DNN)由于其理论函数逼近特性和已证明的特征学习能力而成为最好的选择.

DEC 算法通过同时学习特征空间  中的  个聚类中心  和将数据点映射到  的 DNN 参数  来对数据进行聚类.

DEC有两个阶段:

  1. 预训练深度自编码器进行参数初始化(参数包括网络参数  和聚类质心  ).

  2. 参数优化(即聚类):选取 AE 模型中的 Encoder 部分,加入聚类层,迭代优化辅助目标分布与聚类分配的 KL 散度.

传统的做法是,训练一个自编码网络重构数据,来实现特征的压缩(即得到嵌入后的低维表示),进一步使用一些聚类算法如 k-means 等对低维表示实现聚类,得到聚类质心和聚类分配.

DEC 则考虑在此基础上进一步聚类.

我们直接从阶段2开始描述,假设已有初始网络参数  和聚类质心  .

Math Warning!

软分配

基于 t-SNE 的思想,DEC 使用学生t-分布(Student's t-distribution,简称 t-分布),作为核来测量嵌入点和质心间的相似度:

其中  对应于嵌入后的  是 t 分布的自由度,通常设为1.

注意到  ,且  ,可解释为将样本  分配给簇  的概率(即软分配). 最终的聚类结果选取为  ,即属于每个簇的概率中,概率最大的簇.

目标分布(辅助分布)

DEC 考虑通过在辅助目标分布的帮助下,从聚类的高置信度分配中学习来迭代地蒸馏聚类.

具体来说,通过将软分配与目标分布匹配来训练模型. 为此,将目标定义为软分配  和辅助分布  之间的 KL 散度损失.

目标分布  的选择对于 DEC 的性能至关重要,我们希望目标分布具有以下属性:

  1. 增强预测(即提高簇纯度).  分布为软分配的概率,那么  如果使用delta分布来表示(传统做法),显得比较原始
  2. 更加重视具有高置信度的数据点
  3. 规范每个质心的损失贡献,以防止大类扭曲隐藏的特征空间.

作者根据经验,选择了以下目标分布(辅助分布):

其中  .

在实验中,观察到 DEC 通过学习高置信度预测来提高每次迭代中的初始估计,从而有助于改善低置信度预测.

优化

目标函数是  和  的 KL 散度:

采用梯度下降,求对嵌入结果  和聚类质心  的梯度(事实上现在写代码有自动微分技术,不需要我们手动求梯度了):

接着就是根据链式法则对编码器的参数  求梯度,实际上优化的是参数  和聚类质心  .

训练

接下来将讨论如何进行参数  和聚类质心  的初始化.

一般使用堆叠自编码器(Stacked Autoencoder, SAE)初始化 DEC.

假设网络是对称的,即编码器和解码器层数一致,第  层编码器的输入和倒数第  层解码器的输出维度一致.

首先逐层初始化 SAE 网络,每一层都是一个去噪自编码器(Denoising Autoencoder, DAE),定义为:

其中  和  分别为第  层编码器和倒数第  层解码器, 和  分别为对应的可学习的参数.

通过最小化  和  间的重构损失(通常是MSE)来训练这层编码器和解码器,接着将编码器的输出  作为训练下一层编码器和解码器时的  .

执行完这种贪婪的逐层训练后,将所有编码器层与所有解码器层连接起来,形成一个深层的自编码器,然后对其微调以最小化重建损失.

然而 scDeepCluster 的预训练不是这么贪婪地逐层训练的,而是直接端到端的训练.

接着丢弃解码器,并将编码器用作数据空间  和特征空间  之间的初始映射,于是待训练的参数  实际为编码器的参数.

进一步根据编码器得到降维结果用 k-means 聚类以得到  个初始聚类质心  .

最后,根据式(5)和(6)采用梯度优化更新网络参数和聚类质心,当聚类结果发生改变的点小于总数的 tol% 时,停止训练.

整体网络结构如下图所示:

单细胞转录组聚类算法: scDeepCluster

DEC算法示意图

聚类个数  的确定

事实上,聚类个数  往往是未知的,且  的选择对结果的影响很大. 因此,需要一种确定最佳簇数的方法.

为此,作者定义两个度量:

  1. 归一化互信息(Normalized NMI),是存在真实标签时比较常用的衡量聚类个数的办法,定义为:

其中  是互信息度量, 是熵.

  1. 泛化性(Generalizability),定义为:

表示训练集和验证集上的损失,该指标用来衡量过拟合的程度.

作者观察到,当  大于最优簇数时,G 的值会急剧下降,这表明 G 可以作为估计最优簇数的一个很好的度量.

scDeepCluster 采用第二种方式选取  .

改进的深度嵌入聚类

后续又有一个改进的深度嵌入聚类(Improved Deep Embedded Clustering, IDEC)算法[3].

IDEC的作者发现优化式(4)的目标函数会破坏特征空间,导致学到无意义的表达和损害聚类的性能.

为了尽可能的在特征空间中保留数据的局部结构信息,作者提出保留自编码的结构(其实做法很简单,也就是不丢弃解码器),优化以下改进的目标函数:

其中, 为自编码器的重构误差, 为聚类损失,即 KL 散度, 是控制两个损失相对大小的超参数.

单细胞转录组聚类算法: scDeepCluster

IDEC算法示意图

作者没有分析优化式(4)会损坏特征空间的具体原因,但从实验的角度说明了  的作用主要是保留数据的局部结构信息. 此外,虽然实验显示 IDEC 效果优于 DEC,但 IDEC 收敛得更慢.

最后,scDeepCluster 实际上用的就是 IDEC,只不过采用的是和 DCA 一样的网络结构和重构损失.

scDeepCluster 算法

前文已经讲清楚了 DEC 和 IDEC,于是 scDeepCluster 就可以很快介绍完了.

数据预处理

针对原始的 scRNA-seq 计数数据  作以下预处理(代码是基于SCANPY实现的):

  1. 首先剔除在所有细胞中都不表达的基因
  2. 接着计算每个细胞的大小因子(size factor),并按库大小对计数矩阵作归一化,具体的,记细胞  的库大小(总计数)为  ,则细胞  的大小因子为  . 对每个细胞的总计数除以其大小因子,保证每个细胞都有相同的总计数,以此来达到消除测序深度的偏差.
  3. 最后作对数变换和 z-score 标准化,从而使表达值服从零均值和单位方差.

处理完的数据   和大小因子作为网络的输入.

网络结构和损失函数和训练

网络结构与 DCA 一致,都是基于 ZINB 模型的自编码网络,两个隐层大小分别为 256 和 64,瓶颈层大小为 32, 设为 1.

scDeepCluster 采用 ZINB 作为重构损失:

预训练阶段, 直接端到端地训练. 采用 AMSGrad 优化器,参数设置为  .

聚类阶段,优化器采用 Adadelta,参数设置为  .

预训练和聚类阶段的 batch size 都设置为 256,都对网络每层的输入加高斯白噪声,标准差为 2.5,仅在质心初始化阶段使用不加噪声的编码器.

当聚类结果发生改变的点小于总数的 0.1% 时,停止训练.

整体过程如下图所示:

scDeepCluster算法示意图

代码实现

Take is cheap, show me the code!

(我不喜欢Tensorflow,代码太混乱了)

  1. 数据预处理

使用 scanpy 做数据预处理:

源代码使用的 scanpy 版本为 1.0.4,与现在常用版本存在一定区别

adata 为原始 count 矩阵

import scanpy.api as sc
# Now use 'import scanpy as sc'

if filter_min_counts:
sc.pp.filter_genes(adata, min_counts=1)
sc.pp.filter_cells(adata, min_counts=1)

if size_factors:
sc.pp.normalize_per_cell(adata)
adata.obs['size_factors'] = adata.obs.n_counts / np.median(adata.obs.n_counts)

if logtrans_input:
sc.pp.log1p(adata)

if normalize_input:
sc.pp.scale(adata)
  1. 自编码网络:

是用 keras 写的:

dims 为输入层到瓶颈层的各层维度

def autoencoder(dims, noise_sd=0, init='glorot_uniform', act='relu'):
"""
Fully connected auto-encoder model, symmetric.
Arguments:
dims: list of number of units in each layer of encoder. dims[0] is input dim, dims[-1] is units in hidden layer.
The decoder is symmetric with encoder. So number of layers of the auto-encoder is 2*len(dims)-1
act: activation, not applied to Input, Hidden and Output layers
return:
Model of autoencoder
"""

n_stacks = len(dims) - 1

输入包括:(1) 大小因子 sf_layer,(2) 预处理后的基因表达数据 x

对输入的基因表达数据添加高斯噪声

# input
sf_layer = Input(shape=(1,), name='size_factors')
x = Input(shape=(dims[0],), name='counts')
h = x
h = GaussianNoise(noise_sd, name='input_noise')(h)

编码器和瓶颈层:

采用全连接网络,编码器每层在做完全连接后都添加高斯噪声

# internal layers in encoder
for i in range(n_stacks-1):
h = Dense(dims[i + 1], kernel_initializer=init, name='encoder_%d' % i)(h)
h = GaussianNoise(noise_sd, name='noise_%d' % i)(h) # add Gaussian noise
h = Activation(act)(h)
# hidden layer
h = Dense(dims[-1], kernel_initializer=init, name='encoder_hidden')(h) # hidden layer, features are extracted from here

解码器和输出:

均值 mean、概率 pi 和散度 disp,分别采用指数、sigmoid 和 softplus 激活函数

最终输出的是一个自编码网络,网络输入是 [x, sf_layer],网络输出是均值 mean 和大小因子 sf_layer 的乘积(即去噪的结果) output、

# internal layers in decoder
for i in range(n_stacks-1, 0, -1):
h = Dense(dims[i], activation=act, kernel_initializer=init, name='decoder_%d' % i)(h)

MeanAct = lambda x: tf.clip_by_value(K.exp(x), 1e-5, 1e6)
DispAct = lambda x: tf.clip_by_value(tf.nn.softplus(x), 1e-4, 1e4)

# output
pi = Dense(dims[0], activation='sigmoid', kernel_initializer=init, name='pi')(h)
disp = Dense(dims[0], activation=DispAct, kernel_initializer=init, name='dispersion')(h)
mean = Dense(dims[0], activation=MeanAct, kernel_initializer=init, name='mean')(h)

output = ColWiseMultLayer(name='output')([mean, sf_layer])
output = SliceLayer(0, name='slice')([output, disp, pi])

return Model(inputs=[x, sf_layer], outputs=output)
  1. 聚类层

超参数包括:(1) 聚类簇个数 n_clusters,(2) t 分布参数 

class ClusteringLayer(Layer):
"""
Clustering layer converts input sample (feature) to soft label, i.e. a vector that represents the probability of the
sample belonging to each cluster. The probability is calculated with student's t-distribution.
# Example
```
model.add(ClusteringLayer(n_clusters=10))
```
# Arguments
n_clusters: number of clusters.
weights: list of Numpy array with shape `(n_clusters, n_features)` witch represents the initial cluster centers.
alpha: parameter in Student's t-distribution. Default to 1.0.
# Input shape
2D tensor with shape: `(n_samples, n_features)`.
# Output shape
2D tensor with shape: `(n_samples, n_clusters)`.
"""


def __init__(self, n_clusters, weights=None, alpha=1.0, **kwargs):
if 'input_shape' not in kwargs and 'input_dim' in kwargs:
kwargs['input_shape'] = (kwargs.pop('input_dim'),)
super(ClusteringLayer, self).__init__(**kwargs)
self.n_clusters = n_clusters
self.alpha = alpha
self.initial_weights = weights
self.input_spec = InputSpec(ndim=2)

计算软分配 q:

inputs 为低维表示,self.clusters 为聚类质心

def call(self, inputs, **kwargs):
""" student t-distribution, as same as used in t-SNE algorithm.
q_ij = 1/(1+dist(x_i, u_j)^2), then normalize it.
Arguments:
inputs: the variable containing data, shape=(n_samples, n_features)
Return:
q: student's t-distribution, or soft labels for each sample. shape=(n_samples, n_clusters)
"""

q = 1.0 / (1.0 + (K.sum(K.square(K.expand_dims(inputs, axis=1) - self.clusters), axis=2) / self.alpha))
q **= (self.alpha + 1.0) / 2.0
q = K.transpose(K.transpose(q) / K.sum(q, axis=1))
return q
  1. scDeepCluster

self.autoencoder 为函数 "autoencoder" 返回的自编码网络

class SCDeepCluster(object):
def __init__(self, dims, n_clusters=10, noise_sd=0, alpha=1.0, ridge=0,debug=False):
super(SCDeepCluster, self).__init__()

self.dims = dims
self.input_dim = dims[0]
self.n_stacks = len(self.dims) - 1

self.n_clusters = n_clusters
self.noise_sd = noise_sd
self.alpha = alpha
self.act = 'relu'
self.ridge = ridge
self.debug = debug
self.autoencoder = autoencoder(self.dims, noise_sd=self.noise_sd, act=self.act)

self.encoder 为中间不添加噪声的编码器部分

# prepare clean encode model without Gaussian noise
ae_layers = [l for l in self.autoencoder.layers]
hidden = self.autoencoder.input[0]
for i in range(1, len(ae_layers)):
if "noise" in ae_layers[i].name:
next
elif "dropout" in ae_layers[i].name:
next
else:
hidden = ae_layers[i](hidden)
if "encoder_hidden" in ae_layers[i].name: # only get encoder layers
break
self.encoder = Model(inputs=self.autoencoder.input, outputs=hidden)

self.loss 为 ZINB 损失

pi = self.autoencoder.get_layer(name='pi').output
disp = self.autoencoder.get_layer(name='dispersion').output
mean = self.autoencoder.get_layer(name='mean').output
zinb = ZINB(pi, theta=disp, ridge_lambda=self.ridge, debug=self.debug)
self.loss = zinb.loss

self.model 为 scDeepCluster 的模型,输入为自编码网络的输入(即大小因子和与处理后的基因表达数据),输出为软分配和去噪结果

clustering_layer = ClusteringLayer(self.n_clusters, alpha=self.alpha, name='clustering')(hidden)
self.model = Model(inputs=[self.autoencoder.input[0], self.autoencoder.input[1]],
outputs=[clustering_layer, self.autoencoder.output])

预训练:

x 为大小因子和预处理后基因表达数据,y 为重构目标(即原始 count 数据)

预训练是直接端到端训练的,损失 self.loss 为 ZINB 损失

def pretrain(self, x, y, batch_size=256, epochs=200, optimizer='adam', ae_file='ae_weights.h5'):
print('...Pretraining autoencoder...')
self.autoencoder.compile(loss=self.loss, optimizer=optimizer)
es = EarlyStopping(monitor="loss", patience=50, verbose=1)
self.autoencoder.fit(x=x, y=y, batch_size=batch_size, epochs=epochs, callbacks=[es])
self.autoencoder.save_weights(ae_file)
print('Pretrained weights are saved to ./' + str(ae_file))
self.pretrained = True

模型训练过程:

损失为 loss=['kld', self.loss],为 KL 散度和 ZINB 损失,加权的权重为 loss_weights

def fit(self, x_counts, sf, y, raw_counts, batch_size=256, maxiter=2e4, tol=1e-3, update_interval=140,
ae_weights=None, save_dir='./results/scDeepCluster', loss_weights=[1,1], optimizer='adadelta')
:


self.model.compile(loss=['kld', self.loss], loss_weights=loss_weights, optimizer=optimizer)

首先作预训练或读取训练好的参数:

# Step 1: pretrain
if not self.pretrained and ae_weights is None:
print('...pretraining autoencoders using default hyper-parameters:')
print(' optimizer=\'adam\'; epochs=200')
self.pretrain(x, batch_size)
self.pretrained = True
elif ae_weights is not None:
self.autoencoder.load_weights(ae_weights)
print('ae_weights is loaded successfully.')

接着用 k-means 初始化聚类质心:

注意此时使用的是无噪声添加的编码器 self.encoder 来得到低维表示

# Step 2: initialize cluster centers using k-means
print('Initializing cluster centers with k-means.')
kmeans = KMeans(n_clusters=self.n_clusters, n_init=20)
self.y_pred = kmeans.fit_predict(self.encoder.predict([x_counts, sf]))
y_pred_last = np.copy(self.y_pred)
self.model.get_layer(name='clustering').set_weights([kmeans.cluster_centers_])

最后训练:

为方便展示,删除/调整了部分源代码

注意是,每隔 update_interval 算一次聚类分配和辅助分布,判断是否要停止训练

每次迭代用的辅助分布 p 是上次 update 时算的,而不是每次迭代都算一遍

# Step 3: deep clustering
for ite in range(int(maxiter)):
if ite % update_interval == 0:
q, _ = self.model.predict([x_counts, sf], verbose=0)
p = self.target_distribution(q) # update the auxiliary target distribution p
# evaluate the clustering performance
self.y_pred = q.argmax(1)
# check stop criterion
delta_label = np.sum(self.y_pred != y_pred_last).astype(np.float32) / self.y_pred.shape[0]
y_pred_last = np.copy(self.y_pred)
if ite > 0 and delta_label < tol:
break

batch_idx = index * batch_size:(index + 1) * batch_size
loss = self.model.train_on_batch(x=[x_counts[batch_idx], sf[batch_idx]], y=[p[batch_idx], raw_counts[batch_idx]])
index += 1
  1. 主函数

y 是真实分类结果,仅用于训练过程中计算ARI等指标

噪声方差设为 2.5,和论文一致

y = np.array(data_mat['Y'])

scDeepCluster = SCDeepCluster(dims=[input_size, 256, 64, 32], n_clusters=args.n_clusters, noise_sd=2.5)
# Pretrain autoencoders before clustering
if args.ae_weights is None:
scDeepCluster.pretrain(x=[adata.X, adata.obs.size_factors], y=adata.raw.X, batch_size=args.batch_size, epochs=args.pretrain_epochs, optimizer=optimizer1, ae_file=args.ae_weight_file)

# begin clustering
scDeepCluster.fit(x_counts=adata.X, sf=adata.obs.size_factors, y=y, raw_counts=adata.raw.X, batch_size=args.batch_size, tol=args.tol, maxiter=args.maxiter, update_interval=args.update_interval, ae_weights=args.ae_weights, save_dir=args.save_dir, loss_weights=[args.gamma, 1], optimizer=optimizer2)

最后还是要说一句,我讨厌 TensorFlow 1(Pytorch yyds!).

感兴趣的话还请看源代码,更完整.

实验

作者设计了多个对比实验:

  1. 设计多种不同场景的仿真数据,包括不同 dropout 率、不同聚类信号强度和不平衡样本数
  2. 采用不同测序平台的4个真实数据测试算法性能
  3. 验证 scDeepCluster 可以提取理想的低维嵌入表示,提供不错的可视化结果
  4. 验证 scDeepCluster 具有高度可扩展性,运行时间随细胞数线性增长

实验部分内容不多且很常规,感兴趣的读者建议阅读原文.

结论

总结一下,作者提出了一种用于对 scRNA-seq 数据进行聚类分析的算法 scDeepCluster. 该算法将深度学习和参数模型结合起来,对具有过多零值的 scRNA-seq 数据的生成建模.

仿真和真实数据上的结果表明,该算法优于其他现有算法,可以有效地捕获 dropout 事件,且在大型数据集上的具有高度可扩展性.


参考文献

[1] Petegrosso R, Li Z, Kuang R. Machine learning and statistical methods for clustering single-cell RNA-sequencing data[J]. Briefings in Bioinformatics, 2020, 21(4): 1209–1223.

[2] Xie, J, Girshick R, Farhadi A. Unsupervised deep embedding for  clustering analysis[C]. In Proc. 33rd International Conference on Machine Learning, 2016: 478-487.

[3] Guo X, Gao L, Liu X, Yin J. Improved deep embedded clustering with local structure preservation[C]. In Proc. 26th International Joint Conference on  Artifcial Intelligence, 2017: 1753-1759.

[4] Maaten L V D, Geoffrey H. Visualizing data using t-SNE[J]. Journal of machine learning research, 2008, 9: 2579-2605.

[5] Maaten L V D . Learning a Parametric Embedding by Preserving Local Structure[J]. Journal of Machine Learning Research, 2009, 5:384-391.