334

训练 ADMET 多任务模型:一次预测多个性质

多任务模型一次预测多个 ADMET 性质,能让数据少的终点从数据多的终点受益。这篇给出实现代码、稀疏标签处理与多任务是否有效的判断。

ADMET 建模的典型困境是每个终点的数据都不多,但终点之间有相关性——溶解度与 logP 相关、通透性与分子量相关。多任务学习让这些终点共享表示,理论上数据少的终点能从数据多的终点借力。

数据格式:稀疏标签

import pandas as pd

# 每列一个终点,缺失值留空 —— Chemprop 会自动只在有标签处回传梯度
data = pd.DataFrame({
    "smiles":     ["CCO", "CCN", "CCC", "CCCl"],
    "logS":       [0.5, None, -1.2, None],
    "logD":       [-0.3, 0.8, None, 1.5],
    "hERG_pIC50": [None, None, 4.2, 5.1],
    "HLM_CLint":  [12.0, 45.0, None, None],
})
data.to_csv("admet_multitask.csv", index=False)

# 检查标签稀疏度
for col in data.columns[1:]:
    n = data[col].notna().sum()
    print(f"{col:12s} {n:5d} 条 ({n/len(data):.1%})")

训练多任务模型

chemprop train \
  --data-path admet_multitask.csv \
  --task-type regression \
  --split-type scaffold_balanced \
  --num-folds 5 --ensemble-size 5 \
  --epochs 60 \
  --descriptors-generator rdkit_2d_normalized \
  --save-dir ckpt_admet/ \
  --metrics rmse mae

Chemprop 原生支持稀疏标签:某个分子在某任务上没有标签时,该任务的损失不计入梯度。这是它做 ADMET 多任务的主要便利。

混合回归与分类任务

ADMET 中常有回归(溶解度)与分类(是否为 P-gp 底物)混合。Chemprop 单次训练只支持一种任务类型,需分开处理:

# 方案一:分开训两个模型(最简单可靠)
chemprop train --data-path admet_regression.csv --task-type regression ...
chemprop train --data-path admet_classification.csv --task-type classification ...

# 方案二:把分类转成回归(若有连续的原始值)
# 如「是否 hERG 阳性」→ 直接用 pIC50 回归,更多信息

# 方案三:自己用 PyTorch 实现共享编码器 + 多个任务头
import torch, torch.nn as nn

class MultiTaskHead(nn.Module):
    def __init__(self, enc_dim, reg_tasks, clf_tasks):
        super().__init__()
        self.shared = nn.Sequential(
            nn.Linear(enc_dim, 512), nn.ReLU(), nn.Dropout(0.2))
        self.reg_head = nn.Linear(512, reg_tasks)
        self.clf_head = nn.Linear(512, clf_tasks)
    def forward(self, h):
        z = self.shared(h)
        return self.reg_head(z), self.clf_head(z)

def masked_loss(pred, target, mask, loss_fn):
    """只在有标签处计算损失"""
    if mask.sum() == 0:
        return torch.tensor(0.0, device=pred.device)
    return loss_fn(pred[mask], target[mask])

多任务真的有帮助吗:必须验证

多任务不是必然有益的——任务间若无相关性,共享表示反而会互相干扰(负迁移)。必须做对照:

results = {}

# 对照一:每个终点单独训练
for task in tasks:
    single_df = data[["smiles", task]].dropna()
    single_df.to_csv(f"single_{task}.csv", index=False)
    # chemprop train --data-path single_{task}.csv ...
    results[f"single_{task}"] = rmse_single

# 对照二:多任务训练后,逐任务评估
# results[f"multi_{task}"] = rmse_multi

# 比较
for task in tasks:
    s, m = results[f"single_{task}"], results[f"multi_{task}"]
    delta = (s - m) / s * 100
    flag = "✓ 受益" if delta > 3 else ("✗ 受损" if delta < -3 else "— 无差异")
    print(f"{task:12s} 单任务 {s:.3f} → 多任务 {m:.3f}  ({delta:+.1f}%) {flag}")

典型结果:数据量小的终点(几百条)常从多任务受益明显;数据量大的终点(上万条)通常无差异甚至略有下降。据此可以决定哪些终点放进多任务、哪些单独训。

任务权重与量纲

# 不同终点的量纲差别很大(logS 范围约 -8~2,CLint 可能 0~300),
# 不做归一化会让大量纲任务主导梯度

from sklearn.preprocessing import StandardScaler

scalers = {}
for task in reg_tasks:
    mask = data[task].notna()
    sc = StandardScaler()
    data.loc[mask, task] = sc.fit_transform(data.loc[mask, [task]])
    scalers[task] = sc          # 保存,预测后要反变换

# Chemprop 会自动做目标归一化,自建模型时必须自己处理

# 预测后反变换回原尺度
pred_original = scalers[task].inverse_transform(pred_scaled.reshape(-1, 1))

用哪些终点组合

组合 相关性 多任务效果
logP / logD / 溶解度 通常有益
Caco-2 / MDCK / PAMPA 通透性 通常有益
各 CYP 亚型抑制 常有益
微粒体 / 肝细胞清除率 中强 常有益
hERG 与溶解度 可能无益
毒性终点与理化性质 需验证

常见坑与提示

  • 缺失标签留空即可,Chemprop 自动只在有标签处回传梯度;
  • 必须做单任务对照——多任务不必然有益,可能发生负迁移;
  • 自建模型时务必对各任务做归一化,否则大量纲任务主导梯度;
  • 数据量小的终点最可能从多任务受益,大数据终点通常无差异。

延伸资源