🌌 Deep Learning/Implementation

[PyTorch Implementation] ResNet-B, ResNet-C, ResNet-D, ResNet Tweaks

복만 2022. 6. 5. 21:49

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는 유지한다.

 

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 Binput stem 부분만 수정했다.

Torchvision의 ResNet이 ResNet-B와 동일하기 때문에 downsampling block의 path A는 수정할 필요가 없었다.

 

전체 코드: https://github.com/bo-10000/ResNet-D_PyTorch/tree/main

 

GitHub - bo-10000/ResNet-D_PyTorch: PyTorch implementation of ResNet-D from Bag of Tricks for Image Classification with Convolut

PyTorch implementation of ResNet-D from Bag of Tricks for Image Classification with Convolutional Neural Networks (https://arxiv.org/pdf/1812.01187) - GitHub - bo-10000/ResNet-D_PyTorch: PyTorch im...

github.com

 

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])

 

반응형