328

RDKit 标准化分子:建立可靠训练集

RDKit 标准化是建立可靠训练集的核心环节。这篇讲清 MolStandardize 各组件的行为、需要自定义的场景与版本一致性问题。

rdkit.Chem.MolStandardize 是分子标准化的标准工具。它的各个组件行为明确,但默认设置不一定符合你的需求——理解每个组件在做什么,才能判断该开哪些、该怎么调。

组件清单

from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
组件 行为 建模时是否需要
Cleanup 价键规范化、金属断键、官能团规范写法 必须
Normalize 按规则转换特定官能团写法 包含在 Cleanup 中
MetalDisconnector 断开共价金属键 处理金属配合物时
LargestFragmentChooser 保留最大片段(去盐) 通常需要
FragmentRemover 按盐列表删除已知盐 需精确控制时
Uncharger 中和可中和电荷 通常需要
Reionizer 按酸性强度重新分配质子 处理多电离位点时
TautomerEnumerator 枚举或规范化互变异构体 去重与 2D 建模时

逐组件演示

# 1) Cleanup:规范化写法
m = Chem.MolFromSmiles("C[N+](C)(C)CC(=O)[O-]")
print(Chem.MolToSmiles(rdMolStandardize.Cleanup(m)))

# 2) 去盐:LargestFragmentChooser vs FragmentRemover
lfc = rdMolStandardize.LargestFragmentChooser()
m = Chem.MolFromSmiles("CCN.Cl.Cl")
print(Chem.MolToSmiles(lfc.choose(m)))            # CCN

# 注意:LargestFragmentChooser 对共价二聚体等会误删!
m = Chem.MolFromSmiles("c1ccccc1.c1ccccc1")       # 两个相同片段
print(Chem.MolToSmiles(lfc.choose(m)))            # 只留一个

# 3) Uncharger:中和
unc = rdMolStandardize.Uncharger()
m = Chem.MolFromSmiles("CC(=O)[O-]")
print(Chem.MolToSmiles(unc.uncharge(m)))          # CC(=O)O

# 但季铵盐等永久电荷不会被中和(正确行为)
m = Chem.MolFromSmiles("C[N+](C)(C)C")
print(Chem.MolToSmiles(unc.uncharge(m)))          # 保持带电

# 4) 互变异构体规范化
te = rdMolStandardize.TautomerEnumerator()
for s in ["CC(=O)CC(=O)C", "CC(O)=CC(=O)C"]:      # 酮式与烯醇式
    print(s, "→", Chem.MolToSmiles(te.Canonicalize(Chem.MolFromSmiles(s))))
# 两者输出相同 —— 这正是去重需要的

需要自定义的场景

# 场景一:调整互变异构枚举的上限(大分子可能超时)
params = rdMolStandardize.CleanupParameters()
params.maxTautomers = 1000          # 默认可能过大
params.maxTransforms = 1000
te = rdMolStandardize.TautomerEnumerator(params)

# 场景二:保留特定的盐/共晶组分
# LargestFragmentChooser 会无差别删掉小片段。
# 若某些共晶组分有意义,应改用 FragmentRemover 配自定义盐表,
# 或先判断片段数与组成再决定是否处理。

def safe_desalt(mol, min_heavy_atoms=4):
    frags = Chem.GetMolFrags(mol, asMols=True, sanitizeFrags=True)
    if len(frags) == 1:
        return mol
    # 只保留重原子数达标的片段中最大的那个
    valid = [f for f in frags if f.GetNumHeavyAtoms() >= min_heavy_atoms]
    if not valid:
        return None
    if len(valid) > 1:
        # 有多个「大」片段 —— 可能是共价二聚体或真实共晶,需人工确认
        pass
    return max(valid, key=lambda f: f.GetNumHeavyAtoms())

版本一致性:容易被忽视的坑

不同 RDKit 版本的标准化行为有过变化(互变异构规则、Uncharger 的处理逻辑等都调整过)。后果是:

  • 半年前清洗的数据集,用新版本重跑得到不同的标准化 SMILES;
  • 历史模型的特征与新数据的特征对不上;
  • 去重结果不一致,可能引入泄漏。
# 应对:在数据集中记录处理时的版本
import rdkit
import json, hashlib

meta = {
    "rdkit_version": rdkit.__version__,
    "pipeline": ["Cleanup", "LargestFragmentChooser",
                 "Uncharger", "TautomerCanonicalize"],
    "n_input": n_in, "n_output": n_out,
}
json.dump(meta, open("dataset_meta.json", "w"), indent=2)

# 并在环境中锁定版本
# requirements.txt: rdkit==2024.03.5

标准化后的验证

# 抽样人工检查 —— 这一步不能省
import random
sample = random.sample(list(zip(orig_smiles, clean_smiles)), 20)
for o, c in sample:
    if o != c:
        print(f"{o}\n  → {c}\n")
# 重点看:有没有把重要结构改掉?立体化学是否保持?

# 自动检查:分子式是否只在预期的方式上变化
from rdkit.Chem.rdMolDescriptors import CalcMolFormula
# 去盐会改变分子式(正常),但重原子骨架不应变化

常见坑与提示

  • LargestFragmentChooser 会无差别删小片段,共价二聚体和有意义的共晶组分要特别处理;
  • 互变异构规范化保证一致性而非生理正确性
  • 锁定并记录 RDKit 版本,跨版本标准化结果可能不同;
  • 标准化后务必抽样人工检查,自动流程不能盲信。

延伸资源