Hello Computer Vision

ResNet(2015) 구조 리뷰 본문

딥러닝

ResNet(2015) 구조 리뷰

지웅쓰 2022. 10. 25. 15:02

저번 GoogleNet에 이은 ResNet구조이다.

이전 VGGNet은 19층이상 쌓지 못했고 GoogleNet도 20층가까이 쌓았지만 그 범위에서 크게 벗어나지 못했다.

그 이유는 gradient vanishing 문제가 발생했기 때문에 깊게 쌓아도 성능이 안나왔기 때문이다.

GoogleNet에서는 그나마 중간중간 분류기를 설치하여 훈련할 때 이러한 문제점을 조금이나마 해결했고

이는 몇층이상더 쌓을 수 있는 성과를 냈다.

그리고 GoogleNet보다 효과적으로 문제를 처리한 것이 ResNet이며 Imagenet대회에서 96.43%의 정확도를 기록했다.

ResNet의 기본 구조는 다음과 같으며 층의 깊이가 훨씬 늘어났음을 알 수 있다.

그리고 단순히 층만 늘린 것이 아닌 성능이 좋아졌다고 한다.

이것이 발표한 성능인데 기존 모델들은 깊게 레이어를 쌓아도 성능이 오히려 떨어지는 모습을 보여주었다면

ResNet에서는 깊게 쌓은만큼 성능이 올라갔다고 한다.

그렇다면 어떠한 방법을 통해 레이어를 깊게 쌓아도 gradient vanishing문제를 해결했을까?

ResNet이 해결한 방법은 다음과 같다.

먼저 왼쪽은 기존 모델들이 학습을 진행한 방법이다. 

input인 x가 들어온다면 layer를 거쳐서 H(x)가 만들어짐을 알 수 있다. 결과적으로 우리가 학습하는 것은 H라는 함수이다.

 

오른쪽에 있는 모델을 한번 살펴보자면 input x가 기존 모델과 거침과 동시에 거치지 않은 x는 거친 x와 합쳐지게 된다.

이러한 부분을 shorcut, skip connection 이라고 표현한다.(x가 layer들을 거치지 않고 지름길을 거쳐서 간다는 뜻)

두 layer를 거쳐서 온 x를 F(x), skip connection을 통해 온 x, 이 두개를 합치면 F(x) + x 가 되는데

결과적으로 이는 기존 모델에서의 H(x)와 같다. ( H(x) = F(x) + x ) 

(개인적인 생각이지만 완전히 같지는 않지만 개념적으로 같다고 생각하는 것이 이해하기 쉬운 거 같다.)

그렇다면 기존 모델에서 학습할 H라는 함수는 ResNet에서는 F에다 input x를 뺀 함수를 학습하는 것과 같은 것인데

직관적으로 생각해본다면 우리는 기존의 x를 알고 있는 것이니 학습의 난이도가 조금이나마 내려가는 것을 느낄 수 있다.

나는 이러한 부분을 "답을 구해야하는데 답의 모서리 부분정도를 알고 있다" 비유 정도를 들면서 이해하고 있다.

shotcut을 사용한 기법은 학습의 난이도가 낮아지는 효과가 있었고 이로 인해 레이어를 깊게 쌓아도

gradient vanishing 문제가 덜 발생하는 결과를 낳았다.

다음은 ResNet의 구조이다.

class BasicBlock(nn.Module):
  def __init__(self, in_channels, out_channels, stride = 1):
    super(BasicBlock, self).__init__()

    self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size = 3, stride= stride, padding = 1, bias = False)
    self.bn1 = nn.BatchNorm2d(out_channels)

    self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size = 3, stride = 1, padding = 1, bias = False)
    self.bn2 = nn.BatchNorm2d(out_channels)

    self.shortcut = nn.Sequential() #stride = 1   (identity mapping 이라면)

    if stride != 1: # stride != 1
      self.shortcut = nn.Sequential(
          nn.Conv2d(in_channels, out_channels, kernel_size = 1, stride = stride, bias = False),
          nn.BatchNorm2d(out_channels)
      )
    
  def forward(self, x):
    out = F.relu(self.bn1(self.conv1(x))) 
    out = self.bn2(self.conv2(out))  #2개의 신경망을 쌓는다. 
    out += self.shortcut(x) #학습 난이도를 쉽게하기 위해 기존의 x를 더해준다
    out = F.relu(out)

    return out
class ResNet(nn.Module):
  def __init__(self, block, num_blocks, num_classes = 10): #block > BasicBlock 객체
    super(ResNet, self).__init__()

    self.in_channels = 64

    self.conv1 = nn.Conv2d(3, 64, kernel_size = 3, stride = 1, padding = 1, bias = False) #크기는 그대로, layer만 깊게
    self.bn1 = nn.BatchNorm2d(64)
    self.layer1 = self._make_layers(block, 64, num_blocks[0], stride = 1)
    self.layer2 = self._make_layers(block, 128, num_blocks[1], stride = 2)
    self.layer3 = self._make_layers(block, 256, num_blocks[2], stride = 2)
    self.layer4 = self._make_layers(block, 512, num_blocks[3], stride = 2)
    self.linear = nn.Linear(512, num_classes)

  def _make_layers(self, block, out_channels, num_blocks, stride):
    strides = [stride] + [1] * (num_blocks - 1) #첫번째 레이어에서만 크기변화, 나머지는 identity mapping
    layers = []

    for stride in strides:
      layers.append(block(self.in_channels, out_channels, stride))
      self.in_channels = out_channels
    return nn.Sequential(*layers)
  
  def forward(self, x):
    out = F.relu(self.bn1(self.conv1(x)))
    out = self.layer1(out)
    out = self.layer2(out)
    out = self.layer3(out)
    out = self.layer4(out)
    out = F.avg_pool2d(out, 4)
    out = out.view(out.size(0), -1)
    out = self.linear(out)
    return out
net = ResNet(BasicBlock, [2,2,2,2], num_classes = 10)

이전 구조 코드들은 예전 처음에 공부할 때 keras 를 이용해 짜봤다면

최근에는 컴퓨터비전 분야에서 많은 장점을 가지고 있는 pytorch를 공부하고 있다보니 pytorch로 구현해보았다.

첫번째 코드가 하나의 블럭이라고 생각하면 되고 모델은 여러 블럭들의 집합체로 이루어져있다.

(GoogleNet에서 Inception 모듈이 반복된 것처럼 ResNet에서는 이러한 블럭들이 반복된다.)

ResNet이 활용한 기법은 현재 공부하고 있는 GAN모델에서도 활발히 활용되고 있는데

계속 익숙해질 필요가 있다고 생각한다.

 

정리 : ResNet에서는 shorcut(skip connection)을 활용하여 학습난이도를 낮추었고 이를 통해 기존 모델들보다 층을 더 깊게 쌓아도 gradient vanishing 문제가 발생하지 않는다.