化學資訊學入門與實作:Active Learning 策略在 QSAR 建模中的應用
讓模型主動告訴你「下一步要測試哪個分子」,用不確定性採樣降低實驗成本。
化學資訊學入門與實作:Active Learning 策略在 QSAR 建模中的應用

讓模型主動告訴你「下一步要測試哪個分子」,用不確定性採樣降低實驗成本。
化學資訊學入門與實作:Active Learning 策略在 QSAR 建模中的應用
在上一篇文章中,我們介紹了 SHAP(SHapley Additive exPlanations)如何幫助我們解讀 QSAR 模型的特徵貢獻度,理解哪些分子描述符對預測結果影響最大。本篇延續可解釋 AI 的脈絡,進入另一個在藥物探索中實用的主題 — — Active Learning(主動學習)。
為什麼 QSAR 建模需要 Active Learning?
在藥物探索的早期階段,合成化合物與量測一個化合物的物理化學性質(如水溶解度、logP、結合自由能)往往需要耗費大量的時間與成本。傳統的 QSAR 建模方式是先有大量化合物的物理化學性質,再一次性訓練模型。但如果我們的資源有限,只能量測少量 N 個化合物,如何聰明地選擇「最有資訊量」的那 N 個?
Active Learning(主動學習)要解決的,就是這個問題。Active Learning 是一種迭代式的機器學習範式:模型可以主動詢問「哪些未標注樣本,我最需要看到它的標籤?」也就是說,由模型引導我們決定下一步要量哪個分子。
在化學資訊學的語境中,「未標注樣本」就是尚未量測的化合物,「標注」就是進行實驗量測並取得目標值。整個循環的目標是:用最少的實驗量,建立最準確的預測模型。
Active Learning 的架構與流程
一個完整的 Active Learning 循環包含四個步驟,不斷重複直到達到預算上限或模型效能收斂:
1. 分子庫(Molecular Library): 從分子庫中隨機選取少量樣本(例如 10–20 個),實驗量測後作為初始訓練集。
2. 查詢策略(Query Strategy): 用當前訓練集訓練一個能提供不確定性估計的模型,最常用的選擇是 Gaussian Process Regression(GPR)。GPR 不只給出預測值,還同時輸出每個樣本的預測標準差 σ,即「模型有多不確定」。
3. 實驗(Experiment): 依照查詢策略(Query Strategy)選出下一批最有資訊量的樣本,對選出的樣本進行實驗量測
4. 更新 QSAR 模型:將實驗結果加入訓練集,回到步驟2。

