3、数据集准备与预处理:数据收集、清洗、划分训练集/测试集
好,咱们正式开始动手做ADMET预测模型的第一道工序——数据集。说实在的,做了这么多年项目,我发现一个道理:模型性能的上限,其实80%是由数据质量决定的。调参再努力,如果数据本身就是垃圾,那结果只能是“垃圾进,垃圾出”。
核心要点: 数据预处理不是体力活,是决定模型生死的基石。干净、完整、分布合理的训练集,能让你的模型少走太多弯路。
3.1 数据收集:从哪里找ADMET数据?
数据收集是第一步。我个人习惯先看公开数据库,再考虑自建数据集。
常用的公共数据库:
| 数据库名称 | 内容特点 | 适用场景 |
|---|---|---|
| ChEMBL | 大量化合物活性与ADMET数据 | 分类模型、回归模型的通用底座 |
| PubChem BioAssay | 高通量筛选结果,覆盖多种靶点 | 二分类毒性预测,如AMES毒性 |
| Tox21 / ToxCast | 专门针对毒性端点 | 核受体、应激反应通路预测 |
| DrugBank | 已上市/在研药物信息 | 验证集构建,临床相关性分析 |
我的个人经验: 做Ames致突变性预测时,我通常会同时拉取ChEMBL和Tox21的数据。前者数据量大但噪声多,后者数据质量高但量少。两者合并,再仔细清洗,效果最好。
如果做自建数据集,我建议从企业内部实验报告、文献补充材料或专利数据中提取。注意一点:不同来源的数值单位要统一。比如IC50有的是nM,有的是µM,一旦混用,模型直接完蛋。
# 一个简易的ChEMBL数据拉取示例,用Python
import pandas as pd
from chembl_webresource_client.new_client import new_client
# 获取人源CYP2D6抑制活性数据
target = new_client.target
chembl_target = target.filter(pref_name__iexact='Cytochrome P450 2D6').only(['target_chembl_id'])
target_id = chembl_target[0]['target_chembl_id']
activity = new_client.activity
results = activity.filter(target_chembl_id=target_id, standard_type='IC50').only([
'molecule_chembl_id', 'standard_value', 'standard_units'
])
df = pd.DataFrame.from_records(results)
# 数据量大概几千到上万条,看具体查询范围
3.2 数据清洗:去掉脏数据
数据拉回来后,第一件事不是建模,是清洗。我记得刚入行时曾犯过一个错误,直接拿原始数据训练,结果模型莫名其妙地学到了一些“非活性但结构类似”的偏置——其实就是数据里有大量重复或错误标注。
清洗流程通常包括这几步:
- 去重: 同样的分子结构对应同一个SMILES,只保留一条。
- 规范化: SMILES规范到一种表示(如用RDKit的标准化函数),去掉盐、溶剂等无机部分。
- 缺失值处理: 如果目标值缺失,直接删除该样本;如果特征值缺失,需要额外考虑填充策略。
- 标注一致性: 检查分类任务的标签是否矛盾(同一个分子在不同批次中被标为活性和非活性)。
- 理化过滤: 比如保留分子量、LogP、氢键供体/受体数在一定范围内的合格分子。
# 使用RDKit做数据清洗的片段
from rdkit import Chem
from rdkit.Chem import Descriptors, SaltRemover
def clean_molecule(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None # 无法解析的SMILES,直接剔除
# 去除盐和溶剂
remover = SaltRemover.SaltRemover()
mol = remover.StripMol(mol, dontRemoveEverything=True)
# 标准化
mol = Chem.MolFromSmiles(Chem.MolToSmiles(mol))
# 简单理化过滤:分子量 100-1000,LogP -2到7
mw = Descriptors.MolWt(mol)
logp = Descriptors.MolLogP(mol)
if mw < 100 or mw > 1000 or logp < -2 or logp > 7:
return None
return mol
# 应用到DataFrame
df['clean_mol'] = df['smiles'].apply(clean_molecule)
df = df.dropna(subset=['clean_mol'])
我曾经踩过的坑: 有一次我从PubChem拉了10万条数据,自以为清理得很干净,结果数据里混入了大量的“空白对照”成分(即溶剂DMSO本身的活性数据)。这些分子结构简单,活性和非活性随机分布,直接污染了整个训练集。后来在清洗时,我专门添加了一个步骤:去除分子量低于80或原子数少于5的样本。
3.3 特征工程:把分子“翻译”成机器能懂的语言
清洗完数据,下一步是把分子结构转成特征向量。ADMET预测中,最常用的特征有这几种:
- 分子指纹(Morgan Fingerprints):ECFP/ECFC系列,我个人最喜欢。它们能捕捉官能团和拓扑信息。
- 理化描述符:分子量、LogP、TPSA、氢键供体/受体数、可旋转键数——这些简单粗暴但有效。
- 图神经网络特征:使用图卷积时,直接用原子类型、成键关系作为节点和边特征。
# 生成Morgan指纹(ECFP4)和常见理化描述符
from rdkit.Chem import AllChem
import numpy as np
def compute_features(mol):
# 1024位ECFP4指纹
fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=1024)
fp_arr = np.array(fp)
# 理化描述符
desc = [
Descriptors.MolWt(mol),
Descriptors.MolLogP(mol),
Descriptors.TPSA(mol),
Descriptors.NumHDonors(mol),
Descriptors.NumHAcceptors(mol),
Descriptors.NumRotatableBonds(mol)
]
return np.concatenate([fp_arr, desc])
这里有个注意点:你想想看,如果训练集有1000个分子,每个分子生成了1024+6=1030个特征,那特征维度并不过分。但如果分子数只有几百,特征维度却高得多,就很容易过拟合。我建议至少保持 样本数 : 特征数 ≥ 5:1 的比例,不够就降维或用更短的指纹(如512位)。
3.4 数据划分:训练集/测试集
数据划分看似简单,但里面学问很大。很多新手直接拿随机划分,结果模型在测试集上表现很好,一上真实世界数据就翻车。为什么呢?因为你忽略了数据分布的结构性差异。
常见的划分策略:
| 划分方法 | 做法 | 适用场景 |
|---|---|---|
| 随机划分 | 随机将数据分成训练集(80%)和测试集(20%) | 数据量巨大且分布均匀时 |
| 分层采样 | 按分类标签比例分配 | 分类任务,特别是正负样本不平衡时 |
| 时间序列划分 | 按数据发表日期排序,用早期数据训练,晚期数据测试 | 模拟模型在实际投放中的预测能力 |
| 分子骨架划分 | 按分子的Bemis-Murcko骨架划分 | 评估模型对全新骨架的泛化能力 |
我会这么选: 如果做ADMET分类任务(比如CYP抑制,是/否),我首先用随机+分层采样做基线。对于实战部署,强烈建议增加一个分子骨架划分的测试集——只有这样,你才能知道你的模型是否真的学会了“化学规律”,还是仅仅是记住了已知骨架。
# 分层采样划分示例
from sklearn.model_selection import train_test_split
X = df['features'].tolist() # 特征矩阵
y = df['label'].values # 标签
smiles = df['smiles'].values # 保留SMILES用于溯源
# 80% 训练,20% 测试,保持标签比例
X_train, X_test, y_train, y_test, smi_train, smi_test = train_test_split(
X, y, smiles, test_size=0.2, random_state=42, stratify=y
)
# 检查划分后的正负比例
print(f"训练集: 正例 {sum(y_train==1)}/{len(y_train)} ({sum(y_train==1)/len(y_train):.1%})")
print(f"测试集: 正例 {sum(y_test==1)}/{len(y_test)} ({sum(y_test==1)/len(y_test):.1%})")
注意! 一旦做了特征工程(比如用PCA降维,或用整个数据集的均值和标准差做标准化),一定要先划分训练集/测试集,再用训练集的数据计算均值和标准差,应用到测试集。如果你先做整个数据集的标准化再划分,数据泄漏就产生了,模型评估结果会虚高。
3.5 数据准备全流程
我把整个流程总结成一张图,你一看就明白了:
这套流程我每次做项目都会老老实实走一遍,哪怕是只有几百个分子的数据集。说句实在话,从ChEMBL上直接下数据然后训练,不做这些预处理,模型很可能过拟合到数据源本身的偏差上——比如只有特定机构做的实验才被收录,或者特定类型的分子被反复测试。
3.6 避坑指南小清单
最后,我再快速列几个避坑点。都是我用真金白银的算力和时间换来的教训:
- 不要迷信大型公开数据。我遇到过ChEMBL里有些活性值是通过高通量筛选(HTS)一次跑出来的,单位都是µM,但另一批是低通量验证,单位是nM。两者不统一,合并前必须统一换算并记录转换逻辑。
- SMILES不是越规范越好。 有时候用isomeric SMILES(保留手性信息)和用canonical SMILES效果不同。做ADMET时,我通常去掉手性信息(除非目标任务是立体选择性预测),否则特征维度会爆炸。
- 划分后检查数据泄漏。 比如测试集里有没有和训练集完全相同的分子?用Murcko骨架检查。我以前就发现,有些数据集里同一个分子出现多次,而且每次活性标签不同——这直接导致模型在“背诵”答案。
小结一下: 数据集准备这一步,你投入的时间会直接回报在模型训练效率和最终泛化能力上。不要急着调参,先把数据的家底管好。