
做单细胞这几年我越来越觉得“降维聚类加注释”这套老三样只能回答“有哪些细胞”回答不了“这些细胞从哪来、往哪去”。你手里那张 UMAP本质是一张静态快照它把发育、分化、激活、耗竭这些连续过程压成了一个一个离散的簇。真正做项目的时候甲方或者审稿人追问的往往是那半句没说完的话这群中间态细胞后面会变成谁处理组和对照组的分化路径是不是走偏了空间切片上那些细胞处在发育的哪个阶段这些问题就得靠时间动力学来回答而在 10X 单细胞以及 10X 空间转录组的分析流程里scTour是我近几年用得比较顺手、也相对稳的一个工具。这篇东西写给两类人一类是已经跑过 Seurat 或 Scanpy 基本流程、想做轨迹推断但被 Monocle3 和 RNA 速率折腾得有点烦的同学另一类是手里有 10X Visium 空间数据想让切片上的每一个 spot 都带上“发育时间”这个属性的同学。我会把 scTour 的架构逻辑、参数取舍、实操步骤、结果校验、以及跟空间转录组拼接的完整链路讲一遍中间夹一些我自己踩过的坑包括伪时间整体逆序、分支树塌成一条线、以及把参考图谱映射到空转切片时结果飘掉这些典型问题。1. 时间动力学解决的到底是什么问题1.1 静态快照里的动态信息藏在哪先说清楚概念不然后面全是玄学。单细胞测序是把一群细胞打碎了看每一个细胞的表达谱都代表它在某个瞬间的状态。如果这批细胞本身是一个连续过程比如造血分化、T 细胞活化、肿瘤上皮间质转化那么理论上它们在一个高维表达空间里应该排成一条或者几条曲线而不是几个完全分离的团。时间动力学要做的事情就是把这条曲线还原出来给每个细胞分配一个标量——也就是伪时间pseudotime再进一步给出向量场vector field告诉你这个细胞下一步会往哪个方向移动。伪时间和真实时间不是一回事。它是“相对进度”是经过归一化的0 到 1 之间只有顺序和相对快慢有意义绝对数值没有。很多人第一次用会问“这个细胞是第几天”这个问题没有答案你要问的是“这个细胞排在另一个细胞前面还是后面”。向量场则是伪时间的升级版它不只是一个标量而是一个方向和速率能告诉你有几条路径、在哪个位置分叉。这两样东西加起来才是能拿去写文章的“时间动力学”。1.2 拟时间、RNA 速率与最优传输的分工市面上做这件事的思路大致分三代。第一代是图论方法比如 Monocle 系列的 DDRTree、Slingshot 的聚类最小生成树核心是把细胞建成一个图然后找一条穿过所有簇的最短路径。这类方法稳、快、可解释但它的轨迹形状受聚类结果影响很大聚类稍微一变树就变了。第二代是 RNA 速率用未剪接和已剪接的 ratio 去估计每个基因的导数好处是方向有生物学依据坏处是对数据的深度要求很高10X 那种 3 端测序的未剪接信息经常测不到结果噪声大到没法看。第三代就是深度学习路线包括基于最优传输的 Waddington-OT、基于变分自编码器的 scVI 系列、以及把**神经常微分方程neural ODE**搬进来的 scTour。scTour 的定位其实很清楚它不追求用 RNA 速率那种“物理正确”也不像图方法那样依赖聚类它假设细胞在低维隐空间里按照一个连续可微的动力学系统演化用神经网络去拟合这个动力学方程的右端项。这个假设的好处是隐空间是平滑的、连续的输出天然带方向而且模型训练完之后可以对新数据做预测不需要重新拟合。2. scTour 的架构拆解与选型理由2.1 四种轨迹推断路线的横向对比我把常用的几套方案放在一张表里这是我自己选型时参考的版本参数和结论基于常见实践具体到你的数据还得实测。方法核心原理方向来源对新数据预测主要短板Monocle3降维 最小生成树 主图学习图上的根节点支持但需重新投影受聚类和根节点选择影响大Slingshot聚类 最小生成树 曲线拟合指定的起始簇不支持分支数依赖聚类粒度scVeloRNA 速率 稳态假设未剪接/已剪接不支持需要未剪接信息信噪比低PAGA聚类图 连通性需外部指定不支持只给拓扑不给连续时间scTour变分自编码器 神经常微分方程隐空间动力学支持可迁移训练成本高超参敏感这张表最想说明的一点是scTour 是唯一一个把“连续时间”和“可迁移预测”同时做掉的方案。可迁移这一条在空间转录组场景下几乎是决定性的因为 Visium 一个切片也就几千个 spot单独跑轨迹推断基本没有统计效力你必须借一张单细胞的参考图谱。2.2 编码器-解码器加神经常微分方程的三段式scTour 的主体结构可以拆成三块。第一块是编码模块把每个细胞的全基因表达向量压成一个低维隐变量 z这部分是标准的变分自编码器思路配了一个 KL 散度正则项保证隐空间是连续且可采样的。第二块是动力学模块也是整套东西的核心它在隐空间上定义一个常微分方程 dz/dt f(z, t)右端项 f 用一个多层感知机来参数化然后用数值积分器code 里用的是 torchdiffeq 那套把 z 沿着时间积分出来得到一条连续的轨迹。第三块是解码模块把积分出来的隐状态再解回表达空间用重构损失来约束隐空间不能乱跑。这三块是联合训练的。这意味着两件事隐空间不是随便降维降出来的它是被动力学方程和重构误差一起“塑形”出来的伪时间也不是简单地在图上数跳数而是模型对每个细胞求解“这个状态是从哪个初始状态积分过来的、花了多长时间”所以它对噪声的鲁棒性比图方法好不少。我在 PBMC 和几个肿瘤数据集上做过对比同一份数据用 Monocle3 跑出来的树换了随机种子之后分支结构会变scTour 相对稳定一些——当然这个“稳定”也是相对的随机种子该固定还是要固定。2.3 IES 模块最容易被忽略的那一半大部分人用完 scTour 就觉得“跑完伪时间就完事了”其实它还有一个初始表达状态IES, initial expression state模块这个模块在预测场景里才是主角。它的逻辑是既然隐空间是连续的那我能不能训练一个映射把任意一个细胞的表达谱直接映射到它对应的初始状态上如果能那你手里任何一批新细胞——不管来自另一个病人、另一个时间点还是一张空转切片——都可以直接用训练好的模型预测出它的伪时间不用重新训练。我自己的经验是参考数据集的选择比模型参数重要十倍。参考图谱要覆盖你要研究的整个分化过程如果参考里只有早期细胞那预测出来的所有 query 细胞伪时间都会挤在 0 附近反过来如果参考里全程都有预测分布才会拉开。选参考的时候我一般会先做一次粗聚类看看是不是从起始态到终末态都有人缺哪一段就补哪一段的数据进去。2.4 什么时候该选它什么场景别硬上不是所有数据都适合上 scTour。我总结了几条判断标准样本量太小比如少于 800 到 1000 个细胞的数据不要用神经常微分方程的参数量摆在那细胞太少训不出来这时候 Slingshot 或者 Monocle3 更合适细胞类型极度异质、跨了好几个谱系的数据也别硬上你得先确定一个相对单一的分化谱系把无关的细胞亚群挑出来再跑只有两个时间点、而且两点之间差异巨大的数据伪时间意义不大直接做差异分析更实在。反过来这几种情况我非常推荐用 scTour一是发育或者分化过程连续、中间态细胞很多的体系比如造血、神经发育、上皮分化二是做处理组和对照组的路径比较你想知道某个干预是不是让某条分支提前走或者走偏了三是你要把时间轴搬到空间切片上这一条几乎是刚需下面会专门讲。3. 数据准备从原始矩阵到能喂给模型的 AnnData3.1 输入形态的三条硬性约定scTour 的输入是 AnnData这个跟 Scanpy 生态是一致的但有几个坑得提前避开。第一表达矩阵必须是原始 counts 或者近似 counts 的整数值不要喂 log 归一化之后的数据因为模型的解码器里带了文库大小相关的尺度因子你喂归一化数据进去重构损失会失真。第二基因名不能重复10X 输出的矩阵经常有同名基因尤其是那些带 ENSG 后缀缺省的得先去重不然后面取高变基因的时候索引会错位。第三不要提前做降维有些同学习惯先sc.pp.pca再喂模型其实没必要scTour 内部会自己处理。一个比较标准的准备流程大概长这样我这里给的是骨架具体阈值按你自己的组织类型调import scanpy as sc import sctour as sct adata sc.read_10x_mtx(filtered_feature_bc_matrix/, var_namesgene_symbols, cacheTrue) adata.var_names_make_unique() # 基础质控过滤 sc.pp.filter_cells(adata, min_genes200) sc.pp.filter_genes(adata, min_cells3) # 线粒体比例人用 MT-鼠用 mt- adata.var[mt] adata.var_names.str.startswith(MT-) sc.pp.calculate_qc_metrics(adata, qc_vars[mt], percent_topNone, log1pFalse, inplaceTrue) # 归一化与对数化只用于下游可视化和聚类不是喂给 scTour 的 adata.layers[counts] adata.X.copy() sc.pp.normalize_total(adata, target_sum1e4) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes2000) sc.pp.pca(adata, n_comps30) sc.pp.neighbors(adata) sc.tl.umap(adata) sc.tl.leiden(adata, resolution0.6)注意最后一步sc.tl.leiden这一步不是为了喂模型是为了后面做手动注释和结果校验你会需要它。3.2 质控阈值的取舍逻辑质控这件事没有万能阈值但有一个判断原则过滤要能去掉明显死细胞和双细胞但不能把真实的低 RNA 含量细胞群切掉。比如中性粒细胞、某些终末分化的效应细胞本身 RNA 含量就低你要是把n_genes卡到 800这群细胞直接消失而它们往往是分化轨迹的终点切掉了轨迹就断了。所以我一般把min_genes卡在 200 到 300重点看线粒体比例人外周血数据我一般卡 15% 左右实体瘤组织可以放宽到 20%因为肿瘤细胞本身线粒体就多。双细胞检测如果数据量允许可以用 scrublet 或者 scDblFinder 跑一遍但别迷信双细胞比例超过 20% 的时候宁可回头检查上机浓度有没有问题。这里有个细节你在哪一步做质控会直接影响伪时间的绝对尺度。因为伪时间是从模型积分出来的细胞数量变了积分的路径长度也会变不同批次之间做伪时间数值对比的时候要格外小心。我一般会记录下过滤前后的细胞数写进方法学部分。3.3 高变基因与 percent 参数的配合scTour 里有个参数叫percent容易被人忽略但影响很大它控制的是参与隐空间建模的高变基因占全部非零表达基因的比例默认是 0.01也就是 1%。假设你过滤完还剩 20000 个基因那大概会取 200 个基因进模型。这个数量比我一开始预期的少很多我最初以为是显存限制后来才明白这是有意为之。原因在于轨迹的方向主要靠少数几个高变基因也就是那些真正的 driver来驱动如果把几千个基因全塞进去噪声基因会把动力学方程的右端项稀释掉积分出来的轨迹反而糊。我试过把percent从 0.01 调到 0.05隐空间的局部结构变复杂了但伪时间的单调性反而变差了。所以我的建议是先按默认值跑一版把结果和已知的 marker 顺序对一下对不上再往上调一般 0.01 到 0.03 之间就够了。3.4 批次整合到底要不要做这是个见仁见智的问题我的立场是做轨迹推断之前要做批次整合但要做“保守的整合”。理由很简单如果不整合批次效应会被模型当成一种“状态差异”伪时间里会混进批次信息你就会看到第一个批次全是早期、第二个批次全是晚期这种荒唐结果。但如果整合做得太激进比如把不同细胞类型强行对齐那真实的生物学差异也会被抹掉轨迹就没了。操作上我一般先用 Harmony 或者 scVI 跑一遍跑完之后不要直接信整合后的表达矩阵而是看整合后的 UMAP 上同一细胞类型的不同批次是不是混在一起了、同一批次的不同细胞类型是不是还分得开。这两个条件都满足才算合格。另外一个细节是如果批次之间本身就有真实的时间差比如一个批次是早期取材、一个是晚期取材那整合的时候要小心别把这个真实差异消掉这种情况下我倾向于不整合或者加批次作为协变量而不是硬整合。4. 训练 scTour超参、损失与收敛判断4.1 核心超参逐个说清scTour 的构造函数参数比较多我挑几个真正影响结果的讲。下面的参数名以我常用的版本为准你装完之后建议先看一眼 docstring不同版本会有微调。model sct.scTour( adata, percent0.01, # 参与隐空间建模的高变基因比例 n500, # 向量场网格点数影响 plot 的平滑度不影响伪时间 n_latent5, # 隐空间维度这是最关键的一个参数 enc_hidden[1024, 1024], # 编码器隐层宽度 dec_hidden[1024, 1024], # 解码器隐层宽度 vae_enc_hidden[1024, 1024], vae_dec_hidden[1024, 1024], ode_hidden[1024, 1024], # 动力学网络轨迹形状主要由它决定 alpha_recon_lec0.5, alpha_recon_ies0.5, alpha_kl1.0, alpha_ode1.0, loss_weights[1.0, 1.0, 1.0, 1.0, 1.0], lec1, # 文库大小尺度因子 batch_size128, lr1e-4, wt_decay1e-4, n_epochs1000, random_state0, ) model.train()n_latent我一般从 5 起步。这个维度太小轨迹会挤成一条线分支出不来太大隐空间足够复杂模型可能把噪声也拟合成一条分支你就会看到一堆假的树枝。我的经验法则是预期分支数 2 到 3。如果你预计只有一条主线加一个分叉那n_latent5差不多如果是多谱系分化可以试到 8 到 10。判断标准不是看数是看结果跟你的生物学预期合不合。ode_hidden的学习率lr也要留意。默认 1e-4 是比较保守的如果你的数据细胞数过万可以把batch_size提到 512 到 1024同时把lr微调到 2e-4能明显加快收敛。但别贪lr上到 1e-3 这种量级ODE 积分器容易报数值不稳定你会看到 loss 直接飞成 NaN。4.2 损失函数五项权重怎么调loss_weights对应的是几项损失的相对权重我理解它大概是这个分工重构损失保证隐空间不丢失表达信息、KL 损失保证隐空间连续、IES 相关损失约束初始状态映射、动力学损失保证轨迹平滑可积。默认全 1 是一个比较均衡的起点。什么时候要调我遇到过的两种情况。第一种是隐空间过度平滑所有细胞都糊在一起UMAP 上看不出结构这时候可以适当提高重构损失的权重让隐空间保留更多表达细节。第二种是过拟合训练集上的伪时间很漂亮但用同一批细胞的子集去预测结果对不上这时候要提高 KL 权重也就是给隐空间更强的正则。调权重这件事不建议一次动多项容易搞不清是谁的功劳我一般是固定其他项单项从 1 调到 0.5 或者 2跑三五个组合看结果。4.3 训练日志与收敛判断model.train()跑起来之后会打印每轮的损失值你需要盯的就是这几个数字是不是平稳下来了。我的判断标准是连续 100 到 200 轮 loss 的波动幅度小于 1%且没有上升趋势就可以认为收敛了。如果跑了 1000 轮还在明显下降说明n_epochs不够或者学习率偏小如果中途出现尖峰然后回落那大概是某一批数据的积分出了数值问题这种情况一般不影响最终结果但如果反复出现就该检查数据里有没有极端的异常细胞。训练时间上一万个细胞、两万个基因的数据集单张 GPU 大概跑 20 到 40 分钟这是常见量级的经验值具体取决于硬件和参数。如果只有 CPU那建议先把细胞数下采样到 5000 以内否则一个晚上都跑不完。下采样这件事不用担心丢信息轨迹推断本来就是看整体形状不是看单个细胞。还有一个实操细节固定random_state之后同一个环境里重复跑应该得到完全一致的结果。如果两次结果不一样那大概率是有地方没固定住比如 torch 的随机种子、或者 CUDA 的非确定性算子。写文章要复现的话建议把 torch 的种子也手动设一下。4.4 三种输出结果的读取与解读模型训练完之后主要取三样东西model.get_latent() # 隐空间坐标一般写到 adata.obsm 里 model.get_pseudotime() # 每个细胞的伪时间写到 adata.obs 里 model.get_vector_field() # 向量场用于在隐空间或 UMAP 上画箭头具体写进obsm和obs的键名不同版本略有差异跑完print(adata)看一眼就清楚了。隐空间坐标我一般会单独画一张图跟 UMAP 对照着看如果隐空间上是连续铺开的、没有明显的断层说明模型拟合得还行。伪时间是最常用的一般会画三种图伪时间在 UMAP 上的着色图、伪时间在隐空间上的着色图、以及伪时间沿某个特定细胞类型的分布小提琴图。向量场是画在网格上的箭头主要用来展示分支结构箭头指向哪里细胞就往哪里走。解读的时候有一条铁律伪时间的起点是模型自己推出来的不一定符合你的生物学预期。模型可能把终末态当成起点因为数学上正反都能积。所以拿到结果的第一件事是找一个你确定的早期 marker 基因看看它在伪时间上的趋势是不是从高到低或者从低到高如果完全反了那就要做反向处理比较简单的方式是直接用 1 减去伪时间。这件事后面第 7 节还会展开。5. 把时间轴铺到空间切片上5.1 Visium 数据为什么不能直接跑 scTour这是很多人第一次做空转轨迹时的误解。10X Visium 一个切片大概是 5000 个 spot 左右每个 spot 是 55 微米里面可能包含 1 到 10 个细胞本质上是一个混合表达谱。你直接把这个矩阵喂给 scTour模型的动力学假设是有问题的因为 spot 不是一个细胞状态它是多个状态的加权平均积分出来的伪时间没有生物学含义。另外 spot 数量也偏低训练不充分。所以正确做法是用单细胞数据训练参考模型再用它预测空转 spot 的伪时间。这就用到了前面说的 IES 模块和predict接口。5.2 用 predict 做参考图谱映射流程大概是这样的先在一个匹配的组织单细胞数据集上训练好 scTour 模型然后把空转数据整理成同样基因顺序的 AnnData调predict# 参考数据训练完之后 adata_spatial sc.read_visium(spatial_sample/) adata_spatial.var_names_make_unique() # 关键一步把基因顺序对齐到参考模型 common_genes adata_ref.var_names.intersection(adata_spatial.var_names) adata_spatial adata_spatial[:, common_genes].copy() adata_ref adata_ref[:, common_genes].copy() # 用参考模型的隐空间参数做预测 model.predict(adata_spatial, modepseudotime)这里有三个必须注意的点。第一基因集必须一致参考模型是在某个基因集合上训练的你给新数据必须严格对齐多一个少一个都会报错或者给出错误结果。第二不要在空转数据上重新做归一化之后再喂要用跟参考数据同样的处理方式否则批次差异会被模型当成状态差异。第三预测出来的伪时间分布要检查如果所有 spot 的伪时间都挤在一个很窄的区间里说明参考图谱的覆盖范围不够或者基因对齐出了问题。我个人的习惯是先拿一小部分参考数据本身做 leave-out 验证看看predict出来的伪时间和训练时算出来的伪时间相关性有多高Spearman 相关系数我一般要求 0.85 以上这个相关性过关了再往空转数据上推。5.3 空间邻域一致性校验与方向校正预测完之后光看伪时间在切片上的着色图是不够的你得做两个校验。第一个是空间自相关也就是相邻 spot 的伪时间是不是接近。因为真实的组织发育是有空间连续性的如果一张切片上伪时间像雪花一样随机分布那结果基本不可信。可以用 Morans I 或者简单地算一下每个 spot 和它六个邻居的伪时间差中位数差应该是个比较小的值。第二个是方向一致性看看伪时间的梯度方向跟已知的解剖学结构对不对得上比如肠道切片绒毛顶端和隐窝的伪时间应该是一高一低如果梯度是乱的那就要回去检查参考数据选得对不对。还有一个挺实用的技巧把伪时间和某个空间 marker 的表达做联合可视化。比如你在肿瘤切片上看到伪时间高的区域恰好是某个侵袭性 marker 高表达的区域那这个伪时间梯度就有了生物学支撑写文章的时候也站得住脚。这一步可以帮你挡掉不少审稿人的质疑。6. 伪时间结果的可信度校验注释与克隆信息两把尺子6.1 用人工注释做方向自检单细胞手动注释这个词最近被搜得很多原因就是自动注释工具比如各种基于参考的 label transfer给出的结果经常经不起细看最后还是得人工一个个 marker 去核。做轨迹分析的时候手动注释反而是最好用的校验工具。具体怎么做把你的手动注释结果和伪时间叠在一张图上看。理想情况下生物学上应该更“晚”的细胞类型伪时间应该整体更高。比如你注释出了 naive CD4 T、Tfh、浆细胞这三个群从生物学上 naive 应该在最早浆细胞在最后那伪时间的中位数就应该是 naive Tfh 浆细胞。如果顺序反了说明模型的起点选反了。如果顺序乱成一锅粥那要么是你注释错了要么是这批细胞根本不在同一条轨迹上得回去重新审视细胞类型的选取。我还习惯做一件事看每个细胞类型内部伪时间的分布宽度。如果某个成熟细胞类型内部伪时间的方差特别大说明模型在这个类型内部还硬分出了先后这时候要想想是不是有亚状态被你漏掉了或者这个类型根本没有分化过程在里面。6.2 BCR 克隆型叠加给 B 细胞轨迹加一条硬约束做BCR 单细胞分析的同学应该对这个场景很熟B 细胞的分化过程中克隆扩增和体细胞高频突变是同时发生的。这时候伪时间就不只是一个数学量它有一个物理意义上的对应物——克隆内的突变积累。我做过的一个比较有意思的验证是把 BCR 的克隆型信息也就是重链和轻链的 VDJ 序列跟 scTour 的伪时间拼在一起。具体操作是先按克隆型分组同一个克隆型内的细胞应该属于同一个祖先然后看这些细胞的伪时间是不是集中在一个较窄的区间内。理论上同一个克隆的细胞分化程度相近伪时间应该接近。如果同一个克隆型的细胞伪时间分散在 0 到 1 的整个区间那要么是这个克隆型的判定太宽松比如把不同克隆合并了要么是伪时间算错了。再进一步可以把克隆内的突变位点数可以理解为系统发育距离和伪时间做相关理论上应该是正相关。这个相关性如果成立那就是一个非常强的交叉验证因为它来自完全独立的一套数据VDJ 序列 vs 转录组比单纯看 marker 基因要硬得多。当然这个验证对数据质量要求高配对的重轻链必须测到而且克隆型得有一定的大小。样本量小的时候不要硬做容易得出矛盾结论然后自己纠结半天。7. 常见问题与排查速查7.1 伪时间整体逆序这是最高频的问题具体表现是你很确定某个早期细胞群但它伪时间反而最高。原因通常是模型积分的初始状态选错了也就是数值上积分方向反了。处理办法有两种一是直接用1 - pseudotime翻转这只是个显示层面的修正不影响拓扑结构是最省事的做法二是回到模型层面检查隐空间的动力学方向看是不是可以通过调整损失权重让模型学到正确的方向。我的建议是先用第一种因为伪时间的绝对方向本来就没有物理意义只要相对顺序对就行没必要为了一个方向反复重训。7.2 分支塌缩成一条直线你明明预期有两条分化路径但模型给出的是单调的一条线。这通常是n_latent给得太小隐空间的自由度不够装不下两条路径或者是你选取的细胞群里根本就没包含分叉点只有一条主干。排查顺序先调大n_latent到 8 或 10 重跑如果还是直的那就回去检查细胞选取——把分叉后的两群细胞都画在 UMAP 上看它们是不是真的从同一个点分出来的。有时候所谓的分叉其实是两个独立的过程被放在一起了那硬要模型分叉就是不合理的。7.3 新数据预测结果飘用predict把参考模型套到新数据上结果伪时间分布跟训练集完全不在一个量级。常见原因有三基因顺序没对齐、新数据和参考数据预处理方式不一致、参考图谱的覆盖范围太窄。最直接的排查方式是做一次“回投”就是拿参考数据自己的一个子集比如 20%去 predict看结果跟训练结果的相关性如果不相关那是模型或接口的问题如果相关但新数据不相关那就是新数据这边的问题。7.4 问题速查表现象最可能的原因优先尝试的处理伪时间整体逆序积分方向反了用 1 - pseudotime 翻转分支塌成直线n_latent 太小或缺少分叉点调大 n_latent复查细胞选取隐空间糊成一团损失权重偏平滑提高重构损失权重训练后期 loss 飙升lr 过大导致 ODE 数值不稳定降 lr 到 1e-4 以下空转预测全挤在一点参考图谱覆盖不足补参考数据或换参考预测与训练不一致基因顺序未对齐严格按参考的 var_names 切片同克隆细胞伪时间分散克隆型判定过松收紧克隆型定义再验证UMAP 上伪时间呈块状批次未处理做保守的批次整合8. 落地时几个容易被忽略的工程细节前面讲的都是分析层面的内容最后说几个工程化落地时才会暴露出来的问题这些在文档里基本不会写。第一是显存和内存的估算。scTour 的编码器和解码器都是全连接层参数量跟基因数成正比如果你的percent设得比较大基因数上到几千那单张卡的显存占用量会明显上去。我的经验是一万个细胞、两百个基因的配置8G 显存能跑但如果基因数翻十倍最好还是把 batch size 降下来或者用梯度累积。CPU 内存这边主要是 AnnData 本身占的空间稀疏矩阵一般不成问题但如果你中间某一步不小心把稀疏矩阵转成了稠密内存会瞬间爆掉这个坑我踩过不止一次。第二是结果的可复现性。除了固定random_state还要记录下你的 scanpy 版本、torch 版本、CUDA 版本因为不同版本的归一化函数和数值算子会带来微小差异累积到最后可能让伪时间的分布有肉眼可见的变化。我的习惯是把整个环境冻结成一个 yaml 或者 requirements 文件跟分析脚本一起存档。第三是伪时间数值不要跨数据集直接比。这一条我在前面提过这里再强调一次。伪时间的绝对数值受细胞数量、基因选取、积分步长影响A 数据集里的 0.6 和 B 数据集里的 0.6 完全不是一个东西只有同一模型、同一参考下的结果才能放在一起比较。如果一定要跨样本比较那就用参考模型统一预测或者干脆退回到“处于哪个细胞类型/哪个阶段的细胞比例”这种离散指标上去比较反而更稳。第四是别把伪时间当唯一的证据。我见过不少人拿着一个伪时间着色图就下结论说“A 细胞分化为 B 细胞”这个推断强度其实很弱因为伪时间只给了一个数学上的顺序。要下这种结论你得有至少两到三条互相独立的证据marker 基因的表达梯度、RNA 速率的方向如果数据质量允许、转录因子活性推断、以及前面说的 BCR 或者谱系追踪这类物理证据。证据链越厚结论越硬这也是为什么我一直强调要做交叉校验。第五是保存中间结果。scTour 训练一次不便宜隐空间坐标、伪时间、向量场这三样东西跑完就立刻写进 AnnData 并存成 h5ad别等到画图的时候发现忘了存又得重跑。向量场尤其要注意它不是每个细胞一个值是网格上的单独存成 npz 比较合适。我自己养成了一个习惯每跑完一个关键步骤就存一个带时间戳的 h5ad虽然占点磁盘但省下来的重跑时间远不止这点空间。最后分享一个我用了挺久的小技巧如果你要在文章里展示伪时间结果别只放一张彩色的 UMAP。我一般会配三张图放在一个面板里——UMAP 上的隐空间结构、伪时间着色、以及沿伪时间轴的关键 marker 表达曲线。三张图互相印证读者一眼就能看懂轨迹的形状和方向比单放一张着色图说服力强得多。至于 scTour 后续还能怎么扩展我最近在尝试把它和空间邻域图结合起来让动力学方程不只依赖表达还依赖空间邻居的信息这一块还在摸索等有稳定结果再拿出来聊。