news 2026/9/9 6:38:45

单细胞数据降维可视化:t-SNE、UMAP与自编码器全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单细胞数据降维可视化:t-SNE、UMAP与自编码器全解析

先说我自己的判断:做单细胞转录组数据分析,真正决定你图好不好看的,不是你用的是 t-SNE 还是 UMAP,而是数据预处理和参数调得对不对。但怎么调,又不完全能脱离方法本身说清楚。所以这篇把单细胞数据降维与可视化里最常用的三种思路——t-SNE、UMAP 和自编码器——放在一起,从原理、代码到坑,系统对照一遍。如果你手里正有一批单细胞表达矩阵,想画出一张能说服自己的分群图,也想知道自编码器这种“神经网络降维”到底有没有必要,这篇文章会比较对胃口。

我们处理的很多问题其实都发生在“高维表达矩阵”到“二维散点图”的压缩过程中。t-SNE 是过去十来年最主流的细胞群可视化工具,UMAP 近几年强势上场,自编码器则代表另一条更“重”但更灵活的路线。它们的目的从表面上看似相同,内部逻辑差别很大。先把这个底层差异讲透,再上真实代码,后面你会更容易判断“为什么同样的数据,三种方法画出来不一样”。

1. 先说清楚:单细胞数据为什么非降维不可

1.1 表达矩阵的真实面貌

单细胞转录组测序拿到手之后,最常见的形式是一个稀疏矩阵:行是细胞,列是基因。人类基因组注释到的蛋白编码基因差不多两万个,你测一个 10x 样本,少则几千个细胞,多则几万甚至几十万。每个细胞里真正能够稳定检测到的基因一般也就几千个,所以这个矩阵里大量位置是 0。即使做了标准化和 log 变换,数据的有效维度依然很高,直接在所有基因上算距离或跑聚类,不仅慢,还容易被无关基因和噪音冲淡信号。

很多刚接触单细胞数据的朋友会有一个问题:表达矩阵都已经能用 PCA 了,为什么还要 t-SNE、UMAP 甚至自编码器?答案是 PCA 是线性降维,它假设数据的主要变化方向能用“基因的线性组合”概括。单细胞数据里存在大量 dropout 现象,一个基因在某个细胞里没测到不代表它真没表达;生物学上还普遍存在不同细胞状态连续变化、发育轨迹非线性延伸的情况。单靠 PCA,经常会把本来连续的细胞群切得很别扭,或者把少数类群压缩进一片看不出结构的云里。

1.2 降维的目的不只是画图好看

我们在 scRNA-seq 分析中反复降维,核心目的有四个:一是去掉技术噪音,让有效生物学信号凸显出来;二是把数据压缩到能承载“近邻关系”的维度,再跑 KNN 图聚类;三是做二维/三维投影,方便肉眼观察是否存在若干候选细胞群;四是给轨迹推断、差异表达等下游任务提供一个更稳定的数据入口。

这里必须强调,聚类和可视化是两个动作。现在的标准流程通常是:先做 PCA,用前十几或几十个主成分构建 KNN 图,然后在图上做 Leiden/RNA 聚类,最后把聚类结果放在 t-SNE 或 UMAP 的二维坐标上展示。所以 t-SNE 和 UMAP 更多是“展示层”,它们让细胞按聚类关系在低维空间里形成一团一团的视觉结构;而自编码器则可以承担“特征提取层”,它学习的是一个把高维基因表达压缩到低维瓶颈向量的非线性映射,压缩得到的特征还可以继续走 KNN 图聚类或 UMAP 可视化。

1.3 线性、非线性、神经网络降维的选择

PCA 属于线性方法,t-SNE 和 UMAP 属于非线性流形学习方法,自编码器则是用神经网络逼近非线性映射,本质上也是一种非线性降维。打个不一定严谨但好懂的比方:PCA 像从高处俯视一座山,把三维景观直接压成一张二维平面图,方向是固定的;t-SNE/UMAP 更像在山上架了许多悬浮索道,让相邻的点更愿意靠近自己附近的点;自编码器则像一个勤快的测绘员,先学会把整座山浓缩成一张草图,再根据草图把山还原出来。

