348

在分子生成中加入 SA Score 约束

SA Score 约束让生成模型产出可合成的分子。这篇讲清 SA Score 的原理、局限、配置方式与更可靠的替代方案。

生成模型最常被诟病的问题是产出「看起来合理但没人能做出来」的分子。加入合成可及性约束是最直接的应对。SA Score 是最常用的指标,但要清楚它的原理与局限。

SA Score 的原理

SA Score(Ertl & Schuffenhauer, 2009)由两部分组成:

  • 片段贡献分:统计 PubChem 中约百万个分子的片段出现频率。常见片段容易合成,罕见片段难。分子中所有片段的频率加权得到基础分。
  • 复杂度惩罚:手性中心数、螺环、桥环、大环、分子尺寸等结构复杂度带来额外惩罚。
  • 最终归一化到 1(易合成)~ 10(难合成)

计算

from rdkit import Chem, RDConfig
import sys, os
sys.path.append(os.path.join(RDConfig.RDContribDir, "SA_Score"))
import sascorer

for smi in [
    "CC(=O)Oc1ccccc1C(=O)O",                       # 阿司匹林
    "CN1C=NC2=C1C(=O)N(C)C(=O)N2C",                # 咖啡因
    "CC12CCC3C(CCc4cc(O)ccc34)C1CCC2O",            # 雌二醇
]:
    mol = Chem.MolFromSmiles(smi)
    print(f"{sascorer.calculateScore(mol):.2f}  {smi}")

# 典型范围:
#   1~3   简单,容易合成
#   3~5   中等
#   5~7   较难
#   7~10  很难或需要特殊方法

在 REINVENT4 中配置

[[stage.scoring.component]]
[stage.scoring.component.SAScore]
[[stage.scoring.component.SAScore.endpoint]]
name = "SA"
weight = 1.0
# reverse_sigmoid: 分数低(易合成)时接近 1,分数高时接近 0
transform.type = "reverse_sigmoid"
transform.low = 2.0        # 低于 2 视为完全可接受
transform.high = 6.0       # 高于 6 视为不可接受
transform.k = 0.4          # 陡峭程度

用平滑变换而非硬阈值:直接卡 SA < 5 会造成优化悬崖,reverse_sigmoid 让梯度平滑,模型能学到「往简单的方向走」而不是在阈值边缘徘徊。

SA Score 的局限:必须知道

  • 基于片段频率,不是真正的合成分析。它衡量的是「这个分子的片段在已知化合物中常不常见」,而不是「能否设计出合成路线」。
  • 对新颖但易合成的结构会误判:一个用常规反应就能做出来、但骨架少见的分子,SA Score 可能偏高。这会系统性地惩罚新颖性——而新颖性正是生成模型的价值所在。
  • 不考虑起始原料可得性:结构简单但需要罕见砌块的分子,SA 分可能很低但实际难做。
  • 不考虑立体化学的合成难度:多个手性中心的立体选择性构建可能极难,SA 分未必充分反映。
  • 不考虑官能团兼容性:分子中多个反应性基团的相互干扰、保护基策略,完全不在考虑范围内。

更可靠的替代与补充

# 方案一:SCScore(基于反应数据训练的复杂度分数)
# 从 USPTO 反应数据学习「产物比反应物复杂」这个关系
# pip install scscore  或从 GitHub 获取

# 方案二:RAscore(合成可行性分类器)
# 用 AiZynthFinder 的求解结果训练,直接预测「能否找到路线」
pip install rascore

from RAscore import RAscore_XGB
scorer = RAscore_XGB.RAScorerXGB()
score = scorer.predict("CC(=O)Oc1ccccc1C(=O)O")
print(f"RAscore: {score:.3f}")     # 0~1,越大越可能找到合成路线

# 方案三:直接跑逆合成(最可靠但最慢)
# 用 AiZynthFinder(见 385)判断能否找到完整路线
from aizynthfinder.aizynthfinder import AiZynthFinder

finder = AiZynthFinder(configfile="config.yml")
finder.stock.select("zinc")
finder.expansion_policy.select("uspto")

def is_synthesizable(smiles, time_limit=30):
    finder.target_smiles = smiles
    finder.tree_search(show_progress=False)
    finder.build_routes()
    stats = finder.extract_statistics()
    return stats["is_solved"], stats.get("number_of_steps")

solved, steps = is_synthesizable("CC(=O)Oc1ccccc1C(=O)O")
print(f"可合成: {solved}, 步数: {steps}")

实际的组合策略

阶段 方法 成本
生成时(每步都算) SA Score 毫秒级,可接受
生成后初筛(top 1000) RAscore / SCScore
候选精选(top 100) 逆合成分析 秒到分钟级
最终确认(top 30) 合成化学家评审 人力,但不可替代

SA Score 的正确定位是「生成过程中的快速护栏」——它足够快,能在每一步生成时计算;但它不是最终判据。真正的合成可行性判断,必须靠逆合成分析和人的经验。

阈值怎么定

# 用你自己的化合物库校准,而不是照搬通用阈值
import numpy as np

# 拿公司历史上真正合成过的分子算 SA 分布
historical_sa = [sascorer.calculateScore(Chem.MolFromSmiles(s))
                 for s in synthesized_compounds]

print(f"历史合成分子 SA 分布:")
print(f"  中位数 {np.median(historical_sa):.2f}")
print(f"  90 分位 {np.percentile(historical_sa, 90):.2f}")

# 把 90 分位作为 transform.high,比用通用的 6.0 更贴合实际
# 不同公司的合成能力不同,阈值也应不同

常见坑与提示

  • SA Score 衡量的是「片段常见程度」,不是真正的合成路线分析;
  • 它会系统性惩罚新颖但易合成的结构,不要把阈值卡得太严;
  • 用平滑 transform 而非硬阈值,避免优化悬崖;
  • 用自家历史合成分子校准阈值,比照搬通用值更合理。

延伸资源