Semantic Segmentation
Semantic Segmentation은 이미지 내에 있는 물체들을 의미 있는 단위로 분할하는 것입니다.
더 정확하게는 다음 그림처럼, 이미지의 각 픽셀(Pixel)이 어느 클래스에 속하는지 예측하는 것입니다.
FCN (Fully Convolution Netwroks)
기존의 분류 문제에서 자주 쓰이는 모델인 AlexNet, VGGNet, GoogLeNet 들은 일반적으로 Convolution Layer와 Fully Connected Layer로 구성되어 있습니다.
이러한 모델들은 Convolution Layer에서 Fully Connected를 통과하는 과정에서 모든 노드들이 서로 곱해진 후 더해지는 연결 때문에 이미지의 위치 정보를 잃어버리게 됩니다.
또한 Dense Layer에 가중치 개수가 고정되어 있어, 바로 앞 Layer의 Feature Map 크기도 고정되며, 연쇄적으로 Input Image의 크기 역시 고정됩니다.
이러한 문제를 감안하고 하나의 픽셀마다 클래스를 예측하도록 만들 수는 있지만, 비용이 매우 커지게되어 매우 비효율적입니다.
그래서 전체적으로 문제를 해결하기 위해 FCN (Fully Convolution Networks)는 다음 그림처럼 Fully Connected Layer를 1x1 Convolution Layer로 변경하여 해결하였습니다.
이를 통해, 전체 네트워크가 Convolution Layer로 구성되어 입력 이미지의 위치 정보를 '대략적'으로 유지하고, 입력 이미지의 크기를 조정할수 있으며, 마지막에 Up-Sampling(Deconvolution, Transposed Convolution)를 통해 각 Pixel에 해당하는 클래스 분류정보와 입력 이미지와 같은 사이즈를 회복할 수 있습니다.
구조
FCN의 구조는 크게 4단계로 구분 됩니다.
1. Convolution Layer를 통해 Feature 추출
2. 1x1 Convolution Layer를 이용해 Feature Map의 채널 수를 데이터셋의 클래수와 동일하게 변경
3. Up Sampling(Deconvolution, Transposed Convolution)을 이용해 낮은 해상도의 Heat Map을 Up Sampling 한 뒤, 입력 이미지와 같은 크기의 Map을 생성합니다.
4. Map의 각 Pixel class에 따라 색을 칠한 뒤 결과를 반환합니다.
Down Sampling
위 단계 중 1번 2번 단계로 Encoder라고도 하며, Convolution Layer를 통해 차원을 줄이는 역할을 합니다.
위 그림처럼 stride를 2 이상으로 하거나 Max Pooling을 사용하여 Feature Map의 크기를 점차 줄입니다. 이 과정에서 특히 많은 정보를 잃어버린다는 것을 기억해야 합니다.
Up Sampling
여러 단계의 Down Sampling을 거치면 Feature Map의 크기가 감소되지만 Pixel 단위의 예측을 하기 위해서는 이를 다시 확대하는 과정이 필요합니다. 이러한 과정은 Convolution 연산을 하여 Feature를 압축 시킬 때 Filter의 파라미터를 학습 하듯이, 크기를 다시 확대 시킬 때에도 Up Sampling 연산이란 것을해서 파라미터를 학습하는 방법입니다.
FCN에서는 Strided Transposed Conovlution을 사용하여 차원을 늘려줍니다.
다음 그림과 같은 원리를 2차원에서 적용합니다.
그러나, 단순히 Score(2번 과정의 결과)를 Up Samping 하게 된다면 Max Pooling 시 잃어 버린 정보와 Up Samping시 부족한 정보 때문에 다음 그림의 왼쪽 처럼 뭉뚱그려져 표시 됩니다.
이를 해결하기 위해 논문 저자는 skip combining 이라는 기법을 제안하였습니다.
이전의 ResNet의 Residual Block에서 가져온 발상으로, 컨볼루션과 풀링 단계로 이루어진 이전 단계의 컨볼루션층들의 특성 맵을 참고하여 Up Sampling을 해주면, 더 높은 정확도를 가질 수 있지 않을까? 라는 발상으로 접근하였습니다.
이러한 접근법은 바로 전 컨볼루션 층의 특성맵(pool4, 원본 이미지의 1/16)과 현재 특성맵(conv7, 원본 이미지의 1/32)을 2배 Up Sampling한 것을 더합니다.
그 다음 (pool4 + 2x conv7)을 16배 Upsampling해 얻은 특성맵으로 Segmentation Map을 얻는 방법을 FCN-16s 라고 부릅니다.
이를 반복하기 위해, 더 이전 단계의 특성맵(pool3, 원본 이미지의 1/8)과, 전 단계의 특성맵(pool4, 원본 이미지의 1/16)를 2배 Up Sampling 한 것과, 현 단계의 특성맵(conv7, 원본 이미지의 1/32)을 4배 Up Sampling 한 것을 모두 더한 다음에, 이를 8배 Up Sampling해 얻은 특성 맵으로 Segemention을 얻는 방법을 FCN-8s 라고 합니다.
skip layer를 통해 더 정교하게 Segmentaion이 가능해졌음을 보여줍니다.
Pytorch 구현
FCN-8s
class FCN8s(nn.Module):
def __init__(self, num_classes=10):
super(FCN8s, self).__init__()
# 3x3 Conv 1 Block
self.conv1 = nn.Sequential(
# 1 Conv
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1), # 512
nn.ReLU(inplace=True),
# 2 Conv
nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1), # 512
nn.ReLU(inplace=True),
# 3 Max Pooling
nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True) # 256
)
# 3x3 Conv 2 Block
self.conv2 = nn.Sequential(
# 1 Conv
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1), # 256
nn.ReLU(inplace=True),
# 2 Conv
nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1), # 256
nn.ReLU(inplace=True),
# 3 Max Pooling
nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True) # 128
)
# 3x3 Conv 3 Block
self.conv3 = nn.Sequential(
# 1 Conv
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1), # 128
nn.ReLU(inplace=True),
# 2 Conv
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1), # 128
nn.ReLU(inplace=True),
# 3 Conv
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1), # 128
nn.ReLU(inplace=True),
# 4 Max Pooling
nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True) # 64
)
# Score Pool 3 Block
self.score_3b = nn.Sequential(
nn.Conv2d(in_channels=256, out_channels=num_classes, kernel_size=1, stride=1, padding=0)
)
# 3x3 Conv 4 Block
self.conv4 = nn.Sequential(
# 1 Conv
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1), # 64
nn.ReLU(inplace=True),
# 2 Conv
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1), # 64
nn.ReLU(inplace=True),
# 3 Conv
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1), # 64
nn.ReLU(inplace=True),
# 4 Max Pooling
nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True) # 32
)
# Score Pool 4 Block
self.score_4b = nn.Sequential(
nn.Conv2d(in_channels=512, out_channels=num_classes, kernel_size=1, stride=1, padding=0)
)
# 3x3 Conv 5 Block
self.conv5 = nn.Sequential(
# 1 Conv
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1), # 32
nn.ReLU(inplace=True),
# 2 Conv
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1), # 32
nn.ReLU(inplace=True),
# 3 Conv
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1), # 32
nn.ReLU(inplace=True),
# 4 Max Pooling
nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True) # 16
)
# 1x1 Conv 6 FC
self.fc6 = nn.Sequential(
nn.Conv2d(in_channels=512, out_channels=4096, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Dropout2d()
)
# 1x1 Conv 7 FC
self.fc7 = nn.Sequential(
nn.Conv2d(in_channels=4096, out_channels=4096, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Dropout2d()
)
# 1x1 Conv 8 FC (Score)
self.fc8 = nn.Conv2d(in_channels=4096, out_channels=num_classes, kernel_size=1, stride=1, padding=0)
# Up Score
self.upscore = nn.ConvTranspose2d(in_channels=num_classes, out_channels=num_classes, kernel_size=4, stride=2, padding=1)
# Up Score Conv4
self.upscore_4b = nn.ConvTranspose2d(in_channels=num_classes, out_channels=num_classes, kernel_size=4, stride=2, padding=1)
# Up Score Conv3
self.upscore_3b = nn.ConvTranspose2d(in_channels=num_classes, out_channels=num_classes, kernel_size=16, stride=8, padding=4)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
s3 = self.score_3b(x)
x = self.conv4(x)
s4 = self.score_4b(x)
x = self.conv5(x)
x = self.fc6(x)
x = self.fc7(x)
x = self.fc8(x)
us = self.upscore(x)
sum_us_s4 = us + s4
us4 = self.upscore_4b(sum_us_s4)
sum_us4_s3 = us4 + s3
us3 = self.upscore_3b(sum_us4_s3)
return us3