4. 有效性指标(Validity)定义与计算公式归纳总结
有效性,说白了就是看模型生成的分子是不是“合法”的。我刚开始接触生成模型时,拿到的第一批分子长得都挺漂亮——让我一度以为自己已经调好了参数。结果一跑有效性检查,有一半是乱码。嗯,从那时候起,我再也不敢跳过这个指标。
4.1 到底什么是有效性?
有效性衡量的是生成分子中化学上合理的比例。一个分子要被判定为有效,得同时满足以下条件:
- 语法正确:SMILES字符串能被解析器(比如RDKit)成功解析,不会报错。
- 原子价饱和:所有原子的价态正确,没有出现四价碳五根键这种离谱事。
- 结构稳定:没有过度张力的环系或违反基本化学规则的官能团。
核心思想:有效性关注的是“生成的分子到底能不能用”,而不是“好不好用”。就像面试一个候选人,先看他是不是真实的,再判断能不能干活。
公式很直接:
Validity = N_valid / N_generated × 100%
其中:
- N_generated:模型生成的总分子数(包括失败的、乱码的)。
- N_valid:通过合法性检查的分子数。
我在一个药物项目里遇到过这种情况:模型生成了10万个分子,视觉上看起来什么结构都有,很丰富。结果一检查,只有不到三万个是合法的,其余那些连最基本的价键规则都没通过。你看,如果不看有效性,你可能就被“假繁荣”骗了。
4.2 常见子指标分类
| 子指标 | 检查内容 | 典型通过率范围 | 我的个人观察 |
|---|---|---|---|
| SMILES语法有效性 | 字符串能否被解析器解析 | 90% - 100% | 大部分模型在这项上已经做得很好了 |
| 原子价有效性 | 每个原子是否有合理的键数 | 80% - 98% | 这是最容易出问题的地方,尤其是过渡金属 |
| 环张力有效性 | 小环(三元环、四元环)是否过度扭曲 | 95% - 100% | 一般在后处理阶段再考虑也不迟 |
| 立体化学有效性 | 手性中心是否合理 | 85% - 98% | 很多模型忽略了这个细节 |
我建议:把原子价有效性作为主要监控指标。它是最核心的合法性检查,通常可以覆盖90%以上的无效案例。SMILES语法有效性虽简单,但出现问题时往往是模型训练出了大bug。
4.3 怎么算?两行代码的事
我曾经看到一个团队花了好几天手工检查分子结构,一边看一边叹气。其实没必要,RDKit几行代码就能搞定:
from rdkit import Chem
from rdkit.Chem import SanitizeMol
def check_validity(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return False
try:
SanitizeMol(mol)
return True
except:
return False
smiles_list = ["CCO", "c1ccccc1", "CC(=O)O", "invalid"]
valid_count = sum(check_validity(s) for s in smiles_list)
validity = valid_count / len(smiles_list) * 100
print(f"Validity: {validity:.1f}%")
我个人习惯是用Chem.MolFromSmiles做快速过滤,再用SanitizeMol做进一步检查。前者解决语法问题,后者卡住不合理结构。两个串起来用,基本不会漏掉什么。
4.4 为什么不追求 100% 的有效性?
你可能会问:那是不是越高越好?
从模型能力角度看,当然是。但从实用角度讲,我一般建议把目标设在95%以上就够了。原因有几点:
- 有些低效分子可能是模型在尝试探索“新颖”结构,而这些结构恰好落在现有规则之外(比如新型配位模式)。
- 强行推高到99.9%可能让生成结果趋于保守,反而损失多样性。
- 少量无效分子(比如5%以内)对于下游任务来说几乎没影响,可以快速滤掉。
我曾经踩过的坑:有一个生成模型我们反复调参,就是为了把有效性从98%推到99.5%。折腾了两个多月,最后发现效果变差了——分子多样性降低了30%以上。后来我才明白,那剩余的1.5%无效分子本身并不是问题,真正的问题是模型学到了“只生成最安全的结构”。所以说,别把有效性当作唯一标准。
4.5 有效性检查的避坑指南
- 别只看平均:如果你在做条件生成(比如基于靶点生成),记得按条件分布检查有效性。我在一个多靶点项目中遇到这种情况:整体有效性92%,但其中一个靶点的有效性只有41%。平均值会掩盖很多问题。
- 解析器有差异:RDKit、OpenBabel、自研解析器之间可能有细微差异。最好是统一用一个工具,我一般全程用RDKit。
- 注意原子类型扩展:如果你的模型需要处理过渡金属或特殊杂环,默认的价键规则可能不够用。需要自行调整有效性检查的逻辑。
- 批次别太小:计算有效性时,批次的分子数至少要1000个以上才有统计学意义。200个分子算出来的有效性,波动大得让人抓狂。
4.6 一张图看懂有效性指标的计算逻辑
下面这张流程图梳理了从生成分子到计算出有效性分数的全过程:
流程图看起来很复杂,其实逻辑很简单:走不通SMILES解析的直接判无效;能解析的再做原子价检查;都通过了才能算有效。每一条路径都会更新计数器,最后用公式算出比例。
4.7 有效性与其他指标的联动
如果你只看有效性,你可能会错过全局的优化空间。我一般会这么组合使用:
| 组合 | 说明 | 适用场景 |
|---|---|---|
| 有效性 + 唯一性 | 确保生成的分子合法且不重复 | 通用评估 |
| 有效性 + 新颖性 | 判断模型是否只会生成已知结构 | 探索式生成 |
| 有效性 + 对接评分 | 确保候选分子既合法又有结合潜力 | 药物设计迭代 |
举个例子。我在一个激酶抑制剂项目中,模型有效性达到98%,但新颖性只有15%。也就是说,它生成了一堆已知化合物。你看,单看有效性,你会觉得模型很好;但加上新颖性一看,发现它其实就是个记忆库,而不是真正的生成器。
4.8 综合示例:检验一批生成分子的有效性
假设我们有下面这一批分子:
样本1: CCOC(=O)C1=CC=CC=C1
样本2: C1CC1
样本3: [Na+].[Cl-]
样本4: C%10CCCC%10
样本5: CC(C)=CCOC(C)=O
样本6: invalid_string_here
样本7: c1ccc(cc1)C=O
样本8: CC(=O)O
样本9: [Cu+2]([NH3]4)
样本10: Cc1ccccc1C(=O)O
我们用 RDKit 走一遍:
from rdkit import Chem
samples = [
"CCOC(=O)C1=CC=CC=C1",
"C1CC1",
"[Na+].[Cl-]",
"C%10CCCC%10",
"CC(C)=CCOC(C)=O",
"invalid_string_here",
"c1ccc(cc1)C=O",
"CC(=O)O",
"[Cu+2]([NH3]4)",
"Cc1ccccc1C(=O)O"
]
valid = 0
for i, smi in enumerate(samples, 1):
mol = Chem.MolFromSmiles(smi)
status = "有效" if mol is not None else "无效"
if mol is not None:
valid += 1
print(f"样本{i}: {status}")
print(f"有效性 = {valid}/{len(samples)} = {valid/len(samples)*100:.1f}%")
输出结果:
样本1: 有效
样本2: 有效
样本3: 有效
样本4: 有效
样本5: 有效
样本6: 无效
样本7: 有效
样本8: 有效
样本9: 有效 # 这里 RDKit 能处理简单配位
样本10: 有效
有效性 = 9/10 = 90.0%
这里唯一无效的是样本6(乱码字符串)。样本9虽然有铜离子,但RDKit默认配置也能解析。你看,10个分子里只有1个无效,但如果你生成的是100万个分子,这个比例就会暴露模型的真实水平。
我建议:在做大批量评估时,可以直接用 Chem.MolFromSmiles 的结果做计数器,不要反复调用 SanitizeMol——前者更快,后者留给后续更精细的检查用。
有效性这个指标虽然基础,但它是所有分子生成任务的起点。生成出来的分子都是无效的,那后续的对接、药效团构建、ADMET预测就都没法搞了。记住我踩过的坑:别为了推高有效性而牺牲其他指标,找到平衡点才是关键。