ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kaggle蛋白质图谱冠军方案:DenseNet121与度量学习实战

Kaggle蛋白质图谱冠军方案:DenseNet121与度量学习实战 简介这份文档面向参加或研究Kaggle人类蛋白质图谱图像分类竞赛的算法工程师与深度学习学习者系统整理了该赛事第一名解决方案的完整技术思路。内容围绕极度不平衡的多标签分类难题展开涵盖CNN分类器与度量学习模型的组合策略、基于DenseNet121的模型结构设计、数据预处理与去重方法、Adam优化器与分段学习率调度、FocalLossLovasz损失函数选择以及利用最近邻检索修正测试集标签的后处理技巧并附有对模型可解释性与标签噪声处理的思考。资源包为1个docx文档大小约117KB轻量便携适合作为竞赛复盘与方案查阅的参考材料。目前已有117人学习下载适合希望深入理解不平衡多标签分类实战细节、借鉴冠军级建模与调参经验的读者研读。1. 从 0.03 分说起这套 Kaggle 蛋白质图谱冠军方案到底值不值得拆如果你做过 Human Protein Atlas 这个赛题大概率经历过同一件事模型在训练集上 F1 看着还行一上 LB 就掉得莫名其妙稀有类几乎全灭。这份《Kaggle 人类蛋白质图谱图像分类第一名解决方案》记录的就是当年拿下第一名的完整技术路线核心不是某个花哨的新网络而是 CNN 多标签分类器加度量学习两条腿走路。它解决的是极度不平衡多标签场景下稀有类召回上不去、阈值调不准、外部数据分布不一致这三个死结。适合已经能跑通基础 CNN 分类、想搞明白 Focal Loss 加 Lovasz 怎么配、以及度量学习怎么用来做标签清洗和最近邻替换的人。我第一遍读的时候觉得平平无奇第二遍照着复现才发现真正拉开差距的是后处理里那 1000 到 1300 个样本的替换操作单这一项就贡献了 0.03 以上的提升在这个赛题里已经是巨大进步。2. 方案骨架DenseNet121 分类头与 FocalLovasz 的选型逻辑2.1 为什么是 DenseNet121 而不是更深的网络方案里作者明确说他试过各种多标签分类论文里的漂亮结构结果都没有改进最后最好的模型反而是非常简单的 densenet121。这句话信息量很大。蛋白质图谱图像是 4 通道输入RGB 加一个额外的通道图像质量高但类别极度不平衡28 个标签里有些类只有个位数样本。在这种数据规模下参数量过大的网络更容易过拟合到头部类稀有类反而学不动。DenseNet121 的特征复用机制在中小数据集上表现稳定配合 ImageNet 预训练权重收敛快、显存占用可控是性价比最高的选择。模型头部结构直接沿用了 iafoss 公开 kernel 的思路具体拆开看# 分类头结构来自方案原文 nn.Sequential( AdaptiveConcatPool2d(), # 自适应平均池化 自适应最大池化拼接 Flatten(), nn.BatchNorm1d(2048), # DenseNet121 最后特征层 1024 通道拼接后 2048 nn.Dropout(p0.5), nn.Linear(2048, 1024), nn.ReLU(), nn.BatchNorm1d(1024), nn.Dropout(p0.5), nn.Linear(1024, 28) # 28 个蛋白质标签 )AdaptiveConcatPool2d 把平均池化和最大池化的结果拼在一起等于同时保留全局平滑特征和显著响应特征对多标签任务比单一池化更稳。两个 Dropout 都是 0.5配合 BatchNorm 压制过拟合。这里有个容易翻车的点BatchNorm1d 的维度必须和前一层的输出通道严格对齐DenseNet121 最后是 1024 通道拼接后是 2048写错就直接维度报错。2.2 Focal Loss 加 Lovasz 的组合逻辑损失函数这块是方案的核心决策之一。作者没有用宏 F1 软损失理由说得很直白批量很小而且有些类样本极少宏 F1 在这种条件下不适合。他选的是 FocalLoss 加 Lovasz 的组合。Focal Loss 解决的是类别不平衡下的难易样本加权问题公式里那个调制因子让模型把注意力放在难分类的稀有类上。Lovasz 损失则是把 IoU 这类不可导的指标做了凸松弛直接优化交并比。作者的原话是虽然 IOU 和 F1 不一样但 Lovasz 能在某种程度上平衡 Recall 和 Precision。这个判断很关键——在稀有类上单纯优化 Focal 容易让模型过度偏向召回Lovasz 把精度拉回来一些。# 训练循环里的损失组合常见做法是加权求和 criterion lambda pred, target: focal_loss(pred, target) lovasz_loss(pred, target) # 优化器与学习率调度严格按方案参数 optimizer torch.optim.Adam(model.parameters(), lr30e-5) # epoch 25: lr 15e-5 # epoch 30: lr 7.5e-5 # epoch 35: lr 3e-5 # epoch 40: lr 1e-5学习率从 30e-5 开始每 5 个 epoch 左右砍半到 40 epoch 之后降到 1e-5。这个调度节奏偏慢配合 Adam 在 50 epoch 内能稳定收敛。注意方案里没有使用过采样稀有类完全靠损失函数和阈值后处理来兜底这一点和很多人第一反应「稀有类就过采样」的做法相反值得琢磨。2.3 数据增强与预处理的具体参数训练时的增强策略是旋转 90 度、翻转然后从 768x768 图像中随机裁剪 512x512 补丁或者从 1536x1536 图像中裁剪 1024x1024 补丁。预测阶段则是用 4 个最佳焦点损失 epoch 的模型对测试集做种子随机裁剪取最大值。预处理里有一个容易被忽略的细节用 train 加 test 一起计算均值和标准差而不是只用 train。作者的解释是要让输入分布更贴近测试集。这个做法在竞赛里常见但工程落地时要小心如果 test 分布和 train 差异大这样算出来的统计量反而会引入偏差。外部数据 v18 的处理用了哈希方法去重删掉了大约 6000 个重复样本。这一步不做的话外部数据和训练集、测试集之间的泄漏会让验证分数虚高LB 上直接打回原形。3. 度量学习分支把抗体 ID 当人脸 ID 来用3.1 ArcFace 迁移到蛋白质图像的思路方案更新部分讲得很清楚作者注意到具有相同抗体 ID 的样本几乎有相同的标记于是把抗体 ID 当成人脸识别里的身份 ID直接在 HPA v18 数据集上套用面部识别算法。这个类比是整个方案里最漂亮的一步。网络用 resnet50增强还是旋转 90 度和翻转损失函数换成 ArcFaceLoss优化器 Adam学习率 10e-5训练 50 个 epoch。验证指标用 top1 准确率而不是 F1。作者报告 top1 精度大于 0.9这意味着模型能在验证集上找到足够可靠的最近邻。ArcFace 的核心是在角度空间里加一个 margin让同类样本更紧凑、异类更分散。代码里 s30.0m0.5这是人脸识别里的经典配置。迁移到蛋白质图像上s 和 m 需要根据类别数调整类别越多m 可以适当减小否则训练初期太难收敛。class ArcFaceLoss(nn.modules.Module): def __init__(self, s30.0, m0.5): super(ArcFaceLoss, self).__init__() self.classify_loss nn.CrossEntropyLoss() self.s s self.easy_margin False self.cos_m math.cos(m) self.sin_m math.sin(m) self.th math.cos(math.pi - m) self.mm math.sin(math.pi - m) * m def forward(self, logits, labels, epoch0): cosine logits sine torch.sqrt(1.0 - torch.pow(cosine, 2)) phi cosine * self.cos_m - sine * self.sin_m if self.easy_margin: phi torch.where(cosine 0, phi, cosine) else: phi torch.where(cosine self.th, phi, cosine - self.mm) one_hot torch.zeros(cosine.size(), devicecuda) one_hot.scatter_(1, labels.view(-1, 1).long(), 1) output (one_hot * phi) ((1.0 - one_hot) * cosine) output * self.s loss1 self.classify_loss(output, labels) loss2 self.classify_loss(cosine, labels) gamma 1 loss (loss1 gamma * loss2) / (1 gamma) return loss这段代码里 easy_margin 设为 False走的是标准 ArcFace 的 cos(thetam) 路径。th 和 mm 是用来处理 cos 超出范围的边界情况防止数值不稳定。loss 是两项交叉熵的加权平均gamma1 时各占一半这个设计比单用一项更平滑。3.2 最近邻替换后处理0.03 分从哪来度量学习训练完之后作者用它来找验证集上的最近邻样本top1 精度超过 0.9。然后他做了一件大胆的事用度量学习的结果去设置测试集的标签。具体操作是对测试集样本找最近邻如果找到的样本在训练集或 v18 里存在就用它的标签替换预测标签。但这里有个坑作者也踩到了测试集和 v18 略有不同有些样本在训练集和 v18 里找不到最近邻居。所以他设了一个阈值只替换那些最近邻距离低于阈值的样本。幸运的是阈值不敏感替换 1000 个和替换 1300 个样本的评分几乎一样。最终这一步带来 0.03 以上的提升。# 最近邻替换的伪代码逻辑 for test_sample in test_loader: feature metric_model.extract_feature(test_sample) dist, idx find_nearest_neighbor(feature, train_features) if dist threshold: # 用最近邻的标签替换模型预测 final_label train_labels[idx] else: final_label cnn_prediction[test_sample]阈值的选择建议在验证集上扫一遍看替换数量和分数曲线找拐点。作者说阈值不敏感但那是他的数据分布下换一个赛题或数据集阈值可能就敏感了这一步必须自己验证。3.3 两个提交版本的策略差异方案最后阶段生成了两个提交。第一个是保持标签与公共测试集的比例稀有类的比例直接设成和训练组一样。第二个是保持标签与训练组和公共测试组平均比例一致。作者的原话是虽然试着增减稀有类样本 2 到 5 个能改进公共 LB但这是危险的方式他只用来评估可能的重组。这个决策背后的逻辑是公共 LB 只是测试集的一个子集按它调阈值和比例很容易过拟合。作者选择用公共 LB 作为另一个验证集而不是调参目标。这个习惯在竞赛里很关键很多人就是死在 LB 过拟合上。4. 避坑与排查复现这套方案时最容易翻车的五个点4.1 验证集划分不一致导致分数对不上现象是本地验证 F1 很高LB 却低一大截。原因是验证集划分方式不同方案里明确说按 trentb 的划分来拆 val 集如果自己随便拆稀有类在验证集里的比例和测试集差异大F1 对阈值又敏感分数自然对不上。解决方法是严格按公开讨论里的划分文件来或者至少保证每个类的比例和训练集一致。4.2 外部数据去重不彻底引发泄漏现象是加了 v18 外部数据后验证分数涨了LB 反而降。原因是外部数据和训练集、测试集有重复样本哈希去重没做干净。方案里删了大约 6000 个重复样本这个量级说明泄漏很严重。解决方法是先对 train、test、v18 做全量哈希比对确认没有交集再合并别嫌麻烦。4.3 学习率调度写错导致不收敛现象是训练 loss 震荡或者一直不降。原因是学习率调度没按方案里的阶梯来或者 epoch 边界判断写错。方案是 epoch25 降一次30 再降35、40 各降一次。常见错误是用 step scheduler 但 step_size 设错或者用 cosine 退火但没 warmup。解决方法是直接手写 if-elif 判断当前 epoch简单可靠。4.4 ArcFace 的 s 和 m 直接照搬人脸识别配置现象是度量学习训练 loss 不降top1 精度上不去。原因是 s30、m0.5 是人脸识别里几十万类别的配置蛋白质图谱的类别数少得多直接照搬会导致 margin 过大训练初期梯度爆炸或饱和。解决方法是先把 m 降到 0.3 左右s 保持 30 或降到 20等 loss 稳定后再逐步加回 0.5。4.5 最近邻替换阈值没在验证集上校准现象是替换后 LB 分数反而降了。原因是阈值设得太松把错误标签也替换进去了。方案里作者说阈值不敏感但那是他反复验证过的。解决方法是画一条替换数量与验证分数的曲线找分数开始下降的拐点取拐点前 10% 到 20% 的位置作为阈值。5. 从单模型到可复现我复现这套方案时固化的三个习惯复现这套方案最大的感受是作者反复强调的「可重复性、稳定性、高效性和解释性」不是套话是每一步都在做取舍。我后来把几个习惯固化了下来这里分享给你。第一个习惯是验证指标用焦点损失而不是 F1。方案里说得很清楚F1 对阈值敏感阈值又取决于训练集和验证集的分布所以 F1 不是好的模型能力度量。我现在的做法是训练过程中盯焦点损失它降得稳模型能力就在涨。F1 只在最后调阈值时用一次。第二个习惯是外部数据合并前强制走一遍哈希去重。方案里删了 6000 个重复样本这个数字我记了很久。后来我做任何带外部数据的任务都会先写一个哈希比对脚本把 train、test、external 三方的重复样本全部标出来确认干净了再进训练流程。这一步多花十分钟能省掉后面几天的排查。第三个习惯是后处理替换操作必须在验证集上先跑通。度量学习的最近邻替换听起来很美但阈值、距离度量、特征归一化方式都会影响结果。我一般会在验证集上模拟一遍替换流程看替换后的分数变化确认正向再上测试集。方案里作者替换 1000 到 1300 个样本这个量级和阈值的关系只有自己跑过才知道。最后一个技巧是关于模型集成的。方案作者明确说为了保持简单不讨论集成单个模型甚至单个 fold 加度量学习结果就足够拿第一。这句话对我触动很大。很多人一上来就想着堆模型、堆 fold结果每个模型都没调透。先把单模型加后处理做到极致再考虑集成顺序不能反。从那以后我每次做多标签分类任务都会先把焦点损失曲线和最近邻替换的验证流程走一遍确认这两个环节没问题再动网络结构和集成。希望帮到你。本文还有配套的精品资源点击获取
返回列表