第4章 数据获取与预处理:从公开库到干净数据集

搞分子生成,第一步就是把数据拿到手。别看这一步基础,它直接决定了你模型的上限。我自己刚入行那会儿,以为只要从数据库里拉个文件就能训,结果模型输出一堆乱七八糟的分子——后来发现是数据里藏着大量盐、错键、无效SMILES。嗯,从那以后我养成了一个习惯:拿到数据先不急着训,先花时间清洗。

这一章我们就聊聊怎么从三个主流公开数据库——ChEMBL、PubChem、ZINC——下载数据,然后做清洗、去重、标准化。说白了,就是把你手头的原始材料变成一块能直接喂给模型的干净“食材”。

4.1 数据源怎么选?

三个库各有侧重,我一般这样挑:想搞药物相关的,优先走ChEMBL;想要覆盖全面、分子数量大的,PubChem;如果是虚拟筛选或者想用买得着的数据,ZINC最合适。下面这个表格能帮你快速对号入座:

数据库 特点 典型规模 下载方式
ChEMBL 标注活性数据,偏向药物 ~200万化合物 ftp / SDF / API
PubChem 全面,含文献和专利来源 1.1亿+ FTP / REST PUG
ZINC 可购买/可合成,带物理属性 ~2.3亿(子集) torrent / curated lists
个人习惯: 如果只是做分子生成预训练,我通常首选ChEMBL25+版本,因为它的活性标注能帮你筛选掉一堆无活性的噪声。PubChem太大,全量下载后预处理时间比你训练还长。

4.2 下载实操

最省心的方式是用Python直接调API。下面是我常用的一段脚本,从ChEMBL里拉取指定assay的数据。注意,API有速率限制,所以我加了个sleep:

import requests
import time
import json

# 拉取ChEMBL化合物
url = "https://www.ebi.ac.uk/chembl/api/data/molecule"
params = {"limit": 100, "format": "json"}
headers = {"User-Agent": "MoleculeGen/1.0"}

response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
    data = response.json()
    molecules = data["molecules"]
    print(f"获取到{len(molecules)}条记录")
else:
    print(f"请求失败:{response.status_code}")
time.sleep(1)  # 礼貌性等待

如果是批量下载全库,我建议直接走FTP。ChEMBL提供了压缩好的SDF文件,一行wget搞定:

wget ftp://ftp.ebi.ac.uk/pub/databases/chembl/ChEMBLdb/latest/chembl_33.sdf.gz
gunzip chembl_33.sdf.gz

PubChem那边用PUG REST API也行,但文档稍微绕一点。我的经验是:能用批量下载的就别用逐条API,后者容易断线。ZINC官网提供按子集划分的tar包,下载后解压即可,我前阵子跑虚拟筛选就用ZINC20的clean子集,省了不少洗数据的时间。

4.3 数据清洗:把脏东西筛出去

你下载下来的原始文件里什么都有:空结构、包含错误的SMILES、盐、溶剂分子、超长链。我记得有一次,ChEMBL里居然混进了一条SMILES是"Cl"——那是氯离子,哪能算化合物?下面是我常做的三步清洗:

  • 基本完整性检查: 跳过空行、缺失的SMILES、格式错误的序列。用rdkit.Chem.MolFromSmiles一跑,返回None的直接扔掉。
  • 去除盐和溶剂: 用rdkit.Chem.SaltRemover把常见的Na⁺、Cl⁻、H₂O等碎片剥离。如果你的模型想学配体结构,这一步必须做。
  • 过滤异常分子: 比如分子量小于50Da的,或者重原子数<5的,大概率是碎片。
曾经踩过的坑: 我一开始直接遍历SDF文件,以为每个条目都有效。后来发现有条目SMILES是空的但理化属性居然还在——这种隐式异常需要额外判断。建议进清洗时先做一次len(molecule)检查。

下面这段代码能帮你做基本的盐移除+合法性过滤:

from rdkit import Chem
from rdkit.Chem import SaltRemover

remover = SaltRemover.SaltRemover()
clean_mols = []

with open("raw.sdf") as f:
    supplier = Chem.SDMolSupplier(f)
    for mol in supplier:
        if mol is None:
            continue
        # 移除盐
        mol = remover.StripMol(mol)
        # 检查分子量
        if mol.GetNumHeavyAtoms() < 5:
            continue
        # 标准化SMILES
        smi = Chem.MolToSmiles(mol, canonical=True)
        clean_mols.append((smi, mol))
