이번 포스팅에서는 Google Brain에서 2019년에 발표한 EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks(Image Classfication) 논문에 대해 리뷰하려고 한다. 

1. Introduction


본 논문에서는 어떻게  Network를 확장해야 효율적일지에 대한 연구가 진행되었고 그 결과 기존 Network보다 파라미터 대비 정확도가 높은 효율적인 Network를 제시하였으며 효율적인 Network라는 이름을 본따 EfficientNet으로 정하였다. 위 사진을 보면 EfficientNet이 SOTA image classification network보다 효율적인 모델임을 알 수 있다.(B0~B7는 모델 사이즈를 의미) 


저자들은 기존에 모델을 Scaleing up하려고 시도를 했던 Network들은 위 그림과 같이 Channel(Width)을 늘리거나 Layer(Depth)를 많이 쌓거나 혹은 Input size(Resolution)을 키우는 방식을 따로 1개씩만 적용을 했다는 것에 대해 의문점을 가졌고 이 세가지 방법을 잘 종합하며 모델을 확장시키는 방법을 제시하였다. 그 방법을 논문에서는 Compound Scaling이라고 하고있다.

2. Compound Model Scaling

Depth (d): Network가 깊어지게되면 좀 더 풍부하고 복잡한 특징을 추출할 수 있고 다른 task에 일반화하기 좋지만 vanishing gradient problem이 생기게 된다. 이를 해결하기위해 ResNet의 skip connection, batch normalization 등 다양한 방법들이 있지만 그렇다 하더라도 너무 깊은 Layer를 가진 모델의 성능은 더 좋아지지 않는다. (ResNet101 과 ResNet1000의 정확도가 비슷한 것 처럼.)

Width (w): 보통 Width(Channel)는 작은 모델을 만들기 위헤 scale down(MobileNet 등)을 하는데에 사용되었지만 더 넓은 channel은 더 세밀한 특징을 추출할 수 있고 train하기가 더 쉽다. width의 증가에 따른 성능은 빠르게 saturate되는 현상이 있다.

Resolution (r): 높은 해상도의 이미지를 input으로 사용할때 모델은 더욱 세밀한 패턴을 학습할 수 있기 때문에 성능을 높이기 위해서 Resolution을 크게 가져가고 있으며 최근에는 480x480(GPipe) 600x600(object detection)의 size를 사용하고 있다. 하지만 마찬가지로 너무 큰 해상도는 효율적이지 않다.


위 그래프는 각각 width, depth, resolution을 키웠을 때의 FLOPS(Floating point Operation Per Second)와 Accuracy를 보여준다. width, depth, resolution 모두 80% accuracy까지만 빠르게 saturate(포화)되고 그 이후로의 성능향상은 한계가 있을을 알 수 있다.

그래서 저자들은 width, depth, resolution를 조합하는 Compound Scaling을 아래와 같이 진행하였다.


  • 먼저 depth를 α, width를 β, resolution을 γ로 만들고 ϕ=1 일때의 α x β^2 x γ^2 ≈ 2를 만족하는 α, β, γ를 grid search를 통해 찾는다. (논문에서 찾은 값은 α=1.2, β=1.1, γ=1.15 이다.)
  • 여기서 width와 resolution에 제곱항이 있는 이유는 depth(Layer)가 2배 증가하면 FLOPS는 2배가 되지만 width와 resolution은 그렇지 않기 때문이다.
  • width는 이전 레이어의 출력, 현재 레이어의 입력 이렇게 두곳에서 연산이 이루어 지므로 width가 2배가 되면 4배의 FLOPS가 되고 resolution은 가로 x세로 이기 때문에 당연히 resolution이 2배가 되면 4배의 FLOPS가 된다.
  • grid search를 통해 α, β, γ를 찾았다면 ϕ(0, 0.5, 1, 2, 3, 4, 5, 6)를 사용해 최종적으로 기존 width, depth, resolution에 곱할 factor를 만들게 된다.(파이의 변화로 B0~B7까지의 모델을 설계하였음)

3. EfficientNet Architecture


  • 위 그림은 scaling을 하지 않은 기본 B0모델 구조이다. Operator의 MBConv는 Mobilenet v2에서 제안된 inverted residual block을 의미하고 바로 옆에 1 혹은 6은 expand ratio이다.
  • 이 논문도 마찬가지로 ImageNet dataset의 size인 224x224를 input size로 사용하였다.
  • Activation function으로 ReLU가 아닌 Swish(혹은 SiLU(Sigmoid Linear Unit))를 사용하였다. Swish 는 매우 깊은 신경망에서 ReLU 보다 높은 정확도를 달성한다고 한다.

  • squeeze-and-excitation optimization을 추가하였다. 


