AiZynthFinder 完整教學 — AI 逆合成規劃工具

1. 專案概述

AiZynthFinder 是 AstraZeneca Molecular AI 團隊開源的逆合成規劃工具(retrosynthetic planning tool),也是該領域目前最成熟的開源方案(845 stars,MIT 授權)。

核心能力

  • 從一個**目標分子(target molecule)**出發,自動找出合成路線(synthetic routes)
  • 預設使用 Monte Carlo Tree Search (MCTS) 演算法,以神經網路策略遞迴拆解分子
  • 搜尋終止條件:所有葉節點分子都能在**商用試劑庫(stock)**中買到
  • 支援多種搜尋演算法、多目標最佳化、自訂評分函數

你為什麼需要它

在 lead optimization(先導化合物優化)工作流中,AI 產生的 analog(類似物)候選分子需要回答一個關鍵問題:這個分子做得出來嗎? AiZynthFinder 的角色就是回答這個問題,並找到:

  1. 合成步驟盡可能少的路線(降低實驗成本與時間)
  2. 所有起始原料都是可商業購買的 building block(確保可行性)

flowchart LR
    A["Lead Optimization
AI 產生 analog"] --> B["AiZynthFinder
逆合成分析"] B --> C{"所有 building block
可商購?"} C -->|"Yes"| D["路線評分
選最短路線"] C -->|"No"| E["排除 / 調整結構"] D --> F["送交合成化學家
實驗執行"]

2. 安裝與環境設定

2.1 基本安裝

1# Step 1: 建立獨立的 conda 環境
2conda create "python>=3.10,<3.13" -n aizynth-env
3
4# Step 2: 啟用環境
5conda activate aizynth-env
6
7# Step 3: 安裝完整版(建議)
8python -m pip install aizynthfinder[all]

[all] 安裝內含 pymongo、route-distances、scipy、molbloom 等額外功能。若只需最小安裝可用 pip install aizynthfinder

2.2 下載公開模型與 Stock

1# 自動下載 USPTO 模型 + ZINC stock + 生成 config.yml
2download_public_data ./aizynthfinder_data

這會下載以下檔案:

檔案用途來源
uspto_model.onnxExpansion policy 模型Zenodo
uspto_templates.csv.gz反應模板庫Zenodo
uspto_ringbreaker_model.onnxRingBreaker 模型Zenodo
uspto_ringbreaker_templates.csv.gzRingBreaker 模板Zenodo
uspto_filter_model.onnxFilter policy 模型Zenodo
zinc_stock.hdf5ZINC 可購買分子庫Figshare
config.yml自動生成的設定檔

2.3 驗證安裝

1# 確認 CLI 工具可用
2aizynthcli -h
3
4# 快速測試(單一 SMILES)
5aizynthcli --config ./aizynthfinder_data/config.yml --smiles "c1ccccc1"

3. 核心概念

3.1 MCTS 搜尋原理

AiZynthFinder 的預設搜尋演算法是 Monte Carlo Tree Search (MCTS),每一輪包含四個步驟:


flowchart TB
    A["1. Selection
從 root 走到最有潛力的 leaf node"] --> B["2. Expansion
用 expansion policy (神經網路)
預測可能的反應拆解"] B --> C["3. Rollout
展開子節點直到 terminal state"] C --> D["4. Backpropagation
將 reward 回傳更新所有祖先節點"] D --> A

搜尋終止條件(任一即停):

條件預設值設定參數
達到最大迭代次數100 次iteration_limit
超過時間限制120 秒time_limit
找到第一條解(可選)Falsereturn_first
所有節點都無法展開自動偵測

3.2 狀態(State)與解的定義

每個搜尋樹節點包含一個 MctsState,由一組分子組成:

  • 已解決(in stock):該分子在商用試劑庫中可以買到
  • 可展開(expandable):該分子不在 stock 中,需要進一步拆解
  • is_solved = True:所有分子都在 stock 中 → 這條路線是一個完整的合成方案
  • is_terminal = True:已解決 達到最大合成步驟數(max_transforms

3.3 Expansion Policy

Expansion policy 是一個神經網路,輸入分子指紋(molecular fingerprint),輸出在反應模板庫中各模板的機率分佈。選出機率最高的模板後,嘗試將其套用到分子上生成前驅物。

關鍵參數:

參數預設值說明
cutoff_cumulative0.995累積機率門檻,超過的模板不考慮
cutoff_number50最多考慮的模板數量
use_rdchiralTrue使用 RDChiral 套用模板(立體化學正確)

4. 設定 Buyable Building Block Stock(商用試劑庫)

這是 lead optimization 工作流中最關鍵的設定。Stock 決定了「什麼分子算是可以買到的起始原料」。

4.1 使用預設 ZINC Stock

download_public_data 下載的 zinc_stock.hdf5 包含 ZINC 資料庫中的可購買分子。設定檔中:

1stock:
2  zinc: zinc_stock.hdf5

4.2 使用自訂 Stock(建議用於實際專案)

在藥物化學實作中,你可能想用公司內部的試劑目錄或特定供應商(如 Enamine、Sigma-Aldrich、eMolecules)的 building block 清單。

方法一:從 SMILES 檔案建立 stock

1# 假設你有一份 building_blocks.smi(每行一個 SMILES)
2smiles2stock --files building_blocks.smi --output my_stock.hdf5

方法二:使用 CSV/HDF5 檔案(含價格資訊)

Stock 檔案格式要求:

  • HDF5 檔案須有 table key,含 inchi_key 欄位
  • CSV 檔案須有 inchi_key 欄位
  • 可選 price 欄位用於價格相關評分
1stock:
2  enamine_bb:
3    type: inchiset
4    path: /path/to/enamine_building_blocks.hdf5
5  sigma:
6    type: inchiset
7    path: /path/to/sigma_stock.csv

方法三:多 stock 來源同時使用

1stock:
2  zinc: zinc_stock.hdf5
3  enamine: enamine_bb.hdf5
4  internal: internal_inventory.csv

在 Python API 或 CLI 中,可以選擇使用哪些 stock:

1finder.stock.select(["zinc", "enamine"])  # 只用這兩個
2finder.stock.select(finder.stock.items)   # 使用全部

4.3 Stock Stop Criteria(進階)

可以設定額外的終止條件,讓搜尋更精確:

1stock:
2  zinc: zinc_stock.hdf5
3  stop_criteria:
4    price: 100        # 只接受價格 <= 100 的 building block
5    weight: 250       # 分子量上限
6    counts:           # 原子數量限制
7      C: 10
8      N: 4

4.4 Molbloom Filter(快速 stock 查詢)

對於超大 stock(數千萬分子),可以使用 bloom filter 加速查詢:

1stock:
2  large_catalog: /path/to/catalog.bloom

4.5 自訂 Stock 類別(進階)

可以實作自己的 stock 查詢邏輯,例如結合即時 API 查詢供應商庫存:

1from aizynthfinder.context.stock.queries import StockQueryMixin
2
3class VendorAPIStock(StockQueryMixin):
4    def __contains__(self, mol):
5        # 用 InChI key 查詢供應商 API
6        return self._query_vendor_api(mol.inchi_key)
7
8    def price(self, mol):
9        return self._get_price(mol.inchi_key)

5. 控制最大合成步驟數(Tree Depth)

這是你最關心的參數之一:合成路線不要太長

5.1 max_transforms 參數

max_transforms 控制搜尋樹的最大深度,即允許的最大反應步驟數。預設為 6

1search:
2  max_transforms: 6    # 預設值,最多 6 步反應

設成較小的值(如 3-4),搜尋會更快且偏好短路線:

1search:
2  max_transforms: 4    # 強制限制:最多 4 步
3  iteration_limit: 200  # 增加迭代次數補償
4  time_limit: 300       # 增加時間限制

5.2 深度如何影響搜尋


flowchart TB
    subgraph "max_transforms = 3"
        T1["Target"] --> R1["反應 1"]
        R1 --> I1["中間體 A"] & I2["中間體 B"]
        I1 --> R2["反應 2"]
        R2 --> BB1["Building Block 1
✅ in stock"] & BB2["Building Block 2
✅ in stock"] I2 --> R3["反應 3"] R3 --> BB3["Building Block 3
✅ in stock"] & BB4["Building Block 4
✅ in stock"] end
  • max_transforms = 3:搜尋空間小、速度快,但可能找不到解(如果目標分子很複雜)
  • max_transforms = 6(預設):適合大多數藥物分子
  • max_transforms = 8-10:適合非常複雜的天然物或大環分子,但搜尋時間大幅增加

5.3 實務建議

目標分子複雜度建議 max_transforms建議 iteration_limit
簡單 analog(MW < 400)3-4100-200
標準藥物分子(MW 400-600)5-6200-500
複雜結構(MW > 600 或多環)7-8500-1000

6. MCTS 搜尋策略如何找最短路線

6.1 State Score — 預設 Reward 函數

MCTS 搜尋的核心是 reward function(回饋函數),決定哪些路線更好。預設的 State Score 計算公式為:

1State Score = 0.95 * FractionInStock + 0.05 * MaxTransformScore

其中:

  • FractionInStock:葉節點中在 stock 的分子比例(0~1)。全部在 stock = 1.0
  • MaxTransformScore:使用 squash 函數壓縮的深度分數。步驟越少,分數越高

這意味著 State Score 天生偏好短路線:在同樣都是 solved 的路線中,步驟少的路線會得到更高的 MaxTransformScore。

6.2 使用 return_first 快速找到可行路線

如果你只需要確認「能不能合成」而不需要最佳路線:

1search:
2  return_first: true    # 找到第一條解就停止
3  max_transforms: 4

6.3 增加迭代以找更多/更好的路線

1search:
2  iteration_limit: 500   # 更多迭代 → 探索更多路線
3  time_limit: 600        # 10 分鐘時間限制
4  max_transforms: 5

6.4 MCTS 探索-利用平衡(C 參數)

1search:
2  algorithm_config:
3    C: 1.4              # UCB 公式中的探索常數
4                         # 值越大 → 探索越多新路線
5                         # 值越小 → 集中在已知好的路線

7. Route Scoring(路線評分)機制

搜尋完成後,需要對找到的路線進行排序。AiZynthFinder 提供多種評分函數。

7.1 內建評分函數

Scorer名稱偏好說明
StateScorerstate score高分好預設。0.95 * stock比例 + 0.05 * 深度分數
NumberOfReactionsScorernumber of reactions低分好反應步驟數(越少越好)
NumberOfPrecursorsScorernumber of pre-cursors低分好前驅物數量
NumberOfPrecursorsInStockScorernumber of pre-cursors in stock高分好在 stock 中的前驅物數量
AverageTemplateOccurrenceScoreraverage template occurrence高分好模板的平均出現次數(代表反應可靠性)
PriceSumScorersum of prices低分好所有前驅物價格總和
RouteCostScorerroute cost低分好含反應成本與產率的路線成本

7.2 設定 Post-processing 評分

1post_processing:
2  min_routes: 5
3  max_routes: 25
4  route_scorer: "state score"    # 用哪個 scorer 排序

7.3 提取所有 solved 路線

1post_processing:
2  all_routes: true       # 提取所有已解決的路線
3  min_routes: 5          # 若無解,fallback 至少取 5 條
4  max_routes: 50         # 上限

7.4 自訂評分函數

 1from aizynthfinder.context.scoring.scorers_base import Scorer
 2
 3class ShortRoutePreferenceScorer(Scorer):
 4    """強烈偏好短路線的自訂評分"""
 5
 6    def __repr__(self):
 7        return "short route preference"
 8
 9    def _score_node(self, node):
10        n_reactions = len(node.actions_to())
11        in_stock_frac = sum(node.state.in_stock_list) / len(node.state.mols)
12        # 反應數越少 bonus 越大
13        return in_stock_frac * (1.0 / max(n_reactions, 1))
14
15    def _score_reaction_tree(self, tree):
16        n_reactions = len(list(tree.reactions()))
17        leaves = list(tree.leafs())
18        in_stock_frac = sum(1 for m in leaves if m in self._config.stock) / len(leaves)
19        return in_stock_frac * (1.0 / max(n_reactions, 1))

8. 實戰:從 SMILES 跑逆合成分析

8.1 CLI 方式(單一分子)

1# 直接輸入 SMILES
2aizynthcli --config config.yml --smiles "CC(=O)Oc1ccccc1C(=O)O"

輸出會包含:

  • 搜尋統計(時間、迭代次數、是否找到解)
  • Top-scored 路線的統計資訊
  • trees.json:所有提取的路線(JSON 格式)

8.2 CLI 方式(批次分子)

 1# 建立 SMILES 檔案
 2echo "CC(=O)Oc1ccccc1C(=O)O
 3c1ccc2c(c1)cc1ccc3cccc4ccc2c1c34
 4CC(C)NCC(O)c1ccc(O)c(O)c1" > targets.smi
 5
 6# 批次處理
 7aizynthcli --config config.yml --smiles targets.smi --output results.json.gz
 8
 9# 使用多進程加速
10aizynthcli --config config.yml --smiles targets.smi --nproc 4

8.3 Python API 方式(推薦整合用)

 1from aizynthfinder.aizynthfinder import AiZynthFinder
 2
 3# 1. 初始化
 4finder = AiZynthFinder(configfile="config.yml")
 5
 6# 2. 選擇 stock 和 policy
 7finder.stock.select("zinc")
 8finder.expansion_policy.select("uspto")
 9finder.filter_policy.select("uspto")
10
11# 3. 設定目標分子
12finder.target_smiles = "CC(=O)Oc1ccccc1C(=O)O"  # Aspirin
13
14# 4. 執行搜尋
15finder.tree_search(show_progress=True)
16
17# 5. 提取路線
18finder.build_routes()
19stats = finder.extract_statistics()
20
21# 6. 查看結果
22print(f"找到解: {stats['is_solved']}")
23print(f"路線數: {stats['number_of_routes']}")
24print(f"已解決路線數: {stats['number_of_solved_routes']}")
25print(f"搜尋時間: {stats['search_time']:.1f} 秒")
26
27# 7. 檢查 building block 的 stock 資訊
28stock_info = finder.stock_info()
29for smiles, sources in stock_info.items():
30    print(f"  {smiles}: {sources}")
31
32# 8. 輸出路線圖片
33for i, tree in enumerate(finder.routes.reaction_trees):
34    tree.to_image().save(f"route_{i:03d}.png")

8.4 Python API — 自訂設定(不用設定檔)

 1from aizynthfinder.aizynthfinder import AiZynthFinder
 2
 3# 用 dict 設定(適合程式化整合)
 4config = {
 5    "expansion": {
 6        "uspto": [
 7            "/path/to/uspto_model.onnx",
 8            "/path/to/uspto_templates.csv.gz"
 9        ]
10    },
11    "filter": {
12        "uspto": "/path/to/uspto_filter_model.onnx"
13    },
14    "stock": {
15        "zinc": "/path/to/zinc_stock.hdf5",
16        "enamine": "/path/to/enamine_bb.hdf5"
17    },
18    "search": {
19        "max_transforms": 4,
20        "iteration_limit": 200,
21        "time_limit": 300
22    }
23}
24
25finder = AiZynthFinder(configdict=config)
26finder.stock.select(["zinc", "enamine"])
27finder.expansion_policy.select("uspto")
28finder.filter_policy.select("uspto")

9. 與 Lead Optimization 工作流的整合

9.1 整合架構


flowchart TB
    subgraph "Lead Optimization Pipeline"
        A["AI 生成 analog
(SMILES list)"] --> B["過濾器
(ADMET / 藥物性質)"] B --> C["AiZynthFinder
批次逆合成"] C --> D["結果篩選"] D --> E["排序 & 選擇"] E --> F["送交合成化學家"] end subgraph "AiZynthFinder 篩選標準" D --> G["is_solved = True"] D --> H["steps <= 4"] D --> I["所有 BB 在 stock"] D --> J["route_cost 最低"] end

9.2 批次處理 + 結果篩選腳本

 1import pandas as pd
 2from aizynthfinder.aizynthfinder import AiZynthFinder
 3from aizynthfinder.reactiontree import ReactionTree
 4
 5def evaluate_synthesizability(smiles_list, config_path, max_steps=4):
 6    """
 7    評估一批候選分子的可合成性
 8
 9    Parameters
10    ----------
11    smiles_list : list of str
12        候選分子的 SMILES
13    config_path : str
14        AiZynthFinder 設定檔路徑
15    max_steps : int
16        可接受的最大合成步驟數
17
18    Returns
19    -------
20    pd.DataFrame
21        含可合成性評估結果的 DataFrame
22    """
23    finder = AiZynthFinder(configfile=config_path)
24    finder.stock.select(finder.stock.items)
25    finder.expansion_policy.select(finder.expansion_policy.items[0])
26    finder.filter_policy.select_all()
27
28    results = []
29    for smi in smiles_list:
30        finder.target_smiles = smi
31        try:
32            finder.prepare_tree()
33            finder.tree_search()
34            finder.build_routes()
35            stats = finder.extract_statistics()
36            stock_info = finder.stock_info()
37
38            # 找出步驟最少的 solved route
39            best_steps = None
40            for tree in finder.routes.reaction_trees:
41                n_steps = len(list(tree.reactions()))
42                leaves = list(tree.leafs())
43                all_in_stock = all(mol in finder.stock for mol in leaves)
44                if all_in_stock and (best_steps is None or n_steps < best_steps):
45                    best_steps = n_steps
46
47            results.append({
48                "smiles": smi,
49                "is_solved": stats.get("is_solved", False),
50                "n_routes": stats.get("number_of_solved_routes", 0),
51                "best_steps": best_steps,
52                "synthesizable": (
53                    stats.get("is_solved", False)
54                    and best_steps is not None
55                    and best_steps <= max_steps
56                ),
57                "top_score": stats.get("top_score", 0),
58                "search_time": stats.get("search_time", 0),
59            })
60        except (ValueError, Exception) as e:
61            results.append({
62                "smiles": smi,
63                "is_solved": False,
64                "n_routes": 0,
65                "best_steps": None,
66                "synthesizable": False,
67                "top_score": 0,
68                "search_time": 0,
69            })
70
71    return pd.DataFrame(results)

9.3 推薦的 config.yml(lead optimization 專用)

 1# config_lead_opt.yml — 針對 lead optimization 優化的設定
 2expansion:
 3  uspto:
 4    - /path/to/uspto_model.onnx
 5    - /path/to/uspto_templates.csv.gz
 6
 7filter:
 8  uspto: /path/to/uspto_filter_model.onnx
 9
10stock:
11  zinc: /path/to/zinc_stock.hdf5
12  # 建議加入你實際使用的 building block 供應商
13  # enamine_bb: /path/to/enamine_bb.hdf5
14
15search:
16  algorithm: mcts
17  max_transforms: 4         # 限制最多 4 步,符合 med-chem 實務
18  iteration_limit: 200      # 增加迭代以確保充分搜尋
19  time_limit: 300            # 5 分鐘/分子
20  return_first: false        # 找完整搜尋空間
21  algorithm_config:
22    C: 1.4
23    prune_cycles_in_search: true
24    search_rewards:
25      - state score
26
27post_processing:
28  min_routes: 5
29  max_routes: 25
30  all_routes: true           # 提取所有 solved 路線

10. 進階功能

10.1 多 Expansion Policy(RingBreaker)

預設的 USPTO 模型對開環/成環反應的處理有限。RingBreaker 模型專門處理環系拆解:

 1expansion:
 2  uspto:
 3    - uspto_model.onnx
 4    - uspto_templates.csv.gz
 5  ringbreaker:
 6    - uspto_ringbreaker_model.onnx
 7    - uspto_ringbreaker_templates.csv.gz
 8  multi_expansion:
 9    type: aizynthfinder.context.policy.MultiExpansionStrategy
10    expansion_strategies: [uspto, ringbreaker]
11    additive_expansion: true

10.2 Retro* 搜尋演算法

MCTS 是啟發式搜尋,不保證找到最優解。Retro* 是一種最優優先搜尋,適合需要保證找到最短路線的場景:

1search:
2  algorithm: aizynthfinder.search.retrostar.search_tree.SearchTree
3  algorithm_config:
4    molecule_cost:
5      cost: aizynthfinder.search.retrostar.cost.RetroStarCost
6      model_path: retrostar_value_model.pickle
7      fingerprint_length: 2048
8      fingerprint_radius: 2

10.3 多目標 MCTS(Multi-Objective)

同時最佳化多個目標(例如 state score + 特定化學鍵斷裂):

1search:
2  break_bonds: [[1, 2], [3, 4]]
3  algorithm_config:
4    search_rewards: ["state score", "broken bonds"]

10.4 Focussed Bond Synthesis

控制哪些化學鍵必須斷開或保留:

1search:
2  break_bonds: [[1, 2]]       # 這些鍵必須被斷開
3  freeze_bonds: [[3, 4]]      # 這些鍵不可被斷開
4  break_bonds_operator: or     # any 或 all

10.5 Route Clustering

自動將相似的路線分群,方便化學家評估:

1aizynthcli --config config.yml --smiles targets.smi --cluster

10.6 AiZynthExpander — 單步反應拆解

不需要完整的搜尋樹,只看目標分子的第一步拆解

 1from aizynthfinder.aizynthfinder import AiZynthExpander
 2
 3expander = AiZynthExpander(configfile="config.yml")
 4expander.expansion_policy.select("uspto")
 5reactions = expander.do_expansion(
 6    "CC(=O)Oc1ccccc1C(=O)O",
 7    return_n=10   # 回傳最多 10 種不同的拆解方式
 8)
 9
10for rxn_group in reactions:
11    for rxn in rxn_group:
12        reactants = [mol.smiles for mol in rxn.reactants[0]]
13        print(f"  反應: {rxn.smiles}")
14        print(f"  前驅物: {reactants}")

11. 常見問題與疑難排解

Q1: 搜尋找不到 solved route

可能原因與解法:

  1. Stock 太小 → 加入更多 building block 來源
  2. max_transforms 太小 → 增加到 6-8
  3. 分子太複雜 → 增加 iteration_limittime_limit
  4. 反應模板覆蓋不足 → 加入 RingBreaker 或其他 expansion policy

Q2: 路線步驟太多

  1. 降低 max_transforms(例如 3-4)
  2. 擴大 stock 覆蓋範圍(更多分子可以直接買到 → 更早停止搜尋)
  3. 使用 number of reactions scorer 排序結果

Q3: 如何確認 building block 真的可以買到?

stock_info() 方法回傳每個 building block 的來源資訊:

1stock_info = finder.stock_info()
2for smiles, sources in stock_info.items():
3    if sources:
4        print(f"✓ {smiles} — 來源: {', '.join(sources)}")
5    else:
6        print(f"✗ {smiles} — 不在 stock 中")

Q4: 如何處理大量候選分子(> 1000 個)?

1# 使用多進程
2aizynthcli --config config.yml --smiles all_candidates.smi --nproc 8 --output batch_results.json.gz

Q5: 記憶體不足

  • 降低 cutoff_number(模板數上限)
  • 降低 max_transforms
  • 使用 Molbloom filter 取代 InMemory stock

最後提醒: AiZynthFinder 的預測結果是計算建議,最終合成路線仍需由合成化學家審核。該工具的價值在於快速篩掉不可合成的候選分子,並為可行的候選提供起始路線方向。