1. idea
기존 CNN은 입력 이미지 크기를 고정시켜야했다. 이를 위해 crop, wrap과 같은 방식을 적용하면 다음과 같이 전체 이미지에 대한 정보 손실이 발생한다는 문제점이 생긴다.
이러한 과정은 이미지에 대한 인식 정확도를 떨어뜨릴 수 있다. CNN이 고정된 크기의 입력 이미지를 필요로 하는 이유는 FC layer때문이다.
SPPNet에서는 이러한 영향을 받지 않기 위해 Spatial Pyramid Pooling을 사용한다. 이 새로운 네트워크 구조는 이미지의 크기와 스케일에 영향을 받지 않고 고정 길이의 representation을 형성할 수 있다.
SPPNet의 아이디어를 한 문장으로 정리하면 다음과 같다.
"입력 이미지의 크기에 상관없이 Conv 연산을 진행하고, FC layer의 크기에 맞게 Feature map들의 크기를 조절해주는 Pooling을 쓰자" -> Spatial Pyramid Pooling
이 아이디어는 입력 이미지의 크기를 조절하기 위해 crop/wrap을 쓰지 않아 이미지 정보 손실이 없고, 사물의 크기 변화에 더 견고한 모델을 얻을 수 있다는 장점이 있다. 또한 이것은 image classification이나 object detection과 같은 여러 task들에 일반적으로 적용할 수 있다는 장점이 있다.
2. Spatial Pyramid Pooling
Conv layer를 통과해 추출된 feature map 크기를 64x64x256이라고 가정할 때, 이를 미리 정해져 있는 영역으로 나누어 준다. 위 그림의 예시에서는 4x4, 2x2, 1x1 세 가지 영역을 제공하고, 각각을 하나의 피라미드라고 부른다. 즉 해당 예시에서는 3개의 피라미드를 설정했다. 피라미드 한 칸을 bin이라고 하는데, 4x4 피라미드는 64x64의 input에서 bin의 크기가 16x16이라고 할 수 있다.
각각의 bin에서 max pooling을 수행하고, 이를 이어 붙인다. 입력 feature map의 채널 크기를 K, bin의 개수를 M이라고 했을 때 Spatial Pyramid Pooling의 최종 output은 K=256, M = (4x4 + 2x2 + 1x1) = 21이다. 정리해보면 입력 이미지의 크기와 상관없이 미리 설정한 bin의 개수와 CNN 채널 값으로 SPP의 출력이 결정되 항상 동일한 크기의 결과를 리턴한다. 실험에서 저자들은 1x1, 2x2, 3x3, 6x6 총 4개의 피라미드로 SPP를 적용한다.
3. Object Detection 적용
기존 R-CNN의 문제점은 Selective Search를 통해 찾아낸 2000개의 Region Proposal에 각각 Conv연산을 적용하여 속도가 느리다는 것이였다. 여기 SPPNet을 적용하면 입력 이미지를 그대로 CNN에 통과시켜 feature map을 추출한 다음, 2000개의 RoI를 찾아 Spatial Pyramid Pooling을 적용하여 고정된 크기의 feature를 얻어낼 수 있다. 그리고 이를 FC layer와 SVM classifier에 통과시키면 기존 R-CNN의 문제점을 개선할 수 있다.
4. 한계점
하지만 SPPNet은 여전히 object detection에 있어서 한계점이 있다.
- 아직 Multi-stage pipeline, 즉 end-to-end 방식의 학습이 아니다.
- 여전히 최종 classifier는 binary SVM, Region Proposal은 Selective Search를 사용한다.
'''
간단한 CNN model + SPP layer 구현
'''
import math
import torch
import torch.nn as nn
from torch.nn import init
import functools
from torch.autograd import Variable
import numpy as np
import torch.nn.functional as F
class SPP_NET(nn.Module):
def __init__(self, opt, input_nc, ndf=64, gpu_ids=[]):
super(SPP_NET, self).__init__()
self.gpu_ids = gpu_ids
self.output_num = [4,2,1]
self.conv1 = nn.Conv2d(input_nc, ndf, 4, 2, 1, bias=False)
self.conv2 = nn.Conv2d(ndf, ndf * 2, 4, 1, 1, bias=False)
self.BN1 = nn.BatchNorm2d(ndf * 2)
self.conv3 = nn.Conv2d(ndf * 2, ndf * 4, 4, 1, 1, bias=False)
self.BN2 = nn.BatchNorm2d(ndf * 4)
self.conv4 = nn.Conv2d(ndf * 4, ndf * 8, 4, 1, 1, bias=False)
self.BN3 = nn.BatchNorm2d(ndf * 8)
self.conv5 = nn.Conv2d(ndf * 8, 64, 4, 1, 0, bias=False)
self.fc1 = nn.Linear(10752,4096)
self.fc2 = nn.Linear(4096,1000)
def spatial_pyramid_pool(self, previous_conv, num_sample, previous_conv_size, out_pool_size):
for i in range(len(out_pool_size)):
'''
math.ceil은 실수를 올림하여 정수를 반환시킨다.
previous_conv_size는 첫쨰로 feature map의 높이, 둘쨰로 너비를 원소로 갖는 리스트이다.
'''
h_wid = int(math.ceil(previous_conv_size[0] / out_pool_size[i]))
w_wid = int(math.ceil(previous_conv_size[1] / out_pool_size[i]))
'''
h_wid와 w_wid는 각각 max pooling을 위한 kernel size의 높이, 너비이다.
previous_conv_size가 64x64라고 할 때,
previous_conv_size/out_pool_size(4, 2, 1)을 하게 되면, 각 피라미드 별로 Maxpooling kernel size는
16, 32, 64가 되고, pooling을 거친 후 bin의 총 개수는 4x4 + 2x2 + 1x1 => 21이 된다.
'''
h_pad = (h_wid*out_pool_size[i] - previous_conv_size[0] + 1)/2
w_pad = (w_wid*out_pool_size[i] - previous_conv_size[1] + 1)/2
'''
Max pooling을 보면 kernel size와 stride 크기가 동일해 window가 겹치지 않게 pooling이 진행되는 것을 알 수 있다.
일괄적으로 kernel size와 stride를 동일하게 설정했기 때문에 여러 image size가 들어올 때를 대비해 적절한 padding size를
계산하여 pooling에 적용시켜준다.
'''
maxpool = nn.MaxPool2d((h_wid, w_wid), stride=(h_wid, w_wid), padding=(h_pad, w_pad))
x = maxpool(previous_conv) # 여기서 기존 input이 64x64x256일 때 pooling 후 21x21x256이 된다.
if(i == 0):
spp = x.view(num_sample,-1)
else:
spp = torch.cat((spp,x.view(num_sample,-1)), 1)
return spp
def forward(self,x):
x = self.conv1(x)
x = self.LReLU1(x)
x = self.conv2(x)
x = F.leaky_relu(self.BN1(x))
x = self.conv3(x)
x = F.leaky_relu(self.BN2(x))
x = self.conv4(x)
# x = F.leaky_relu(self.BN3(x))
# x = self.conv5(x)
spp = self.spatial_pyramid_pool(x,1,[int(x.size(2)),int(x.size(3))],self.output_num)
# print(spp.size())
fc1 = self.fc1(spp)
fc2 = self.fc2(fc1)
s = nn.Sigmoid()
output = s(fc2)
return output