Dezeroでセマンティックセグメンテーション

## 0.はじめに
読んでから数ヶ月経つと『ゼロから作るDeep Learning 3』の内容も頭から綺麗サッパリ消え去りました。
というわけで復習を兼ねて、DeZeroでセマンティックセグメンテーションをしてみたいと思います。

https://github.com/oreilly-japan/deep-learning-from-scratch-3

windows10/gpuはgeforce gtx1060(6G)

使用しているのはこちら
python 3.7.13
DeZero 0.0.13
numpy 1.21.5
cupy 6.0.0
pillow 9.0.1
matplotlib 3.5.1

今回のnotebookは以下のリンク先で見られます。
https://github.com/takanashiyoruyoru/dezero_sample/blob/main/dezero_Semantic_Segmentation.ipynb

## 1. DeZeroの機能拡張

まずはDeZeroにないupsamplingとconcatの機能を作成します。

upsamplingは、「N * C * X * Y 」のデータを「N * C * 2X * 2Y 」にして、解像度を2倍にします。
forwardのときは、X軸, Y軸方向に2回、同じデータを繰り返します。
backwardのときは、4つの塊ごとに合計します。
![プレゼンテーション1_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141993/42e18985-0da0-087e-a5b8-1d4d7e26aa51.jpeg)

```python
class Up(Function):
    def __init__(self):
        super().__init__()
      
    def forward(self, x):
        return x.repeat(2, axis=2).repeat(2, axis=3)
    
    def backward(self, gy):
        return gy[:, :, ::2, ::2] + gy[:, :, 1::2, ::2]\
                + gy[:, :, ::2, 1::2] + gy[:, :, 1::2, 1::2]
```
forwardではcupy/numpyのrepeatを使用しています。
backwardではスライス機能で表現しています。
このUpのクラスでは解像度2倍にする機能しかありませんが、今回はこれで事足ります。


次にconcatでは, 「N * C1 * X * Y 」のデータと「N * C2 * X * Y 」のデータを組み合わせて「N * (C1 + C2) * X * Y 」にします。

```python
class Concat(Function):
    def forward(self, x1, x2):
        xp = cuda.get_array_module(x1)
        #self.x1_len = xp.shape(x1)[1]
        self.x1_len = x1.shape[1]
        return xp.concatenate([x1, x2], axis=1)
  
    def backward(self, gy):
        gy_1 = gy[:, :self.x1_len, :, :]
        gy_2 = gy[:, self.x1_len:, :, :]
        return gy_1, gy_2


def concat(x1, x2):
    """
    Join two Variables along axis 1.
    The shapes of x1 and x2 must be the same, except in the axis 1 direction.
  
    Parameters
    ----------
    x1 : Variable
    x2 : Variable
    
    The axis along which the Variables will be joined. 
    """
    return Concat()(x1, x2)
```

forwardのときに, C1の大きさ(x1.shape[1])を記憶しておくのがポイントです。


## 2. ネットワークの作成

ここではU-net風のネットワークを組んでみます。
U-netについては様々な文献で多くが語られていると思うので、あまり説明はしません。
今回は爪 or 爪でない を判断するネットワークを作成したいので、最後はsigmoid関数です。

```python
class Net(Model):
    def __init__(self):
        super().__init__()
        self.conv_0 = L.Conv2d(32, kernel_size=3, stride=1, pad=1)
        self.conv_01 = L.Conv2d(32, kernel_size=3, stride=1, pad=1)
        self.conv_1 = L.Conv2d(64, kernel_size=5, stride=1, pad=2)
        self.conv_2 = L.Conv2d(128, kernel_size=5, stride=1, pad=2)
        self.conv_3 = L.Conv2d(128, kernel_size=5, stride=1, pad=2)
        self.conv_4 = L.Conv2d(128, kernel_size=3, stride=1, pad=1)
        self.conv_5 = L.Conv2d(64, kernel_size=3, stride=1, pad=1)
        self.conv_6 = L.Conv2d(32, kernel_size=3, stride=1, pad=1)
        self.conv_head0 = L.Conv2d(32, kernel_size=3, stride=1, pad=1)
        self.conv_head1 = L.Conv2d(1, kernel_size=3, stride=1, pad=1)
        
        self.up4 = Up()
        self.up5 = Up()
        self.up6 = Up()
        
        self.bn_0 = L.BatchNorm()
        self.bn_01 = L.BatchNorm()
        self.bn_1 = L.BatchNorm()
        self.bn_2 =  L.BatchNorm()
        self.bn_3 =  L.BatchNorm()
        self.bn_4 =  L.BatchNorm()
        self.bn_5 =  L.BatchNorm()
        self.bn_6 =  L.BatchNorm()
        self.bn_head =  L.BatchNorm()
    
    def forward(self, x0):
        x0 = F.relu(self.bn_0(self.conv_0(x0)))
        x0 = F.relu(self.bn_01(self.conv_01(x0)))    
        x1 = F.relu(self.bn_1(self.conv_1(F.pooling(x0, 2, 2))))
        x2 = F.relu(self.bn_2(self.conv_2(F.pooling(x1, 2, 2))))
        x3 = F.relu(self.bn_3(self.conv_3(F.pooling(x2, 2, 2))))
        x4 = F.relu(self.bn_4(self.conv_4(self.up4(x3))))
        x4 = concat(x2, x4)
        x5 = F.relu(self.bn_5(self.conv_5(self.up5(x4))))
        x5 = concat(x1, x5)
        x6 = F.relu(self.bn_6(self.conv_6(self.up6(x5))))
        x6 = concat(x0, x6)
        x = F.relu(self.bn_head(self.conv_head0(x6)))
        x = F.sigmoid(self.conv_head1(x))
        return x
```


