基因疾病关联分析这个方向这几年在生信圈子里越来越热。传统做法要么靠统计学关联检验要么靠通路富集但一个核心痛点始终绕不过去基因和疾病之间的关系本质上是网络化的基因不是孤立起作用的。于是很多人把目光投向了图卷积神经网络GCN用它来做基因疾病关联预测。这玩意儿的思路说白了就是把基因放进一张图里让模型自己学着从邻居节点那里聚合信息最终判断某个基因和某类疾病之间到底有没有关系。我基于这个方向做过几个模拟项目从数据准备到模型调参踩了不少坑也积累了一些实操经验。这篇就把它拆开揉碎了讲清楚希望能给准备入坑或者正在被数据折腾的同行提供一份能直接抄作业的参考。我默认的读者是做过一些机器学习或者深度学习基础项目、但没怎么接触过图神经网络的人。就算你眼下对GCN完全陌生问题也不大后面每个环节我都会尽量配合具体例子讲用到什么概念就现场解释。整体内容覆盖从数据怎么整理、图怎么构建到模型结构怎么定、训练评估怎么做再到那些网上文档很少提及的坑全部是实战视角。项目代号我就用某疾病基因关联分析项目来称呼。1. 整体设计思路为什么非要用图卷积1.1 传统方法到底卡在哪里先聊聊常规路线。如果我们想找出某种疾病相关的基因最经典的办法就是做全基因组关联分析跑出一堆显著性位点然后再根据位点位置或者功能注释往基因上靠。另一种常见路线是差异表达分析拿患病样本和健康样本比一比筛出一批表达量显著变化的基因。本质上这两种办法都是把基因当成一个个独立变量去检验基因之间的交互关系基本是丢掉的。为什么丢掉交互关系不行最典型的一个例子就是很多疾病驱动基因在差异表达分析里表现并不突出。它们不是靠自身表达量大幅波动来发挥作用的而是通过改变自己在调控网络中的位置、影响周边基因的表达来推动疾病进程。你要是只看单个基因的表达变化这类关键节点很容易被漏掉。还有个现实问题实验数据有偏好。不少疾病的关联基因已经研究得很充分文献里积累了大量标注样本但还有大量罕见病、复杂病已知关联少得可怜。面对稀疏标注的情况传统监督学习很容易过拟合毕竟标签就那么点。这时候把基因放进网络结构里去想就合理了一个基因很可能是通过与它物理互作、共表达或者共调控的邻居基因来参与疾病通路的。判断一个基因跟某个疾病有没有关联参考一下它在网络里的社交圈子往往比只看它自己靠谱得多。图卷积神经网络干的事情本质上就是把这个社交圈子信息自动编码成特征再用来做预测。1.2 图卷积网络的消息传递直觉图卷积的概念听着抽象但它背后的直觉其实非常朴素。想象一个社区里所有人组成一张熟人关系网你想判断某个人最近会不会生病你光问他自己还不够你会去问他家人、同事、常打交道的人。一层邻居不够就再往上一层问。图卷积的操作就是这么个过程每个节点通过一系列聚合操作把自己的特征和邻居的特征融合起来生成新的表示多叠几层就能吸收到更远处的信息。和图像卷积对比会更好理解。图像卷积是在规整的像素网格上滑动一个小窗口把窗口内像素加权求和图卷积则是在任意结构的图上把邻居集合当成窗口对邻居特征做某种形式的聚合。区别只是邻居的数目不固定、排列没顺序所以聚合方式要做特殊设计最常见的做法是加权求和再配上非线性变换。放到基因疾病关联的场景里节点是基因边是基因之间的互作关系比如蛋白质互作、共表达、基因调控等。初始特征是基因自身的属性比如序列特征、表达谱、功能注释编码。模型跑完几层卷积每个基因节点都会得到一个整合了网络上下文信息的向量最后拿这个向量去预测它和某种疾病之间是否有关联。就这么简单没有黑魔法就是一个端到端的特征学习过程。1.3 在链路预测和节点分类之间怎么选具体到基因疾病关联任务建模方式还能再分细一点。第一种叫节点分类把疾病作为标签判断一个基因是否属于这个疾病的相关基因集合。第二种是链接预测把基因和疾病定义成两类不同节点基因疾病关联就是两类节点之间的边预测某条边是否存在。我在项目里两种都试过实际体验是链接预测更符合这个任务的天性。原因很直观一个基因可能和多种疾病相关节点分类在这种多标签情况下处理起来有点拧巴而链接预测天然就是二元关系判断基因节点、疾病节点都进一张异构图边代表关联模型学会的是这种类型的节点之间会不会连边。不过节点分类实现起来简单如果数据集不大、疾病类别单一也能用。后面讲到模型结构时我会把两条路线都说明白方便大家根据自己的任务做取舍。2. 图数据构建与特征工程这不只是把表格塞给模型2.1 网络从哪里来边的构建方案图卷积的输入不是简单的特征矩阵加标签向量你首先得有一张图。如何构建这张图直接决定了模型性能的上限这比调模型参数重要得多。常用信息来源大概分几类。蛋白质互作网络是最常见的很多公开数据源把已知的蛋白质物理互作关系收录得很全面适用范围广。基因共表达网络也常用拿大规模表达谱数据算基因之间的相关系数然后按阈值截断超过阈值的就连一条边。基因调控网络则偏上游关系转录因子和靶基因之间的调控边对理解疾病机制特别有意义。不同来源的边能提供不同视角的信息所以实际项目中往往会做多网络融合。我用的做法比较简化但有效给不同来源的边赋不同权重然后合并成一张加权网络。比如蛋白质互作关系权重高一点共表达关系权重低一点因为共表达关系噪声相对大。还要注意网络里边的数量级。一个全连接的基因共表达图可能产生几百万条边计算量非常可观而且噪声巨大。通常的做法是只保留每个基因相关性最高的若干邻居比如每个节点取前几十个强关联基因这样可以网络变稀疏计算快很多效果反而更好。2.2 节点特征选什么图卷积用到的节点特征有两种情况。一种是没有外部特征纯结构学习也就是只靠图的拓扑信息来学节点表示另一种是有外部特征比如基因表达谱、序列编码、功能注释等等。实际项目中带外部特征的效果明显更好因为纯粹的拓扑信息虽然有用但信息量有限。我试过几类特征谈谈体验。基因表达谱是最常用的不同组织条件下某个基因的表达水平形成的向量被视为基因的状态签名相似表达模式的基因往往有类似功能。基因序列特征也可以比如用预训练的语言模型直接抽取DNA序列向量这类特征对没有表达数据的冷门基因尤其重要。功能注释信息比如基因本体注释编码成向量也是很好的补充维度。特征维度上也有讲究。GCN层数较浅的时候初始特征直接决定了表达能力的上限。我自己做实验发现把表达谱和功能注释拼在一起作为输入比单独用任何一种都要稳定提升幅度大约在几个百分点。所以磨刀不误砍柴工特征工程阶段值得多花一些精力。2.3 标签怎么整理正样本和负样本的匹配这个问题容易被忽略但实际操作中坑最多。已知的基因疾病关联就是正样本负样本怎么来这个问题没有标准答案。很多文献的做法是随机抽取与正样本数量相当的基因疾病对作为负样本但这里有个陷阱随机负样本里可能混入尚未被发现的真实关联属于潜在正样本。另一种更谨慎的办法是用已知的、明确不相关的基因疾病对来做负样本比如通过数据库中的排除信息或专门的反向标注。但这类数据的覆盖率很低往往只能覆盖一小部分基因。我在模拟项目里从实操角度给大家一个折中方案随机负样本为主但要加清洗步骤。比如把那些在多个疾病里反复出现、注释很泛的基因从负样本候选集中剔除因为这类基因往往是任意疾病都可能沾点边的万金油基因。清洗完之后负样本质量会明显改善模型学到的东西也更可信。标签的划分方式也有讲究。不能直接随机切分因为同一个基因的不同关联被分到训练集和测试集时会形成信息泄露。更好的做法是尽量保证测试集中的边对应的基因在网络里也出现过。换句话说训练集负责让模型学会模式测试集用来验证泛化能力这两部分不能产生过多重叠信息。3. GCN模型结构拆解两三层就够用别迷信深度3.1 GCN层的工作原理和公式拆解图卷积层的数学表达并不复杂。设输入特征矩阵为 H邻接矩阵为 A加上自环之后得到 A A I对应的度矩阵为 D那么单层图卷积的输出可以写成H σ(D^{-1/2} A D^{-1/2} H W)这里 W 是这一层可学习的权重矩阵σ 是激活函数比如ReLU。中间那块 D^{-1/2} A D^{-1/2} 是归一化后的邻接矩阵作用是让每一条边都以对称的方式缩放权重避免度大的节点在聚合时特征数值被过分放大。这个对称归一化处理相当于先把每个节点的邻居特征平均一下再乘上自己的可学习权重确实是合理的设计。为什么需要自环如果不加自环每个节点在聚合时只会看到邻居自己原本的信息反而丢掉了这里参数更新之后就会导致表达能力受限。加自环用一句话说就是让节点在聚合邻居时也听到自己说话。两层GCN的结构大概是Z softmax(A D^{-1/2} A D^{-1/2} ReLU(A D^{-1/2} A D^{-1/2} X W_0) W_1)上面这个式子看着有点长但本质就是聚合一次非线性变换一次再聚合一次再变换一次。第一层聚合函数能得到一跳邻居的信息第二层就能把两跳邻居的信息揉进来。实际处理复杂网络时两到三层的GCN通常就够了。再叠更多层反而会发生过平滑现象就是所有节点表示趋同、难以区分这在基因相互关联度很高的生物网络里尤其明显。3.2 模型变体怎么选GCN、GAT还是GraphSAGE基础GCN是骨架但实际项目里往往会针对具体问题做一些调整。这里列出几种常见变体大家可以按需求对照。标准GCN实现简单计算效率高适合边权重相对均匀的网络是默认首选。GAT图注意力网络给每条边学习一个注意力权重不同邻居的重要性不一样。处理异质性强的生物网络很有用比如基因网络中有些邻居是核心调控因子有些只是弱共表达关系注意力机制能自动学会区分。代价就是计算复杂度上升。GraphSAGE采用采样聚合策略每层只随机采样固定数量的邻居训练时间更可控特别适合超大图。但采样的随机性会引入一点噪声。带边权重的GCN如果图本身有边权重比如共表达系数可以把它直接乘到聚合过程中。标准GCN不支持权重导入需要改一下实现这个改动在代码上并不复杂。我在模拟项目X里用带边权重的标准GCN作为baseline然后加了一组GAT对照效果上GAT在精确率上略高一点但训练时间长了不少。考虑到生物网络的噪声本来就大把这些噪声拟合进去未必是好事最终我选的还是改进权重后的标准GCN稳定性和效率的平衡最好。3.3 下游任务头设计两个方向的分叉点模型上半身设计好之后下半身还要根据任务类型接不同的输出头。做节点分类就简单了最后一层节点的表示向量接一个全连接层再加softmax或者sigmoid。每个基因得到一个分数代表它和这个疾病是否相关的概率。如果疾病类别很多可以每个疾病都建一个独立的二分类头这是多标签分类的常见做法。做链接预测就有意思了需要额外考虑基因节点和疾病节点的表示怎么交互。最朴素的做法是把两个节点的向量拼接起来过一个多层感知机输出一个分数。这个方案实现简单、表现稳定我推荐先拿它做baseline。稍微高级一点的做法是向量点积或者双线性变换比如分数 u^T M v。这种做法假设关联强度可以由两个节点表示的双线性映射决定参数更少在关系建模上有一些理论优势。不过实际性能差异没有特别大具体还是要看数据集。4. 实操过程与关键实现从空图到稳定收敛4.1 数据准备与邻接矩阵构建直接说代码。假设我们已经把基因互作关系整理成一个边列表import torch import numpy as np import pandas as pd # edge_list: 两列分别为基因A、基因B第三列为边权重 edge_df pd.read_csv(gene_network_edges.csv) gene_list sorted(set(edge_df[gene_a]) | set(edge_df[gene_b])) gene2id {g: i for i, g in enumerate(gene_list)} num_genes len(gene_list) print(基因节点数:, num_genes) # 构建邻接矩阵加上自环 adj np.zeros((num_genes, num_genes)) for _, row in edge_df.iterrows(): i, j gene2id[row[gene_a]], gene2id[row[gene_b]] w row.get(weight, 1.0) adj[i, j] w adj[j, i] w # 无向边 np.fill_diagonal(adj, 1.0) # 自环 # 对称归一化D^{-1/2} A D^{-1/2} deg adj.sum(axis1) deg_inv_sqrt np.power(deg, -0.5) deg_inv_sqrt[np.isinf(deg_inv_sqrt)] 0.0 adj_norm deg_inv_sqrt[:, None] * adj * deg_inv_sqrt[None, :] adj_tensor torch.FloatTensor(adj_norm)这里说明一下几点。一是稀疏矩阵优化的问题基因网络动辄几万个节点这个数目的稠密邻接矩阵就可能有几亿个元素显存会爆。实际代码里一定要用torch.sparse格式。二是要提前检查连通性图中如果有大量孤立节点这些节点没有邻居信息初始特征就直接决定了输出网络对它们束手无策要尽早意识到这一点要么补数据要么在评估时单独统计。4.2 模型编写与训练循环两层的GCN实现用PyTorch写出来特别短import torch.nn as nn import torch.nn.functional as F class TwoLayerGCN(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout0.3): super().__init__() self.gc1 GraphConvolution(in_dim, hidden_dim) self.gc2 GraphConvolution(hidden_dim, out_dim) self.dropout nn.Dropout(dropout) def forward(self, x, adj): x F.relu(self.gc1(x, adj)) x self.dropout(x) x self.gc2(x, adj) return x class GraphConvolution(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.weight nn.Parameter(torch.FloatTensor(in_dim, out_dim)) self.bias nn.Parameter(torch.FloatTensor(out_dim)) self.reset_parameters() def reset_parameters(self): nn.init.xavier_uniform_(self.weight) nn.init.zeros_(self.bias) def forward(self, x, adj): support torch.mm(x, self.weight) output torch.spmm(adj, support) return output self.bias特征矩阵、邻接矩阵都准备好之后训练循环和普通神经网络没太大区别。损失函数根据任务选节点分类一般用交叉熵链接预测更常用二元交叉熵。做链路预测时关键是把样本组织成边预测的样子每条训练样本是一个基因疾病对模型输出预测分数然后和真实标签算损失。这个数据组织和普通分类任务完全不同需要额外构造样本加载逻辑。4.3 评估指标别只看准确率基因疾病关联数据是典型的类别不平衡场景正样本通常只占极少数。准确率在这种场景下就是自欺欺人全预测成负样本都可能拿到很高的准确率但一点用都没有。我们项目里最看重的是AUROC和AUPRC。AUROC反映的是模型区分正负样本的总体能力对类别不平衡相对不敏感。AUPRC则重点考量正样本预测的准确性和召回率在正样本稀疏时AUPRC比AUROC更能反映真实表现。我举个例子某个模型AUROC是0.9但AUPRC只有0.2这意味着它虽然能把正负样本大致分开但实际挑出来的正样本里大量是假阳性对实际生物学实验验证来说几乎没法用。前K命中率也很适合这个场景。实际操作中我们不是要模型给所有基因打分而是要它推荐一个候选列表供后续湿实验验证。这种情况下把排名前100的基因里包含多少个真实关联基因作为指标比全局指标更有参考意义。这个评价思路跟搜索引擎的推荐系统非常像。5. 训练调参与问题排查我踩过的那些坑5.1 类别不平衡处理基因疾病关联的正负样本比在很多数据集里能达到1比几十甚至更悬殊。不处理的话模型会疯狂偏向预测多数类梯度被大量负样本主导正样本几乎学不到有效信号。我试过的方法里最直接有效的是给损失函数加拉偏项。比如正样本损失权重设为2到5之间负样本设为1。权重具体取多少取决于数据集的正负比例一个经验法则是让加权后的正负样本损失贡献大致均衡。还有一个更稳的办法是用了难负样本挖掘就是训练过程中每轮额外挑选那些被模型当前错误判成正样本的负样本加大它们的惩罚这能让模型更快学会边界。做数据增强的时候也要小心不能随便采样或者简单翻转特征读取生物网络数据时基因的语义是有方向的翻转没有任何生物学意义。别照着计算机视觉那套方法直接用。5.2 过平滑问题层数并不是越多越好前面提过过平滑现象GCN堆叠层数多之后由于多次聚合每个节点最终表示会趋向于整个图的平均状态节点之间几乎没有区分度。在生物网络中因为很多关键基因天然连接密集这个问题来得更早。我试过四层GCN效果反而不如两层节点表示的分布确实糊成一片了。解决办法有几个。一是降低层数两到三层通常是黄金区。二是给每层输出加残差连接让节点每层都能保留自己的一份原始信息。三是用跳跃连接的方式把每一层的表示拼起来再用。我把残差连接加到项目里之后三到四层的效果才勉强接近两层的水平让我坚定地回到两层模型。更实用的做法是训练过程中记录每个节点的表示向量定期算一算平均相似度。相似度持续走高就是过平滑的信号。这个监控手段很简单但能救命。5.3 数据泄露一个容易让结果虚高的陷阱图模型里数据泄露问题特别隐蔽。假设某个基因有多个疾病关联如果你随机把所有关联边分到训练集和测试集这个基因的信息其实同时在两边出现。测试集的表现会虚高但实际应用时根本没有这种便利模型面对的是全新的基因或者全新的关联。解决思路是按节点划分而不是按样本划分。做节点分类要保证同一个基因的关联不会出现在两个集合里。做链接预测更严格的做法是训练时把测试集相关边的两个节点在图中做屏蔽处理让它们无法通过邻居结构偷看信息。我第一次在这个问题上栽跟头实验指标特别漂亮但换了一个验证数据集立即崩了后来排查出是这个原因。这个教训让我对每一条评估结果都会先问一句信息有没有通过图结构泄漏到测试集里。5.4 训练不稳定的排查顺序遇到训练不收敛或者loss振荡不用慌按照顺序排查基本都能定位。先看特征归一化。基因表达谱数据分布跨度很大有些基因数值高几个数量级不归一化直接喂进模型梯度更新会非常不稳。我的做法是逐特征做标准化或者至少做log1p变换压一下动态范围。再看学习率GCN在小数据集上很敏感学习率过高容易直接NaN。我一般从0.01开始试不行就0.001、0.0001阶梯式降。再看邻接矩阵是否归一化到位没归一化的话每次聚合数值会越滚越大几个batch之后loss直接起飞。有个我在实际项目里常用的技巧把训练过程分成预热和正式两个阶段预热阶段用较低学习率跑几个epoch让参数先落到一个合理区域再加速。看着是小事但对稳定收敛帮助很明显尤其在图特别大的时候。5.5 常见问题速查表现象可能原因常用解决办法训练loss持续不降特征未归一化、学习率过高、标签噪声过大检查输入数据分布降低学习率清洗负样本验证集指标很高但换数据集失效数据泄露、过拟合按节点划分数据集关闭测试节点邻居信息节点表示相似度过高过平滑降低层数加残差连接预测结果全是同一类类别极度不平衡加样本权重难负样本挖掘孤立节点预测效果差缺少邻居信息补充额外特征对孤立节点做单独评估显存不足邻接矩阵未稀疏化使用稀疏矩阵存储必要时采样邻居6. 实操心得我从这个项目里真正学到的东西如果让我给后来者一句最核心的建议那就是在基因疾病关联这个任务上数据质量比模型上限重要得多这也是我做了几轮实验之后最深的体会。很多人一上来就在模型结构上反复折腾把图注意力、多层堆叠、各种正则试个遍结果提升幅度还不如认认真真洗一遍负样本、重新构建一版更可靠的基因网络。图构建阶段值得多花时间。不同数据源的网络质量差别极大质量差的数据源不仅没帮助还会把噪声传播到整个网络。我在第二次迭代的时候大幅削减了低质量共表达网络的边只保留了高置信互作数据和经过验证的调控关系单单这个改动就让AUPRC有了明显提升。这比改任何模型参数都见效。关于预测结果的解读也要保持清醒。模型输出的分数本质是这个基因与疾病存在关联的可能性的统计估计而不是直接的因果证据。一个基因排到前十名只意味着它值得进入后续验证流程绝不代表它就是驱动基因。实际项目里我们通常会拿预测结果和通路富集分析、共定位分析等独立证据相互印证多方汇合了才敢真正往下推。另外一个比较有价值的扩展方向是把知识图谱技术加进来。基础的GCN只利用了基因互作网络但疾病其实还关联着表型、药物、通路等大量实体。如果把基因、疾病、药物、表型都作为节点构建异质图用关系图卷积网络来建模模型能捕捉的信息维度会丰富很多。我在实验性质的小规模测试里做过一次复杂度明显上升但对一些特别稀疏的疾病效果改善比较明显。这块算是进阶玩法等基础版本稳定了再考虑也不迟。
阅读完成 · 觉得有帮助?