理解这个比喻之后,接下来我们看每种方法的具体思路。

2. 三种主力降维方法各自的“脑回路”

2.1 t-SNE:维护邻居之间的概率关系

t-SNE 的全称是 t-distributed Stochastic Neighbor Embedding。它的基本想法是:在高维空间中,把“细胞 A 和细胞 B 是不是邻居”转化成一个概率值;然后在低维空间中,也想办法给每个细胞一套二维坐标,让低维空间里的邻居概率尽量还原高维空间里的邻居概率。

它用对称化的条件概率定义高维相似度,计算时会根据 perplexity 参数给每个细胞自动分配一个方差。低维空间则使用 t 分布而不是正态分布,t 分布有更厚的尾巴,允许低维空间中相对较远的点仍然有可忍受的概率值,这样能缓解所谓的“拥挤问题”——也就是高维空间里大量彼此距离差不多的点,投影到二维后会被挤成一堆。

t-SNE 的代价函数采用了 KL 散度。这个方法有个很著名的特点:它对“本来应该靠近却没靠近”的点惩罚很大,对“本来不近却被分到了一起”的点惩罚相对较小。换句话说,t-SNE 非常愿意保留局部近邻结构,但全局结构不一定可靠。所以你在 t-SNE 图上看到的“聚成一团”的细胞群,往往比较紧,但不同群之间的距离远近,并不能严谨说明它们之间的相似程度。

实践里还要注意:t-SNE 每次跑都可能得到不同的全局布局;直接在高维稀疏表达矩阵上跑 t-SNE 很慢而且不稳定。我们一般会先用 PCA 把数据压缩到 15~50 维,再在这个结果上跑 t-SNE。还有一点,t-SNE 的目标是最小化二维布局与原始近邻关系的差异,因此基本上只为可视化服务,很少有人直接拿 t-SNE 坐标做下游定量分析。

2.2 UMAP:在流形上构建模糊图

UMAP 全称是 Uniform Manifold Approximation and Projection,它名字里自带“流形”这个词。流形这个概念听起来玄,但可以理解为:高维数据虽然在两万个基因空间里分布,但真正有意义的点很可能分布在一个较低维度的连续“曲面”附近。UMAP 假设数据服从流形分布,并且大致均匀,只是当前观测的采样密度不均匀。

具体做法上,UMAP 会先在原始高维空间里为每个点找 k 个邻居,用局部距离为每个点确定一个特征尺度,并据此构造一个加权的 k 近邻图。然后在低维空间里,它希望低维图与高维图尽量相似,优化目标是交叉熵。这个设计中,局部邻居关系和全局结构都被纳入了目标函数,因此 UMAP 比 t-SNE 更容易保留一些广域结构,比如不同细胞群之间的相对位置。

实际体验上,UMAP 有一个非常明显的好处:速度快、内存友好。同样几万个细胞,UMAP 比 t-SNE 快很多,这也是它逐渐取代 t-SNE 成为单细胞主图的重要原因。但速度与稳定不是免费的,UMAP 对邻居数、最小距离等参数更敏感,不同的参数组合会把同一份数据切成完全不同的“视觉故事”。后面我会专门讲参数调整。

2.3 自编码器:先压缩,再重建

自编码器是一种神经网络结构,由编码器 Encoder 和解码器 Decoder 两部分组成。输入是高维表达向量,编码器把它压成一个低维向量,也就是瓶颈特征;解码器从瓶颈特征尝试还原原始输入。如果输入是两组数量级不同的特征,比如基因表达量还带着批次、测序深度等变量,那常见做法是先做标准化再进入网络。模型训练的目标是让输出接近输入,理想情况下,瓶颈层就保存了能够重建整个表达谱的关键信息。

单细胞领域会用到的自编码器变体不少。最基础的是全连接自编码器。因为基因表达矩阵非常稀疏,大量基因在绝大多数细胞中不表达,普通全连接网络容易过度关注那些“高表达驱动重建”的基因,忽略与细胞身份密切相关的低表达 marker,因此我们通常会给瓶颈层或隐藏层加稀疏约束,形成稀疏自编码器。稀疏性本质上是让大部分神经元对大多数样本保持抑制,只对一小部分激活,这能避免网络把表达矩阵中的 dropout 噪音当成重要结构记住。

