公开数据集:Tox21、ClinTox、SIDER、PubChem BioAssay——数据获取、清洗与格式转换

做ADMET深度学习,说白了第一步就是找对数据。很多人一上来就堆模型,调参,结果发现效果差,回头一看是数据源选错了或者清洗时埋了坑。我今天就聊聊这四个我反复用过的公开数据集——Tox21、ClinTox、SIDER、PubChem BioAssay,怎么拿、怎么洗、怎么转成模型能吃的格式。

四个数据集到底长什么样?

先看一眼它们各自的“身份标签”吧。我个人习惯用一张表来记关键区别:

数据集任务类型样本量(约)数据格式主要难点
Tox21多任务分类(12个靶点)~8000CSV + SDF极度稀疏标签
ClinTox二分类(临床毒性/失败原因)~1500CSV正样本极少
SIDER多标签分类(药物副作用)~1400TSV标签维度高,严重不平衡
PubChem BioAssay单任务/多任务(活性筛选)~10万~100万CSV / SDF / JSON数据量大,噪声多

嗯,这里要注意:不平衡是这四个数据集的共同标签,但表现形式不一样。Tox21里每个任务的活性比例可能只有1%-10%,ClinTox里临床毒性阳性样本不到100条。我刚开始做的时候以为直接用原标签训练就行,结果模型学成了全负分类器。所以下面每一步都跟不平衡有关。

数据获取:别走错路

获取这些数据集,我们一般走官方渠道或者摩力(MoleculeNet)整合后的版本。我个人推荐直接去MoleculeNet拿,因为格式统一,省去很多麻烦。当然,如果你要追溯原始实验细节,还是得去PubChem官网。举个例子,Tox21的官方地址是 Tox21 Challenge(实际不链接),但我用DeepChem的load_tox21()接口更省事。

获取代码我一般这样写:

import deepchem as dc

# Tox21
tox21_tasks, tox21_datasets, _ = dc.molnet.load_tox21()
train, valid, test = tox21_datasets

# ClinTox
clintox_tasks, clintox_datasets, _ = dc.molnet.load_clintox()
# 注意:ClinTox只有train和valid,没有官方test split

# SIDER
sider_tasks, sider_datasets, _ = dc.molnet.load_sider()

# PubChem BioAssay(例如AID 1234)
# 需要先知道AID编号,再通过PubChemREST API或下载CSV

如果你不想依赖DeepChem,也可以用pandas直接读原始CSV。但我觉得DeepChem已经把数据拆成train/valid/test,清洗也做过一轮,适合快速验证想法。

⚠️ 注意: ClinTox官方没有提供test setsplit。我在项目中遇到过好几次,直接用valid当作test去汇报结果,后来发现过拟合严重。我建议自己手动再做一次分层抽样,保留20%作为独立测试集。

数据清洗:坑在哪里?

数据清洗是重头戏。我按数据集的顺序讲几个关键点。

Tox21:标签稀疏与缺失处理

Tox21有12个任务,但每个分子只被部分实验测过。原始数据里未测的标签用NaN表示。你不能简单地补0或补-1,那样会引入偏见。我一般保留NaN,在训练时通过mask机制忽略掉。DeepChem的dc.models.MultitaskClassifier天然支持mask,如果你自己写PyTorch,记得在loss里过滤掉NaN对应的logits。

# 示例:PyTorch中处理NaN标签
def masked_binary_cross_entropy(y_pred, y_true):
    mask = ~torch.isnan(y_true)
    loss = F.binary_cross_entropy_with_logits(y_pred[mask], y_true[mask])
    return loss

另外,每个任务的正样本比例差异也很大。我曾遇到一个任务只有0.5%的阳性,连验证集都看不到一个正例。这时候最好先检查一下任务分布,把过于极端(比如阳性<1%)的任务单独处理,或者考虑不参与训练。

ClinTox:小样本 + 类别极不平衡

ClinTox只有约1500个分子,其中临床毒性(CT_TOX)的正例只有约10%。数据清洗时要注意:原数据里有一些字段是字符串,比如smiles列偶尔会有空格或换行符,需要strip。我建议写一个通用清洗函数:

def clean_clintox_df(df):
    df['smiles'] = df['smiles'].str.strip()
    df = df.dropna(subset=['smiles'])
    # 移除长smiles(可能有问题)
    df = df[df['smiles'].str.len() < 200]
    return df

还有个坑:ClinTox的原始标签是'active'和'inactive'字符串,需要转成0/1。我习惯用LabelEncoder或直接map。

SIDER:高维稀疏标签

SIDER有超过5000种副作用标签,但每个分子只关联少数几种。数据清洗主要做两件事:去除出现次数太少的标签(比如少于5个分子),以及合并一些同义词标签。我通常保留出现频率至少在10次以上的标签。清洗后的标签矩阵非常稀疏,建议用稀疏矩阵存储,否则内存会炸。

