数据收集中的常见错误:从源头把模型带偏
做ADMET预测模型,最怕什么?我最怕数据本身就有问题。数据是模型的「食物」,你喂进去的是馊饭,就别指望它能给你做出满汉全席来。这一章我想聊聊数据收集阶段最容易踩的三个坑:量不够、选错库、整合出乱子。都是我这些年亲手填过的坑。
数据量不足与不平衡
先说数据量。我见过太多人,手里只有一两百个化合物,就开始训深度学习模型。你想想看,ADMET预测本质上是高维化学空间里的拟合问题,数据量少了,模型根本学不到通用特征,只会「死记硬背」那几百个样本。
我记得有次接手一个CYP抑制模型,对方说「数据集很干净」。我一看,一共380个样本,活性样本只有56个。好嘛,这模型训出来之后准确率看着挺高——0.92,但一查混淆矩阵,它把所有的样本都预测成了非活性。这种「假准确率」我见得太多了。
我的经验阈值:
- 分类模型:至少1000个样本,活性类不少于200个
- 回归模型:至少500个样本,且覆盖整个活性范围
- 深度学习模型:至少5000个样本起步
不平衡的问题更隐蔽。我做过一个hERG毒性预测模型,训练集里阳性样本只占3%。尝试了SMOTE过采样后,内部验证AUC达到0.88,心里还挺美。结果在外部测试集上一测,AUC直接掉到0.62。为什么?因为合成样本引入了偏差,模型学到的不是真实的毒性特征,而是人工模式。
我的建议:处理不平衡时不要只用一种方法。我会同时做几个实验——欠采样、过采样、加权重、异常检测,然后看它们在外部验证集上的表现。别迷信AUC,多看看精确率-召回率曲线。
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 欠采样 | 数据总量较大时 | 可能丢失重要信息 |
| 过采样 | 数据量不足时 | 容易过拟合合成样本 |
| 加权重 | 用于损失函数 | 权重设置需调参 |
| 异常检测 | 极端不平衡 | 模型选择有限 |
数据库选择不当
选数据库这件事,很多人不重视。我从DrugBank、ChEMBL、PubChem这三个库里都拉过数据,它们的质量差异大得很。ChEMBL的IC50值经过人工审核,非活性标记比较可靠。PubChem的数据量大,但很多是HTS的初筛结果,假阳性率偏高。DrugBank适合找已知药物,但数据稀疏,很多ADMET endpoints只有几十条记录。
有一次我想建Caco-2渗透性模型,在PubChem里找到了4000条记录,兴奋得不行。后来一看,60%的数据是同一个实验室用同一种方法测的。这叫什么?这叫「伪多样」——数据虽然多,但化学结构多样性低,模型的泛化能力会大打折扣。
我曾经踩过的坑:千万不要直接从数据库里拉完数据就跑模型。你至少要做这四件事:
- 检查每条数据的来源文献和实验室
- 查看数据的时间分布——陈旧数据的测定标准可能不同
- 分析化学结构多样性——用Tanimoto相似度矩阵看一下
- 确认活性值单位是否统一——pIC50、IC50、Ki不要混用
说到单位,我曾经在整合数据时吃过亏。一个项目从三篇文献里收集了500条logD数据,批处理的时候没注意单位,结果把mL/min/mg和µL/min/mg混在了一起。建模之后预测值始终偏大一两个数量级,查了两天才发现是单位换算的问题。从那以后我养成了一个习惯:
# 我会强制标准化所有活性值
def standardize_activity(value, unit, target_unit='uM'):
conversions = {
'nM': 0.001,
'uM': 1,
'mM': 1000,
'pM': 0.000001
}
if unit.lower() != target_unit.lower():
value *= conversions.get(unit.lower(), 1)
return -math.log10(value * 1e-6) # 统一转为pIC50
外部数据整合问题
外部数据整合,说白了就是「混数据」。很多人觉得这是个苦力活,交给实习生干就行了。嗯,我当年也这么想,直到发现实习生把两篇文献的Ames test数据按照「mutagenic/non-mutagenic」硬编码匹配,但其实一篇用的标准是「positive/negative」,另一篇用的「mutagen/non-mutagen」,编码方式不一样,最后整合出来的数据全是错的。
数据整合最大的挑战是标准化。我做过一个项目,需要整合来自4个数据库的10个ADMET endpoints。你以为只是把表拼接起来?实际上每张表的结构都不一样:
# 数据库A: 宽表格式
Compound_ID, logD, logP, Solubility
C001, 2.5, 3.1, 45.2
# 数据库B: 长表格式
Compound_ID, Property, Value
C001, logD, 2.5
C001, logP, 3.1
C001, Solubility, 45.2
# 数据库C: 有附加列
Compound_ID, logD, logP, pH, Temperature
C001, 2.5, 3.1, 7.4, 25
还有批次效应。同一化合物在不同实验室测定,结果可能差很大。这不是数据错了,是实验误差和条件差异。怎么处理?我的做法是:
- 记录每批数据的来源标记
- 用ComBat算法做跨批次校正
- 如果某一批次数据量太少(<10条),直接舍弃
我强烈建议你在整合后的数据集中保留一个「source」列。这在后面的模型验证阶段帮了我大忙——我可以按数据来源分群做验证,看看模型是不是只对某个来源的数据表现好,而对其他来源表现差。如果出现这种情况,说明模型学到了数据库特有的模式,而非真正的化学特征。
最后提一下数据去重。你以为去重就是删掉重复的SMILES?没那么简单。同一个分子在不同数据库里可能有不同的表示形式——半角全角括号、Kekulé结构和芳香结构的差异都会导致SMILES不一致。不管是在项目开始还是开发过程中,我都会在程序里跑一遍标准化脚本,用RDKit做Canonical SMILES转换,确保同一化合物只保留一个身份。
from rdkit import Chem
from rdkit.Chem import MolFromSmiles, MolToSmiles
def standardize_smiles(smiles):
mol = MolFromSmiles(smiles)
if mol is None:
return None
# 中和电荷、调整互变异构
mol = Chem.RemoveHs(mol)
return MolToSmiles(mol, canonical=True, isomericSmiles=False)
说这么多,其实核心就一句话:数据收集是建模的根基,在这个阶段偷的懒,会在后面十倍百倍地还回来。数据集越小、越脏、越乱,模型就越不可靠。我宁愿花80%的时间在数据上,只花20%的时间调模型,也不愿反过来。