如果你处理的对象不是表达矩阵而是单细胞图像,比如高内涵筛选、空间组学里的局部图像块,那么卷积自编码器会更合适,时间序列或体积数据则可能用到 3D 卷积自编码器。这篇文章我们主要讨论普通 scRNA-seq 表达矩阵,所以后续代码以全连接稀疏自编码器为主。自编码器还有一类带条件输入的变体,像 scVI 那样把样本来源、批次信息作为额外变量输入,可以实现在压缩特征里把批次效应解释掉,这是普通 t-SNE 和 UMAP 做不到的。

2.4 核心差异一张表看完

对照维度t-SNEUMAP自编码器
基本原理高维/低维邻居概率对齐高维/低维模糊图逼近编码后解码重建
是否线性非线性非线性非线性,取决于网络
全局结构保真度较弱,偏局部相对较好取决于瓶颈维度和训练
输出维度通常 2D/3D任意维度瓶颈层任意维度
是否天然适合流形是,但偏向局部流形是,兼顾全局是,但需要调网络
大批量计算速度有 GPU 时快
可解释性中等,可看瓶颈基因权重
典型用途可视化可视化、中间降维特征提取、去批次、建模
随机性较高较低但存在较高,取决于随机种子

这张表的意思很清楚:t-SNE 适合“看局部结构”,UMAP 适合“快速看整体并做展示”,自编码器则更多用于“为下游模型生成低维特征”。它们不完全是替代关系,经常是先后配合的关系。

3. 在同一套 PBMC 数据上的实战流程

3.1 数据预处理与 PCA 基线构建

纸上谈兵没有用,我们用一套公开的 PBMC 单细胞数据来跑一遍。数据量不大,适合演示。实际项目中的分析步骤会更多,但核心流程是一样的。

首先做好预处理。这个阶段不能省,具体顺序通常是:细胞质控过滤、基因过滤、库大小归一化、log 变换、高变基因筛选、标准化、PCA。

import scanpy as sc import numpy as np import torch import torch.nn as nn # 加载示例数据,PBMC 3k adata = sc.datasets.pbmc3k() adata.var_names_make_unique() # 基本质控:每个细胞至少检测到 200 个基因,每个基因至少在 3 个细胞中出现 sc.pp.filter_cells(adata, min_genes=200) sc.pp.filter_genes(adata, min_cells=3) # 线粒体高比例通常提示细胞状态不佳 adata.var['mt'] = adata.var_names.str.startswith('MT-') sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False, inplace=True) adata = adata[adata.obs.pct_counts_mt < 20, :].copy() # 库大小归一化到 10000,再做 log1p 变换 sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata) # 这里把全基因表达备份到 raw,后续给 marker 基因上色用 adata.raw = adata # 挑选高变基因 sc.pp.highly_variable_genes(adata, n_top_genes=2000) adata = adata[:, adata.var.highly_variable].copy() # 标准化:缩放每个基因的表达量 sc.pp.scale(adata, max_value=10) # PCA sc.tl.pca(adata, svd_solver='arpack', n_comps=50) sc.pl.pca_variance_ratio(adata, n_pcs=50)

处理完后可以用pca_variance_ratio画碎石图,观察前多少个主成分贡献了主要方差。PBMC 3k 这种数据,通常前 10~20 个 PC 已经够用。我们这里选前 15 个 PC 作为后续邻居图的输入。

需要提醒一点:adata.raw = adata这句是在高变基因筛选之前做的,这样后面无论从高变基因子集还是从全基因表达中取 marker 基因,Scanpy 都能快速找到。预处理阶段的“标准化”尤其关键,漏掉 scale,PCA 和后续距离计算会被高表达基因主导。

3.2 用 Scanpy 一次跑出 t-SNE 与 UMAP

预处理完成后,先构建邻居图,然后跑聚类。这一步决定你后面的图上颜色怎么划分。

