生成模型最常被诟病的问题是产出「看起来合理但没人能做出来」的分子。加入合成可及性约束是最直接的应对。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 而非硬阈值,避免优化悬崖;
- 用自家历史合成分子校准阈值,比照搬通用值更合理。