圖 1:Active Learning 循環示意圖 — — 模型主動引導實驗採樣方向
這個架構的好處在於:每一輪的量測預算都集中在「模型最不確定」的區域,避免浪費資源在模型已經掌握的化學空間中。換句話說,我會只要對「模型無法準確預測」的分子進行實驗,就能更有效率地探索藥物。
查詢策略(Query Strategy)
查詢策略決定了「哪些未標注樣本值得被量測」。在 QSAR 的應用中,最常見的兩種策略是:
不確定性採樣(Uncertainty Sampling): 選擇模型預測不確定性最高的樣本。以 Gaussian Process 為例,不確定性就是預測值的標準差 σ;σ 越大,代表模型對這個分子的了解越少,量測它的資訊增益就越高。具體操作是:對所有候選分子計算 σ,選出前 k 個 σ 最大的進行量測。
多樣性採樣(Diversity Sampling): 選擇在化學空間中與已知訓練集差異最大的樣本,確保訓練集覆蓋足夠廣泛的化學多樣性。這常與不確定性採樣結合,形成 Query-by-Committee 或 BALD(Bayesian Active Learning by Disagreement)等進階方法。
在實際應用中,不確定性採樣因為容易實作且效果穩健,是最廣泛使用的策略。
用 DeepChem + Scikit-learn 實作 Active Learning
以下程式碼使用 ESOL(Delaney)水溶解度資料集,示範一個完整的 Active Learning 循環。資料集包含 1,128 個有機分子的 log(mol/L) 值,來自 Delaney 2004 年發表於 J. Chem. Inf. Comput. Sci. 的論文(DOI: https://doi.org/10.1021/ci034243x)。
# Dataset: ESOL (Delaney, J. Chem. Inf. Comput. Sci. 2004)
# DOI: https://doi.org/10.1021/ci034243x
# 分子數:1,128 目標值:log 水溶解度(log mol/L)
import deepchem as dc
import numpy as np
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import Matern, ConstantKernel as C
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
from rdkit.Chem import Descriptors
# ── 載入 ESOL 資料集 ─────────────────────────────────────────────
tasks, datasets, transformers = dc.molnet.load_delaney(
featurizer='RDKit', # RDKit 2D 描述符(200 維連續特徵)
splitter='scaffold', # scaffold split 避免資料洩漏
reload=True,
)
train, valid, test = datasets
X_all = np.vstack([train.X, valid.X])
y_all = np.concatenate([train.y.flatten(), valid.y.flatten()])
# transformers 預設對 y 做 normalization,還原原始 logS 值:
y_raw = np.concatenate([train.y.copy(), valid.y.copy()])
for t in transformers:
y_raw = t.untransform(y_raw)
y_all_raw = y_raw.flatten()
X_test, y_test = test.X, test.y.flatten()
for t in transformers:
y_test = t.untransform(y_test.reshape(-1, 1)).flatten()
# ── Active Learning 循環 ─────────────────────────────────────────
def run_active_learning(X_pool, y_pool, X_test, y_test,
n_init=10, n_query=10, n_rounds=8,
seed=42):
rng = np.random.default_rng(seed)
labeled = list(rng.choice(len(X_pool), n_init, replace=False))
unlabeled = [i for i in range(len(X_pool)) if i not in labeled]
scaler = StandardScaler()
kernel = C(1.0) * Matern(length_scale=1.0, nu=2.5)
gpr = GaussianProcessRegressor(kernel=kernel, alpha=0.3,
n_restarts_optimizer=3)
rmse_list, n_list = [], []
for _ in range(n_rounds):
X_tr = scaler.fit_transform(X_pool[labeled])
gpr.fit(X_tr, y_pool[labeled])
y_pred, _ = gpr.predict(scaler.transform(X_test), return_std=True)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
rmse_list.append(rmse)
n_list.append(len(labeled))
if not unlabeled:
break
# 不確定性採樣:選 σ 最大的前 n_query 個
X_un = scaler.transform(X_pool[unlabeled])
_, sigma = gpr.predict(X_un, return_std=True)
top_k = np.argsort(-sigma)[:n_query]
new_idx = [unlabeled[i] for i in top_k]
labeled.extend(new_idx)
unlabeled = [i for i in unlabeled if i not in new_idx]
return n_list, rmse_list
n_labeled, rmse_active = run_active_learning(
X_all, y_all_raw, X_test, y_test)
print("Training set sizes:", n_labeled)
print("Test RMSE history:", [f"{r:.3f}" for r in rmse_active])
學習曲線比較:隨機採樣基線
為了量化主動學習的優勢,我們建立一個隨機採樣基線(Random Baseline)作為對比。隨機基線在每輪迭代中隨機選擇下一批量測樣本,不考慮模型的不確定性估計:
def run_random_baseline(X_pool, y_pool, X_test, y_test,
n_init=10, n_query=10, n_rounds=8,
seed=42):
rng = np.random.default_rng(seed)
labeled = list(rng.choice(len(X_pool), n_init, replace=False))
unlabeled = [i for i in range(len(X_pool)) if i not in labeled]
scaler = StandardScaler()
kernel = C(1.0) * Matern(length_scale=1.0, nu=2.5)
gpr = GaussianProcessRegressor(kernel=kernel, alpha=0.3,
n_restarts_optimizer=3)
rmse_list, n_list = [], []
for _ in range(n_rounds):
X_tr = scaler.fit_transform(X_pool[labeled])
gpr.fit(X_tr, y_pool[labeled])
y_pred = gpr.predict(scaler.transform(X_test))
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
rmse_list.append(rmse)
n_list.append(len(labeled))
if not unlabeled:
break
# 隨機選擇——不考慮不確定性
k = min(n_query, len(unlabeled))
new_idx = list(rng.choice(unlabeled, k, replace=False))
labeled.extend(new_idx)
unlabeled = [i for i in unlabeled if i not in new_idx]
return n_list, rmse_list
n_rand, rmse_random = run_random_baseline(
X_all, y_all_raw, X_test, y_test)

圖 2:Active Learning vs. 隨機採樣的學習曲線(20 個 random seed 的平均 ± 標準差,ESOL logS,RDKit Descriptors + Matérn GPR)
從學習曲線中可以看到,在標注預算較少的早期階段(訓練集 < 30 個分子),主動學習策略的測試集 RMSE 通常顯著低於隨機採樣。這說明不確定性採樣能更有效率地探索化學空間,讓每一次量測都帶來更多的資訊增益。
不確定性估計的視覺化
除了學習曲線,我們也可以將 GPR 模型在化學空間中的預測不確定性視覺化,觀察哪些分子是模型「最不確定」的候選目標。以下程式碼將 RDKit 描述符投影到二維 PCA 空間,並用顏色深淺表示每個分子的 σ 值:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
# 用 PCA 將 RDKit 描述符降到 2D 進行視覺化
pca = PCA(n_components=2, random_state=42)
X_2d = pca.fit_transform(np.vstack([X_all, X_test]))
X_pool_2d = X_2d[:len(X_all)]
X_test_2d = X_2d[len(X_all):]
# 用全量 pool fit GPR,取得不確定性
scaler_viz = StandardScaler()
gpr_viz = GaussianProcessRegressor(
kernel=C(1.0)*Matern(length_scale=1.0, nu=2.5), alpha=0.3)
gpr_viz.fit(scaler_viz.fit_transform(X_all), y_all_raw)
_, sigma_pool = gpr_viz.predict(
scaler_viz.transform(X_all), return_std=True)
fig, ax = plt.subplots(figsize=(8, 6))
sc = ax.scatter(X_pool_2d[:, 0], X_pool_2d[:, 1],
c=sigma_pool, cmap='YlOrRd',
s=50, alpha=0.85, edgecolors='none')
plt.colorbar(sc, ax=ax, label='Prediction Uncertainty σ')
ax.set_xlabel('PC1 (RDKit Descriptors)')
ax.set_ylabel('PC2 (RDKit Descriptors)')
ax.set_title('GPR Uncertainty in Chemical Space — ESOL Dataset')
plt.tight_layout()
plt.savefig('fig3_uncertainty_pca.png', dpi=150, bbox_inches='tight')

圖 3:化學空間中的 GPR 預測不確定性(PCA 2D 投影,橘紅色 = 高 σ = Active Learning 優先選取)
橘紅色(高 σ 值)的分子通常位於化學空間的邊緣或孤立區域,對應於結構上較為獨特、與訓練集化學多樣性差異較大的化合物。這些正是 Active Learning 應優先納入訓練集的候選分子 — — 量測它們能帶來最大的資訊增益,讓模型快速補足化學空間的「盲區」。
結語
Active Learning 提供了一個減少實驗預算的思路:每次量測都選最有資訊量的分子,而不是均勻或隨機採樣。在藥物探索的早期篩選階段,預算往往有限,主動學習策略能有效減少達到目標模型精度所需的實驗次數。
配合 Gaussian Process 的不確定性估計,以及 DeepChem 提供的標準化分子表示(dc.molnet.load_delaney() + scaffold split),整個 Active Learning 工作流程可以在 Python 中以相對簡潔的程式碼實現。
下一步,我們可以進一步探索 Multi-fidelity Active Learning — — 結合快速低成本的計算篩選(如半經驗量子化學 GFN2-xTB)與高成本的實驗量測,在不同精度層次之間做出最優決策,讓每一分資源都花在刀口上。
配合現代深度學習模型的不確定性估計(如 MC Dropout、Deep Ensemble),Active Learning 在化學資訊學中的應用範圍也在持續延伸,不只限於 QSAR 建模。
메타데이터
- post_id
- ffc6e8e02dbf
- slug
- 化學資訊學入門與實作-active-learning-策略在-qsar-建模中的應用-ffc6e8e02dbf
- url
- https://medium.com/@cheninformatics/%E5%8C%96%E5%AD%B8%E8%B3%87%E8%A8%8A%E5%AD%B8%E5%85%A5%E9%96%80%E8%88%87%E5%AF%A6%E4%BD%9C-active-learning-%E7%AD%96%E7%95%A5%E5%9C%A8-qsar-%E5%BB%BA%E6%A8%A1%E4%B8%AD%E7%9A%84%E6%87%89%E7%94%A8-ffc6e8e02dbf
- canonical_url
- https://medium.com/@cheninformatics/%E5%8C%96%E5%AD%B8%E8%B3%87%E8%A8%8A%E5%AD%B8%E5%85%A5%E9%96%80%E8%88%87%E5%AF%A6%E4%BD%9C-active-learning-%E7%AD%96%E7%95%A5%E5%9C%A8-qsar-%E5%BB%BA%E6%A8%A1%E4%B8%AD%E7%9A%84%E6%87%89%E7%94%A8-ffc6e8e02dbf
- author_url
- https://medium.com/@cheninformatics
- status
- ok
- fetched_at
- 2026-06-09 15:37:30