返回列表

3rd Place Solution: 2.5D U-Net

567. Google Research - Identify Contrails to Reduce Global Warming | google-research-identify-contrails-reduce-global-warming

开始: 2023-05-10 结束: 2023-08-09 双碳与可持续发展 数据算法赛

第三名解决方案:2.5D U-Net

作者:knshnb

比赛排名:第3名

发布时间:2023-08-10

最后更新:2023-08-18

团队成员:knshnb、Yiemon773、charmq

感谢主办方和Kaggle团队组织这场比赛,也祝贺所有获奖者!同时非常感谢我的队友(@charmq@yoichi7yamakawa)!

概述

我们的解决方案是三位成员各自pipeline的集成。在此,我将主要讲解我的pipeline,它被用作主要解决方案。

我的方案基于简单的2.5D U-Net(下面会详述)。我使用512x512(实验阶段使用256x256)的灰度图像作为输入,这些图像来自官方notebook,预测人类个体掩膜的平均值。我使用AdamW优化器训练了25个epoch,配合带warmup的余弦退火调度器。使用的损失函数为:(-dice_coefficient + binary_cross_entropy) / 2

验证

在比赛初期,我进行了5折交叉验证。但由于正像素比例的差异,验证集分数的趋势与out-of-fold分数略有不同。

随着训练计算量增大,我决定仅检查在整个训练数据上训练的验证分数。验证分数似乎与公开排行榜分数有较好的相关性,只有少量噪声。

架构

我们的主要方法是所谓的2.5D:通过将帧堆叠到批处理维度,将3D输入转换为2D,然后输入到2D骨干网络中。

我首先尝试了2.5D U-Net架构,在完整的U-Net之后使用3D卷积,相比2D模型获得了小幅提升(验证分数约+0.01)。

随后我尝试将3D卷积移到U-Net跳跃连接层的中间位置,这些层包含了每个下采样特征的更丰富信息。我们使用第2、3、4帧(从0开始计数)。在3D卷积中,我们通过堆叠两个kernel_size=2、padding=0的卷积层,将帧维度从3减少到1。这样获得了大幅提升(验证分数约+0.02)。

伪代码如下(严重依赖segmentation_models.pytorch库):

class Conv3dBlock(torch.nn.Sequential):
    def __init__(
        self, in_channels: int, out_channels: int, kernel_size: tuple[int, int, int], padding: tuple[int, int, int]
    ):
        super().__init__(
            torch.nn.Conv3d(in_channels, out_channels, kernel_size, padding=padding, padding_mode="replicate"),
            torch.nn.BatchNorm3d(out_channels),
            torch.nn.LeakyReLU(),
        )

