公开数据集:Tox21、ClinTox、SIDER、PubChem BioAssay——数据获取、清洗与格式转换
做ADMET深度学习,说白了第一步就是找对数据。很多人一上来就堆模型,调参,结果发现效果差,回头一看是数据源选错了或者清洗时埋了坑。我今天就聊聊这四个我反复用过的公开数据集——Tox21、ClinTox、SIDER、PubChem BioAssay,怎么拿、怎么洗、怎么转成模型能吃的格式。
四个数据集到底长什么样?
先看一眼它们各自的“身份标签”吧。我个人习惯用一张表来记关键区别:
| 数据集 | 任务类型 | 样本量(约) | 数据格式 | 主要难点 |
|---|---|---|---|---|
| Tox21 | 多任务分类(12个靶点) | ~8000 | CSV + SDF | 极度稀疏标签 |
| ClinTox | 二分类(临床毒性/失败原因) | ~1500 | CSV | 正样本极少 |
| SIDER | 多标签分类(药物副作用) | ~1400 | TSV | 标签维度高,严重不平衡 |
| 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,清洗也做过一轮,适合快速验证想法。
数据清洗:坑在哪里?
数据清洗是重头戏。我按数据集的顺序讲几个关键点。
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)]
pubchempy库来直接按AID编号下载CSV,比自己爬网页稳定得多:pip install pubchempy,然后compounds = pubchempy.get_compounds('aspirin', namespace='name')。不过注意速率限制。
格式转换:统一成模型能吃的样子
清洗完之后,四个数据集的格式五花八门:有的CSV,有的TSV,有的SDF。我们需要统一成特征矩阵 + 标签矩阵的形式。我的标准流程是:
- 分子表示:将SMILES转成分子图(Graph)或分子指纹(Morgan Fingerprint)。我通常先用
ECFP4(半径2,2048bit)做基线,方便和传统方法对比。 - 标签对齐:不同数据集的任务数不同,我统一存储为
numpy.array,shape为(n_samples, n_tasks),缺失标签为NaN。 - 外部验证集:每个数据集我都单独保留一个外部测试集(比如从官方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:四数据集处理流程
下面这张图概括了整个数据获取→清洗→转换的流程,你看看就知道了:
避坑指南 & 个人经验总结
最后总结几个我踩过的坑,希望你们能绕开:
- 千万不要直接合并四个数据集:它们的标签空间完全不一样,强行合并会引入无关特征。我建议对每个数据集单独训练模型,或者做多任务学习时保留各自的任务头。
- 注意数据泄露:SIDER和ClinTox里有些分子是重复的(比如同一个药物在不同实验中)。我曾在清洗过程中误删了重复条目,导致测试集里出现训练集样本。所以重复检测时一定要基于SMILES标准化后再去重。
- 标签不平衡的预处理:我习惯在清洗后统计每个任务的正负样本比例,并记录在元数据文件里。这样后续做重采样或加权loss时直接引用,不用每次重新算。
- 保存中间结果:清洗过的数据建议保存为
.npz或.h5,这样可以快速加载,不用每次都重新跑指纹计算。我在做大规模PubChem数据时,一次计算指纹就要花几小时,保存下来后多次实验能省掉80%的时间。
# 保存中间结果示例
np.savez_compressed('tox21_processed.npz', X=X, y=y, task_names=task_names)
data_report.txt的习惯,记下每个任务的样本数和阳性率,方便后续排查。