返回列表

6th place solution: Luck is All You Need

596. SenNet + HOA - Hacking the Human Vasculature in 3D | blood-vessel-segmentation

开始: 2023-11-07 结束: 2024-02-06 医学影像分析 数据算法赛
第六名解决方案:幸运是唯一需要

第六名解决方案:幸运是唯一需要

作者:Volodymyr

竞赛排名:第6名

发布时间:2024-02-07

嗨,Kagglers!

在经历了大洗牌后重新登上排行榜并恢复心理健康后,我们可以深入探讨第六名的解决方案了。但在此之前,有几句非常重要的话要说:

我要感谢乌克兰武装部队、乌克兰国家安全局、乌克兰国防情报局和乌克兰国家紧急服务局,为参与这场伟大竞赛、完成这项工作以及帮助科学、技术和商业继续前进提供了安全保障。

验证?并不 really

我使用2个器官进行验证(不同折):kidney_3_dense 和 kidney_2。在发布了3D表面Dice的快速版本后,我能够在训练时计算验证分数,并得到以下见解:

  • 跟踪 kidney_2 的分数对我而言毫无用处。从第2-3个epoch开始,kidney_2 的验证分数就开始下降
  • kidney_3_dense 的分数对于检查"激进"特征(如额外数据和新的损失函数)很有意义。但最优分数在0.9-0.925 dice之间波动,与Public或Private分数没有任何合理的相关性
  • kidney_3_dense 上的最优阈值对Private、Public和kidney_3_dense分数都是最优的 - 0.1及更低
  • 预测时调整为恒定um/voxel(我选择了50 um/voxel)会增加CV和Public上的最优阈值,但会显著降低最优分数。但在Private上,它成为最稳健的方法之一

总之,验证没有起作用(至少我的验证是这样)。这并不奇怪,因为CV、Public和Private都只有一个数据点。

数据

我使用了除kidney_1_voi样本外的所有训练数据。为了扩大训练数据,我使用了来自人类器官图谱的50um_LADAF_2020_31_kidney_pag数据。

对于数据归一化,我使用了@hengck23提出的方法 - 百分位归一化

训练设置

我主要采用5个切片的2.5D方法。我最初从一个视图模型开始,沿着最后一个轴迭代,但后来切换到多视图,并在训练中使用所有三个轴的切片。

我使用了512方形裁剪,非空概率为0.5,相当标准的增强方法,以及在一个器官和一个视图内使用CutMix,概率为0.5,alpha为1.0:

"cutmix_transform":lambda : [
    A.PadIfNeeded(
        min_height=crop_size,
        min_width=crop_size,
        always_apply=True,
    ),
    # 以0.5概率采样非空掩码
    # 否则将采样空或非空掩码
    A.OneOrOther(
        first=A.CropNonEmptyMaskIfExists(crop_size, crop_size), 
        second=A.RandomCrop(crop_size, crop_size), 
        p=0.5
    ),
],
"transform": A.Compose(
    [
        A.ShiftScaleRotate(
            scale_limit=0.2,
        ),
        # 二面体增强
        A.RandomRotate90(p=0.5),
        A.HorizontalFlip(p=0.5),
        A.VerticalFlip(p=0.5),
        A.Transpose(p=0.5),
        A.OneOf(
            [
                A.RandomBrightnessContrast(),
                A.RandomBrightness(),
                A.RandomGamma(),
            ],
            p=1.0,
        ),
        ToTensorV2(transpose_mask=True),
    ]
),

"do_cutmix": True,
"cutmix_params": {"prob": 0.5, "alpha": 1.0},

我使用Adam优化器,并使用CosineAnnealingLR从1e-3降低学习率到1e-6。

关于损失函数的选择,我最初从经典的BCE+Dice损失开始,然后尝试实现直接优化指标的损失函数,但效果不佳。幸运的是,我发现了BoundaryLoss,它最初与BCE+Dice损失相当,后来效果更好。有趣的是,最好的(未被选中的)模型是在BoundaryLoss + 0.5 Focal Symmetric Loss上训练的,在Private上得分0.756,Public上0.867,kidney_3_dense上0.916,这是一个相当平衡的分数(当然,与其他所有模型的分数分布相比)。

我训练了30个epoch,将原始训练集重复3次,伪训练集重复2次。我使用14的批次大小,在2个GPU上用DDP训练,所以最终批次大小为28。

在我看到关于3D模型有希望的结果的帖子后,我开始探索3D方法,它们效果很好。为了使其训练时不出现NaN,我改变了优化策略,切换到SGD,momentum=0.99,weight_decay=3e-5,nesterov=True,并将起始学习率改为1e-6 - 取自monai示例

