3. 化合物结构标准化:SMILES规范化、处理盐/溶剂、立体化学、InChI与哈希去重

数据清洗做到这一步,其实最难啃的骨头才开始。你拿到的化合物结构,十有八九都是「野路子」——有人用ChemDraw随手画个结构导出,有人从Excel里复制粘贴个名字,还有人干脆传了个模糊的PDF截图。嗯,这时候你指望ADMET模型能好好干活?想都别想。

标准化,就是把所有「野生」结构变成模型能看懂的统一语言。我习惯把这步拆成四个环节:SMILES怎么洗干净、盐和溶剂怎么剥掉、立体化学要不要留、最后怎么去重。下面咱们一个一个说。

3.1 SMILES规范化:给分子一张标准身份证

SMILES这个东西,说白了就是个字符串。但同一个分子,不同的人写出来能差十万八千里。比如苯酚,有人写c1ccc(O)cc1,有人写Oc1ccccc1,还有人写C1=CC=C(C=C1)O。模型才不管你哪个写法好看,它只看两串字符是不是完全一样。不一样,它就认为是两个不同的分子。

核心原则: SMILES不标准,后续建模全白搭。规范化后,同一个分子必须且只能对应一个SMILES。

我个人习惯用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。我在项目里见过太多号称已经标准化、但传入模型后依然冲突的案例了。

避坑指南: 我曾经接了一批从PubChem抓下来的数据,看着挺规整,结果一跑MolFromSmiles,大概有2%是None。原因很离谱——有人把括号写成了全角字符。所以,标准化之前,记得先做一轮字符集清洗。

3.2 处理盐和溶剂:剥掉「外衣」才能看本质

化合物库里经常出现盐酸盐、钠盐,或者带结晶水的。比如阿托伐他汀钙,结构文件里往往是钙盐的形式。但ADMET模型只要活性成分,它不关心你配了个什么离子。所以,盐和溶剂必须剥离。

怎么做?我一般用两步法:

  1. 定义盐/溶剂片段库:把常见阴离子(Cl⁻, Br⁻, SO₄²⁻)、阳离子(Na⁺, K⁺, Ca²⁺)和溶剂分子(H₂O, DMSO, EtOH)做成一个SMARTS模式列表。
  2. 遍历匹配,逐一移除:匹配到的片段直接丢掉,剩下的就是「裸」活性分子。

代码可以参考这个思路:

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
注意: 别一刀切。有些分子本身就带电荷(比如季铵盐),你把电荷也给剥了,就闹笑话了。我建议对每个移除操作做一次「合理性检查」——剥完之后分子量不能小于某个阈值(比如50 Da),否则可能是把活性片段误杀了。

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个:苯(统一)和苯酚
我踩过的坑: 早期我用MD5对SMILES做哈希去重,结果同一个分子因为写法的不同,搞出来一堆重复。后来改用InChI Key,问题直接清零。建议你也别折腾,直接用InChI Key做为主键。

3.5 本章知识体系总览

下面这张图是我自己整理的标准化流程,你写代码的时候可以照着这个顺序来,不会漏。

化合物结构标准化核心流程 原始结构 SMILES规范化 剥盐/溶剂 立体化学处理 生成InChI Key 哈希去重(前14位/完整25位) 标准化数据库 注意: 先规范化再剥盐 否则SMILES解析会出错

最后说两句: 标准化这关过不了,后面所有建模工作都是空中楼阁。我见过太多团队花几个月调模型、做验证,结果模型死活不行,最后排查出来是结构数据没洗干净。你想想看,这种亏一次就够了,我可不想再吃第二次。