# 邻域图:在 PCA 前 15 维上找邻居 sc.pp.neighbors(adata, n_neighbors=15, n_pcs=15) # Leiden 聚类 sc.tl.leiden(adata, resolution=0.5, key_added='leiden_0.5') # UMAP 可视化 sc.tl.umap(adata, min_dist=0.5, random_state=42) sc.pl.umap(adata, color=['leiden_0.5', 'CD3D', 'MS4A1', 'LYZ'], frameon=False) # t-SNE 可视化 sc.tl.tsne(adata, n_pcs=15, perplexity=30, random_state=42) sc.pl.tsne(adata, color=['leiden_0.5', 'CD3D', 'MS4A1', 'LYZ'], frameon=False)

你自己跑时会发现:UMAP 跑的很快,几分钟内能出结果;t-SNE 稍慢,但 3k 细胞也不会太离谱。UMAP 的图通常各个细胞群之间有清晰的“通路”或“隔断”,t-SNE 的团块更紧凑,局部边界更清晰。

加 marker 基因上色非常关键。CD3D是 T 细胞 marker,MS4A1是 B 细胞 marker,LYZ在单核细胞里比较突出。如果只看聚类色块,你只能说“我分出了几个群”,但只有 marker 表达图上色后,你才能说“这个群大概率是 T 细胞,那个群是 B 细胞”。这一步很多人会跳过,然后注释细胞类型时就容易翻车。

3.3 用 PyTorch 实现一个稀疏自编码器

自编码器部分我们用 PyTorch 从零写一个足够用的稀疏自编码器。输入是高变基因标准化后的矩阵,瓶颈维度设 16,相当于自编码器学习到的压缩表示。

X = adata.raw[:, adata.var_names].X # 上面的写法并不严谨,我们直接使用当前 adata.X 即可 X = adata.X.astype(np.float32) if hasattr(X, 'toarray'): X = X.toarray() class SparseAutoencoder(nn.Module): def __init__(self, n_input, n_hidden=256, n_latent=16, sparsity_lambda=1e-3): super().__init__() self.sparsity_lambda = sparsity_lambda self.encoder = nn.Sequential( nn.Linear(n_input, n_hidden), nn.BatchNorm1d(n_hidden), nn.ReLU(), nn.Linear(n_hidden, n_latent) ) self.decoder = nn.Sequential( nn.Linear(n_latent, n_hidden), nn.BatchNorm1d(n_hidden), nn.ReLU(), nn.Linear(n_hidden, n_input) ) def forward(self, x): z = self.encoder(x) return self.decoder(z), z model = SparseAutoencoder(n_input=X.shape[1], n_hidden=256, n_latent=16) X_t = torch.tensor(X, dtype=torch.float32) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() def train_model(n_epochs=30, batch_size=256, seed=42): torch.manual_seed(seed) n = X_t.shape[0] for epoch in range(n_epochs): model.train() perm = torch.randperm(n) total_loss = 0.0 for i in range(0, n, batch_size): idx = perm[i:i + batch_size] batch = X_t[idx] x_hat, z = model(batch) rec_loss = loss_fn(x_hat, batch) l2 = sum(p.pow(2).sum() for p in model.parameters()) sparse_loss = model.sparsity_lambda * z.abs().mean() loss = rec_loss + 1e-5 * l2 + sparse_loss optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * len(idx) if (epoch + 1) % 10 == 0: print(f"epoch {epoch + 1}/{n_epochs}, avg_loss={total_loss / n:.4f}") train_model(n_epochs=30, batch_size=256) model.eval() with torch.no_grad(): codes = model.encoder(X_t).numpy()

代码里我加了两项正则:一个是常见的 L2 权值衰减,另一个是 L1 稀疏惩罚。L1 惩罚会让编码器输出层的激活值更稀疏,不至于让每个细胞都用全部 16 维特征编码。单细胞矩阵里大量零值和 dropout,如果完全不做稀疏约束,自编码器很容易学会“把所有细胞都压成同一个巨大模态”,然后再靠解码器硬撑还原。

接下来把自编码器得到的瓶颈特征作为 UMAP 的输入。为了不破坏原始数据的 neighbor 图和 UMAP 结果,建议复制一份 adata 再操作。

