3. 化学相似性度量:Tanimoto系数、欧氏距离、余弦相似度
好了,咱们开始聊点硬核的。化学相似性度量,说白了就是量化评估两个分子有多“像”。你想想看,在ADMET预测里,我们常假设“结构相似的分子,性质也相似”。但这个“像”怎么算?这就是今天要讲的三种核心度量方法。
我个人习惯把这三种方法分成两类:一类是基于分子指纹的,另一类是基于连续特征向量的。Tanimoto系数属于前者,欧氏距离和余弦相似度属于后者。理解了这点,你在实际项目中选哪个,心里就有谱了。
3.1 Tanimoto系数:分子指纹的黄金标准
如果你做过药物筛选,对Tanimoto系数一定不陌生。它是专门为二值指纹设计的。什么叫二值指纹?就是用一个0/1向量来表示分子中是否存在某种子结构。
举个例子,一个分子如果有苯环,某一位就是1;有羰基,另一位就是1。这样每个分子都被编码成一个固定长度的二进制串。比如我们常见的 Morgan 指纹(ECFP),生成的就是这种形式。
Tanimoto系数的定义非常简单:
T(A, B) = |A ∩ B| / |A ∪ B|
其中:
A、B 分别表示分子 A 和 B 的指纹集合
|A ∩ B| = 两个指纹中同时为1的位数(交集)
|A ∪ B| = 两个指纹中至少有一个为1的位数(并集)
这个值在0到1之间。0代表完全不相似,1代表完全相同。
下面是我在实际项目中用的计算代码,Python实现:
import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem
def tanimoto_similarity(mol1, mol2):
"""
计算两个分子的 Tanimoto 相似度
"""
# 生成 Morgan 指纹,半径=2,2048位
fp1 = AllChem.GetMorganFingerprintAsBitVect(mol1, 2, nBits=2048)
fp2 = AllChem.GetMorganFingerprintAsBitVect(mol2, 2, nBits=2048)
# 转换为 numpy 数组
arr1 = np.array(fp1)
arr2 = np.array(fp2)
# 计算交集和并集
intersection = np.sum(arr1 & arr2)
union = np.sum(arr1 | arr2)
# 避免除零错误
if union == 0:
return 0.0
return intersection / union
# 使用示例
smiles1 = "Cc1ccccc1" # 甲苯
smiles2 = "c1ccccc1" # 苯
mol1 = Chem.MolFromSmiles(smiles1)
mol2 = Chem.MolFromSmiles(smiles2)
sim = tanimoto_similarity(mol1, mol2)
print(f"甲苯与苯的 Tanimoto 相似度: {sim:.4f}")
# 预期输出:0.7500 左右
应用场景:
- 化合物库虚拟筛选:从百万级库中找到与先导化合物最相似的结构
- 聚类分析:对分子库进行去冗余,确保训练集多样性
- 药效团匹配:评估候选分子与已知活性分子的结构相似程度
我曾经踩过的坑:别忘了Tanimoto系数对指纹的公共位点很敏感。如果两个分子都有大量相同的“背景”子结构(比如长烷基链),即使活性位点完全不同,相似度也可能很高。我问你,这合理吗?明显不合理。所以我建议你结合特征重要性分析,只看对活性贡献大的子结构片段。
个人习惯:我在做ADMET预测时,通常设一个阈值,比如0.6。训练集里所有与测试分子Tanimoto低于0.6的,我都视为“结构足够远”。但阈值怎么定?根据你具体的项目和分子库分布来试。没有万能的参数。
3.2 欧氏距离:简单直接,但有陷阱
欧氏距离可能是你最早接触的距离度量了。两点之间的直线距离嘛。但在化学信息学里,它用于比较的是连续特征向量,比如分子量、LogP、氢键供体数、极性表面积……这些东西组成的向量。
公式也简单:
d(A, B) = sqrt( Σ(Ai - Bi)² )
其中:
Ai、Bi 分别是分子 A 和 B 的第 i 个特征值
Σ 表示对 i 从 1 到 n 求和
数值越小,表示相似度越高。
你可能会问:这东西有什么不好吗?有。且听我细说。
import numpy as np
from scipy.spatial.distance import euclidean
def euclidean_similarity(mol1_descriptors, mol2_descriptors):
"""
计算两个分子的欧氏距离,并转换为相似度
注意:这里先计算距离,再映射到 [0, 1] 区间
"""
dist = euclidean(mol1_descriptors, mol2_descriptors)
# 将距离转换为相似度(距离越大越不相似)
# 使用 sigmoid 函数进行映射
sim = 1 / (1 + dist)
return sim
# 假设分子特征:[MW, LogP, HBD, HBA, TPSA, RotBonds]
mol1_feat = np.array([180.0, 2.5, 1, 2, 37.0, 3])
mol2_feat = np.array([200.0, 3.0, 2, 3, 45.0, 4])
sim = euclidean_similarity(mol1_feat, mol2_feat)
print(f"基于 ADMET 描述符的相似度: {sim:.4f}")
重要避坑:欧氏距离对量纲极度敏感。分子量几百、LogP不到10、TPSA可能上百……这些数值的尺度完全不在一个级别。如果直接算,分子量会把其他特征淹没掉。我刚开始用欧氏距离时就踩过这个坑——看到结果全是分子量在主导,完全忽略了关键的结构信息。
标准化是必须的:
from sklearn.preprocessing import StandardScaler
# 假设你的训练集分子特征存储在 X_train 中
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
# 测试分子也要用同样的 scaler
x_test_scaled = scaler.transform([x_test])
# 然后计算欧氏距离
dist = euclidean(X_train_scaled[0], x_test_scaled[0])
我建议你在做任何基于距离的ADMET预测之前,都先做好特征标准化。这能省掉你90%的调试时间。
3.3 余弦相似度:专注“方向”而非“长度”
余弦相似度衡量的是两个向量之间夹角的余弦值。如果说欧氏距离关心的是“直线距离”,那余弦相似度关心的就是“方向是否一致”。
公式更漂亮:
cos(A, B) = (A · B) / (||A|| × ||B||)
其中:
A · B = Σ(Ai × Bi) 向量点积
||A|| = sqrt(ΣAi²) 向量模长
取值范围是[-1, 1]。1代表方向完全相同(相似度最高),-1代表完全相反。
这个度量在什么场景下特别好用?我告诉你——当你用词袋模型或潜在语义索引来表示分子时。比如你用一个很长的向量来表示分子中所有子结构出现的频次,这时候向量长度可能差异很大,但方向才是关键。
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def cosine_mol_similarity(fp1, fp2):
"""
计算两个分子指纹的余弦相似度
这里 fp 可以是计数的(非二值),比如 ECFP 的计数版本
"""
# 保持向量形状
vec1 = np.array(fp1).reshape(1, -1)
vec2 = np.array(fp2).reshape(1, -1)
sim = cosine_similarity(vec1, vec2)[0][0]
return sim
# 示例:使用计数型 Morgan 指纹
from rdkit.Chem import AllChem
mol1 = Chem.MolFromSmiles("CCCO") # 丙醇
mol2 = Chem.MolFromSmiles("CCCOCC") # 乙醚
# 计数指纹:记录每个子结构出现的次数,不只是0/1
fp1 = AllChem.GetHashedMorganFingerprint(mol1, 2, nBits=2048, useCounts=True)
fp2 = AllChem.GetHashedMorganFingerprint(mol2, 2, nBits=2048, useCounts=True)
sim = cosine_mol_similarity(fp1, fp2)
print(f"余弦相似度(计数指纹): {sim:.4f}")
我个人的选择经验:
| 度量方法 | 适合场景 | 不适用场景 |
|---|---|---|
| Tanimoto 系数 | 二值分子指纹、化合物库筛选 | 连续特征、稀疏的非二值向量 |
| 欧氏距离 | 物理化学描述符、经过标准化的连续特征 | 未标准化的特征、高维稀疏数据 |
| 余弦相似度 | 计数指纹、频次特征、忽略量级差异的场景 | 需要绝对数量信息的场景 |
3.4 实战选择:我怎么选?
好,理论讲完了,该聊聊实战了。你面对一批分子,到底用哪种度量?
我总结了一个简单的决策流程:
- 看你的分子表示方式:是二值指纹?计数指纹?还是连续描述符向量?这直接决定了你用什么。
- 看数据稀疏程度:二值指纹用Tanimoto。如果指纹里1的个数很少(很稀疏),Tanimoto比余弦更鲁棒。
- 看是否需要对量纲敏感:如果特征都在同一尺度,欧氏距离直觉最好。否则先用余弦或做好标准化再用欧氏。
- 看最终任务:做ADMET定性分类(好/差),三种差别不大。做定量预测(精确数值),我建议你试一下,甚至把三种做成特征拼接进去。
说实话,没有绝对的“最佳”度量。我自己的做法是:项目初期,用Tanimoto快速跑一遍筛选。如果效果不理想,再换成余弦或欧氏,对比一下哪个的AUC更高。
记一个项目经验:上次做口服吸收性预测,我用Tanimoto做了聚类去冗余,用欧氏距离计算物理化学特征的相似度,最后把两类相似度作为特征输入到随机森林模型。结果比只用单一度量高了5%的准确率。你看,有时候不一定要二选一。
3.5 本节要点总结
嗯,到这里关于三种化学相似性度量的核心内容就全了。我把它整理成几点,方便你回忆:
- Tanimoto系数:二值指纹的标配,化合物库筛选的基石。注意它受公共位点影响大。
- 欧氏距离:直观好用,但必须做特征标准化。不然量纲大的特征会主导结果。
- 余弦相似度:专注方向,适合计数指纹和稀疏向量。忽略向量长度,别有洞天。
- 选择依据:看分子表示方式 → 看数据稀疏程度 → 看是否需要对量纲敏感 → 看最终任务。