刚拿到一批新鲜测序的宏基因组序列时,我们做的第一件事永远是功能注释。跑完一轮同源搜索,屏幕上安静地躺着那行数字:只有不到三成的序列能找到看得过去的同源匹配,剩下的七成全部掉进“未知蛋白”的黑洞里。这个比例在当前的新基因挖掘里相当常见,也恰恰是蛋白质功能预测这个领域最真实的动力来源——光靠序列同源性远远不够了。过去二十年,我们用 BLAST、HMMER 那套思路解决了很多问题,但面对孤儿序列、趋同演化和结构域重排,传统方法的天花板已经肉眼可见。这篇文章从我的角度聊一聊蛋白质功能预测是怎么从“序列同源性”一步步走到“多模态深度学习”的,包括方法背后的原理、我做过的实验设计、数据准备与评估里的坑,以及真正跑起来之后的性能变化。适合正在做宏基因组注释的同行、刚接触生信加深度学习的同学,以及想给自己的功能预测流程加一个“升级包”的团队参考。
1. 同源注释为什么“失灵”:传统方法的天花板
1.1 从 BLAST 到 HMM:同源假设的底层逻辑
先回到传统方法的核心逻辑。BLAST 这类工具做的事情很朴素:把查询序列丢进数据库里逐一比对,按 E-value、一致性等指标筛出最像的已知蛋白,然后把那个蛋白的注释“搬”到查询序列头上。HMMER 更进一步,用多序列比对构建一个家族的概率模型(Profile HMM),再去数据库里寻找那些远端的、BLAST 已经扫不出来的同源序列。
这套思路背后的推理链是:序列决定结构,结构决定功能;两条序列在演化上同源,大概率有相似的结构,也就大概率有相似的功能。所以同源注释本质上是“在演化树上找一个最近的、已经被人研究过的亲戚”,然后接管它的注释。
链条听起来很合理,但它有两个隐藏前提:第一,你的序列必须真的能在数据库里找到亲戚;第二,亲戚的注释必须是对的。这两个前提今天都越来越靠不住了。一方面,测序通量涨得太快,数据库里“未注释蛋白”的数量以亿为单位增长,而经过实验验证的注释增长缓慢;另一方面,“同源即同功能”这个等式在特定场景下会被打破,而且不是罕见场景。
1.2 三种典型失灵场景:孤儿序列、趋同演化与结构域重排
孤儿序列。这是做宏基因组和单细胞测序的人最常碰到的。序列来自一个从未进入参考数据库的新分支,基因组学的说法叫“孤儿基因”或“孤儿家族”。你用 BLAST 搜一圈,Top hit 的一致性可能不到 30%,E-value 也不好看;用 HMMER 搜那个家族的 profile,同样没有结果。数据库里根本没有“亲戚”,自然谈不上注释转移。
趋同演化。有些蛋白从不同祖先出发,最终演化出了相似的功能和结构,但序列层面的相似度很低。比如某些糖苷水解酶和酯酶在活性位点的几何结构上高度一致,序列却排不上号。这里真正要做的是“找结构相似、功能相似”的目标,序列同源搜索直接失效。
结构域重排与功能分化。还有一个更容易被忽略的问题:多结构域蛋白在演化中不断重组,A 蛋白的催化结构域可能来自祖先甲,调控结构域来自祖先乙。当你拿整条序列去做同源搜索时,最高分的匹配往往被某个高保守结构域主导,注释结果会把你的注意力引向那个结构域的功能,而不是整条蛋白真正行使的生物学角色。更麻烦的是旁系同源物的功能分化:同一个祖先复制出来的两个基因,一个还做原来的事,另一个可能已经换了底物、换了通路,但序列相似度依然很高。你把“亲戚”的注释照搬过来,就错了。
这些失灵场景说明一个问题:功能信号并不只藏在序列的保守位点里,它还藏在结构上、基因组上下文里、相互作用网络里。传统方法没法把这些证据整合进来,于是轮到深度学习出场——但也不是一步到位。
2. 端到端序列模型:从特征工程到蛋白语言模型
2.1 手工特征时代的做法和局限
在深度学习大规模进入生物信息学之前,大家是怎么用机器学习做功能预测的?常见的套路是把序列转成一组手工设计的特征向量:氨基酸组成频率(20 维)、二肽频率(400 维)、理化性质统计(疏水性、分子量、等电点)、进化谱(PSSM)等,然后丢给随机森林或 SVM 做分类。
这种做法的局限在于特征工程的上限。你只能捕捉到“自己想到过”的信号,而蛋白质序列里的功能信号常常是非线性的、长程的:催化位点的几个残基在序列上可能隔得很远,但折叠之后聚在一起;一个功能注释往往由几十个残基的联合模式决定。手工二肽频率根本表达不了这种组合关系。而且不同蛋白长度差异很大,你把一条 100 个残基的序列和一条 800 个残基的序列压成同样长度的特征向量,信息损失已经很严重了。
2.2 卷积与注意力如何自动提取功能信号
深度学习的思路完全反过来:我不告诉你用什么特征,你自己从序列里学。把序列变成 one-hot 矩阵——20 种氨基酸在每条序列的每个位置上的存在标记——然后交给网络。
最早用起来的是 1D CNN。它的直觉很好理解:把卷积核想象成一个滑动的“k-mer 探针”,它在序列上逐步滑过,自动去识别那些频繁出现的短模式(motif)。第一个卷积层学到的是短基序,堆叠更多层之后,高层可以组合这些基序形成更复杂的模式。CNN 的优势在于局部性和平移不变性:不管这个功能 motif 出现在序列的 100 位还是 300 位,都能被同一个卷积核捕捉到。再加上池化操作,模型可以把全序列的证据聚合成一个固定长度的蛋白质级向量。
但 CNN 的视野始终受限于卷积核大小。为了捕捉远程依赖——例如 N 端和 C 端的两个位点共同形成一个功能界面——你需要堆很多层,或者用很大的卷积核。Transformer 架构把这个问题彻底改写了。多头注意力机制允许序列中任意两个位置的残基直接进行信息交互,不管它们在序列上隔了多远。蛋白质序列本身就像一种语言,氨基酸是字母,结构域是“词组”,注意力机制则学会了“哪些位置之间应该互相参考”。
我在实际使用中的体感是:对于功能预测任务,CNN 适合做“局部模式检测器”,Transformer 适合做“全局上下文建模”,两者不是替代关系,而是配合关系。很多有效的序列模型都采用“CNN 底层提取基序 + Transformer 上层捕捉长程依赖”的组合,既控制了计算量,又保留了表达能力。
2.3 迁移学习支柱:蛋白语言模型的“预训练”本质
如果说卷积和注意力解决了“怎么学”的问题,那“拿什么学”就是蛋白语言模型的功劳。自然语言处理里的预训练语言模型给了计算生物学家一个巨大启发:你不需要有标注数据,只要有无标注的序列,就能训练一个模型学到蛋白质的“语法”。
做法是掩码语言建模:随机遮住序列里一部分氨基酸,让模型根据上下文去预测被遮住的位置是什么。训练数据是数十亿条公开序列,模型在完成这个“猜词”任务的过程中,被迫学会了氨基酸之间的共现规律、结构域的组成习惯、乃至部分进化的约束。预训练结束之后,这个模型本身就成了一把特别好用的“序列编码器”——把一条蛋白序列变成一个高维向量,这个向量装满了从海量序列里总结出来的隐含特征。
迁移学习就是这么来的:先在大规模无标注序列上预训练,再在功能预测的小规模标注数据上微调。这套思路彻底解决了蛋白质功能标注稀缺的问题。我个人做过对比实验:直接用 one-hot 序列训练模型,和用预训练蛋白语言模型的 embedding 训练模型,在很多功能类别上的 Fmax 差距可以达到 0.05 以上,千万别小看这个数。
到这里,序列模态已经做得相当成熟了。但只靠序列,依然绕不开一个根本问题:很多功能信息在序列层面的编码极其微弱。这时候就需要多模态融合——把结构、上下文、相互作用这类“序列之外的证据”也拉进来。
3. 多模态融合的核心设计:结构、上下文、相互作用如何对齐
3.1 结构模态如何补充序列信息
蛋白质的很多功能,本质上是由三维结构决定的:活性位点是一个空间裂缝,底物识别依赖口袋的形状,蛋白互作依赖结合界面的互补性。序列上那些不显眼的残基,在折叠后的空间里聚成一个功能中心——这种情况用序列模型很难看出来。
所以结构模态的意义在于:把一维序列里丢失的空间关系补回来。过去主要瓶颈是实验测定的结构数据太少,PDB 里只有十几万条结构,相对于上亿条已知序列杯水车薪。结构预测模型的成熟改变了这个局面——一批预测结构已经覆盖了大部分已知蛋白。虽然预测结构还有误差,但作为功能预测的模态输入,它的骨架和残基间的空间邻近关系已经够用了。
关键是怎么把结构喂给模型。主流做法有以下几类:
- 距离矩阵/接触图:把所有残基对的 Cα 距离抽出来,形成 L×L 的矩阵,相当于把结构编码成二维图像,可以直接用 2D CNN 处理;
- 残基图:以残基为节点、空间距离为边建图,用图神经网络在图上做消息传递,显式学习“哪些残基在空间上互相影响”;
- 体素化:把三维结构转成体素网格,用 3D CNN 处理,但计算开销大,用得相对少。
我实际推荐距离矩阵加图神经网络这条路线,它兼顾了表达能力和工程难度。结构模态输入后,编码器输出的是一个结构特征向量,表达“这条蛋白在空间上有什么功能倾向”。
3.2 基因组上下文与相互作用组:给模型“环境背景”
结构模态补的是“蛋白内部”的信息,上下文模态补的则是“蛋白身处的系统”信息。功能从来不是孤立的。
最直观的例子是基因组上下文:原核生物里,同一个代谢通路的基因经常聚在一个操纵子里,编码相邻蛋白的基因往往参与同一个生物学过程。所以你可以把“目标基因上下游若干个基因的功能注释、结构域组成、COG 类别”编码成特征,作为目标蛋白的旁证。这种证据在序列和结构上都完全没有体现,却能大幅提升通路类功能(GO BP)的预测效果。
另一个常见的上下文是蛋白质相互作用网络。如果蛋白 A 和蛋白 B 在实验里被反复检测到互作,而 B 的注释已经很明确是某个信号通路的成员,那 A 大概率也在同一个通路上。具体做法是把蛋白映射到 PPI 网络,对网络里的邻居做聚合(类似图神经网络里的邻居消息传递),得到网络的嵌入。系统发育谱也有类似作用:两个蛋白在不同物种里的共现模式越一致,功能关联越强。
这些模态有一个共同点:它们的“分辨率”和序列、结构完全不一样。序列是残基级,结构是空间级,上下文是分子级甚至基因组级。多模态融合的设计难点,正在于把这些不同尺度的证据放在同一个表征空间里。
3.3 融合策略:拼接、注意力加权与门控机制
拿到三个模态的特征向量之后,怎么融合?这个选择决定了多模态模型的智力上限。我用过三种策略,效果从低到高排列:
简单拼接。三个向量拼成一个长向量,再过几层 MLP。这种做法省事,适合建立基线,但问题是:模型能学到的只是“三个向量每个维度的加权和”,它没法根据具体情况有选择性地信任某个模态。结构预测质量差的时候,拼接照样把噪音送进去。
注意力融合。把其中一个模态(通常是序列模态)作为 Query,其他模态作为 Key 和 Value,让模型为每个功能预测任务去“查”其他模态里有哪些信息相关。这种做法比拼接灵活得多,因为模型学会了按需取用。
门控融合。为每个模态学习一个可信度权重,权重由模态本身的内容决定,然后加权求和。灵感来自动态路由:如果一个蛋白的预测结构置信度很低,门控模块可以学到把结构模态的权重压下去;如果结构很可靠,就抬高它。门控融合通常加一个 Sigmoid 或 Softmax 层来输出权重。
我自己的一个经验是:简单拼接能让提升“到达门槛”,注意力融合会在某些特定功能类别上再涨一截,而真正收益最大的是把门控机制的“模态置信度”显式建模出来——它让模型学会了什么时候该忽略哪一个模态。对于噪声很大的实际数据,这个能力非常重要。
为了让大家直观理解,我放一段门控融合的简化实现(PyTorch 风格):
class GateFusion(nn.Module): def __init__(self, d_model, n_mods): super().__init__() self.proj = nn.ModuleList( [nn.Linear(d_model, d_model) for _ in range(n_mods)] ) self.gate = nn.Sequential( nn.Linear(d_model * n_mods, d_model), nn.ReLU(), nn.Linear(d_model, n_mods), nn.Softmax(dim=-1), ) def forward(self, feats): # feats: list of (B, D),每个元素是一个模态的特征向量 concat = torch.cat(feats, dim=-1) weights = self.gate(concat) # (B, n_mods) fused = torch.zeros_like(feats[0]) for i, f in enumerate(feats): fused = fused + weights[:, i:i+1] * self.proj[i](f) return fused门控的 Softmax 让三个模态的权重总和恒为 1,这在模态之间相关性强的时候会牺牲一部分信息——我后来把它换成不加总约束的 Sigmoid,又涨了一点。大家可以根据自己的数据情况试。
4. 从论文到代码:数据、标签与评估的落地细节
4.1 GO 标签层级与训练集构建
功能预测最常用的标签体系是 Gene Ontology(GO)。它分三大类:生物学过程(BP)、分子功能(MF)、细胞组分(CC)。其中 MF 是最好预测的,因为它描述的是蛋白本身“能做什么”;BP 描述的是“在什么过程里做事”,涉及上下文,更难;CC 难在定位信号并不总是写在序列里。
GO 标签不是扁平的,它是一个有向无环图,子类继承父类。建数据集的时候有个细节:如果一个蛋白被实验证据标注为“葡萄糖激酶活性”(一个叶子节点),那它的所有祖先节点其实也隐含成立。很多初学者直接把 GOA 文件里的注释拿过来当标签,漏掉了父类传播,导致模型在父类标签上的召回很低。正确的做法是把每个蛋白的 GO 标签沿 DAG 向父节点传播,补齐所有满足的标签,再用于训练。
标注来源也要筛。UniProt 的 GOA 文件里,evidence code 决定了这条注释的可靠性。比如 EXP、IDA、IMP 这类实验证据可以进训练集;IEA(电子注释)是自动推理出来的,放进训练集会引入大量噪音,通常要过滤掉。如果目标是一个更大规模的训练集,可以保留部分可靠的自动注释,但一定要按证据代码做权重区分。
最后是标签空间压缩。GO 里很多标签在数据集里只出现几次,这些长尾标签几乎学不出来。我一般会按“至少出现在 N 条蛋白里”这个阈值过滤,N 通常取 10 到 50,保证每个标签有足量的正样本。如果不做这一步,模型会把注意力浪费在永远预测不准的稀有标签上,还拖慢训练。
4.2 数据去冗余与评估协议
这一步直接决定你的模型成绩是真是假。蛋白质功能预测做大模型最容易翻车的坑就是数据泄漏:训练集和测试集里有同源序列。假如测试集里的某条蛋白和训练集里的一条序列有 90% 的一致性,模型其实只需要记住那个训练样本的答案,测试分数当然漂亮,但这根本不是泛化能力。
正确的做法是把所有序列先用 CD-HIT 或 MMseqs2 做聚类去冗余,比如按 30% 的序列一致性格子聚类,然后按“族”拆分训练、验证、测试集——同一个族里任何两条序列都不能跨集合。更严格的做法是模拟 CAFA 评估协议:只拿“在某时间点之前发现的序列”训练,拿之后新发现的序列测试,这是最贴近实际使用场景的评估。
评估指标也别搞错。功能预测不是单标签二分类,是多标签分类:每条蛋白可以同时命中好几个 GO 标签。你不能用准确率这种指标,因为大部分标签是负例,模型永远输出“没有功能”也能拿到很高的准确率。主流指标是 Fmax 和 AUPR。Fmax 的做法很有意思:模型为每个标签输出一个 0 到 1 的概率,你在验证集上扫描阈值 t ∈ [0,1],每个阈值下算一组整体的精确率和召回率,两个曲线相交时的 F1 最大值就是 Fmax。
Fmax 实现代码不长,但要小心“蛋白级别”和“标签级别”的区别。CAFA 的计算方式是:把所有蛋白的所有标签预测概率拉平,按阈值决定哪些算预测命中,再统一算精确率和召回率,而不是先对每个标签算指标再平均。我用错误方式算过高估 0.02 的成绩,教训深刻。
4.3 损失函数与多标签训练循环
多标签训练常用的损失函数是 BCEWithLogitsLoss,每个标签当作独立的二分类问题。但这个损失面对 GO 的长尾分布有个问题:正样本太少,模型倾向把所有标签都输出成 0。两个解决思路:
第一是用pos_weight给正样本加权,权重设为负样本数除以正样本数(或 10 倍),把缺失的梯度补回来。第二是用 Focal Loss,让模型更关注难分的样本、降低对已正确分类样本的注意力。我在遇到“全阴性预测”的时候,Focal Loss 的效果比简单的 pos_weight 更稳定,代价是要多调一个 gamma 超参数。
训练循环里还有一个工程细节:蛋白质序列长度差异极大,短的几十残基,长的几千残基。朴素的 DataLoader 会按批次里的最长序列 padding,导致计算浪费严重。我习惯先按长度分桶(比如 0-200、200-400、400-800、800+),在每个桶内单独构建 batch,最大程度减少 padding。配合混合精度训练和梯度累积,显存和训练时间都能降下来。
下面是一个简化但可跑通的多标签训练循环骨架:
for epoch in range(n_epochs): model.train() for batch in bucket_dataloader(train_data): with torch.amp.autocast("cuda"): logits = model(batch) # (B, num_labels) loss = focal_loss(logits, batch.labels) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) scaler.step(optimizer) scaler.update() optimizer.zero_grad() # 验证 valid_scores = predict(model, valid_data) fmax = compute_fmax(valid_scores, valid_labels)验证阶段收集到的 logits 要保存成 0-1 的概率,用于在验证集上搜索最佳阈值。这个过程尽量不要用默认的 0.5——我在多个数据集上都看到最佳阈值在 0.2 到 0.4 之间,直接按 0.5 截断会把召回率砍掉一大截。
5. 实测记录:从基线到多模态模型的性能变化
5.1 基线是多少,提升来自哪一步
我用自己的数据集跑过一整套对比实验,数据来自一个微生物宏基因组的功能注释项目,MF 类标签,去冗余后有大约 3 万条训练蛋白、4000 条测试蛋白。先把结果摆出来:
| 方法 | Fmax(MF) | 备注 |
|---|---|---|
| BLAST 最佳命中注释迁移 | 0.31 | 有 25% 序列没有同源匹配 |
| 序列组成特征 + 随机森林 | 0.35 | 手工特征天花板明显 |
| 蛋白语言模型 embedding + 1D CNN | 0.44 | 端到端序列模型的明显优势 |
| 上面基础上 + 结构模态(距离图+GNN) | 0.48 | 结构信息补了口袋类功能 |
| 再加上基因组上下文与门控融合 | 0.51 | 上下文对 BP 类提升更明显 |
这里有个非常直观的规律:BLAST 时代的基线其实很弱,Fmax 只有 0.3 出头;换成深度序列模型,立涨 0.1;加结构模态再涨 0.04;换成完整多模态加门控,总共比 BLAST 高了 0.2。Fmax 到 0.5 之后每涨 0.01 都非常难,0.2 的差距意味着相当大的进步。
拆开细看时,提升不是均匀分布的。序列模型主要提升了“整体功能概率的打分”,结构模态显著提升的是与“口袋和活性位点”相关的标签类别——比如“氧化还原酶活性”、“转移酶活性”;上下文模态对“代谢过程”这类 BP 标签的增益最明显。这印证了一个观点:不同模态服务的功能类型不一样,多模态融合的价值在于“让每个标签都找到最能佐证它的那个模态”。
5.2 踩坑:数据泄漏、全阴性预测与显存瓶颈
分享三个让我花掉最多时间的坑。
数据泄漏。最开始我做了一个错误的测试集划分:随机按序列 ID 抽 10% 当测试集。Fmax 跑了 0.61,当时还挺高兴。后来发现同一家族的序列同时出现在训练集和测试集里,模型显然“见过”答案。改成按 30% 相似度聚类去冗余后,成绩一下掉了 0.1,才是真实水平。从那以后,我评估任何功能预测模型,先问一句话:“你的训练集和测试集做过序列级去冗余吗?”没有去冗余的排行榜成绩,我一律打个问号。
全阴性预测。有一次训练一个融合结构模态的模型,验证显示精度很高、召回却几乎为零。检查后发现,因为 GO 标签极其稀疏,模型优化到最后学出了一个“取巧”策略:所有输出概率都很低,这样在阈值上砍掉一切预测就能拿到很低的损失。这显然不是我们想要的模型。解决方法是给正样本加权,配合 Focal Loss,再在验证集上按 Fmax 而不是按 loss 选 best checkpoint,才把召回拉起来。
显存瓶颈。长序列加注意力机制是个吃显存大户,5000 残基以上的蛋白序列直接跑 Transformer 会非常难受。我的处理方式是分桶加梯度累积,必要时把序列切成 1024 的窗口片段,只保留片段表征并做最终池化。这个操作会牺牲一部分长程依赖信息,但在有限硬件下是值得的取舍。训练时可以用半精度,推理时再把参数转回单精度,大多数情况下精度损失可以忽略。
5.3 我的最后选型建议
经过这些实验,我现在对不同场景的推荐是:
- 快速批量注释(几万个蛋白、只求大概方向):BLAST + HMMER 扫描就够,成本低、解释性强。别为这个场景上多模态。
- 一个中等规模的深度研究项目(新基因家族、孤儿序列占大头):直接上多模态。以蛋白语言模型 embedding 为底,加结构模态,上下文模态如果数据容易获得就加。
- 把功能预测做成稳定服务(定期更新、处理大量新序列):先按序列相似度聚类去掉冗余,再走推理流程,避免同样的计算重复花费在近同源序列上。别把所有家底都押在单一模态上,保留门控融合机制才能应对真实数据里的模态缺失。
版本管理也是一个常常被忽略的点:GO 本体每次更新都会新增或调整标签,GOA 注释文件更是持续增量更新。如果不记录 GO 版本和注释文件日期,过两个月你自己都搞不清实验结果横向对比的是哪个标签集。我会把数据集构建脚本、GO 版本、种子号一起写进实验的 README,方便日后复现。
我现在的习惯是:拿到一批新蛋白,先跑一轮去冗余,再看同源覆盖率。覆盖率高于 70% 就用传统注释加人工核对;低于 50% 就直接走多模态模型,把序列 embedding、预测结构距离图和基因组上下文一起灌进去,然后对预测结果按标签来检查关键用例。工具确实更聪明了,但每次在人机结合观察几个具体蛋白的预测理由时我还是会想,蛋白质功能预测说到底是在帮我们排一个“最可能方向”的优先级,最后的验证终究还得回到实验台前。这也是为什么我对多模态深度学习的态度一直是:用它来缩小搜索空间,而不是用它来代替生物学判断。