adata_ae = adata.copy() adata_ae.obsm['X_ae'] = codes sc.pp.neighbors(adata_ae, n_neighbors=15, use_rep='X_ae') sc.tl.umap(adata_ae, min_dist=0.5, random_state=42) sc.pl.umap(adata_ae, color=['leiden_0.5', 'CD3D', 'MS4A1', 'LYZ'], frameon=False)

这里需要注意,adata_ae.copy()保留了之前的 Leiden 聚类标签,所以图上颜色可以直接沿用。对比 PCA 输入得到的 UMAP 和 AE 瓶颈特征得到的 UMAP,你会发现两者大概率能分出相似的细胞群,但细胞的边界、紧凑程度以及一些稀有细胞群的“地位”可能不同。如果训练不稳定或 epoch 太少,AE 版本可能会表现得非常混乱,这也是它比 t-SNE/UMAP 更难用的原因之一。

3.4 三种方法结果怎么对比才有意义

拿到三张图之后,先别急着下结论。最直接的做法是把同一套已知 marker 基因分别放在三张图上,看 marker 的表达模式是否一致。比如 T 细胞 marker 是否都集中在同一个区域。如果 t-SNE 上 B 细胞和单核细胞靠得很近而 UMAP 上它们隔得较远,不要立刻说 UMAP 更准,应该回到 PCA 结果和 marker 表达上去查。

我实际跑 pbmc3k 的经验是:PCA + UMAP 给出的整体格局最“干净”,t-SNE 的群内更紧凑但群之间的空间感觉会被放大,AE + UMAP 在瓶颈维度设置合理时能给出类似结构,但没有深度学习调参经验的话,它不会比 PCA + UMAP 自动更好。所以如果你是第一次做单细胞分析,我建议先把 PCA + UMAP + Leiden 跑顺,自编码器当作进阶工具使用。

4. 参数调优:把图调到能发文章的关键细节

4.1 t-SNE 的困惑度到底在调什么

t-SNE 有三个参数特别值得关注:perplexity、learning rate 和迭代次数。

perplexity 可以粗略理解为“每个点认为多少个其他点是它的邻居”。比较推荐的范围是 5~50,并不是越大越好。perplexity 太小,图中容易出现散碎的小点团;perplexity 太大,强制的邻居范围变大,可能把本来独立的细胞群强行拉在一起。数据量越大,通常可以稍微调大 perplexity,但也要结合细胞类型复杂程度。

在 Scanpy 的sc.tl.tsne里,一般通过perplexity参数直接控制,数据规模大时还可能用到early_exaggerationlearning_rate。如果你用的是 sklearn 底层接口,early_exaggeration 控制初始阶段簇与簇之间的推开力度,调试时如果发现所有细胞都挤成一团,可以适当增大这个值。真实单细胞数据中,t-SNE 坐标点分布非常依赖随机初始化,所以实验报告里建议固定random_state,否则换一个种子可能得到左右翻转甚至局部结构变化的图。

4.2 UMAP 的 n_neighbors 和 min_dist

UMAP 最影响观感的是两个上游参数:n_neighborsmin_dist。第一个在sc.pp.neighbors里出现,第二个在sc.tl.umap里出现。

n_neighbors控制 UMAP 构建高维图时考虑多少个邻近点。数值小,比如 5,会更关注局部结构,适合寻找容易漏掉的稀有群,但代价是整体容易碎成很多小岛;数值大,比如 50,更能反映连续分化或全局关系,但可能把小群彻底淹没。单细胞数据一般从 10~30 起步,我用 PBMC 3k 时常用 15;遇到细胞数很多的数据,可以加大到 20~50。

min_dist则控制低维空间中点与点之间允许的最小间距。min_dist越小,聚类团之间的边界越清楚,细胞点会像星云一样彼此推开,但可能导致一个真实连续的分化过程被拆成好几段;min_dist越大,整体越松,细胞群更容易连成片,适合展示连续轨迹。很多人习惯把min_dist调到 0.1 以下让细胞群边界更干净,但如果你研究的对象是发育分化轨迹,可能需要把 min_dist 调到 0.3 以上,避免把 stage 之间的过渡细胞硬生生劈开。

4.3 自编码器:瓶颈维度、归一化与训练轮数

