资讯动态

知识图谱+推荐系统:药物重定位中DTI预测的最小实现与避坑指南

发布时间:2026/10/26 15:49:34 来源:尧图企业网站定制
简介这是一套基于知识图谱与推荐系统预测药物靶点相互作用的Python源码附带详细操作指南主要面向计算机相关专业学生用于课程设计、期末大作业或项目实战。项目覆盖数据预处理、知识图谱构建、推荐算法实现与模型评估等完整流程代码中整合了药物、疾病、基因等语义关系并内置深度模型、随机森林与神经因子分解机等多种可运行算法模块方便对照不同建模思路。资源共40个文件以.py源码、.md说明文档、Pipfile依赖清单等类型为主并包含少量辅助脚本与配置文件压缩包整体仅56KB轻量易用目录结构清晰。目前已有95人学习下载。借助操作指南使用者可快速完成环境配置、数据准备、模型训练与结果解读等关键步骤既能深入理解知识图谱与推荐系统结合的实际应用也可将其作为高分课程设计的起点在现有代码上扩展药物靶点预测实验。1. 当药物研发遇到推荐系统知识图谱如何把“老药新用”变成可计算的排序问题药物研发最贵的不只是临床试验还有“猜靶点”。一种药可能已经在降血压但它的某个靶点蛋白还参与炎症通路能不能顺手治关节炎如果只靠专家阅读文献一年看不了几个候选。把药物、靶点、疾病、通路之间的关系整理成知识图谱再用推荐系统里给用户推荐物品那套协同过滤思路为“未知的药物-靶点交互”打分排序实验室就能把验证资源集中到最有希望的候选上。这是我见过最实际的落地方式。这套方案会用 Python 从零搭一套最小流程建图、特征、训练 TransE、评估与排错适合刚开始做药物重定位的学生也给生信算法岗的读者提供一份可直接修改的操作指南。2. 先把知识图谱建起来从药物、靶点、疾病的原始关系表到 Python 邻接矩阵2.1 选数据源为什么我推荐先用 CTD 而不是一上来就拼全库知识图谱的质量决定推荐系统上限。很多人一开口就是 UniProt、DrugBank、DisGeNET 全上结果数据对齐就耗掉两周。我建议原型阶段用 CTDComparative Toxicogenomics Database的 chemical-gene 和 chemical-disease 关系表。CTD 是公开下载的制表符文件每条记录几乎都是清洗好的三元组化学物 ID、基因 ID、相互作用类型。它不像 DrugBank 需要申请审批也不像 UniProt 的 GOA 文件那样需要你自己推断关系上手最快。选它的第二个理由CTD 自带物种标识。比如基因一列是9606:1956这种格式冒号前是 NCBI Taxonomy ID9606 就是人。做药物靶点预测最怕人和鼠的数据混在一起CTD 把物种写死在 ID 里过滤非常方便。等流程跑通再去补 DrugBank 的药物-靶点条目和 Reactome 的通路关系把图谱扩大。数据下载之后不要直接训练先做一次“关系计数”。我一般会输出每个关系类型的分布看数据是不是偏到个别 hub 节点上。比如某个基因同时出现在一万条记录里后面计算邻居特征时它会主导相似度模型会学成“跟大节点有关就预测正例”。这个后面会在避坑章专门讲。下面这段是读取与过滤的常见做法注意 CTD 表头前 27 行到 30 行是统计信息skiprows 要看实际文件重新调我不会写死为永远 27import pandas as pd # 第一次打开文件先看前 40 行确认注释头占多少行 with open(CTD_chemical_gene.tsv) as f: for i, line in enumerate(f): if i 40: break print(i, line[:100]) # 确认后读取skiprows 跳过注释 chem_gene pd.read_csv( CTD_chemical_gene.tsv, sep\t, skiprows27, low_memoryFalse, ) # 只要人和有相互作用注释的行 chem_gene chem_gene[chem_gene[GeneID].str.startswith(9606)].copy() chem_gene chem_gene.dropna(subset[ChemicalID, GeneID, InteractionActions]) print(raw triples:, chem_gene.shape[0])这里low_memoryFalse是因为混合类型列很多不设置的话 pandas 可能把某些列读成 object后面 ID 对齐会翻车。skiprows是这个文件常见的注释行数换版本后要用head命令重新确认。我不建议把全表直接塞进内存再 groupby先按物种过滤能省掉一大半无关记录。得到原始表之后要把 ID 统一。CTD 里基因是9606:1956化学物是 MESH ID 如D0001。如果想在知识图谱里把“基因”和“靶点”混用建议保留 GeneID 并转成 intdef norm_gene(gid: str) - int: CTD 的 GeneID 形如 9606:1956只取冒号后面的 EntrezGene ID return int(gid.split(:)[1]) kg chem_gene[[ChemicalID, GeneID, InteractionActions]].copy() kg[target] kg[GeneID].map(norm_gene) kg[relation] interacts_with km kg[[ChemicalID, target, relation]].drop_duplicates()这段逻辑看起来简单但有两个极易踩的坑。第一map(norm_gene)遇到非 9606 的行会先被外层过滤掉所以不用 try但若混着9606:1956和10090:...而外层过滤写错int 转换会直接抛异常。我会把过滤和转换写在一起用loc而不是链式赋值避免 SettingWithCopyWarning。第二InteractionActions 有的是increases有的是decreases|increases这里统一成interacts_with是故意丢信息。原型阶段做“是否交互”二分类足够跑通之后再回来把细粒度关系加为边类型。2.2 把三元组变成图对象节点类型、边类型、重复边数据变成三元组但推荐算法需要的是图结构。我用networkx.Graph搭无向图。注意我不用 DiGraph因为 DTI 是“药物-靶点”双向相关没有严格的上下行关系虽然 CTD 里有的 interaction 是化学物“增加”基因表达有方向性但二分类不需要方向。如果后面做因果推断再换 DiGraph。构造图时把节点和边分开添加是有价值的节点上保存node_type后面做路径特征时能区分“药物→通路→靶点”和“药物→疾病→靶点”这两种路径的语义完全不同。代码import networkx as nx G nx.Graph() G.add_nodes_from(kg[ChemicalID].drop_duplicates(), node_typedrug) G.add_nodes_from(kg[target].astype(str).drop_duplicates(), node_typetarget) G.add_edges_from( zip(kg[ChemicalID], kg[target].astype(str)), relinteracts_with, ) print(nx.info(G))nx.info会打印节点数、边数和平均度。我一般要求平均度在 2 以上才继续否则图太稀疏协同过滤也救不了。这里把 target 转成str是防止 ChemicalID字符串和 targetint在 networkx 内部被当成不等的节点类型导致建图后同一个节点出现两种 ID 表示。不少初学者在这翻车你查G.nodes会发现1956和1956同时存在。2.3 用 scipy.sparse 构建邻接矩阵别听教程里的 numpy dense推荐系统里大量操作是矩阵乘法和特征广播所以我习惯同时保留 networkx 图和稀疏邻接矩阵。邻接矩阵的维数是“全部实体数量”几千到几万都很正常如果用np.zeros((n,n))1 万节点就要 800MB float后面 TransE 训练再一比组合就爆了。正确做法是 coo_matrix 再转 csrimport numpy as np from scipy.sparse import coo_matrix nodes list(G.nodes()) node2idx {n: i for i, n in enumerate(nodes)} row, col [], [] for u, v in G.edges(): row.append(node2idx[u]) col.append(node2idx[v]) adj coo_matrix( (np.ones(len(row)), (row, col)), shape(len(nodes), len(nodes)), ).tocsr()coo_matrix只记录非零位置内存随边数而不是节点平方增长。转成csr是因为后面做adjvector这类稀疏矩阵运算时 CSR 格式更快。node2idx这个字典在后面提取嵌入、生成训练样本时必须复用否则对齐错位不会报错只会默默给你一个错误的 AUC。为了验证邻接矩阵和 networkx 图一致我通常会抽查几个节点对比adj[node2idx[drug]]和G.neighbors(drug)test_drug nodes[0] row_idx node2idx[test_drug] neigh_in_adj adj[row_idx].indices neigh_in_G [node2idx[n] for n in G.neighbors(test_drug)] assert set(neigh_in_adj) set(neigh_in_G), graph and adj mismatch这里的断言很便宜但值得做。数据管道写好之后改过一次 ID 清洗就可能导致两套结构不一致模型训练还看不出问题。我每次换数据源都会跑一遍这个断言作为回归测试。2.4 建完图先看三样东西连通分量、节点度、路径数量别急着调模型。我先打印三样检查结果孤立节点数如果某药物没有和任何靶点相连它在图模型里永远预测不出分数要么回退到内容特征要么在构建阶段用相似关系连到已有节点。度最大的 10 个节点是否有类似“万金油”的 hub 节点比如 TP53、EGF 这种后面随机游走很容易都游走到它们身上。随机抽 100 对“无直接边”的节点统计 3 跳内可达的比例作为图连通性的粗略指标。如果大多数不可达说明图谱碎片化推荐算法的协同信号很弱。这三个检查不需要单独的代码文件用 Python 交互式环境几行就能完成。但这些检查结果直接决定你后续选 PRA 还是 TransE如果图严重碎片化TransE 嵌入很难学到远程依赖基于局部路径的方法反而是更稳的起点。3. 把 DTI 当推荐问题为什么药物靶点交互可以套用推荐系统3.1 把药物想象成用户靶点想象成物品在推荐系统里协同过滤的基本假设是“相似用户喜欢相似物品”。放到药物重定位场景如果药物 A 和药物 B 在已知交互的靶点集合上高度重叠share 很多靶点那么药物 A 很可能和药物 B 的其余靶点也有交互。靶点同理。这就是最朴素的基于召回和排序的 DTI 预测。这里的“交互”是隐式反馈不像评分有 1-5 星只有“有/无”所以我们要用 implicit feedback 的思路不能直接套皮尔逊相关系数之类依赖评分值的方法。这时知识图谱的价值就出来了传统协同过滤只看药物-靶点二部图容易冷启动而知识图谱给每个药物关联了适应症、机制、ATC 分类给每个靶点关联了通路、疾病、基因本体这些边信息一方面增加路径特征一方面降低对“评分数据”的依赖。这正是推荐系统里把知识图谱作为 side information 的标准用法。选型理由提一下基于平移距离的模型TransE和基于路径的模型PRA都是“知识图谱 推荐”的常见呈现。前者用嵌入表示实体后者用显式路径特征。两者不冲突可以 ensemble。我先在原型里用 PRA 做可解释基线再用 TransE 做训练。3.2 先跑一个“普通邻居重合度”基线零训练、可解释、稳做机器学习前先做一个合理的启发式基线。它不依赖随机种子也能帮你检查图谱质量。我们要预测候选药物-靶点对是否真有交互一个非常强的特征是它们在图上的普通邻居重合度。如果药物 D 和靶点 T 没有直接边但它们都连到基因 X那么 X 作为共同邻居是有信号的。代码def jaccard_neighbors(G, drug, target): neigh_drug set(G.neighbors(drug)) neigh_target set(G.neighbors(target)) if not neigh_drug or not neigh_target: return 0.0 union neigh_drug | neigh_target inter neigh_drug neigh_target return len(inter) / len(union)一样地还可以算 Dice 系数或共同邻居数。jaccard隐含的意义是两个节点共享的邻居占整体邻居的比。共享邻居太少说明它们在网络中处于不同区域预测为正例的可信度低。我用它作为特征而不是直接当分数是因为单独用它的精度有限但在样本量小的时候足以区分大部分明显负例。在所有候选对上跑这个函数然后用sklearn的roc_auc_score评估你会得到一个“不用任何训练”的基线。如果这个基线 AUC 低于 0.6说明图谱质量有问题后面再上深度学习也是白搭。我会把这个当作数据质检关卡。3.3 把路径变成特征PRA 思路与随机游走实现普通邻居只走一跳。更长路径比如“药物 → 疾病 → 靶点”虽然没有共享的靶点节点但它们在同一个疾病分支里也有语义信号。基于路径的排序就是这样对每种“节点类型序列”统计路径数量。实现可以用nx.all_simple_paths但深度不要超过 3也可以随机游走统计命中这两种我都用它们对应不同的稳定性。先看简单路径版from collections import Counter def path_type_hist(G, drug, target, max_depth3): hist Counter() for path in nx.all_simple_paths(G, drug, target, cutoffmax_depth): type_seq tuple(G.nodes[n].get(node_type, other) for n in path) hist[type_seq] 1 return histcutoff3表示最多走 3 条边。路径再长组合数爆炸而且很容易绕圈绕到一个 hub 节点带来大量假信号。这个函数返回类似{(drug,target,target): 2}的字典但注意路径中可能经过多个 target——如果图谱里有二部图的边那drug - target - other_drug - target这种会很多。可以把这种路径类型看成特征名字逻辑回归就能学出哪些路径类型更代表正例。问题在于all_simple_paths在大图上非常贵因为它是生成器会枚举所有简单路径。如果图有 5000 节点、平均度 5深度 3 的路径数量可能十几万条乘以候选对就是灾难。因此我更多使用随机游走import random def random_walk_hit_rate(G, start, target, walk_len4, n_walks50): hits 0 for _ in range(n_walks): node start for _ in range(walk_len): neighbors list(G.neighbors(node)) if not neighbors: break node random.choice(neighbors) if node target: hits 1 break return hits / n_walks这里有参数n_walks和walk_len。n_walks越多估计越稳但耗时线性涨walk_len越长越能探索远程关系但也容易跑偏。我一般从n_walks50, walk_len3起步后面加到 200 和 4看指标有没有变化没有就停下来。这个 hit rate 可以当一个标量特征和普通邻居重合度、直接路径数并起来。它本质上是 DeepWalk/Node2Vec 的简化把图的局部结构压缩成一个概率值只不过我们只关心起点到终点的命中率而不是全图嵌入。随机游走比 all_simple_paths 快的另一个原因是它不保证“无重复节点”允许走回头路等于在采样带噪路径。这对小样本 DTI 反而有正则效果。推荐系统里很多图算法也用这种带噪采样因为它更能抵抗图噪音。3.4 参数设置与优先级先调哪些不玄学这里给一个“先动谁”的顺序表参数建议范围影响优先级路径最大长度 max_step/cutoff2~3太长特征爆炸且信号变杂先固定2或3随机游走次数 n_walks50~200稳定性200后收益低第二个调随机游走长度 walk_len3~5语义半径太长容易游走到 hub第三个调负采样比例1:1~1:2类别平衡影响阈值必须监控嵌入维度32~128太小欠拟合太大过拟合看数据量参数不是越多越好。我踩过的坑就是同时调五个参数最后根本不知道谁提升的指标。建议按这个优先级先把 max_step 固定为 3n_walks 设为 100然后把特征归一化用逻辑回归跑一遍再说。逻辑回归本身有 L2 正则参数 C 可以搜但对原型够用。把上述三个特征拼成一个向量标准化后用 LogisticRegression。示例from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_val_score # X 是所有候选药物-靶点对的 [jaccard, path_type_count, walk_hit_rate] clf make_pipeline(StandardScaler(), LogisticRegression(C1.0, max_iter1000)) scores cross_val_score(clf, X, y, cv5, scoringroc_auc) print(scores.mean())先别上 GNN一个逻辑回归加路径特征通常就能到 0.7~0.85 的 AUC而且完全可解释。这个阶段对理解数据非常有帮助。4. 从二部图嵌入到完整模型最小 TransE 训练、评估与操作指南4.1 负采样这是二分类模型的地基请务必用随机种子在知识图谱中我们只有正例边没有“不交互”的边。推荐系统里常见的做法是把未知当成负样本但在 DTI 里这是有风险的很多未知只是“未被验证”不是真的没有交互。所以负样本也叫“软负例”。采样方法有三种随机替换头实体、随机替换尾实体、随机替换两头。我常用随机替换头或尾但必须保证替换后不在已知正样本集合里。下面是一个负采样函数它接受正样本边列表和已知边集合。关键参数neg_ratio建议从 1.0 开始也就是每个正样本配一个负样本。你会发现训练更快指标也更稳定import numpy as np def negative_sampling(edges, all_nodes, known_set, neg_ratio1.0, seed42): rng np.random.default_rng(seed) all_nodes np.asarray(all_nodes) pos_size len(edges) neg_size int(pos_size * neg_ratio) negs [] while len(negs) neg_size: idx rng.integers(0, pos_size) h, t edges[idx] # 从正样本中取一个真边 if rng.integers(0, 2) 0: h2 rng.choice(all_nodes) pair (h2, t) else: t2 rng.choice(all_nodes) pair (h, t2) if pair not in known_set: negs.append(pair) return negs其中edges是正样本列表all_nodes是全部实体列表known_set是“任何已知但当前不在训练集里的关系”集合。有一项必须提醒known_set里要把所有正样本包括测试集都放进去。最安全的方式是先把所有正样本分成 train_pos 和 test_pos然后负采样时known_set all_pos这样训练负样本不会误伤 train 里未出现的正样本但训练时模型可能把 test_pos 当作负例严格来说负采样阶段要把 test_pos 也排除在外否则会造成“测试集正例被模型当作负例训练”这不算泄漏但会让评估标签不一致。更干净的做法是先在全部已知边上采样负样本然后按分边划分到 train/test 的负样本集但划分后必须保证同一个 pair 不出现在两侧。这里提供的是基础版足够复现。4.2 用 PyTorch 写一个最小 TransE20 行核心代码TransE 是知识图谱嵌入最简单也最稳定的模型。它把每个实体映射到一个 d 维向量每个关系也映射为一个 d 维“平移向量”要求对于一个正确三元组(h, r, t)满足h r ≈ t。这正好适合 DTI药物 交互关系 ≈ 靶点。关系interacts_with只有一个所以模型会退化成“药物向量 关系向量 靶点向量”再通过内积或距离打分。这在信息学上也等价于一种基于平移的矩阵分解。代码如下import torch import torch.nn as nn class TransE(nn.Module): def __init__(self, n_ent, n_rel, dim50, margin1.0): super().__init__() self.emb_e nn.Embedding(n_ent, dim) self.emb_r nn.Embedding(n_rel, dim) self.margin margin def distance(self, h, r, t): # L1 距离h r 应该靠近 t return torch.norm(self.emb_e(h) self.emb_r(r) - self.emb_e(t), p1, dim1) def forward(self, ph, pr, pt, nh, nr, nt): d_pos self.distance(ph, pr, pt) d_neg self.distance(nh, nr, nt) loss torch.relu(self.margin d_pos - d_neg).mean() return lossmargin的意义是正样本距离至少要比负样本距离小 margin 这个余量。margin太小正负样本区分不明显太大模型只关注难分样本训练震荡。我常用 1.0 起步数据量小的时候可以放到 2.0。dim默认 50当节点只有几千时50 维已经够用盲目上 200 维只会过拟合。p1是曼哈顿距离比欧氏距离更稳定这也是 TransE 论文里常用的。训练循环model TransE(n_entlen(nodes), n_rellen(relations), dim50) opt torch.optim.Adam(model.parameters(), lr0.001) for epoch in range(100): opt.zero_grad() loss model(pos_h, pos_r, pos_t, neg_h, neg_r, neg_t) loss.backward() opt.step() if epoch % 10 0: print(fepoch {epoch}: loss {loss.item():.4f})这里pos_h, pos_t是从训练正样本对映射到整数索引的 LongTensorpos_r全为关系interacts_with的索引 0。负样本同理。注意每次迭代都用同一份负样本可能让模型记住它们所以理论上每个 epoch 要重新采样。为了代码简洁我一般每 10 个 epoch 重新采样一次。如果你要复现论文指标必须固定随机种子固定负样本序列这两者矛盾我的取舍是调参时固定负样本正式训练时每 10 epoch 重采样。4.3 把嵌入内积当作推荐分数评估的时候不看 loss看 AUC 和 APTransE 训练完成后我们要给“候选药物-靶点对”打分。两种方式一是用distance的负数距离越近分数越高二是直接把药物向量和靶点向量做内积。内积没有经过 margin 约束但实现最简单和推荐系统的矩阵分解很像。我一般用内积再单独做一次标准化。import numpy as np from sklearn.metrics import roc_auc_score, average_precision_score with torch.no_grad(): emb_e model.emb_e.weight.numpy() score_test np.sum(emb_e[test_drug_idx] * emb_e[test_target_idx], axis1) # score_test 高代表预测交互 print(AUC, roc_auc_score(test_label, score_test)) print(AP, average_precision_score(test_label, score_test))评估要在独立测试样本上做。测试样本包括两部分一部分是训练图中去除部分边的“半冷启动”链路预测一部分是“完全冷门”的新药物或新靶点。如果只有前者模型很可能靠记忆节点 ID 来得分不能证明泛化能力。所以我在分割数据时保留了一组“new target”样本测试时这些靶点不出现在图里。不过 TransE 在这种情况下无法直接训练需要冷启动回退这点在下一节和避坑章细说。4.4 操作指南从建好图到跑通的最小命令与常见环境问题到了落地环节给出可复制的最小命令。首先项目目录建议这样组织dti_project/ data/CTD_chemical_gene.tsv src/build_kg.py src/train_transe.py src/evaluate.py output/然后环境安装。Python 版本建议 3.83.10PyTorch 在 3.11 上也可以装但有些旧版本 SciPy 不兼容没必要给自己增加不确定性。在命令行执行python -m venv dti_env # Windows PowerShell 激活: dti_env\Scripts\activate # macOS/Linux 激活: source dti_env/bin/activate pip install --upgrade pip pip install pandas numpy networkx scipy scikit-learn pip install torch --index-url https://download.pytorch.org/whl/cpu最后一行是指定 CPU 版 PyTorch如果你的机器有 Nvidia GPU去掉--index-url直接pip install torch会自动装 CUDA 版。这里有个小经验首次学习不需要 GPUCPU 版跑这两千条边和两万条负样本100 epoch 也就是几分钟。接着把三段脚本串起来。我通常会写一个run_all.sh但在 Windows 上更顺手的是直接在命令行里依次执行python src/build_kg.py python src/train_transe.py python src/evaluate.py提示CTD 文件的注释行数会随版本变化skiprows 请用第 2.1 节的查看方法确认后填写。如果build_kg.py报ImportError: No module named sklearn按上面 pip 命令重新安装即可。还有一个高频错误是skiprows27不匹配因为 CTD 的表头统计行数会变。最简单的排查方法是在脚本里用第 2.1 节里的读取方式先打印前 40 行确认实际行数再填。装好环境之后torch显示 CPU 版不像 GPU 版那样有设备名但训练速度没有大差别。到这里整个“推荐系统 知识图谱 DTI”的最小闭环就跑通了。但是别急着高兴真正会让你翻车的是第 5 章那些细节。5. 避坑 / 常见问题 / 排查药物靶点交互预测最容易翻车的五个细节5.1 数据泄漏训练集和测试集剪不断理还乱现象roc_auc_score给出 0.94你在心里已经把论文题目都想好了。但把模型放到一个从未见过的新药物上预测结果和随机猜差不多。原因最常见的错误是在构建测试集时直接从全部三元组中随机抽取 10% 的边。这样同一个药物在训练集中出现在测试集中也有它的另一条边图是全局共享的TransE 的实体向量自然记住这个药物。测试时它只是“还记得这个节点”而不是“真的推理”。这就造成了数据泄漏。解决用“按药物分组”的留出法。先把所有药物 ID 划分成训练药物和测试药物两个集合确保测试药物在整个图的构建过程中不出现在任何一条训练边里。在构建图的时候用训练药物包含的所有边建图测试药物本身虽然可作为新节点加入但它的所有边都进测试集不参与训练。代码上就是分组unique_drugs train_drugs | test_drugs然后建图前过滤。不要等到建完图再删边否则图结构仍然包含测试实体的间接信息。5.2 负采样方向随机替换头实体和替换尾实体结果完全不一样现象loss 收敛到 0.1 以下但 AUC 只有 0.65。另一个模型用同样的参数只改了替换方式AUC 到了 0.82。原因在 DTI 这个场景里药物实体和靶点实体的数量差异很大。如果靶点只有几百个随机替换尾实体靶点时很容易撞到另一个已知交互靶点负例虽然不在 known_set 里但它本质上仍是潜在正例类别噪声大而随机替换头实体药物因为药物多负例更可靠。反向则相反。解决不要固定一种替换策略。在负采样时同时生成两类负样本药物替换组和靶点替换组然后合并比例可以按照 1:1。此外known_set必须包含所有“当前已知能排除”的边。我一般还维护一个“已标注但不在训练集中”的关系集合用来做召回过滤保证测试正例不会被当成负例输出。具体做法负采样函数加一个参数exclude_set传入全部正样本的并集而不是训练集正样本。5.3 孤立节点与冷启动新药一进图就成了无依无靠的孤儿现象你新增了 100 个候选药物它们在知识图谱里一个邻居都没有比如还没有任何实验记录。模型对这些药物输出的分数全部在一个很窄的区间波动基本等于随机。原因TransE 和基于路径的模型都是局部结构方法没有邻居就没有信息。你不可能要求一个“纯图模型”在零边情况下做出有意义的推断。这是知识图谱方法的天花板不是工程 bug。解决采用“冷启动策略分离”。如果新药有分子结构就计算 Morgan 指纹半径 2长度 1024 位用 Tanimoto 相似和已有药物做最近邻如果新靶点有蛋白序列就用氨基酸 k-mer 的 comp 计数和已有靶点做相似度。然后把这些内容相似度作为另一个特征和 TransE 分数融合。换句话说把问题拆成两块有邻居的节点走图模型没有邻居的节点走内容相似度模型。这个融合不要等模型训练完再做应该在验证时就用 holdout 的孤立节点评估效果。5.4 图谱太稠密万金油 hub 节点让随机游走变成“景点打卡”现象随机游走特征几乎全是 0.3 左右的均值药物和靶点之间的分数差异被抹平。原因图中有极少数节点比如 TP53、EGF、TNF几乎和所有疾病、药物都有关联。随机游走只要走到这些节点就很容易走到目标而不管这个候选对在机制上是否相关。结果是每个候选靶点都有很高的 hit rate模型学不到区分度。解决做“hub 惩罚”。计算每个节点度随机游走在选择下一步时对大于某个阈值的节点降权或者直接去掉一些“万金油”节点。还有一个简单做法是每次游走记录是否经过 hub把“经过 hub 的次数”作为额外特征让逻辑回归学一个负系数。这个惩罚权重需要实验调我通常从度分布的 99% 分位数作为阈值开始把度高于它的节点概率乘以 0.3。节点度分布如何看用nx.degree(G)直接画个直方图在 hub 处会断崖。如果不做处理TransE 的嵌入也能学到“只要向量朝向 hub 就加分”这和药物机制无关。5.5 训练评估拆分方式别把“旧药新靶点”和“新药新靶点”混在一起报指标现象你报告一个 AUC 0.90换一种数据切分同一步代码报 0.70审稿人或领导只会有一种反应——“你对结果负责吗”原因DTI 预测有多种实际场景已知药物、新靶点新药物、已知靶点两者都新。每种场景难度完全不一样。随机的边级切分因为存在重叠相当于开卷考试AUC 虚高按实体切分才像闭卷考试。解决在验证时把测试集划分为四个子集旧药-旧靶点但边从图中去掉、旧药-新靶点、新药-旧靶点、新药-新靶点分别汇报 AUC/AP。我在代码里用一个dataset_version参数标记切分方式模型评估直接复用同一个函数。这样评委问起来你拿出来的每个数字都有对应的任务定义不会翻车。我见过的成熟团队都会在 README 里写一句“本模型的指标是基于实体药物/靶点独立切分并非随机边切分”这句话能挡掉一大半质疑。注意这里说的“新药-新靶点”不是指分子序列从未出现而是指该实体的所有边都不在训练图内如果只是在测试集里删掉对应边仍然会泄漏。6. 验证与进阶让预测结果不再是黑匣子用路径回溯把 Top-N 变成可解释证据模型跑完指标报完工作其实只做了一半。如果领导问了“这个预测二甲双胍和 EGFR 相互作用的依据是什么”你不能只说“模型输出 0.87”。我一般会把 Top-N 预测对重新映射回知识图谱用最短路径给每条预测补一段人工可读的证据链。比如import networkx as nx for drug, target, score in top_n: try: shortest nx.shortest_path(G, sourcedrug, targettarget) evidence - .join(shortest) except nx.NetworkXNoPath: evidence no connectable path print(f{drug}\t{target}\t{score:.3f}\t{evidence})这段代码看着简单价值却很高它把纯向量内积变成了可复核的因果链。如果路径是“药物 - 基因A - 药物B - 靶点”你就知道这是个间接证据权重可能低如果路径是“药物 - 疾病 - 靶点”更接近临床语义。我把路径类型当成一个后置过滤器只保留“drug - disease - target”这类强证据路径。这一步能在不重训练的情况下显著提高推荐结果的可信度。进阶做法是把路径证据和嵌入分数做一个极简融合排序先把路径计数归一化到 0~1再和 TransE 内积分数各取 0.5 权重相加。相比只用内积混合排序往往能抑制“hub 节点带来高嵌入相似度但缺乏机制关联”的假阳性。这个融合不需要调复杂模型一个weight_dict足够。验证时先看混合排序下 Top-50 里被文献支持的比例而不是只看 AUC我在实际项目里用这个比例作为给实验室的交付指标。最后说一个我自己的教训。很早以前我跑 DTI用全局随机切分拿到了 0.96 的 AUC还沾沾自喜发了个内部分享。后来被同事提醒做一次冷启动验证相同的代码、相同的数据按新靶点切分后 AUC 直接摔到 0.71。从那以后我给自己立了个规矩无论模型多花哨第一版指标永远先报“新药-新靶点”那栏那个数字才是实验室愿意投入真金白银的依据。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