返回列表

1st place solution

558. Vesuvius Challenge - Ink Detection | vesuvius-challenge-ink-detection

开始: 2023-03-15 结束: 2023-06-14 医学影像分析 数据算法赛
第一名解决方案 - Vesuvius Challenge 墨水检测竞赛

第一名解决方案

作者:ryches(Kaggle Grandmaster)

发布日期:2023年6月16日

竞赛:Vesuvius Challenge 墨水检测

这场比赛的结局非常紧张,我们虽然长时间保持第一,但我始终担心会有变数。我们试图采取非常保守的策略,确保不会过度针对排行榜进行优化。尽管如此,当公共排行榜样本如此之少,而本地验证又不够稳定时,总会存在某种程度的不确定性。

简要总结

从整体来看,我们的成功主要归功于以下几点:

  • 更大的裁剪区域
  • 强大的深度不变模型
  • 融合多个模型以获得更好的校准效果
  • 在严格验证片段1后,使用全部可用数据进行训练

数据准备

我们最初从2.5D starter代码开始,并逐步演进。最终我们确定只取中间16层,但我们尝试了许多其他方法但未见成效(这部分我留到另一篇文章再讲)。初始代码配置为针对图像的所有裁剪区域进行训练,但我们基于裁剪区域是否空白进行了简单过滤。这大大减少了训练时间,因为许多裁剪区域完全没有数据。

早期进展甚微,随后我们进行了简单的消融实验来观察裁剪大小对性能的影响,发现其中存在良好的扩展潜力。这似乎是我们早期观察到的最明显的规律之一:128被512超越,512又被1024超越。

x、y方向的上下文信息似乎非常重要,因为裁剪区域可以完整查看一个字母甚至多个字母,从而更完整地提取它们。较小的裁剪区域产生的输出更加零散。这种方法的一个好处是它实际上完全没有增加训练时间。如果使用1024×1024的裁剪区域,相比128×128,需要处理的裁剪区域数量大大减少,因此每个epoch的时间和整体收敛时间几乎完全相同,这与一些其他竞赛不同(在那里增加尺寸会使计算时间变长)。

模型设计

我们最初采用2.5D方法,但很快发现这并非最优方案,因为模型在学习哪些层含有墨水,而我们知道这在不同的碎片之间是变化的。我们希望找到一个深度不变的解决方案,能够在任意层检测墨水。一种方法是在3D体积的每个切片上训练一个简单的2D模型,但由于数据标注的方式(最初只有2D标注),如果我们告诉模型某些层有墨水而实际上没有,或反之,就会给模型提供错误的信号。我们就如何处理这个问题进行了多次讨论:1D卷积、最大池化、使用尺寸为(8, 1, 1)的3D卷积使其仅真正关注深度模式。我们发现性能最好的方法是使用强大的3D模型,输出一个新的具有多个通道的3D体积,然后沿深度轴进行最大值池化。

例如,我们在这方面最早的方法是一个简单的3DCNN。输入形状为(batch_size, 1, 16, 1024, 1024)。我们在其上应用4层3D卷积,滤波器数量逐渐增加,最终得到输出体积(batch_size, 16, 16, 1024, 1024)。此时我们只需在z维度上取最大值,将其压缩为(batch_size, 16, 1024, 1024),这样我们的16个深度维度就变成了16个特征维度。仅此方法本身已经不错,但再通过一个强大的2D分割模型后效果显著提升。我们在此过程中大量依赖segformer,正如其他人提到的,b3 backbone表现优异,但我们也发现更大的模型在排行榜上表现更好,即使它们在本地验证上并未显著超越。

我们迭代了这个设计,因为它似乎满足了我们想要的特质:一个应用于3D体积的强有力2D分割器,且对深度不变。我们尝试了不同的第一阶段3D方法,发现更强大的3D模型产生了更好的结果,逐步演进到3D UNet,最终到3D UNETR。有时结果并不明确是否更好,但我们发现一个明显趋势:理论更好的模型在排行榜上也表现更好。我们最好的单个模型是UNETR第一阶段,将32个通道传入b5 segformer,在公共排行榜上得分0.82,私有排行榜上0.67。我们在3D和2D阶段之间的通道上应用了少量dropout。

我一直想知道segformer的优势是否并非因为它本身是最佳模型,而是因为它以较低分辨率进行预测。我们输入1024×1024,它输出256×256的分割图,我们仅用简单的conv2d转置进行上采样。我相信第二名的解决方案验证了这一点:较低分辨率确实有帮助,因为它进行更粗略的分类,相比试图更精确地获取每个像素,任务变得更简单。

我们最终的最佳解决方案融合了9个不同的模型:

第一阶段 第二阶段 分辨率 公共分数
3d unet(16通道) segformer b3 1024 .78
3d unet(16通道) segformer b3 512 .77
3d unet(16通道带SWA) segformer b3 512 .78
3d cnn(32通道) segformer b3 1024 .78
3d cnn(32通道) segformer b5 1024 .77
3d cnn(64通道) segformer b3 1024 .78
3d unet(32通道) segformer b5 1024 .79
3d unetr(32通道) segformer b5 512 .82
3d unetr 多分类(32通道) segformer b5 512 ?

