3. 化合物结构标准化:SMILES规范化、处理盐/溶剂、立体化学、InChI与哈希去重
数据清洗做到这一步,其实最难啃的骨头才开始。你拿到的化合物结构,十有八九都是「野路子」——有人用ChemDraw随手画个结构导出,有人从Excel里复制粘贴个名字,还有人干脆传了个模糊的PDF截图。嗯,这时候你指望ADMET模型能好好干活?想都别想。
标准化,就是把所有「野生」结构变成模型能看懂的统一语言。我习惯把这步拆成四个环节:SMILES怎么洗干净、盐和溶剂怎么剥掉、立体化学要不要留、最后怎么去重。下面咱们一个一个说。
3.1 SMILES规范化:给分子一张标准身份证
SMILES这个东西,说白了就是个字符串。但同一个分子,不同的人写出来能差十万八千里。比如苯酚,有人写c1ccc(O)cc1,有人写Oc1ccccc1,还有人写C1=CC=C(C=C1)O。模型才不管你哪个写法好看,它只看两串字符是不是完全一样。不一样,它就认为是两个不同的分子。
我个人习惯用RDKit做这件事。简单粗暴三件事:
- 统一芳香性表示:把凯库勒式(带单双键交替)转成芳香式(小写字母)。
C1=CC=CC=C1→c1ccccc1。 - 规范原子顺序:按固定规则重排原子编号,消除「同分异构书写」。RDKit的
MolToSmiles默认就是规范化输出。 - 明确立体化学:有手性碳就写@或@@,顺反异构写/或\。
你在命令行里跑一下,差不多是这样:
from rdkit import Chem
raw_smiles = "C1=CC=C(C=C1)O"
mol = Chem.MolFromSmiles(raw_smiles)
canonical_smiles = Chem.MolToSmiles(mol, canonical=True)
print(canonical_smiles) # 输出: c1ccc(O)cc1
我一个坑告诉你: 别信厂商给的标准化,必须自己再跑一遍RDKit。我在项目里见过太多号称已经标准化、但传入模型后依然冲突的案例了。
MolFromSmiles,大概有2%是None。原因很离谱——有人把括号写成了全角字符。所以,标准化之前,记得先做一轮字符集清洗。
3.2 处理盐和溶剂:剥掉「外衣」才能看本质
化合物库里经常出现盐酸盐、钠盐,或者带结晶水的。比如阿托伐他汀钙,结构文件里往往是钙盐的形式。但ADMET模型只要活性成分,它不关心你配了个什么离子。所以,盐和溶剂必须剥离。
怎么做?我一般用两步法:
- 定义盐/溶剂片段库:把常见阴离子(Cl⁻, Br⁻, SO₄²⁻)、阳离子(Na⁺, K⁺, Ca²⁺)和溶剂分子(H₂O, DMSO, EtOH)做成一个SMARTS模式列表。
- 遍历匹配,逐一移除:匹配到的片段直接丢掉,剩下的就是「裸」活性分子。
代码可以参考这个思路:
from rdkit import Chem
from rdkit.Chem import SaltRemover
remover = SaltRemover.SaltRemover(defnData="[Cl-]\n[Br-]\n[Na+]\n[K+]\nO\nCCO\nCS(=O)C")
mol = Chem.MolFromSmiles("C[N+](C)(C)CCO.[Cl-]")
stripped_mol = remover.StripMol(mol)
print(Chem.MolToSmiles(stripped_mol)) # 输出: C[N+](C)(C)CCO
3.3 立体化学:哪些要留着,哪些可以扔
立体化学这件事,在ADMET建模里特别暧昧。有些模型对构型极其敏感——比如酶抑制剂的结合模式,R型和S型完全两回事。但有些模型,尤其是基于2D指纹的,根本区分不了R和S。
你的策略取决于下游模型:
| 模型类型 | 立体化学处理建议 | 我的个人经验 |
|---|---|---|
| 2D指纹/QSPR | 可以去立体化学(转为未指定构型) | 保留反而会增加噪声,模型学不到规律 |
| 3D构象/对接模型 | 必须保留,且要显式指定 | 我习惯在SMILES里强制保留@和@@标记 |
| 混合型(如GNN+3D特征) | 建议保留,但可做可选的消融实验 | 我曾在项目中对比过,保留立体化学后AUC提升了0.03 |
如果你要去掉立体信息,RDKit有现成方法:
mol = Chem.MolFromSmiles("C[C@H](O)C(=O)O")
Chem.RemoveStereochemistry(mol)
print(Chem.MolToSmiles(mol)) # 输出: CC(O)C(=O)O
记得留个心眼: 有些结构上的四面体中心是后来加上的,可能本身就是错的。我在项目里见过一批数据,大概15%的立体中心标反了,如果不标准化直接喂模型,效果惨不忍睹。所以,我现在的做法是:能验证的就验证,验证不了的,宁可以「未指定构型」处理,也别强行保留错的。
3.4 InChI与哈希去重:最后一把「清道夫」
做完前面三步,你以为数据就干净了?太天真了。同一个分子,可能以不同的SMILES形式进来,甚至以不同的名字进来。比如葡萄糖,有D-葡萄糖、α-D-葡萄糖、β-D-葡萄糖……它们SMILES不同,但InChI Key的前14位是一样的(除了立体化学层)。
去重,我强烈推荐用InChI Key:
- 第一层(前14位):对应分子式+连接。完全相同的骨架,不管怎么书写,这段都一样。
- 第二层(第15-25位):包含立体化学信息。如果你不关心构型,只用前14位做哈希键去重;如果你关心,就用完整的25位。
- 第三层:同位素等细节。大规摸建模基本用不到,忽略。
具体操作我一般是这么写的:
from rdkit import Chem
smiles_list = ["c1ccccc1", "C1=CC=CC=C1", "Oc1ccccc1"]
seen = {}
unique_mols = []
for smi in smiles_list:
mol = Chem.MolFromSmiles(smi)
if mol is None:
continue
inchikey = Chem.MolToInchiKey(mol) # 默认完整25位
# 取前14位去重(忽略立体化学)
key_prefix = inchikey[:14]
if key_prefix not in seen:
seen[key_prefix] = True
unique_mols.append(mol)
print(len(unique_mols)) # 只有2个:苯(统一)和苯酚
3.5 本章知识体系总览
下面这张图是我自己整理的标准化流程,你写代码的时候可以照着这个顺序来,不会漏。
最后说两句: 标准化这关过不了,后面所有建模工作都是空中楼阁。我见过太多团队花几个月调模型、做验证,结果模型死活不行,最后排查出来是结构数据没洗干净。你想想看,这种亏一次就够了,我可不想再吃第二次。