effdetによるモデル学習中のエラーたち

[前回](https://qiita.com/nekot0/items/0d831c9fdf9e89c87f0d)、EfficientDet（effdet）を使って単純な例を実装できたことで、なんとなくコードを理解した。このコードを応用して[本題](https://www.kaggle.com/competitions/siim-covid19-detection/overview)に取り組んでいる。

ソースコードはまだ通して動くものではないが、これまでに遭遇したエラーとその対処についてメモしておく。



### numpy.ndarrayからtorch.tensorへの変換

クリティカルなエラーではないらしいが、numpy.ndarrayから直接torch.tensorに変換しようとすると警告が出る。変換の際、ndarrayの各要素ごとに変換を施すため、extremely slowになるらしい。今回の場合、numpy.arrayをリストに格納したものをtorch.tensorに変換しようとしたので警告された。事前にnumpy.arrayに変換してからtorch.tensorに変換するだけでよいらしい。

```
# エラーメッセージ
/opt/conda/lib/python3.7/site-packages/ipykernel_launcher.py:47: 
UserWarning: Creating a tensor from a list of numpy.ndarrays is extremely slow. Please consider converting the list to a single numpy.ndarray with numpy.array() before converting to a tensor. 
(Triggered internally at ../torch/csrc/utils/tensor_new.cpp:230.)
```


### 1枚の画像に対して複数のバウンディングボックスをもつ場合の処理

[こちらのコンペ](https://www.kaggle.com/competitions/siim-covid19-detection/overview)のデータセットでは、一枚の画像に対して複数のバウンディングボックスを持つ場合がある。さらに、画像によってバウンディングボックスの個数が異なっている。

1画像に対して1バウンディングボックスの場合、Datasetクラスからのバウンディングボックスの出力は下の（A）となるようにするが、1画像に対して複数バウンディングボックスの場合は（B）のようにする。
```
(A) 1画像に対して1バウンディングボックスの場合
[x0, y0, x1, y1]
# x0: 画像左上のx座標
# y0: 画像左上のy座標
# x1: 画像右下のx座標
# y1: 画像右下のy座標

(B) 1画像に対して複数バウンディングボックスの場合
[[x0, y0, x1, y1],   # バウンディングボックス1の座標
 [x0, y0, x1, y1],   # バウンディングボックス2の座標
 ...
 [x0, y0, x1, y1]]   # バウンディングボックスnの座標
```
つまり、1画像に対して複数バウンディングボックスをもち、さらに画像ごとにバウンディングボックスの数が異なる場合、Datasetクラスから出力されるバウンディングボックスのサイズが異なることになる。

この異なるサイズのテンソルをDataLoaderが読み取り、バッチとして出力しようとする際に下記のエラーが生じる。
```python
# DataLoaderまわりの定義
dataset = MyDataset(args)
   # 画像とバウンディングボックスを出力するためのtorch.utils.data.Datasetの子クラスのインスタンス
   # dataset.__getitem__()でこれらを出力するよう設定

dataloader = DataLoader(dataset, batch_size=4, num_workers=4)
   # バッチ処理をするためのtorch.utils.data.DataLoaderのインスタンス
```
```
# エラーメッセージ
RuntimeError: stack expects each tensor to be equal size, but got [2, 4] at entry 0 and [1, 4] at entry 1

```

このエラーはDataLoaderを作る際にcollate_fnを設定することで回避することができる。collate_fnはバッチ化の処理内容を定義することができる。ここでバウンディングボックスのサイズが異なる場合、サイズが小さいものには0をパディングすることですべてのサイズを合わせるようにする。以下はその例である。

```python
import torch.nn.functional as F
from torch.utils.data import default_collate

def pad_collate_fn(batch):
    # バウンディングボックスの最大数を調べる
    shapes = [item[1]['bbox'].shape[0] for item in batch]
    max_shape = max(shapes)
    
    padded_batch = []
    for x, y in batch:

        # バウンディングボックスをもたない画像を排除
        if any(elem == 0 for elem in y['cls']):
            continue

        # バウンディングボックス最大数に合わせて0パディング
        pad_size = max_shape - y['bbox'].shape[0]
        bbox_padding = [0, 0, 0, pad_size]
        cls_padding = [0, 0, 0, pad_size]
        padded_y = {
            'bbox': F.pad(y['bbox'], bbox_padding, mode='constant', value=0),
            'cls': F.pad(y['cls'].reshape((y['cls'].shape[0],1)), cls_padding, mode='constant', value=0)
        }
        padded_batch.append((x, padded_y))
    
    # 通常のバッチ処理を施して返す
    return default_collate(padded_batch)


# datasetとdataloaderのインスタンス化
dataset = MyDataset(args)
dataloader = DataLoader(
    dataset, batch_size=4, num_workers=4, 
    collate_fn=pad_collate_fn   # 上記のpad_collate_fnを組み込み
)
```



### 入力画像のチャネル数とデータ構造

通常、カラー画像はRGBの3チャネルもつのに対し、医療系でよく使われるdicom形式の画像は白黒の1チャネルの場合が多い。effdetでは入力チャネルが3に固定されているので、1チャネルしかない場合は3チャネルへの拡張が必要である。これはopencvの機能を使うことで対処した。

```python
import pydicom
import cv2

dcm = pydicom.dcmread(dcm_path)
image = dcm.pixel_array.astype("float32")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
```

なお、画像データの構造には注意しておきたい。上記の方法では、拡張後の画像は(Width, Height, Channel)のインデックスとなる。これはのちの処理、例えばAlbumentationsで画像を変換する場合には問題ない。しかしデータを読み込むの際にopencvを利用した場合、データは(Height, Width, Channel)の順で格納される（らしい）。画像のデータ構造と後の処理で必要とされるデータ構造が異なると、思わぬエラーが発生するため注意しておきたい。

(4/5修正)
上記の方法で得られたチャネル拡張後のインデックスは(Height, Width, Channel)の順でした。これにバウンディングボックスを(x0, y0, x1, y1)の形式で入力するとうまく動きました。



### effdet内、anchors.pyで生じたエラー

datasetやdataloaderの問題が解決した後、モデルに学習用のデータを入力した際に発生したエラー。マスクの形状が合わないとのことだが、エラーメッセージに表示されるサイズに全く見覚えがない。

```
# エラーメッセージ
---------------------------------------------------------------------------
IndexErrorTraceback (most recent call last)
/tmp/ipykernel_27/103307951.pyin <module>
     28forinputs,targets int:
     29optimizer.zero_grad()
---> 30         losses =bench(inputs,targets)
     31loss =losses['loss']
     32loss.backward()

/opt/conda/lib/python3.7/site-packages/torch/nn/modules/module.pyin _call_impl(self, *input, **kwargs)
   1188if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1189or _global_forward_hooks or _global_forward_pre_hooks):
-> 1190             returnforward_call(*input,**kwargs)   1191# Do not call functions when jit is used
   1192full_backward_hooks,non_full_backward_hooks =[],[]

/kaggle/input/effdet-030-package-dataset/packages/effdet/bench.pyin forward(self, x, target)
    140else:    141cls_targets, box_targets, num_positives = self.anchor_labeler.batch_label_anchors(
--> 142                 target['bbox'], target['cls'])
    143
    144loss,class_loss,box_loss =self.loss_fn(class_out,box_out,cls_targets,box_targets,num_positives)

/kaggle/input/effdet-030-package-dataset/packages/effdet/anchors.pyin batch_label_anchors(self, gt_boxes, gt_classes, filter_valid)
    376iffilter_valid:
    377valid_idx =gt_classes[i]>-1# filter gt targets w/ label <= -1
--> 378                 gt_box_list =BoxList(gt_boxes[i][valid_idx])
    379gt_class_i =gt_classes[i][valid_idx]
    380else:

IndexError: The shape of the mask [2, 1] at index 1 does not match the shape of the indexed tensor [2, 4] at index 1

```


該当箇所である'anchors.py'にコメントをはさみながら原因を探ったところ、バッチを受け取ったモデルは負の値のラベルとそれに対応するバウンディングボックスを省く処理をしていた。この処理の仕方が複数バウンディングボックスにうまく適用されないことが原因のようだ。この処理を書き直すことでエラーは解消された。

```python
# 元のファイル 378行目
gt_box_list = BoxList(gt_boxes[i][valid_idx])

# gt_boxesが同じバッチに含まれるバウンディングボックスデータ
# 例えば
# gt_boxes:
#    tensor([[[17.1509, 58.3014, 51.9944, 78.0274],
#             [ 6.3188, 22.3562, 27.2609, 40.9863],
#             [38.4542, 19.5068, 53.6192, 37.4795]],
#            [[ 0.0000, 11.3287, 32.6629, 27.3655],
#             [ 0.0000,  0.0000,  0.0000,  0.0000],
#             [ 0.0000,  0.0000,  0.0000,  0.0000]], 
#            [[25.7564, 29.2258, 51.8807, 42.6300],
#             [34.2192, 82.6232, 57.5839, 96.0275],
#             [ 0.0000,  0.0000,  0.0000,  0.0000]]])
# この場合、gt_boxes[2]には2番目の画像に対応したバウンディングボックスデータが格納されている。
# gt_boxes[2]:
#    tensor([[25.7564, 29.2258, 51.8807, 42.6300],
#            [34.2192, 82.6232, 57.5839, 96.0275],
#            [ 0.0000,  0.0000,  0.0000,  0.0000]]]
#
# valid_idxは各画像に対し、何番目のバウンディングボックスが意味のあるものかを示すマスク。
# 上記の2番目の画像においては1, 2行目のラベルは1であるが、3行目は0であり意味がないので省かれる。
# valid_idx: tensor([[True], [True], [False]])
```

```python
# 書き直したファイル
gt_boxes_output = []
    for j in range(valid_idx.shape[0]):
        if valid_idx[j]: 
            gt_boxes_output.append(np.array(gt_boxes[i][j]))
gt_box_list = torch.FloatTensor(np.array(gt_boxes_output))
gt_box_list = BoxList(gt_box_list)
```



<br>

プログラムはまだ全体を通して動いていないので、エラーとの格闘はまだ続く。