返回列表

2nd Place Solution with Code [MIT-Compliant]

398. Global Wheat Detection | global-wheat-detection

开始: 2020-05-04 结束: 2020-08-19 作物智能识别 数据算法赛
第二名方案分享及代码 [MIT协议]

第二名方案分享及代码 [MIT协议]

作者: Liao (Kaggle Grandmaster)

[更新] 代码现已发布在 https://github.com/liaopeiyuan/TransferDet

既然比赛结果已经出炉,我决定比我之前的帖子分享更多细节。

首先,非常感谢 @rwightman 提供的 EfficientDet PyTorch 实现,以及 @shonenkov 出色的入门 Kernel。我也从讨论区的帖子中学到了很多。

这是一个有趣的挑战,数据集噪声很大,训练集和测试集之间存在巨大的域偏差,训练集和测试集内部也存在差异。Kernel 的运行时间限制也使得半监督学习成为可能,这在一定程度上可以缓解上述问题。

我做的第一个选择是验证和模型选择的方式。这是一个艰难的选择,因为:1. 目标检测本质上是一项艰巨的任务;2. 训练数据嘈杂且不稳定。最终,我的决定是在移除了错误标注框(通过人工检查)的20%分层拆分上使用“相对检测损失”,正如这里所提示的。我的理由如下:

我们知道训练集和测试集的分布差异很大,所以对我来说 mAP 意义不大,因为它是一个经过重度处理的结果(中间包含 NMS 等超参数,以及看哪个 mAP 等)。而我们用来训练网络的检测损失可以捕捉整体的分类和回归性能。此外,我不使用这个损失来跨模型比较,而是积累了一系列我认为“在损失下具有可比性”的过程,通过纯实证结果和一点直觉,用它们来选择次要选项,如数据增强。对于整体的模型选择,我对 Public LB 持保留态度,而且因为我不没有足够的时间训练多样化的模型,我不必做模型集成这种巨大的选择。我相信一旦整个数据集发布并清理干净,我可以对此进行更多研究。

数据洞察

大家都可以同意,即使移除了明显错误的标注框,数据仍然非常嘈杂,主要有两种形式:噪声锚点和噪声标注框。

  • 噪声标注框: 这是标注者(包括自动标注的 Yolov3)对麦穗理解不同的直接结果。所以我做了一些实验来确定噪声的影响。我将标注框的 x, y 坐标分别降低了 10% 和 5%(这大概是鲁棒目标检测文章中的常态),并用我之前最好的流程进行训练。有趣的是,对于这两种降级,我们都看到了模型性能的下降,所以在某种程度上,数据比 5% 的偏移更具信息量。我的模型运行成本极高,所以我没运行更多测试或设计实验来揭示更多关于 bbox 噪声的信息,但我对如何进行有了基本的概念。
  • 噪声锚点: 另一方面,这有点棘手,因为问题不仅仅是标注者之间的分歧。值得注意的是,我们的训练数据是大图的 1024 裁剪图,并且(如果我没记错的话)标注只在裁剪图上进行,边缘上的麦穗只有在可见部分超过 1/3 时才被标注。这很关键,因为本次比赛中的 AP 值对最终的 mAP 性能有巨大影响。对边界过于自信的模型(识别所有麦穗,无论是否超过 1/3,有时甚至错误地识别叶子)或过于悲观的模型都会降低最终性能。这也解释了我的拼图技巧,我将在接下来的段落中解释。最后,来源图像差异很大。可以训练一个简单的 CNN 分类器,以 99.99% 的验证准确率对来源进行分类。即便如此,可以在比如 4 个来源上学习一个度量学习模型,并在模型从未见过的其余 3 个来源上提取嵌入时实现 80% 以上的同质性。

模型选择与改进

因此,我开发的所有模型都在试图解决这些问题。我首先尝试了 DetectorRS,但在 LB 上只达到了 0.72。所以我转向了 EfficientDet 流程。我从 D4 到 D7 运行了一些简单的测试,结论是带有 COCO 预训练权重的 D6 是最好的。以下是我的理由:

  • D0-D5: 不够大。
  • D7: D7 是通过更大的输入尺寸(以及更大的锚点)获得精度的。我真的无法在我的 GPU 上放入 1536 尺寸的图像并获得合理的结果。
  • 带有 Noisy Student/AdvProp 预训练分类骨干网络和随机初始化 FPN/box, cls net 的 D6: 目标检测相当难。我无法让这些模型收敛。