由于图像的整体分辨率显著增加,我不得不将批次减少到每个GPU上3个,所以聚合批次大小为6。我总共训练了约12万次迭代。

至于增强方法 - 它们与2.5D设置基本相同,只是去掉了Zoom。

"transform_init": lambda : mt.Compose(
    [
        mt.OneOf([
            mt.RandRotate90d(keys=('image', 'mask'), prob=0.5, spatial_axes=(-3,-1)),
            mt.RandRotate90d(keys=('image', 'mask'), prob=0.5, spatial_axes=(-2,-1)),
            mt.RandRotate90d(keys=('image', 'mask'), prob=0.5, spatial_axes=(-3,-2))
        ]),
        mt.RandFlipd(keys=('image', 'mask'), prob=0.5, spatial_axis=-1),
        mt.RandFlipd(keys=('image', 'mask'), prob=0.5, spatial_axis=-2),
        mt.RandFlipd(keys=('image', 'mask'), prob=0.5, spatial_axis=-3),

        mt.RandScaleIntensityd(keys=('image'), prob=0.5, factors=0.2)
    ]
),

Zoom在CV上效果更好,但在Public和Private上更差(为什么?- 谁知道呢……)

神经网络

我主要使用EfficientNet系列作为编码器(来自noisy student权重),从B3开始,然后切换到B5,不幸的是B7在CV和Public上效果都不好。

有趣的是,se_resnext50_32x4d在Public LB(0.852)和CV(0.909)上表现不佳,但在Private上表现很好(0.702)。

对于解码器,我主要使用Unet++。我尝试过Unet3+,但结果明显更差。

对于3D网络,我使用DynUNet,并根据下一个脚本调整了模型架构。我尝试过使用MONAI Model Zoo中的预训练Unet,但它在所有数据集上表现都很差。

推理和后处理

我使用512滑动窗口,0.5重叠,flip TTA,以及2折的最后一个检查点。切换到多视图模型后,我还添加了多视图TTA。

下一步是创建肾脏掩码。我尝试了几种方法:

  1. 使用在这个数据集上训练的segmentation网络 + 轻微后处理去除二进制孔洞和小连通区域
  2. 使用基于强度阈值、腐蚀和膨胀的算法方法

第一种方法有很高的FP率但几乎为零的FN率,而第二种方法有很高的FN率。两者在kidney_3上表现几乎完美,所以没有真正影响fold 0的分数,但算法方法切掉了kidney_2的肾脏区域,显著降低了fold 1的分数。但同时,第二种方法提高了Public分数(0.874->0.882)。我明白这有90%过拟合到Public LB,但我决定冒险。

最终模型

对于最终提交,我选择了以下模型:

  • 纯2.5D -> 算法后处理
    • Public: 0.886
    • Private: 0.681
    • Kidney 3 dense分数: 0.917
  • 2.5D(权重3.0)与3D(权重1.0)混合
    • Public: 0.871
    • Private: 0.676
    • Kidney 3 dense分数: ~0.918

对于两个模型,我使用了0.05的阈值。

本次比赛最受欢迎的环节:未被选中的最佳提交

在这里,我想指出几个对我来说最有趣的方法:

  • 纯2.5D但添加0.5系数的Symmetric Focal loss
    • Public: 0.867
    • Private: 0.756
    • Kidney 3 dense分数: 0.916
  • 将2D切片调整为50 um/voxel进行预测,然后调整回原大小
    • Public: 0.799
    • Private: 0.753
    • Kidney 3 dense分数: 0.907
  • 使用scipy.zoom将整个体积调整为50 um/voxel进行预测,然后调整回原大小
    • Public: 0.726
    • Private: 0.745
    • Kidney 3 dense分数: 未检查
  • 单独的3D模型
    • Public: 0.849
    • Private: 0.723
    • Kidney 3 dense分数: 0.915

对我来说,选择第一个或第二个是合乎逻辑的,但与其他更好的提交一样,这听起来像是纯粹的随机。

结论

在单个数据样本上计算指标会导致严重的洗牌。

结束语

希望你们在阅读时没有睡着。最后,我要感谢整个Kaggle社区,祝贺所有参与者和获胜者。特别感谢印第安纳大学布卢明顿分校、伦敦大学学院、Yashvardhan Jain (@yashvrdnjain)、Claire Walsh (@clairewalsh)、Kaggle团队和其他组织者。

同比赛其他方案