自编码器参数弹性很大。瓶颈维度决定了最终压缩特征的容量:太小可能丢失关键信息,太大又可能只是把 PCA 结果换了个非线性表达。表达矩阵输入为高变基因时,我会优先试 8、16、32 三个维度。比如 2000 个高变基因压到 16 维,相当于每个维度要承载很多基因的信息,模型压力较大;如果下游只做可视化,16 维通常够用,如果还要做分类或轨迹推断,可能 32 维更稳。

训练前要对输入做标准化,这一点很容易被忽略。自编码器用 MSE 作为重建损失,如果不同基因的表达量水平差好几档,网络会把大部分容量用于还原高表达基因,细胞类型特异的低表达信息会被当作噪声忽略。代码里我们沿用sc.pp.scale之后的矩阵,就是对每个基因做了均值 0、方差 1 的标准化,这样重建损失对不同基因是公平的。

训练轮数也不能盲目求多。单层瓶颈网络在几千个细胞的小数据上很容易过拟合。一个比较土但有效的办法是:固定随机种子,分别用 20、40、80 个 epoch 训练,比较瓶颈特征跑出来的 UMAP 是否稳定。如果 40 和 80 轮结果差异很大,说明模型对初始化太敏感,最好加大数据量或增强正则化。

4.4 如何客观衡量降维效果,而不是只靠眼睛

眼睛看久了会自我说服,所以最好搭配一些量化指标。常用的是 trustworthiness、连续性和剪影系数。trustworthiness 衡量的是原空间中的近邻在低维空间中是否仍然是近邻,值越接近 1 越好;连续性则反过来,看低维空间中的近邻在原空间中是否也近。这两个指标比单纯看散点图靠感觉要客观得多。

不过要注意,这些指标只衡量“几何关系的保持程度”,不代表生物学上更准。单细胞分析里最硬的标准还是 marker 基因验证。你可以把候选 marker 的表达值映射到二维图上,看是否形成清晰的“颜色梯度”或“局部亮斑”;也可以计算每个 Leiden 群相对于其他群的差异表达,取 top 基因去注释。一个降维布局如果连已知 marker 都无法对齐,无论几何指标多高都说明特征提取环节可能出了问题。

5. 高频问题与避坑现场记录

5.1 每次运行结果不一致,是不是代码写错了

不是。t-SNE、UMAP、自编码器都依赖随机初始化或随机采样,结果存在一定随机性。t-SNE 对随机种子尤其敏感,换一个随机种子可能出现全局布局旋转、翻转,甚至局部群组关系变化。UMAP 的稳定性相对好一些,但也不是完全确定。自编码器因为涉及神经网络权重初始化、batch 采样顺序,天然有随机性。

建议从一开始就固定随机种子。在 Scanpy 里设置sc.settings.seed = 42,在 PyTorch 里调用torch.manual_seed(42),有 GPU 时还要设置torch.cuda.manual_seed_all(42)。如果固定种子后多次运行结果仍然有明显差别,那说明数据本身的信号可能不强,或者参数位于敏感区域,这时候要回头检查预处理和离群细胞,而不是反复换随机种子硬找“好看”的图。

5.2 UMAP 图全是“连体婴儿”,群边界模糊怎么办

这种问题最常见的原因不是 UMAP 而是输入。如果高维距离空间里本来没有清晰的边界,UMAP 不可能强行造出边界。你先检查 Leiden 聚类结果里是否存在一个巨大的群,比如 90% 的细胞都被分到一起。如果是,大概率是细胞质控没过关,或者 PCA 维数选得过高,把技术噪音也当成信号。

排除了输入问题后,可以试着调小min_dist,比如从 0.5 调到 0.1 或 0.05,细胞群之间会更“炸开”。也可以稍微减小n_neighbors,让算法更关注局部连接。如果 t-SNE 图上能看到边界而 UMAP 看不到,大概率是 UMAP 的全局结构建模把边界抹掉了,并不是 UMAP 比 t-SNE 差,只是两者侧重不同。

反过来,如果 UMAP 图碎成太多小岛,而且每个小岛只有寥寥几个细胞,一般不是稀有细胞群,而是过拟合噪音。这时可以增大n_neighbors,或先降 PCA 到更少的维度。

