Pytorch로 ResNet을 구현한 내용에 대해 정리해보겠습니다. 

 

원 논문은 아래에서 확인하실 수 있고, 

https://arxiv.org/abs/1512.03385

 

Deep Residual Learning for Image Recognition

Deeper neural networks are more difficult to train. We present a residual learning framework to ease the training of networks that are substantially deeper than those used previously. We explicitly reformulate the layers as learning residual functions with

arxiv.org

(Review에서 사용된 이미지는 원 논문에서 추출한 이미지입니다.)

 

구현 코드는 아래에서 확인하실 수 있습니다.

https://github.com/LimYooyeol/AI-Paper-Code/tree/main/ResNet

 

GitHub - LimYooyeol/AI-Paper-Code

Contribute to LimYooyeol/AI-Paper-Code development by creating an account on GitHub.

github.com

 

<Review>

- Background

딥러닝 모델에서 층을 깊게 쌓으면 gradient vanishing/explosion과 같은 문제로 인해 학습이 잘 이뤄지지 않았었지만,   Batch Normalization 등의 새로운 기법이 등장하면서 gradien vaninshing/explosion 문제를 해결할 수 있었고, 층을 깊게 쌓더라도 학습이 이뤄지게 되었습니다.

 

Degradation

그러나 층을 깊게 쌓을수록 오히려 성능이 안좋아지는 'degradation' 문제가 발생하게 되었는데, 

단순히 parameters의 수가 많아져서 test 성능이 떨어지는 것이 아니라, training data에 대해서도 더 깊은 모델이 더 큰 error를 보이는 문제였습니다. 

 

따라서 본 논문에서는 degradation 문제를 해결하기 위해 'Residual Learning'이라는 새로운 방법을 소개합니다.

 

- Residual Learning

Residual learning은 다음과 같은 아이디어로 부터 시작되었습니다.

 

'(1) Shallower 구조를 갖는 모델'과 '(2) 해당 모델에 몇몇 layers를 더 추가한 모델'이 있을 때,

(2)번 모델이 (1)번 모델보다 최소한 같거나 더 좋은 성능을 보여야한다.

(추가된 layers가 identity mapping을 수행하면 되므로)

 

그러나 앞서 background에서 살펴봤듯이, 실제 학습은 그렇게 이뤄지지 않았습니다.

 

따라서 연구자들은 다음과 같은 생각을 하게 됩니다. 

'위 그림에서 H(x)가 해당 layer를 거친 후의 이상적인 출력이라고 가정했을 때,

현재의 모델은 x -> H(x) 로의 mapping을 찾는 것에 어려움을 겪고 있다. 

 

H(x) = F(x) + x 라고 할 때(F(x) : residual), 모델이 residual(F(x))을 학습하게 하는 것이 어떨까?'

 

위와 같은 방법에서 만약 identity mapping이 H(x)라고 가정하면,

model은 모든 weight를 0으로 학습하면 되므로 x -> F(x) 로의 mapping은 x -> H(x) 로의 mapping보다 상대적으로 간단할 것입니다.

(항상 identity mapping이 ideal하다는 것은 아니고 하나의 예시이긴 하지만, 실제로도 layer의 뒤로 갈수록 F(x)의 출력은 작아지는 양상을 보인다고 합니다.)

 

Residual Learning

그렇게 등장한 것이 residual learning입니다. 

 

위 그림에서 두 개의 weight layer를 하나의 block으로 취급하면, 해당 block에서는 residual, F(x)를 학습하게 됩니다. 

 

- Results

다양한 실험을 보여주지만 ImageNet data에 대해 총 4개의 모델을 비교한 결과를 살펴보겠습니다.

 

  • plain 18 layers vs plain 34 layers

      Residual learning을 사용하지 않으면, 층이 깊어질수록 성능이 떨어지는 degradation 문제를 확인할 수 있습니다.

 

  • ResNet 18 layers vs ResNet 34 layers

       Residual learning을 사용한 결과, degradation 문제가 해결되어 더 깊은 모델이 좋은 성능을 보이는 것을 확인할           수 있습니다.

 

  • plain vs ResNet

        18 layers의 경우 큰 성능 차이는 보이지 않지만 ResNet의 경우가 더 빨리 수렴했고, 34 layers의 경우 큰 성능 차          이를 확인할 수 있습니다. 

 

 

