📑

RSNA 2022 3rd Place Solution

2023/09/03に公開

Introduction

概要

  • bounding box が与えられているがそれを使用せず、segmentation だけを使用した

  • パイプライン

    • C1-C7 vertebrae の bbox を作成
    • slice level の vertabrae ratio を作成
    • slice ごとの vertebrae と fracture ラベルを学習
    • study level vertebrea features をさらに学習
  • 資料

各ステージの詳細

Stage-1

bounding box の再構成を実施

  • train_bounding_box.csv は StudyInstanceUID  ごとの bbox 情報をもっている
    • ただし全スライスに対して与えられているわけではない
    • これを study level labels と呼んでいる
print(len(df.query('StudyInstanceUID=="1.2.826.0.1.3680043.10051"')))
print(len(glob.glob("/kaggle/input/rsna-2022-cervical-spine-fracture-detection/train_images/1.2.826.0.1.3680043.10051/*.dcm")))
# 16
# 272
  • そこで各スライス単位での bbox 情報を出力するために、segmentation 情報を使用したのだと思われる

    • segmentation 情報は全スライス(training data の subset ではあるが)に対して与えられているので、segmentation から bbox 情報を引き出して ground truth としている
    • x0, y0, x1, y1, has_bbox を予測した
    • 座標点には RMSE、has_bbox には BCE loss を使用
  • segmentation map (87 study id) を使って 2.5D + 1D RNN を学習した

  • データは nii ファイルの中に次元として segmentation 情報が詰まっている

for fname in glob.glob("/kaggle/input/rsna-2022-cervical-spine-fracture-detection/segmentations/*.nii"):
    a = nib.load(fname).get_fdata()
    b = glob.glob(f'/kaggle/input/rsna-2022-cervical-spine-fracture-detection/train_images/{fname.split("/")[-1].replace(".nii","")}/*.dcm')
    
    print(a.shape, len(b))
  • 予測するスライスレベルの bbox をそのまま使用するのではなく、StudyInstanceUID の中で最も大きな bbox で各スライスをクロップするようにしたとのこと

Stage-2

imgls = [cv2.merge(imgls[i:i + 3]) for i in range(0, len(imgls), 3)]
  • ランダムなスライスでは fracture がないので、バッチサイズを大きくして、各バッチに本当に学習したい情報が乗るようにしている

    • resnet50d、sersenext50を使用
    • 1 fold 9時間程度の学習時間
  • CNN + GAP (Global Average Pooling), LSTM, Dense

  • slice vertebrae label を学習している

  • モデル概要

    • CNNとLSTM を初期化している
self.backbone = timm.create_model('resnest50d')
hidden_size = self.backbone.fc.in_features # 2048
self.backbone.fc = torch.nn.Identity()
self.rnn = nn.LSTM(hidden_size, hidden_size, batch_first=True,  bidirectional=True)
  • forward
    • まずデータ構成として、batch size, seuqence length, in_channs, height, widht になっている。
    • 32 はスライスをランダムに取得するもので、overlap をゆるしながら選択した各スライスに対して隣接するスライスを取得している(32, 3 の意味)
    • そのままだと resnet に入れられないので view で修正している(bs=64, in_chans=3, 512, 512の画像情報として扱うことになる)
    • 出てきた embedding の情報を修正して LSTM (=RNN)に入力できる形式にしている
    • こうすることで、本来は (2, 3, 512, 512) の画像をCNNに 32回通して...ということをする必要があるが、うまいこと次元を扱うことで1回の fowardで済ませている
batchsize, seqlen, ch, h, w = batch['image'].shape # (2,32,3,512,512)
x = batch['image'].view(-1, ch, h, w) # (64,3,512,512)
emb = self.backbone(x) #  (64,2048)
emb = emb.view(batchsize, seqlen, -1) # (2, 32 ,2048)
logits = self.rnn(emb)[0] # (2, 32 , 4096)

雑記

  • StratifiedKFold するときの split した df は保存しておいたほうが再現性的によいかもしれない
  • glob.glob("**/*.png", recursive=True) でサブディレクトリ以下のすべてのpng をリストアップできる
  • .nii データは nibabel.load(...).get_fdata() で読み込める
  • シード値固定:
def set_seed(seed=1234):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = False
    torch.backends.cudnn.benchmark = True
  • CNN と RNN はどういう感じにしているのかと思ったが、view をうまいこと使っているのだな。「この次元にはこの意味をもたせる」くらいの軽い気持ちで(明示的に与えなくても)いいのだな
GitHubで編集を提案

Discussion