Bag of Tricks for Image Classification with Convolutional Neural Networks (He et al., CVPR 2019)에서 소개된 ResNet의 변형 모델들(tweaks)에 대한 PyTorch 코드이다.
논문에서는 두 개의 유명한 변형 모델인 ResNet-B, C를 소개하고, 새로운 구조인 ResNet-D를 제안하고 있다.
ResNet
기본적인 ResNet의 구조는 Input stem과 4개의 stage, 그리고 마지막 output layer로 이루어져 있다.
- Input stem : 7x7 conv와 maxpool로 이루어져 있으며, channel dim을 64로 늘리고 input size를 4배 줄인다.
- Stage : 한 개의 downsampling block과 여러 개의 residual block으로 이루어져 있다.
- Downsampling block : Path A와 path B로 구성되며, 마지막에 두 path의 output을 더한다.
- Path A : 3개의 conv로 구성된 bottleneck 구조로, input size를 2배 줄이고 channel dim을 4배 늘린다.
- Path B: channel size만 Path A와 동일하게 늘리는 1x1 conv 하나로 구성되어 있다.
- Residual block은 downsampling block과 동일한 구조인데, 대신 stride=1로 설정하여 input size는 유지한다.
- Downsampling block : Path A와 path B로 구성되며, 마지막에 두 path의 output을 더한다.
ResNet-B, C, D
다음 세 개의 모델은 ResNet의 일부 구조를 변형하여 성능을 높였다.
ResNet-B는 Torch implementation에서 제안된 구조로, Path A의 1x1 conv w/ stride=2가 input feature map의 3/4를 무시하게 되기 때문에, 이를 방지하기 위해 downsampling block에서 path A의 첫 두 conv block의 구조를 변경했다.
ResNet-C는 Inception-v2에서 제안된 구조로, SENet, DeepLabV3 등 이후 다양한 모델에서 사용된 구조이다. Computational cost를 낮추기 위해 input stem의 7x7 conv를 3x3 conv 3개로 변경했다.
ResNet-D는 본 논문에서 새롭게 제시한 구조로, ResNet-B에서 제안한 바와 같이 Path B의 1x1 conv w/ stride=2 역시 information loss를 야기한다고 판단하여 Path B의 1x1 conv를 avgpool + 1x1 conv 구조로 변경했다.
*세 구조가 모두 별개가 아니라, 이전 구조에 추가로 변경하는 개념이다. ResNet-C는 ResNet-B의 downsampling block 구조를 가져가고, ResNet-D는 ResNet-B의 downsampling block에서 사용한 path A 구조와, ResNet-C의 input stem 구조를 가져간다.
ResNet-50을 기준으로 한 한 각 모델의 ImageNet validation set에 대한 성능은 위와 같다. Top-1, Top-5 accuracy 모두에서 ResNet-D가 가장 높은 성능을 보였으며 (ResNet에 비해 1% 상승), 동시에 computational cost (FLOPs)도 가장 높지만 ResNet과 비교했을 때 15% 정도밖에 차이가 나지 않으며 학습시간은 3% 정도밖에 차이가 나지 않는다고 한다.
PyTorch Implementation
torchvision.models
의 ResNet을 기반으로 downsampling block의 path B와 input stem 부분만 수정했다.
Torchvision의 ResNet이 ResNet-B와 동일하기 때문에 downsampling block의 path A는 수정할 필요가 없었다.
전체 코드: https://github.com/bo-10000/ResNet-D_PyTorch/tree/main
ResNet 코드 (일부 생략)
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().__init__()
...
#input stem
self.input_stem = self._make_input_stem()
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
#stages 1~4
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2, 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])
#output layer
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
...
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
...
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = self._make_downsample(planes, block, stride, norm_layer)
layers = []
layers.append(
block(
self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation, norm_layer
)
)
self.inplanes = planes * block.expansion
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 _make_downsample(self, planes, block, stride, norm_layer):
return nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
def _make_input_stem(self):
norm_layer = self.norm_layer
return nn.Sequential(
nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False),
norm_layer(self.inplanes),
nn.ReLU(inplace=True),
)
def forward(self, x):
# Input stem
x = self.input_stem(x)
x = self.maxpool(x)
# Stages 1~4
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
# Output layer
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
ResNet-D 코드
ResNet
을 상속하여 _make_downsample
과 _make_input_stem
만 수정했다.
만약 ResNet-C를 이용하고 싶으면 ResNet
의 _make_downsample
을 수정하지 않고 그대로 사용하면 된다.
class ResNetD(ResNet):
def _make_downsample(self, planes, block, stride, norm_layer): #conv1x1 -> AvgPool+conv1x1
if stride != 1:
return nn.Sequential(
nn.AvgPool2d(2, stride=stride),
conv1x1(self.inplanes, planes * block.expansion),
norm_layer(planes * block.expansion),
)
else:
return nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion),
norm_layer(planes * block.expansion),
)
def _make_input_stem(self): #conv7x7 -> 3 conv3x3
norm_layer = self.norm_layer
return nn.Sequential(
nn.Conv2d(3, self.inplanes // 2, kernel_size=3, stride=2, padding=1, bias=False),
norm_layer(self.inplanes // 2),
nn.ReLU(inplace=True),
nn.Conv2d(self.inplanes // 2, self.inplanes // 2, kernel_size=3, padding=1, bias=False),
norm_layer(self.inplanes // 2),
nn.ReLU(inplace=True),
nn.Conv2d(self.inplanes // 2, self.inplanes, kernel_size=3, padding=1, bias=False),
norm_layer(self.inplanes),
nn.ReLU(inplace=True),
)
ResNet-50과 ResNet-50-D 모델의 parameter 수와 크기를 torchsummary로 확인한 결과는 다음과 같다. (Input size=[3, 64, 64])
'🌌 Deep Learning > Implementation' 카테고리의 다른 글
[PyTorch Implementation] StyleGAN2 (2) | 2022.09.26 |
---|---|
[PyTorch Implementation] PointNet 설명과 코드 (0) | 2022.08.12 |
[PyTorch Implementation] CBAM: Convolutional Block Attention Module 설명 + 코드 (0) | 2022.04.22 |
[PyTorch Implementation] PyTorch로 구현한 cycleGAN의 loss 부분 설명 (0) | 2021.08.04 |
[PyTorch Implementation] 3D Segmentation model - VoxResNet, Attention U-Net, V-Net (0) | 2020.12.30 |