567. Google Research - Identify Contrails to Reduce Global Warming | google-research-identify-contrails-reduce-global-warming
感谢主办方和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%,希望这是正确的比例。
我们的最佳单模型使用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分!
遗憾的是,我们未能选择该提交。在最后一天,我们增加了一个使用完全翻转和旋转增强以及测试时增强训练的模型,这并未显著改变验证分数。但这可能因像素偏移问题(或仅仅是随机波动)而降低了在私有数据上的泛化能力。
无论如何,未能找出像素偏移问题是导致失利的原因,我也从中学习了很多。
我的2D模型集成结果
私有排行榜:0.709+ 使用伪标签
私有排行榜:0.705+ 不使用伪标签
我将归一化过程从normalize_range改为normalize_mean_std。每个波段的均值和标准差使用训练数据计算。
resnest269e, maxvit_base, maxvit_large, efficientnetv2_l
以下损失的加权平均值:硬标签Dice、硬标签BCE、软标签Dice、软标签BCE
上面提到的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
我们感谢Preferred Networks, Inc允许我们使用计算资源。