Resnetを解説，実装する！

# はじめに
今回はResnet [He et al.,2016] を解説・実装していこうと思います！
元論文はこちら
[Deep Residual Learning for Image Recognition](https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf)
被引用数：199975（とんでもないですねw）

Resnet内部における恒等写像に関する解析論文
[Identity Mappings in Deep Residual Networks](https://arxiv.org/abs/1603.05027)

# 解説編
（できる限り論文を引用して解説していきます，訳などに問題があればご指摘ください．）

まずは，こちらをご覧くださいませませ．

![resnet1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/103de926-365e-f1b4-a3c3-8791c963c1c5.png)

NNの層を深くすれば良いモデルになりそうですが，実際に学習させてみるとそのようにならないわけですね．（画像では，２０層の方が良い精度を出している）
これを`degradation`というらしいです．（日本語だとモデル劣化？と訳せば良いですかね．勾配消失問題などを含めた一般的な話ですかね？）

じゃあ！ということで筆者たちは，`Let us consider a shallower architecture and its deeper counterpart that adds more layers onto it.There exists a solution by construction to the deeper model:the added layers are identity mapping, and the other layers are copied from the learned shallower model`と，浅いモデルに`恒等写像`を追加して深いモデルを構築したら，浅いモデルと同等の精度が深いモデルで出せるのでは？と考えたわけです．

しかしながら，`But experiments show that our current solvers on hand are unable to find solutions that are comparably good or better than the constructed solution(or unable to do so in feasible time)`だったそうで，思った通りの結果が得られなかったそうです．

これの原因が`恒等写像`を正しく学習できていないとして，今回のResnetを提案するに至ったらしいです．

## Residual Blockとは
まずは，Residual Block（以下，ResBlock）アーキテクチャ図から．
![resnet2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/07995c57-600a-c505-6878-c5088d9a6864.png)

ここでいう，$\mathcal{F}(\mathbf{x})$が`ResBlock`です．

筆者らは，`To the extreme, if an identity mapping were optimal, it would be easier to push the residual to zero than to fit an identity mapping by a stack of nonlinear layers.`と，このResBlockの存在によって，恒等写像を非線形変換の積み重ねで学習するよりも残差ブロックを０にする方が簡単になるでしょうとのこと．

だから，このResBlockの導入によって恒等写像の学習が楽になるそうです．

さらに，`Our deep residual nets can easily enjoy accuracy gains from greatly increased depth,producing results substantially better than previous networks`
らしく，このResBlockを積み重ねてDeepにする（下図参照）と精度が上がったらしいです．
![resnet4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/5f989811-a030-e981-99e9-baf7697fe614.png)

## 疑問
個人的には，精度が上がる点が結構疑問ポイントでして。。。
学習の結果，下図のように
<img width="250" alt="mojikyo45_640-2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/d3f1fc1f-16cc-9628-5211-aeb0a64575ad.png">
浅いネットワークに恒等写像が積み重なったモデルになるのではないか？と思ったわけです．
これだと，浅いネットワークと表現力が変わらないんじゃ無いかと．
すなわち，昨今のResnetの精度に説明がつかない気がしました．

ですが[こちら](https://ai.stackexchange.com/questions/15743/what-is-the-benefit-of-using-identity-mapping-layers-in-deep-neural-networks-lik)にあるように，ResBlockによって恒等写像の学習が`"楽になる"`だけで，厳密にResBlockを用いて恒等写像を学習していないとすれば，多少説明がつく気がします．
つまり，どっかのResBlock+skip connection部分で恒等写像を学習して，どっか別の部分では普通に表現を学習しているのかなと．
そして，`Our derivation simply that identity shortcut connections and identity after-addition activation are essential for making information propagation smooth.`とResnetの解析論文にあるように，このどっかの恒等写像が勾配消失問題に寄与しているから，精度が上がっているのかなと．
この辺は要調査・検証が必要ですね（力尽きました笑）．

## 解説まとめ
いずれにせよ．
モデル劣化問題に対してResBlockを導入することで，恒等写像の学習を楽にし，勾配消失問題が解決され精度を上げることに成功したらしいです．

以上，論文の引用を元に自分なりの解釈を交えつつ解説してみました．
何かあればぜひコメントいただきたいです．

# 実装編
コーディングはあまり得意ではないので、良い実装にはなっていないかもしれませんがご了承ください、、、
もしよければpytorchが出してる，[こちらのコード](https://github.com/pytorch/vision/blob/main/torchvision/models/resnet.py)を参照してみると良いかもです．
ひとまず，`BottleNeckBlock`やバッチ正規化を含まないResnetを実装します．
下表中，18-layerと書いてあるresnet18を実装します．
![resnet5_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/2a0a5adf-ba8f-938a-8eb3-ca7faf81ce43.png)

この表はアーキテクチャを表しているのですが，如何せん行間が省かれていてこのまま素朴に実装はできません。。。（例えば，活性化関数やpaddingの値は省かれています）
一応，Resnet論文が[VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION](https://arxiv.org/abs/1409.1556)の論文で提案されたモデルを参考にしているので，こちらを読むと行間が埋まるはずです．
適宜，補足しながら実装していきます．

## conv1層
![resnet6_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/805f7005-f2fc-64f0-84bd-e1c4f941f8b4.png)

まず，conv1層から．
入力画像の次元を`3チャネル，224×224pixel`とします．
ここで，conv1によって`3チャネル，224×224`→7×7のカーネルとストライド数：２により，`64チャネル，112×112`になります．（この時paddingを3にすることで，画像サイズを224→112にできます）
これに注意して，次のように実装できます．

```python:resnet.py
import torch
import torch.nn as nn
import torch.nn.functional as F

class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)
    
    def forward(self, x):
        # conv1
        out = self.conv1(x)
        out = F.relu(x) #活性化関数
        print("===conv1（conv 7*7, stride:2, padding:3）===")
        print(" ", out.size(),"\n")
        
        return out
        
if __name__ == "__main__":
    # test
    x = torch.randn(3, 224, 224)
    model = ResNet18(in_channels=3, num_classes=1000)
    print("===input size===")
    print(" ",x.size())
    model(x)
```

```zsh
>>python resnet.py
===input size===
  torch.Size([3, 224, 224])
===conv1（conv 7*7, stride:2, padding:3）===
  torch.Size([64, 112, 112]) 
```

## conv2_x（maxpooling層）
![resnet6_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/622f1ccf-772b-ea55-58f3-f7731e4ca65f.png)

続いて，conv2_xのmaxpooling層を実装します．
`conv1`での出力`[64, 112, 112]`を`[64, 56, 56]`に変換します．
この時Maxpoolingを行い，その際のカーネルサイズを`3×3`，ストライドを2とします．
この元でpaddingを1とすることで，画像サイズを半分の`56×56`にできます．
以下実装．
```python:resnet.py
class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)

        # conv2_x(maxpool)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
    
    def forward(self, x):
        # conv1
        out = self.conv1(x)
        out = F.relu(x) #活性化関数
        print("===conv1（conv 7*7, stride:2, padding:3）===")
        print(" ", out.size(),"\n")

        # conv2_x(maxpool)
        out = self.maxpool(out)
        out = F.relu(out)
        print("===maxpool（maxpool 3*3, stride:2, padding:1）===")
        print(" ",out.size(),"\n")
        
        return out      
if __name__ == "__main__":
    # test
    x = torch.randn(3, 224, 224)
    model = ResNet18(in_channels=3, num_classes=1000)
    print("===input size===")
    print(" ",x.size())
    model(x)
```

```zsh
>>python resnet.py
===input size===
  torch.Size([3, 224, 224])
===conv1（conv 7*7, stride:2, padding:3）===
  torch.Size([64, 112, 112]) 

===maxpool（maxpool 3*3, stride:2, padding:1）===
  torch.Size([64, 56, 56]) 
```

## conv2_x（ResBlock）
![resnet6_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/13502fb3-049e-414b-abf6-c24eee859212.png)

さて，ここからResBlockの実装を行います．が，ここで表中の`conv2_x`の添字`x`について簡単に補足しておくと，これは層の番号を表しています．
さらに表を補足します．上表の赤枠部分は
```math
\begin{bmatrix}
    3 \times 3, \,64\\
    3 \times 3, \,64
\end{bmatrix}\times 2
```
のようになっています．これは，
```math
3 \times 3, \,64
```
が一つの畳み込み層を表し，$3\times3$のカーネルを64枚用いて64チャネルの出力をする畳み込み層であることを表しています．
そして，
```math
\begin{bmatrix}
    3 \times 3, \,64\\
    3 \times 3, \,64
\end{bmatrix}
```
は，そのような畳み込み層が２つ組み込まれたResBlockを表します．
さらに，$\times 2$はこのResBlockが２つあることを表しています．
このことを図示すると下の図のようになります（skip connectionと活性化関数もついでに書いています）．
かなり雑な図ですみません。

![resnet7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/0664403d-5336-7efa-c059-d4fc4da0b401.png)

なお，論文中でこのようなResBlockがBuildingBlockと呼ばれていたので，それに準えて実装におけるクラス名はそのようになっています．
さて，これを元にBuildingBlockを実装します．
```python:resnet.py
class BuildingBlock(nn.Module):
    """
    H(x) = BuilidingBlock(x) + x
    """
    def __init__(self, in_channels, med_channels, out_channels, is_downsample=False):
        super().__init__()
        # 一旦このif文部分は無視で．
        if is_downsample == True:
            stride = 2
        else:
            stride = 1
        self.m_1 = nn.Conv2d(in_channels, med_channels, kernel_size=3, stride=stride, padding=1)
        self.m_2 = nn.Conv2d(med_channels, out_channels, kernel_size=3, stride=1, padding=1)

    def forward(self, x):
        out = self.m_1(x)
        out = F.relu(out)
        out = self.m_2(out)

        return out

class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        self.num_classes = num_classes
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)

        # conv2_x(maxpool)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # conv2_x
        self.resblock2_1 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)
        self.resblock2_2 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)

    def forward(self, x):
        # conv1
        out = self.conv1(x)
        out = F.relu(x) #活性化関数
        print("===conv1（conv 7*7, stride:2, padding:3）===")
        print(" ", out.size(),"\n")

        # conv2_x(maxpool)
        out = self.maxpool(out)
        out = F.relu(out)
        print("===maxpool（maxpool 3*3, stride:2, padding:1）===")
        print(" ",out.size(),"\n")

        # conv2_x 
        out = self.resblock2_1(out) + out
        out = F.relu(out)
        print("===conv2_1（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock2_2(out) + out
        out = F.relu(out)
        print("===conv2-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")
        
        return out
    if __name__ == "__main__":
    # test
    x = torch.randn(3, 224, 224)
    model = ResNet18(in_channels=3, num_classes=1000)
    print("===input size===")
    print(" ",x.size())
    model(x)
```

```zsh
===input size===
  torch.Size([3, 224, 224])
===conv1（conv 7*7, stride:2, padding:3）===
  torch.Size([64, 112, 112]) 

===maxpool（maxpool 3*3, stride:2, padding:1）===
  torch.Size([64, 56, 56]) 

===conv2_1（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv2-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 
```

## conv3_x
続いて，`conv3_x`層です．
ここが終われば残りの出力層直前までは同じブロックの繰り返しです．
![resnet6_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/8223596d-879a-76e9-20cc-084be8d13f64.png)
ここで，表中の`output size`とチャネル数に注目してください．
[64, 56, 56]から[128, 28, 28]に変わっていますね．
どこで変わったかというと，表のキャプションに`Downsampling is performed by conv3_1, conv4_1,and conv5_1 with a stride of 2.`と書いてある通り，conv3_1で変わっています．
そこで，下図のようにチャネル数を2倍に，ストライドを2にすることによってデータサイズを半分にします．
![resnet8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/f713068a-8d0d-f589-0f3c-ea5f813cfa66.png)

しかし，さらに問題が．
下図をご覧下さい．
![resnet9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/853fe6d5-58c0-0056-8e76-57281e245ea4.png)

そう．サイズが違うので足せないんですよ．
そこで，入力の$x$を
![resnet10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/2d30d813-bbdf-677f-8e79-32c09040429c.png)

と変換します（論文中Eqn.(2)に相当）．
論文では，`We also note that although the above notations are about fully-connected layers for simplicity, they are applicable to convolutional layers.`と書いてあり，簡単のため全結合っぽく書いているけど，畳み込み演算による変換でも良い（適用可能だ）よと言ってます．

具体的には，`(A)The shortcut still performs identity mapping,with extra zero entries padded for increasing dimensions.This option introduces no extra parameter;(B)The projection shortcut in Eqn.(2)is used to match dimensions (done by 1 × 1 convolutions). For both options,when the shortcuts go across feature maps of two sizes,they are performed with a stride of 2.`だそうなので，`1×1`のカーネルでチャネル数を増やし，strideを２にすることで画像サイズを半分にして調整します．
よーするに，
![resnet11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/aafda823-34ab-ae54-b6af-6af8a47a7796.png)
ということです．なお，論文ではこのサイズ調整についても実験をしていますので，よければご参照ください．

てことで，実装．
```python:resnet.py
class BuildingBlock(nn.Module):
    """
    H(x) = BuildingBlock(x) + x
    """
    def __init__(self, in_channels, med_channels, out_channels, is_downsample=False):
        super().__init__()
        #　先ほど無視したif文で，ダウンサンプリングが必要な場合はstrideを2にしています．
        if is_downsample == True:
            stride = 2
        else:
            stride = 1
        self.m_1 = nn.Conv2d(in_channels, med_channels, kernel_size=3, stride=stride, padding=1)
        self.m_2 = nn.Conv2d(med_channels, out_channels, kernel_size=3, stride=1, padding=1)

    def forward(self, x):
        out = self.m_1(x)
        out = F.relu(out)
        out = self.m_2(out)

        return out

class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        self.num_classes = num_classes
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)

        # conv2_x(maxpool)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # conv2_x
        self.resblock2_1 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)
        self.resblock2_2 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)

        # conv3_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock3_1 = BuildingBlock(in_channels=64, med_channels=128, out_channels=128, is_downsample=True)
        self.resblock3_2 = BuildingBlock(in_channels=128, med_channels=128, out_channels=128)

 def conv11(self, in_channels, out_channels):
        """
        入力xの調整用
        if the feature map size is halved, the number of filters is doubled so as to preserve the time complexity per layer.
        だそうなので，画像のサイズが1/2になり，フィルタの数が２倍になっていく．
        そのため，画像のサイズを調整し，チャンネル数も調整する必要がある．
        画像のサイズはstrideを２にすることで，1/2に，チャンネル数は1*1のカーネルをin_channelsの２倍用いて，畳み込みをすればよい．
        """
        return nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=2, bias=False)

    def forward(self, x):
        # conv
        out = self.conv1(x)
        out = F.relu(out)
        print("===conv1（conv 7*7, stride:2, padding:3）===")
        print(" ", out.size(),"\n")
        # maxpool
        out = self.maxpool(out)
        out = F.relu(out)
        print("===maxpool（maxpool 3*3, stride:2, padding:1）===")
        print(" ",out.size(),"\n")

        # conv2_x
        out = self.resblock2_1(out) + out
        out = F.relu(out)
        print("===conv2_1（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock2_2(out) + out
        out = F.relu(out)
        print("===conv2-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv3_x
        # resblock3_1で入力画像のサイズ調整
        out = self.resblock3_1(out) + self.conv11(64, 128)(out)
        out = F.relu(out)
        print("===conv3-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock3_2(out) + out
        out = F.relu(out)
        print("===conv3-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")
        return out
if __name__ == "__main__":
    # test
    x = torch.randn(3, 224, 224)
    model = ResNet18(in_channels=3, num_classes=1000)
    print("===input size===")
    print(" ",x.size())
    model(x)
```
```zsh
===input size===
  torch.Size([3, 224, 224])
===conv1（conv 7*7, stride:2, padding:3）===
  torch.Size([64, 112, 112]) 

===maxpool（maxpool 3*3, stride:2, padding:1）===
  torch.Size([64, 56, 56]) 

===conv2_1（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv2-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv3-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([128, 28, 28]) 

===conv3-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([128, 28, 28]) 
```

## 以降のconv4,5_x層
conv3_xとほぼ同様なので割愛します．
以下実装．
```python:resnet.py
class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        self.num_classes = num_classes
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)

        # conv2_x(maxpool)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # conv2_x
        self.resblock2_1 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)
        self.resblock2_2 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)

        # conv3_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock3_1 = BuildingBlock(in_channels=64, med_channels=128, out_channels=128, is_downsample=True)
        self.resblock3_2 = BuildingBlock(in_channels=128, med_channels=128, out_channels=128)

        # conv4_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock4_1 = BuildingBlock(in_channels=128, med_channels=256, out_channels=256, is_downsample=True)
        self.resblock4_2 = BuildingBlock(in_channels=256, med_channels=256, out_channels=256)

        # conv5_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock5_1 = BuildingBlock(in_channels=256, med_channels=512, out_channels=512, is_downsample=True)
        self.resblock5_2 = BuildingBlock(in_channels=512, med_channels=512, out_channels=512)

        # 出力のサイズを指定してAvgPooling
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))

        # fully connectionによって変換
        self.fc = nn.Linear(512, self.num_classes)
    def conv11(self, in_channels, out_channels):
        """
        if the feature map size is halved, the number of filters is doubled so as to preserve the time complexity per layer.
        だそうなので，画像のサイズが1/2になり，フィルタの数が２倍になっていく．
        そのため，画像のサイズを調整し，チャンネル数も調整する必要がある．
        画像のサイズはstrideを２にすることで，1/2に，チャンネル数は1*1のカーネルをin_channelsの２倍用いて，畳み込みをすればよい．
        """
        return nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=2, bias=False)
    
    def forward(self, x):
        # conv
        out = self.conv1(x)
        out = F.relu(out)
        print("===conv1（conv 7*7, stride:2, padding:3）===")
        print(" ", out.size(),"\n")
        # maxpool
        out = self.maxpool(out)
        out = F.relu(out)
        print("===maxpool（maxpool 3*3, stride:2, padding:1）===")
        print(" ",out.size(),"\n")

        # conv2_x
        out = self.resblock2_1(out) + out
        out = F.relu(out)
        print("===conv2_1（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock2_2(out) + out
        out = F.relu(out)
        print("===conv2-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv3_x
        out = self.resblock3_1(out) + self.conv11(64, 128)(out)
        out = F.relu(out)
        print("===conv3-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock3_2(out) + out
        out = F.relu(out)
        print("===conv3-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv4_x
        out = self.resblock4_1(out) + self.conv11(128, 256)(out)
        out = F.relu(out)
        print("===conv4-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock4_2(out) + out
        out = F.relu(out)
        print("===conv4-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv5_x
        out = self.resblock5_1(out) + self.conv11(256, 512)(out)
        out = F.relu(out)
        print("===conv5-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock5_2(out) + out
        out = F.relu(out)
        print("===conv5-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        return out
if __name__ == "__main__":
    # test
    x = torch.randn(3, 224, 224)
    model = ResNet18(in_channels=3, num_classes=1000)
    print("===input size===")
    print(" ",x.size())
    model(x)
```
```zsh
===input size===
  torch.Size([3, 224, 224])
===conv1（conv 7*7, stride:2, padding:3）===
  torch.Size([64, 112, 112]) 

===maxpool（maxpool 3*3, stride:2, padding:1）===
  torch.Size([64, 56, 56]) 

===conv2_1（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv2-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv3-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([128, 28, 28]) 

===conv3-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([128, 28, 28]) 

===conv4-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([256, 14, 14]) 

===conv4-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([256, 14, 14]) 

===conv5-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([512, 7, 7]) 

===conv5-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([512, 7, 7]) 
```
## 出力層
出力層では，average poolingによって`1 × 1`まで画像サイズを落とし，１次元化した後に全結合をして，クラスラベルの数まで次元を変換します．

```python:resnet.py
class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        self.num_classes = num_classes
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)

        # conv2_x(maxpool)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # conv2_x
        self.resblock2_1 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)
        self.resblock2_2 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)

        # conv3_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock3_1 = BuildingBlock(in_channels=64, med_channels=128, out_channels=128, is_downsample=True)
        self.resblock3_2 = BuildingBlock(in_channels=128, med_channels=128, out_channels=128)

        # conv4_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock4_1 = BuildingBlock(in_channels=128, med_channels=256, out_channels=256, is_downsample=True)
        self.resblock4_2 = BuildingBlock(in_channels=256, med_channels=256, out_channels=256)

        # conv5_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock5_1 = BuildingBlock(in_channels=256, med_channels=512, out_channels=512, is_downsample=True)
        self.resblock5_2 = BuildingBlock(in_channels=512, med_channels=512, out_channels=512)

        # 出力のサイズを指定してAvgPooling
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))

        # fully connectionによって変換
        self.fc = nn.Linear(512, self.num_classes)
    def conv11(self, in_channels, out_channels):
        """
        if the feature map size is halved, the number of filters is doubled so as to preserve the time complexity per layer.
        だそうなので，画像のサイズが1/2になり，フィルタの数が２倍になっていく．
        そのため，画像のサイズを調整し，チャンネル数も調整する必要がある．
        画像のサイズはstrideを２にすることで，1/2に，チャンネル数は1*1のカーネルをin_channelsの２倍用いて，畳み込みをすればよい．
        """
        return nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=2, bias=False)
    
    def forward(self, x):
        # conv
        out = self.conv1(x)
        out = F.relu(out)
        print("===conv1（conv 7*7, stride:2, padding:3）===")
        print(" ", out.size(),"\n")
        # maxpool
        out = self.maxpool(out)
        print("===maxpool（maxpool 3*3, stride:2, padding:1）===")
        print(" ",out.size(),"\n")

        # conv2_x
        out = self.resblock2_1(out) + out
        out = F.relu(out)
        print("===conv2_1（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock2_2(out) + out
        out = F.relu(out)
        print("===conv2-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv3_x
        out = self.resblock3_1(out) + self.conv11(64, 128)(out)
        out = F.relu(out)
        print("===conv3-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock3_2(out) + out
        out = F.relu(out)
        print("===conv3-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv4_x
        out = self.resblock4_1(out) + self.conv11(128, 256)(out)
        out = F.relu(out)
        print("===conv4-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock4_2(out) + out
        out = F.relu(out)
        print("===conv4-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # conv5_x
        out = self.resblock5_1(out) + self.conv11(256, 512)(out)
        out = F.relu(out)
        print("===conv5-1（residual Block 3*3, stride:2, padding=1）===")
        print(" ",out.size(),"\n")
        out = self.resblock5_2(out) + out
        out = F.relu(out)
        print("===conv5-2（residual Block 3*3, stride:1, padding=1）===")
        print(" ",out.size(),"\n")

        # avgpool
        out = self.avgpool(out)
        out = F.relu(out)
        print("===avgpool===")
        print(" ",out.size())

        # fully connection
        out = self.fc(out.flatten())p
        print("===fully connection===")
        print(" ",out.size())
        
        # 必要に応じてsoftmax

        return out
if __name__ == "__main__":
    # test
    x = torch.randn(3, 224, 224)
    model = ResNet18(in_channels=3, num_classes=1000)
    print("===input size===")
    print(" ",x.size())
    model(x)
```
```zsh
===input size===
  torch.Size([3, 224, 224])
===conv1（conv 7*7, stride:2, padding:3）===
  torch.Size([64, 112, 112]) 

===maxpool（maxpool 3*3, stride:2, padding:1）===
  torch.Size([64, 56, 56]) 

===conv2_1（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv2-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([64, 56, 56]) 

===conv3-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([128, 28, 28]) 

===conv3-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([128, 28, 28]) 

===conv4-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([256, 14, 14]) 

===conv4-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([256, 14, 14]) 

===conv5-1（residual Block 3*3, stride:2, padding=1）===
  torch.Size([512, 7, 7]) 

===conv5-2（residual Block 3*3, stride:1, padding=1）===
  torch.Size([512, 7, 7]) 

===avgpool===
  torch.Size([512, 1, 1])
===fully connection===
  torch.Size([1000])
```

これでresnet18の実装が完了です．

## コード（printデバッグ抜き）

```python:resnet.py
class BuildingBlock(nn.Module):
    """
    H(x) = BuildingBlock(x) + x
    """
    def __init__(self, in_channels, med_channels, out_channels, is_downsample=False):
        super().__init__()
        if is_downsample == True:
            stride = 2
        else:
            stride = 1
        self.m_1 = nn.Conv2d(in_channels, med_channels, kernel_size=3, stride=stride, padding=1)
        self.m_2 = nn.Conv2d(med_channels, out_channels, kernel_size=3, stride=1, padding=1)

    def forward(self, x):
        out = self.m_1(x)
        out = F.relu(out)
        out = self.m_2(out)

        return out

class ResNet18(nn.Module):
    def __init__(self, in_channels, num_classes):
        self.num_classes = num_classes
        super().__init__()
        # conv1
        self.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3)

        # conv2_x(maxpool)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # conv2_x
        self.resblock2_1 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)
        self.resblock2_2 = BuildingBlock(in_channels=64, med_channels=64, out_channels=64)

        # conv3_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock3_1 = BuildingBlock(in_channels=64, med_channels=128, out_channels=128, is_downsample=True)
        self.resblock3_2 = BuildingBlock(in_channels=128, med_channels=128, out_channels=128)

        # conv4_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock4_1 = BuildingBlock(in_channels=128, med_channels=256, out_channels=256, is_downsample=True)
        self.resblock4_2 = BuildingBlock(in_channels=256, med_channels=256, out_channels=256)

        # conv5_x（一つ目はダウンサンプリングのため，ストライドを2とする）
        self.resblock5_1 = BuildingBlock(in_channels=256, med_channels=512, out_channels=512, is_downsample=True)
        self.resblock5_2 = BuildingBlock(in_channels=512, med_channels=512, out_channels=512)

        # 出力のサイズを指定してAvgPooling
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))

        # fully connectionによって変換
        self.fc = nn.Linear(512, self.num_classes)
    def conv11(self, in_channels, out_channels):
        """
        if the feature map size is halved, the number of filters is doubled so as to preserve the time complexity per layer.
        だそうなので，画像のサイズが1/2になり，フィルタの数が２倍になっていく．
        そのため，画像のサイズを調整し，チャンネル数も調整する必要がある．
        画像のサイズはstrideを２にすることで，1/2に，チャンネル数は1*1のカーネルをin_channelsの２倍用いて，畳み込みをすればよい．
        """
        return nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=2, bias=False)
    
    def forward(self, x):
        # conv
        out = self.conv1(x)
        out = F.relu(out)
        # maxpool
        out = self.maxpool(out)
        out = F.relu(out)

        # conv2_x
        out = self.resblock2_1(out) + out
        out = F.relu(out)
        out = self.resblock2_2(out) + out
        out = F.relu(out)

        # conv3_x
        out = self.resblock3_1(out) + self.conv11(64, 128)(out)
        out = F.relu(out)
        out = self.resblock3_2(out) + out
        out = F.relu(out)

        # conv4_x
        out = self.resblock4_1(out) + self.conv11(128, 256)(out)
        out = F.relu(out)
        out = self.resblock4_2(out) + out
        out = F.relu(out)

        # conv5_x
        out = self.resblock5_1(out) + self.conv11(256, 512)(out)
        out = F.relu(out)
        out = self.resblock5_2(out) + out
        out = F.relu(out)

        # avgpool
        out = self.avgpool(out)
        out = F.relu(out)

        # fully connection
        out = self.fc(out.flatten())

        #　必要に応じてsoftmax

        return out
```

## まとめと感想
今回はresnetについて解説・実装を行いました．
アイデア自体は難しくないですが，実装がやややっかいでした（実装力不足なのもありますが。。。）
次はODENetやりたいですね．（torchdiff使うか，pytorchでRunge-Kutta書くか。。。）
何かあればぜひコメントください．

## 補足

後は，BottleNeckBlockを組み込めばResnet50などの大規模なモデルを構築できます．
BottleNeckBlockについて簡単に補足すると，
![resnet12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711911/a8c353e1-c0b7-9dd8-785d-f99e31255297.png)
のようにBuildingBlockを少し書き換えたものになります．
論文中に`. If the identity shortcut in Fig. 5(right) is replaced with projection, one can show that the time complexity and model size are doubled, as the shortcut is connected to the two high-dimensional ends. So identity shortcuts lead to more efficient models for the bottleneck designs.`とあり，計算量？が抑えられる的なことが書いてありました．
なので，モデルが大きくなった時にこのBottleNeckBlockを組み込んでモデルが構成されています．