567. Google Research - Identify Contrails to Reduce Global Warming | google-research-identify-contrails-reduce-global-warming
首先,我们要感谢Kaggle和主办方举办了这场有趣(而且*对我们来说没什么效果*)的比赛。这并不是一个 fancy 的解决方案,但我们为采取正确步骤在私有测试集上实现良好泛化感到自豪(确实有42个名次的波动 😃)。
我们的最终解决方案是一个包含8个模型的集成,结合了不同的编码器和图像尺寸。每个模型配置使用不同的随机种子训练3次(共24个模型),并且始终使用所有可用数据(训练+验证)。
ASH伪彩色图像,仅使用带标签帧的2D模型和硬标签。
使用官方提供的训练/验证分割。
所有模型都基于SMP库中的UNet架构。我们通过对单个模型输出的概率进行加权平均来组合它们,其中权重通过Optuna优化验证集上的全局dice分数来确定。最终的CV分数为0.6825(由于我们直接在验证集上优化权重,这个分数可能有些偏差)。
| 权重 | 编码器 | 图像尺寸 | 学习率 | 轮数 | 批大小 | CV分数 |
|---|---|---|---|---|---|---|
| 0.02 | timm-resnest101e | 384 | 5e-4 | 20 | 48 | 0.64397 |
| 0.07 | maxvit_base_tf_384.in21k_ft_in1k | 384 | 1e-4 | 20 | 16 | 0.65699 |
| 0.18 | maxvit_base_tf_512 | 512 | 1e-4 | 40 | 8* | 0.66414 |
| 0.02 | tf_efficientnetv2_b3.in1k | 768 | 5e-4 | 35 | 24* | 0.65178 |
| 0.23 | tf_efficientnetv2_m.in21k_ft_in1k | 512 | 5e-4 | 35 | 24* | 0.6525 |
| 0.14 | tf_efficientnetv2_l.in21k_ft_in1k | 384 | 5e-4 | 35 | 24* | 0.64538 |
| 0.11 | maxvit_large_tf_224.in1k | 448 | 1e-4 | 40 | 8* | 0.66407 |
| 0.23 | maxvit_large_tf_384.in1k | 384 | 1e-4 | 40 | 8* | 0.66782 |
*梯度累积步数为2。
集成模型的最优阈值为0.43,这是通过遍历0到1之间以0.1为步长的阈值并计算对应的全局dice分数得到的。我们观察到在0.43处有一个相对宽泛的最大值,这给了我们足够的信心选择该阈值。
我们使用了CosineAnnealingLR学习率调度器和AdamW优化器。需要注意的是,我们总是尝试选择轮数,使得最后一个epoch就是最佳模型。这比在训练过程中选择最佳检查点具有更好的泛化能力,特别是在我们的情况中,训练曲线表现出不规则的行为。最后,一旦模型验证通过,我们会使用相同的配置并在所有可用数据上重新训练。
我们没有注意到掩码偏移的问题,因此翻转和旋转对我们没有效果。其他有效的增强方法包括:
A.Compose([
A.RandomResizedCrop(height=256, width=256, p=0.5),
A.RandomBrightnessContrast(p=0.5)
])
CutMix(p=0.5)
MixUp(p=0.5)
以下是我们的CutMix和MixUp实现:
def rand_bbox(size, lam):
W = size[2]
H = size[3]
cut_rat = np.sqrt(1. - lam)
cut_w = int(W * cut_rat)
cut_h = int(H * cut_rat)
# 均匀分布
cx = np.random.randint(W)
cy = np.random.randint(H)
bbx1 = np.clip(cx - cut_w // 2, 0, W)
bby1 = np.clip(cy - cut_h // 2, 0, H)
bbx2 = np.clip(cx + cut_w // 2, 0, W)
bby2 = np.clip(cy + cut_h // 2, 0, H)
return bbx1, bby1, bbx2, bby2
def cutmix(data, target, alpha):
indices = torch.randperm(data.size(0))
# shuffled_data = data[indices]
shuffled_target = target[indices]
lam = np.clip(np.random.beta(alpha, alpha), 0.3, 0.4)
bbx1, bby1, bbx2, bby2 = rand_bbox(data.size(), lam)
new_data = data.clone()
new_data[:, :, bby1:bby2, bbx1:bbx2] = data[indices, :, bby1:bby2, bbx1:bbx2]
# 调整lambda以精确匹配像素比例
lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (data.size()[-1] * data.size()[-2]))
# targets = (target, shuffled_target, lam)
return new_data, target, shuffled_target, lam
def mixup(x: torch.Tensor, y: torch.Tensor, alpha: float = 1.0):
assert alpha > 0, "alpha应大于0"
assert x.size(0) > 1, "Mixup不能应用于单个实例。"
lam = np.random.beta(alpha, alpha)
rand_index = torch.randperm(x.size()[0])
mixed_x = lam * x + (1 - lam) * x[rand_index, :]
target_a, target_b = y, y[rand_index]
return mixed_x, target_a, target_b, lam
损失计算方式如下:
if self.cfg.mixup and torch.rand(1)[0] < self.cfg.mixup_p:
mix_images, target_a, target_b, lam = mixup(images, masks, alpha=self.cfg.mixup_alpha)
logits = self(mix_images)
loss = self.loss_fn(logits, target_a) * lam + (1 - lam) * self.loss_fn(logits, target_b)
elif self.cfg.cutmix and torch.rand(1)[0] < self.cfg.cutmix_p:
mix_images, target_a, target_b, lam = cutmix(images, masks, alpha=self.cfg.cutmix_alpha)
logits = self(mix_images)
loss = self.loss_fn(logits, target_a) * lam + (1 - lam) * self.loss_fn(logits, target_b)
很多方法都没有效果:
感谢我们出色的队友 @maxdiazbattan、@edomingo、@juansensio(如果我遗漏了什么,请在评论中补充)。