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可解析? 标记为无效 原子价检查 通过? N_invalid += 1 N_valid += 1 有效性 = N_valid / N_generated N_generated = N_valid + N_invalid 💡 关键点 原子价检查是无效分子的主要来源。建议先用 SMILES 解析做初步过滤,再用原子价检查做二次筛选。

流程图看起来很复杂,其实逻辑很简单:走不通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预测就都没法搞了。记住我踩过的坑:别为了推高有效性而牺牲其他指标,找到平衡点才是关键。