第4节 分子描述符计算与一致性验证归纳总结
分子描述符计算,说白了就是把分子结构变成数字。这些数字就是模型的“食材”——食材不新鲜,菜就做不好。我在ADMET项目中见过太多次了,描述符算错了,后面所有工作全白搭。
这一节,我把分子描述符计算和一致性验证的核心逻辑做个归纳。咱们不聊虚的,只讲你实际干活时用得上的东西。
1. 描述符计算的三个核心维度
我个人习惯把描述符计算拆成三个维度来看。你想想看,任何一个描述符计算任务,只要这三个维度没问题,结果基本靠谱。
| 维度 | 关注点 | 常见问题 |
|---|---|---|
| 化学空间覆盖 | 描述符能否区分不同分子? | 全是零或常数值,白算 |
| 计算可重复性 | 同一分子两次算的结果一样吗? | 工具版本不同结果不同 |
| 物理意义可解释性 | 这个数字到底代表什么? | 负的logP、超过100的TPSA |
2. 主流工具与差异分析
目前常用的工具就那几个:RDKit、PaDEL、Mordred。我在项目里试过把它们全跑一遍,结果发现差异比想象中大。
| 工具 | 描述符数量 | 速度 | 维护状态 |
|---|---|---|---|
| RDKit | ~200(2D) | 快 | 活跃,社区大 |
| PaDEL | ~1800(包含3D) | 中等 | 维护放缓 |
| Mordred | ~1800 | 快 | 活跃 |
为什么会这样?同一个logP,RDKit算的是基于原子贡献的ClogP,PaDEL算的可能包括不同校正项。你直接拿两个工具的结果做混合建模,不出问题才怪。
3. 一致性验证的三种方法
验证方法不用太花哨,实用就行。我常用的就三种,覆盖了从单个分子到整批数据的场景。
方法一:多工具交叉验证
选10~20个结构差异大的分子,用两个工具分别算,对比相同描述符的数值。
# 示例:RDKit vs Mordred 交叉比对脚本片段
from rdkit import Chem
from mordred import Calculator, descriptors
def cross_validate(smiles_list, desc_name='SLogP'):
rdkit_vals = []
mordred_vals = []
for smi in smiles_list:
mol = Chem.MolFromSmiles(smi)
# RDKit 计算
from rdkit.Chem import Descriptors
rdkit_vals.append(Descriptors.MolLogP(mol))
# Mordred 计算
calc = Calculator(descriptors.SLogP)
mordred_vals.append(calc(mol))
return rdkit_vals, mordred_vals
方法二:批处理统计检验
对整批数据,计算每个描述符的均值、标准差、缺失率。两次计算之间的差异超过阈值就报警。
| 检查项 | 阈值参考 | 触发动作 |
|---|---|---|
| 均值漂移 | 超过 5% | 检查工具版本 |
| 缺失率变化 | 超过 1% | 检查分子标准化 |
| 极值异常 | 超出 3σ | 单独复核分子结构 |
方法三:边界值与异常检测
故意构造一些边界分子——比如极端大环、含重金属原子、带电荷的片段。这些分子容易暴露出描述符计算的bug。
核心逻辑: 一致性验证不是为了证明“两个工具算得一样”,而是为了发现“哪里不一样,为什么不一样,影响有多大”。
4. 常见问题与排查经验
这部分全是真金白银的实战教训。我自己踩过的坑,给你列出来,别再走一遍。
问题1:版本差异导致描述符漂移
我曾经在同一个项目里,用RDKit 2020.03算了一批描述符,半年后用2022.09重算,发现SLogP有系统性的0.1偏移。虽然不大,但对某些高精度模型来说,这0.1足以改变排序。
解决方案:固定工具版本,并在项目文档里写明。
# 记录工具版本信息
# environment.yaml 示例
name: admet_desc
channels:
- conda-forge
dependencies:
- rdkit=2023.09
- pandas=2.1.0
- numpy=1.25.0
问题2:分子标准化不一致
同一个SMILES,有的工具自动去盐,有的保留盐,有的做tautomer归一化。结果算出来的描述符能差20%。
我的做法:在计算之前,统一走一遍分子标准化管线。
def standardize_mol(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 去盐
from rdkit.Chem import SaltRemover
remover = SaltRemover.SaltRemover()
mol = remover.StripMol(mol)
# 电荷中和
from rdkit.Chem import rdMolDescriptors
mol = rdMolDescriptors.RemoveUncharged(mol)
return mol
问题3:缺失值与异常值处理
有些描述符对特定结构算不出来,返回NaN或inf。如果你不处理,模型直接报错。
| 描述符类型 | NaN原因 | 处理方式 |
|---|---|---|
| 3D描述符 | 构象生成失败 | 回退到2D描述符 |
| 带电描述符 | 分子不含可电离基团 | 填充0或中位数 |
| 拓扑描述符 | 分子包含非标准原子 | 标记后剔除或替换 |
5. 与SVA验证的衔接
SVA验证需要可靠的输入。如果描述符计算这一步有抖动,SVA的结论就是空中楼阁。
我在项目中是这样衔接的:
- 描述符计算完成后,先跑一致性验证,通过后再交给SVA管线
- SVA的“输入验证”环节,加入描述符范围检查(比如logP是否在合理区间)
- 如果SVA验证失败,先回溯到描述符计算,排除数据来源问题
归纳一下,分子描述符计算不是跑个脚本就完事的事。工具选型、版本管理、标准化管线、一致性验证,这几个环节缺一不可。把这些基础打牢了,后面的ADMET预测和SVA验证才能站得住脚。