44
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ResNet50をpytorchで実装

Posted at

ResNetとは

ざっくり説明すると畳み込み層の出力値に入力値を足し合わせる残差ブロック(Residual Block)の導入により、層を深くしても勾配消失が起きることを防ぎ、高い精度を実現したニューラルネットワークのモデルのことです。ResNetについての解説は他にもたくさんあるため、ここでの解説は割愛します。

【論文】
Deep Residual Learning for Image Recognition

【参考ページ】
[Residual Network(ResNet)の理解とチューニングのベストプラクティス]
(https://deepage.net/deep_learning/2016/11/30/resnet.html)
代表的モデル「ResNet」、「DenseNet」を詳細解説!
「2019年前半に読むべきディープラーニング論文 19選」 Deep Residual Learning for Image Recognition

ResNet50の構造

ResNetには層の数に合わせてResNet34、ResNet50、ResNet101などの種類がありますが、今回はResNet50を実装します。
構造は下記の図の通りです。conv2_x~conv5_xは残差ブロックで構成されます。(conv2_xは3つの残差ブロック、conv3_xは4つの残差ブロック...)
image.png

実装

ここからが実装となります。こちらのyoutubeの動画、及び、gitのコードを参考にしました。
【youtube】
Pytorch ResNet implementation from Scratch
【git】
https://github.com/aladdinpersson/Machine-Learning-Collection/blob/master/ML/Pytorch/CNN_architectures/pytorch_resnet.py
https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py#L64

残差ブロック

まずはResNetの主要パーツとなる残差ブロックのクラスを作成します。
残差ブロックは基本的な構造は同じですが、inputとoutputのchannel数、sizeによって下記の3パターンに分けることができます。

パターン1 inputとoutputでchannel数、sizeが同じ
パターン2 outputのchannel数がinputの4倍
パターン3 outputのchannel数がinputの4倍、且つ、outputのsizeがinputの1/2

上記の3パターン全てに対応する対応できるようにクラスを設計します。
パターン2はidentify(元の入力値)を足す際にchannel数が合わなくなってしまうため、identifyを足す前に1×1のconv層を通すことでoutput channelを調整します(identity_conv)。パターン3は、残差ブロック内の3×3のconv層のstrideを2にすることでサイズを半分にします。

image.png

import torch
import torch.nn as nn

class block(nn.Module):
    def __init__(self, first_conv_in_channels, first_conv_out_channels, identity_conv=None, stride=1):
        """
        残差ブロックを作成するクラス
        Args:
            first_conv_in_channels : 1番目のconv層(1×1)のinput channel数
            first_conv_out_channels : 1番目のconv層(1×1)のoutput channel数
            identity_conv : channel数調整用のconv層
            stride : 3×3conv層におけるstide数。sizeを半分にしたいときは2に設定
        """        
        super(block, self).__init__()

        # 1番目のconv層(1×1)
        self.conv1 = nn.Conv2d(
            first_conv_in_channels, first_conv_out_channels, kernel_size=1, stride=1, padding=0)
        self.bn1 = nn.BatchNorm2d(first_conv_out_channels)

        # 2番目のconv層(3×3)
        # パターン3の時はsizeを変更できるようにstrideは可変
        self.conv2 = nn.Conv2d(
            first_conv_out_channels, first_conv_out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn2 = nn.BatchNorm2d(first_conv_out_channels)

        # 3番目のconv層(1×1)
        # output channelはinput channelの4倍になる
        self.conv3 = nn.Conv2d(
            first_conv_out_channels, first_conv_out_channels*4, kernel_size=1, stride=1, padding=0)
        self.bn3 = nn.BatchNorm2d(first_conv_out_channels*4)
        self.relu = nn.ReLU()

        # identityのchannel数の調整が必要な場合はconv層(1×1)を用意、不要な場合はNone
        self.identity_conv = identity_conv

    def forward(self, x):

        identity = x.clone()  # 入力を保持する

        x = self.conv1(x)  # 1×1の畳み込み
        x = self.bn1(x)
        x = self.relu(x)
        x = self.conv2(x)  # 3×3の畳み込み(パターン3の時はstrideが2になるため、ここでsizeが半分になる)
        x = self.bn2(x)
        x = self.relu(x)
        x = self.conv3(x)  # 1×1の畳み込み
        x = self.bn3(x)

        # 必要な場合はconv層(1×1)を通してidentityのchannel数の調整してから足す
        if self.identity_conv is not None:
            identity = self.identity_conv(identity)
        x += identity

        x = self.relu(x)

        return x

##ResNet50の実装
ここからのResNet50を実装となります。
conv1はアーキテクチャ通りベタ打ちしますが、conv〇_xは_make_layerという関数を作成し、先ほどのblockクラスを使用して残差ブロックを重ねていきます。例えばconv2_xなら3つの残差ブロック、conv4_xなら6つの残差ブロックを重ねる形になります。
この時、conv〇_xの1つ目の残差ブロックでchannel数、size調整が発生することがポイントとなります。そのため、conv〇_xで1つ目の残差ブロックを作成する際はblockの引数identity_convを追加します。さらにサイズ変更が必要なconv3_x~conv5_xはstrideを2に設定します。2つ目以降の残差ブロックはchannel数、sizeともに変更不要なため、blockの引数identity_convはNone、stride=1でブロックを必要な分だけループ処理で作成します。
image.png

class ResNet(nn.Module):
    def __init__(self, block, num_classes):
        super(ResNet, self).__init__()

        # conv1はアーキテクチャ通りにベタ打ち
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU()
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # conv2_xはサイズの変更は不要のため、strideは1
        self.conv2_x = self._make_layer(block, 3, res_block_in_channels=64, first_conv_out_channels=64, stride=1)

        # conv3_x以降はサイズの変更をする必要があるため、strideは2
        self.conv3_x = self._make_layer(block, 4, res_block_in_channels=256,  first_conv_out_channels=128, stride=2)
        self.conv4_x = self._make_layer(block, 6, res_block_in_channels=512,  first_conv_out_channels=256, stride=2)
        self.conv5_x = self._make_layer(block, 3, res_block_in_channels=1024, first_conv_out_channels=512, stride=2)

        self.avgpool = nn.AdaptiveAvgPool2d((1,1))
        self.fc = nn.Linear(512*4, num_classes)

    def forward(self,x):

        x = self.conv1(x)   # in:(3,224*224)、out:(64,112*112)
        x = self.bn1(x)     # in:(64,112*112)、out:(64,112*112)
        x = self.relu(x)    # in:(64,112*112)、out:(64,112*112)
        x = self.maxpool(x) # in:(64,112*112)、out:(64,56*56)

        x = self.conv2_x(x)  # in:(64,56*56)  、out:(256,56*56)
        x = self.conv3_x(x)  # in:(256,56*56) 、out:(512,28*28)
        x = self.conv4_x(x)  # in:(512,28*28) 、out:(1024,14*14)
        x = self.conv5_x(x)  # in:(1024,14*14)、out:(2048,7*7)
        x = self.avgpool(x)
        x = x.reshape(x.shape[0], -1)
        x = self.fc(x)

        return x

    def _make_layer(self, block, num_res_blocks, res_block_in_channels, first_conv_out_channels, stride):
        layers = []

        # 1つ目の残差ブロックではchannel調整、及びsize調整が発生する
        # identifyを足す前に1×1のconv層を追加し、サイズ調整が必要な場合はstrideを2に設定
        identity_conv = nn.Conv2d(res_block_in_channels, first_conv_out_channels*4, kernel_size=1,stride=stride)
        layers.append(block(res_block_in_channels, first_conv_out_channels, identity_conv, stride))

        # 2つ目以降のinput_channel数は1つ目のoutput_channelの4倍
        in_channels = first_conv_out_channels*4

        # channel調整、size調整は発生しないため、identity_convはNone、strideは1
        for i in range(num_res_blocks - 1):
            layers.append(block(in_channels, first_conv_out_channels, identity_conv=None, stride=1))

        return nn.Sequential(*layers)

実装としては以上になります。最後にちゃんと動くかを確認します。

# 1000種のカテゴリに分類
model = ResNet(block, 1000)

# inputは3×224×224の画像が4枚の想定
x = torch.rand(4,3,224,224)

model(x).shape
# >torch.Size([4, 1000])

#終わりに
今回はyoutubeの動画や、gitのコードを参考にしながら実装しました。参考にしたコードはいずれもResNet50だけでなく、ResNet101など層の数が変わっても汎用的に使用できるよう実装がされており、分かりづらい部分も多かったため色々と簡略化/変更を実施しています。これに伴った間違った挙動になっているかもしれないので、ご注意頂くとともにその際はご指摘頂けますと幸いです。

44
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?