여기서 squeeze-and-excitation에 대해 간단히 설명하자면 위 그림처럼 Squeeze 즉 pooling을 통해 1x1 size로 줄여서 정보를 압축한 뒤에 excitation 즉 압축된 정보들을 weighted layer와 비선형 activation function으로 각 채널별 중요도를 계산하여 기존 input에 곱을 해주는 방식을 말한다.

SENet 논문 : arxiv.org/abs/1709.01507

4. Experiments


위 차트는 ImageNet dataset에 대한 성능 지표이고 EfficientNet의 B0~B7 까지 각각의 성능과 비슷한 SOTA모델을과 비교를 하고 있다. 같은 성능 대비 파라미터와 FLOPS가 EfficentNet 모델들이 압도적으로 적은 것을 볼 수 있다.


위 그림은 width, height, resolution을 각각 따로 scaling up 했을때와 compund scaling 했을때의 Class Activation Map을 나타낸 사진이다. compound scaling이 각각의 객체들을 잘 담고 있으며 좀더 정확한 것을 볼 수 있다.

이외 굉장히 다양한 실험을 진행하였는데 직접 논문을 찾아보면 좋을 것 같다.


Pytorch 모델 구현

import torch
import torch.nn as nn
from math import ceil
import pdb
from torchsummary import summary

base_model = [
    # expand_ratio, channels, repeats, stride, kernel_size
    [1, 16, 1, 1, 3],
    [6, 24, 2, 2, 3],
    [6, 40, 2, 2, 5],
    [6, 80, 3, 2, 3],
    [6, 112, 3, 1, 5],
    [6, 192, 4, 2, 5],
    [6, 320, 1, 1, 3],
]

phi_values = {
    # tuple of: (phi_value, resolution, drop_rate)
    'b0': (0, 224, 0.2),  # alpha, beta, gamma, depth = alpha ** phi
    'b1': (0.5, 240, 0.2),
    'b2': (1, 260, 0.3),
    'b3': (2, 300, 0.3),
    'b4': (3, 380, 0.4),
    'b5': (4, 456, 0.4),
    'b6': (5, 528, 0.5),
    'b7': (6, 600, 0.5),
}
class CNNBlock(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding, groups=1):
        super(CNNBlock, self).__init__()
        self.cnn = nn.Conv2d(
            in_channels,
            out_channels,
            kernel_size,
            stride,
            padding,
            groups=groups,  # groups=1이면 일반적인 Conv, groups=in_channels 일때만 Depthwise Conv 수행
            bias=False
        )
        self.bn = nn.BatchNorm2d(out_channels)
        self.silu = nn.SiLU()  # SiLU <-> Swish

    def forward(self, x):
        return self.silu(self.bn(self.cnn(x)))
class SqueezeExcitation(nn.Module):
    def __init__(self, in_channels, reduced_dim):
        super(SqueezeExcitation, self).__init__()
        self.se = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),  # C x H x W -> C x 1 x 1
            nn.Conv2d(in_channels, reduced_dim, 1),
            nn.SiLU(),
            nn.Conv2d(reduced_dim, in_channels, 1),
            nn.Sigmoid(),  # 각 채널에 대한 score (0~1)
        )

    def forward(self, x):
        return x * self.se(x)  # input channel x 채널의 중요도
class InvertedResidualBlock(nn.Module):
    def __init__(
            self,
            in_channels,
            out_channels,
            kernel_size,
            stride,
            padding,
            expand_ratio,
            reduction=4,  # squeeze excitation
            survival_prob=0.8,  # for stochastic depth
    ):
        super(InvertedResidualBlock, self).__init__()
        self.survival_prob = survival_prob
        self.use_residual = in_channels == out_channels and stride == 1
        hidden_dim = in_channels * expand_ratio
        self.expand = in_channels != hidden_dim
        reduced_dim = int(in_channels / reduction)

        if self.expand:
            self.expand_conv = CNNBlock(
                in_channels, hidden_dim, kernel_size=3, stride=1, padding=1,
            )

        self.conv = nn.Sequential(
            CNNBlock(
                hidden_dim, hidden_dim, kernel_size, stride, padding, groups=hidden_dim,  # Depthwise Conv
            ),
            SqueezeExcitation(hidden_dim, reduced_dim),
            nn.Conv2d(hidden_dim, out_channels, 1, bias=False),  # point wise conv
            nn.BatchNorm2d(out_channels),
        )

    def stochastic_depth(self, x):
        '''
        vanishing gradient로 인해 학습이 느리게 되는 문제를 완화시키고자 stochastic depth 라는 randomness에 기반한 학습 방법
        Stochastic depth란 network의 depth를 학습 단계에 random하게 줄이는 것을 의미
        복잡하고 큰 데이터 셋에서는 별다를 효과를 보지는 못한다고 함
        '''
        if not self.training:
            return x

        binary_tensor = torch.rand(x.shape[0], 1, 1, 1, device=x.device) < self.survival_prob
        return torch.div(x, self.survival_prob) * binary_tensor  # torch.div으로 감싼 연산은 stochastic_depth 논문에 나와있음.

    def forward(self, inputs):
        x = self.expand_conv(inputs) if self.expand else inputs

        if self.use_residual:
            return self.stochastic_depth(self.conv(x)) + inputs
        else:
            return self.conv(x)
