2. 分子图数据构建与清洗

分子图模型的第一道工程关口不是模型结构,而是数据。一个分子在磁盘上可能以 SMILES 字符串、SDF 记录块或 MOL2 文本存在,但模型真正消费的是“节点特征矩阵 + 边索引 + 边特征”这一组张量。本节围绕这条转换链展开:先解析成带三维或二维坐标的分子对象,再抽取原子与键特征,最后做去重、标准化与缺失值处理,得到可训练、可复现的图数据集。

一、三种格式的解析差异与统一中间表示

SMILES 是线性字符串,解析快、体积小,但它默认只携带拓扑与元素信息,不含显式氢原子和坐标。以 RDKit 为例,Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)O") 得到的是重原子图,氢原子被隐式处理,需要 Chem.AddHs() 才能显式化。SDF 是记录式格式,每条记录包含原子块、键块和属性块,通常带 2D 或 3D 坐标,适合直接构建带几何信息的图。MOL2 常见于对接与动力学输出,原子类型采用 Tripos 命名,键级信息有时缺失,需要按距离或化学规则补全。

工程上不建议让下游代码直接面对三种格式,而应统一为中间表示:原子列表(元素、坐标、形式电荷、芳香性、手性)、键列表(起止原子索引、键级、是否共轭、是否成环)、以及分子级属性(分子量、logP、来源 ID)。这样后续特征工程只写一套逻辑。

from rdkit import Chem

def parse_any(path):
    if path.endswith(".sdf"):
        supp = Chem.SDMolSupplier(path, removeHs=False)
        return [m for m in supp if m is not None]
    if path.endswith(".mol2"):
        return [Chem.MolFromMol2File(path, removeHs=False)]
    if path.endswith(".smi"):
        mols = []
        for line in open(path):
            smi = line.split()[0]
            m = Chem.MolFromSmiles(smi)
            if m: mols.append(Chem.AddHs(m))
        return mols
    raise ValueError("unsupported format")

注意 removeHs=False 与 AddHs() 的取舍:若模型需要显式氢(如某些反应性预测),必须保留;若只做性质回归,隐式氢可显著减小图规模。这一决定要在数据构建阶段固定,不能训练时再变。

二、原子与键特征工程

原子特征通常包括:元素类别(one-hot 或嵌入索引)、度、形式电荷、杂化类型、是否芳香、是否在环内、手性标签、以及可选的坐标。键特征包括:键级(单/双/三/芳香)、是否共轭、是否成环、立体构型、键长。特征维度一旦确定,就要写入配置并随数据集版本一起保存,否则不同批次数据会因特征顺序不一致而静默出错。

下面是一个可复用的原子特征抽取示例,输出固定长度向量:

ATOM_TYPES = ["C","N","O","S","F","Cl","Br","I","P","Other"]

def atom_features(atom):
    return [
        ATOM_TYPES.index(atom.GetSymbol()) if atom.GetSymbol() in ATOM_TYPES else ATOM_TYPES.index("Other"),
        atom.GetDegree(),
        atom.GetFormalCharge(),
        int(atom.GetIsAromatic()),
        int(atom.IsInRing()),
        int(atom.GetChiralTag()),
    ]

键特征同理,用 bond.GetBondType()、bond.GetIsConjugated()、bond.IsInRing() 映射为整数或 one-hot。若使用 3D 坐标,键长可由两原子坐标欧氏距离计算,并做标准化(减去均值、除以标准差),否则不同来源的坐标尺度差异会干扰模型。

分子图数据构建与清洗链路 SMILES 拓扑 / 无坐标 SDF 原子块+键块+坐标 MOL2 Tripos 类型 / 键级可能缺失 统一解析器 Mol 对象 显式/隐式氢 坐标保留 原子特征 元素/度/电荷/芳香/环 键特征 键级/共轭/成环/键长 去重与标准化 InChIKey / 盐剥离 / 电荷中和 缺失值处理 丢弃 / 补全 / 掩码标记 图数据集 节点特征 + 边索引 + 边特征

三、去重、标准化与缺失值处理

去重不能只比较 SMILES 字符串,因为同一分子可能有多种合法写法。推荐用 InChIKey 作为唯一标识:Chem.MolToInchiKey(mol)。若数据来自多个供应商,还要先做盐剥离与电荷中和,否则同一活性分子的盐酸盐与游离碱会被当成两条记录。标准化流程可固定为:去盐 → 中和电荷 → 统一互变异构体(可选)→ 生成 InChIKey → 按 InChIKey 去重。

缺失值分三类处理。第一类是标签缺失,通常直接丢弃或放入无监督预训练集,不能填 0 后混入回归训练。第二类是原子/键特征缺失,例如 MOL2 缺键级,可用距离阈值补全,或增加“未知”类别并保留掩码位。第三类是坐标缺失,SMILES 来源没有 3D 坐标,若模型依赖几何,需要用 ETKDG 等构象生成方法补齐,并记录“坐标为生成值”这一来源标记,避免与实验晶体结构混用。

不要把缺失的数值特征简单填 0。0 在标准化后往往对应均值附近,会引入虚假信号。更稳妥的做法是增加一个二值掩码维度,让模型自己学习“该特征是否可用”。

四、可复现的数据构建检查清单

为了让数据构建可复现,建议在每次生成数据集时输出一份元数据记录,至少包含:源文件路径与哈希、解析器版本、是否加氢、特征维度与顺序、去重前后数量、标准化规则、缺失值策略、随机种子。下面给出一个可直接参考的记录结构:

{
  "source": "chembl_30.sdf",
  "source_sha256": "a1b2c3...",
  "parser": "rdkit-2023.09",
  "add_hs": true,
  "atom_feat_dim": 6,
  "bond_feat_dim": 4,
  "n_raw": 12000,
  "n_after_dedup": 11340,
  "standardize": ["desalt", "neutralize", "inchikey"],
  "missing_policy": "mask",
  "seed": 42
}

实际执行时,建议先在小样本(如 200 条)上跑通全链路,检查原子数分布、键数分布、特征取值范围是否合理,再放大到全量。常见异常包括:解析失败率突然升高(多为编码或格式混用)、芳香键被误判为单双键交替、以及去重后数量骤降(多为盐剥离规则过激)。这些都要在数据版本发布前定位并记录。

完成本节后,你应当能够把任意来源的分子文件转成统一的图数据集,并说清每一步对最终张量的影响。下一阶段才是在此基础上划分训练/验证/测试集并设计模型。