[Paper Review] Deep Residual Learning for Image Recognition
논문 list는 repo에 정리되어있습니다.
Deep Residual Learning for Image Recognition
K. He, X. Zhang, S. Ren and J. Sun, “Deep Residual Learning for Image Recognition,” 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR), Las Vegas, NV, USA, 2016, pp.
본 논문이 Image Recognition에 큰 영향(인용수 20만 이상)을 끼치는 이유는 크게 2가지가 있다.
-
ResNet의 성능이 매우 좋다.
-
논문에서 제안한 아이디어가 간단하고 이해하기 쉽다.
Main Idea : 깊은 네트워크를 학습시키기 위한 방법으로 residual learning을 제안한다.
Overview
지금까지의 어떤 문제가 있었나?
- depth가 깊이가 모델의 성능에 큰 영향을 끼치는 것은 자명하지만, 이전의 framework는 network가 깊어질수록 훈련시키기 어려운 문제를 가진다. (오버피팅, gradient 소멸, 연산량 증가 등의 문제)
해당 논문에서는 어떻게 해결할 수 있나?
- 함수를 새로 만드는 방법 대신에 residual function(잔차 함수)을 통해 잔차 함수를 learning에 사용하는 것으로 layer를 재구성한다.
- Residual? : 결과의 오류라고 할 수 있는데, 지금까지 평가의 기준으로 삼았지만 이를 이용해 학습하였다.
- 이를 통한 학습은 학습하기 더 쉽고 이전의 모델보다 훨씬 깊은 layer(152 layer)를 가질 수 있다.
해당 연구를 통한 성과는 무엇이 있나?
- ResNet은 VGG net보다 8배 깊은 152 layer를 가지지만 복잡하지 않고 최적화시키기 쉽고, 깊은 깊이에서도 정확도를 얻을 수 있다.
- 여기에 앙상블 기법을 적용하여 2015년 ILVRC에서 우승(3.57% error)를 가진다.
- Image classification 뿐만 아니라, object detection등 전분야에서 탁월한 성능을 보인다.
Introduction
지금까지의 연구를 통해 깊은 ConvNet은 이미지 분류에 있어 중요한 요소이며, 좋은 결과를 위해서는 많은 layer가 필요하다는 것을 알 수 있다.
이전의 VGG-Net이나, GoogLeNet 또한 layer가 깊어짐에 따라 얻는 장점을 적절히 취했다.
필자는 아래와 같은 질문을 남긴다.
Is learning better networks as easy as stacking more layers?
그러나 단순히 more layer에는 다음과 같은 문제를 야기한다.
- Vanishing/exploding gradient
위의 문제(layer가 깊어지면서 역전파로 얻는 gradient가 너무 작아지거나 커지면서 생기는 현상)은 normalized initialization(가중치를 초기에 적절히 초기화), intermediate normalization layer의 방법을 통해 개선하여 수십개의 layer까지는 해결하였다.
그러나 더 깊어질수록, degradation 문제가 발생한다.
degradation 이란?
-
degradation : 모델의 capacity가 너무 증가하면서 train accuracy(test도 마찬가지)가 낮아짐
-
overfitting : 모델의 capacity가 증가하면서 test accuracy가 낮아짐
이 degradation의 문제는 단순히 오버피팅때문만이 아니라 layer가 많아질 수록 생기는 문제이다.
위의 그래프가 그 예시인데, 그림에서 볼 수 있듯이 layer가 더 깊은 layer(56-layer)가 training error, test error 모두 높은 것을 확인할 수 있다.
Core
motive idea : deeper model should produce no higher training error than its shallower counter part
얇은 모델에 identity mapping layer를 추가하면, training error가 높아지진 않아야한다.
identity mapping layer이란?
- 단순히 아무것도 하지 않는 layer
- convolution을 통과하지 않고 값을 전달
- 입력값을 그대로 전달한다는 의미에서 identity
이를 해결하기 위해 해당 논문에서는 deep residual learning framework 를 제안한다.
What is Residual Learning(with shortcut connection)
Residual Learning은 차이만을 학습한다고 해서 잔차(Residual) 학습이다.
변화량만 학습하면 되니, 학습이 쉽다.
Residual Learning의 핵심 기법은 Shortcut connection이다.
Shortcut connection은 Skip connection이라고도 한다.
기존 네트워크는 input $x$를 받고, layer를 거쳐 $F(x)$를 output으로 출력한다.
layer에 들어온 $x$를 받아서 만들고 싶은 이상적인 output이 $H(x)$라고 할 때,
$F(x)$로 $H(x)$를 만드는 것보다 $x + F(x)$로 $H(x)$만드는 것이 학습이 잘 되는 결과를 보였다.
- WHY?
$x$로 $H(x) \approx x$를 만들고 싶다고 가정해본다면, skip-connection이 없는 MLP(Multi Layer Perceptron) 라면 weight matrix는 identity matrix가 되어야한다.
마찬가지로, $x$ 가 $H(x)$와 비슷하다고 가정하면, skip-connection이 있는 MLP 라면 weight matrix는 0 행렬이어야한다.
weight는 애초에 0 근처로 초기화된다.
따라서 $H(x)$를 만드는 것은 skip-connection이 훨씬 쉽다.
- $H(x) \approx x$가 왜나오지?
- 왜 만들고 싶은 것($H(X)$)이 $x$랑 비슷한가?
layer가 상당히 깊다면 입력으로부터 차근차근 값을 바꾸는 것이 이상적일 것이다.
예를들어, 잘 학습된 ResNet-40은 34 layer -> 36 layer 갈 때 값의 변화는 그리 크지 않을 것이다.
∴ $H(x) \approx x$일 것이다.
즉, skip-connection이 있을 때는 $x$랑 비슷한 $H(x)$를 만들기 쉽기 때문에, skip-connection 을 연결해 준다는 것은 "값의 변화가 그리 크지 않을테니 layer 하나에서 모든 것을 할려하지말고 조금씩만 바꿔라"라고 말하는 것과 같다.
그것이 아니라면 함수는 매번 새로운 mapping을 진행하기때문에 학습하기가 더 어렵다.
이를 위의 간단한 숫자로 예시를 들어 설명을 해보자.
$x$의 input이 3.0이 들어오고 $H(x)$가 3.1이라고 할 때, 기존이라면 $F(x)$가 3.1을 훈련시켜야하는 반면 $x+F(x)$에서의 $F(x)$가 0.1만을 훈련시키면 된다.
Residual Learning의 장점을 위의 그래프에서 확인할 수 있는데, ResNet은 일반적으로 Plain Net보다 작은 Response validation을 보였다.
즉, plain net(skip-connection이 없는)은 variation이 큰 것을 알 수 있다.
Resnet의 skip-connection을 통해 Identity Mapping이 되도록 Weight가 0이 되도록 학습이 되었다는 것이다.
그리고 깊은 모델일수록 Response Validation의 크기가 작았다.
또한, $F(x)$가 vanishing gradient 현상으로 학습이 되지않아 zero mapping이 되어도, 최종 output인 $H(x)$는 $x$가 남아 identity mapping이 가능하다.
–> 위의 motivate idea가 여기서 결정적인 역할을 하는데, vanishing gradient의 문제가 발생하여도, 성능 저하가 일어나지 않는다. 성능 저하가 일어나지 않기때문에 더 많은 layer를 쌓을 수 있고 결론적으로 더 깊은 layer를 쌓을 수 있다.
building block : $y=F(x,{W_{i}})+x$
$y$가 output이고, $x$가 input이다.
$F(x, {W_{i}})$는 residual mapping을 의미하고, $x$는 identity mapping(shortcut mapping)을 의미한다.
위의 shortcut connection을 하는 것은 추가적인 파라미터가 필요하지 않고, 복잡도가 증가하지않는다
또한 SGD에 따른 역전파 end-to-end 학습이 가능하다.
이때의 $x$, $F$의 dimension은 같아야한다.
$F = W_{2}\sigma(W_{1}x)$
여기서 $\sigma$는 ReLU계산을 뜻하고, bias는 고려하지않는다.
dimension이 같지않을 때, $y = F(x, {W_{i}} + W_{s} x)$처럼 $W_{s}$ 으로 linear projection을 통해 dimension을 맞춰준다.
위 처럼, input 값과 residual mapping한 값의 dimension이 같지 않다면, 아래의 그림처럼 projection을 통해 dimension을 맞춰준다.
Compare Architectures Plain Net vs ResNet
Example network architectures for ImageNet. Left: the VGG-19 model [40] (19.6 billion FLOPs) as a reference. Middle: a plain network with 34 parameter layers (3.6 billion FLOPs). Right: a residual network with 34 parameter layers (3.6 billion FLOPs). The dotted shortcuts increase dimensions. Table 1 shows more details and other variants.
위는 VGG-net과 VGG-net과 유사한 34-layer net, 34-residual net을 비교한 그림이다.
34-layer net과 34-residual net의 차이점은 오직 residual function을 추가한 것이다.
이때 점선은 입력단과 출력단의 dimension이 일치하지 않아, dimension을 맞추는 shortcut connection이 이용된 부분이다.
비교해보면 VGG와 비교했을 때 FLOPs(계산 복잡도의 지표)는 감소한 것을 확인할 수 있다.(=VGG보다 연산량이 감소했다.)
입력단과 출력단의 dimension이 동일할때는 identity mapping 사용할 수 있다.
dimension이 다르면, (A) 한 쪽에 padding을 하고 identity mapping하는 방법과 (B) projection 연산을 활용한 shortcut connection 방법을 이용한다.
Down sampling
- plain net(VGG-net)에서는 max pooling을 통해 이미지 크기를 축소시켰지만, ResNet은
stride=2
로 대체했다. - 위를 통해, 복잡도가 감소한다.
Experiment & Result
위는 (ImageNet에 적용한)ResNet의 layer 수에 따른 architecture을 보인다.
Plain network
plain network는 layer가 깊어질수록, 성능이 안 좋아지고, ResNet은 layer가 깊어질 수록 성능이 좋아지는 것을 확인 할 수 있다.
필자는 plain network의 이러한 문제가 vanishing gradient 때문이라고 하지 않는다.
학습 과정에서 forward, backward signal이 사라지는 문제는 발견되지 않았으며, 이러한 문제는 exponentially low convergence rate (극단적으로 낮은 수렴율)때문이라고 필자는 추정한다.
ResNet
Identity vs Projection
(A) zero-padding을 통해 dimension을 증가시키고, identity mapping
(B) dimension이 증가할 때 마다 projection 연산
(C) 모든 shortcut에 대해서 항상 projection 연산
성능을 확인해보면, 성능 자체는 (C)가 가장 성능이 좋은 것으로 확인이 된다.
다만 본 논문에서는 projection shortcut이 필수라고 할 정도로 큰 차이라고 보지 않는다.
기본적으로 identity shortcut을 이용해 성능을 많이 개선시킬 수 있기때문에, 해당 논문에서는 메모리, 시간 복잡도, 모델 size를 줄이기 위해 (C)를 이용하지않는다.
Bottleneck architecture
오른쪽 그림(Bottleneck architecture)을 통해 확인하면, 초반에 1x1, 64를 사용하고 중간에는 3x3, 64 filter를 사용하고 마지막에는 1x1, 256을 이용하는 것을 확인할 수 있다.
(위와 같이 중간 볼록한 구조를 Bottleneck Architecture이라고 한다.)
1x1 Layer는 Dimension을 줄였다가 키우는 역할을 한다
이유는 3x3 Layer의 Input과 Output Dimension을 작게 만들기 위해서이다.
ResNet은34-layer까지는 Basic Block를 사용하고, 더 깊은 구조에서는 bottleneck 구조를 사용했다.
Basic Block은 3x3 convolution + 3x3 convolution의 구조를 가진다.
깊이가 50층 이상일 경우 parameter가 너무 많아지기때문에, residual block으로 bottleneck block을 사용하여 층을 쌓는다.
그림과 같이 1x1 conv –> 3x3 conv –> 1x1 conv로 구성되며, 처음 1x1 conv에서 차원을 축소하여, 3x3 layer에서는 작은 입출력 값을 갖게돼 연산 부담을 줄이고, 마지막 1x1 conv에서 차원을 다시 늘려준다.
위의 Bottleneck 구조의 예시를 보면, Layer가 1개 더 많음에도 불구하고 Time Complexity는 둘이 비슷하다.
여기서 Identity Shortcuts( = identity mapping by shortcut, Identity mapping by shortcut)은 중요한 역할을 한다. 만약에 Identity Shortcut이 Projection으로 바뀐다면 Time Complexity와 모델의 크기는 두배가 된다.
따라서 Identity Shortcut은 Bottleneck 구조에서 효율적인 구조를 위해 꼭 필요하다.
50 - layer ResNet
기존에 만들었던 34-layer에 3-layer bottleneck block을 추가해서 50-layer ResNet을 만든다.(option (B) dimension이 증가할 때 마다 projection 연산)
깊이가 50 layer ResNet을 34 layer ResNet와 비교했을 때, 파라미터와 계산속도는 거의 비슷한 것을 확인할 수 있다.
101-layer and 152-layer ResNet
마찬가지로, 101 layer ResNet과 152 layer ResNet은 depth가 상당히 증가했음에도 여전히 낮은 복잡성을 가진다.
layer를 깊게 쌓으면 더욱 좋은 성능을 확인할 수 있는 것을 확인 할 수 있고, degradation problem이 관찰되지 않는다.
ensemble을 통해 가장 좋은 성능(3.57%)을 보일 수 있다.
Exploring Over 1000 Layers
그럼 layer를 계속 늘리면 어떨까?
위 질문에 대한 답으로, 1202개의 Layer를 가진 모델을 실험하였고, Optimization 과정의 문제는 없었다.
위의 Table을 보면 꽤 괜찮은 성능을 보이고 있다.
하지만 여기에도 문제들이 있었다. Training Error는 두 모델이 비슷했지만, 1202 Layer의 모델이 110 Layer 모델보다 성능이 좋지 않았다. 그 이유를 여기서는 Overfitting을 이야기한다.
1202 모델은 ImageNet을 학습하기에는 너무나도 큰 모델이다.
(여기서 말하는 큰 모델은 파라미터 수가 많다는 것이다. )
Conclusion
결론적으로 shortcut connection은 덧셈 연산량이 증가할 뿐, 학습 파라미터의 수에는 큰 영향이 없으며, 더 많은 수의 레이어를 갖는 깊은 모델도 잘 학습하고, 심지어 더 쉽고 빠르게 학습이 가능하다는 것이다.
ResNet은 현재도 많은 모델들의 대표적인 backbone으로 사용되고 있으며, 발전된 형태의 inception-resnet이나 resnext등도 많이 공개되어있다.
Code
# 공통 Arguments
## pretrained (bool): If True, returns a model pre-trained on ImageNet
## progress (bool): If True, displays a progress bar of the download to stderr
# 기본 ResNet 34층
def resnet34(pretrained=False, progress=True, **kwargs):
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress, **kwargs)
# 기본 ResNet 50층
def resnet50(pretrained=False, progress=True, **kwargs):
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs)
# Wide ResNet
def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs)
# ResNext
def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
kwargs['groups'] = 32 # input channel을 32개의 그룹으로 분할 (cardinality)
kwargs['width_per_group'] = 4 # 각 그룹당 4(=128/32)개의 채널으로 구성.
# 각 그룹당 channel 4의 output feautre map 생성, concatenate해서 128개로 다시 생성.
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs)
def _resnet(arch, block, layers, pretrained, progress, **kwargs):
r"""
- pretrained: pretrained된 모델 가중치를 불러오기 (saved by caffe)
- arch: ResNet모델 이름
- block: 어떤 block 형태 사용할지 ("Basic or Bottleneck")
- layers: 해당 block이 몇번 사용되는지를 list형태로 넘겨주는 부분
"""
model = ResNet(block, layers, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls[arch], progress=progress)
model.load_state_dict(state_dict)
return model
- Convolution Layer
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
r"""
3x3 convolution with padding
- in_planes: in_channels
- out_channels: out_channels
- bias=False: BatchNorm에 bias가 포함되어 있으므로, conv2d는 bias=False로 설정.
"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
Blocks: BasicBlock, Bottleneck
- BasicBlock
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
r"""
- inplanes: input channel size
- planes: output channel size
- groups, base_width: ResNext나 Wide ResNet의 경우 사용
"""
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Basic Block의 구조
self.conv1 = conv3x3(inplanes, planes, stride) # conv1에서 downsample
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# short connection
if self.downsample is not None:
identity = self.downsample(x)
# identity mapping시 identity mapping후 ReLU를 적용합니다.
# 그 이유는, ReLU를 통과하면 양의 값만 남기 때문에 Residual의 의미가 제대로 유지되지 않기 때문입니다.
out += identity
out = self.relu(out)
return out
- Bottleneck
class Bottleneck(nn.Module):
# Bottleneck in torchvision places the stride for downsampling at 3x3 convolution(self.conv2)
# while original implementation places the stride at the first 1x1 convolution(self.conv1)
# according to "Deep residual learning for image recognition"https://arxiv.org/abs/1512.03385.
# This variant is also known as ResNet V1.5 and improves accuracy according to
# https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch.
expansion = 4 # 블록 내에서 차원을 증가시키는 3번째 conv layer에서의 확장계수
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# ResNext나 WideResNet의 경우 사용
width = int(planes * (base_width / 64.)) * groups
# Bottleneck Block의 구조
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation) # conv2에서 downsample
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
# 1x1 convolution layer
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# 3x3 convolution layer
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
# 1x1 convolution layer
out = self.conv3(out)
out = self.bn3(out)
# skip connection
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
- ResNet class
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
# default values
self.inplanes = 64 # input feature map
self.dilation = 1
# stride를 dilation으로 대체할지 선택
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
r"""
- 처음 입력에 적용되는 self.conv1과 self.bn1, self.relu는 모든 ResNet에서 동일
- 3: 입력으로 RGB 이미지를 사용하기 때문에 convolution layer에 들어오는 input의 channel 수는 3
"""
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
r"""
- 아래부터 block 형태와 갯수가 ResNet층마다 변화
- self.layer1 ~ 4: 필터의 개수는 각 block들을 거치면서 증가(64->128->256->512)
- self.avgpool: 모든 block을 거친 후에는 Adaptive AvgPool2d를 적용하여 (n, 512, 1, 1)의 텐서로
- self.fc: 이후 fc layer를 연결
"""
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2, # 여기서부터 downsampling적용
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
r"""
convolution layer 생성 함수
- block: block종류 지정
- planes: feature map size (input shape)
- blocks: layers[0]와 같이, 해당 블록이 몇개 생성돼야하는지, 블록의 갯수 (layer 반복해서 쌓는 개수)
- stride와 dilate은 고정
"""
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
# the number of filters is doubled: self.inplanes와 planes 사이즈를 맞춰주기 위한 projection shortcut
# the feature map size is halved: stride=2로 downsampling
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
# 블록 내 시작 layer, downsampling 필요
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion # inplanes 업데이트
# 동일 블록 반복
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))
return nn.Sequential(*layers)
def _forward_impl(self, x):
# See note [TorchScript super()]
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
def forward(self, x):
return self._forward_impl(x)
__init__
에서 처음 conv1층과 마지막층(pooing과 fully connected)
이외에는 _make_layer
함수로 모델의 제일 큰 단위의 층을 생성 및 정의한다..
_make_layer
함수는 논문의 conv2_X, conv3_x, conv4_X, conv5_x을 구현하며 각 층에 해당하는 block을 갯수에 맞게 생성 및 연결시켜주는 역할
이렇게 생성자에서 정의된 층들은 forward
함수로 모델에 대한 feedforward를 진행한다.
이후 학습에서는 loss를 지정한 후 해당 모델에 대한 backpropagation을 진행한다.