안녕하세요. 지난 포스팅의 [IC2D] MobileNet V2: Inverted Residuals and Linear Bottlenecks (CVPR2018)에서는 기존의 MobileNet과 ShuffleNet에서 성능이 하락한다는 문제점을 보완한 MobileNet V2에 대해 소개시켜드렸습니다. 핵심은 채널은 적은 경우 정보 보존을 위해 linear 활성화 함수를 적용하고 residual 연산량을 감소시키기 위해 채널의 수를 늘렸다가 줄이는 방식으로 선택합니다. 오늘은 multi-scale을 중요성을 강조한 Res2Net에 대해서 소개시켜드리도록 하겠습니다.
Background
지금까지 저희가 공부했던 다양한 모델들은 주로 서로 다른 필터의 크기를 병렬적으로 연결한 InceptionNet, Residual Module을 적용한 ResNet, 특징 맵 간의 조밀한 연결을 제안한 DenseNet, 그리고 계층적으로 여러 개의 특징 맵을 하나로 합치는 DLA였습니다. 이러한 모델들의 특징은 서로 다른 scale을 가지는 특징 맵을 사용하기 때문에 저희는 알게 모르게 multi-scale의 정보를 모델에 녹여왔던 것 이죠.
이와 같이 multi-scale을 사용하는 것은 모델이 효율적으로 동작하고 효과적으로 성능을 향상시킬 수 있는 중요한 요소라고 볼 수 있습니다. 하지만, 지금까지 제안된 모델들은 모두 서로 다른 resolution, 즉 서로 다른 block 간의 multi-scale 정보를 이용했기 때문에 블록 내에 내재된 정보를 충분히 활용하지 않았습니다.
본 논문에서는 이러한 점을 지적하며 새로운 모델인 Res2Net을 제안하였습니다. 이 모델은 굉장히 단순하고 효과적으로 기존 SOTA 성능의 모델을 능가합니다. 또한, 블록 내에서 특징 맵을 세분화시켜 학습하기 때문에 multi-scale 정보를 하나의 블록에서 활용합니다. 이러한 구조는 블록 내부에서 계층적인 residual connection을 지니게 되어 기존 모델들보다 훨씬 더 큰 receptive field를 가진다는 것이 특징입니다. 따라서, Res2Net은 깊이, 너비, 다양성, cardinality에 이어 새로운 차원인 scale을 제안합니다. 실제로 scale 파라미터는 기존의 차원보다 훨씬 효율적임을 증명합니다. 마지막으로 Res2Net은 multi-scale 정보를 활용하는 것이 중요한 약 6개의 컴퓨터 비전 작업에서 모두 높은 성능을 달성합니다.
Res2Net
1). Motivation
저희는 그림1과 같이 많은 상황에서 multi-scale의 중요성을 느끼고 있습니다. 같은 색깔 = 같은 스케일을 의미합니다. 저희가 쇼파라는 큰 객체를 인식한다고 가정하면 초록색 스케일로 확인해야합니다. 노란색 스케일로 인지하는 것은 불가능하다는 것을 알 수 있죠. 그렇다면 화분이라는 작은 객체를 인식하기 위해서는 노란색 스케일로 인지해야합니다. 물론 파란색 스케일도 가능하죠. 초록색 스케일도 가능하겠지만 이 경우에는 화분뿐만 아니라 다른 다양한 객체들도 포함되기 때문에 조금 이해하기 어려울 수도 있습니다. 이와 같이 다른 종류의 객체 간에 다른 스케일을 가지는 경우가 있을수 있고, 같은 종류의 객체 중에서도 관찰자의 시점에 따라서 크게 보일수도 있고 작게 보일수도 있기 때문에 multi-scale 정보는 컴퓨터 비전에서 궁극적으로 원하는 "인식 (recognition)"이라는 문제에서 굉장히 중요하다고 볼 수 있습니다.
2). Res2Net Module
그림2에서 (a)는 ResNet의 Bottleneck Block을 도식화하였습니다. (b)는 본 논문에서 제안된 Res2Net Module의 모습입니다. Res2Net에서 Bottleneck에 multi-scale 정보를 함양시키기 위한 핵심 아이디어는 다음과 같습니다.
1). $1 \times 1$ 합성곱 계층을 통과한 특징 맵을 $s$개의 특징 맵으로 나눔 (Split)
2). 첫번째 그룹을 제외하고 각 그룹에 대해서 $3 \times 3$ 크기의 합성곱 계층을 적용
3). 첫번째 그룹은 skip connection, 두번째 그룹부터 마지막 그룹까지는 계층적 residual connection을 적용
즉, 위 그림과 같이 Bottleneck block의 중앙에 있는 $3 \times 3$ 크기의 합성곱 계층을 분해하는 것이 핵심입니다. 여기서, Residual Bottleneck 내부에 Residual Connection이 존재하기 때문에 Res2Net이라는 이름이 붙게 되었습니다. 위 그림을 수식적으로 풀어서 써보면 다음과 같습니다.
$$\begin{align*} \mathbf{y}_{i} = \begin{cases} \mathbf{x}_{i} &\text{ if } i = 1 \\ \mathbf{K}_{i}(\mathbf{x}_{i}) &\text{ if } i = 2 \\ \mathbf{K}_{i}(\mathbf{x}_{i} + \mathbf{y}_{i - 1}) &\text{ if } 2 < i \le s \end{cases} \end{align*}$$
여기서, $\mathbf{x}_{i}$와 $\mathbf{y}_{i}$는 각각 $i$번째 그룹의 입력 특징 맵과 출력 특징 맵을 의미합니다. 여기서, 본 논문에서 제안하는 scale 차원인 $s$가 추가되었습니다. 즉, $s$는 처음에 쪼개지는 그룹의 개수를 의미하고 쪼개지는 개수가 많을수록 내부에 residual connection이 증가하고 이는 더욱 넓은 receptive field를 가지게 만들기 때문에 multi-scale 정보의 양을 늘리는 것이라고 볼 수 있습니다. 다음으로 $\mathbf{K}_{i}$는 각 그룹에 배정된 $3 \times 3$ 크기의 합성곱 계층입니다.
3). Integration with Modern Modules
최근 수많은 논문들이 쏟아지고 있는 상황에서 각 논문들에서 제안되고 있는 모듈들간의 호환성이 가능한 지 확인하는 것은 굉장히 중요한 문제입니다. 다양한 모듈들간의 호환성이 높다면 그만큼 더 성능을 향상시킬 수 있는 여지가 되기 때문이죠. 그림3은 ResNext에서 제안한 cardinality의 개념과 SE Block을 추가한 모습입니다. cardinality $c$는 중간에 $3 \times 3$에적용하고 SE Block은 Res2Net 모듈의 출력 특징 맵에 적용하는 것을 볼 수 있습니다.
Experiment Results
본 논문에서는 6개의 컴퓨터 비전 작업에 대한 성능을 측정하였습니다. 이번 포스팅에서는 이 중에서 중요한 영상 분류에 초점을 맞추어 설명드리도록 하겠습니다.
1). ImageNet-1K Classification Results
표1은 ImageNet-1K에서의 성능을 보여주고 있습니다. 본 논문에서는 Res2Net 모듈을 다양한 모델에 교체함으로써 얻을 수 있는 성능 향상을 중심으로 설명하고 있습니다. 결과를 보시면 모든 부분에 있어서 Res2Net 모듈로 교체했을 때 높은 성능 향상을 달성하게 됩니다.
표2는 ImageNet-1K에서 더 깊은 모델을 구성하였을 때 성능 향상 정도를 보여주고 있습니다. 일반적으로 저희는 깊은 모델일수록 그만큼 성능이 오르는 것을 기대하기 때문에 깊은 모델에서도 성능이 높다는 것을 보여주는 결과도 중요합니다.
표3은 scale 파라미터 $s$에 따른 성능 변화를 분석하고 있습니다. 복잡도를 상승시키는 상태 (Increased Complexity)에서 $s$가 증가할수록 성능이 올라가는 것을 볼 수 있습니다. 또한, 복잡도를 고정한 상태 (Preserved Complexity)에서 기존의 ResNet과 동일한 FLOPs를 가짐에도 불구하고 훨씬 낮은 에러율을 얻을 수 있습니다.
2). CIFAR100 Classification Results
표4는 CIFAR100에서 성능을 측정한 결과입니다.
그림5는 최근에 제안된 cardinality를 증가시키는 것과 본 논문에서 제안하는 scale을 증가시키는 것 중 어떤 것이 효율적인지에 대한 분석입니다. 결과는 cardinaltiy를 증가시키는 것보다 scale을 증가시키는 것이 성능 향상 정도가 훨씬 높은 것을 볼 수 있습니다.
3). Class Activation Mapping
그림4는 ExAI의 가장 기본적인 Grad-CAM을 이용하여 Res2Net이 정말 다양한 크기의 객체를 잘 인지하고 있는 지 분석합니다. 보시면 서로 다른 객체에서도 높은 인식 능력을 보여주고 있습니다.
Implementation Code
import math
import torch
import torch.nn as nn
class Bottle2Neck(nn.Module) :
expansion = 4
def __init__(self, inplanes, features, stride=(1, 1), downsample=None, baseWidth=26, scale=4, stype='normal'):
super(Bottle2Neck, self).__init__()
width = int(math.floor(features * (baseWidth / 64.0)))
self.conv1 = nn.Conv2d(inplanes, width * scale, kernel_size=(1, 1), stride=(1, 1), padding=(0, 0), bias=False)
self.bn1 = nn.BatchNorm2d(width * scale)
if scale == 1 : self.nums = 1
else : self.nums = scale - 1
if stype == 'stage' : self.pool = nn.AvgPool2d(kernel_size=(3, 3), stride=stride, padding=(1, 1))
convs, bns = [], []
for i in range(self.nums) :
convs.append(nn.Conv2d(width, width, kernel_size=(3, 3), stride=stride, padding=(1, 1), bias=False))
bns.append(nn.BatchNorm2d(width))
self.convs = nn.ModuleList(convs)
self.bns = nn.ModuleList(bns)
self.conv3 = nn.Conv2d(width * scale, features * self.expansion, kernel_size=(1, 1), stride=(1, 1), padding=(0, 0), bias=False)
self.bn3 = nn.BatchNorm2d(features * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stype = stype
self.scale = scale
self.width = width
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
spx = torch.split(out, self.width, 1)
for i in range(self.nums):
if i == 0 or self.stype == 'stage':
sp = spx[i]
else:
sp = sp + spx[i]
sp = self.convs[i](sp)
sp = self.relu(self.bns[i](sp))
if i == 0:
out = sp
else:
out = torch.cat((out, sp), 1)
if self.scale != 1 and self.stype == 'normal':
out = torch.cat((out, spx[self.nums]), 1)
elif self.scale != 1 and self.stype == 'stage':
out = torch.cat((out, self.pool(spx[self.nums])), 1)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class Res2Net(nn.Module) :
def __init__(self, block, layers, baseWidth, scale, num_classes=1000):
super(Res2Net, self).__init__()
self.baseWidth = baseWidth
self.scale = scale
self.init_features = 32
self.inplanes = 64
######### Stem layer #########
self.conv1 = nn.Sequential(
nn.Conv2d(3, self.init_features * 1, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False),
nn.BatchNorm2d(self.init_features * 1), nn.ReLU(inplace=True),
nn.Conv2d(self.init_features * 1, self.init_features * 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False),
nn.BatchNorm2d(self.init_features * 1), nn.ReLU(inplace=True),
nn.Conv2d(self.init_features * 1, self.init_features * 2, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
)
self.bn1 = nn.BatchNorm2d(self.init_features * 2)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
######### Stem layer #########
self.layer1 = self._make_layer(block, self.init_features * 2, layers[0])
self.layer2 = self._make_layer(block, self.init_features * 4, layers[1], stride=(2, 2))
self.layer3 = self._make_layer(block, self.init_features * 8, layers[2], stride=(2, 2))
self.layer4 = self._make_layer(block, self.init_features * 16, layers[3], stride=(2, 2))
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self, block, features, blocks, stride=(1, 1)):
downsample = None
if stride != (1, 1) or self.inplanes != features * block.expansion :
downsample = nn.Sequential(
nn.AvgPool2d(kernel_size=stride, stride=stride, ceil_mode=True, count_include_pad=False),
nn.Conv2d(self.inplanes, features * block.expansion, kernel_size=(1, 1), stride=(1, 1), padding=(0, 0), bias=False),
nn.BatchNorm2d(features * block.expansion)
)
layers = []
layers.append(block(self.inplanes, features, stride, downsample=downsample, stype='stage', baseWidth=self.baseWidth, scale=self.scale))
self.inplanes = features * block.expansion
for i in range(1, blocks) :
layers.append(block(self.inplanes, features, baseWidth=self.baseWidth, scale=self.scale))
return nn.Sequential(*layers)
def forward(self, x):
######### Stem layer #########
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
######### Stem layer #########
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def forward_feature(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x0 = self.maxpool(x)
x1 = self.layer1(x0)
x2 = self.layer2(x1)
x3 = self.layer3(x2)
x4 = self.layer4(x3)
return [x, x1, x2, x3], x4