PFLD의 backbone은 Mobilenet V2를 사용했다. 해당 Backbone에 대해 깊은 이해가 필요함에 따라 논문 리뷰를 해보려고 한다.
Mobilenet V2의 논문 링크는 다음과 같다.
https://arxiv.org/pdf/1801.04381
SSD lite라는 새로운 Framework를 통해 모바일 모델을 object detection에 효율적으로 적용하는 방법을 설명한다. mobile DeepLab V3라고 부르는 간소화된 형태의 DeepLab V3를 통해 모바일 환경에서의 의미론적 분할(Semantic Segmentation) 모델을 구축하는 방법을 제시한다.
-> PFLD에서 중요한 부분은 아니므로 읽고 넘어간다.
Mobilenet V2는 Inverted Residual 기반이며 bottleneck 사이에 shortcut을 배치한다. Expansion layer는 경량화된 depthWise convolution을 사용하여 비선형성(non-linearity)을 추가하여 배치했다. narrow layer에서는 비선형성을 제거하여 linear convolution을 사용하는 것이 표현력 유지에 좋다.
해당 논문의 주된 기여는 Inverted residual with linear bottleneck 이라는 새로운 module에 대한 내용이다. 이 module은 저차원의 압축된 표현을 입력으로 받아 먼저 고차원으로 확장한 후 경량화된 depthwise convolution을 통해 특징을 추출한 후 다시 저차원의 표현으로 투영(projection)하여 linear convolution을 적용한다.
*(고차원이나 저차원에 대한 내용은 결국 해당 차원의 크기를 의미한다. input_channel은 output_channel에 비해 feature 크기가 크다. 물론, depthwise convolution은 input_channel과 output_channel의 크기가 동일하기 때문에 상관없지만 standard convolution을 생각하면 일반적으로 input_channel에 해당하는 feature map의 size는 output_channel의 feature map보다 크다. 쉽게 말하면, input_channel이 고차원이고 output_channel이 저차원이라고 생각하면 된다.)
해당 Convolution module은 모바일 설계에 적합하다. 이유는 큰 중간 tensor를 완전히 물질화(materialize)하지 않으면서도 추론시 메모리 사용량을 크게 줄일 수 있기 때문이다.
materialize란 중간 계산 결과를 실제 메모리에 저장하는 과정이다. standard convolution을 생각해보면 input_layer - hidden_layer - output_layer 순으로 model design이 설계되는데, 여기서 hidden layer의 계산결과는 모두 메모리에 저장되어 있다.
그 이유는
1. 다음 레이어의 입력으로 사용하기 위함.
2. backpropagation(역전파)를 하기 위함.
이 두가지의 경우 때문인데, 메모리가 크지 않은 모바일이나 임베디드같은 경우에는 모델이 크면 클수록 저장되는 중간 계산의 메모리량이 늘어나기 때문에, mobilenet V2는 이 같은 문제를 해결했다는 것에 초점을 두고 있다.
기존에는 위와 같은 연구가 많이 도입되었으나 네트워크가 복잡해지고 무거워지는 경향이 있다.
따라서, 저자들은 더나은 intuition과 간단한 network design을 mobilenet V1을 기반으로 설계 하였다.
해당 sector는 standard CNN과 비교를 하는데, mobilenet V1 논문에서 썼던 내용을 다시 복기하는 과정과 같다.
standard CNN의 경우에는 계산량이 다음과 같다. 
연산량이 왜 이렇게 나올까? 이에 대한 의문을 풀려면 standard CNN에 대해서 정확히 아는 것이 중요하다. 매우 기본적인 내용이지만 실제로 중간 연산 과정에 대해 모르는 사람이 많다.
Standard CNN은 하나의 kernel이 모든 channel을 순회한다. 즉, 3x3 kernel은 총 9개의 weight를 가지고 있으며 input_channel의 모든 채널을 순회하고 weighted sum(가중합)을 진행한다. 이로써 output_channel 한개의 출력을 얻게 되는 것이다. 그 뒤로 서로 다른 weight 9개를 가진 3x3 kernel을 모든 input_channel과 합성곱(convolution)을 진행한 후 weighted sum(가중합)을 하고 두번째 output_channel의 출력값이 된다.
이러니 hi· wi· di· dj · k · k 의 연산량을 갖게 되는 것이다.
하지만 depthwise CNN은 연산량이 대폭 줄어든다.