我们后期尝试的一个方向是多分类输出。不再使用二分类,而是预测"无-掩码-墨水"三类。结果实际上产生了更干净的输出,在大多数模型中,只要有纸莎草的地方就有一些噪声,而这种方法似乎大大降低了噪声。不过我们未能充分探索以确认其是否真的有效。

训练流程

我们的训练流程相对标准,使用现有的adamW优化器、dice+bce损失和hyperparameters,修复了一些设置min-lr的小bug,并继续使用渐进式warmup学习率调度。我们添加了随机权重平均(SWA)来获得更宽的极小值,而不是需要选择特定检查点,因为我们发现这些检查点相当不一致。我们发现对片段1进行验证并不完美,但至少具有方向性指导意义。一旦在fold 1的本地验证中找到有效方法,我们就会提交到排行榜进行双重验证,然后使用所有fold继续训练若干个epoch。我们会提交并评估新检查点(在全部fold上训练),通常能提升约0.04分。有时我们从一开始就在所有片段上训练,而不是先训练片段2、3,后期再加入1,但我们没有对此进行充分评估。

数据增强

我们尝试了许多不同的增强方式,主要来自albumentations,一些自定义增强,以及一些3D包的增强,但最终在花哨的增强方法中未能找到显著的alpha。我们最佳模型使用了以下增强:

  • 50%水平或垂直翻转
  • 75%90度旋转
  • 50%亮度对比度调整
  • 25%1-2通道dropout(此例中通道实际指深度)
  • 10%位移缩放旋转
  • 10%噪声和模糊
  • 10% coarse dropout
  • 10%网格畸变

整体来看,旋转和翻转似乎至关重要,其他因素基本无影响。旋转似乎很重要,但我们并未确认测试集本身是否经过旋转,我们只是知道它重要,因此试图使模型尽可能保持旋转不变性。

模型融合

我们最终得到了大量模型检查点,需要筛选出我们认为性能最佳的组合,在运行时间和吞吐量之间进行权衡。我们尝试了许多不同组合:更重的TTA(包含所有旋转和翻转)、更多模型、更小的步幅。最终获胜方案是4倍旋转TTA,配合1/4裁剪步幅窗口,以及尽可能多的优质模型。将步幅减半额外带来了0.01的提升,但增加更多模型的效果更为显著,而翻转似乎完全没有帮助。如果仅使用正确的方向而非TTA,我们可能做得更好,但对旋转不变的模型似乎同样强大。

我们的一大难点是如何最佳地融合预测结果。由于我们的步幅方法,每个模型的每个像素被预测了4次,又因为TTA再乘以4。使用多个模型实际上为每个像素的预测聚合提供了大量选择。我们本地尝试了很多方法,但最终效果最好的是:对每个模型的所有约16次预测进行逐像素平均,然后对该平均信号应用sigmoid,再将这些概率值平均。这在Kaggle系统上给我们带来了严峻的内存限制,因此我们必须更高效地存储数据并密集累积,而不是简单地创建大型数组并在最后平均。

阈值选择

我们长期纠结的一个问题是预测的校准。正如其他帖子中讨论的,决定阈值对获得良好结果至关重要,选择错误的阈值可能会给模型性能带来非常误导的信号。在训练和评估过程中,我们持续监控不同阈值下的AUC、精确率、召回率和f0.5分数。一些模型在0.5阈值下校准良好,但许多并非如此。

因此,我们知道即使一个优秀的模型,如果阈值设置不正确,在排行榜上也可能表现糟糕。我们考虑过使用一些人采用的百分位法,但甚至没有为此提交一次尝试,因为如果墨水分布与预期不符,这种方法似乎风险太大。最终我们所依赖的是:预测结果在平均后会变得校准。我们在本地验证和排行榜上都发现了这一点。单个模型的最优阈值范围可能很宽,但在平均多个模型的预测后,几乎都集中在0.5附近。我们最后一次提交其实想大胆尝试0.55,但在公共排行榜上表现更差(仅提前30分钟完成,险些未能选中)。不过它在私有排行榜上的表现确实略高一些。我认为我们在模型融合和最优阈值选择方面其实还有很大提升空间。

后处理

我们从早期的云分割竞赛中借鉴了一个小"魔法函数"来清理预测结果。在所有处理完成后并二值化后,我们使用cv2.connectedcomponents查找连通区域,如果它们超过特定大小就将其移除。这会清理掉过小的部分,即那些小斑点噪声。我们发现本地最优策略是先将阈值设得较低,然后清理多余部分(25000像素以下),但我们提交时只清理了10k像素以下的部分,并未尝试更激进的清理。

处理前

处理后

同比赛其他方案