625. RSNA 2024 Lumbar Spine Degenerative Classification | rsna-2024-lumbar-spine-degenerative-classification
首先,我要衷心感谢竞赛主办方和 Kaggle 工作人员组织了如此迷人的比赛。我 thoroughly enjoyed 这次比赛,并在这个过程中学到了很多!
此外,我要感谢 @hengck23 和 @brendanartley。@hengck23 的讨论和笔记本是我解决方案的起点,而 @brendanartley 的 这个数据集 帮助了我的坐标预测模型。他们对 Kaggle 社区的贡献令我印象深刻。
这是我第一次写解决方案总结,欢迎大家留言评论或提出改进建议!
我的解决方案采用两阶段方法:创建 test_label_coordinates.csv 和预测严重程度。此外,我将第一阶段分为 instance_number(实例编号/层级)预测和坐标预测。因此,我准备了 3 种类型的模型:instance_number 预测模型、坐标预测模型和严重程度预测模型。流程如下图所示。
在第一阶段,我使用了两种类型的模型:3D 卷积模型和 2D 卷积模型。这些模型非常简单,采用 编码器 + 分离层级头(level-separated heads)的结构。
在这部分,我使用了简单的 3D ConvNeXt 来预测每个层级的 instance_number。输入模型的数据仅从 0 到 1 归一化,按 dicom 元数据排序,并在深度方向填充至 32 以对齐形状。数据预处理如下图所示 (scs 示例)。
在训练模型时,我训练了两个任务:回归和分类,分别使用 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 预测模型的结果如下表所示 (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 折交叉验证)。
在坐标预测任务中,我使用了 2D 编码器 + 分离层级头,几乎与 instance_number 回归模型相同。数据是 3 通道图像。图像是使用 L1 ~ S1 的 instance_number 的中位数选取的。然后对数据进行归一化和重塑 (512x512)。标签为 (x', y') = (x/width, y/height),与 instance_number 回归相同,我也使用了 L1 loss。模型架构如下图所示。
我为此任务使用了 ConvNeXt-base 和 Efficientnet-v2-l。在训练这些模型之前,我使用 @brendanartley 的 数据集 进行了预训练。这些预训练模型比使用 imagenet 预训练的模型略好。我使用均值对这些预测进行了集成。
对于轴状面的 instance_number 预测,我借用了 @hengck23 的方法(笔记本在 这里)。然后我预测了轴状面的坐标,与矢状面的坐标预测相同。
在第二阶段,我尝试了简单的 2.5D 模型和 MIL (多实例学习)。2.5D 模型可以很容易实现,但最终 MIL 的效果比简单的 2.5D 更好。
我的预处理策略是裁剪。例如,我为 scs 裁剪了 sagt2 图像:
裁剪图像后,图像可能如下所示 (scs 的 sagt2, L1/L2)。
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)。
针对 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 裁剪范围。
我使用了以下几种增强方法:
裁剪前
裁剪后
特别是,instance_number 的随机偏移对于第一阶段误差的鲁棒性至关重要。
我的模型架构如下图所示。
[已编辑] 我已更新了说明模型架构的图表,以修正之前版本中的错误。下面代码中的 aux_attn_score 直接输入到交叉熵损失中。
我使用 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
所有训练代码均在 Google Colaboratory 中实现。所有模型都用于 此推理代码。以下是模型名称和训练笔记本链接的对应列表。你可以在 推理代码 中查看这些模型名称。
你可以在带有 T4 + 高内存的 Google Colaboratory 环境中进行训练。