안녕하세요. 지난 포스팅의 [IC2D] Deep Layer Aggregation (CVPR2018)에서는 feature aggregation을 iterative 및 hierarchical 하게 제안한 DLA에 대해서 소개시켜드렸습니다. 오늘은 일전에 소개시켜드렸던 MobileNet의 다음 버전인 MobileNet V2에 대해서 소개시켜드리겠습니다. 두 모델이 어떤 차이점이 있는지를 중심으로 보시면 더욱 재밌을 거 같습니다.
Background
기본적인 배경은 MobileNet V1과 유사합니다. 지금까지 많은 합성곱 기반의 신경망들이 제안되어 인간의 성능을 뛰어넘게 되었습니다. 하지만 문제점은 너무 많은 연산 비용 및 파라미터가 존재하기 때문에 실제로 핸드폰 및 작은 칩 같은 곳에 사용하기에 적절치 못하다는 것이죠. MobileNet V1에서는 이에 대한 문제점을 해결하기 위해 depthwise separable convolution을 제안하였으며 성능은 떨어지지만 그래도 높은 효율성을 보여주었습니다.
MobileNet V2는 이러한 MobileNet V1에서 성능 하락 문제를 해결하기 위해 제안되었습니다. 크게 2가지 모듈로 inverted residual과 linear bottleneck으로 구성됩니다. 두 모듈은 각각 연산량 감소와 정보 보존에 초점을 맞추어 설계되었습니다.
MobileNet V2
1). Depthwise Separable Convolution
MobileNet V2의 각 합성곱 블록은 모두 depthwise separable convolution으로 이루어져있기 때문에 한번 간단하게 복습을 하고 진행하도록 하겠습니다. depthwise separable convolution은 기존의 합성곱을 크게 2개의 연산으로 나누어 계산하는 방법입니다. 각각 depthwise convolution과 pointwise convolution이라고 부르죠.
- depthwise convolution: 입력 특징 맵을 각 채널 단위로 연산을 수행하는 방법으로 group convolution에서 group을 입력 특징 맵의 채널 개수와 동일하게 정의하여 연산할 수 있음
- pointwise convolution: depthwise convolution을 수행한 뒤 합쳐진 특징 맵에 대해서 $1 \times 1$ 크기의 필터를 가진 합성곱 계층을 적용하는 과정
이 두 연산을 통해 필터 크기의 제곱만큼의 연산량을 감소시킬 수 있습니다. 실제로 필터 크기는 보통 $3 \times 3$ 크기를 주로 사용하기 때문에 기존 합성곱 연산에 비해 8 ~ 9배의 연산량을 줄일 수 있습니다.
2). Linear Bottlenecks
저희가 합성곱 연산을 수행하는 이유는 입력 영상의 다양한 특징 맵을 추출하기 위함입니다. 이때, 각 특징은 어떤 공간에 매핑된다고 볼 수 있는 데 이를 subspace라고 부르도록 하겠습니다. 그리고 이 subspace에 매핑된 특징들을 저희는 manifold (다양체)라고 정의합니다.
일반적으로 저희는 합성곱 계층 + 배치 정규화 + ReLU 활성화 함수를 하나로 두어 블록의 형태로 많이 사용합니다. 하지만, ReLU 합성곱의 가장 큰 문제는 저희가 합성곱 계층을 통해 저희가 원하는 manifold (interest of manifold)에 적절하게 매핑했다고 하더라도 만약 음수값이 나오게 되면 그 영역을 그대로 삭제된다는 것 입니다. 다만, 고차원의 정보일수록 그만큼 정보가 많아지기 때문에 ReLU를 쓰더라도 어느정도 정보가 보존되기는 합니다.
그림 1은 제 설명을 뒷받침하고 있습니다. 출력 차원이 낮으면 낮을수록 기존에 입력 manifold와 완전히 다른모습이 됩니다. 하지만, 차원이 높을수록 입력 manifold와 어느정도 유사한 모습으로 정보가 많이 보존된 모습을 볼 수 있죠. 따라서, 채널의 개수가 많은 경우에는 ReLU를 사용하고 채널의 개수가 적은 경우에는 ReLU를 사용하지 않고 단순히 선형 함수 (linear transformation)을 적용하는 것이 Linear Bottleneck의 주요 내용입니다.
3). Invertivle Residuals
여기까지 오셨다면 Linear Bottleneck을 통과한 계층은 이미 충분한 정보를 포함하고 있음을 수 있습니다. 그러면 이 특징에서 더 많은 특징들을 추출해볼 수 있지 않을까요? Invertivle Residual은 기존의 Residual Block과 정반대의 연산을 수행합니다.
그림 3을 보시면 빠르게 이해할 수 있습니다. (a)는 기존의 Residual Block으로 저희가 알고 있듯이 bottleneck에서 채널의 개수가 줄어드는 것을 볼 수 있습니다. 하지만 (b)는 bottleneck에서 오히려 채널의 개수를 증가시켜 linear bottleneck으로부터 얻은 특징들을 더 많이 추출하게 됩니다.
그렇다면 이 과정이 어떻게 연산량을 줄일 수 있을까요? (a)는 큰 채널을 가지는 특징 맵이 입력 특징 맵과 출력 특징 맵 2개가 존재합니다. 하지만, (b)는 중간에만 큰 채널을 가지기 때문에 이후 residual 연산에서 연산량 감소를 기대해볼 수 있는 것이죠.
MobileNet V2 Architecture
MobileNet V2에서는 새로운 하이퍼파라미터로 expansion factor $t$를 도입하였습니다. 이 파라미터는 linear bottleneck에서 입력 채널에 비해 얼마나 많이 늘릴 것 인지에 대한 파라미터입니다.
표1과 같이 expansion factor $t$를 도입하여 연산을 진행하게 됩니다.
그림 4는 기존에 제안된 다양한 효율적은 모델 간의 블록 다이어그램을 비교하고 있습니다.
표3은 MobileNet V1과 ShuffleNet 그리고 MobileNet V2 사이에 최대 저장 용량을 비교하고 있습니다. MobileNet이 가장 적은 것을 볼 수 있죠.
Experiment Results
본 논문에서는 Image Classification (ImageNet-1K), Object Detection, Semantic Segmentation (PASCAL VOC 2012)에서 성능 향상 및 효율성을 검증합니다.
1). ImageNet-1K Classification Results
그림5와 표4는 ImageNet-1K에서의 각 모델간의 성능 및 효율성을 비교하고 있습니다.
2). Ablation Study
그림 6은 Linear Bottleneck 및 Inverted Residual의 성능을 보여주고 있습니다.
Implementation Code
import torch
import torch.nn as nn
from layers import BasicConv
def _make_divisible(v, divisor, min_value=None):
"""
This function is taken from the original tf repo.
It ensures that all layers have a channel number that is divisible by 8
It can be seen here:
https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py
:param v:
:param divisor:
:param min_value:
:return:
"""
if min_value is None: min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v: new_v += divisor
return new_v
class InvertedResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride, expansion_factor):
super(InvertedResidualBlock, self).__init__()
assert stride in [1, 2]
inter_channels = int(in_channels * expansion_factor)
self.identity = stride == 1 and in_channels == out_channels
if expansion_factor == 1:
self.conv = nn.Sequential(
# Depthwise Convolution
nn.Conv2d(in_channels, inter_channels, kernel_size=3, stride=stride, padding=1, groups=inter_channels, bias=False),
nn.BatchNorm2d(inter_channels), nn.ReLU6(inplace=True),
# Pointwise Convolution
nn.Conv2d(inter_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_channels))
else:
self.conv = nn.Sequential(
# Pointwise Convolution
nn.Conv2d(in_channels, inter_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(inter_channels), nn.ReLU6(inplace=True),
# Depthwise Convolution
nn.Conv2d(inter_channels, inter_channels, kernel_size=3, stride=stride, padding=1, groups=inter_channels, bias=False),
nn.BatchNorm2d(inter_channels), nn.ReLU6(inplace=True),
# Pointwise Linear Convolution
nn.Conv2d(inter_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_channels))
def forward(self, x):
if self.identity: return x + self.conv(x)
else: return self.conv(x)
class MobileNetV2(nn.Module):
def __init__(self, num_channels=3, num_classes=1000, width_multiplier=1.0):
super(MobileNetV2, self).__init__()
self.cfgs = [
# [t, c, n, s]
[1, 16, 1, 1],
[6, 24, 2, 2],
[6, 32, 3, 2],
[6, 64, 4, 2],
[6, 96, 3, 1],
[6, 160, 3, 2],
[6, 320, 1, 1]]
in_channels = _make_divisible(32 * width_multiplier, 4 if width_multiplier == 0.1 else 8)
layers = []
layers.append(BasicConv(num_channels, in_channels, kernel_size=3, stride=2, padding=1, activation=nn.ReLU6(inplace=True)))
for t, c, n, s in self.cfgs:
out_channels = _make_divisible(c * width_multiplier, 4 if width_multiplier == 0.1 else 8)
for i in range(n):
layers.append(InvertedResidualBlock(in_channels, out_channels, s if i == 0 else 1, t))
in_channels = out_channels
self.feature = nn.Sequential(*layers)
out_channels = _make_divisible(1280 * width_multiplier, 4 if width_multiplier == 0.1 else 8) if width_multiplier > 1.0 else 1280
self.conv = BasicConv(in_channels, out_channels, kernel_size=1, stride=1, padding=1, activation=nn.ReLU6(inplace=True))
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Linear(out_channels, num_classes)
def forward(self, x):
x = self.feature(x)
x = self.conv(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x
if __name__=='__main__':
model = MobileNetV2()
print(model)
inp = torch.randn(2, 3, 224, 224)
oup = model(inp)
print(oup.shape)