5.3 预处理的坑:不要跳过 scale,也不要忽略数据版本

很多刚上手的同学会直接拿原始 count 矩去跑 UMAP,结果图里所有细胞被几个超高水平基因主导,分群完全不是生物学表现。还有同学会在高变基因筛选前做 scale,导致后续高变基因选择受缩放影响,这也是不太合理的顺序。标准顺序是先归一化、log、高变基因筛选,再 scale,然后 PCA。

如果用的是 10x 的 Cell Ranger 流程,表达矩阵已经过初步处理,但不同版本之间基因命名和重复基因存在差异。跑分析前记得用adata.var_names_make_unique()处理重复基因名,否则后面 marker 上色会找错基因。此外,adata.raw不能乱覆盖,它一旦被删掉,很多扫描raw的函数会报错,画 marker gene 散点图会非常痛苦。

5.4 什么时候不该用自编码器

相比 PCA 加 UMAP,自编码器并不是银弹。如果你的目标就是出一张细胞分群主图,PCA + UMAP 基本足够,没有必要引入神经网络。自编码器的优势在于能学习非线性特征、把批次变量或测序深度作为条件输入、并生成用于下游预测或聚类的高维抽象特征。

如果你的样本量只有几百个细胞,自编码器很容易过拟合;如果你没有 GPU 或者不熟悉反向传播训练,排查训练异常会非常耗时;如果你很在意结果可复现并可能被审稿人要求解释“为什么用这个架构”,那自编码器的门槛会明显提升。普通单细胞分析报告里,自编码器不是必选项,PCA 仍然是默认基线。

如果决定用自编码器,记得把稀疏约束加上。没有稀疏或噪声正则的自编码器可能会学习到“把每个细胞都近似编码成均质向量”的捷径。那种简化版在二维 UMAP 上往往表现为一个巨大的细胞团,完全看不出离散群,这种结果我也遇到过几次,基本都是输入没标准化、正则太弱或训练轮数太少这三类原因。

5.5 下游聚类和细胞注释,别被降维图带偏

降维图上的“视觉距离”不等于“细胞相似度”。在单细胞分析流程里,更稳的做法是先用 PCA 或自编码器瓶颈特征构建 KNN 图,然后用 Leiden 聚类。聚类的分辨率决定你看到的群数量,resolution 越大,分群越多。分群结束后,再把群标签映射到 t-SNE 或 UMAP 图上做展示。

很多新手容易反过来:觉得 UMAP 图上某两坨分得开,就直接把颜色标签手动指定成两个细胞类型。这样非常危险,因为 UMAP 的布局会受参数影响,同一批数据不同参数下可能把两个群画得更开或更近。科学上的判断依据应该是 marker 基因表达、差异表达统计和已知细胞类型注释,而不是纯几何位置关系。

我在实际项目中会先跑 PCA + Leiden,再用rank_genes_groups找每个群的标记基因。只有标记基因表达模式与文献已知细胞类型一致时,我才会把“群号”翻译成“细胞类型”。可视化只是让这个判断过程更直观,不能反过来主导判断。

6. 选型快查表与我的使用习惯

6.1 快速决策表

场景推荐组合理由
快速看整体细胞群结构PCA + UMAP速度快、局部与全局平衡
投稿时需要更紧凑的细胞群展示PCA + t-SNE细胞群视觉更紧,侧面辅助审稿
大样本(十万级以上)PCA + UMAPt-SNE 内存和耗时都偏高
探索稀有细胞群PCA + UMAP,调低 n_neighbors增强局部敏感度
发育轨迹/连续状态展示PCA + UMAP,调高 min_dist避免过度离散化
非线性低维特征用于免疫分型或预测自编码器或 scVI能获得抽象特征向量
多批次数据整合后可视化条件自编码器类工具,或校正后 PCA+UMAP显式建模批次来源
严格可解释、低门槛分析PCA + UMAP默认基线,容易排查

自编码器在单细胞领域更适合作为“特征提取前置”,而不是直接替代 PCA 画图。条件自编码器像 scVI 通常还会输出一个校正后的表达矩阵或潜在表示,再连接 UMAP/Leiden 使用,这种流程在批次效应明显的多样本项目中更可靠。

