안녕하세요. 지난 포스팅의 [IC2D] ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design (ECCV2018)에서는 효율적인 모델을 구성하기 위한 4가지 가이드라인과 함께 이를 활용하여 기존의 ShuffleNet V1을 개선한 ShuffleNet V2를 소개하였습니다. 오늘은 효율적인 모델 중에서 아주 유명한 EfficientNet에 대해서 소개시켜드리도록 하겠습니다.
Background
지금까지 보아왔던 모델들 모두 특징 중 하나는 성능이 향상될 수록 그만큼 모델의 복잡도와 학습 시간도 비례에서 상승한다는 사실입니다. 이를 개선하기 위해 지금까지 보았던 MobileNet V1, ShuffleNet V1, MobileNet V2, ShuffleNet V2, Pelee, NASNet과 같은 모델들이 제안되었습니다. 이러한 모델들은 모두 새로운 "구조"이나 "학습 방법"을 제안하는 것으로 효율성을 증가시켰습니다. 하지만, 지금까지 제안된 모델의 중요한 차원들인 깊이 (depth), 너비 (width), 해상도 (resolution)을 조정하는 것만으로도 성능 향상을 달성할 수 있습니다. 오늘 소개할 논문에서는 이 3가지 차원을 균형있게 조절하는 방법인 Compound Scaling을 제안하고 이를 활용하여 EfficientNet을 제안합니다.
Compound Model Scaling
1). Problem Formulation
일단, 저희가 풀고자하는 문제부터 정확하게 정의해보도록 하겠습니다. 이를 위해서는 합성곱 모델부터 정의를 해야하겠죠. 일반적으로 합성곱 모델 $\mathcal{N}$은 연속적인 계층들의 합성함수의 형태로 구성되어 있습니다. 그리고, 저희가 지금까지 구현했던 ResNet 과 같은 모델들의 코드를 보면 여러 개의 stage로 구성되어 있음을 알 수 있습니다. 이때, 각 stage는 다운샘플링을 담당하는 첫번째 계층을 제외하고는 모두 동일한 타입의 합성곱 계층을 구성되어 있습니다. 따라서, 저희는 다음과 같이 쓸 수 있죠.
$$\mathcal{N} = \bigodot_{i = 1, \dots, s} \mathcal{F}^{L_{i}}_{i} (X_{<H_{i}, W_{i}, C_{i}>})$$
여기서, $\mathcal{F}^{L_{i}}_{i}$는 합성곱 계층 $\mathcal{F}_{i}$가 $L_{i}$번 반복되는 $i$번째 stage임을 의미합니다. 그리고 $X_{i}$와 $Y_{i}$는 각각 입력 및 출력 텐서를 의미하죠. 그리고 입력 텐서의 크기가 $<H_{i}, W_{i}, C_{i}>$라고 가정하는 것이죠.
최근 많은 합성곱 신경망을 설계할 때 다음의 규칙을 따를려고 합니다.
(1). 최고의 성능을 낼 수 있는 합성곱 계층 $\mathcal{F}_{i}$를 찾는다.
(2). 주어진 모델에 대해서 깊이 $L_{i}$, 너비 $C_{i}$, 해상도 $(H_{i}, W_{i})$를 최적화시킨다. (Model Scaling)
이제 저희는 (1)번을 고정하고 (2)번에 집중하여 Model Scaling에 집중한다고 가정하겠습니다. 그러면 저희가 한정된 자원에 대해서 3가지 scaling dimension인 깊이, 너비, 해상도에 대해 합성곱 신경망 $\mathcal{N}$을 최적화한다는 것은 다음과 같이 쓸 수 있습니다.
2). Scaling Dimensions
하지만 문제는 최적화된 scaling dimensions $d, w, r$들을 찾는 것은 쉽지 않습니다. 기본적으로 각각의 dimension들은 서로 유기적으로 연결되어 있고 자원의 한정이 달라진다면 다시 최적화된 scaling dimension을 찾아야하기 때문이죠. 이를 해결하기 위해서는 각각의 scaling dimension들인 깊이, 너비, 해상도의 특징을 생각해볼 필요가 있습니다.
(1). 깊이 (Depth), $d$
일반적으로 합성곱 신경망의 성능을 향상시키는 가장 쉬운 방법으로 깊은 모델일수록 더욱 풍부하고 복잡한 특징을 잡아낼 수 있으며 새로운 task에 쉽게 전이시킬 수 있습니다. 하지만, 학습시키기 어렵다는 문제가 있죠.
(2). 너비 (Width), $w$
너비도 모델의 깊이와 마찬가지로 성능을 향상시키는 데 있어 중요한 요소입니다. 특징은 넓은 모델일 수록 해당 계층에서 더욱 많은 특징 맵을 추출하기 때문에 훨씬 fine-grained (세밀한) 정보를 추출할 수 있습니다. 이를 통해, 훨씬 적은 깊이로도 어느정도 유사한 성능을 얻을 수 있기 때문에 학습하기도 쉬운 편입니다. 하지만, 굉장히 넓고 깊이가 얕은 모델은 오히려 고차원 특징을 추출할 수 없기 때문에 성능이 떨어진다는 문제점이 있습니다.
(3). 해상도 (Resolution), $r$
큰 해상도를 가지는 입력 영상을 넣어서 학습하면 상대적으로 더 상세한 정보를 추출할 수 있기 때문에 성능을 향상시키는 데 도움이 될 수 있습니다.
이와 같이 각 scaling dimension에 대한 특징을 알아보았습니다. 이를 통해, 각 dimension 모두 모델의 성능을 향상시키는 데 도움이 된다는 것을 알 수 있죠. 하지만, 모델이 커질수록 단일 dimension에서 얻을 수 있는 성능 향상의 폭은 적습니다.
그림2는 각 dimension에서 scaling을 한다는 것이 무슨 의미인지 설명하고 있습니다.
3). Compound Scaling
위 결과에서 얻을 수 있는 것은 각 scaling dimension이 독립적이지 않고 유기적으로 연결되어 있다는 점 입니다. 예를 들어, 높은 해상도의 영상을 입력으로 받으면 내재된 고차원의 상세한 정보를 추출하기 위해 깊이와 너비를 올려야하는 것처럼 말이죠. 즉, 어떤 모델의 scaling dimension을 증가시킬 때는 각 dimension들을 균형있게 조정해야한다는 것이죠. 이를 위해 본 논문에서는 다음과 같이 Compound Scaling Method를 제안합니다.
여기서 $\phi$는 유저가 직접 선택하는 compound coefficient로 모델의 깊이, 너비, 해상도를 균형있게 증가시키는 역할을 합니다. 이때, 한정된 자원에 맞춰 $\phi$를 조정하면 됩니다. 그리고 $\alpha, \beta, \gamma$는 상수값으로 grid search를 통해 얻은 값입니다. 이때, 전체 연산량은 FLOPs 기준으로 $2^{\phi}$로 한정시킵니다.
EfficientNet Architecture
자, 이제 모델을 scaling하는 방법을 알았습니다. 다음은 어떤 모델을 scaling할 지 결정해야겠죠? 본 논문에서는 MNASNet을 기반으로 최적화된 모델을 baseline으로 삼습니다. 이를 EfficientNet-B0라고 부르도록 하겠습니다.
이 모델을 기준으로 저희는 $\alpha, \beta, \gamma$ 값을 최적화할 것입니다. 다음과 같은 스텝으로 진행됩니다.
STEP1. $\phi = 1$로 고정합니다.
STEP2. $\alpha, \beta, \gamma$에 대해 grid search를 적용합니다. 이를 통해, 본 논문에서는 $\alpha = 1.2, \beta = 1.1, \gamma = 1.15$를 얻었다고 합니다.
STEP3. $\alpha, \beta, \gamma$를 고정하고 $\phi$를 바꾸면서 EfficientNet-B0를 scaling합니다. 이를 통해 EfficientNet-B1 ~ B7까지 얻습니다.
Experiment Results
Implementation Code
import torch
import torch.nn as nn
import torch.nn.functional as F
from layers import BasicConv
class Swish(nn.Module):
def __init__(self, train_beta=False):
super(Swish, self).__init__()
if train_beta:
self.weight = nn.Parameter(torch.Tensor([1.]))
else:
self.weight = 1.0
def forward(self, input):
return input * torch.sigmoid(self.weight * input)
class SqeezeExcitation(nn.Module):
def __init__(self, inplanes, se_ratio):
super(SqeezeExcitation, self).__init__()
hidden_dim = int(inplanes*se_ratio)
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Linear(inplanes, hidden_dim, bias=False)
self.fc2 = nn.Linear(hidden_dim, inplanes, bias=False)
self.swish = Swish()
self.sigmoid = nn.Sigmoid()
def forward(self, x):
out = self.avg_pool(x).view(x.size(0), -1)
out = self.fc1(out)
out = self.swish(out)
out = self.fc2(out)
out = self.sigmoid(out)
out = out.unsqueeze(2).unsqueeze(3)
out = x * out.expand_as(x)
return out
class Bottleneck(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, expand, se_ratio):
super(Bottleneck, self).__init__()
if expand == 1:
self.conv2 = nn.Conv2d(in_channels * expand, in_channels * expand, kernel_size=kernel_size, stride=stride, padding=kernel_size//2, groups=in_channels*expand, bias=False)
self.bn2 = nn.BatchNorm2d(in_channels * expand, momentum=0.99, eps=1e-3)
self.se = SqeezeExcitation(in_channels * expand, se_ratio)
self.conv3 = nn.Conv2d(in_channels * expand, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels, momentum=0.99, eps=1e-3)
else:
self.conv1 = nn.Conv2d(in_channels, in_channels * expand, kernel_size=1, stride=1, padding=0, bias=False)
self.bn1 = nn.BatchNorm2d(in_channels * expand, momentum=0.99, eps=1e-3)
self.conv2 = nn.Conv2d(in_channels * expand, in_channels * expand, kernel_size=kernel_size, stride=stride, padding=kernel_size//2, groups=in_channels*expand, bias=False)
self.se = SqeezeExcitation(in_channels * expand, se_ratio)
self.conv3 = nn.Conv2d(in_channels * expand, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels, momentum=0.99, eps=1e-3)
self.swish = Swish()
self.correct_dim = (stride == 1) and (in_channels == out_channels)
def forward(self, x):
if hasattr(self, 'conv1'):
out = self.conv1(x)
out = self.bn1(out)
out = self.swish(out)
else:
out = x
out = self.conv2(out)
out = self.bn2(out)
out = self.swish(out)
out = self.se(out)
out = self.conv3(out)
out = self.bn3(out)
if self.correct_dim:
out += x
return out
class MBConv(nn.Module):
def __init__(self, in_channels, out_channels, repeat, kernel_size, stride, expand, se_ratio, count_layer=None):
super(MBConv, self).__init__()
layer = []
layer.append(Bottleneck(in_channels, out_channels, kernel_size, stride, expand, se_ratio))
for l in range(1, repeat):
layer.append(Bottleneck(out_channels, out_channels, kernel_size, 1, expand, se_ratio))
self.layer = nn.Sequential(*layer)
def forward(self, x):
out = self.layer(x)
return out
class Upsample(nn.Module):
def __init__(self, scale):
super(Upsample, self).__init__()
self.scale = scale
def forward(self, x):
return F.interpolate(x, scale_factor=self.scale, mode='bilinear', align_corners=False)
class Flatten(nn.Module):
def __init(self):
super(Flatten, self).__init__()
def forward(self, x):
return x.view(x.size(0), -1)
class EfficientNet(nn.Module):
def __init__(self, num_channels=3, num_classes=1000,
width_coef=1.0, depth_coef=1.0, scale=1.0,
dropout_rate=0.2, se_ratio=0.25):
super(EfficientNet, self).__init__()
b0_channels = [32, 16, 24, 40, 80, 112, 192, 320, 1280]
b0_repeats = [1, 1, 2, 2, 3, 3, 4, 1, 1]
kernel_size_list = [3, 3, 5, 3, 5, 5, 3]
stride_list = [1, 2, 2, 2, 1, 2, 1]
expand = [1, 6, 6, 6, 6, 6, 6]
channels = [int(channel * width_coef) for channel in b0_channels]
repeats = [int(repeat * depth_coef) for repeat in b0_repeats]
self.upsample = Upsample(scale)
self.stage1 = BasicConv(num_channels, channels[0], kernel_size=3, stride=2, padding=1, bias=False, activation=False)
self.stage2 = MBConv(channels[0], channels[1], repeats[0], kernel_size_list[0], stride_list[0], expand[0], se_ratio=se_ratio)
self.stage3 = MBConv(channels[1], channels[2], repeats[1], kernel_size_list[1], stride_list[1], expand[1], se_ratio=se_ratio)
self.stage4 = MBConv(channels[2], channels[3], repeats[2], kernel_size_list[2], stride_list[2], expand[2], se_ratio=se_ratio)
self.stage5 = MBConv(channels[3], channels[4], repeats[3], kernel_size_list[3], stride_list[3], expand[3], se_ratio=se_ratio)
self.stage6 = MBConv(channels[4], channels[5], repeats[4], kernel_size_list[4], stride_list[4], expand[4], se_ratio=se_ratio)
self.stage7 = MBConv(channels[5], channels[6], repeats[5], kernel_size_list[5], stride_list[5], expand[5], se_ratio=se_ratio)
self.stage8 = MBConv(channels[6], channels[7], repeats[6], kernel_size_list[6], stride_list[6], expand[6], se_ratio=se_ratio)
self.stage9 = nn.Sequential(
nn.Conv2d(channels[7], channels[8], kernel_size=1, bias=False),
nn.BatchNorm2d(channels[8], momentum=0.99, eps=1e-3),
Swish(),
nn.AdaptiveAvgPool2d((1, 1)),
Flatten(),
nn.Dropout(p=dropout_rate),
nn.Linear(channels[8], num_classes))
def forward(self, x):
x = self.upsample(x)
x = self.swish(self.stage1(x))
x = self.swish(self.stage2(x))
x = self.swish(self.stage3(x))
x = self.swish(self.stage4(x))
x = self.swish(self.stage5(x))
x = self.swish(self.stage6(x))
x = self.swish(self.stage7(x))
x = self.swish(self.stage8(x))
logit = self.stage9(x)
return logit
def efficientnet_b0(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.0, depth_coef=1.0, scale=1.0, dropout_rate=0.2)
def efficientnet_b1(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.0, depth_coef=1.1, scale=240/224, dropout_rate=0.2)
def efficientnet_b2(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.1, depth_coef=1.2, scale=260/224, dropout_rate=0.3)
def efficientnet_b3(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.2, depth_coef=1.4, scale=300/224, dropout_rate=0.3)
def efficientnet_b4(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.4, depth_coef=1.8, scale=380/224, dropout_rate=0.4)
def efficientnet_b5(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.6, depth_coef=2.2, scale=456/224, dropout_rate=0.4)
def efficientnet_b6(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=1.8, depth_coef=2.6, scale=528/224, dropout_rate=0.5)
def efficientnet_b7(num_channels=3, num_classes=1000):
return EfficientNet(num_channels, num_classes, width_coef=2.0, depth_coef=3.1, scale=600/224, dropout_rate=0.5)
if __name__=='__main__':
model = efficientnet_b0()