class Segmentor25d(torch.nn.Module):
    def __init__(self, ...):
        super().__init__()
        self.n_frames = 3
        self.backbone = smp.Unet(...)
        conv3ds = [
            torch.nn.Sequential(
                Conv3dBlock(ch, ch, (2, 3, 3), (0, 1, 1)), Conv3dBlock(ch, ch, (2, 3, 3), (0, 1, 1))
            )
            for ch in self.backbone.encoder.out_channels[1:]
        ]
        self.conv3ds = torch.nn.ModuleList(conv3ds)

    def _to2d(self, conv3d_block: torch.nn.Module, feature: torch.Tensor) -> torch.Tensor:
        total_batch, ch, H, W = feature.shape
        feat_3d = feature.reshape(total_batch // self.n_frames, self.n_frames, ch, H, W).transpose(1, 2)
        return conv3d_block(feat_3d).squeeze(2)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        n_batch, in_ch, n_frame, H, W = x.shape
        x = x.transpose(1, 2).reshape(n_batch * n_frame, in_ch, H, W)

        self.backbone.check_input_shape(x)

        features = self.backbone.encoder(x)
        features[1:] = [self._to2d(conv3d, feature) for conv3d, feature in zip(self.conv3ds, features[1:])]
        decoder_output = self.backbone.decoder(*features)

        masks = self.backbone.segmentation_head(decoder_output)
        return masks

数据增强

虽然完全的翻转和旋转增强会因像素偏移问题而失效(正如第一名解决方案第九名解决方案所指出的),但小比例地应用这些增强仍能略微提升性能。我使用的全部增强如下:

import albumentations as A

augments = [
    A.HorizontalFlip(p=0.1),
    A.VerticalFlip(p=0.1),
    A.RandomRotate90(p=0.4),
    A.ShiftScaleRotate(0.05, 0.1, 15, p=0.3),
    A.RandomResizedCrop(512, 512, scale=(0.75, 1.0), ratio=(0.9, 1.1111111111111), p=0.7),
]

伪标签

我们将模型对第2、3、5、6、7帧的预测结果以0.25为间隔离散化,用作伪标签。由于可能存在与原始训练数据的分布偏移,我们使用伪标签进行预训练,使用原始训练数据进行微调。

阈值

由于我们使用验证数据训练了一些模型,因此很难通过验证数据来优化阈值。我们采用了百分位阈值的方法。通过验证数据我们确认,最优百分位几乎等于正像素的比例(0.18%)。因此,我们通过提交时间的排行榜探测,确定了验证数据中某些模型最佳阈值在测试数据中的对应百分位。结果约为0.16%,希望这是正确的比例。

其他技巧

  • 设置梯度检查点节省了一半以上的内存使用
  • 略微增加U-Net的解码器通道数提升了性能
  • 批处理Dice系数的平均值与全局Dice系数不一致,且在小批量情况下不稳定。为缓解此问题,我们在分子上启发性地增加了700000,在分母上增加了1000000

最终提交

我们的最佳单模型使用maxvit_large骨干网络,在验证/私有/公开排行榜上的分数分别为0.706/0.71770/0.71629。这个分数仍然可以赢得第三名!

通过集成18个2.5D模型(使用不同的骨干网络:maxvit_large_tf_512、tf_efficientnet_l2、resnest269e、maxvit_base_tf_512、maxvit_xlarge_tf_512、tf_efficientnetv2_xl)以及略有不同的设置(使用伪标签、包含验证数据进行训练、微调学习率等),我们达到了0.72233的私有排行榜分数。此外,加入@yoichi7yamakawa@charmq的模型后,私有分数提升至0.72305,本可以领先第二名0.00001分!

遗憾的是,我们未能选择该提交。在最后一天,我们增加了一个使用完全翻转和旋转增强以及测试时增强训练的模型,这并未显著改变验证分数。但这可能因像素偏移问题(或仅仅是随机波动)而降低了在私有数据上的泛化能力。

无论如何,未能找出像素偏移问题是导致失利的原因,我也从中学习了很多。

未生效的方法

  • Double U-Net架构(训练不稳定……)
  • 通过微调增加图像尺寸
  • 在3D卷积层之间增加2D卷积层

Yiemon773部分

我的2D模型集成结果
私有排行榜:0.709+ 使用伪标签
私有排行榜:0.705+ 不使用伪标签

预处理

我将归一化过程从normalize_range改为normalize_mean_std。每个波段的均值和标准差使用训练数据计算。

骨干网络

resnest269e, maxvit_base, maxvit_large, efficientnetv2_l

损失函数

以下损失的加权平均值:硬标签Dice、硬标签BCE、软标签Dice、软标签BCE

未生效的方法

  • 多种增强方法
    • Mixup
    • 帧打乱
    • 通道打乱
    • ……
  • 使用11、13、14、15波段以外的其他波段

charmq部分

架构

上面提到的2.5D模型以及2D模型。2.5D模型的表现优于2D模型。

骨干网络

resnest269e, resnetrs420, maxvit_base, maxvit_large, efficientnetv2_l

图像尺寸

2.5D模型和2D maxvit模型使用(512, 512)尺寸训练。对于2D resnest269e,(1024, 1024)比(512, 512)更好,但在其他模型上无效。

损失函数

以下损失的加权平均值:硬标签(像素掩膜)Dice、硬标签(1-像素掩膜)Dice、软标签(个体掩膜均值)Dice、硬标签(个体掩膜最小值和最大值)Dice

未生效的方法

  • 裁剪正像素周围区域的增强方法
  • 使用convnext和swin transformer的UPerNet(mmsegmentation)

发现

  • 大型架构如resnest269e、resnetrs420和maxvit表现良好
  • maxvit与AMP的训练不稳定
    • 我们对maxvit关闭了AMP
    • 梯度检查点有助于减少内存使用
  • efficientnet_l2表现良好但有时会输出nan
    • 我们简单地将nan输出替换为0
  • 集成能提升分数
    • 例如:0.68模型 + 0.68模型 → 0.695
    • 即使是5折模型的集成也能提升公开分数

致谢

我们感谢Preferred Networks, Inc允许我们使用计算资源。

同比赛其他方案