返回列表

1st place solution

625. RSNA 2024 Lumbar Spine Degenerative Classification | rsna-2024-lumbar-spine-degenerative-classification

开始: 2024-05-17 结束: 2024-10-08 医学影像分析 数据算法赛
第一名解决方案 - RSNA 2024
作者: NANACHI (wadakoki)
发布时间: 2024-10-12
竞赛: RSNA 2024 Lumbar Spine Degenerative Classification

第一名解决方案

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

此外,我要感谢 @hengck23@brendanartley。@hengck23 的讨论和笔记本是我解决方案的起点,而 @brendanartley 的 这个数据集 帮助了我的坐标预测模型。他们对 Kaggle 社区的贡献令我印象深刻。

这是我第一次写解决方案总结,欢迎大家留言评论或提出改进建议!

总结

我的解决方案采用两阶段方法:创建 test_label_coordinates.csv 和预测严重程度。此外,我将第一阶段分为 instance_number(实例编号/层级)预测和坐标预测。因此,我准备了 3 种类型的模型:instance_number 预测模型、坐标预测模型和严重程度预测模型。流程如下图所示。

pipeline

第一阶段:创建 test_label_coordinates

在第一阶段,我使用了两种类型的模型:3D 卷积模型和 2D 卷积模型。这些模型非常简单,采用 编码器 + 分离层级头(level-separated heads)的结构。

instance_number 预测 (矢状面 sagittal)

在这部分,我使用了简单的 3D ConvNeXt 来预测每个层级的 instance_number。输入模型的数据仅从 0 到 1 归一化,按 dicom 元数据排序,并在深度方向填充至 32 以对齐形状。数据预处理如下图所示 (scs 示例)。

scs_volume_example

