分子相似性度量:Tanimoto系数、Dice系数、余弦相似度

做药物相似性计算,说白了就是测两个分子“长得像不像”。我入行那会儿,导师丢给我一个指纹文件和一套公式,说:“你把这100万个分子对跑完。” 我盯着屏幕,心里想的是——哪种度量最靠谱?

今天咱们就把三个最常用的度量一次说透:Tanimoto系数、Dice系数和余弦相似度。它们都能算相似性,但脾气完全不同。

1. Tanimoto系数

这是圈里用得最多的系数,也叫Jaccard系数。它的核心逻辑很简单:只看两个分子共同拥有的特征,不看都不拥有的。

公式长这样(假设指纹是位向量):

T = |A ∩ B| / (|A ∪ B|)
   = 共同非零位数量 / 总非零位数量

举个数值例子:A有3个位为1,B有4个位为1,其中2个是共有的。那么T = 2 / (3+4-2) = 2/5 = 0.4。

我个人习惯拿它做初筛。为什么?因为它对“稀疏”很敏感。两个分子如果不活跃的特征都很多,但活跃特征交集小,Tanimoto会给你一个诚实的低分。我在项目中遇到过,用Tanimoto筛完的top-100候选,实验命中率比余弦高出一大截。

关键点:Tanimoto 值域 [0,1],完全一样是1,完全不同是0。通常0.85以上算“高度相似”,0.7-0.85算“中等相似”。

2. Dice系数

Dice和Tanimoto长得像,但分母不一样:

D = 2 * |A ∩ B| / (|A| + |B|)
   = 2 × 共同非零位 / (A非零位数量 + B非零位数量)

它给了共同特征双倍权重。我刚开始做虚拟筛选时不理解,为什么Dice分数总是比Tanimoto高一点?你想想看,分母里没有减去交集,所以当交集占主导时,Dice会“膨胀”。

避坑指南:我曾经在结构聚类时用Dice做相似度阈值,结果把很多骨架不同的分子硬凑成了一类。后来才明白,Dice对共享特征少的分子对不够惩罚。如果你要区分“有没有”,Tanimoto更公平;如果你更关注“有多少共同点”,Dice更宽容。

Dice在化学信息学里常用于分子对接后处理和相似性融合。我个人的经验是:当指纹很稠密(比如MACCS指纹166位,平均一半是1),Dice和Tanimoto排名差异不大;但要是用ECFP4这种稀疏指纹(2048位,活跃特征只有几十个),差异就明显了。

3. 余弦相似度

余弦相似度来自向量空间模型:

cos(A, B) = (A · B) / (||A|| × ||B||)
           = 点积 / (模长乘积)

对于二进制指纹,分子完全相同:cos = 1;完全不同:cos = 0。但注意,它不考虑特征的总数差异——比如A有10个活性位,B有100个活性位,只要它们的夹角小,余弦就会给高分。

我记得有个项目,用余弦排序后,一个只有2个关键位、另一个有30个关键位的两个分子,居然被排到了前10。后来发现是余弦对“尺度”不敏感。那怎么办?我通常的做法是:余弦只用在已经做过归一化的特征向量上,比如用σ-π值或物化性质向量,而不是原始指纹位。

4. 三种系数对比

度量 公式(位向量形式) 值域 特点
Tanimoto |A∩B| / (|A|+|B|-|A∩B|) [0, 1] 最常用,对稀疏特征公平
Dice 2|A∩B| / (|A|+|B|) [0, 1] 给共同特征更高权重,分数偏高
余弦 (A·B) / sqrt((A·A) × (B·B)) [0, 1] 忽略尺度差异,适合连续特征向量

从代码实现看,它们都可以用指纹位操作快速计算。下面是一个Python示例:

def tanimoto(fp_a, fp_b):
    a = set(fp_a.bitlist())
    b = set(fp_b.bitlist())
    common = len(a & b)
    total = len(a | b)
    return common / total if total != 0 else 0.0

def dice(fp_a, fp_b):
    a = set(fp_a.bitlist())
    b = set(fp_b.bitlist())
    common = len(a & b)
    return 2 * common / (len(a) + len(b)) if (len(a)+len(b)) != 0 else 0.0

def cosine(fp_a, fp_b):
    dot = sum(1 for i in range(len(fp_a)) if fp_a[i] and fp_b[i])
    norm_a = sum(fp_a)
    norm_b = sum(fp_b)
    return dot / (sqrt(norm_a * norm_b) + 1e-10)
实战小贴士:如果你用RDKit的GetMorganFingerprintAsBitVect,里面自带DataStructs.TanimotoSimilarity()和DataStructs.DiceSimilarity(),别自己造轮子。但余弦相似度RDKit没直接提供,上面的代码正好补上。

最后,放一张我常画的对比图,把三种度量的关系理清楚:

分子相似性度量关系图 分子对 (A, B) Tanimoto Dice 余弦 相似性分数 聚类 / 排序 / 筛选 选择依据: 稀疏指纹 + 公平排序 → Tanimoto;关注共同特征量 → Dice;连续性质向量 → 余弦

嗯,上面这张图把它们的流程串起来了。你可能会问:实际项目里怎么选?我的建议是:

  • 标准虚拟筛选:用Tanimoto,业界基准,审稿人认。
  • 相似性融合:Dice+权重调和,我试过效果不错。
  • 跨描述子比较(比如把ECFP和物化性质拼成一个向量):用余弦,因为它不怕特征量纲不同。

三种度量就像三把刀——切不同的菜用不同的刀。你要是也踩过坑,或者有自己偏好的场景,欢迎在课后来找我聊聊。