이처럼 residual learning은 매우 성공적인 결과를 불러왔고, 발표될 당시 ImageNet challenge 및 기타 대부분의 challenge에서 모두 1위를 차지하게 됩니다.

<Implementation>

Pytorch에서 제공하는 CIFAR 10 dataset을 사용하여 구현 및 실험을 진행하였습니다.

 

먼저 모델의 구조는 논문에서 연구자들이 CIFAR 10 dataset에 대해 적용한 구조를 그대로 따랐습니다.

 

① 기본적으로 VGG의 방식을 따라 filter는 모두 3x3크기를 사용합니다. 

 

② Feature map size를 32x32 -> 16x16 -> 8x8 으로 감소시키고, feature map size가 감소할 때마다 channel의 수는 2배로 증가시켜 16->32->64의 순서로 증가합니다.

 

③ 각 feature map size마다 2n 개의 convolution layers( = n개의 residual block)를 적용합니다.

(모든 convolution 연산의 뒤에는 BN layer가 추가되어있습니다.)

 

가장 처음의 3x3 convolution layer와 마지막의 Linear layer까지 최종적으로 총 6n+2개의 layer가 존재하게 됩니다.

 

논문에서는 다양한 n에 대해 실험 결과를 제시하지만, n = 5, 즉 layers가 32개인 경우에 대해서만 실험을 진행했습니다.

- Residual Block

먼저 2개의 convolution layers로 이뤄진 residual block의 구현입니다. 

# Basic residual block consists of pair of convolution layer
class Residual_Block(nn.Module) :
  def __init__(self, in_channel, out_channel, feature_reduce = False) :
    super().__init__()

    self.feature_reduce = feature_reduce

    if feature_reduce :
      stride = 2
    else : 
      stride = 1

    self.Conv = nn.Sequential(
        nn.Conv2d(in_channel, out_channel, kernel_size = 3, stride = stride, padding = 1, bias = False), 
        nn.BatchNorm2d(out_channel),
        nn.ReLU(),
        nn.Conv2d(out_channel, out_channel, kernel_size = 3, padding = 1, bias =  False),
        nn.BatchNorm2d(out_channel)
    )

    if feature_reduce :
      self.shortcut = nn.Sequential(
          # reduce feature map by pooling (No more parameters, option (A))
          nn.MaxPool2d(kernel_size = 2, stride = 2)
      )
    else :
      self.shortcut = nn.Identity()

  def forward(self, x) :
    x_prev = self.shortcut(x)
    
    if self.feature_reduce :
      # zero padding for dimension matching
      concat = torch.zeros_like(x_prev)
      x_prev = torch.concat((x_prev, concat), axis = 1)
    else :
      x_prev = x_prev

    F_x = self.Conv(x)

    return F.relu(F_x + x_prev)

Shortcut을 제외하면 Conv-BN-ReLU-Conv-BN-ReLU 순으로 구성된 간단한 구조이므로, shortcut에 대해서만 조금 더 살펴보겠습니다.

 

첫 번째 convolution layer만 짚고 넘어가면, feature map size를 줄이는 경우 stride = 2인 convolution을 통해 처리하기 때문에 feature map size를 줄일 때는 첫번째 layer의 stride가 2가 되도록 구현했습니다. 

 

다시 shortcut으로 돌아가면, CIFAR10 dataset에 대해서는 모두 option (A), identity mapping을 적용하는 논문의 내용을 따랐습니다. (그 외에는 convolution을 이용합니다. 논문 참고)

 

따라서 Feature map size가 바뀌는 block의 shortcut은 MaxPooling을 이용하여 feature map size를 줄인 후, 늘어난 channel은 zero로 채우는 zero padding을 이용합니다.

 

그 외의 경우는 그대로 전달되는 identity mapping을 이용합니다. 

 

