第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 |
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的,大概率是碎片。
下面这段代码能帮你做基本的盐移除+合法性过滤:
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)统一成一个规范的字符串。我用的套路:
- 用
Chem.MolToSmiles(mol, canonical=True, isomericSmiles=False)生成无立体信息的规范SMILES。 - 存进一个字典,键就是规范SMILES,值随便。
- 最后取字典的.values()。
如果分子数量太大(比如几亿个),内存存不下,可以换用布隆过滤器(Bloom Filter)做近似去重,但需要容忍极低的误判率。我个人还是倾向于分批用哈希做精确去重,反正现在内存便宜。
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 整体管道流程图
下面这张图概括了我们刚才聊的整个流程。从数据库的原始文件到最后干净的数据集,每一步都在去除噪声、降低分子空间的冗余度。
管道里每一步都不难,但顺序很重要。比如先去重再标准化?你可能会把同一个分子的不同电荷形式当成两个分子。我个人习惯先清洗(去掉那些没法处理的垃圾),然后标准化,最后去重——这样去重时用的SMILES已经是规整的版本,漏掉重复的概率最低。
4.7 一些实用技巧
关于性能: 如果数据量是百万级,用multiprocessing把清洗步骤并行化。我会把SDF文件按块读,每个进程处理2000个分子,最后合并。Python的GIL会在CPU密集的RDKit操作上拖后腿,多进程能带来近线性的加速。
关于存储: 预处理后的数据推荐存成两个文件:一个SMILES文本文件(每行一个,方便快速预览),一个HDF5或Parquet文件(带所有属性,便于后续加载)。我一般用pandas+parquet,读取又快又能保留列式索引。
最后提醒一点:预处理完的数据一定要做一轮抽样人工检查。跑一个可视化脚本,随机挑几百个分子画出来看看,确认没有奇怪的环、异常的原子类型。毕竟机器只能按规则清理,但有些奇怪的东西是规则覆盖不到的——我曾因为一个包含锇的有机金属分子没被过滤掉,导致模型生成了诡异的金属键。嗯,从那以后我再也不敢完全信任自动清洗了。