简介本资源是一套面向计算机及相关专业本科生的课程设计与期末大作业实战项目聚焦生物信息学前沿交叉场景——利用知识图谱与推荐系统联合建模预测药物-靶点相互作用。项目代码完整、结构清晰涵盖数据预处理hetionet.py、yamanishi_08.py、知识图谱构建BioKG.py、多种推荐模型实现deepdti.py、kge_rf.py、kge_nfm.py及训练评估全流程配套详细操作指南与环境配置说明Pipfile、requirements.txt、README.md。压缩包共40个文件主体为9个核心Python脚本、1个说明文档、1个许可证及若干配置与日志文件总大小仅56KB轻量易部署。已有94人下载学习适合初学者快速上手知识图谱嵌入与协同过滤在生物医药领域的落地实践可直接用于课程设计复现、算法对比实验或模型微调拓展。1. 项目缘起当药物研发遇上推荐算法最近在整理一个老项目的代码发现几年前做的一个药物靶点预测工具核心思路挺有意思的不是传统的分子对接模拟而是把药物和靶点蛋白当成“用户”和“商品”用推荐系统的思路来预测它们之间会不会“看对眼”。这个项目当时是为了验证一个想法在已知的药物-靶点相互作用DTI数据稀疏得像撒了芝麻的大饼时能不能用知识图谱把药物、靶点、疾病、副作用这些实体关联起来然后用协同过滤这类推荐算法去挖掘潜在的、还没被实验验证的相互作用。这听起来有点跨界但逻辑是通的。你想在电商平台系统通过分析用户的历史购买交互记录和商品属性能推荐你可能会喜欢的新商品。在这里我们把“用户”换成药物分子“商品”换成蛋白质靶点“购买记录”就是已知的、经过验证的相互作用。那些还没被记录的药物-靶点对就成了待推荐的“潜在商品”。知识图谱的作用就是提供了更丰富的“用户画像”和“商品描述”比如药物A治疗什么病、有什么副作用、化学结构属于哪一类靶点蛋白B参与哪些信号通路、在哪些组织中高表达、和哪些疾病相关。这些关联信息能极大地弥补单纯交互数据的稀疏性让推荐预测更准。今天我就把这个项目的Python实现核心逻辑、踩过的坑以及一个完整的操作指南复盘出来。如果你正在做生物信息学、计算药物发现或者单纯对如何将推荐系统和图神经网络GNN应用到非传统领域感兴趣这篇内容应该能给你一些直接的代码参考和思路启发。整个项目不依赖特别昂贵的商业软件主要用PyTorch、PyTorch GeometricPyG和Neo4j可选代码结构力求清晰方便你跑通和修改。2. 核心架构拆解从数据到预测的流水线整个项目的流程可以看作一个标准的数据处理、特征工程、模型训练和评估的流水线。但关键在于每个环节都融入了“图”和“推荐”的思想。2.1 数据层构建药物-靶点-疾病知识图谱数据是模型的基石。我们需要的核心数据包括已知的药物-靶点相互作用DTI这是我们的“正样本”可以从公开数据库如DrugBank、ChEMBL、BindingDB中获取。数据格式通常是一个列表每一行是(Drug_ID, Target_ID, Interaction_Score/Type)。药物和靶点的特征药物特征可以是分子指纹如ECFP4、MACCS keys、分子描述符如LogP、分子量或预先训练好的分子图神经网络GNN编码得到的向量。靶点特征通常是蛋白质序列衍生的特征如氨基酸组成、PSSM位置特异性评分矩阵或者使用蛋白质语言模型如ESM-2编码得到的嵌入向量。辅助关系数据用于构建知识图谱药物-疾病关联什么药治什么病靶点-疾病关联什么蛋白与什么病相关药物-副作用关联靶点-通路关联药物-药物相似性基于结构或作用机制靶点-靶点相似性基于序列或功能我们的第一步就是把这些离散的数据表构建成一个统一的知识图谱。这里我提供了两种实践路径路径A使用Neo4j图数据库推荐用于探索和复杂查询Neo4j能直观地存储和查询关系数据。你可以通过py2neo这个Python驱动来操作。from py2neo import Graph, Node, Relationship # 连接本地Neo4j数据库假设默认端口7687无密码 graph Graph(bolt://localhost:7687, auth(neo4j, password)) # 定义节点和关系类型 def create_drug_node(drug_id, name, smiles): node Node(Drug, drug_iddrug_id, namename, smilessmiles) graph.create(node) return node def create_target_node(target_id, name, uniprot_id): node Node(Target, target_idtarget_id, namename, uniprot_iduniprot_id) graph.create(node) return node def create_interaction(drug_node, target_node, evidence): rel Relationship(drug_node, INTERACTS_WITH, target_node, evidenceevidence) graph.create(rel) # 示例添加一个药物节点和一个靶点节点并建立关系 drug create_drug_node(DB00173, Metformin, CN(C)C(N)NC(N)N) target create_target_node(P35568, IRS1, P35568) create_interaction(drug, target, Ki10 uM)在Neo4j中你可以很方便地执行图查询例如“找出与药物A相互作用的所有靶点以及这些靶点关联的所有疾病”。这些查询结果可以作为后续模型丰富的上下文信息。路径B使用PyTorch GeometricPyG的Data对象推荐用于直接输入GNN模型如果你打算直接用GNN模型在图上进行端到端学习那么将知识图谱转换为PyG的Data或HeteroData对象更高效。这里我们需要处理异构图多种节点和边类型。import torch from torch_geometric.data import HeteroData data HeteroData() # 假设我们有药物和靶点两种节点以及它们之间的相互作用边 # 1. 添加节点特征 num_drugs 1000 num_targets 500 drug_feat_dim 1024 # 例如1024位分子指纹 target_feat_dim 1280 # 例如ESM-2嵌入维度 data[drug].x torch.randn(num_drugs, drug_feat_dim) # 药物节点特征 data[target].x torch.randn(num_targets, target_feat_dim) # 靶点节点特征 # 2. 添加边索引已知的相互作用 # edge_index_drug_to_target 是一个 [2, num_edges] 的tensor第一行是药物索引第二行是对应靶点索引 # 这里假设我们从数据集中加载了已知的相互作用对 known_interactions torch.tensor([[0, 1, 2], [10, 20, 30]]) # 示例药物0-靶点10, 药物1-靶点20, 药物2-靶点30 data[drug, interacts_with, target].edge_index known_interactions # 3. 可以继续添加其他类型的边例如药物-疾病关联 # data[drug, treats, disease].edge_index ...使用HeteroData对象我们可以将整个知识图谱包括多种节点和关系打包成一个数据结构直接喂给异构图神经网络如RGCN、HGT进行学习。实操心得1数据质量决定天花板公开数据库的数据往往存在噪声、不一致和缺失。在构建图谱前必须进行严格的数据清洗统一ID命名空间如将药物映射到PubChem CID或DrugBank ID将靶点映射到UniProt ID处理重复记录对于冲突的证据如一个文献说结合另一个说不结合需要制定仲裁规则如采纳更高置信度的来源。这一步耗时最长但也是最关键的一步脏数据进去垃圾预测出来。2.2 特征工程如何让药物和靶点“数字化”模型不认识分子结构或蛋白质序列它只认识数字向量。因此为药物和靶点生成有意义的特征向量至关重要。对于药物小分子分子指纹首选速度快可解释性中等使用RDKit计算。from rdkit import Chem from rdkit.Chem import AllChem def smiles_to_ecfp(smiles, radius2, nBits1024): mol Chem.MolFromSmiles(smiles) if mol is None: return None fp AllChem.GetMorganFingerprintAsBitVect(mol, radius, nBitsnBits) return torch.tensor(fp, dtypetorch.float)预训练GNN编码器效果可能更好但计算成本高使用预训练模型如ChemBERTa或GROVER将分子图编码为向量。这相当于得到了一个包含丰富化学信息的“分子嵌入”。对于靶点蛋白质预训练蛋白质语言模型嵌入当前主流且有效使用ESM-2或ProtBERT等模型。你可以直接使用Hugging Facetransformers库提取最后一个隐藏层的平均池化作为特征。from transformers import AutoTokenizer, AutoModel import torch tokenizer AutoTokenizer.from_pretrained(facebook/esm2_t6_8M_UR50D) model AutoModel.from_pretrained(facebook/esm2_t6_8M_UR50D) def protein_to_embedding(sequence): inputs tokenizer(sequence, return_tensorspt, paddingTrue, truncationTrue, max_length1024) with torch.no_grad(): outputs model(**inputs) # 取最后一层隐藏状态并对序列长度维度取平均得到每个蛋白质的向量 embedding outputs.last_hidden_state.mean(dim1) return embedding.squeeze()传统特征如氨基酸组成、自相关描述符等在深度学习时代效果通常不如语言模型嵌入。实操心得2特征归一化与对齐药物特征如1024维指纹和靶点特征如1280维ESM嵌入维度不同量纲也可能不同。在将它们拼接或输入某些模型前必须进行归一化。我常用sklearn的StandardScaler分别对药物特征矩阵和靶点特征矩阵进行标准化按特征维度即列方向。同时确保你的训练集和测试集使用同一个Scaler用训练集的均值和方差去变换测试集数据这是避免数据泄露的常见坑。2.3 模型选型推荐系统算法的跨界应用这是项目的核心。我们将药物-靶点预测问题形式化为一个链接预测问题给定一个二分图其中一部分节点是药物另一部分是靶点已知的相互作用是边目标是预测哪些缺失的边药物-靶点对很可能存在。方案一基于矩阵分解的协同过滤浅层模型可解释性强这直接类比推荐系统中的用户-物品评分矩阵。我们构建一个矩阵R行是药物列是靶点已知的相互作用位置有值可以是1也可以是结合强度如pKi未知位置为0。目标是将R分解为两个低维矩阵的乘积R ≈ P * Q^T其中P是药物隐因子矩阵Q是靶点隐因子矩阵。import torch.nn as nn import torch.nn.functional as F class MatrixFactorization(nn.Module): def __init__(self, n_drugs, n_targets, latent_dim): super().__init__() self.drug_embedding nn.Embedding(n_drugs, latent_dim) self.target_embedding nn.Embedding(n_targets, latent_dim) # 初始化 nn.init.xavier_uniform_(self.drug_embedding.weight) nn.init.xavier_uniform_(self.target_embedding.weight) def forward(self, drug_idx, target_idx): drug_vec self.drug_embedding(drug_idx) # [batch_size, latent_dim] target_vec self.target_embedding(target_idx) # [batch_size, latent_dim] # 内积作为预测得分 prediction (drug_vec * target_vec).sum(dim1) return prediction # 训练时使用已知的相互作用对作为正样本并采样一些未观察到的对作为负样本。 # 损失函数可以用BPRBayesian Personalized Ranking损失或均方误差MSE。优点简单、快速、易于实现。隐因子可以看作药物和靶点在潜在空间中的表示。缺点无法利用药物和靶点自身的特征只能通过ID也无法利用知识图谱中除直接相互作用外的其他关系。方案二神经网络协同过滤DeepCF在矩阵分解的基础上引入深度神经网络来学习药物和靶点ID嵌入的非线性交互函数。class NeuralCF(nn.Module): def __init__(self, n_drugs, n_targets, latent_dim, hidden_dims): super().__init__() self.drug_embedding nn.Embedding(n_drugs, latent_dim) self.target_embedding nn.Embedding(n_targets, latent_dim) # 交互学习层 layers [] input_dim latent_dim * 2 # 将药物和靶点嵌入拼接 for h_dim in hidden_dims: layers.append(nn.Linear(input_dim, h_dim)) layers.append(nn.ReLU()) layers.append(nn.Dropout(0.2)) input_dim h_dim layers.append(nn.Linear(input_dim, 1)) self.mlp nn.Sequential(*layers) def forward(self, drug_idx, target_idx): drug_vec self.drug_embedding(drug_idx) target_vec self.target_embedding(target_idx) concat_vec torch.cat([drug_vec, target_vec], dim1) prediction self.mlp(concat_vec).squeeze() return prediction方案三图神经网络GNN - 利用知识图谱结构效果潜力大这是最能发挥我们构建的知识图谱价值的方法。我们使用异构图神经网络如RGCN、HGT直接在整个知识图谱上进行消息传递和学习节点表示。import torch.nn.functional as F from torch_geometric.nn import HeteroConv, GCNConv, SAGEConv, Linear class HeteroGNN(torch.nn.Module): def __init__(self, hidden_channels, out_channels, num_layers, node_types, edge_types): super().__init__() # 首先为每种节点类型定义一个线性变换层将原始特征映射到隐藏空间 self.lin_dict torch.nn.ModuleDict() for node_type in node_types: self.lin_dict[node_type] Linear(-1, hidden_channels) # -1表示自动推断输入维度 # 然后堆叠多个异构图卷积层 self.convs torch.nn.ModuleList() for _ in range(num_layers): conv HeteroConv({ # 这里为每种边类型指定一个图卷积算子例如GCNConv edge_type: SAGEConv((-1, -1), hidden_channels) for edge_type in edge_types }, aggrsum) self.convs.append(conv) # 最终的输出层为每种节点类型映射到输出维度 self.lin_out torch.nn.ModuleDict() for node_type in node_types: self.lin_out[node_type] Linear(hidden_channels, out_channels) def forward(self, x_dict, edge_index_dict): # 1. 特征投影 for node_type, x in x_dict.items(): x_dict[node_type] self.lin_dict[node_type](x).relu() # 2. 消息传递 for conv in self.convs: x_dict conv(x_dict, edge_index_dict) x_dict {key: F.relu(x) for key, x in x_dict.items()} # 3. 最终输出 out_dict {} for node_type, x in x_dict.items(): out_dict[node_type] self.lin_out[node_type](x) return out_dict # 模型输出是所有节点的最终表示。对于链接预测我们需要一个解码器Decoder。 # 常用的是对一对节点药物靶点的表示进行内积或MLP计算得到交互得分。 class Decoder(torch.nn.Module): def __init__(self, in_channels): super().__init__() self.mlp nn.Sequential( nn.Linear(in_channels * 2, in_channels), nn.ReLU(), nn.Linear(in_channels, 1) ) def forward(self, drug_emb, target_emb): pair_emb torch.cat([drug_emb, target_emb], dim-1) score self.mlp(pair_emb).squeeze() return score训练GNN进行链接预测时我们需要正样本已知边和负样本随机采样的不存在边。损失函数通常使用二元交叉熵BCEWithLogitsLoss或Margin Ranking Loss。实操心得3负样本的采样策略是GNN链接预测的关键随机采样负样本即随机选择一对没有边的药物-靶点是最简单的但可能不够“硬”。因为很多随机对确实不太可能相互作用。更好的策略是采用“基于度的负采样”或“对抗性负采样”让模型学习区分更难的负样本。一个简单的改进是对于每个正样本边药物i-靶点j在采样负样本时固定药物i随机选择一个不与药物i相连的靶点k同时固定靶点j随机选择一个不与靶点j相连的药物l。这样生成的负样本更接近决策边界有助于模型学到更精细的区分能力。2.4 训练与评估生物信息学场景下的特殊考量药物靶点预测本质上是一个极度不平衡的分类/排序问题。已知的相互作用正样本相对于所有可能的药物-靶点对来说只是沧海一粟。因此评估指标不能只看准确率。1. 数据集划分切忌随机划分因为同一个药物可能与多个靶点作用。如果随机划分可能导致同一个药物出现在训练集和测试集造成信息泄露使评估结果过于乐观。必须采用按药物或按靶点冷启动划分按药物冷启动将一部分药物以及它们所有的相互作用边完全放入测试集模型在训练时从未见过这些药物。这模拟了预测全新药物靶点的场景。按靶点冷启动同理将一部分靶点完全放入测试集。2. 评估指标AUROC (Area Under the ROC Curve)常用衡量模型将正样本排序高于负样本的整体能力。AUPRC (Area Under the Precision-Recall Curve)在正负样本极度不平衡时比AUROC更敏感、更具参考价值。因为PR曲线聚焦于正样本的检索性能。Top-k Hit Rate对于测试集中的每个药物或靶点模型对其所有候选靶点或药物进行打分看真实相互作用的靶点是否出现在预测得分最高的前k个里面。这模拟了实际药物重定位中研究者只关注最有可能的前几个候选靶点的场景。3. 损失函数对于二分类任务使用BCEWithLogitsLoss。对于排序任务可以使用MarginRankingLoss或BPRLoss。import torch from sklearn.metrics import roc_auc_score, average_precision_score def evaluate(model, data_loader, device): model.eval() all_preds [] all_labels [] with torch.no_grad(): for batch in data_loader: drug_idx, target_idx, label batch drug_idx, target_idx, label drug_idx.to(device), target_idx.to(device), label.to(device) # 假设我们的模型输出的是logits pred model(drug_idx, target_idx) all_preds.append(pred.cpu()) all_labels.append(label.cpu()) all_preds torch.cat(all_preds) all_labels torch.cat(all_labels) # 计算指标 auroc roc_auc_score(all_labels.numpy(), torch.sigmoid(all_preds).numpy()) auprc average_precision_score(all_labels.numpy(), torch.sigmoid(all_preds).numpy()) return auroc, auprc3. 完整操作指南从零跑通一个预测流程假设我们已经准备好了清洗后的数据下面是一个基于矩阵分解MF的简化版端到端流程。选择MF是因为它代码简洁依赖少能快速验证流程。3.1 环境准备与数据加载首先创建一个新的conda环境并安装依赖。conda create -n dti_pred python3.9 conda activate dti_pred pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 根据你的CUDA版本选择 pip install scikit-learn pandas numpy tqdm假设我们有一个CSV文件dtis.csv包含三列drug_id,target_id,label1表示已知相互作用。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split import torch from torch.utils.data import Dataset, DataLoader # 加载数据 df pd.read_csv(dtis.csv) # 为药物和靶点创建映射字典 unique_drugs df[drug_id].unique() unique_targets df[target_id].unique() drug_to_idx {drug: i for i, drug in enumerate(unique_drugs)} target_to_idx {target: i for i, target in enumerate(unique_targets)} df[drug_idx] df[drug_id].map(drug_to_idx) df[target_idx] df[target_id].map(target_to_idx) # 构建正样本对 positive_pairs df[[drug_idx, target_idx]].values positive_labels np.ones(len(positive_pairs))3.2 负样本生成与数据集构建我们需要生成负样本标签为0。采用每个正样本对应采样一个负样本的策略。def generate_negative_samples(positive_pairs, num_drugs, num_targets, seed42): np.random.seed(seed) negative_pairs [] # 为每个正样本生成一个负样本 for d_idx, t_idx in positive_pairs: # 随机选择一个靶点确保不是当前药物对应的正样本靶点 # 这里简化处理随机选如果撞上正样本就重选概率很低 neg_t_idx np.random.randint(num_targets) while (d_idx, neg_t_idx) in set(map(tuple, positive_pairs)): neg_t_idx np.random.randint(num_targets) negative_pairs.append([d_idx, neg_t_idx]) return np.array(negative_pairs) num_drugs len(unique_drugs) num_targets len(unique_targets) negative_pairs generate_negative_samples(positive_pairs, num_drugs, num_targets) negative_labels np.zeros(len(negative_pairs)) # 合并正负样本 all_pairs np.vstack([positive_pairs, negative_pairs]) all_labels np.hstack([positive_labels, negative_labels])3.3 实现矩阵分解模型使用PyTorch实现一个简单的矩阵分解模型。import torch.nn as nn import torch.nn.functional as F class DrugTargetMF(nn.Module): def __init__(self, num_drugs, num_targets, embedding_dim): super().__init__() self.drug_embedding nn.Embedding(num_drugs, embedding_dim) self.target_embedding nn.Embedding(num_targets, embedding_dim) # 初始化 nn.init.normal_(self.drug_embedding.weight, std0.01) nn.init.normal_(self.target_embedding.weight, std0.01) def forward(self, drug_indices, target_indices): drug_vec self.drug_embedding(drug_indices) target_vec self.target_embedding(target_indices) # 点积作为预测得分 prediction (drug_vec * target_vec).sum(dim1) return prediction3.4 训练循环划分数据集设置训练循环。from torch.utils.data import TensorDataset, DataLoader # 划分训练集和测试集 (注意这里为了简单是随机划分实际应用应采用冷启动划分) train_pairs, test_pairs, train_labels, test_labels train_test_split( all_pairs, all_labels, test_size0.2, random_state42, stratifyall_labels ) # 转换为PyTorch张量 train_pairs_tensor torch.LongTensor(train_pairs) train_labels_tensor torch.FloatTensor(train_labels) test_pairs_tensor torch.LongTensor(test_pairs) test_labels_tensor torch.FloatTensor(test_labels) # 创建DataLoader train_dataset TensorDataset(train_pairs_tensor, train_labels_tensor) train_loader DataLoader(train_dataset, batch_size256, shuffleTrue) test_dataset TensorDataset(test_pairs_tensor, test_labels_tensor) test_loader DataLoader(test_dataset, batch_size512, shuffleFalse) # 初始化模型、优化器、损失函数 device torch.device(cuda if torch.cuda.is_available() else cpu) model DrugTargetMF(num_drugs, num_targets, embedding_dim64).to(device) optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.BCEWithLogitsLoss() # 因为我们的输出是logits # 训练循环 num_epochs 50 for epoch in range(num_epochs): model.train() total_loss 0 for batch in train_loader: pairs, labels batch drug_idx, target_idx pairs[:, 0], pairs[:, 1] drug_idx, target_idx, labels drug_idx.to(device), target_idx.to(device), labels.to(device) optimizer.zero_grad() pred model(drug_idx, target_idx) loss criterion(pred, labels) loss.backward() optimizer.step() total_loss loss.item() # 每个epoch后在测试集上评估 model.eval() test_preds, test_true [], [] with torch.no_grad(): for batch in test_loader: pairs, labels batch drug_idx, target_idx pairs[:, 0], pairs[:, 1] drug_idx, target_idx, labels drug_idx.to(device), target_idx.to(device), labels.to(device) pred model(drug_idx, target_idx) test_preds.append(torch.sigmoid(pred).cpu()) # 将logits转为概率 test_true.append(labels.cpu()) test_preds torch.cat(test_preds).numpy() test_true torch.cat(test_true).numpy() from sklearn.metrics import roc_auc_score epoch_auc roc_auc_score(test_true, test_preds) print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f}, Test AUC: {epoch_auc:.4f})3.5 进行新预测训练完成后可以用模型预测任意药物-靶点对的相互作用概率。def predict_interaction(model, drug_id, target_id, drug_to_idx, target_to_idx, device): model.eval() # 将ID转换为索引 drug_idx torch.tensor([drug_to_idx.get(drug_id, -1)], devicedevice) target_idx torch.tensor([target_to_idx.get(target_id, -1)], devicedevice) if drug_idx.item() -1 or target_idx.item() -1: print(fWarning: Drug {drug_id} or Target {target_id} not in training vocabulary. Returning default score.) return 0.5 # 或处理OOV问题 with torch.no_grad(): logit model(drug_idx, target_idx) prob torch.sigmoid(logit).item() return prob # 示例预测药物DB00173和靶点P35568的相互作用概率 prob predict_interaction(model, DB00173, P35568, drug_to_idx, target_to_idx, device) print(fPredicted interaction probability: {prob:.3f})4. 进阶与避坑从能跑到跑好上面的流程提供了一个可运行的基线。但要得到真正有说服力的结果还需要考虑以下进阶问题和避坑点。4.1 如何处理新药物或新靶点冷启动问题矩阵分解模型严重依赖ID嵌入无法处理训练集中未出现过的药物或靶点OOV Out-Of-Vocabulary。这是其最大短板。解决方案是引入侧信息Side Information也就是我们之前提到的药物和靶点特征。改进方案神经矩阵分解Neural Matrix Factorization with Features将药物/靶点的特征向量与它们的ID嵌入结合起来。class NMFWithFeatures(nn.Module): def __init__(self, num_drugs, num_targets, drug_feat_dim, target_feat_dim, latent_dim): super().__init__() # ID嵌入层 self.drug_id_embed nn.Embedding(num_drugs, latent_dim) self.target_id_embed nn.Embedding(num_targets, latent_dim) # 特征变换层 self.drug_feat_layer nn.Linear(drug_feat_dim, latent_dim) self.target_feat_layer nn.Linear(target_feat_dim, latent_dim) # 交互层 self.interaction_mlp nn.Sequential( nn.Linear(latent_dim * 2, latent_dim), nn.ReLU(), nn.Dropout(0.2), nn.Linear(latent_dim, 1) ) def forward(self, drug_idx, target_idx, drug_feat, target_feat): # 获取ID嵌入 drug_id_emb self.drug_id_embed(drug_idx) target_id_emb self.target_id_embed(target_idx) # 获取特征嵌入 drug_feat_emb self.drug_feat_layer(drug_feat) target_feat_emb self.target_feat_layer(target_feat) # 融合ID嵌入和特征嵌入例如相加 drug_final_emb drug_id_emb drug_feat_emb target_final_emb target_id_emb target_feat_emb # 拼接后通过MLP pair_emb torch.cat([drug_final_emb, target_final_emb], dim1) prediction self.interaction_mlp(pair_emb).squeeze() return prediction对于新药物不在训练ID列表中我们可以将其drug_id_embed设为零向量或者用一个可学习的“未知药物”嵌入模型主要依靠drug_feat即其分子指纹或GNN编码来进行预测。这大大缓解了冷启动问题。4.2 知识图谱信息如何有效整合在GNN方案中我们通过消息传递整合了知识图谱信息。在非GNN方案中我们可以将知识图谱的信息作为额外的特征。方法使用图嵌入算法如Node2Vec, TransE预计算节点向量你可以先在完整的知识图谱包含药物-靶点-疾病-副作用等上运行Node2Vec为每个节点药物、靶点、疾病等学习一个低维向量。然后将这些向量作为对应药物的额外特征与分子指纹等拼接。# 假设我们已通过Node2Vec得到了所有节点的嵌入字典 node_embeddings # node_embeddings[node_id] np.array of shape (embed_dim,) def augment_features_with_kge(drug_id, original_feat, node_embeddings, kge_dim): kge_vec node_embeddings.get(drug_id, np.zeros(kge_dim)) augmented_feat np.concatenate([original_feat, kge_vec]) return augmented_feat这样药物的特征就包含了它在知识图谱全局结构中的位置信息例如与它相连的疾病有哪些它和哪些其他药物相似等。4.3 模型评估的陷阱数据泄露与过拟合这是初学者最容易踩的坑。陷阱1随机划分导致泄露如前所述必须使用冷启动划分。可以使用sklearn的GroupShuffleSplit以药物ID或靶点ID作为分组依据确保同一组不出现在训练集和测试集。陷阱2负样本采样泄露生成负样本时必须在训练集划分完成后仅基于训练集的正样本来采样。绝对不能用测试集中存在的药物-靶点对作为负样本否则模型会在训练时“看到”测试集信息。陷阱3特征标准化泄露对药物/靶点特征进行标准化如StandardScaler时fit只能用在训练集上然后用训练集得到的均值和方差去transform测试集。陷阱4早停法Early Stopping的验证集需要从训练集中再划分一个验证集用于早停这个验证集同样需要遵循冷启动原则。4.4 性能优化与可扩展性负采样加速对于大规模数据集在每轮epoch都动态采样负样本比固定负样本集效果更好但计算开销大。可以使用负采样缓存策略或者使用像PyTorch Geometric的LinkNeighborLoader这样的专用数据加载器它能在GPU上高效地进行随机负采样。使用更高效的GNN库对于超大规模图谱可以考虑使用DGL或PyG的GraphStore/FeatureStore接口或者使用采样方法如NeighborSampling来训练。多任务学习除了预测相互作用可以同时预测药物的副作用或靶点的功能这些辅助任务可以作为正则项提升主任务的泛化能力。这个项目从构思到实现最大的体会是跨领域的思路融合往往能带来惊喜。将推荐系统的框架用于生物医学问题其核心价值不在于使用了多复杂的模型而在于提供了一种新的、数据驱动的问题视角。当然最终效果的提升永远离不开对领域知识药物化学、生物学的深入理解以及严谨、细致的数据处理和实验设计。代码和算法是引擎高质量的数据和合理的评估才是导航仪。希望这个详细的复盘和代码指南能帮你快速上手并在此基础上探索出更适合你具体问题的解决方案。本文还有配套的精品资源点击获取