6.2 我个人在实际项目中的使用习惯

跑过大量单细胞数据之后,我的固定流程大致是这样:先按标准流程做质控和预处理,PCA 保留前 15~30 个主成分;用 PCA 构建邻居图,跑 Leiden;先出 UMAP 看整体格局,再跑一个 t-SNE 用来交叉验证那些只在 UMAP 上看起来离群的小簇。如果时间允许,我会把 AE 得到的瓶颈特征也投影一次,看它能否提供额外可解释信号,但不会用它作为唯一结论。

一个值得分享的小技巧是:不要只看聚类标签着色的图,一定要把若干关键 marker 同时放在 UMAP 上检查。比如免疫细胞,T、B、NK、单核这几类的 marker 表达模式如果与聚类群对不上,多半是聚类参数或数据质控出了问题。把颜色阈值、图例范围和布局列统一后,再去比较 t-SNE 和 UMAP,你会少走很多弯路。还有一点,降维图和聚类结果要保持 seed 固定,否则后期复现结果时会浪费大量时间。

最后说句实在话,方法学工具没有绝对的高低之分。t-SNE 适合精细展示,UMAP 适合整体快速把握,自编码器适合在理解神经网络原理的前提下做更有弹性的特征学习。先用公认流程把基础结果做扎实,再根据具体问题决定要不要引入深度学习,这条路对绝大多数人来说更稳也更省力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 6:38:23

轻量级规则引擎ruflo:从if-else到配置化流程编排的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 6:38:09

鼠标防拍击误触与快速触发兼得:硬件、固件到系统三层调校

鼠标左键出现“单击变双击”、快速连点时被系统吞键、拍击按键瞬间触发两次&#xff0c;这些问题几乎每个用电脑的人都遇到过。更麻烦的是&#xff0c;当你为了防拍击误触把消抖调高&#xff0c;快速连点又变“肉”了&#xff1b;调低消抖&#xff0c;误触又回来了。防拍击误触…

作者头像 李华
网站建设 2026/9/9 6:36:53

从314页论文看coding agent运行机制:Claude Code翻车排查与配置调优指南

说实话&#xff0c;我一开始看到这个标题里的数字时&#xff0c;第一反应是“谁会把一篇三百多页的论文当床头读物”。但真的&#xff0c;如果你这段时间正在被 Claude Code 折磨&#xff0c;或者你身边有人天天吐槽 coding agent 改坏代码、烧光 token、反复在一个 bug 里打转…

作者头像 李华
网站建设 2026/9/9 6:36:31

Kiro 进阶实战:Skill 封装、CLI 自动化与权限配置指南

直接上干货。上一篇文章写了 Kiro 的安装、基础配置和日常对话技巧&#xff0c;这篇我们往前再走一步。如果你已经用 Kiro 做了一些事&#xff0c;但总觉得它还能更顺手——比如希望它按你的项目习惯自动执行命令、把重复发生的任务封装成一句指令、或者干脆把 Kiro 的模型能力…

作者头像 李华
网站建设 2026/9/9 6:34:46

基于PSO的光伏MPPT仿真:从粒子群算法原理到MATLAB/Simulink完整实现

1. 从实际工程问题说起&#xff1a;为什么要用PSO做光伏MPPT搞光伏发电系统仿真的人&#xff0c;对这组矛盾一定不陌生&#xff1a;光伏电池的输出特性曲线是非线性的&#xff0c;而且会随着光照强度和温度实时变化。在任意一组环境条件下&#xff0c;P-V曲线上都存在唯一的最大…

作者头像 李华
网站建设 2026/9/9 6:33:52

抖音网红时钟资源包解析:从避坑指南到5分钟复刻教程

简介&#xff1a;一份围绕抖音网红时钟实现的RAR压缩资源&#xff0c;面向喜欢个性化桌面时钟、或希望借鉴前端动效思路的开发者。压缩包内是可直接运行的网页源码&#xff0c;共7个文件&#xff0c;以JavaScript、CSS与HTML为主&#xff0c;体积仅36KB&#xff0c;整体结构十分…

作者头像 李华