ResNet50 코드리뷰


서론

이 장은 ResNet50 모델의 코드를 리뷰한다.

사전지식으로는 pytorch에서 torch.Size([1, 64, 56, 56])와 같은 형태를 사용하는데 이 의미는 batch=1, channels=64, output_featuremap_height=56, output_featuremap_width=56 에 해당한다.

목차

  1. ResNet50
  2. 결론
  3. 전체 코드

ResNet50

기본적으로 stride=1을 가지고 1x1 conv2d + relu를 형성하는 함수이다.

기본적으로 stride=1을 가지고 3x3 conv2d + relu를 형성하는 함수이다.

아래는 down=True인 경우 stride=2로 설정하면서 feature map의 크기를 절반으로 줄이는 bottleneck을 형성한다. down=False인 경우 크기를 줄이지 않고 bottleneck을 형성한다.

자세히 알아보자.

bottlneck의 마지막 부분에서만 feature map의 height, width를 절반으로 줄이게 되는데, 이 경우 skip connection에 self.downsample = nn.Conv2d(in_channels, out_channels, kernel, stride=2) 로 설정하여 입력 크기를 절반으로 줄인다.

그리고 self.layer의 conv_block_1으로 인해 절반으로 준 입력 크기를 self.downsample과 더해준다.

다음 코드는 feature map을 절반으로 줄이지 않는 부분에 해당하는 부분이다.

self.dim_equalizer 맴버 변수는 절반으로 줄이지 않는 부분에 대해서 skip connection을 위해 사용된다.

Resnet50에서의 x.size() 와 out.size()는 항상 다르기 때문에 사실상 if문은 크게 의미가 없다.

input의 크기를 output과 맞춰주기 위해 input의 차원을 바꿔주는(확장시키는) self.dim_equalizer = nn.Conv2d(in_dim, out_dim, kernel_size = 1) 역할을 해준다.

ResNet50의 경우Conv2d의 out_channels에 해당하는 base_dim=64이다. kernel=7, padding=3인 경우에 대해서는 input_size가 유지되는데 stride=2로 설정함으로써 input_size가 절반으로 줄어든다. 따라서 입력이 (1, 3, 224, 224)인 경우에 대해서 Conv2d를 통과하게되면 (1, 64, 112, 112)가 나온다.

그 다음 MaxPool2d에서 kernel=3, padding=1로 input_size를 유지하나, stride=2임으로 input_size가 절반으로 줄어든다. 따라서 (1, 64, 56, 56)이 나온다.

아래 표에 따라서 총 16개의 Bottleneck이 만들어진다.

이후 AvgPool2d(7, 1)을 적용하는데 마지막 bottleneck인 layer_5의 최종 output_shape은 (1, 2048, 7, 7) 이 나온다. 이에 kernel=7이고 stride=1인 pool을 적용하면 다음과 같은 공식으로 인해 (1, 2048, 1, 1) output_shape으로 나오게된다. Conv의 output shape 과 pooling의 output shape의 계산 공식은 동일하다.

최종적으로 out.view함수로 인해 (1, 2048)만 남게되고 이를 self.fc_layer = nn.Linear(base_dim * 32, num_classes) 의 형태로 classifier하는 부분으로 모델구조를 결정짓는다.

결론

논문에서는 단순히 skip connection을 사용하여 bottleneck의 output과 더해준다고 말하고 있지만, 구현된 코드를 분석해보면 이미지의 크기를 down_sampling하는 경우에는 input에 1x1convolution, stride=2를 적용하여 down_sampling된 크기로 output_feature와 shape을 맞추어 add를하고 down_sampling을 하지 않는 경우에는 output과 차원을 맞춰주는 단지 1x1 Conv를 사용하여 output_feature와 shape을 맞추어 add를 한다는 점이다. 즉 그림으로 보면 아래와 같은 식으로 된다.

전체 코드

Code
from torch import nn
import torchsummary
import torch

def conv_block_1(in_dim, out_dim, act_fn, stride = 1):
    """  bottleneck 구조를 만들기 위한 1x1 convolution """
    model = nn.Sequential(
        nn.Conv2d(in_dim, out_dim, kernel_size = 1, stride = stride),
        act_fn
    )
    return model

def conv_block_3(in_dim, out_dim, act_fn):
    """  bottleneck 구조를 만들기 위한 3x3 convolution """
    model = nn.Sequential(
        nn.Conv2d(in_dim, out_dim, kernel_size = 3, stride = 1, padding = 1),
        act_fn
    )
    return model
    
