728x90

2023-11-30 61th Class

Convolutional Neural Network - ResNet

#️⃣ ResNet 소개

개요

  • 2019년 ISVRC 이미지 데이터 분류 대회에서 우승한 모델
  • shortcut skip connection을 도입한 모델
  • Microsoft 팀에서 발표

ResNet Architecture

#️⃣ 등장 배경

Drawing 2023-11-30 10.14.10.excalidraw.png

f*는 target model을 의미

ResNet 이전의 방식 (ex: VGG)

  • ResNet 이전의 딥러닝 모델링 방식은 학습을 시켰을 때 모델의 성능이 만족스럽지 않은 경우
  • 모델의 층을 늘려 complexity(복잡도)를 더하면서 모델의 표현 범위를 늘렸음
  • ex) 은 VGG-11로 만들 수 있는 모델들의 집합이고, > 으로 확장하며 (VGG-13 > VGG-19) 모델의 표현력을 높힘
  • 하지만 아래의 좌측 그림과 같은 문제가 발생할 수 있음

Pasted image 20231130102005.png

왼쪽 이미지

  • ResNet 개발 팀에서는 F1, F2, F3으로 모델이 확장되면서 분류할 수 있는 범위가 이동하는 현상을 문제로 제기함
  • 최종 확장 모델인 F6의 범위에는 F1의 일부가 포함되지 않는 문제가 발생
  • 이러한 현상이 발생하는 모델의 확장은 좋은 결과가 아님

오른쪽 이미지

  • 기존 모델을 포함하면서 레이어를 추가해서 범위를 확장할 수 있는 방법을 찾게 됨
  • 따라서 ResNet에서는 모델의 복잡도(complexity) 증가 이외의 접근 방법을 고안하게 됨

#️⃣ 항등함수(identity function)와 Shortcut Skip Connection

Identity Function :

추가되는 레이어가 초기 상태에서 위의 식과 같이 항등함수(identity function)라면,
초기 상태에서는 기존 모델의 출력에 변화없음

추가된 레이어가 항등함수에서부터 학습이 시작되면,
기존 모델을 포함한 상태에서 표현의 범위가 더 넓어지는 효과를 가짐

Shortcut Skip Connection

Pasted image 20231130110256.png|400

  • 합성곱층과 렐루 한 쌍을 f(x)라고 정의
  • skip connection은 f(x) = x가 아니라 f(x) = 0으로 만드는 것
  • 통계적으로 f(x)는 0에 가까울 확률이 높음

residual block

Pasted image 20231130110223.png|400

  • path1 : Conv > ReLU > Conv
  • path2 : x
  • result: path1 + path2

기존과 같이 Conv2D + ReLU를 통과하는 path1의 출력과
그대로 입력이 skip되는 skip2를 더한 값이
block의 출력값이 되는 구조

residual block code

import torch.nn as nn  
import torch  
  
  
class ResidualBlock(nn.Module):  
    def __init__(self, in_channels, out_channels):  
        super(ResidualBlock, self).__init__()  
        self.conv_path = nn.Sequential(  
            nn.Conv2d(in_channels=in_channels, out_channels=out_channels,  
                      kernel_size=3, padding=1, stride=1),  
            nn.ReLU(),  
            nn.Conv2d(in_channels=out_channels, out_channels=out_channels,  
                      kernel_size=3, padding=1, stride=1))  
  
        if in_channels != out_channels:  
            self.skip_path = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,  
                                      kernel_size=1)  
        else:  
            self.skip_path = nn.Identity()  
  
        self.out_act = nn.ReLU()  
  
    def forward(self, x):  
        out = self.conv_path(x)  
        out += self.skip_path(x)  
        out = self.out_act(out)  
        return out  
  
  
def run_residual_block():  
    BATCH_SIZE = 32  
    H, W = 100, 100  
    channels = 3  
    input_tensor = torch.randn(size=(BATCH_SIZE, channels, H, W))  
    print("in:", input_tensor.shape)  
    block = ResidualBlock(in_channels=channels, out_channels=192)  
    x = block(input_tensor)  
    print("out:", x.shape)  
  
  
if __name__ == '__main__':  
    run_residual_block()

'''
in: torch.Size([32, 3, 100, 100])
out: torch.Size([32, 192, 100, 100])
'''

#️⃣ Residual Block 파라미터 수 계산하기

block = ResidualBlock(in_channels=64, out_channels=64)  
summary(block, input_size=(64, 56, 56), device='cpu')

위의 코드에서 파라미터 수 계산

Formula for Param # ={(kernel x kernel x in_channel) + 1} x out_channel

Layer(type) Output Shape Formula for Param # Param #
Conv2d-1 (-1, 64, 56, 56) {(3x3x64)+1}x64 36,928
ReLU-2 (-1, 64, 56, 56)
Conv2d-3 (path1) (-1, 64, 56, 56) {(3x3x64)+1}x64 36,928
Identity-4 (path2) (-1, 64, 56, 56)
ReLU-5 (-1, 64, 56, 56)

#️⃣ Residual Block Downsizing

