398. Global Wheat Detection | global-wheat-detection
[更新] 代码现已发布在 https://github.com/liaopeiyuan/TransferDet 。
既然比赛结果已经出炉,我决定比我之前的帖子分享更多细节。
首先,非常感谢 @rwightman 提供的 EfficientDet PyTorch 实现,以及 @shonenkov 出色的入门 Kernel。我也从讨论区的帖子中学到了很多。
这是一个有趣的挑战,数据集噪声很大,训练集和测试集之间存在巨大的域偏差,训练集和测试集内部也存在差异。Kernel 的运行时间限制也使得半监督学习成为可能,这在一定程度上可以缓解上述问题。
我做的第一个选择是验证和模型选择的方式。这是一个艰难的选择,因为:1. 目标检测本质上是一项艰巨的任务;2. 训练数据嘈杂且不稳定。最终,我的决定是在移除了错误标注框(通过人工检查)的20%分层拆分上使用“相对检测损失”,正如这里所提示的。我的理由如下:
我们知道训练集和测试集的分布差异很大,所以对我来说 mAP 意义不大,因为它是一个经过重度处理的结果(中间包含 NMS 等超参数,以及看哪个 mAP 等)。而我们用来训练网络的检测损失可以捕捉整体的分类和回归性能。此外,我不使用这个损失来跨模型比较,而是积累了一系列我认为“在损失下具有可比性”的过程,通过纯实证结果和一点直觉,用它们来选择次要选项,如数据增强。对于整体的模型选择,我对 Public LB 持保留态度,而且因为我不没有足够的时间训练多样化的模型,我不必做模型集成这种巨大的选择。我相信一旦整个数据集发布并清理干净,我可以对此进行更多研究。
大家都可以同意,即使移除了明显错误的标注框,数据仍然非常嘈杂,主要有两种形式:噪声锚点和噪声标注框。
因此,我开发的所有模型都在试图解决这些问题。我首先尝试了 DetectorRS,但在 LB 上只达到了 0.72。所以我转向了 EfficientDet 流程。我从 D4 到 D7 运行了一些简单的测试,结论是带有 COCO 预训练权重的 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 来了解如何恢复原始全尺寸图像。在这之后,我意识到了另外两个问题:
我的解决方案如下:
我尝试手动纠正每一个标签,但 30 分钟后就厌倦了。所以我写了一个自动程序来替我做:
然后我使用纠正后的数据来训练我的模型。此外,为了鼓励模型的 AP 增益,图像有 0.5 的几率在不裁剪的情况下输入网络,例如,将 2048x2048 缩小到 1024x1024,而不是从大图像中裁剪。我做了一些消融实验,这确实提高了模型性能。
我在这里做的基本上是将拼图获得的图像视为“伪标签”,或者简单地说,是来自不同分布的图像。
我还设计了一个 FixMatch 的检测变体,但在图像大小 = 1024 的情况下无法放入我的 GPU。
最后,半监督学习部分非常平凡,因为花哨的技术(STAC, SplitBatchNorm, Pi-Model, Mean Teacher 等)不适合 Kernel 评估时间框架。我所做的只是冻结骨干网络以允许使用 16GB 显存进行训练,以及使用不同的超参数来生成伪标签。你可以在我的开源 Kernel 中查看详细信息。大多数选择与其说是深刻的见解,不如说是经验之谈。
抱歉这篇解决方案日记写得有点乱,独自工作让你养成尝试太多东西而没有好好记录的坏习惯。另外,因为我是独自工作,我没有足够的时间深入调查上面提到的一些想法。我将在接下来的几天里寻求改进解决方案日记,如果有时间可能会写一份技术报告。