返回列表

9th place solution

639. CZII - CryoET Object Identification | czii-cryo-et-object-identification

开始: 2024-11-06 结束: 2025-02-05 医学影像分析 数据算法赛
第 9 名解决方案

第 9 名解决方案

作者: NANACHI (wadakoki)
发布日期: 2025 年 2 月 6 日

首先,我要衷心感谢竞赛主办方的和 Kaggle 工作人员组织了如此迷人的竞赛。我非常享受这次竞赛,并在这个过程中学到了很多!

此外,我要感谢 @hengck23@davidlist@hengck23 的讨论和 notebook 是我解决方案的起点,而 @davidlist 的许多讨论和评论也非常有帮助。

总结

我的解决方案简单直接。我使用了一个类似 3D ConvNeXt 的模型进行分割,并进一步采用了尽可能多的模型集成。随后,我使用 cc3d 计算了粒子的质心,并用 DBSCAN 对预测的质心进行了整理。

分割掩膜 (Segmentation Mask)

我使用了针对每个粒子调整半径后的 ground truth 掩膜。结果,公共 leaderboard 分数提高了 0.02~0.04。具体而言,我如下表所示调整了掩膜大小;

铁蛋白 (r=60) β-半乳糖苷酶 (r=90) 核糖体 (r=150) 甲状腺球蛋白 (r=130) 病毒样颗粒 (r=135)
60/2 90/2 150/3 130/3 135/3

由于竞赛指标要求预测的质心落在距离 ground truth 质心 r×0.5 的半径内,我认为将每个粒子的半径乘以 0.5 或更小的因子是合理的。

模型架构

在本节中,我将解释我的模型架构,包括编码器和解码器。

编码器 (Encoder)

为了实现编码器,我从 ConvNeXt 开始,因为 ConvNeXt 非常强大且快速。然后,我 customized 了模型以适应预测小粒子的任务。以下是相对于原始 ConvNeXt 的更改;

  • 所有卷积层从 2D 改为 3D
  • 4x4 stem -> 2x2 stem。因为粒子非常小,我认为较大的 stem 会对模型的预测产生不利影响,尤其是铁蛋白和β-半乳糖苷酶。此修改减慢了模型的推理速度,但提高了其交叉验证 (CV) 性能。
  • conv block 中的 7x7 内核大小 -> 3x3。我这样修改的原因与 stem 相同。此修改提高了推理速度和 CV。
  • (3, 3, 9, 3) 块数量 -> (3, 3, 3, 3)。此修改是为了减少参数数量并获得更快的推理速度。CV 没有下降。

解码器 (Decoder)

解码器的流程基于 U-Net。conv block 的实现代码如下;

# conv block for decoder
class ConvBlock3D(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.conv1 = nn.Conv3d(in_channels, in_channels, kernel_size=3, padding=1, bias=False, groups=in_channels)
        self.norm1 = nn.GroupNorm(num_groups=1, num_channels=in_channels)
        self.act1 = nn.GELU()
        self.conv2 = nn.Conv3d(in_channels, in_channels*3, kernel_size=1, bias=False)
        self.conv3 = nn.Conv3d(in_channels*3, out_channels, kernel_size=1, bias=False)

    def forward(self, x):
        # x: (B, C, D, H, W)
        x = self.conv1(x)
        x = self.norm1(x)
        x = self.conv2(x)
        x = self.act1(x)
        x = self.conv3(x)
        return x

解码器的实现代码如下;

class Decoder3D(nn.Module):
    def __init__(self,
                 encoder_dims=[64, 128, 256, 512],
                 decoder_dims=[32, 64, 128, 256],
                 out_channels=5):
        super().__init__()
        self.up3 = nn.Upsample(scale_factor=(2,2,2), mode='trilinear', align_corners=True)
        #self.up3 = nn.ConvTranspose3d(encoder_dims[3], encoder_dims[3], kernel_size=2, stride=2)
        self.dec3 = ConvBlock3D(in_channels=encoder_dims[3] + encoder_dims[2], out_channels=decoder_dims[2])
       ...

    def forward(self, features):
        x, f0, f1, f2, f3 = features

        # --- 1) f3 -> f2 ---
        d3 = self.up3(f3)
        d3 = torch.cat([d3, f2], dim=1)
        d3 = self.dec3(d3)
        ...

由于分割的 ground truth simplement 是一个球体,我使用了基本的 nn.Upsample 进行上采样,而不是 nn.ConvTranspose3d,以减少参数。

其他细节

  • 损失函数 (loss function) 是 BCE
  • 我的推理代码基于 @hengck这个 notebook,DBScan 的想法来自 @linheshennotebook
  • 推理的窗口大小是 (32, 320, 320) (z 轴重叠为 6)。训练的窗口大小是 (32, 256, 256)。
  • TTA 包括 rot90, 180 和 270。
  • 预处理仅归一化
def normalize_numpy(self, x):
    lower, upper = np.percentile(x, (1, 99))
    x = np.clip(x, lower, upper)
    x = x - np.min(x)
    x = x / np.max(x)
    return x
  • 训练的数据增强包括 rot90, 180, 270 和 xyz 翻转。

无效的方法

  • 使用主办方提供的合成数据进行预训练
  • 越来越小的掩膜(例如,大粒子因子=0.25,小粒子因子=0.4)
  • 使用翻转 TTA 而不是 rot90
同比赛其他方案