Standard CNN처럼 가중합을 할 필요가 없게 되니 Cout channel을 곱해줄 필요가 없게 되고 가중합이 필요한 부분은 Pointwise CNN을 통해 이뤄지니 연산량이 대폭 줄어들게 된다.
논문에서는 k^2만큼 계산량이 줄어든다고 언급했다.
해당 sector를 정확하게 이해하기 위해서는 manifold라는 concept을 정확하게 이해해야 한다.
manifold는 고차원 데이터가 실제로는 저차원 공간에 내재될 수 있다는 아이디어이다.
예를 들어보자. 32 32 3 이미지가 있다고 가정할때, 이 이미지를 32,768차원의 vector로 표현할 수있다. 이렇게 벡터가 많다? 차원이 많다? 라면 고차원 공간 or 고차원 데이터라고 표현할 수 있다.
PFLD처럼 얼굴 데이터라고 가정했을때, euler_angle를 구할때 모든 landmark를 사용하지 않고 TRACKED_POINTS를 사용했던 것을 기억하는가? 총 196개의 차원의 vector를 사용하지 않고 14개 차원의 vector만 사용했던 것이 기억날 것이다. 이처럼 고차원의 데이터 모두가 필요하지 않고 저차원의 데이터로도 중요한 특징을 충분히 표현할 수 있다는 것에 의미가 있다.
<핵심>
즉, 데이터는 모두 고차원 공간에 위치하고 표현하지만 해당 데이터의 본질적인 구조는 사실상 저차원 공간에 있다고 표현해도 무방하다라는 것이다.
Mobilenet v1에서는 이 방법을 성공적으로 활용하여 연산량과 정확도 간의 균형을 조정하였지만 깊은 합성곱 신경망이 비선형적 변환을 포함하고 있어 한계가 있다고 말하고 있다.
Q. 합성곱 신경망이 비선형적 변환을 포함하고 있어서 한계가 있다?
이게 도대체 무슨 말일까? manifold를 기준으로 봐야한다. manifold는 결국엔 저차원 데이터(소수의 vector)로 고차원 데이터를 해석한다는데에 있는데, 이 수많은 비선형성은 manifold를 복잡하게 한다는 문제가 있다는 것.
예를 들자면 ReLU(x) = max(0, x)로 표현할 수 있고 input이 0보다 작으면 0으로 0보다 크면 input 값으로 표현할 수 있다보니 총 2가지를 표현할 수 있게 된다. 즉, 하나의 채널 혹은 하나의 데이터가 2개의 출력을 만들어 비선형성을 유지할 수 있다는 것인데, 저차원의 정보(차원을 줄여도 큰 상태라고 가정)에 비선형성을 적용하면 결국 저차원의 정보 * 2 만큼의 manifold가 생기는 것과 같다는 것이다. Mobilenet의 특성을 살린다면 해당 activation function을 nonlinear가 아니라 linear로 바꾸면 어떨까? 라는 아이디어가 나오게 된 것.

해당 이미지의 해석도 중요한데, 일반적인 bottleneck과는 다른 양상을 보여준다. 우리가 bottleneck하면 Resnet을 떠올리게 되는데, Resent의 경우 input의 차원이 높고 내부에선 차원을 낮추며 output은 차원을 다시 높이는 결과로 이어진다.
input(고차원) -> [bottleneck] -> output(고차원)
하지만 expansion layer를 포함한 Bottleneck은
input(저차원) -> [bottleneck] -> output(저차원)
순으로 진행하는 것을 볼 수 있다.

여기서 Inverted residual의 핵심이 나오게 되는데, expansion layer는 tensor의 비선형 변환을 수행하는 구현상의 세부사항에 불과하다고 판단했다. 그렇기 때문에, 저자들은 병목 레이어들 사이에 shortcut을 연결했다고 한다.
이 말이 무슨 뜻이냐면 bottleneck에 해당하는 layer들은 핵심 정보를 다 갖고 있다는 것. 차원을 늘렸으므로 더 많은 정보(feature)를 뽑을 수 있게 됨. 이렇게 나온 feature와 residual을 더해주는 것이 핵심임.
output의 경우 차원이 줄어든 상태(채널이 줄음)로 manifold가 늘어나거나 표현이 복잡해지지 않기 때문에, 저자들이 처음에 말한 내용과 위배되진 않음.
이 sector는 network의 capacity와 expressiveness를 분리 시켜 봐야 하는 것에 의미가 있다.
일반적으로 깊은 신경망을 설계할때, 채널수와 차원이 capcaity를 담당하게 되고 Expressiveness는 activation function이 담당하게 되어 있다. 이 두가지를 동시에 사용한다면 expressiveness와 capcaity가 동시에 증가하므로 효율적으로 최적화할 수 없기 때문이다.
capacity를 담당하는 하는 곳(Bottleneck layer)은 capacity만 담당할 수 있도록 해당 activation function은 Linear function을 사용하고 expressiveness를 담당하는 곳(convolution)의 activation function은 ReLU6를 사용하는게 어떻냐라는 말이다.

