📑
RSNA 2022 3rd Place Solution
Introduction
- RSNA 2022 3rd place solution のまとめ記事
概要
-
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
- z-axis 方向に 32* 3 スライスをランダムに取得
- オーバーラップしながら(隣接する3つのスライスを取得しつつ)データを作成する
- https://github.com/darraghdog/RSNA22/blob/main/data/ds_dh_seg_2C.py
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)に入力できる形式にしている
- こうすることで、本来は
の画像をCNNに 32回通して...ということをする必要があるが、うまいこと次元を扱うことで1回の fowardで済ませている(2, 3, 512, 512)
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
をうまいこと使っているのだな。「この次元にはこの意味をもたせる」くらいの軽い気持ちで(明示的に与えなくても)いいのだな
Discussion