ex) input : 32x32x16, output(2번째 conv) : 16x16x32 인 경우, input을 그대로 output에 더해줄 수 없습니다.

따라서 maxpooling을 통해 16x16x16으로 만들어주고, zero padding을 통해 16x16x32로 만들어주는 것입니다. 

 

- Feature Block

Feature map 크기가 동일한 구간(n개의 residual blcok으로 구성)을 하나의 feature block으로 구현했습니다.

# Block consists of n residual blocks
class Feature_Block(nn.Module) :
  def __init__(self, in_channel, out_channel, n, feature_reduce = False) :
    super().__init__()
    self.n = n

    residual_blocks = [Residual_Block(in_channel, out_channel, feature_reduce = feature_reduce)]

    for i in range(0, n-1) :
      residual_blocks.append(Residual_Block(out_channel, out_channel))
    
    self.residual_blocks = nn.ModuleList(residual_blocks)

  def forward(self, x) :
    for i in range(0, self.n) :
      x = self.residual_blocks[i](x)

    return x

ModuleList를 통해 앞서 구현한 Residual_Block을 n개 쌓아줍니다. 

 

- ResNet

최종 ResNet은 다음과 같습니다.

# ResNet
class ResNet(nn.Module) :
  def __init__(self, n) :
    super().__init__()

    self.init_layer = nn.Sequential(
        nn.Conv2d(3, 16, kernel_size = 3, padding = 1, bias = False),
        nn.BatchNorm2d(16),
        nn.ReLU()
    )

    self.Feature_Block1 = Feature_Block(16, 16, n)

    self.Feature_Block2 = Feature_Block(16, 32, n, feature_reduce = True)

    self.Feature_Block3 = Feature_Block(32, 64, n, feature_reduce = True)

    self.FC = nn.Sequential(
        nn.AvgPool2d(kernel_size = 2, stride = 2),
        Flatten(),
        nn.Linear(4*4*64, 10)
    )

  def forward(self, x) :
    x = self.init_layer(x)
    x = self.Feature_Block1(x)
    x = self.Feature_Block2(x)
    x = self.Feature_Block3(x)
    x = self.FC(x)

    return x

우선 3x3 Conv layer를 통해 channel을 16으로 만들어주고, 

 

이후에 feature map size : 32 -> 16 -> 8, channel : 16 -> 8 -> 4의 순서대로 앞서 만든 Feature_Block을 3개 쌓아줍니다.

 

마지막으로 average pooling을 거친 후 linear layer를 통해 최종 출력이 계산됩니다. 

 

- Result

앞서 구현한 ResNet과 residual learning, short cut을 제외하면 동일한 구조의 plain 모델을 하나 더 구현하여 두 model의 성능을 비교해봤습니다. 

(plain 모델 및 traing 등의 코드는 github 링크에서 확인하실 수 있습니다. )

 

Loss는 training data에 대해, accuracy는 test data에 대해 계산한 결과이며, 

두 model 모두 lr = 5e-3, momentum = 0.9, nesterov 방식으로, 같은 hyperparameters를 사용했습니다. 

 

실험1(상), 실험2(하) / without average pooling

Training을 3번이나 진행하게 되었는데, 그 중 위 2번은 끝에 average pooling을 추가하지 않고 진행한 결과입니다.

 

ResNet을 적용한 모델이 더 빠르게 수렴하는 것을 확인할 수 있습니다. 

 

실험 3

두 번이나 training을 돌리고 나서 논문에서는 끝에 average pooling이 추가되어 있다는 것을 확인해서 추가로 한 번 더 실험을 진행했습니다. 

 

15 epochs로는 plain model이 수렴하기에 부족한 것 같아서 이번엔 25 epoch로 실험을 진행한 결과입니다. 이미 training에 시간을 너무 많이 쓰기도 했고.. ResNet은 빨리 수렴하여 20 epochs만 진행했습니다.

 

마찬가지로 ResNet이 더 빠르게 수렴하고 더 좋은 성능을 보이는 것을 확인할 수 있습니다. 

 

 

 

 

 

 

 

 

+ Recent posts