t-SNE降维算法
在科学研究中处理高维数据的童鞋们,常常会遇到这种问题:我们明明知道自己的数据具有很好的内部特征,却无法找到合适的降维算法展示出来。由于每一个样品特征内都可能会存在一些离散点,线性降维例如PCA、PCoA常常难以有效的区分不同的样品特征,而且忠实于相互距离的线性算法往往难以获得满意的排序结果。这时候,你就需要更新自己的算法库啦!
这里我们介绍一种非线性算法,t分布随机邻域嵌入(t-distributed stochastic neighbor embedding,t-SNE)是一种用于探索高维数据的非线性降维机器学习算法。它将多维数据映射到适合于人类观察的两个或多个维度。PCA是一种线性算法,它不能解释特征之间的复杂多项式关系。而t-SNE是基于在邻域图上随机游走的概率分布来找到数据内的结构。线性降维算法的一个主要问题是不相似的数据点放置在较低维度表示为相距甚远,但为了在低维度用非线性流形表示高维数据,相似数据点必须表示为非常靠近,这不是线性降维算法所能做的。
具体原理步骤如下所示:
随机邻接嵌入(SNE)从通过将数据点之间的高维欧几里得距离转换为表示相似性的条件概率而开始,数据点xi、xj之间的条件概率pj|i由下式给出:
其中σi是以数据点xi为中心的高斯方差。对于高维数据点xi和xj的低维对应点yi和yj而言,可以计算类似的条件概率qj|i:
可以看出,SNE通过仿射(affinitie)变换将数据点映射到概率分布上,将两个数据点之间的欧式距离转换为以一个点为中心一定范围(也即高斯方差)内另一个点出现的条件概率。与线性算法最小化低维与高维下距离差不同,SNE试图最小化低维与高维下两个分布条件概率的差异,我们称之为KL散度,目标函数也即两个分布的cost如下所示:
由cost函数可以看出,KL散度具有不对称性,这主要通过最后对数相体现出来,高维下条件概率p与低维下条件概率q对调cost值就会不同,具体表现为该cost函数倾向于使用较大的q建模较小的p,也即会使原始数据中不同的特征之间区分更加明显,从而有效保留数据的局部特征,因此,SNE算法可以看成一种聚类簇识别算法。
SNE的代价函数关注于映射中数据的局部结构,优化该函数是非常困难的,因此在SNE的基础上提出t-SNE,其在高维空间下使用高斯分布将距离转换为概率分布,在低维空间下使用更加偏重长尾分布的方式来将距离转换为概率分布,使得高维度下中低等的距离在映射后能够有一个较大的距离,以减轻拥挤问题。
在最小化这个这两个分布的差异之后,我们最关心的是条件概率中涉及到的范围也即高斯方差σ。有些特征点周围数据点是稀疏的,有些是紧密的(聚类簇的特征不同),因此高斯方差大小也不同,因此定义困惑度:
其中H(Pi)是香农熵:
高斯方差σ越大,也即中心点周围划定的范围越大,那么其他点出现的条件概率的熵越大,那么困惑度也越大。困惑度可以被解释为一个点周围有效近邻点的数目。困惑度由用户指定,典型值在5和50之间。
t-SNE非线性降维算法通过基于具有多个特征的数据点的相似性识别观察到的簇来在数据中找到模式。本质上是一种降维和可视化技术。另外t-SNE的输出可以作为其他分类算法的输入特征。t-SNE几乎可用于所有高维数据集,广泛应用于图像处理,自然语言处理,基因组数据和语音处理。在R中具有Rtsne包可以实现t-SNE分析,所使用的函数为Rtsne(X, ...),其中X为数据矩阵,每一行为一个记录,Rtsne对行进行降维排序。
在生物学中,扩增子、宏基因组群落组成数据以及宏基因组、基因组的功能注释数据也可以使用t-SNE算法进行分析,下面进行实例分析:
代码语言:javascript复制#读取KEGG注释数据
kegg=read.table("genome_kegg.txt", header=TRUE)
rownames(kegg)=kegg[, 1]
kegg=kegg[, -1]
kegg=as.matrix(kegg)
#读取分组与颜色配置文件
group=read.table("genome_group_col.txt", header=FALSE)
rownames(group)=group[,1]
col=read.table("genome_tax_col.txt", header=FALSE)
rownames(col)=col[,1]
#进行t-SNE分析
library(Rtsne)
kegg_tsne=Rtsne(kegg,dims=2, perplexity=40, check_duplicates=FALSE)
#绘制做图结果
ggdata=data.frame(Dim1=kegg_tsneY[,1],Dim2=kegg_tsneY[,2], Group=group[,3])
ggplot(ggdata)
geom_point(aes(x=Dim1, y=Dim2, col=Group), size=3)
scale_colour_manual(values=as.character(col[,2]))
theme_classic()
其中dims为降维后的维数,由用户指定,一般为二维;perplexity为困惑度,由用户指定,应该小于(nrow(X) - 1)/3。困惑度越小,得到的聚类簇越多、越分散;困惑度越大,得到的聚类簇越少、越集中。最终绘图结果如下所示:
下面使用相同的数据进行PCA分析,可以对比两者的差别:
代码语言:javascript复制kegg_pca=prcomp(kegg, scal=FALSE)
pc12=kegg_pca$x[, 1:2]/10
pca_sum=summary(kegg_pca)
pcap=as.vector(round(pca_sum$importance[2, 1:2], 3), mode="any")
pcapp=paste(pcap*100,"%",sep="")
pcap1=paste("PC1(",pcapp[1], ")", sep="")
pcap2=paste("PC2(",pcapp[2], ")", sep="")
ggdata=data.frame(pc12,Group=group[,3])
ggplot(ggdata)
geom_point(aes(x=PC1, y=PC2, col=Group),size=3)
scale_colour_manual(values=as.character(col[,2]))
labs(x=pcap1, y=pcap2)
theme_classic()
可以看到,相同数据集经过PCA分析不同聚类簇之间难以区分,而t-SNE则获得了区分明显的聚类簇,将数据集内部的结构特征充分挖掘出来。那么问题来了,既然困惑度由用户指定,t-SNE根据用户指定的困惑度探索最佳的降维方法,那么用户如何确定适合自己数据集的困惑度呢?在后续的文章中将会给出解答。