class EfficientNet(nn.Module):
    def __init__(self, version, num_classes):
        super(EfficientNet, self).__init__()
        width_factor, depth_factor, dropout_rate = self.calculate_factors(version)
        last_channels = ceil(1280 * width_factor)
        self.features = self.create_features(width_factor, depth_factor, last_channels)
        self.pool = nn.AdaptiveAvgPool2d(1)  # stage9 pool
        self.classifier = nn.Sequential(  # stage9 FC
            nn.Dropout(dropout_rate),
            nn.Linear(last_channels, num_classes),
        )

    def calculate_factors(self, version, alpha=1.2, beta=1.1):
        phi, res, drop_rate = phi_values[version]
        depth_factor = alpha ** phi
        width_factor = beta ** phi
        return width_factor, depth_factor, drop_rate

    def create_features(self, width_factor, depth_factor, last_channels):
        channels = int(32 * width_factor)  # B0의 32는 첫레이어의 channel
        features = [CNNBlock(3, channels, 3, stride=2, padding=1)]  # stage 1
        in_channels = channels

        for expand_ratio, channels, repeats, stride, kernel_size in base_model:
            out_channels = 4 * ceil(int(channels*width_factor) / 4)  # SqueezeExcitation reduction에서 4로 잘 나눠지도록 처리
            # pdb.set_trace()
            layers_repeats = ceil(repeats * depth_factor)

            for layer in range(layers_repeats):  # stage 2~8
                features.append(
                    InvertedResidualBlock(
                        in_channels,
                        out_channels,
                        kernel_size=kernel_size,
                        stride=stride if layer == 0 else 1,
                        padding=kernel_size // 2,  # if k=1:pad=0, k=3:pad=1, k=5:pad=2
                        expand_ratio=expand_ratio
                    )
                )
                in_channels = out_channels


        features.append(
            CNNBlock(in_channels, last_channels, kernel_size=1, stride=1, padding=0)  # stage9 Conv 1x1
        )

        return nn.Sequential(*features)

    def forward(self, x):
        x = self.pool(self.features(x))
        return self.classifier(x.view(x.shape[0], -1))  # flatten
if __name__ == '__main__':

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    version = 'b0'
    phi, res, drop_rate = phi_values[version]
    batch_size, num_classes = 4, 10
    x = torch.randn((batch_size, 3, res, res)).to(device)
    model = EfficientNet(
        version=version,
        num_classes=num_classes,
    ).to(device)

    print(model(x).shape)
    summary(model, input_size=(3, 224, 224))

간단히 모델을 Test하기 위해 (4, 3, 224, 224) randn tensor를 생성하여 모델의 출력 Size를 확인 하면 torch.Size([4, 10])으로 잘 나오는 것을 볼 수 있다. (torchsummary를 통해 output shape과 parameter수를 확인 할 수 있다.)


End

본 논문은 새로운 Architecture로 적은 파라미터 수로 좋은 성능을 낸 모델을 제시한 것이 아니라 기존의 모델들의 성능을 좀 더 효율적으로 scaling up 시키기 위해 Compound Scale을 제안하였다. 저자들은 후속으로 Object Detection task에도 이 아이디어를 적용한 EfficientDet 논문발표를 하였는데 다음에 리뷰를 할 예정이다.

+ EfficientNet B0 모델을 CIFAR-10 데이터셋으로 100 epoch으로 학습시켜 보았는데 정확도가 85%까지밖에 나오지 못했다. 하이퍼 파라미터를 좀 더 잘 만져서 적용시켜봐야 할 것같다. Keep Going..

Reference

업데이트:

댓글남기기