☰
知识图谱+推荐系统:药物靶点交互预测工程化落地
2026/10/3 18:28:58 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生的知识图谱与推荐系统实战项目源码,以药物靶点相互作用预测为赛题场景,适合用作课程设计、期末大作业或项目实战练习,需要读者具备一定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_cid
Target靶点蛋白target_id, gene_symbol, uniprot_id
Disease疾病disease_id, name, mesh_id
Pathway通路pathway_id, name, kegg_id
关系类型起点 → 终点属性
INTERACTS_WITHDrug → Targetaffinity, ic50, source
TREATSDrug → Diseasephase, status
ASSOCIATED_WITHTarget → Diseasescore, source
PARTICIPATES_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_id=drug_id, drug_name=drug_name, target_id=target_id, gene_symbol=gene_symbol, affinity=affinity ) 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_id=drug_id, target_id=target_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, k=64): """ interaction_matrix: scipy sparse matrix, shape (n_drugs, n_targets) k: 隐向量维度 返回: 预测分数矩阵和药物/靶点的隐向量 """ # 先做均值中心化,否则SVD对稀疏矩阵效果很差 drug_means = np.array(interaction_matrix.mean(axis=1)).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[i+1] centered.data[start:end] -= drug_means[i] # SVD分解 U, sigma, Vt = svds(centered, k=k) 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, k=64)

逻辑说明:先对每一行做均值中心化,这是因为交互矩阵非常稀疏(通常不到 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, p=0.3, training=self.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], dim=1) return torch.sigmoid(self.classifier(pair_emb)).squeeze() # 参数说明: # in_channels: 节点初始特征维度,可以用one-hot或预训练嵌入 # hidden_channels: 中间层维度,常用128或256 # out_channels: 最终嵌入维度,常用64或128 # dropout p=0.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_id=drug_id, target_id=target_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 预测,才说明模型真的学到了东西。这个人工检查步骤花不了多少时间,但能避免很多「指标好看但没实际价值」的情况。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询