안녕하세요. 지난 포스팅의 [IC2D] Wide Residual Networks (BMVC2016)에서는 모델의 깊이 (depth)보다는 너비 (width)에 초점을 맞추어 깊은 모델이 아니더라도 충분히 좋은 성능을 낼 수 있음을 증명하였습니다. 오늘은 효율적인 모델로 유명한 MobileNet에 대해서 소개해드리도록 하겠습니다.
Background
일단, 저희가 지금까지 보았던 영상 분류 모델은 VGGNet, GoogLeNet, ResNet, PreAct ResNet, InceptionNet, WRN입니다. 그나마 WRN이 효율을 중시하여 깊이보다는 너비에 초점을 맞추었지만 여전히 복잡도는 높은 편입니다. 2017년에 들어서 연구자들의 관점은 크게 바뀌게 됩니다. 이제 성능은 충분히 올랐다고 느꼈기 때문에 이를 실제 생활에 활용하는 것이 중요하다고 느끼게 되었죠.
여러분들이 핸드폰으로 웹서핑을 한다고 가정해 보겠습니다. 일반적으로 저희가 페이지를 바꾸면 바로바로 바뀌는 것을 볼 수 있을 텐데요, 만약 0.5초가 걸리면 어떨까요? 1초가 더 걸리면 또 어떨까요? 굉장히 스트레스받을 것입니다. 지금까지 등장한 심층신경망들도 마찬가지입니다. 성능은 좋지만 이를 핸드폰과 같은 휴대용 장치에 이식하게 되면 당연히 속도는 떨어지게 되겠죠.
본 논문에서는 이러한 문제점을 인지하고 휴대용 장치에 이식할 수 있는 적은 파라미터 (공간복잡도)와 연산 시간 (시간복잡도)를 가지는 모델인 MobileNet을 제안하였습니다. 하지만, 컴퓨터 공학 전반에 걸쳐 효율성과 성능은 반비례 관계를 가지기 때문에 성능은 최대한 유지하면서 파라미터와 연산시간을 줄이는 것이 제일 중요한 목표라고 봐야 합니다.
MobileNet Architecture
위 그림을 MobileNet의 기본적인 목표를 보여주고 있습니다. 휴대용 장치에서 다양한 작업을 수행할 수 있는 모델인 MobileNet을 제안하고 싶은 것이죠. 이를 위해서는 Depthwise Separable Filter에 대한 개념을 먼저 이해해야합니다.
Depthwise Separable Convolution
MobileNet은 기본적으로 Depthwise Separable Convolution으로 이루어져 있습니다. 이는 기존에 입력 특징 맵이 들어오면 한번에 처리하는 것이 아니라 부분적으로 처리하겠다는 것을 의미하죠. 위 그림을 보시면 보다 쉽게 이해할 수 있습니다.
(a)는 일반적으로 저희가 알고 있는 합성곱 연산입니다. $D_{K}$는 필터의 해상도, $M$은 입력 채널의 개수, $N$은 출력 채널의 개수를 의미하죠. 그러면 합성곱 연산 $K$의 필터 차원은 $D_{K} \times D_{K} \times M \times N$이 됩니다. 그리고 다음과 같이 연산을 정의할 수 있죠.
$$\mathbf {G}_{k, l, n} = \sum_{i, j, m} \mathbf {K}_{i, j, m, n} \cdot \mathbf {F}_{k + i - 1, l + j - 1, m}$$
여기서 합성곱 연산의 stride와 padding은 전부 1로 가정합니다. 그러면 입력 특징맵의 해상도가 $D_{F} \times D_{F} \times M$이라고 가정하면 합성곱 연산량은 얼마나 될까요? 답은 아래와 같이 계산됩니다.
$$D_{K} \times D_{K} \times M \times N \times D_{F} \times D_{F}$$
위 수식을 통해 알 수 있는 것은 일반적인 합성곱 연산의 복잡도는 필터의 크기 $D_{K}$, 입력 특징맵의 채널 개수 $M$, 출력 특징맵의 채널 개수 $N$ 그리고 입력 특징 맵의 해상도 $D_{F}$에 의해 결정된다는 것이죠.
일단, 어차피 필터 사이즈는 $3 \times 3$으로 쓴다고 가정하고 출력 특징맵의 크기도 저희가 바꿀 수는 없기 때문에 저희가 조절할 수 있는 것은 $M$과 $N$ 밖에 없습니다. 본 논문에서는 하나의 일반적인 합성곱 연산을 2개의 연산 과정으로 쪼개서 연산합니다.
1). Depthwise Convolution
(b)을 보시면 첫번째 연산과정인 Depthwise convolution에 대해서 설명하고 있습니다. 과정은 아주 단순합니다. "Depthwise"이기 때문에 입력 특징 맵의 각 채널에 대해서 합성곱 연산을 수행하죠. 따라서, $M$개의 특징 맵이 입력되면 동일하게 $M$개의 특징 맵이 출력되어 연산량은 다음과 같습니다.
$$D_{K} \times D_{K} \times M \times D_{F} \times D_{F}$$
즉, Depthwise convolution은 $N$에 대해 자유롭게 만드는 연산이라고 해석할 수 있습니다. 이를 수식으로 표현하면 아래와 같이 쓸 수 있죠.
$$\hat {\mathbf {G}}_{k, l, m} = \sum_{i, j} \hat {\mathbf {K}}_{i, j, m} \cdot \mathbf {F}_{k + i - 1, l + j - 1, m}$$
2). Pointwise Convolution
(c)를 보시면 두번째 연산과정인 Pointwise convolution에 대해서 설명하고 있습니다. 이 역시 과정은 아주 단순합니다. "Pointwise"이기 때문에 입력 특징 맵의 각 spatial resolution에 대해 $1 \times 1$ 합성곱 연산을 수행하죠. 이 과정에서 $M$개의 입력 특징 맵은 $N$개의 출력 특징 맵을 생성하게 됩니다. 따라서 연산량은 다음과 같죠.
$$M \times N \times D_{F} \times D_{F}$$
즉, Pointwise convolution은 $D_{K}$에 대해 자유롭게 만드는 연산이라고 해석할 수 있습니다.
3). Depthwise Separable Convolution
이제 두 연산을 sequential하게 적용하게 되면 저희가 원하던 Depthwise Separable Convolution이 되는 것입니다. 연산량은 Depthwise/Pointwise Convolution이 따로 적용되기 때문에 두 연산량의 합이라고 볼 수 있습니다.
$$D_{K} \times D_{K} \times M \times D_{F} \times D_{F} + M \times N \times D_{F} \times D_{F}$$
마지막으로 일반적인 합성곱과 Depthwise Separable Convolution 사이의 연산량을 비교해 보면 다음과 같습니다.
$$\frac {D_{K} \times D_{K} \times M \times D_{F} \times D_{F} + M \times N \times D_{F} \times D_{F}}{D_{K} \times D_{K} \times M \times N \times D_{F} \times D_{F}} = \frac {1}{N} + \frac {1}{D^{2}_{K}}$$
일반적으로 저희는 $3 \times 3$ 크기의 필터를 사용하기 때문에 8~9배 정도 연산량이 줄어드는 것을 관찰할 수 있죠.
MobileNet Architecture
위 표는 Depthwise Separable Convolution을 적용한 MobileNet의 구조입니다.
MobileNet Hyperparameters
MobileNet에서는 두 가지 하이퍼파라미터가 존재합니다. 두 하이퍼파라미터 모두 추가적으로 모델의 크기를 줄여줄 수 있는 요소로 당연하겠지만 성능에 굉장히 큰 영향을 주는 하이퍼파라미터이기 때문에 잘 결정해야 합니다.
1). Width Multiplier: Thinner Models
지난 포스팅의 WRN에서도 보았지만 모델의 너비를 늘리는 것은 모델의 성능을 높이는 좋은 요소 중 하나입니다. 하지만, 그만큼 파라미터의 수와 연산량이 급격하게 늘어나게 되죠. 본 논문에서는 WRN과는 반대로 너비를 줄이는 방법을 채택하여 연산량을 줄이는 것을 우선적으로 고려합니다. 이를 $\alpha$라고 두겠습니다. 그러면 Depthwise Separable Convolution을 적용하면 연산량을 얻을 수 있습니다.
$$D_{K} \times D_{K} \times (\alpha M) \times D_{F} \times D_{F} + (\alpha M) \times (\alpha N) \times D_{F} \times D_{F}$$
여기서 $\alpha \in (0, 1]$이므로 $\alpha$는 항상 너비를 줄이거나 같게 만듭니다. 본 논문에서는 $\alpha = 1, 0.75, 0.5, 0.25$로 세팅하여 학습을 진행하였으며 $\alpha = 1$인 경우에는 MobileNet-baseline이라고 부르고 $\alpha < 1$인 경우에는 reduced MobileNet이라고 언급합니다. 위 연산량을 보시면 아시겠지만 $\alpha^{2}$만큼 연산량이 감소하는 것을 볼 수 있습니다. 하지만, 그만큼 성능이 크게 감소하기 때문에 신중히 결정해야 하죠.
2). Resolution Multiplier: Reduced Representation
연산량을 계산하는 수식을 보면 큰 영향을 끼치는 요소 중 하나는 입력 특징맵의 크기 $D_{F}$입니다. 이를 줄이기 위해 $\rho$를 추가하고 연산량을 계산하면 다음과 같습니다.
$$D_{K} \times D_{K} \times (\alpha M) \times (\rho D_{F}) \times (\rho D_{F}) + (\alpha M) \times (\alpha N) \times (\rho D_{F}) \times (\rho D_{F})$$
여기서 $\rho \in (0, 1]$이므로 $\rho$는 항상 특징 맵의 해상도를 줄이거나 같게 만듭니다. 실질적으로 $\rho$는 입력 영상의 크기에 따라서 암묵적으로 결정되는 요소입니다. 예를 들어, ImageNet을 학습한다고 가정했을 때 해상도가 $224 \times 224$라면 $\rho = 1$입니다. 여기서 더 작은 해상도인 $192 \times 192$로 크기를 낮추어 학습하게 되면 $\rho = \frac {192}{224} \approx 0.857$이 되는 것이죠. 본 논문에서는 영상의 해상도를 $224 (\rho = 1), 192 (\rho = 0.857), 160 (\rho = 0.714), 128 (\rho = 0.571)$로 세팅하여 학습을 진행하였습니다. 모델의 너비를 줄이는 것과 마찬가지로 영상의 해상도를 줄임으로써 $\rho^{2}$만큼 연산량이 감소하는 것을 볼 수 있습니다.
Experiment Results
먼저, 해상도 감소 비율 $\rho$는 고정하고 너비 감소 비율 $\alpha$를 바꾸면서 실험을 진행합니다. 연산량과 파라미터의 개수는 급격하게 줄어드는 것을 볼 수 있지만 그만큼 성능도 크게 하락하고 있습니다.
다음으로 너비 감소 비율 $\alpha$는 고정하고 해상도 감소 비율 $\rho$를 바꾸면서 실험을 진행합니다. 해상도는 모델의 파라미터 개수와는 독립 성분이기 때문에 파라미터 개수는 차이가 없습니다. 다만 연산량은 감소하는 것을 볼 수 있죠. 이 역시 해상도가 줄어들수록 성능이 크게 감소하는 것을 볼 수 있습니다.
이제 MobileNet-baseline을 GoogLeNet과 VGGNet과 비교해 봅니다. 연산량이 GoogLeNet과 VGGNet에 비해 각각 0.37, 0.04 만큼 감소하였습니다. 심지어 GoogLeNet과 비교하면 오히려 성능이 상승하였고 VGGNet과 비교해 보면 성능은 0.9% 밖에 차이가 안 나기 때문에 괜찮은 결과라고 볼 수 있습니다.
Implementation Code
import torch.nn as nn
class DepthwiseSeparableConv(nn.Module):
def __init__(self, in_channels, out_channels, stride, padding, groups):
super(DepthwiseSeparableConv, self).__init__()
self.depthwise_conv = nn.Sequential(
nn.Conv2d(in_channels, in_channels, kernel_size=(3, 3), stride=stride, padding=padding, groups=groups, bias=False),
nn.BatchNorm2d(in_channels), nn.ReLU(inplace=True)
)
self.pointwise_conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=(1, 1), stride=(1, 1), padding=(0, 0), bias=False),
nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True)
)
def forward(self, x):
x = self.depthwise_conv(x)
x = self.pointwise_conv(x)
return x
class MobileNetV1(nn.Module):
def __init__(self, num_channels, num_classes):
super(MobileNetV1, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(num_channels, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False),
nn.BatchNorm2d(32), nn.ReLU(inplace=True)
)
self.dsc1 = DepthwiseSeparableConv(in_channels=32, out_channels=64, stride=(1, 1), padding=(1, 1), groups=32)
self.dsc2 = DepthwiseSeparableConv(in_channels=64, out_channels=128, stride=(2, 2), padding=(1, 1), groups=64)
self.dsc3 = DepthwiseSeparableConv(in_channels=128, out_channels=128, stride=(1, 1), padding=(1, 1), groups=128)
self.dsc4 = DepthwiseSeparableConv(in_channels=128, out_channels=256, stride=(2, 2), padding=(1, 1), groups=128)
self.dsc5 = DepthwiseSeparableConv(in_channels=256, out_channels=256, stride=(1, 1), padding=(1, 1), groups=256)
self.dsc6 = DepthwiseSeparableConv(in_channels=256, out_channels=512, stride=(2, 2), padding=(1, 1), groups=256)
self.dsc7 = DepthwiseSeparableConv(in_channels=512, out_channels=512, stride=(1, 1), padding=(1, 1), groups=512)
self.dsc8 = DepthwiseSeparableConv(in_channels=512, out_channels=512, stride=(1, 1), padding=(1, 1), groups=512)
self.dsc9 = DepthwiseSeparableConv(in_channels=512, out_channels=512, stride=(1, 1), padding=(1, 1), groups=512)
self.dsc10 = DepthwiseSeparableConv(in_channels=512, out_channels=512, stride=(1, 1), padding=(1, 1), groups=512)
self.dsc11 = DepthwiseSeparableConv(in_channels=512, out_channels=512, stride=(1, 1), padding=(1, 1), groups=512)
self.dsc12 = DepthwiseSeparableConv(in_channels=512, out_channels=1024, stride=(2, 2), padding=(1, 1), groups=512)
self.dsc13 = DepthwiseSeparableConv(in_channels=1024, out_channels=1024, stride=(2, 2), padding=(1, 1), groups=1024)
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(1024, num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.dsc1(x)
x = self.dsc2(x)
x = self.dsc3(x)
x = self.dsc4(x)
x = self.dsc5(x)
x = self.dsc6(x)
x = self.dsc7(x)
x = self.dsc8(x)
x = self.dsc9(x)
x = self.dsc10(x)
x = self.dsc11(x)
x = self.dsc12(x)
x = self.dsc13(x)
x = self.avg_pool(x)
x = x.view(-1, 1024)
x = self.fc(x)
return x
if __name__ == '__main__':
import torch
model = MobileNetV1(num_classes=1000, num_channels=3)
inp = torch.rand(2, 3, 224, 224)
oup = model(inp)
print(oup.shape)
코드 구현은 Table1을 천천히 보시면서 구현하시면 아주 쉽게 구현할 수 있습니다. 특히, Depthwise Separable Convolution을 구현하기 위해서 제일 중요한 인자는 nn.Conv2d의 groups입니다. groups는 $C$개의 채널이 주어졌을 때 $N$개의 그룹으로 나누어 각 그룹이 $C / N$개의 채널을 가지도록 만든 뒤 각 그룹에 대해 연산을 하여 concatenate을 하는 연산입니다. 따라서, $N$을 입력 채널과 동일하게 세팅하면 depthwise convolution이 구현되는 것이죠.
'논문 함께 읽기 > 2D Image Classification (IC2D)' 카테고리의 다른 글
[IC2D] Densely Connected Convolutional Networks (CVPR2017) (1) | 2023.05.10 |
---|---|
[IC2D] Aggregated Residual Transformations for Deep Neural Networks (CVPR2017) (0) | 2023.05.09 |
[IC2D] Wide Residual Networks (BMVC2016) (0) | 2023.05.05 |
[IC2D] Rethinking the Inception Architecture for Computer Vision (CVPR2016) (0) | 2023.05.04 |
[IC2D] Deep Networks with Stochastic Depth (ECCV2016) (0) | 2023.05.03 |