在训练模型时,我训练了两个任务:回归和分类,分别使用 L1 Loss 和 Cross Entropy Loss。在分类任务中,这些头输出每个层级形状为 (bs, 32) 的 logits。在回归任务中,这些头输出每个层级形状为 (bs, 3) 的向量。(bs, 3) 形状的向量意味着 (x, y, z),我使用 z 进行深度预测,(x, y) 用作辅助损失。在回归任务中,我将坐标标签归一化为 0 到 1 以稳定训练过程中的模型。具体来说,我使用的标签为 (x', y', z') = (x/width, y/height, z/32)。模型架构如下图所示 (scs 示例)。我为此任务实现了 3D ConvNeXt(实现 3D ConvNeXt 时参考了 这个仓库)。

instance_number_prediction_scs_example

instance_number 预测模型的结果如下表所示 (sagt2, scs)。

模型/误差 +-0 +-1 +-2 error>+-2
cls (分类) 71.08% 27.04% 1.43% 0.44%
reg (回归) 67.48% 30.59% 1.61% 0.31%

我对这两种类型的预测使用了中位数进行集成(实际上每个任务使用了 5 折交叉验证)。

坐标预测 (矢状面 sagittal)

在坐标预测任务中,我使用了 2D 编码器 + 分离层级头,几乎与 instance_number 回归模型相同。数据是 3 通道图像。图像是使用 L1 ~ S1 的 instance_number 的中位数选取的。然后对数据进行归一化和重塑 (512x512)。标签为 (x', y') = (x/width, y/height),与 instance_number 回归相同,我也使用了 L1 loss。模型架构如下图所示。

coordinate_prediction_model_scs_example

我为此任务使用了 ConvNeXt-base 和 Efficientnet-v2-l。在训练这些模型之前,我使用 @brendanartley 的 数据集 进行了预训练。这些预训练模型比使用 imagenet 预训练的模型略好。我使用均值对这些预测进行了集成。

instance_number 计算和坐标预测 (轴状面 axial)

对于轴状面的 instance_number 预测,我借用了 @hengck23 的方法(笔记本在 这里)。然后我预测了轴状面的坐标,与矢状面的坐标预测相同。

第二阶段:严重程度预测

在第二阶段,我尝试了简单的 2.5D 模型和 MIL (多实例学习)。2.5D 模型可以很容易实现,但最终 MIL 的效果比简单的 2.5D 更好。

预处理

裁剪方法

我的预处理策略是裁剪。例如,我为 scs 裁剪了 sagt2 图像:

  1. 选取 5 张图像(中心是被分配了 instance_number 的图像)
  2. 重塑为 512x512
  3. 使用坐标裁剪图像(从坐标 x 向左 96 像素,向右 32 像素;从坐标 y 向上 40 像素,向下 40 像素)

裁剪图像后,图像可能如下所示 (scs 的 sagt2, L1/L2)。

scs_cropped_image

sagt2, sagt1 和 axial 针对每个分类任务进行了裁剪。下表表示从 (x, y) 坐标开始的裁剪范围。

针对 scs

类型
sagt2 96 32 40 40
axial 96 96 96 96

注意,当我从 axial 裁剪图像时,我随机选取左侧或右侧的 subarticular stenosis (SS) 坐标,并且为了调整裁剪点,我在 SS 坐标 x 上增加了 +-20。结果,裁剪范围可能如下图所示(示例为右侧 SS 坐标 x + 20)。

axial_for_scs_cropping

针对 nfn

类型
sagt1 (左右两侧) 96 64 32 32
axial (右) 144 48 96 96
axial (左) 48 144 96 96

针对 ss

类型
axial (右) 144 48 96 96
axial (左) 48 144 96 96

下图是右侧 subarticular stenosis 的 axial 裁剪范围。

axial_cropping_for_ss_right

数据增强

我使用了以下几种增强方法:

裁剪前

  • 坐标 x 和 y 的随机偏移 (-10~+10 像素)
  • instance_number 的随机偏移 (-2~+2。偏移概率由每个 instance_number 预测模型的误差概率决定)

裁剪后

  • RandomBrightnessContrast(p=0.25)
  • ShiftScaleRotate(shift_limit=0.1, scale_limit=(-0.1, 0.1), rotate_limit=20, p=0.5)

特别是,instance_number 的随机偏移对于第一阶段误差的鲁棒性至关重要。

模型架构

我的模型架构如下图所示。
[已编辑] 我已更新了说明模型架构的图表,以修正之前版本中的错误。下面代码中的 aux_attn_score 直接输入到交叉熵损失中。

severity_prediction_model_scs_fixed severity_prediction_model_ss_fixed

我使用 ConvNeXt-small 和 Efficientnet-v2-s 作为编码器。在实现基于 Attention 的 MIL 后,我的公共 LB 分数从 0.37 提高到了 0.35。然后,添加 bi-LSTM、辅助损失和集成将我的分数从 0.35 提高到了 0.33。bi-LSTM + 基于 Attention 的 MIL 实现如下:

class LSTMMIL(nn.Module):
    def __init__(self, input_dim):
        super(LSTMMIL, self).__init__()
        self.lstm = nn.LSTM(input_dim, input_dim//2, num_layers=2, batch_first=True, dropout=0.1, bidirectional=True)
        self.aux_attention = nn.Sequential(
            nn.Tanh(),
            nn.Linear(input_dim, 1)
        )
        self.attention = nn.Sequential(
            nn.Tanh(),
            nn.Linear(input_dim, 1)
        )
    def forward(self, bags):
        batch_size, num_instances, input_dim = bags.size()
        bags_lstm, _ = self.lstm(bags)
        attn_scores = self.attention(bags_lstm).squeeze(-1)
        aux_attn_scores = self.aux_attention(bags_lstm).squeeze(-1)
        attn_weights = torch.softmax(attn_scores, dim=-1)
        weighted_instances = torch.bmm(attn_weights.unsqueeze(1), bags_lstm).squeeze(1)

        return weighted_instances, aux_attn_scores

未成功的方法

  • 使用 MAMBA 和 Self-Attention 代替 bi-LSTM
  • 在 aux_attention 层和 attention 层之间共享权重
  • scs 使用 sagt1 图像,ss 使用 sagt1 和 sagt2 图像,nfn 使用 sagt2 图像
  • 更多的 epoch(我在实验中 convnext-small 用了 7 个 epoch,efficientnet-v2-s 用了 14 个 epoch)
  • 更大的模型 (在我的实验中 convnext-large < convnext-base < convnext-small)
  • Vision Transformers (我认为这是我的问题。但在我的实验中卷积模型优于 ViT)
同比赛其他方案