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つの残差ブロック...)
実装
ここからが実装となります。こちらの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にすることでサイズを半分にします。
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でブロックを必要な分だけループ処理で作成します。
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など層の数が変わっても汎用的に使用できるよう実装がされており、分かりづらい部分も多かったため色々と簡略化/変更を実施しています。これに伴った間違った挙動になっているかもしれないので、ご注意頂くとともにその際はご指摘頂けますと幸いです。