MobileNet V2 architecture는 32개의 filter로 시작하는 cnn과 위의 표처럼 19개의 residual bottleneck layer로 구성된다.
expansion 계수는 5~10사이일때 성능 곡선이 거의 동일하게 나타났다.
작은 네트워크에서는 작은 expansion 계수가 더 좋고, 큰 네트워크에서는 더 큰 expansion 계수가 높은 성능을 보였다. 저자는 expansion 계수를 6으로 사용했다.
아래는 메모리 효율화에 대한 설명이므로 PFLD에 초점을 둔 것과는 거리가 멀어 일단은 패스하려고 한다.
# mobilenetV2_config.yaml
# t, c, n, s
# expansion, channel, repeat, stride
model_architecture : [
# bottleneck
[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],
]
#mobilenet_V2.py
import yaml
import torch
from torch import nn
path = './landmark/config/mobilenetV2.yaml'
def load_model(path):
with open(path, 'r') as f:
config = yaml.safe_load(f)
model = config['model_architecture']
return model
class CNNBlock(nn.Module):
def __init__(self, in_channels, out_channels, **kwargs):
super().__init__()
self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
self.bn = nn.BatchNorm2d(out_channels)
self.act = nn.ReLU6()
def forward(self, x):
return self.act(self.bn(self.conv(x)))
class DepthwiseBlock(nn.Module):
def __init__(self, in_channels, out_channels, **kwargs):
super().__init__()
self.layers = CNNBlock(in_channels,
out_channels,
kernel_size=3,
groups=in_channels,
bias=False,
**kwargs)
def forward(self, x):
return self.layers(x)
class PointwiseBlock(nn.Module):
def __init__(self, in_channels, out_channels, nonlinear=None, **kwargs):
super().__init__()
self.R_conv = CNNBlock(in_channels, out_channels,kernel_size=1, bias=False, **kwargs)
self.L_conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels)
)
self.nonlinear = nonlinear
def forward(self, x):
if self.nonlinear:
return self.R_conv(x)
else:
return self.L_conv(x)
class Bottleneck(nn.Module):
def __init__(self, in_channels, out_channels, expansion, repeat, stride):
super().__init__()
layers = []
self.residual_check = (stride == 1 and in_channels == out_channels)
for i in range(repeat):
current_stride = stride if i == 0 else 1
current_padding = 0 if i == 0 else 1
layers.append(
nn.Sequential(
PointwiseBlock(in_channels, in_channels * expansion, nonlinear=True),
DepthwiseBlock(in_channels * expansion, in_channels * expansion,
stride=current_stride,
padding=current_padding),
PointwiseBlock(in_channels * expansion, out_channels, nonlinear=False)
)
)
in_channels = out_channels
self.bottleneck = nn.ModuleList(layers)
if not self.residual_check:
self.residual = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
else:
self.residual = None
def forward(self, x):
for idx, layer in enumerate(self.bottleneck):
out = layer(x)
if idx == 0 and self.residual:
x = self.residual(out)
print("residual : ",x.shape)
x = x + out
else:
x = out
return x
class MobileNetV2(nn.Module):
def __init__(self, in_channels=3, num_classes=1000, **kwargs):
super().__init__()
self.in_channels = 32
self.conv1 = CNNBlock(in_channels, self.in_channels, kernel_size=3, stride=2, padding=1, bias=False)
self.architecture = load_model(path)
self.layer = self._create_block(self.architecture)
self.conv2 = PointwiseBlock(320, 1280, nonlinear=True)
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.l_conv = nn.Linear(1280, num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.layer(x)
x = self.conv2(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.l_conv(x)
return x
def _create_block(self, architecture):
layers = []
in_channels = self.in_channels
for config in architecture:
t, c, n, s = config
layers.append(Bottleneck(in_channels, c, t, n, s))
in_channels = c
return nn.Sequential(*layers)
if __name__ == "__main__":
a = torch.randn(1, 3, 224, 224)
model = MobileNetV2()
print(model(a).shape)