본 글은 2020-2학기 “컴퓨터 비전” 수업을 듣고, 스스로 학습하면서 개인적인 용도로 정리한 것입니다. 지적은 언제나 환영입니다 :)

5 minute read

본 글은 2020-2학기 “컴퓨터 비전” 수업을 듣고, 스스로 학습하면서 개인적인 용도로 정리한 것입니다. 지적은 언제나 환영입니다 :)


pyTorch를 이용해 ResNet을 구현한 github/KellerJordan의 코드에 대한 개인적인 분석글입니다.

ResNet32을 구현한 것으로 추정됩니다.

picture from Pablo Ruiz's blog

ResNet20 코드 분석

ResNet20

class ResNet(nn.Module):
  ...
  self.layers1 = self._make_layer(n, 16, 16, 1)
  ...

  def _make_layer(self, layer_count, channels, channels_in, stride):
    return nn.Sequential(
      ResBlock(channels, channels_in, stride, ...),
      *[ResBlock(channels) for _ in range(layer_count-1)])

class ResBlock(nn.Module):
  ...

ResNet 모듈 하나만 만들어 모델을 구축한 것이 아니라 ResBlock 모듈을 만들어 사용한 점이 눈에 띈다.

즉, nn.Module을 상속 받은 모듈 내부에 또 다른 모듈을 심어서 모델 구조를 디자인할 수 있음을 보여준다! (굳이 따지자면, dependency를 부여했다는 말)


ResBlock 모듈을 nn.Sequential를 이용해 이어붙였다.

def _make_layer(self, layer_count, channels, channels_in, stride):
  return nn.Sequential(
      ResBlock(channels, channels_in, stride, ...),
      *[ResBlock(channels) for _ in range(layer_count-1)])

[ResBlock(channels) for _ in range(layer_count-1)] 이 부분을 보면 알 수 있듯 내부에 위치한 ResBlock에선 채널수가 유지된다.


코드에서는 layer_count로 변수값으로 지정되어 있는데, default 값은 5라고 한다.

그래서 _make_layer 함수는 채널수를 두 배로 늘리는 ResBlock과 채널수가 유지되는 4개의 ResBlock을 생성한다.

ResBlock은 2개의 conv layer를 갖는데, 따라서 _make_layer가 10개의 conv layer를 생성함을 알 수 있다.


또, [ResBlock(channels) for _ in range(layer_count-1)]는 inline for문을 채용해 코드를 경량화 했다.


그리고 nn.Sequential() 내부에 *[]를 사용했는데, 실제로 list 타입에 *를 붙여서 nn.Sequential()에 전달할 수 있다고 한다. 아래는 예시 코드

import torch.nn as nn
net = nn
layers = [nn.Linear(2, 2), nn.Linear(2, 2)]
net = nn.Sequential(*layers)
print(net)


이 ResNet 코드는 _make_layer() 함수를 세번 정도 호출한다.

class ResNet(nn.Module):
  def __init__(self, ...):
    ...
    self.layers1 = self._make_layer(n, 16, 16, 1)
    self.layers2 = self._make_layer(n, 32, 16, 2)
    self.layers3 = self._make_layer(n, 64, 32, 2)
    ...



다이어그램으로 표현한 구조와 코드를 비교해보자.

picture from Pablo Ruiz's blog

class ResNet(nn.Module):
  def forward(self, x):
    out = self.conv1(x)
    out = self.norm1(out)
    out = self.relu1(out)
    out = self.layers1(out) # in: 16, out: 16
    out = self.layers2(out) # in: 16, out: 32
    out = self.layers3(out) # in: 32, out: 64
    out = self.avgpool(out)
    out = out.view(out.size(0), -1)
    out = self.linear(out) # in: 64, out: 10
    return out

ResNet의 총 conv layer 수를 따지면,

1 + (10 + 10 + 10) + 1 = 32

그래서 이 코드는 ResNet32를 구현한 것이다!


ResBlock

ResNet의 꽃은 skip connection이 구현된 ResBlock 부분이다.

class ResBlock(nn.Module):
  ...
  def forward(self, x):
    residual = x # store residual
    out = self.conv1(x)
    out = self.bn1(out)
    out = self.relu1(out)
    out = self.conv2(out)
    out = self.bn2(out)
    out += residual # skip connection!
    out = self.relu2(out)
    return out

본인은 이 코드를 보고나서야 비로소 ResNet이 완전히 이해가 되었다 ㅎㅎ

참고로 ResBlock에서 사용된 layer 수는 2개이다.


residual projection options

이 구현에선 residual을 바로 더하는 게 아니라 self.projection을 한번 거치게 하는 옵션도 구현을 했다.

class ResBlock(nn.Module):
  def __init__(self, num_filters, ...):
    ...
    if res_option == 'A':
      self.projection = IdentityPadding(num_filters, channels_in, stride)
    elif res_option == 'B':
      self.projection = ConvProjection(num_filters, channels_in, stride)
    elif res_option == 'C':
      self.projection = AvgPoolPadding(num_filters, channels_in, stride)
    ...
  def forward(self, x):
    ...
    if self.projection: # residual projection!
      residual = self.projection(x)
    ...


각각 2차원의 residual 이미지를 처리하는 옵션들로

  • residual 이미지를 그대로 보내기도 하고; IdentityPadding()
  • residual 이미지를 Convolution 하기도 하고; ConvProjection()
  • residual 이미지를 Average Pooling 하기도 한다; AvgPoolPadding()

residual projection 옵션들에 대한 더 자세한 내용은 이 링크를 통해 확인할 수 있다!



KellerJordan의 ResNet은 모델 구현을 깔끔하게 잘 해두어서 정말 좋은 코드라고 생각한다 ㅎㅎ


ResNet을 구현한 또다른 코드도 있다.

github/kuangliu

이 코드에선 ResNet18, ResNet34, ResNet50, ResNet101, ResNet152까지 모두 구현되어 있다.

이 코드도 모듈 분리를 잘 해두어 깔끔한 편이지만, 주석이 부족한 점이 아쉽다.