## 3.学習

データセットとデータローダーを作成します。

trainがtrueのときには、自作の関数でデータオーギュメンテーションするようにします。

```python
class MyDataset:
    def __init__(self, img_path, mask_path, train=False, size=192):
        self.img_path = img_path
        self.mask_path = mask_path
        self.train = train
        self.size = size

    def __getitem__(self, index):
        #open two images, PIL
        img = Image.open(self.img_path[index]).convert('RGB')
        mask_img = Image.open(self.mask_path[index]).convert(mode="L")
        
        if self.train:
            img = random_gaussian_blur(img, p=0.4)
            img = random_brightness(img, p=1.)
            img = random_enhance(img, p=0.8)
            img = random_contrast(img, p=0.8)
        
        img = img.resize((self.size, self.size))
        img = np.array(img)

        mask_img = mask_img.resize((self.size, self.size))
        mask_img = np.array(mask_img)
        
        if self.train:
            img, mask_img = random_horizontal_flip(img, mask_img, p=0.5)
            img, mask_img = random_vertical_flip(img, mask_img, p=0.5)
    
        #convert ndarray, channel first,  0 to 1
        img = img.transpose((2, 0, 1))
        img = img / 255.
        mask_img = np.where(mask_img > 0, 1.0, 0.0)
        mask_img = mask_img.reshape(1, self.size, self.size)
        return img, mask_img
    
    def __len__(self):
        return len(self.img_path)
```


```python
np.random.seed(42)

img_path = np.array(glob.glob("nailseg/images/*"), dtype=object)

random_nums = np.random.rand(len(img_path))
img_train_path = img_path[random_nums < 0.9]
img_test_path = img_path[random_nums > 0.9]

mask_train_path = [p.replace("images", "labels") for p in img_train_path]
mask_test_path = [p.replace("images", "labels") for p in img_test_path]

train_set = MyDataset(img_train_path, mask_train_path, train=True, size=128)
train_loader = DataLoader(train_set, 4, shuffle=True)

test_set = MyDataset(img_test_path, mask_test_path, train=False, size=128)
test_loader = DataLoader(test_set, 3, shuffle=False)
```

画像はimg_train_pathに42枚, img_test_pathに6枚が配分されるようになっています。




``` python
train_losses = []
test_losses = []

net = Net()

net.to_gpu()
train_loader.to_gpu()
test_loader.to_gpu()

optimizer = dezero.optimizers.Adam(alpha=0.0002).setup(net)

max_epoch = 100
for epoch in range(max_epoch):
    #Initialize the tmp_loss
    tmp_loss = 0.0
    
    for x, y in train_loader:
        y_pred = net(x)
        loss = F.binary_cross_entropy(y_pred, y)
        
        net.cleargrads()
        loss.backward()
        optimizer.update()

        tmp_loss += float(loss.data) * len(y)
    train_losses.append(tmp_loss / (len(train_set)))
    
    tmp_loss = 0.0
    for x, y in test_loader:
        with dezero.test_mode():
            y_pred = net(x)
            loss = F.binary_cross_entropy(y_pred, y)
        tmp_loss += float(loss.data) * len(y)
    test_losses.append(tmp_loss / (len(test_set)))
    
    print(epoch, ": ", train_losses[epoch]," test:", test_losses[epoch])
```


## 4.結果

エポック毎のlossの結果はこちら
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141993/e6c7ed1b-78eb-e0d2-887a-4775c00ac746.png)

実際のtest画像の推論結果(100 epoch終了後)
左が元画像, 真ん中が推論結果, 右が正解
![res.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141993/2d10dc65-5067-e78c-e0f7-b99586c0d465.png)

流石に学習画像42枚はキツいが、それなりに認識出来ている様子


## 終わりに

DeZeroを使うと結構簡単に追加で機能作れてすごくべん....

な訳はなく、実用的にはpytorch使った方がよい(確実に)。
それでも色々と作れると楽しい!!