深度学习药物相互作用预测:从数据构建到模型选型与避坑指南
简介这份资源是面向医药信息学、生物信息学方向的学习者与研究者整理的深度学习实战项目包聚焦药物相互作用预测这一交叉课题适合具备Python基础、希望了解图神经网络与分子表示建模的中高级读者参考。压缩包共19个文件以13个py脚本和3个ipynb笔记本为主辅以2张模型架构与图结构示意图及1个依赖说明文件整体约621KB结构紧凑便于快速上手。内容围绕decagon模型展开涵盖药物分子SMILES编码与指纹特征预处理、图神经网络对分子拓扑结构的建模、二元分类标签构建、模型训练与交叉验证、AUC-ROC与AUPRC等指标评估以及SHAP等可解释性分析思路并配有探索性分析与测试数据集笔记本。目前已有192人学习下载可作为复现药物相互作用预测流程、理解GNN在药理数据上应用的参考范例。1. 药物相互作用预测为什么值得用深度学习重做一遍两个药一起吃会不会出事这个问题在临床上每天都要回答无数次。传统做法靠药代动力学实验、靠文献检索、靠药师经验覆盖的药物对数量极其有限。已知上市药物两两组合是万级到十万级的天文数字靠人力穷举根本不现实。基于深度学习的药物相互作用预测本质上是把「这对药能不能一起吃」变成一个二分类或多分类问题用药物分子结构、靶点、酶、通路等特征训练模型让模型去推断那些还没被实验验证过的组合。它解决的是「覆盖率」和「成本」两个死结实验做不完的模型先筛一遍把高风险组合挑出来优先验证。适合谁做药物警戒、临床决策支持、药企早期筛选、以及想拿深度学习做真实生物医药项目的工程师。这个方向数据公开、任务定义清晰、模型可解释性有抓手是少有的「学术能发论文、工业能落地」的交叉领域。2. 药物相互作用预测的数据从哪来、特征怎么建2.1 三类主流数据源和它们的取舍做这个任务第一步不是搭模型是搞清楚你手里有什么数据。常见的数据源分三类各有各的坑。第一类是药物分子结构数据最典型的是 SMILES 字符串和分子指纹。SMILES 是药物的文本表示比如阿司匹林是CC(O)Oc1ccccc1C(O)O。它的好处是几乎任何药物都能拿到坏处是字符串本身没有显式编码化学性质需要模型自己去学。分子指纹如 Morgan fingerprint、MACCS keys是把结构压成固定长度的 0/1 向量计算快、可复现但会丢失部分拓扑信息。第二类是生物实体关联数据包括靶点蛋白、代谢酶尤其是 CYP450 家族、转运体、通路。药物相互作用很大一部分机制是「A 药抑制了代谢 B 药的酶」所以酶和靶点的共享关系是强特征。这类数据通常从 DrugBank、PubChem、ChEMBL 这类公开库整理但要注意版本差异——不同年份的库同一个药物的靶点标注可能不一样。第三类是已知相互作用标签也就是监督学习的 y。DrugBank 的 DDI 表是最常用的但它有个致命问题正样本多、负样本少且不可靠。没有标注相互作用的药物对不代表真的没有相互作用可能只是没人研究过。这是整个任务最大的数据陷阱后面避坑章节会细说。数据源典型来源特征形式主要问题分子结构PubChem、DrugBankSMILES / 指纹需自行编码指纹丢信息生物实体DrugBank、ChEMBL靶点/酶多热向量版本不一致缺失多相互作用标签DrugBank DDI二分类/多分类负样本不可靠2.2 把 SMILES 变成模型能吃的张量分子结构编码是绕不开的一步。我一般会同时准备两套特征一套指纹向量给传统模型和快速基线一套字符级序列给深度模型。下面这段代码把 SMILES 转成 Morgan 指纹并做基本的合法性校验。from rdkit import Chem from rdkit.Chem import AllChem import numpy as np def smiles_to_fingerprint(smiles, radius2, n_bits2048): 把 SMILES 转成 Morgan 指纹向量。 radius: 考虑周围几层原子2 是常用值 n_bits: 指纹长度2048 在药物任务里够用 mol Chem.MolFromSmiles(smiles) if mol is None: return None # 非法 SMILES必须过滤否则后面全崩 fp AllChem.GetMorganFingerprintAsBitVect(mol, radius, nBitsn_bits) return np.array(fp, dtypenp.float32) # 批量处理记录失败的样本 def build_fingerprint_matrix(smiles_list): feats, valid_idx [], [] for i, smi in enumerate(smiles_list): fp smiles_to_fingerprint(smi) if fp is not None: feats.append(fp) valid_idx.append(i) return np.stack(feats), valid_idx逻辑说明MolFromSmiles返回 None 说明这个 SMILES 写错了或者 RDKit 解析不了必须丢弃并记录索引否则后续矩阵对齐会错位。radius2对应 ECFP4是药物化学里被验证过最稳的默认值n_bits2048是精度和内存的折中药物分子原子数一般不超过 1002048 位足够稀疏表达。参数怎么改如果你的药物分子特别大比如多肽类radius 可以提到 3n_bits 提到 4096但要注意特征维度上升后小数据集容易过拟合。2.3 负样本构造这一步决定模型上限正样本从 DrugBank 拿负样本怎么办直接随机采样是最常见的做法但会引入大量「假阴性」——你随机抽的一对药可能其实有相互作用只是没被记录。我的经验是分层构造先从「已知无相互作用」的明确记录里取一部分再用「靶点/酶完全不重叠」的药物对补充最后才用随机采样兜底。比例上正负 1:1 到 1:3 之间比较稳负样本太多会让模型偏向多数类。import random def build_negative_samples(drug_ids, positive_pairs, ratio2): 基于随机采样构造负样本ratio 为负正比。 注意这只是基线做法生产环境要叠加规则过滤。 pos_set set(positive_pairs) negatives [] target len(positive_pairs) * ratio while len(negatives) target: a, b random.sample(drug_ids, 2) if (a, b) not in pos_set and (b, a) not in pos_set: negatives.append((a, b)) return negatives这段代码能跑但我要提醒它没有做任何机制层面的过滤。真正上线前至少要把「共享靶点」「共享代谢酶」的药物对从负样本里剔除否则模型学到的只是「共享靶点有相互作用」这个捷径换个数据集就翻车。3. 模型选型从指纹 MLP 到图神经网络怎么选3.1 基线模型先跑通别一上来就上 GNN很多人的第一反应是直接上图神经网络觉得分子天然是图结构。但我的血泪经验是先用指纹 MLP 跑一个基线把数据管道、评估指标、划分方式全部验证一遍再考虑复杂模型。原因很简单如果基线只有 0.6 的 AUC你换成 GNN 大概率也就 0.65问题出在数据不在模型。基线模型的结构很朴素两个药物的指纹各过一个编码器拼接后接分类头。import torch import torch.nn as nn class DDI_MLP(nn.Module): def __init__(self, fp_dim2048, hidden512, n_class2): super().__init__() self.encoder nn.Sequential( nn.Linear(fp_dim, hidden), nn.ReLU(), nn.Dropout(0.3), # 药物特征维度高dropout 必加 nn.Linear(hidden, hidden // 2), nn.ReLU(), ) self.classifier nn.Sequential( nn.Linear(hidden, 128), nn.ReLU(), nn.Linear(128, n_class), ) def forward(self, fp_a, fp_b): h_a self.encoder(fp_a) h_b self.encoder(fp_b) # 拼接 逐元素乘积乘积能捕捉两药特征的交互 combined torch.cat([h_a, h_b, h_a * h_b], dim-1) return self.classifier(combined)逻辑说明两个药物共享同一个 encoder这是合理的因为药物特征空间是同一套。h_a * h_b这个逐元素乘积是关键它显式建模了两个药物特征的交互比单纯拼接效果好。参数上hidden512对 2048 维指纹是合适的压缩比dropout 0.3 是防止高维特征过拟合的常用值。如果你的数据集小于 5000 对dropout 可以提到 0.5。3.2 图神经网络什么时候真正带来增益GNN 的价值在于它直接从原子和键的图结构学表示不需要人工设计指纹。常见做法是用 GCN 或 GIN 编码每个药物分子图得到图级表示后再做交互。什么时候 GNN 会明显超过指纹基线我的观察是当你的数据量足够大万级以上药物对且任务依赖精细的局部结构时。如果数据只有几千对GNN 参数多、容易过拟合反而不如指纹稳。from torch_geometric.nn import GINConv, global_add_pool class DrugGNN(nn.Module): def __init__(self, node_dim78, hidden128, n_layer3): super().__init__() self.convs nn.ModuleList() for _ in range(n_layer): mlp nn.Sequential(nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, hidden)) self.convs.append(GINConv(mlp)) self.input_proj nn.Linear(node_dim, hidden) def forward(self, x, edge_index, batch): h self.input_proj(x) for conv in self.convs: h conv(h, edge_index).relu() return global_add_pool(h, batch) # 图级表示逻辑说明node_dim78是原子特征的标准维度原子类型、度、电荷、手性等 one-hot 拼接。n_layer3意味着每个原子能感知到 3 跳以内的邻居对大多数药物分子够用层数再深会出现过平滑。global_add_pool把原子表示聚合成分子表示也可以用 mean 或 attention pooladd 对分子大小不敏感是我更常用的选择。3.3 交互建模拼接、双线性还是注意力两个药物的表示拿到之后怎么建模它们的交互直接决定模型上限。最简单的拼接在基线里够用但如果你想再往上推双线性池化和注意力是两条路。双线性用一个小矩阵 W 建模 h_a 和 h_b 的二次交互参数量可控注意力则让模型自己决定关注哪些特征维度。交互方式参数量适用场景注意点拼接低基线、小数据交互靠后续全连接隐式学逐元素乘积低通用增强与拼接叠加最稳双线性中中等数据需低秩分解防过拟合注意力高大数据要足够数据才训得动我一般的组合是「拼接 逐元素乘积」作为默认数据量过万再考虑加双线性。注意力机制在 DDI 任务上不是必须的除非你要做可解释性分析想看模型关注了哪些子结构。4. 训练与评估别被虚高的 AUC 骗了4.1 数据划分方式决定你的指标可不可信这是整个任务里最容易翻车的地方。如果你随机划分训练集和测试集模型会记住某些药物测试集里出现的药物在训练集里也出现过指标会虚高得离谱。正确的做法是按药物划分测试集里的药物在训练集里完全不出现。这才模拟了「预测新药相互作用」的真实场景。from sklearn.model_selection import GroupShuffleSplit def split_by_drug(pairs, groups, test_size0.2): 按药物分组划分保证测试集药物不出现在训练集。 groups 是每个样本对应的药物 id 列表取其中一个即可。 gss GroupShuffleSplit(n_splits1, test_sizetest_size, random_state42) train_idx, test_idx next(gss.split(pairs, groupsgroups)) return train_idx, test_idx逻辑说明GroupShuffleSplit保证同一组的样本不会同时出现在训练和测试。groups 传每个药物对里的第一个药物 id 就行。参数test_size0.2是常规比例但如果你的药物总数少测试集药物太少会导致指标方差大这时候要做交叉验证而不是单次划分。4.2 类别不平衡和阈值选择DDI 数据里严重的相互作用比如禁忌往往只占很小比例。如果你做多分类无/弱/中/强类别不平衡会非常明显。处理方式有三种重采样、类别加权损失、以及调整预测阈值。我一般用加权交叉熵简单有效。# 按类别频率的倒数设置权重 class_counts torch.bincount(train_labels) weights 1.0 / class_counts.float() weights weights / weights.sum() criterion nn.CrossEntropyLoss(weightweights)逻辑说明bincount统计每个类别的样本数取倒数让稀有类获得更大权重。归一化是为了让权重尺度稳定不影响学习率。注意权重不要设得太极端否则模型会把所有样本都预测成稀有类召回上去了但精确率崩了。阈值选择上不要用默认的 0.5要在验证集上画 PR 曲线根据你的业务需求选——药物警戒场景宁可误报也不能漏报阈值要往低里调。4.3 评估指标AUC 之外必须看的东西AUC 是标配但它对类别不平衡不敏感容易给人虚假的安全感。我必看的还有三个AUPRC不平衡数据更真实、召回率高精度业务关心的区间、以及按相互作用类型分层的指标。如果模型在「禁忌」类上召回很低那这个模型上线就是灾难。指标含义什么时候重点看AUC整体排序能力快速对比模型AUPRC正类识别能力类别不平衡时RecallPrecision指定精度下召回业务阈值选择分层指标各类型表现上线前必查5. 避坑指南五个让模型翻车的真实问题5.1 负样本假阴性导致指标虚高现象模型在测试集上 AUC 0.95一换数据集掉到 0.7。原因负样本是随机采样的里面混了大量实际有相互作用但没被标注的药物对。模型学到的是「没标注无相互作用」这个错误信号。解决负样本构造时叠加机制过滤剔除共享靶点、共享代谢酶的药物对同时用「已知无相互作用」的明确记录优先填充。5.2 按药物划分后指标暴跌现象随机划分 AUC 0.92按药物划分只有 0.68。原因随机划分下模型记住了药物身份测试集药物在训练集见过。按药物划分才是真实泛化能力。解决接受这个更低的数字它才是真的。如果按药物划分指标太低说明模型学的是药物记忆而非相互作用机制要回去改特征和交互建模。5.3 SMILES 解析失败静默丢样本现象训练正常但预测时某些药物报错或结果异常。原因RDKit 解析非法 SMILES 返回 None如果代码里没检查None 会传播到后面导致维度错乱或静默跳过。解决所有 SMILES 入口都做MolFromSmiles校验记录失败列表人工核对。生产环境要有兜底解析失败的药物走指纹缓存或直接拒绝预测。5.4 特征泄漏靶点信息混进了标签现象模型指标好得不真实但换一批药就废。原因构造特征时用了「该药物对是否有相互作用」相关的信息比如从 DDI 记录里反推的靶点关联等于把答案喂给了模型。解决严格区分特征构建阶段和标签使用阶段。靶点、酶特征只能来自药物本身的独立注释不能来自 DDI 记录。做特征时问自己一句这个特征在预测时拿得到吗5.5 过平滑让深层 GNN 失效现象GNN 层数加到 5 层以上效果不升反降。原因图卷积反复聚合邻居所有原子表示趋同丢失区分度这就是过平滑。解决层数控制在 3 到 4 层加残差连接或 Jumping Knowledge 结构或者干脆回到指纹基线。不是越深越好分子图通常很小3 层足够覆盖大部分拓扑。6. 把模型推到可用的几个进阶技巧模型能跑出指标只是起点要真正可用还有几件事值得做。第一个是集成把指纹 MLP 和 GNN 的预测做加权平均两者犯错模式不同集成后通常能涨 2 到 3 个点。权重不用调太细0.5 比 0.5 起步在验证集上微调即可。第二个是不确定性估计用 MC Dropout 或者深度集成对每个预测给出置信度低置信度的样本交给人工复核这在药物警戒场景比单纯提高准确率更有价值。def predict_with_uncertainty(model, fp_a, fp_b, n_forward20): MC Dropout 估计预测不确定性。 推理时保持 dropout 开启多次前向取均值和方差。 model.train() # 关键保持 dropout 激活 preds [] with torch.no_grad(): for _ in range(n_forward): logits model(fp_a, fp_b) preds.append(torch.softmax(logits, dim-1)) preds torch.stack(preds) mean preds.mean(dim0) std preds.std(dim0) return mean, std逻辑说明model.train()让 dropout 在推理时也生效每次前向得到略有不同的结果多次采样后方差就是不确定性的代理。n_forward20是精度和耗时的折中一般 10 到 30 之间。拿到 std 后可以设一个阈值std 高于阈值的预测标记为「需人工复核」这样模型不是替代人而是帮人排优先级。第三个技巧是可解释性回溯。用 GNN 的时候可以用 GNNExplainer 或者注意力权重看模型对哪几个子结构最敏感。如果模型判断某对药有相互作用是因为两个药共享了某个反应性官能团这个信息对药师来说比一个概率值有用得多。我一般会把 top-5 重要子结构可视化出来附在预测结果旁边。最后一个习惯永远保留一个「傻瓜基线」。我用「两药是否共享靶点」这个规则做基线如果深度学习模型打不过这个规则那说明模型没学到东西别急着上线。这个习惯帮我省过好几次面子——有次模型 AUC 0.85 看着不错结果规则基线 0.83等于深度学习白做。后来回去查发现是特征工程里靶点信息编码方式有问题。做这个方向敬畏数据比迷信模型重要得多。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →