☰
PyTorch实现深度度量学习:蛋白质二级结构预测实战指南
2026/10/1 23:00:48 网站建设 项目流程

简介:基于Python深度度量学习实现蛋白质二级结构预测的完整课程设计源码,面向生物信息学、机器学习方向的高校学生,适用于期末大作业、课程设计等场景。项目已通过导师指导并获97分,下载解压即可直接运行,涵盖数据加载、模型定义、训练与推理等核心模块。资源共12个文件,以5个Python脚本为主,另含预训练权重参数(pdparams)与结果文本,压缩包约43.75MB,结构清晰便于对照学习。其中同时提供ResNet编码器与Transformer编码器两套参数,可直观对比不同架构在二级结构预测任务上的效果。目前已有129人学习下载,适合希望快速复现完整项目、理解深度度量学习在生物序列分析中应用的读者。

1. 蛋白质二级结构预测,为什么期末作业偏偏要写“深度度量学习”

蛋白质二级结构预测,本质上是一个逐残基的分类问题:给一条氨基酸序列,为每个残基标上 H(螺旋)、E(折叠)或 C(线圈)。如果只是做分类,一个普通的卷积网络就能交差,但作业题目里出现“深度度量学习”,说明老师想考察的不是“你能不能训一个分类器”,而是“你能不能把数据映射到一个有意义的嵌入空间”——让同类结构的残基互相靠近,不同类结构的残基互相远离。这个思路在序列特征噪声大、标签类别不平衡的生物数据上,比硬分类更稳,也更容易在答辩时讲出亮点。这篇按一个可交付的源码包来拆解:从 DSSP 标签体系、损失函数选型,到 PyTorch 最小实现、参数设置和常见坑,最后落到可视化验证。适合已经有 python 基础、正在做生物信息学或深度学习课程设计的人。

2. 先立住两个基础:Q3/Q8 标签怎么定,度量损失怎么选

动手写模型前,先把两个概念钉死:你预测的“二级结构”到底是什么,以及“度量学习”在这个任务里到底在学什么。这两个问题没想清楚,后面调参全是碰运气。

2.1 Q3/Q8 与 DSSP:先搞清楚你在预测什么

蛋白质二级结构的权威定义来自 DSSP,它把每个残基分成 8 类:H(α-螺旋)、G(3-螺旋)、I(5-螺旋)、E(β-折叠)、B(孤立β桥)、T(转角)、S(弯曲)和 C(线圈)。但这 8 类之间并不平衡:H 和 C 占大头,E 其次,B、I 这类很少。如果你直接做 8 分类,模型很容易把 B、I 完全忽略,导致 Q8 准确率看起来还行、实际分类质量很差。

期末作业最常用的做法是先把 8 类合并成 3 类,也就是 Q3:H/G/I 合并为 H,E/B 合并为 E,T/S/C 合并为 C。这样类别更均衡,训练更稳定,答辩时也更容易把准确率指标讲清楚。合并映射关系如下:

DSSP 原始类别含义Q3 标签
H, G, I螺旋类结构H
E, B折叠类结构E
T, S, C转角、弯曲、无规卷曲C

如果你拿到的数据是 PDB 结构文件,可以用 BioPython 直接抽取 DSSP 标签。常见做法是给每条蛋白链生成一个label数组,和序列逐一对齐:

from Bio.PDB import PDBParser, DSSP def load_q3_labels(pdb_path, dssp_path): parser = PDBParser() model = parser.get_structure("protein", pdb_path)[0] dssp = DSSP(model, dssp_path) seq, labels = [], [] for key in dssp.keys(): aa = dssp[key]["aa"] ss = dssp[key]["ss"] seq.append(aa) labels.append(reduce_to_q3(ss)) return "".join(seq), labels def reduce_to_q3(ss): if ss in "HGI": return 0 # H 类 if ss in "EB": return 1 # E 类 return 2 # C 类

这里reduce_to_q3返回的是整数标签,后续可以直接喂给CrossEntropyLoss。注意 BioPython 的 DSSP 需要系统里提前装好dssp可执行文件,否则这段代码会在初始化时报错。如果你的机器上没有 DSSP,另一个常见做法是直接下载别人已经解析好的.dssp文本文件,按列读取二级结构字母,再套用同样的合并逻辑。

2.2 三种度量学习目标怎么选:triplet、prototype、contrastive

深度度量学习的核心不是预测标签,而是学习一个映射函数,让同类别样本在嵌入空间里的距离小于不同类别样本。放到二级结构任务里,就是让 H 类的残基特征聚成一团,E 类聚成一团,C 类聚成一团。常见的三种目标函数各有取舍。

Triplet loss 是最经典的方案:每个样本有一个锚点、一个正样本、一个负样本,损失函数让锚点与正样本的距离比锚点与负样本的距离近一个 margin。这个方案的优点是直观,缺点是样本组合爆炸,随机采样时最容易出现“全是简单样本、loss 不下降”的怪圈,所以实际要做 batch-hard mining,也就是只在每个 batch 内部挑最困难的正负样本对。

Prototypical network 的思路更轻:对每个类别求样本均值作为原型,预测时看样本距离哪个原型更近。它天然适合小样本场景,但表达力不如 triplet,因为直接用均值代表整个类别,忽略了类内结构。期末报告里讲这个方案比较容易,但想把 Q3 做高一点,光靠 prototype 不够。

Supervised Contrastive(SupCon)是近几年更稳的选择:正类样本互相对齐,所有负类样本互相推开,用温度系数控制聚拢程度。它比 triplet 稳定得多,调参压力小,GPU 利用率也高。如果你不想在 triplet 的 margin 上磨太久,直接上 SupCon 是更省力的路径。

作为期末大作业,我给三个参考方案:想快速交差用 prototype;想讲清楚经典理论用 triplet + batch-hard;想拿高分又不想太痛苦,用 SupCon 或者“交叉熵 + triplet 联合训练”。后者也是最贴合标题的做法,下文代码就用这个组合。

2.3 输入特征:one-hot、PSSM 与预训练向量怎么搭

模型输入不是一段字符串,而是每个残基对应的特征向量。最基础的是 21 维 one-hot 编码(20 种氨基酸加一个未知符号),信息量够用但太稀疏。进阶做法是加上 PSSM 特征:通过 PSI-BLAST 多次迭代比对同源序列,得到每个位置 20 种氨基酸的替换得分,维度 20,代表进化保守性。PSSM 对二级结构预测的增益很大,也是传统方法能达到 70% 以上的关键之一。

常见做法是把 one-hot 和 PSSM 拼接,得到每个残基 41 维输入。如果你的环境里没有 PSI-BLAST,或者跑不动大数据库,也可以用 ESM 系列预训练模型提取每个残基的向量,但向量维度动辄几百维,训练和内存开销明显变大,期末作业不一定划算。我一般建议:先跑通 one-hot + PSSM 的特性组合,再考虑是否引入预训练向量。

环境准备上没什么特殊的,PyTorch 加上 numpy 就够。如果你用的是 PyCharm 配置 python 环境,直接给项目建一个requirements.txt,把torch、numpy、scikit-learn、matplotlib写进去,终端执行pip install -r requirements.txt即可。可视化阶段会用 scikit-learn 和 matplotlib,后面专门说。

3. 用 PyTorch 把“编码网络 + 度量学习 + 分类头”跑起来:一份可交付的源码骨架

标题既然叫“源码”,那这章就是整篇的核心:怎么把前面说的方案落成一份能跑、能交、能讲的结构。代码组织方式直接影响答辩观感,也影响你自己排错的效率。我一般会把工程拆成 6 个文件,各管一件事。

3.1 工程结构:一份能交的期末代码要拆成几个文件

先看文件分工,这样后面每段代码都有归宿:

文件职责关键内容
config.py统一管理超参数窗口大小、学习率、embedding 维度、数据集路径
data_loader.py读取序列与标签DSSP 解析、特征编码、滑动窗口、训练/验证切分
model.py定义网络结构1D 残差编码器、投影头、分类头
loss.py度量学习损失batch-hard triplet loss
train.py训练主循环联合损失、早停、模型保存
eval.py评估与可视化Q3 准确率、混淆矩阵、t-SNE 图

拆开的核心原因是:期末答辩老师大概率会逐文件问你“这是什么、为什么这么写”。如果你把所有代码塞在一个 Python 文件里,功能都能跑,但讲到第三分钟就没法往下聊了。另一个实际好处是排错范围小——训练崩了先查data_loader,不需要在 500 行代码里找 bug。

3.2 数据加载与标签对齐:窗口化是关键

二级结构预测是逐残基的,模型看到的是“这个残基和它左右邻居的局部片段”。所以数据预处理要做滑动窗口:对每个位置 i,取前后各window // 2个残基的特征,拼成一个窗口,标签就是位置 i 的二级结构类别。窗口太小,模型看不到上下文;窗口太大,边界残基增多且训练变慢。我常用的默认值是 15。

代码里要注意两个边界问题:序列开头和结尾没有足够邻居,需要 padding;pad 出来的位置必须不计入损失,否则模型被无效样本带偏。下面这段是特征窗口化的核心逻辑:

def build_windows(features, labels, window=15): half = window // 2 feat_padded = np.pad(features, ((half, half), (0, 0)), mode="edge") # 标签 padding 用 -1,后续 loss 里 ignore lab_padded = np.pad(labels, (half, half), constant_values=-1) windows, targets, masks = [], [], [] n = len(labels) for i in range(n): windows.append(feat_padded[i: i + window]) # (window, feat_dim) targets.append(lab_padded[i]) masks.append(1 if lab_padded[i] != -1 else 0) return np.array(windows), np.array(targets), np.array(masks)

np.pad用mode="edge"做特征 padding,意思是最边上的特征直接复制到 pad 位置。这是比补零更合理的做法,因为补零会给模型输入一个“不存在的化学环境”,而边界复制至少保留了序列末端的特征。标签 padding 用-1,喂给CrossEntropyLoss(ignore_index=-1)后会自动忽略。

训练集和验证集切分这里要特别小心:必须按“蛋白质链”来切,不能在残基级别随机打乱。否则同一条链里结构相似的片段会同时出现在训练集和验证集,验证集虚高。正确做法是先给每一条蛋白链编号,按链划分,再对每条链做窗口化。

3.3 模型:1D 残差编码器 + 投影头 + 分类头

模型设计上,我不建议期末作业一上来就上 Transformer。1D CNN 在序列局部特征提取上稳稳够用,训练快、显存小、好向老师解释。结构分三块:编码器提取每个残基的隐藏特征,投影头把隐藏特征映射到度量空间,分类头输出结构类别。编码器用两层残差卷积:

import torch import torch.nn as nn import torch.nn.functional as F class ResidualBlock1D(nn.Module): def __init__(self, channels): super().__init__() self.conv1 = nn.Conv1d(channels, channels, 3, padding=1) self.bn1 = nn.BatchNorm1d(channels) self.conv2 = nn.Conv1d(channels, channels, 3, padding=1) self.bn2 = nn.BatchNorm1d(channels) def forward(self, x): identity = x out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out = out + identity return F.relu(out) class ProteinNet(nn.Module): def __init__(self, in_dim=41, hidden_dim=128, embed_dim=64, n_classes=3): super().__init__() self.encoder = nn.Sequential( nn.Conv1d(in_dim, hidden_dim, 7, padding=3), nn.BatchNorm1d(hidden_dim), nn.ReLU(), ResidualBlock1D(hidden_dim), ResidualBlock1D(hidden_dim), ) self.projection = nn.Sequential( nn.Linear(hidden_dim, hidden_dim // 2), nn.ReLU(), nn.Linear(hidden_dim // 2, embed_dim), ) self.classifier = nn.Linear(hidden_dim, n_classes) def encode(self, x): # x: (B, L, F) -> (B, F, L) -> (B, L, hidden) x = x.transpose(1, 2) x = self.encoder(x) return x.transpose(1, 2) def forward(self, x): enc = self.encode(x) proj = self.projection(enc) proj = F.normalize(proj, p=2, dim=-1) # L2 归一化,稳定度量距离 logits = self.classifier(enc) return enc, proj, logits

这里in_dim=41对应 one-hot 21 维加 PSSM 20 维;如果你只用 one-hot,改成 21 即可。hidden_dim=128是隐藏特征宽度,embed_dim=64是度量空间的维度,不建议再大,否则 triplet loss 在 64 维以上的距离区分力会变差。投影头输出做了 L2 归一化,这样距离计算相当于余弦距离,避免 embedding 尺度漂移导致 loss 不收敛。

残差块里的 7x1 卷积核负责看更宽一点的范围,两层残差保证梯度传播稳定。“为什么用 Conv1d 不用 LSTM”是答辩高频问题——答案就一条:局部结构模式更依赖相邻残基的短程相互作用,卷积天然适合这种平移等变特征,而 LSTM 在这类中短序列任务里更慢且更容易过拟合。

3.4 训练循环:交叉熵和 triplet loss 联合优化

有了模型,接下来是把两个目标揉进同一个训练循环。交叉熵负责让分类头准确,triplet loss 负责让投影空间有结构。训练时只对有效位置计算损失。先写 batch-hard triplet:

def batch_hard_triplet_loss(emb, labels, margin=0.3): # emb: (N, D) 已做 L2 归一化; labels: (N,) N = emb.size(0) dist = torch.cdist(emb, emb, p=2) # (N, N) same = labels.unsqueeze(1) == labels.unsqueeze(0) diff = ~same same.fill_diagonal_(False) # 不把自己当正样本 hardest_pos = dist.masked_fill(~same, -1.0).max(dim=1)[0] hardest_neg = dist.masked_fill(diff, 999.0).min(dim=1)[0] loss = F.relu(hardest_pos - hardest_neg + margin).mean() return loss

这段代码做的事是:对每个样本,在同类里找距离最远的那个作为正样本,在异类里找距离最近的那个作为负样本,然后要求“正负距离差”大于 margin。这样永远不会因为随机采样到简单样本而让 loss 失去梯度。复杂度是 N 的平方,所以 batch 别设太大,64 左右刚好。

主训练循环里,把两个损失加权相加:

def train_one_epoch(model, loader, optimizer, ce_loss, device, alpha=0.2): model.train() total_loss = 0.0 for x, y, mask in loader: x, y, mask = x.to(device), y.to(device), mask.to(device) _, proj, logits = model(x) valid = mask > 0 loss_ce = ce_loss(logits[valid], y[valid]) loss_trip = batch_hard_triplet_loss(proj[valid], y[valid], margin=0.3) loss = loss_ce + alpha * loss_trip optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) return total_loss / len(loader.dataset)

alpha是度量损失的权重,我一般从 0.2 起步。太小等于没加,太大投影空间会干扰分类头的梯度。y[valid]这样做是因为标签 pad 位置是 -1,布尔 mask 过滤后只剩有效残基。训练批次里的类别均衡问题也很重要:如果 C 类占一半,随机 batch 里正负样本比例会很离谱。常见做法是用WeightedRandomSampler按类别频率反加权采样:

sample_weights = 1.0 / class_count[train_labels] sampler = WeightedRandomSampler(sample_weights, len(train_labels), replacement=True) train_loader = DataLoader(dataset, batch_size=64, sampler=sampler)

优化器用 Adam,初始学习率1e-3,每 5 个 epoch 验证一次,验证集 Q3 连续 5 次不升就早停。这一套下来,训练过程基本不需要人为盯着。

3.5 评估:用 per-residue 准确率和混淆矩阵说话

二级结构预测的评价指标不是“整条链预测对”,而是“每个残基预测对”。Q3 准确率就是正确预测的残基数除以总残基数。另外必须看每类的召回率,因为 C 类占多数,只看 Q3 会被 C 类撑高,误导你自己。

def evaluate(model, loader, device, n_classes=3): model.eval() all_preds, all_trues = [], [] with torch.no_grad(): for x, y, mask in loader: x, y, mask = x.to(device), y.to(device), mask.to(device) _, _, logits = model(x) valid = mask > 0 pred = logits[valid].argmax(dim=-1) all_preds.extend(pred.cpu().numpy()) all_trues.extend(y[valid].cpu().numpy()) acc = (np.array(all_preds) == np.array(all_trues)).mean() cm = confusion_matrix(all_trues, all_preds, labels=[0, 1, 2]) f1 = f1_score(all_trues, all_preds, average="macro", labels=[0, 1, 2]) return acc, f1, cm

上面用到了sklearn的confusion_matrix和f1_score,这也是前面说需要python 安装 sklearn 库的原因。报告里最好同时给 Q3 和 macro-F1,前者是业界习惯,后者能暴露类别不平衡问题。只看 Q3 的话,H/E 类几乎不识别也能拿到 50% 以上的分数,这个坑很多人踩过。

4. 避坑记录:蛋白质数据集的坑比模型多,这 5 个最致命

模型本身的 bug 通常好定位,真正让人通宵的是数据问题。我按踩坑频率排了五条,每一条都先讲现象、再给原因、最后给可执行的解法。

4.1 标签和序列错位:最常见的翻车现场

现象:训练 loss 掉得很稳,但验证集 Q3 只有 30% 左右,混淆矩阵对角线上几乎没值,预测结果看起来像是随机猜。为什么?最常见原因是 DSSP 文件按 PDB 的残基编号输出,而你的序列文件在清洗时删掉了某些残基,两个数组长度不一致,直接按下标 zip 就整体错位了。另一个常见来源是窗口 padding 时,特征用了 edge padding、标签却没同步 pad,导致标签整体往后偏了一位。

原因:残基编号没有作为主键对齐,而是依赖数组下标。解决:训练前务必做一次 sanity check,随机抽 20 个位置打印“序列字母-PSSM 索引-标签”,肉眼确认第 i 个特征对应的确实是第 i 个标签。更稳的对齐方式是统一用“链 ID + 残基编号”作为字典键,不用数组下标。这个小检查花不了两分钟,能省一个通宵。

4.2 类别不平衡:C 类占 50% 以上,无脑预测 C 也有五成准确率

现象:训练结束 Q3 有 55%,你觉得还行,但一看每类报告,H 和 E 的召回率接近 0,模型对所有残基几乎都输出 C。原因:C 类在绝大多数蛋白里占比最高,交叉熵被多数类主导,模型只需要把一切都预测成 C,就能拿到不算难看的 loss。解决:给交叉熵加类别权重,或者用WeightedRandomSampler在采样阶段做均衡。更公平的评估指标是 macro-F1,先按 H/E/C 分别算 F1 再取平均,这时候“全预测成 C”的模型会露出原形。

一个简单的类别权重计算方式:

class_counts = np.bincount(train_labels, minlength=3) weights = class_counts.sum() / (3.0 * class_counts + 1e-6) ce_loss = nn.CrossEntropyLoss(weight=torch.tensor(weights, device=device))

4.3 triplet 不收敛,margin 和 batch 构造都是玄学

现象:triplet loss 在训练前几个 epoch 几乎不降,或者交叉熵已经收敛到不错水平,但投影空间的 embedding 看起来仍然是混成一团;换一个随机种子,结果波动很大。原因:随机 batch 里大多数负样本都很容易区分,loss 计算出来是 0,梯度消失;margin 设得太大,所有样本都满足“正负距离差大于 margin”,同样没有梯度。解决:必须用 batch-hard mining 代替随机 triplet,这已经在 3.4 节写好了;margin 从 0.3 开始试,不要一上来就设 1.0;embedding 输出做 L2 归一化,防止距离尺度漂移。还有一个我常用的技巧:前两轮只用交叉熵训编码器,让特征先有个合理结构,再打开 triplet loss 联合优化,这样收敛速度明显提升。

4.4 PSSM 特征跑不动或生成太慢:本地小机器的后悔药

现象:PSI-BLAST 在一个小蛋白上跑了几分钟还没结束,几百条训练蛋白这样跑下去,作业截止日期到了 PSSM 还没生成完。原因:本地没有完整的 nr 数据库,或者把 PSI-BLAST 迭代次数设得太大,同源搜索耗时成倍增长。解决:如果只有 CPU,不要让模型反复现场生成 PSSM,批量离线生成后用 pickle 或 numpy 文件缓存下来;PSI-BLAST 迭代次数调到 1 到 2 次,足够提取保守性信息。实在不行,先用 one-hot 特征把整个流程跑通,后面再补 PSSM。这个取舍我踩过,提前一天批量生成 PSSM 存成.npy文件,能省掉最痛苦的一环。

4.5 同源泄漏:验证集 90%,独立测试 60%

现象:验证集上 Q3 高得离谱,模型自己都以为要封神了,换到一份新蛋白数据上立刻崩回及格线。原因:数据切分时按残基随机打乱了,来自同一条链的相邻片段同时出现在训练和验证集。这些片段高度相似,模型等于提前“见过”答案,这是生物信息学作业里最典型的假成功。解决:按蛋白质链整体切分,并且最好用 CD-HIT 做序列去冗余,保证训练和测试集合之间没有超过 30% 的序列同一性。给一个最小去冗余命令:

cd-hit -i train.fasta -o train_cdhit.fasta -c 0.3 -n 2

-c 0.3表示序列同一性超过 30% 的序列会被聚成一簇,-n 2是 k-mer 大小,配合 0.3 阈值使用。去冗余以后再切分,验证集才可信。

5. 参数与可信预期:一份让答辩老师挑不出毛病的实验表

参数怎么设、指标做到多少算好,这两件事决定了你的报告是真材实料还是空中楼阁。下面给出一张可以直接抄的参数表,以及一个不会被质疑的预期范围。

5.1 必调参数:一张表看清影响

参数推荐值常见范围不收敛或效果差时怎么改
window1511–21准确率低且训练慢,先降 window
in_dim4121–41只用 one-hot 就改成 21
hidden_dim12864–256过拟合就降到 64,欠拟合升 256
embed_dim6432–128triplet 震荡就降到 32
margin0.30.2–0.5一直不降,从 0.5 往下扫
alpha0.20.1–0.5投影空间混乱,把 alpha 调小
learning_rate1e-33e-4–1e-3loss 发散就降一半
batch_size6432–128batch-hard 计算是平方复杂度,别超 128
epochs30–5020–80结合早停,别硬跑满

这里面最关键的两个旋钮是window和alpha。window直接决定模型看多远的上下文,小窗口适合局部明显的转角结构,大窗口更吃全局趋势但边界残基多,训练噪声大。alpha决定度量损失对总梯度的贡献,调太大分类头会“学不动”。我一般先固定window=15, alpha=0.2跑通,再按结果微调这两个值。

5.2 合理的目标:Q3 70%、Q8 55% 已经是很能交差的分数

期末大作业要的是“稳定、可信、能复现”,不是冲击 SOTA。只用 PSSM + 1D 残差网络 + 度量学习,在公开基准上 Q3 落在 65% 到 72%、Q8 落在 50% 到 58% 是这类配置的常见区间。那些动辄 Q3 80% 以上的工作,背后要么用了大规模预训练模型,要么引入了同源序列的深度比对,作业环境里通常不具备这个条件。

能做出这个区间,报告里已经可以写三组对比:只用交叉熵、只用 triplet、交叉熵 + triplet 联合,看 Q3 和 macro-F1 的差异。通常联合训练比单独交叉熵高 1 到 3 个点,这个涨幅不大,但恰好说明度量学习确实帮模型学到了更结构化的特征。千万别在报告里编一个 Q3 95%,答辩老师只要现场跑一次测试集就会露馅。

5.3 三张图:把损失函数和学习效果说清楚

答辩 PPT 里放三张图基本就够:第一张是训练过程曲线,横轴 epoch,纵轴同时画 CE loss、triplet loss 和验证 Q3,证明你是看着指标调参的。第二张是混淆矩阵热力图,直接说明 H/E/C 三类各自的识别情况。第三张是 embedding 的 t-SNE 投影,这一步是最能体现“深度度量学习”价值的可视化。

t-SNE 代码很短,把验证集经过投影头输出的向量收集起来画散点即可:

from sklearn.manifold import TSNE import matplotlib.pyplot as plt tsne = TSNE(n_components=2, perplexity=30, random_state=42) z = tsne.fit_transform(proj_vectors) # proj_vectors: (N, embed_dim) plt.figure(figsize=(8, 6)) plt.scatter(z[:, 0], z[:, 1], c=trues, cmap="Set1", s=8, alpha=0.7) plt.colorbar() plt.savefig("tsne_q3.png", dpi=150)

画出来的图如果三类点有明显聚类,答辩时一句话就能讲完:“度量学习让同类结构的表征聚在一起,分类头只需要在这个结构化的空间里做线性划分。”这比解释任何数学公式都更有说服力。

6. 再用一个原型距离把“度量学习”讲透:可解释地测一个残基

最后分享一个进阶技巧,它能让你的作业从“跑出来了”升级成“模型有解释性”。训练结束后,取投影头输出的 embedding,对每个结构类别算一个中心向量,也就是“类别原型”。推理时不看分类头的 logits,而是计算每个残基 embedding 到三个原型的距离,最近的那个类别就是预测结果。这个做法正是度量学习的本质:分类不是直接读 logits,而是比较距离。

def compute_prototypes(proj_vectors, labels, n_classes=3): prototypes = [] for cls in range(n_classes): vecs = proj_vectors[labels == cls] prototypes.append(vecs.mean(axis=0)) return np.stack(prototypes) def predict_by_prototype(embedding, prototypes): dist = np.linalg.norm(embedding - prototypes, axis=-1) return dist.argmin(), dist

这样做的好处是你能输出“这个残基到 H 原型 0.52、到 E 原型 0.48”,如果两个距离很接近,说明模型认为这是一个边界残基或转折区,它自己也没有把握。落到报告里,可以挑一条测试蛋白,画一张逐残基的距离热力图,展示模型在哪里犹豫、哪里坚定。这比单一准确率生动得多,也更能回扣标题里的“深度度量学习”。

我自己有一个固定习惯:每次交作业前,固定随机种子,重跑三遍,确认 Q3 和 macro-F1 的波动不超过一个百分点;然后打印一条蛋白的逐残基预测对照表,放到附录里。这一套做完,基本不会再被答辩老师问到答不上来。希望这篇梳理能帮你在期末周少走几趟弯路,把时间花在真正值得讲清楚的指标和可视化上。

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

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

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

立即咨询