ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

知识图谱+推荐系统:药物靶点交互预测工程化落地

知识图谱+推荐系统:药物靶点交互预测工程化落地 简介这份资源是面向计算机相关专业学生的知识图谱与推荐系统实战项目源码以药物靶点相互作用预测为赛题场景适合用作课程设计、期末大作业或项目实战练习需要读者具备一定Python基础并对知识图谱、推荐算法有初步了解。压缩包共40个文件约56KB以9个py脚本为核心覆盖deepdti、kge_rf、kge_nfm等模型实现与train_all.py训练入口另含Pipfile、requirements.txt等依赖配置、README.md操作说明、LICENSE授权文件及若干目录占位文件结构清晰便于按模块阅读。目前已有94人学习下载。项目完整呈现了从数据预处理、知识图谱构建到推荐模型训练与评估的全流程读者可据此理解协同过滤、矩阵分解、深度学习等算法在药物靶点预测中的落地方式并借助操作指南快速跑通代码、复现实验结果为后续课程项目或研究提供可参考的实现思路。1. 药物靶点交互预测从知识图谱到推荐排序的工程化路径药物靶点交互Drug-Target Interaction, DTI预测这件事本质上是在回答一个二部图上的链接预测问题给定一批药物节点和一批靶点节点判断哪些边存在、哪些边可能成立。传统做法靠湿实验逐个验证成本高、周期长一个靶点筛下来动辄数月。而把知识图谱和推荐系统这两套思路接进来之后问题就变成了用图结构表达药物、靶点、疾病、通路之间的多跳关系再用推荐算法对候选边做排序打分。这套组合在工业场景下的知识图谱设计里已经相当常见Python 生态也足够成熟Neo4j 构建知识图谱、PyTorch 搭模型、FastAPI 出接口一条链路能跑通。这篇文章面向的是想把这套方案真正落地的人——不管你是刚配好 vscode python 环境的新手还是已经在做推荐系统想往生物医药方向迁移的熟手下面从数据建模、图构建、特征工程到模型训练和排坑一步步拆开讲。2. 知识图谱怎么建药物-靶点二部图的 schema 设计与 Neo4j 落地2.1 为什么选图数据库而不是关系表药物靶点交互数据天然是图结构。一个药物可以作用多个靶点一个靶点也可以被多个药物命中再加上疾病、通路、基因本体这些中间层关系表做三度以上的关联查询就要反复 JOIN性能断崖式下跌。Neo4j 这类图数据库的优势在于多跳查询是原生操作路径检索不用递归 CTE而且 Cypher 语法对「找两个节点之间所有长度为 3 的路径」这种需求表达非常直接。我一般会先把 schema 定下来再灌数据因为 schema 一旦跑偏后面特征工程全得返工。常见的节点类型和关系类型如下节点标签含义关键属性Drug药物drug_id, name, smiles, pubchem_cidTarget靶点蛋白target_id, gene_symbol, uniprot_idDisease疾病disease_id, name, mesh_idPathway通路pathway_id, name, kegg_id关系类型起点 → 终点属性INTERACTS_WITHDrug → Targetaffinity, ic50, sourceTREATSDrug → Diseasephase, statusASSOCIATED_WITHTarget → Diseasescore, sourcePARTICIPATES_INTarget → Pathway—这个 schema 的好处是DTI 预测任务直接对应 INTERACTS_WITH 边的链接预测而疾病和通路节点提供了额外的上下文特征可以在推荐模型里当作 side information 用。2.2 用 Python 批量导入节点和关系数据源常见的有 DrugBank、ChEMBL、STRING 这几类。假设你已经拿到了 CSV 格式的交互数据下面是用 neo4j 官方 Python driver 批量写入的代码from neo4j import GraphDatabase import csv driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, your_password)) def create_drug_target_interaction(tx, drug_id, drug_name, target_id, gene_symbol, affinity): tx.run( MERGE (d:Drug {drug_id: $drug_id}) SET d.name $drug_name MERGE (t:Target {target_id: $target_id}) SET t.gene_symbol $gene_symbol MERGE (d)-[r:INTERACTS_WITH]-(t) SET r.affinity $affinity , drug_iddrug_id, drug_namedrug_name, target_idtarget_id, gene_symbolgene_symbol, affinityaffinity ) def batch_import(csv_path): with driver.session() as session: with open(csv_path, r) as f: reader csv.DictReader(f) batch [] for row in reader: batch.append(row) if len(batch) 1000: # 每1000条提交一次 for r in batch: session.execute_write( create_drug_target_interaction, r[drug_id], r[drug_name], r[target_id], r[gene_symbol], float(r[affinity]) ) batch [] # 处理剩余不足1000条的 for r in batch: session.execute_write( create_drug_target_interaction, r[drug_id], r[drug_name], r[target_id], r[gene_symbol], float(r[affinity]) ) batch_import(drug_target_pairs.csv)这段代码的逻辑说明MERGE而不是CREATE保证重复导入不会产生重复节点每 1000 条做一次批量提交避免单条事务开销过大execute_write是 Neo4j 4.x 之后的推荐写法自带重试机制。参数方面affinity字段建议统一量纲不同数据源的 IC50、Ki、Kd 值范围差异很大后面做特征的时候要么归一化要么分桶。提示导入前先在 Neo4j 里给drug_id和target_id建唯一约束否则 MERGE 在数据量大时会退化成全表扫描导入速度从每秒几千条掉到几十条。2.3 从图里抽特征的 Cypher 查询图建好之后下一步是把图结构转成模型能吃的特征向量。最基础的一类特征是节点的度、共同邻居数、路径数。比如计算某个药物和某个靶点之间的共同邻居数量def get_common_neighbors(tx, drug_id, target_id): result tx.run( MATCH (d:Drug {drug_id: $drug_id})-[:INTERACTS_WITH]-(t:Target) MATCH (t2:Target {target_id: $target_id})-[:INTERACTS_WITH]-(d2:Drug) WHERE t t2 OR d d2 RETURN count(DISTINCT t) AS common_targets, count(DISTINCT d2) AS common_drugs , drug_iddrug_id, target_idtarget_id ) return result.single()这个查询返回两个数该药物和该靶点共享的靶点数量、共享的药物数量。这两个值可以直接作为特征喂给推荐模型。实际工程里我会一次性把所有候选对的共同邻居数、Jaccard 系数、Adamic-Adar 指数都算出来存成特征表避免训练时反复查库。参数上注意一点Cypher 查询的深度不要超过 3 跳超过之后查询时间指数增长。如果确实需要更长路径的特征建议用图嵌入方法比如 Node2Vec 或 GraphSAGE先把节点映射成低维向量再在向量空间里算相似度。3. 推荐系统怎么做排序从矩阵分解到图神经网络的选型对比3.1 把 DTI 预测形式化成推荐问题推荐系统的核心是给用户-物品对打分排序。DTI 预测可以完全套用这个框架药物是「用户」靶点是「物品」已知的交互关系是「历史行为」任务是预测未观测到的药物-靶点对是否存在交互。区别在于推荐系统里用户行为有显式反馈评分和隐式反馈点击、购买而 DTI 数据通常只有二值标签有交互/无交互偶尔带一个亲和力数值。这个形式化带来的直接好处是推荐系统领域积累的大量方法可以直接迁移。矩阵分解、因子分解机、神经协同过滤、图神经网络这些在电商推荐里被验证过的模型换成药物-靶点数据一样能跑。3.2 矩阵分解基线最快能跑通的方案如果只是想先跑通一个基线矩阵分解是最省事的。把药物-靶点交互矩阵做 SVD 分解用隐向量的内积作为预测分数import numpy as np from scipy.sparse import csr_matrix from scipy.sparse.linalg import svds def matrix_factorization_predict(interaction_matrix, k64): interaction_matrix: scipy sparse matrix, shape (n_drugs, n_targets) k: 隐向量维度 返回: 预测分数矩阵和药物/靶点的隐向量 # 先做均值中心化否则SVD对稀疏矩阵效果很差 drug_means np.array(interaction_matrix.mean(axis1)).flatten() # 对没有交互的药物均值设为全局均值 global_mean interaction_matrix.mean() drug_means[np.isnan(drug_means)] global_mean # 中心化 centered interaction_matrix.copy().astype(float) for i in range(centered.shape[0]): start, end centered.indptr[i], centered.indptr[i1] centered.data[start:end] - drug_means[i] # SVD分解 U, sigma, Vt svds(centered, kk) sigma np.diag(sigma) # 预测 predicted np.dot(np.dot(U, sigma), Vt) drug_means.reshape(-1, 1) return predicted, U, Vt # 假设 interaction_matrix 已经构建好了 # predicted_scores, drug_emb, target_emb matrix_factorization_predict(interaction_matrix, k64)逻辑说明先对每一行做均值中心化这是因为交互矩阵非常稀疏通常不到 1% 的非零元素不中心化的话 SVD 会主要拟合零值。svds是稀疏 SVD比numpy.linalg.svd快很多适合大规模矩阵。参数k控制隐向量维度64 到 128 之间是比较常见的取值太小欠拟合太大过拟合且计算量上升。这个基线的优点是快几千个药物和靶点的矩阵几秒钟就能跑完。缺点是它只用了交互矩阵本身的信息没有利用知识图谱里的疾病、通路、基因本体这些 side information。如果你的数据里药物和靶点的交互非常稀疏比如每个药物平均只有几个已知靶点矩阵分解的效果会明显下降。3.3 图神经网络方案用 GraphSAGE 聚合邻居信息当交互数据稀疏时知识图谱里的额外关系就成了关键。GraphSAGE 的思路是每个节点的嵌入向量不只由自己决定还由它的邻居聚合而来。药物节点的嵌入可以聚合它连接的靶点、疾病、通路的信息这样即使某个药物只有一个已知靶点也能通过靶点关联的疾病和通路获得丰富的表示。import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import SAGEConv class DTIGraphSAGE(nn.Module): def __init__(self, in_channels, hidden_channels, out_channels): super().__init__() # 两层SAGE卷积第一层聚合邻居第二层进一步抽象 self.conv1 SAGEConv(in_channels, hidden_channels) self.conv2 SAGEConv(hidden_channels, out_channels) # 用于最终二分类的打分层 self.classifier nn.Linear(out_channels * 2, 1) def forward(self, x, edge_index, drug_indices, target_indices): # 第一层聚合 ReLU激活 x self.conv1(x, edge_index) x F.relu(x) x F.dropout(x, p0.3, trainingself.training) # 第二层聚合 x self.conv2(x, edge_index) # 取出药物和靶点的嵌入拼接后过分类器 drug_emb x[drug_indices] target_emb x[target_indices] pair_emb torch.cat([drug_emb, target_emb], dim1) return torch.sigmoid(self.classifier(pair_emb)).squeeze() # 参数说明 # in_channels: 节点初始特征维度可以用one-hot或预训练嵌入 # hidden_channels: 中间层维度常用128或256 # out_channels: 最终嵌入维度常用64或128 # dropout p0.3: 防止过拟合图数据上通常用0.2-0.5这段代码的关键设计点SAGEConv支持归纳学习也就是说训练时没见过的节点在推理时也能生成嵌入这对新药和新靶点的冷启动场景很重要。drug_indices和target_indices是当前 batch 里涉及的药物和靶点节点编号分类器接收的是药物嵌入和靶点嵌入的拼接。训练时用二元交叉熵损失正样本是已知交互对负样本通过随机采样生成。实际训练中负采样策略对效果影响很大。我一般按 1:3 到 1:5 的正负样本比例采样负样本从「没有已知交互」的药物-靶点对里随机选。但要注意有些对只是「尚未被发现」并不是真正的负样本所以负采样时最好避开那些在知识图谱里有共同疾病或通路关联的对减少假阴性。3.4 两种方案的选型边界矩阵分解适合数据量中等、交互密度尚可、需要快速出基线结果的场景。部署简单不依赖 GPU推理速度快。GraphSAGE 适合交互稀疏、知识图谱里有丰富的辅助关系、对冷启动有要求的场景。代价是训练需要 GPU调参更复杂推理时需要维护图结构。我的经验是先用矩阵分解跑一个基线记录 AUC 和 AUPRC。如果 AUPRC 低于 0.3说明交互太稀疏直接上 GraphSAGE。如果矩阵分解已经能到 0.5 以上可以先在特征工程上做优化不一定急着上图神经网络。4. 避坑与排查DTI 预测落地时最容易翻车的五个地方4.1 数据泄漏随机划分训练集导致指标虚高现象模型在验证集上 AUC 到 0.95换一批新数据直接掉到 0.6。原因随机划分药物-靶点对的时候同一个药物的不同靶点对被分到了训练集和验证集。模型实际上记住了这个药物的嵌入在验证集上只是复现了训练时见过的药物模式。这不是真正的泛化能力。解决按药物做划分而不是按对做划分。具体来说留出一部分药物完全不出现在训练集中只用它们的交互做验证。这样测出来的才是模型对全新药物的预测能力。代码上就是在划分前先按drug_id分组然后按组划分。4.2 负样本采样偏差随机负样本太「假」现象模型训练 loss 降得很快但推理时对候选对的打分区分度很低。原因随机采样的负样本里很多药物和靶点在化学空间或序列空间上差异极大模型很容易区分学到的决策边界过于粗糙。真正难区分的是那些「结构相似但没有已知交互」的对。解决用难负样本挖掘。先训练一个初始模型用它给所有未观测对打分选出分数最高的那批作为难负样本再重新训练。或者从知识图谱里找那些有共同通路或疾病关联但没有直接交互的药物-靶点对作为负样本。4.3 Neo4j 导入时 MERGE 性能断崖现象导入前几千条很快到几万条之后每条要等好几秒。原因没有建唯一约束MERGE 每次都在做全标签扫描。Neo4j 的 MERGE 在没有索引的情况下是 O(n) 复杂度。解决导入前先执行CREATE CONSTRAINT FOR (d:Drug) REQUIRE d.drug_id IS UNIQUE和对应的 Target 约束。另外用apoc.periodic.iterate做批量操作比逐条 execute_write 快一个数量级。4.4 特征归一化遗漏亲和力数值量纲不统一现象模型训练时 loss 震荡不收敛或者某些特征权重异常大。原因不同数据源的亲和力数值范围差异巨大。ChEMBL 里 IC50 可能从 0.1 nM 到 10000 nM而某些数据集的 Ki 值单位是 μM。不做归一化直接喂给模型梯度会被大量纲特征主导。解决统一转成 pIC50即 -log10(IC50 in M)这个变换把数值压缩到 4 到 10 的区间而且生物学上更有意义。对于其他数值特征用 z-score 或 min-max 归一化。4.5 图神经网络过平滑层数多了反而变差现象GraphSAGE 从 2 层加到 4 层训练集 loss 继续降但验证集指标反而下降。原因图神经网络过平滑问题。每一层聚合都让相邻节点的嵌入趋同层数太多之后所有节点嵌入变得几乎一样失去了区分度。解决DTI 任务上 2 到 3 层就够了。如果确实需要更大感受野用残差连接把浅层嵌入拼到深层或者用 JKNet 这类跳跃连接结构。另外在每层之后加 LayerNorm 也有帮助。5. 进阶技巧用元路径特征提升冷启动场景的预测精度前面讲的 GraphSAGE 是通用方案但在冷启动场景——新药没有已知靶点、新靶点没有已知药物——纯图神经网络的效果会打折扣。这时候元路径meta-path特征是一个很实用的补充。元路径是指在知识图谱里按特定关系类型序列走出来的路径。比如「药物 → 靶点 → 疾病 ← 靶点 ← 药物」这条路径表达的是「两个药物作用于同一个疾病相关的靶点」。这种路径特征在推荐系统里被验证过很多次迁移到 DTI 场景同样有效。具体做法是对每个药物-靶点候选对枚举几种预定义的元路径统计路径数量、路径上的平均亲和力等指标作为额外特征拼到模型输入里。下面是一个用 Cypher 统计元路径数量的例子def count_metapath(tx, drug_id, target_id): # 元路径: Drug - Target - Disease - Target - Drug - Target result tx.run( MATCH (d1:Drug {drug_id: $drug_id})-[:INTERACTS_WITH]-(t1:Target) -[:ASSOCIATED_WITH]-(dis:Disease)-[:ASSOCIATED_WITH]- (t2:Target)-[:INTERACTS_WITH]-(d2:Drug) -[:INTERACTS_WITH]-(t3:Target {target_id: $target_id}) RETURN count(*) AS path_count , drug_iddrug_id, target_idtarget_id ) record result.single() return record[path_count] if record else 0这个查询统计的是从药物 d1 出发经过靶点 t1 关联到疾病再从疾病反向找到另一个靶点 t2再找到作用于 t2 的药物 d2最后 d2 作用于目标靶点。路径数量越多说明这两个节点在知识图谱里的语义关联越强。实际工程里我会预计算所有候选对的元路径特征存成一张宽表训练时直接 join。元路径的设计需要领域知识常见的几条包括药物-靶点-疾病-靶点、药物-靶点-通路-靶点、药物-靶点-基因本体-靶点。每条元路径贡献几个统计量路径数、平均权重、最大权重最终拼成几十维的特征向量。验证这套方案是否有效我一般会做两组对比一组只用图嵌入一组图嵌入加元路径特征在冷启动测试集上比 AUPRC。如果元路径特征能带来 3 个点以上的提升就值得加到生产流程里。如果提升不明显可能是元路径设计得不够有区分度或者知识图谱里疾病和通路关系的覆盖率太低。一个我踩过的坑元路径查询在 Neo4j 里很容易写成笛卡尔积尤其是路径长度超过 4 跳之后。写 Cypher 的时候一定要用PROFILE看执行计划确认没有CartesianProduct操作。如果有要么拆成多条查询要么在中间节点上加索引。最后说一个习惯每次跑完模型我都会把预测分数最高的前 50 个候选对导出来人工看一眼这些药物和靶点的已知信息。如果 Top 50 里有一半是已经报道过的交互说明模型在拟合已知数据如果有很多看起来合理的 novel 预测才说明模型真的学到了东西。这个人工检查步骤花不了多少时间但能避免很多「指标好看但没实际价值」的情况。希望帮到你。本文还有配套的精品资源点击获取
返回列表