BBKNN 算法原理:基于平衡 k 近邻
上一章我们聊了整合质量的评估指标,但说到底,你得先有整合结果才能去评估。那么 BBKNN 到底是怎么把那些「各自为政」的单细胞数据集揉到一起的?说白了,它玩的还是「找邻居」这个老路子,不过加了个很巧妙的平衡策略。
我刚开始接触单细胞数据整合时,试过不少传统方法。那些方法要么计算量太大,要么容易把不同批次的细胞强行拉到一起,造成「假整合」。BBKNN 给了我不一样的感受——它干净利落,只在局部做文章。
从 k 近邻说起
先回顾一下最基础的 k 近邻(kNN)算法。你想想看,在降维后的空间里,每个细胞就是一个点。找最近邻,就是计算欧氏距离或余弦距离,挑出距离最近的 k 个点。
不过问题来了——单细胞数据里,不同批次的细胞往往因为技术差异而各自抱团。如果你直接在全局范围内找 k 个最近邻,大概率找到的都是自己批次的「老乡」。整合?根本谈不上。
嗯,传统的 kNN 在这里确实不太灵光。
BBKNN 的核心改进
BBKNN 全称是 Balanced Batch K-Nearest Neighbors,它的改进思路其实很直白:
不是在全量数据里找 k 个最近邻,而是每个批次各找若干近邻,拼成一个「跨批次」的邻居集合。
这里的「平衡」体现在两点:
- 批次内平衡:每个批次贡献的邻居数量大致相同,避免大批次「淹没」小批次
- 距离尺度平衡:不同批次间的距离分布不同,需要自适应缩放
我有个项目恰好处理了 8 个不同技术平台的 PBMC 数据。用传统 kNN 时,10x 平台的细胞邻居全是 10x 的,Smart-seq2 的细胞邻居全是 Smart-seq2 的。而 BBKNN 一次性就把「跨平台桥梁」搭起来了。避坑指南:如果你的数据集批次数量差异很大(比如 1 个大批次 vs 9 个小批次),BBKNN 的平衡效果会打折扣。 我曾经踩过这个坑,后来通过先对小批次做 oversampling 才解决。
算法执行流程
BBKNN 的具体步骤可以拆成 4 个阶段:
- 输入准备:接收降维后的细胞坐标矩阵(通常是 PCA 结果)和批次标签
- 批次内搜索:对每个细胞,分别在它所属批次的其它细胞中找
n_neighbors个最近邻 - 跨批次邻居合并:从其它每个批次中各自找
n_neighbors // (n_batches - 1)个近邻,拼成平衡邻居集 - 加权图构建:用这些邻居关系构建一个加权图,后续用于 UMAP 可视化
我用 SVG 画了一张流程图,把这几个步骤串起来——
平衡机制详解
你可能会问:这个「平衡」到底是怎么做到数学上可操作的?
BBKNN 引入了两个关键的缩放因子:
| 缩放因子 | 作用对象 | 计算公式说明 |
|---|---|---|
| 批次内距离缩放 | 每个批次内部的距离矩阵 | 除以该批次所有距离的中位数,使不同批次的距离尺度归一 |
| 邻居配额分配 | 每个批次向目标细胞贡献的邻居数 | ceil(k / (B - 1)),其中 B 为总批次数 |
举个例子——你有 3 个批次的数据,设置 n_neighbors = 30。那么对于批次 A 中的一个细胞,它会在批次 B 和批次 C 中各找 ceil(30 / 2) = 15 个最近邻,共计 30 个跨批次邻居。至于它自己批次(批次 A)里的邻居,完全不参与整合。
n_neighbors 适当调大到 50-80。
代码实现要点
BBKNN 的 Python 接口非常简单。以下是核心调用示例——
import bbknn
# 假设你已经有了降维后的数据 (adata.obsm['X_pca'])
# 批次标签存在 adata.obs['batch'] 中
bbknn.bbknn(adata,
batch_key='batch',
n_neighbors=30,
neighbors_within_batch=3,
metric='euclidean')
这里的 neighbors_within_batch 参数值得多说两句。它控制每个批次的内部邻居数量:
- 设为 0(默认):完全不考虑同一批次的邻居,只做跨批次连接
- 设为 3-5:保留少量同批次邻居,有助于保持细胞亚群的局部结构
neighbors_within_batch 不要设太大。我有一个教训——有次设了 10,结果整合后的图上出现了明显的批次内「团块」,失去了跨批次整合的意义。经验值:neighbors_within_batch 不要超过 n_neighbors 的 20%。
跟传统 kNN 比,区别在哪?
我整理了一张对比表,应该能帮你快速抓住 BBKNN 的独特之处——
| 维度 | 传统 kNN | BBKNN |
|---|---|---|
| 邻居来源 | 全局搜索,不考虑批次 | 各批次分别搜索,强制跨批次 |
| 批次偏倚 | 严重,容易形成批次团簇 | 显著抑制,难以形成纯批次团簇 |
| 内存消耗 | O(n²),n 为总细胞数 | 约等于 O(n²),但分批次搜索可并行 |
| 适用场景 | 单批次或批次效应很弱 | 多批次、批次效应明显 |
| 调参复杂度 | 低,k 即可 | 中等,多了 batch_key 和 neighbors_within_batch |
一些容易忽略的细节
BBKNN 的原理虽然不复杂,但实际用起来有几个地方得留个心眼:
- 输入空间必须是降维后的:BBKNN 在原始基因表达矩阵上跑会慢到怀疑人生。我个人习惯先用 PCA 降到 50-100 维。
- 距离度量建议用 'euclidean':虽然支持 'cosine',但在高维 PCA 空间里,欧氏距离的表现通常更稳定。
- 对称性要求:BBKNN 构建的图默认是单向的(细胞 A 选了 B 做邻居,但 B 不一定选了 A)。最终会做一次「对称化」处理,把单向边转成双向。
- 跟 UMAP 的衔接:BBKNN 生成的邻接图可以直接丢给 UMAP 做可视化。这也是 BBKNN 的最大卖点——整合和可视化一体化。
一句话总结:BBKNN 通过「分批搜索 + 配额分配 + 距离缩放」三套组合拳,在局部邻居层面实现了跨批次的知识迁移。它不全局对齐,不做变换,只改邻居关系——效果却出奇的好。