from scipy.sparse import csr_matrix

def get_sparse_labels(df, label_columns):
    data = df[label_columns].values
    return csr_matrix(data, dtype=np.float32)

PubChem BioAssay:大数据 + 噪声

PubChem BioAssay的数据量可能非常大,一个assay可能有几十万个化合物。清洗时重点关注:重复分子(同一个CID出现多次)、不一致的活性(同一个分子在不同实验中结果矛盾)、以及结构验证(有些SMILES可能解析失败)。我曾经下载过一个包含10万分子的assay,跑完RDKit后发现5%的SMILES无法解析。所以第一步一定是做合法性校验:

from rdkit import Chem

def validate_smiles(smiles):
    mol = Chem.MolFromSmiles(smiles)
    return mol is not None

# 过滤
df = df[df['smiles'].apply(validate_smiles)]
💡 小技巧: 对于PubChem数据,我推荐使用pubchempy库来直接按AID编号下载CSV,比自己爬网页稳定得多:
pip install pubchempy,然后compounds = pubchempy.get_compounds('aspirin', namespace='name')。不过注意速率限制。

格式转换:统一成模型能吃的样子

清洗完之后,四个数据集的格式五花八门:有的CSV,有的TSV,有的SDF。我们需要统一成特征矩阵 + 标签矩阵的形式。我的标准流程是:

  1. 分子表示:将SMILES转成分子图(Graph)或分子指纹(Morgan Fingerprint)。我通常先用ECFP4(半径2,2048bit)做基线,方便和传统方法对比。
  2. 标签对齐:不同数据集的任务数不同,我统一存储为numpy.array,shape为(n_samples, n_tasks),缺失标签为NaN。
  3. 外部验证集:每个数据集我都单独保留一个外部测试集(比如从官方split里取),不参与任何预处理。

下面是我常用的数据管道函数:

def dataset_to_arrays(smiles_list, labels_df, fp_radius=2, fp_bits=2048):
    from rdkit.Chem import AllChem
    fps = []
    for smi in smiles_list:
        mol = Chem.MolFromSmiles(smi)
        if mol:
            fp = AllChem.GetMorganFingerprintAsBitVect(mol, fp_radius, nBits=fp_bits)
            fps.append(np.array(fp, dtype=np.float32))
        else:
            fps.append(None)  # 标记无效
    valid_idx = [i for i, fp in enumerate(fps) if fp is not None]
    X = np.stack([fps[i] for i in valid_idx], axis=0)
    y = labels_df.iloc[valid_idx].values.astype(np.float32)
    return X, y, valid_idx

SVG:四数据集处理流程

下面这张图概括了整个数据获取→清洗→转换的流程,你看看就知道了:

四个ADMET公开数据集的处理流程 数据获取→清洗→格式转换流程 Tox21 ClinTox SIDER PubChem BioAssay DeepChem MoleculeNet PubChemREST CSV/JSON下载 数据清洗 • 去重、去非法SMILES • 标签稀疏/缺失处理 • 任务筛选(稀有标签过滤) 格式转换 → 指纹矩阵 + 标签矩阵 输出格式 X: (n, 2048) fp y: (n, n_tasks) 含NaN 保存为 .npz

避坑指南 & 个人经验总结

最后总结几个我踩过的坑,希望你们能绕开:

  • 千万不要直接合并四个数据集:它们的标签空间完全不一样,强行合并会引入无关特征。我建议对每个数据集单独训练模型,或者做多任务学习时保留各自的任务头。
  • 注意数据泄露:SIDER和ClinTox里有些分子是重复的(比如同一个药物在不同实验中)。我曾在清洗过程中误删了重复条目,导致测试集里出现训练集样本。所以重复检测时一定要基于SMILES标准化后再去重。
  • 标签不平衡的预处理:我习惯在清洗后统计每个任务的正负样本比例,并记录在元数据文件里。这样后续做重采样或加权loss时直接引用,不用每次重新算。
  • 保存中间结果:清洗过的数据建议保存为.npz或.h5,这样可以快速加载,不用每次都重新跑指纹计算。我在做大规模PubChem数据时,一次计算指纹就要花几小时,保存下来后多次实验能省掉80%的时间。
# 保存中间结果示例
np.savez_compressed('tox21_processed.npz', X=X, y=y, task_names=task_names)
🔑 核心要点: 四个公开数据集各有脾气,但数据处理的核心思路一致——标准化SMILES、处理缺失标签、控制不平衡。先用DeepChem/MoleculeNet快速上手,然后再深入原始数据做定制化清洗。我在项目里经历了几次“训练时没问题,测试时崩盘”之后,终于养成了每次清理数据都写一份data_report.txt的习惯,记下每个任务的样本数和阳性率,方便后续排查。