我做的下一件事是尝试提高 EfficientDet-D6 模型的基线性能。我通过逐行剖析 timm-efficientdet 代码库并对其进行修改,尝试了 10 多种不同的技术,但结果证明简单和优雅胜出。D6 模型的默认设置实现了最佳性能(锚点未更改;我尝试了训练集中锚点的 KNN 聚类,但结果更差),使用默认的 Huber Loss 和 Focal Loss。在数据增强方面,这只是一个经验推导的配方:

 A.Compose(
        [
            A.RandomSizedCrop(min_max_height=(800, 1024), height=1024, width=1024, p=0.5),
            A.OneOf([
                A.HueSaturationValue(hue_shift_limit=0.2, sat_shift_limit= 0.2, 
                                     val_shift_limit=0.2, p=0.9),
                A.RandomBrightnessContrast(brightness_limit=0.2, 
                                           contrast_limit=0.2, p=0.9),
            ],p=0.9),
            A.ToGray(p=0.01),
            A.HorizontalFlip(p=0.5),
            A.VerticalFlip(p=0.5),
            A.RandomRotate90(p=0.5),
	    A.Transpose(p=0.5),
	    A.JpegCompression(quality_lower=85, quality_upper=95, p=0.2),
	    A.OneOf([
		A.Blur(blur_limit=3, p=1.0),
		A.MedianBlur(blur_limit=3, p=1.0)
	    ],p=0.1),
            A.Resize(height=1024, width=1024, p=1),
            A.Cutout(num_holes=8, max_h_size=64, max_w_size=64, fill_value=0, p=0.5),
            ToTensorV2(p=1.0),
        ], 
        p=1.0, 
        bbox_params=A.BboxParams(
            format='pascal_voc',
            min_area=0, 
            min_visibility=0,
            label_fields=['labels']
        )
    )

以及 __getitem__ 部分:

if self.test or random.random() > 0.33:
            image, boxes = self.load_image_and_boxes(index)
elif random.random() > 0.5:
            image, boxes = self.load_cutmix_image_and_boxes(index)
else:
            image, boxes = self.load_mixup_image_and_boxes(index)

请注意,mixup 是 1:1 版本;我没能让 beta 版本工作(我尝试在损失计算期间重新加权框;结果更差)。然后,我通过基本的健全性检查并排除得分低或太大/太小的框,稍微调整了 TTA(8x,因为 16x 有重复)上的 WBF 流程。整体基线的 Public LB 约为 0.753。

拼图技巧

然后,我意识到就像我几年前参加的 TGS 比赛一样,我们可以通过做拼图来获取更多数据。你可以查看一个流行的公共 Kernel 来了解如何恢复原始全尺寸图像。在这之后,我意识到了另外两个问题:

  1. 边缘框断裂问题: 如果一个麦穗在一半出现在一张图像中,另一半出现在另一张图像中,最终图像中会出现两个框。这会产生大量噪声,肯定会降低模型性能。
  2. 分布差异: 这更加微妙。回顾数据洞察部分,我谈到了边缘框的难题。现在,如果我们从大图像中裁剪 1024 的补丁,结果分布实际上与我们现在看到的特殊位置裁剪所代表的分布不同。我没有真正记录模型性能的变化,但这确实对训练有显著影响。

我的解决方案如下:

  1. 我尝试手动纠正每一个标签,但 30 分钟后就厌倦了。所以我写了一个自动程序来替我做:

    • 获取所有坐标之一可被 1024 整除的框
    • 修剪大图边缘的那些
    • 生成边缘上的段并将其匹配到框(即我们只关心边缘的一侧)
    • 计算边缘上的成对 IoU
    • 使用贪婪方法配对框
    • 融合框

    然后我使用纠正后的数据来训练我的模型。此外,为了鼓励模型的 AP 增益,图像有 0.5 的几率在不裁剪的情况下输入网络,例如,将 2048x2048 缩小到 1024x1024,而不是从大图像中裁剪。我做了一些消融实验,这确实提高了模型性能。

  2. 我在这里做的基本上是将拼图获得的图像视为“伪标签”,或者简单地说,是来自不同分布的图像。

    • 我开发的第一个模型基于论文“A Simple Semi-Supervised Learning Framework for Object Detection”。基本上,结果损失是分别由原始图像和拼图图像计算的损失的加权和。
    • 然后,我想起了论文“Adversarial Examples Improve Image Recognition”中介绍的技巧,其中不同分布的图像通过不同的 BatchNorm。所以我从基线复制了 BatchNorm 统计信息并将其复制两次,在第二阶段训练期间,原始数据和拼图数据通过不同的 Norm,统计信息单独计算。在推理过程中,我使用原始数据的分布。我尝试了扩展场景,例如按来源分割的 7 分割 Norm,或在推理期间不同分割 Norm 的平均值,所有这些都在本地带来了改进,但在 Public LB 上没有,所以我没有进一步研究。

我还设计了一个 FixMatch 的检测变体,但在图像大小 = 1024 的情况下无法放入我的 GPU。

最后,半监督学习部分非常平凡,因为花哨的技术(STAC, SplitBatchNorm, Pi-Model, Mean Teacher 等)不适合 Kernel 评估时间框架。我所做的只是冻结骨干网络以允许使用 16GB 显存进行训练,以及使用不同的超参数来生成伪标签。你可以在我的开源 Kernel 中查看详细信息。大多数选择与其说是深刻的见解,不如说是经验之谈。

抱歉这篇解决方案日记写得有点乱,独自工作让你养成尝试太多东西而没有好好记录的坏习惯。另外,因为我是独自工作,我没有足够的时间深入调查上面提到的一些想法。我将在接下来的几天里寻求改进解决方案日记,如果有时间可能会写一份技术报告。

同比赛其他方案