濕實驗 × 最佳化演算法 × PyTorch → 蛋白質AI
| 優先級 | 技術 | 說明 | 預估時間 |
|---|---|---|---|
| 🔴 最優先 | 蛋白質AI領域知識 | AlphaFold2、ProteinMPNN、ESM-2 的數學與應用 | 2–3週 |
| 🔴 最優先 | 強化學習基礎 | MDP框架、PPO目標函數、Reward設計 | 1–2週 |
| 🔴 最優先 | 圖神經網路(GCN) | 節點/邊訊息傳遞、PyTorch Geometric | 1週 |
| 🟡 中優先 | 擴散模型 | DDPM數學原理、RFdiffusion應用 | 1週 |
| 🟡 中優先 | Hugging Face生態 | ESM模型載入、微調基礎 | 3–5天 |
| 🟢 低優先 | 大模型微調 | LoRA、instruction tuning概念 | 按需補 |
| # | 論文 | 核心數學概念 | 優先級 |
|---|---|---|---|
| 1 | ProteinMPNN (Science 2022) | 自迴歸條件概率、圖上訊息傳遞 | 必讀 |
| 2 | ESM-2 / ESMFold (Meta 2022) | 遮罩語言模型訓練目標 | 必讀 |
| 3 | AlphaFold2 (Nature 2021) | attention + 幾何約束最佳化 | 必讀 |
| 4 | RFdiffusion (Nature 2023) | 擴散過程的去噪目標函數 | 第2批 |
| 5 | DPO/RLHF 綜述 | 將偏好轉化為最佳化問題 | 第2批 |
展示三個核心優勢:最佳化背景 × PyTorch × 生物直覺。完整可執行,附詳細注釋。
pip install transformers torch botorch gpytorch scikit-learn matplotlib pandas
import pandas as pd
import numpy as np
import torch
# 使用 ProteinGym 中的 GB1 突變穩定性數據集(公開)
# 下載:https://github.com/OATML-Markslab/ProteinGym
# 簡化版:直接用含序列和 fitness 分數的 CSV
def load_data(csv_path):
"""
CSV 格式:sequence | fitness_score
fitness_score 越高代表熱穩定性越好
"""
df = pd.read_csv(csv_path)
sequences = df['sequence'].tolist()
labels = torch.tensor(df['fitness_score'].values, dtype=torch.float32)
return sequences, labels
# Demo 用假數據(實際使用時替換為真實 CSV)
def make_demo_data(n=100, seq_len=56):
"""生成示意用的隨機序列和隨機穩定性分數"""
amino_acids = 'ACDEFGHIKLMNPQRSTVWY'
sequences = [
''.join(np.random.choice(list(amino_acids), seq_len))
for _ in range(n)
]
# 假設 fitness 與序列中 'A'、'L' 比例正相關(簡化模擬)
labels = torch.tensor([
(s.count('A') + s.count('L')) / len(s) + np.random.normal(0, 0.05)
for s in sequences
], dtype=torch.float32)
return sequences, labels
sequences, labels = make_demo_data(n=200)
print(f"數據集大小: {len(sequences)} 條序列")
from transformers import EsmModel, EsmTokenizer
# 使用最小的 ESM-2 版本(8M 參數),適合本地跑
MODEL_NAME = "facebook/esm2_t6_8M_UR50D"
tokenizer = EsmTokenizer.from_pretrained(MODEL_NAME)
esm_model = EsmModel.from_pretrained(MODEL_NAME)
esm_model.eval()
def get_embeddings(sequences, batch_size=16):
"""
輸入:蛋白質序列列表
輸出:shape (N, 320) 的 embedding tensor
320 = ESM-2 8M 模型的隱藏層維度
用 mean pooling 把變長序列壓縮為固定維度向量
"""
all_embeddings = []
for i in range(0, len(sequences), batch_size):
batch = sequences[i:i+batch_size]
inputs = tokenizer(
batch,
return_tensors="pt",
padding=True,
truncation=True,
max_length=512
)
with torch.no_grad():
outputs = esm_model(**inputs)
# last_hidden_state: (batch, seq_len, hidden_dim)
# attention_mask: (batch, seq_len) — 0 表示 padding
hidden = outputs.last_hidden_state
mask = inputs['attention_mask'].unsqueeze(-1).float()
# Masked mean pooling(忽略 padding token)
embeddings = (hidden * mask).sum(dim=1) / mask.sum(dim=1)
all_embeddings.append(embeddings)
return torch.cat(all_embeddings, dim=0)
print("正在提取 ESM-2 embedding(首次執行會下載模型約 30MB)...")
embeddings = get_embeddings(sequences)
print(f"Embedding shape: {embeddings.shape}") # (200, 320)
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# ── 數據分割 ──
X = embeddings.numpy()
y = labels.numpy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 標準化(重要:穩定 GP 和 NN 訓練)
scaler = StandardScaler()
X_train_s = torch.tensor(scaler.fit_transform(X_train), dtype=torch.float32)
X_test_s = torch.tensor(scaler.transform(X_test), dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
y_test_t = torch.tensor(y_test, dtype=torch.float32).unsqueeze(1)
# ── 模型架構 ──
class StabilityPredictor(nn.Module):
"""
簡單的前饋網路,把 ESM-2 embedding 映射到穩定性分數
Dropout 防止小數據集過擬合
"""
def __init__(self, input_dim=320):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 128),
nn.LayerNorm(128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(64, 1)
)
def forward(self, x):
return self.net(x)
model = StabilityPredictor(input_dim=X_train_s.shape[1])
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
criterion = nn.MSELoss()
loader = DataLoader(TensorDataset(X_train_s, y_train_t), batch_size=32, shuffle=True)
# ── 訓練迴圈 ──
train_losses = []
for epoch in range(100):
model.train()
epoch_loss = 0
for xb, yb in loader:
optimizer.zero_grad()
pred = model(xb)
loss = criterion(pred, yb)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
train_losses.append(epoch_loss / len(loader))
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1:3d} | Loss: {train_losses[-1]:.4f}")
# ── 評估 ──
model.eval()
with torch.no_grad():
y_pred = model(X_test_s).squeeze().numpy()
corr = np.corrcoef(y_pred, y_test)[0, 1]
print(f"\nPearson 相關係數 (test): {corr:.3f}")
from botorch.models import SingleTaskGP
from botorch.fit import fit_gpytorch_mll
from botorch.acquisition import ExpectedImprovement
from botorch.optim import optimize_acqf
from gpytorch.mlls import ExactMarginalLogLikelihood
def bayesian_optimization_loop(
X_init, # 初始訓練點 (n_init, d)
y_init, # 初始觀測值 (n_init, 1)
bounds, # 搜索範圍 (2, d),通常是標準化後的 [-3, 3]
n_iter=10 # BO 迭代輪數
):
"""
貝葉斯最佳化核心流程:
1. 用現有數據擬合高斯過程(代理模型)
2. 用 Expected Improvement(EI)採集函數選下一個候選點
3. 评估候選點(這裡用 NN 代理,實際應用中送去實驗)
4. 更新數據,重複
EI 的核心思想:平衡探索(不確定性高的區域)與利用(預測分數高的區域)
"""
X_obs = X_init.clone()
y_obs = y_init.clone()
best_values = [y_obs.max().item()]
for i in range(n_iter):
# Step 1: 擬合 GP
gp = SingleTaskGP(X_obs, y_obs)
mll = ExactMarginalLogLikelihood(gp.likelihood, gp)
fit_gpytorch_mll(mll)
# Step 2: 定義 EI 採集函數
EI = ExpectedImprovement(model=gp, best_f=y_obs.max())
# Step 3: 最佳化採集函數,找下一個候選點
candidate, acq_value = optimize_acqf(
EI,
bounds=bounds,
q=1, # 每輪提議 1 個候選點
num_restarts=5, # 多起點避免局部最佳
raw_samples=50
)
# Step 4: 模擬「評估」新候選點(真實應用中為實驗結果)
with torch.no_grad():
new_y = model(candidate).detach()
# 更新觀測集
X_obs = torch.cat([X_obs, candidate], dim=0)
y_obs = torch.cat([y_obs, new_y], dim=0)
best_values.append(y_obs.max().item())
print(f"BO 第 {i+1:2d} 輪 | 當前最佳: {best_values[-1]:.4f} | EI: {acq_value.item():.4f}")
return X_obs, y_obs, best_values
# 用 embedding 空間的主成分做為搜索空間(降維後更穩定)
from sklearn.decomposition import PCA
pca = PCA(n_components=10)
X_pca = pca.fit_transform(X_train_s.numpy())
X_bo = torch.tensor(X_pca[:20], dtype=torch.float64) # BoTorch 需要 float64
y_bo = torch.tensor(y_train[:20], dtype=torch.float64).unsqueeze(1)
# 搜索範圍:標準化後各維度的 [-3, 3]
bounds = torch.stack([
torch.full((10,), -3., dtype=torch.float64),
torch.full((10,), 3., dtype=torch.float64)
])
print("開始貝葉斯最佳化...")
X_final, y_final, improvement_curve = bayesian_optimization_loop(X_bo, y_bo, bounds, n_iter=10)
# 執行 pipeline 後,outputs/ 目錄會產生 results.png
# 內含 2×2 四格圖,每格對應一個面試故事:
python run_pipeline.py --mode bo
訓練與驗證損失同步下降、間距收窄 → 模型有效學習且未過擬合。
點越靠近紅色對角線(Ideal line)代表預測越準確。X 軸為實驗測量值,Y 軸為模型預測值。
每輪迭代後「當前最佳 fitness」的變化。曲線持續上升(或 Kd 持續下降)代表 BO 有效在序列空間探索。
用 UMAP 將高維 ESM-2 embedding 壓縮到 2D。紅星是 BO 推薦的候選序列。
給定蛋白質結構 G(原子座標),找最佳序列 s = (s₁, s₂, ..., sₙ),使序列能折疊回該結構。
自迴歸分解:每個位置的胺基酸,依賴結構 G + 已知的前序位置 s<ᵢ。
蛋白質結構 → 圖 G = (V, E):
每一層更新節點表示:
最大化條件對數似然:
理解為:帶圖結構約束的交叉熵最小化,目標是讓模型學到「什麼結構偏好什麼序列」。
| 維度 | Rosetta | ProteinMPNN |
|---|---|---|
| 目標函數 | 物理能量函數(手工設計) | 學習到的條件概率 |
| 搜索方式 | Monte Carlo / 貪婪搜索 | 自迴歸採樣 |
| 泛化能力 | 依賴能量函數精度 | 從大量實驗數據學習 |
| 速度 | 慢(秒~分鐘/序列) | 快(毫秒) |
| 局限 | 無法學習未知物理交互 | 依賴訓練數據分布 |
ESM-2 使用 Masked Language Modeling(MLM)預訓練:
| 模組 | 數學本質 | 生物對應 |
|---|---|---|
| Evoformer | 雙軸 attention(序列軸 + 殘基對軸) | 序列共進化資訊 → 接觸圖 |
| Structure Module | SE(3)-equivariant transformer | 骨架剛體旋轉 + 平移迭代細化 |
| 訓練目標 | FAPE(Frame Aligned Point Error) | 預測坐標 vs 真實坐標的幾何損失 |
| Recycling | 迭代精化(3~4輪) | 模擬折疊的漸進收斂 |
| MDP要素 | 在分子設計中的對應 |
|---|---|
| State | 當前分子/序列的狀態表示 |
| Action | 添加原子、突變一個殘基、修改側鏈 |
| Reward | 預測的親和力、穩定性、可開發性評分 |
| Policy | 生成下一步修改的模型(可以是 LLM/GNN) |
| Episode | 從初始序列到最終設計序列的完整設計流程 |
問題:固定抗體框架,優化 CDR3 序列以提高對靶點的親和力
| RL設定 | 實作細節 |
|---|---|
| State | 當前 CDR 序列 + ESM-2 embedding + 結構 context |
| Action | 在某個位置替換為某個胺基酸(20 × CDR長度 種) |
| Reward | Rosetta 計算的結合能 or 代理模型預測的 Kd(需要 sign 轉換:-ΔΔG) |
| Policy | 自迴歸語言模型(微調的 ESM-2 作為 policy) |
直觀理解:讓 reward 高的序列生成概率上升,reward 低的下降。
這就是你熟悉的梯度上升,只是目標函數換成了期望 reward。
import torch
import torch.nn.functional as F
def reinforce_update(model, sequences, rewards, optimizer, baseline=None):
"""
REINFORCE 算法核心更新
sequences: 採樣到的序列(token ids 列表)
rewards: 對應的 reward(如親和力代理模型分數)
baseline: 方差縮減的基線值(通常用歷史 reward 的移動平均)
"""
if baseline is None:
baseline = rewards.mean()
advantages = rewards - baseline # 優勢函數:高於平均就鼓勵
total_loss = 0
for seq_tokens, advantage in zip(sequences, advantages):
# 計算策略的 log 概率
logits = model(seq_tokens[:-1]) # 預測下一個 token
log_probs = F.log_softmax(logits, dim=-1)
seq_log_prob = log_probs[range(len(seq_tokens)-1), seq_tokens[1:]].sum()
# REINFORCE 目標:最大化 E[R] = 梯度上升
loss = -advantage * seq_log_prob # 負號:因為 optimizer 做梯度下降
total_loss += loss
optimizer.zero_grad()
total_loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 防止梯度爆炸
optimizer.step()
return total_loss.item()
真實場景需要同時優化多個目標(可開發性三元組):
你的最佳化背景在這裡最有價值——如何設定權重、如何處理目標衝突(Pareto front)。
| 方法 | 適用場景 | 數學工具 |
|---|---|---|
| 加權和 | 目標間可量化 trade-off | 線性組合,需要調參 |
| Pareto RL | 需要找 trade-off frontier | 多目標優化、Hypervolume 指標 |
| 約束 RL | 某些指標必須達到硬性閾值 | Lagrangian duality + 對偶梯度下降 |
| Lexicographic | 目標有明確優先級 | 分層最佳化 |
| 維度 | 貝葉斯最佳化(BO) | 強化學習(RL) |
|---|---|---|
| 評估代價 | 高代價(少量實驗) | 低代價(模擬/代理模型) |
| 搜索空間 | 連續、低維(<20D) | 離散、高維、序列型 |
| 先驗知識 | GP kernel 編碼先驗 | Policy 架構編碼歸納偏置 |
| 數據效率 | 高(主動學習) | 低(需要大量樣本) |
| 可解釋性 | 高(GP 提供不確定性估計) | 低(policy 為黑箱) |
建議:先閉著答案想30秒,再展開參考回答。重點不是背答案,而是用「最佳化框架」解讀每個問題。
「AlphaFold2 本質是一個帶幾何約束的最佳化問題,目標是學習一個映射 f: 序列 → 3D坐標,損失函數是FAPE(Frame Aligned Point Error),在SE(3)空間度量剛體坐標的對齊誤差。
架構上有兩個關鍵創新:Evoformer用雙軸attention同時建模序列和殘基對關係,Structure Module用可微分的幾何操作迭代精化骨架坐標,類似約束最佳化的迭代细化。
能做: 單體蛋白質結構預測(從序列),精度接近實驗方法(PDB benchmark <1Å RMSD多數情況)。
不能做: 預測動態狀態(只給一個靜態構象);對本質無序蛋白(IDR)置信度低(pLDDT <70要注意);無法預測結合後的構象變化(需要AlphaFold-Multimer或Rosetta dock);不預測序列→功能。」
「我會先把這個問題框架化:目標函數是最大化CDR序列的抗原結合親和力(可用Kd或ΔΔG量化),約束包括可開發性(溶解度、免疫原性、PTM位點),搜索空間是CDR3位置的20^L種組合(L為序列長)。
方案一(數據豐富): 用現有親和力SAR數據微調ESM-2或IgLM,作為序列生成的prior;用PPO或REINFORCE微調,reward為代理模型預測的ΔΔG,加入可開發性作為約束reward。
方案二(數據稀缺): 用ESM-2 embedding作特徵,用貝葉斯最佳化做主動學習循環,每輪選擇EI最大的幾條候選序列送實驗,模型隨實驗數據迭代更新。這在早期項目、預算緊張時更高效。
我的優勢: 因為我有濕實驗背景,我能評估哪個方案在實際實驗流程中可落地,避免『模型很漂亮但實驗無法配合』的問題。」
「從最佳化的角度,兩者解決的問題相同——給定結構,找最佳序列——但目標函數本質不同。
Rosetta 使用手工設計的物理能量函數(Lennard-Jones、氫鍵項等),在這個能量函數上用Monte Carlo做序列搜索。它的問題是:能量函數是人對物理的近似,有誤差,而且計算慢(秒~分鐘/序列)。
ProteinMPNN 直接學習 P(sequence | structure),目標函數是最大化對數條件概率。它學到的不是物理規則,而是大量真實蛋白質數據中「哪種結構喜歡哪種序列」的統計模式。優點是能捕捉到能量函數沒有明確建模的交互,速度快几千倍。
但 ProteinMPNN 的局限是:它的生成域受訓練數據分布限制,對非天然骨架可能外推失效。在實際應用中,我會用 ProteinMPNN 快速生成大量候選序列,再用 Rosetta 對 top 候選做精細能量評估,兩者互補。」
「分子天然是圖結構:原子是節點,化學鍵/空間接觸是邊。這種結構有兩個特性使得傳統向量方法不適用:一是變長(不同分子/蛋白質原子數不同);二是不具位置不變性(分子圖的節點沒有固定的全局座位)。
GCN 的訊息傳遞機制天然滿足這兩個需求:無論分子多大,都可以用相同的訊息函數 Message(v_i, v_j, e_ij) 聚合局部鄰居信息;而且由於用的是局部聚合,對節點重排是不變的(置換不變性)。
在蛋白質結構中,每個殘基的化學性質由它周圍的環境決定(正是GCN的感受野概念),這和實驗中觀察到的接觸殘基協同進化現象完全吻合。這也是為什麼 ProteinMPNN 用圖上訊息傳遞比純序列模型在設計任務上效果好:它顯式用了結構圖的局部環境信息。」
「這是大多數計算背景的人容易忽略的問題,但實際上卻是整個pipeline最脆弱的部分。
數據清洗層面: 親和力測定(如SPR/ITC)的結果受surface density、參考buffer、批次等影響,直接拿 Kd 值做訓練標籤會引入系統誤差。我在實驗中學到:最好是用同批次內的相對排名(ordinal label)而不是絕對 Kd 值,或者對跨批次數據做 batch correction。
selection bias: 實驗通常只測定「看起來有希望」的候選,導致訓練數據分布偏向序列空間的特定區域,模型對未探索區域的外推能力差。主動學習(貝葉斯最佳化)能部分解決這個問題。
Label noise: 多次重複實驗之間的差異(尤其是 cell-based assay)可能超過 2~3倍,需要在損失函數中顯式建模不確定性,或用異方差回歸。」
「我會系統性地排查三個層次:
1. 是否是數據問題: 新實驗結果是否在訓練集的分布範圍內?如果不一致的序列和訓練數據差異很大,這是正常的外推誤差,不是模型bug。確認實驗assay條件是否有變化(batch effect)。
2. 是否是特徵問題: 模型用的特徵(如ESM embedding)是否捕獲了導致實驗差異的物理機制?例如:如果親和力差異來自特定的構象變化,而模型只看了一個靜態結構,那模型天然會盲。
3. 是否是模型問題: 在已知正確答案的留存集上,模型在類似序列上是否也有這種偏差?是系統性誤差(需要重新設計特徵)還是隨機誤差(需要更多數據)?
最後,我很重視把這些不一致的案例記錄下來,它們往往是模型改善最寶貴的信號。」
第 1 週
第 2 週
第 3 週
第 4 週
第 5 週
第 6 週
| 資源 | 用途 | 連結 |
|---|---|---|
| Hugging Face ESM-2 | Mini Project 特徵提取 | facebook/esm2_t6_8M_UR50D |
| ProteinGym | 突變穩定性數據集 | github.com/OATML-Markslab/ProteinGym |
| OpenAI Spinning Up | RL 基礎入門 | spinningup.openai.com |
| PyTorch Geometric | GCN 實作 | pytorch-geometric.readthedocs.io |
| BoTorch | 貝葉斯最佳化 | botorch.org |
| fast.ai Practical DL | PyTorch 補強 | fast.ai(免費) |