Resnet에서는 점선으로 표기된 층이 있는데,
3x3 conv, 2xprevious_channel/2 층을 통해
pooling 대신 stride를 통해 이미지 사이즈를 반으로 줄이고
channel을 2배로 늘리게 됨

residual block downsizing code

class ResidualBlockDown(nn.Module):  
    def __init__(self, in_channels, out_channels):  
        super(ResidualBlockDown, self).__init__()  
  
        self.conv_path = nn.Sequential(  
            nn.Conv2d(in_channels=in_channels, out_channels=out_channels,  
                      kernel_size=3, padding=1, stride=2),  
            nn.ReLU(),  
            nn.Conv2d(in_channels=out_channels, out_channels=out_channels,  
                      kernel_size=3, padding=1, stride=1))  
  
        self.skip_path = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,  
                                   kernel_size=1, padding=0, stride=2)  
  
        self.out_act = nn.ReLU()  
  
    def forward(self, x):  
        out = self.conv_path(x)  
        out += self.skip_path(x)  
        out = self.out_act(out)  
        return out

def run_rb_down():  
    BATCH_SIZE = 32  
    H, W = 100, 100  
    channels = 64  
    input_tensor = torch.randn(size=(BATCH_SIZE, channels, H, W))  
    print("in:", input_tensor.shape)  
    block = ResidualBlockDown(in_channels=channels, out_channels=128)  
    output_tensor = block(input_tensor)  
    print("out:", output_tensor.shape)

if __name__ == '__main__':  
    # run_residual_block()  
    run_rb_down()
in: torch.Size([32, 64, 100, 100])
out: torch.Size([32, 128, 50, 50])

#️⃣ ResNet34 구현

아래의 아키텍처와 표를 참고하여 ResNet34를 구현

Monosnap DL_15_ResNet_SeSac양정은_필기.pdf 2023-11-30 14-08-46.png

resnet34 2023-11-30 14-08-15.png

ResNet34 code

class ResNet34(nn.Module):  
    def __init__(self, in_channels):  
        super(ResNet34, self).__init__()  
        # 1  
        self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=64,  
                               kernel_size=7, stride=2, padding=3)  
        self.conv1_act = nn.ReLU()  
  
        # 2  
        self.conv2 = nn.Sequential(  
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),  
            ResidualBlock(in_channels=64, out_channels=64),  
            ResidualBlock(in_channels=64, out_channels=64),  
            ResidualBlock(in_channels=64, out_channels=64))  
  
        # 3  
        self.conv3 = nn.Sequential(  
            ResidualBlockDown(in_channels=64, out_channels=128),  
            ResidualBlock(in_channels=128, out_channels=128),  
            ResidualBlock(in_channels=128, out_channels=128),  
            ResidualBlock(in_channels=128, out_channels=128))  
  
        # 4  
        self.conv4 = nn.Sequential(  
            ResidualBlockDown(in_channels=128, out_channels=256),  
            ResidualBlock(in_channels=256, out_channels=256),  
            ResidualBlock(in_channels=256, out_channels=256),  
            ResidualBlock(in_channels=256, out_channels=256),  
            ResidualBlock(in_channels=256, out_channels=256),  
            ResidualBlock(in_channels=256, out_channels=256))  
  
        # 5  
        self.conv5 = nn.Sequential(  
            ResidualBlockDown(in_channels=256, out_channels=512),  
            ResidualBlock(in_channels=512, out_channels=512),  
            ResidualBlock(in_channels=512, out_channels=512))  
  
        # 6  
        self.avgpool = nn.AvgPool2d(kernel_size=7, stride=7)  
        # self.avgpool = nn.AdaptiveAvgPool2d(output_size=1)  
        self.fc = nn.Linear(in_features=512, out_features=1024)  
  
    def forward(self, x):  
        x = self.conv1(x)  
        x = self.conv1_act(x)  
        x = self.conv2(x)  
        x = self.conv3(x)  
        x = self.conv4(x)  
        x = self.conv5(x)  
  
        x = self.avgpool(x)  
        x = x.view(x.size(0), -1)  
        x = self.fc(x)  
  
        return x
  
  
def run_resnet():  
    input_tensor = torch.randn(size=(32, 3, 224, 224))  
    model = ResNet34(in_channels=3)  
    pred = model(input_tensor)  
    print(pred.shape)
torch.Size([32, 1024])
  • Input Image -> ISVRC 이미지 사이즈인 (3, 224, 224)로 시작
  • 마지막 pooling층에서는 average pooling 이후 이미지 사이즈가 1x1로 출력되어야 함
  • self.avgpool = nn.AvgPool2d(kernel_size=7, stride=1)
  • self.avgpool = nn.AdaptiveAvgPool2d(output_size=1)
    • adaptive average pooling은 이미지의 output size를 정하면 알아서 계산해줌
    • 실제 계산 값으로 입력하려면 마지막 6층의 pooling 레이어에 도달한 이미지 사이즈가 7x7이기 때문에
    • kernel size를 7, pooling을 7로 설정하면 됨
반응형