最近在整理非线性降维的方法笔记,翻到 UMAP 相关的资料,顺手把从 UMAP 到 UMAP-Dense 这条线重新梳理了一遍。我这个笔记的切入点其实很朴素:很多项目里我都用 UMAP 做高维特征的可视化,比如单细胞表达谱、图像特征向量、用户行为序列的 embedding,但工作中陆续发现“降维效果好”和“可视化效果好”是两回事。尤其数据集本身密度不均匀的时候,普通 UMAP 会把低密度区域压缩得特别严重,聚类看起来清清楚楚,但实质上把局部结构给破坏了。后来接触了 UMAP-Dense 这种密度保持型扩展,才好了一些。
这篇笔记适合两类人看:一类是刚开始做高维数据可视化的同学,想弄明白 UMAP 到底在算什么、参数怎么调;另一类是已经跑熟 UMAP、但总觉得结果“好看但不可信”的从业者,想搞清楚普通 UMAP 和密度保持版 UMAP-Dense 的差别,以及各自该用在什么场景。笔记不会堆公式,但会把原理和实操经验结合着写,重点说清楚为什么 UMAP-Dense 能保留密度、又在什么情况下反而会添乱。
1. 非线性降维需求与 UMAP 的定位
先说一个很多人忽略的前提:线性降维,比如 PCA,适合的是“数据本身大致在一个线性子空间里”的情况。但真实数据几乎不满足这个假设。拿基因表达数据来说,不同细胞类型对应的是高维空间里弯弯曲曲的流形结构,而不是一个平直的平面,PCA 投影之后各类群很容易互相叠在一起。非线性降维要做的就是把这些弯曲的流形“展开”到二维或三维,同时尽量保留点与点之间的局部邻接关系。
在非线性降维这个领域,UMAP 出现的时间比 t-SNE 晚,但它几乎是目前最常用的方法。原因很实际:速度快,能处理几十万甚至上百万级别的样本;初始化和随机种子对结果的影响相对可控;参数少,默认值往往就能出比较合理的结果;而且它保留了相对更好的全局结构。很多做单细胞测序的团队,已经默认把 UMAP 作为标准可视化工序了。
我个人的体会是,UMAP 最核心的设计思想可以概括成两步:先在原始高维空间里构建一张加权近邻图,再用优化的方式把这张图“尽量原样”地搬到低维空间里。第一步负责定义“什么是附近”,第二步负责把“附近”变成平面上的距离。
这里要特意提一下 UMAP-Dense。它并不是一个完全独立的新算法,而是在 UMAP 框架基础上加入了“密度保持”约束的变体。市面上实现这套思想的库不算多,最常见的实现方式是在损失函数里加一个密度相关性正则项,让低维嵌入中的局部密度顺序与高维空间中的局部密度顺序尽量一致。社区里有人管它叫 densMAP,也有人直接叫 UMAP-Dense,我这里就用 UMAP-Dense 作为统称,因为这篇笔记标题就是这么写的。
适用场景大致可以这样划分:
- 可视化为主,样本量几万到几十万,特征维度几十到几千:直接用 UMAP。
- 数据来源混杂、不同群体密度差异明显,比如某些细胞群体数量特别少、某些用户群体行为特别集中:建议加密度保持机制。
- 需要根据可视化结果判断“这个群体是不是足够紧凑”“那个边界是否真实”时,UMAP-Dense 比普通 UMAP 更接近真实拓扑。
2. UMAP 原理拆解:图构建与低维布局
2.1 高维图构建:近邻搜索与模糊集表示
UMAP 的第一步,是找到每个点的 k 个近邻。这个“近邻”的定义会在很大程度上决定最终嵌入形态。如果样本量很大,直接计算两两距离是 O(n²) 的,根本跑不动,所以实际实现里会用最近邻搜索树或者近似近邻算法来加速。
拿到近邻之后,UMAP 做的不是简单地把两个点连接起来,而是把这段局部距离转换成一个区间化的相似度。这个转换和 t-SNE 里计算高斯核概率相似,但 UMAP 用的是一个更宽松的指数函数,并且不需要像 t-SNE 那样做严格的归一化。简单理解:距离越近,相似度越高,但相似度不会因为某个局部区域密度太高而无限逼近 1。
这一步里的关键参数是n_neighbors。它控制着每个点会“看见”多少个邻居,本质上是在平衡局部细节和全局结构。n_neighbors越小,每个点只跟距离最近的少数点建立连接,结果会偏向于展现非常精细的局部结构,但容易碎成很多孤立的小团;n_neighbors越大,连接关系越宏观,局部细节会被平滑掉,容易挤压掉一些小群体。
另一个参数metric决定距离怎么算。默认是euclidean,但如果特征是稀疏的高维向量,比如 TF-IDF 向量或者某些 one-hot 编码特征,欧氏距离的表现并不好,换成cosine往往更稳定。我调参的时候,第一步永远是看metric,因为它决定了输入数据的“世界观”。
2.2 低维布局:从谱初始化到梯度优化
图构建完成之后,UMAP 会在低维空间里初始化一组点的位置,然后通过优化把高维图中的连接关系“复刻”到低维坐标中。
初始化方式通常有两种:随机初始化和谱初始化。谱初始化是用高维图的拉普拉斯特征向量作为低维坐标,相当于先用线性方法给一个大致布局,再做非线性微调。这种方式的好处是可复现性更好,起始状态不容易陷入局部最小值。默认情况下,UMAP 会使用谱初始化,只是当样本量特别大时会自动切换成随机初始化。
优化目标是一个交叉熵损失。这个交叉熵可以粗略地理解成两部分:一部分鼓励原本在高维中相邻的点在低维中也靠在一起,另一部分鼓励原本不相邻的点不要被强行拉近。UMAP 从 t-SNE 的 KL 散度中演化而来,但做了两点改动:一是取消了归一化,二是对非邻居点使用了不同的权重策略,从而让优化过程更快,也让全局结构保持得更好。
低维空间中点与点之间的距离,由一个名为min_dist的参数控制。min_dist越小,点与点之间的最小间隔越小,聚类内部的点会挤得越紧,群体边界越分明;min_dist越大,嵌图越是松散的“云团”分布,更适合看整体趋势。
2.3 为什么普通 UMAP 会在密度不均时失真
理解 UMAP 的图构建方式之后,就不难解释密度失真的来源了。
在均匀密度的流形上,每个点的近邻距离分布是类似的,最终嵌入能很好地反映结构。可当数据密度差异悬殊时,就会出问题。设想有两个区域:一个区域包含大量密集样本,另一个区域只有少量稀疏样本。对稀疏区域的点来说,它找到的 k 个近邻可能已经跨了很长的实际距离;对密集区域的点来说,k 个近邻可能只在很小的半径内。但 UMAP 的布局优化过程会“一视同仁”地要求所有近邻对在低维空间里靠得足够近,于是出现一种常见后果:稀疏区域的点被严重压缩,和密集区域之间的空白被抹平,看起来就像所有群体都挤在一起。
这种现象在单细胞数据里尤其常见。某一类罕见细胞群体数量少,内部表达差异也不大,在普通 UMAP 图上往往会被压到主群体旁边,几乎看不出独立分布。这不是算法有 bug,而是损失函数压根没有把“密度”作为需要保持的信息。
3. 从 UMAP 到 UMAP-Dense:密度信息如何被保留
3.1 UMAP-Dense 要解决的问题
UMAP-Dense 这类密度保持型变体的目标可以一句话说清楚:让低维空间中每个点的局部密度,尽量保持与高维空间中该点的局部密度一致。
什么是“局部密度”?一个直观的计算方式是:取每个点到它的第 k 个近邻的距离,距离越小说明周围点越密集,距离越大说明周围越稀疏。这个距离可以当作密度的反指标。普通 UMAP 在优化过程中完全不管这个指标,而 UMAP-Dense 会在优化时增加一个衡量“高维密度序列”和“低维密度序列”相关性的损失项。
损失函数把两部分加在一起:一部分是原本的 UMAP 布局损失,负责保持邻接关系;另一部分是密度相关性损失,负责让“拥挤的地方继续拥挤,稀疏的地方继续稀疏”。两部分之间有一个权重系数,默认值会尽量平衡两者,但实际使用时,如果数据密度差异极大,可以把密度损失权重调高一点,让密度保持优先。
3.2 UMAP-Dense 的典型实现方式
在常用 Python 库umap-learn中,可以通过开启densmap参数来启用密度保持模式,同时配合output_dens=True输出每个点的局部密度估计值。
典型调用形式:
import umap # 普通 UMAP model = umap.UMAP( n_neighbors=15, min_dist=0.1, n_components=2, random_state=42 ) embedding = model.fit_transform(X) # UMAP-Dense(密度保持模式) model_dense = umap.UMAP( n_neighbors=15, min_dist=0.1, n_components=2, densmap=True, output_dens=True, random_state=42 ) embedding_dense, dens_row, dens_col = model_dense.fit_transform(X)需要注意,densmap不能与所有参数组合同时使用。比如某些metric设置、某些版本的n_neighbors太小或者样本量过少的情况下,密度估计会变得不稳定。具体可以看库的文档和报错信息,我后面会单独说几个常见的坑。
dens_row是每个点在原始高维空间中的局部密度估计,dens_col是嵌入后在低维空间中的局部密度估计。拿到这两个值之后,可以算它们的相关系数。这个系数是衡量 UMAP-Dense 是否真正保住了密度的最直接指标:越接近 1,说明低维图中的密度顺序越接近原始数据。
3.3 什么时候该用 UMAP-Dense
用不用 UMAP-Dense,不是看“想不想保留密度”,而是看数据里有没有值得保留的密度差异。
第一种典型场景是稀有群体的识别。比如混合样本里有一类样本只占 1%,但它们内部表达模式稳定,具备独立的生物学意义。普通 UMAP 会因为近邻图构建的天然偏向,把这些样本揉进大群体的边缘。UMAP-Dense 能在损失函数层面强制低维空间的局部密度与高维空间对齐,给这类稀有群体更多“空间”。
第二种典型场景是多批次数据的可比性分析。不同批次的数据,采集条件不同,常常会引入密度上的系统性偏差。普通 UMAP 做出来的图,可能某个批次因为整体特征更集中,在图上占据的面积特别小,看起来像被压缩了。UMAP-Dense 至少能保留“这个批次在原始空间里更稀疏还是更密集”的宏观信息,降低批次效应对布局的干扰。
第三种场景是异常检测前的探索性分析。如果数据里天然存在少数孤立点,普通 UMAP 很可能把它们随机分配到某个聚类的边缘,导致你根本看不出来。UMAP-Dense 的密度项会让孤立点保持在稀疏区域,可视化时更容易通过密度着色发现问题。
反过来说,如果数据本身密度比较均匀,或者你只关心“分成几簇、每簇是否分得开”,那 UMAP-Dense 带来的收益不大,反而会因为多了一个约束项,需要额外调参和忍受更长的运行时间。
4. 实操案例:同一份数据跑 UMAP 与 UMAP-Dense
4.1 实验数据准备
为了把两类算法放一起对比,我构造了一个带有密度差异的模拟数据集:三群点,一群密度很高,两群密度较低,然后在部分区域加一点噪声点。这个场景和很多真实业务数据的感觉类似——大多数样本聚集在少数几个典型模式里,剩下的样本分布在边缘。
数据生成大致是:
import numpy as np from sklearn.datasets import make_blobs # 高密度群体 X1, _ = make_blobs(n_samples=3000, centers=[(0, 0)], cluster_std=0.5, random_state=1) # 低密度群体 X2, _ = make_blobs(n_samples=300, centers=[(5, 5)], cluster_std=1.2, random_state=1) X3, _ = make_blobs(n_samples=300, centers=[(-4, 4)], cluster_std=1.5, random_state=1) X = np.vstack([X1, X2, X3])真实项目中很少直接对二维数据降维,所以我把它映射到更高维空间,再通过 PCA 稍微加上线性变换,制造一些相关性,避免低维可视化直接“就是原始图”的错觉。实际操作时也会故意混入一些不是明显聚类的背景点,模拟噪声。
4.2 大规模调参思路
我不建议一开始就用默认参数直接跑。先跑一个快速版本,观察是否出现以下几类现象:点全部挤成一团、某个群体被拉伸成细线、不同群体边界处颜色交错明显。根据现象反推需要调整的参数。
我常用的调参顺序是:
- 先定
metric。特征连续且标准化较好,用euclidean;稀疏高维特征,用cosine。 - 再调
n_neighbors。小数据集里从 5 到 50 都试一下,画几张图出来对比。n_neighbors=15是我比较常用的起点。 - 然后调
min_dist。想看清晰的聚类,从 0.1 往下降;想看连续流形结构,从 0.1 往上升。 - 最后才调整
densmap相关的参数。开启密度保持后,需要重新观察嵌入的整体形态是否发生太大改变,如果改变太大,说明密度损失权重对当前数据过重。
这个顺序的核心逻辑是:先保证“邻接关系”被正确学习,再考虑“密度信息”的保持。顺序反过来的话,很容易把噪声当成结构。
4.3 结果对照与密度相关性评估
跑完两份嵌入后,我一般会同时看三样东西:整体散点图、局部密度相关性、群体分离度。
散点图上最直观的变化是,普通 UMAP 中低密度群体和高密度群体之间的距离被压缩了,高密度群体占据的面积很大;UMAP-Dense 的结果里,低密度群体的分布范围更宽,边界更清晰,群体之间的空白也更明显。
用np.corrcoef(dens_row, dens_col)计算密度相关,普通 UMAP 的相关系数通常在 0.5 到 0.7 之间,UMAP-Dense 能到 0.85 以上。这个数字是评估密度保持最直接的量化指标,比单纯看图片“顺不顺眼”靠谱得多。
还有一个细节值得说:如果只做“看起来漂亮”的降维,普通 UMAP 也能达到目的。但一旦你后续要根据图的距离做定量分析,比如算群体之间的最近邻距离、统计边界密度,普通 UMAP 已经无法保证这些数字反映真实高维结构。UMAP-Dense 在这类定量场景里优势明显。
5. 常见问题与排查技巧实录
5.1 UMAP-Dense 报错与参数冲突
densmap=True和某些参数组合会直接报兼容性错误。我个人踩过的坑主要有这几个:
densmap=True不支持部分metric。解决方法是保持固定metric,优先使用euclidean或cosine。output_dens=True只对开启密度保持模式有效,不开densmap时不要指望拿到密度估计。- 样本量太小,比如只有几百个点,
n_neighbors=15时密度估计很不稳定,跑出来的结果怪怪的。此时可以把n_neighbors降到 5 到 10。
如果报错信息里明确说某个参数冲突,我的建议是不要强行绕过,换一组参数往往更省时间。密度保持本身就是额外约束,参数空间天然比普通 UMAP 小,这也是“用功能换约束”的代价。
5.2 嵌入结果的稳定性问题
UMAP 的另一个常见问题是随机性。即便random_state固定,使用不同的初始化也会导致最终嵌图有或大或小的差异。如果你只是做一次探索性可视化,这没什么问题;但如果要把 UMAP 结果作为后续分析的特征输入,就需要谨慎。
我通常会跑 5 次,计算嵌入坐标之间的平均距离以及密度相关的波动范围。如果多次运行结果差异很大,大概率是数据本身的流形结构不够清晰,或者n_neighbors太小导致图结构对初始化过于敏感。这种情况下,增加n_neighbors、使用谱初始化,或者先做 PCA 降维去噪,都会让结果更稳定。
有一点经验:不要在一开始就对原始几千维特征跑 UMAP。先做 PCA 取前几十个主成分,效果会好很多。这不仅能加速近邻搜索,还能减少噪声对图构建的干扰。UMAP-Dense 对这种“先线性去噪再非线性降维”的流程同样适用。
5.3 密度相关系数不够高
密度相关系数偏低,通常不是因为算法能力问题,而是数据本身就不适合这种密度保持方式。
常见原因有两个:一是特征尺度没有统一,某些维度的方差远大于其他维度,导致距离计算几乎只被少数特征主导;二是噪声点太多,干扰了局部密度估计。解决办法是先把特征标准化,过滤掉明显离群点,再跑 UMAP-Dense。
还有一个容易被忽略的点:局部密度估计对n_neighbors极其敏感。n_neighbors=5时,每个点的局部密度只取决于最近 5 个点,估计方差大;n_neighbors=50时,密度估计更平滑,但会牺牲局部精度。我一般会在n_neighbors=15的基础上做对比,如果密度相关始终不高,就尝试把n_neighbors提到 30 或 50,看趋势如何。
5.4 运行速度优化技巧
UMAP-Dense 比普通 UMAP 慢,这是必然的,因为密度损失项的计算需要额外维护每个点的局部密度信息。样本量到十万级别时,跑一次 UMAP-Dense 可能要等很长时间。
优化手段有几个:先把维度压到 30 维左右再进 UMAP;减少n_neighbors;用precomputed距离矩阵避免重复计算;或者用更小规模的随机子样先跑一遍参数验证,再全量跑。千万别一上来就全量数据加全参数跑,光是等结果的时间就够浪费的。
6. 我对 UMAP-Dense 的实操体会
笔记写到这里,我想补充一点偏个人经验的内容。
过去很长一段时间里,我都是 UMAP 的忠实用户,因为它的可视化效果实在太容易给人“洞见”的感觉,点簇分明、颜色清爽。但后来在做聚类评估时,我意识到一个问题:很多人把 UMAP 的可视化结果直接当成了“真实的高维几何”来解读,这其实是危险的。UMAP 的输出只是低维投影,投影必然有扭曲,扭曲方式由参数和损失函数决定。普通 UMAP 的扭曲主要集中在密度不均匀处,这恰恰是数据分析中最不该被扭曲的地方。
UMAP-Dense 的好处不是“更漂亮”,而是更诚实。它让嵌入空间里的密度分布与原始空间里的密度分布保持可验证的相关性,给了我一个量化指标去判断降维结果的可信度。在实际项目中,我会同时跑普通 UMAP 和 UMAP-Dense,把两张图放在一起对比。如果两者的整体模式差异不大,说明数据本身结构稳健;如果差异巨大,那就说明密度信息起了决定性作用,后续分析需要格外小心。
最后再分享一个小技巧:做完了 UMAP-Dense,可以用dens_row和dens_col的差距来识别“视觉上聚集但实际上稀疏”的点。这些点在图上可能被周围的密集点拽过去了,密度相关性却不匹配。把这些点单独标出来看,经常能发现有意思的异常样本或者边缘群体。这个方法是我偶然发现的,但对混合群体数据特别有用,建议你们试一下。