class BottleNeck(nn.Module):
    """
    Residual Network의 bottleneck 구조
    - down 파라미터는 down 블록을 통과하였을 때, feature map의 크기가 줄어드는 지의 여부의 불리언 값 입니다.
        - True인 경우 stride = 2가 되어 크기가 반으로 줄어듭니다.
    - 경우에 따라서 채널의 갯수가 달라 더해지지 않는 경우가 있는데, 이럴 때는 차원을 맞춰 주는 1x1 conv를 추가하여
      입력의 채널을 출력의 채널과 같게 만들어 줍니다.
     """
    def __init__(self, in_dim, mid_dim, out_dim, act_fn, down=False):
        super(BottleNeck, self).__init__()
        self.act_fn = act_fn
        self.down = down

        if self.down:
            self.layer = nn.Sequential(
                conv_block_1(in_dim, mid_dim, act_fn, 2), 
                conv_block_3(mid_dim, mid_dim, act_fn), 
                conv_block_1(mid_dim, out_dim, act_fn) 
            )            
            self.downsample = nn.Conv2d(in_dim, out_dim, 1, 2)
        else:
            self.layer = nn.Sequential(
                conv_block_1(in_dim, mid_dim, act_fn), 
                conv_block_3(mid_dim, mid_dim, act_fn), 
                conv_block_1(mid_dim, out_dim, act_fn)
            )
        
        # shape을 맞추기 위한 1x1 conv
        self.dim_equalizer = nn.Conv2d(in_dim, out_dim, kernel_size = 1)
        
    def forward(self, x):
        # input_size를 줄이는 경우에 대해서는 skip connection 부분에 convolution을 적용하고
        if self.down:
            downsample = self.downsample(x) # (1, 256, 28, 28)
            out = self.layer(x)
            out = out + downsample
        else:
            # input_size를 줄이지 않는 경우에 대해서는 그냥 skip connection을 진행한다.
            out = self.layer(x) # x_size -> (1, 64, 56, 56), out_size -> (1, 256, 56, 56)
            if x.size() is not out.size():
                x = self.dim_equalizer(x) # (1, 256, 56, 56) (1, 256, 56, 56)
                #print(x.shape)
            out = out + x
        return out


class ResNet(nn.Module):
    """ ResNet 50 layer """
    def __init__(self, base_dim, num_classes = 2):
        super(ResNet, self).__init__()
        self.act_fn = nn.ReLU()
        self.layer_1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=base_dim, kernel_size=7, stride=2, padding=3),
            nn.ReLU(),
            # nn.MaxPool2d(kernel_size, stride, padding)
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
        )
        
        # random_input = torch.randn(1, 3, 224, 224)
        # out = self.layer_1(random_input)
        # print(out.shape)
        # layer_1 출력 : (1, 64, 56, 56) 
        
        self.layer_2 = nn.Sequential(
            # BottleNeck(in_dim, mid_dim, out_dim, act_fn, down)
            BottleNeck(base_dim, base_dim, base_dim * 4, self.act_fn),
            BottleNeck(base_dim * 4, base_dim, base_dim * 4, self.act_fn),
            BottleNeck(base_dim * 4, base_dim, base_dim * 4, self.act_fn, down = True),
        )
        self.layer_3 = nn.Sequential(
            BottleNeck(base_dim * 4, base_dim * 2, base_dim * 8, self.act_fn),
            BottleNeck(base_dim * 8, base_dim * 2, base_dim * 8, self.act_fn),
            BottleNeck(base_dim * 8, base_dim * 2, base_dim * 8, self.act_fn),
            BottleNeck(base_dim * 8, base_dim * 2, base_dim * 8, self.act_fn, down = True),
        )
        self.layer_4 = nn.Sequential(
            BottleNeck(base_dim * 8, base_dim * 4, base_dim * 16, self.act_fn),
            BottleNeck(base_dim * 16, base_dim * 4, base_dim * 16, self.act_fn),
            BottleNeck(base_dim * 16, base_dim * 4, base_dim * 16, self.act_fn),
            BottleNeck(base_dim * 16, base_dim * 4, base_dim * 16, self.act_fn),
            BottleNeck(base_dim * 16, base_dim * 4, base_dim * 16, self.act_fn),
            BottleNeck(base_dim * 16, base_dim * 4, base_dim * 16, self.act_fn, down = True),
        )
        self.layer_5 = nn.Sequential(
            BottleNeck(base_dim * 16, base_dim * 8, base_dim * 32, self.act_fn),
            BottleNeck(base_dim * 32, base_dim * 8, base_dim * 32, self.act_fn),
            BottleNeck(base_dim * 32, base_dim * 8, base_dim * 32, self.act_fn),
        )
        
        self.avgpool = nn.AvgPool2d(7, 1)
        self.fc_layer = nn.Linear(base_dim * 32, num_classes)
        
    def forward(self, x):
        out = self.layer_1(x)
        out = self.layer_2(out)
        out = self.layer_3(out)
        out = self.layer_4(out)
        out = self.layer_5(out) # (1, 2048, 7, 7)
        out = self.avgpool(out) # (1, 2048, 1, 1)
        out = out.view(out.size(0), -1) # (1, 2048)
        out = self.fc_layer(out)
        return out
    
    
if __name__=='__main__' :
    model = ResNet(base_dim=64)
    input_tensor = torch.randn(1, 3, 224, 224)
    model(input_tensor)
    
    #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    #model = model.to(device)
    #input_size = (3, 224, 224)
    #torchsummary.summary(model, input_size)
    
    #print(model)
    
    # for name, module in model.named_modules() :
    #     if isinstance(module, nn.Conv2d) :
    #         print(f"{name}: {module.weight.shape}")

[reference]

https://gaussian37.github.io/dl-concept-resnet/