print(f"清洗后剩下{len(clean_mols)}个分子")

4.4 去重:同一个分子只留一次

不同数据库之间、或者同库不同版本里,同一个分子可能有多个记录。去重最靠谱的方法是标准化SMILES。说白了,就是把同一个分子的不同表示(比如酸碱互变异构、tautomer)统一成一个规范的字符串。我用的套路:

  1. 用Chem.MolToSmiles(mol, canonical=True, isomericSmiles=False)生成无立体信息的规范SMILES。
  2. 存进一个字典,键就是规范SMILES,值随便。
  3. 最后取字典的.values()。

如果分子数量太大(比如几亿个),内存存不下,可以换用布隆过滤器(Bloom Filter)做近似去重,但需要容忍极低的误判率。我个人还是倾向于分批用哈希做精确去重,反正现在内存便宜。

一个实用建议: 除了SMILES,还可以同时用InChI Key做第二次去重。有些分子SMILES不同但InChI Key相同——比如互变异构体。我一般在去重管道里跑两遍:先SMILES去重,再InChI Key去重,双重保险。

4.5 标准化:让数据整齐划一

标准化不是一个固定的步骤,它取决于你后续要做什么。但有一条底线:所有分子必须用同一个表示形式。我会做以下几件事:

  • 电荷标准化: 用Chem.Mol.Standardize把内盐、偶极等处理成中性形式(除非你的任务需要显式保留电荷)。
  • 去除同位素和立体信息: 除非你搞手性生成,否则把同位素标记和气密性信息去掉,减少过于细致的差异。
  • 键序规范化: 芳香键统一用Kekulé表示还是大π表示?我一般保持Kekulé,因为有些模型处理芳香键有歧义。
  • 结构过滤: 按目标分子量范围、LogP范围、可旋转键数等过滤。比如你要生成类药分子,就按“类药五规则”卡一道。

这段代码展示了用RDKit自定义标准化函数:

def standardize_molecule(mol):
    # 去除同位素
    for atom in mol.GetAtoms():
        atom.SetIsotope(0)
        atom.SetFormalCharge(0 if atom.GetAtomicNum() != 0 else atom.GetFormalCharge())
    # 标准芳香表示
    Chem.Kekulize(mol, clearAromaticFlags=True)
    # 重新生成规范SMILES
    smi = Chem.MolToSmiles(mol, canonical=True, isomericSmiles=False)
    return smi

std_smiles = [standardize_molecule(mol) for mol in molecules]

4.6 整体管道流程图

下面这张图概括了我们刚才聊的整个流程。从数据库的原始文件到最后干净的数据集,每一步都在去除噪声、降低分子空间的冗余度。

数据获取与预处理管道 公开数据库 ChEMBL / PubChem / ZINC 下载 API / FTP / wget 清洗 去盐、过滤异常、空值 去重 规范SMILES / InChI Key 标准化 电荷、芳香、分子量过滤 干净数据集 SMILES + 属性 避坑提醒 API限速、FTP断连 SMILES包含隐式错误 标准顺序影响去重

管道里每一步都不难,但顺序很重要。比如先去重再标准化?你可能会把同一个分子的不同电荷形式当成两个分子。我个人习惯先清洗(去掉那些没法处理的垃圾),然后标准化,最后去重——这样去重时用的SMILES已经是规整的版本,漏掉重复的概率最低。

4.7 一些实用技巧

关于性能: 如果数据量是百万级,用multiprocessing把清洗步骤并行化。我会把SDF文件按块读,每个进程处理2000个分子,最后合并。Python的GIL会在CPU密集的RDKit操作上拖后腿,多进程能带来近线性的加速。

关于存储: 预处理后的数据推荐存成两个文件:一个SMILES文本文件(每行一个,方便快速预览),一个HDF5或Parquet文件(带所有属性,便于后续加载)。我一般用pandas+parquet,读取又快又能保留列式索引。

最后提醒一点:预处理完的数据一定要做一轮抽样人工检查。跑一个可视化脚本,随机挑几百个分子画出来看看,确认没有奇怪的环、异常的原子类型。毕竟机器只能按规则清理,但有些奇怪的东西是规则覆盖不到的——我曾因为一个包含锇的有机金属分子没被过滤掉,导致模型生成了诡异的金属键。嗯,从那以后我再也不敢完全信任自动清洗了。