Naver Ai Boostcamp

Pstage3_Day1 ] Semantic Segmentation의 기초와 이해

잡담연구소 2021. 4. 26. 21:03

📌 학습목표

  1.  FCN 구현하기 
  2.  FCN 을 baseline에 적용해보기  -> 서버가 터졌다!! 내일 하기 

 

 

📌 학습 내용 정리

 

1. 3줄 요약 

  • VGG backbone을 사용
  • VGG 네트워크의 fc layer를 1x1 Convolution으로 대체 
  • Transposed Convolution을 이용해 Pixel Wise prediction 수행

2. 왜 Segmentation에 Fully connected 보다 convolution이 더 잘 어울릴까?

Image classification 문제는 물체의 위치는 중요하지 않다. "어떤" 물체가 있는 지가 핵심이므로, 어떤 픽셀이든 같은 label을 갖게 된다. 

하지만 Segmentation은 "어떤"물체가 "어디"위치해 있는 지가 중요하다. 위치정보를 살려야한다. 

 

Fully connected는 모든 픽셀이 일자로 쫙 펴지는 (flatten) 형태기 때문에 픽셀의 위치정보를 해치게 된다. 

그리고 fully connected를 사용하기 위해서는 이전 이미지의 채널 수 , 가로 , 세로 길이를 전부 알아야 한다.

FC에 맞춰서 사용해야하므로 Input size가 동일해야한다.

하지만 Convolution 연산은 각 픽셀의 위치 정보를 해치지 않으면서도 그 픽셀에 대한 특징을 추출해낸다.

특히 Convolution은 kernel의 파라미터에 의해 영향을 받고, 이전 이미지의 크기와는 상관이 없다.

이전 이미지의 채널 수 ! 이거 하나만 알면 된다. 

3. Transposed Convolution

엄연히 Deconvolution과 Upsampling과는 다르지만 논문에서는 혼용해서 사용하기도 한다. 

Transposed Convolution은 정확히는 Convolution의 역연산은 아니다. 이전 Convolution의 값과 동일한 값이 아니라 Convolution 처럼 학습이 가능한 파라미터로 이루어져있으며 backpropagation 과정에서 업데이트가 된다. 

왜 "Transposed "인지 정확히 이해가 잘 안 갔는데 오늘 수업을 듣고 바로 이해가 갔다.

먼저 Conv연산이 일어나는 과정을 보면, 4x4 input에 3x3 kernel을 계산하는 것을 이렇게 행렬 곱으로 나타낼 수 있다. 

Convolution 행렬을 그대로 전치를 취해준 후 다시 계산을 하면 

2x2 input을 다시 4x4로 만들어줄 수 있게 된다.

하지만 정확한 역연산은 아니므로 shape를 똑같이 만들어 줄 뿐, 값 자체가 동일해지진 않는다.

 

4. FCN에서 성능을 향상시키기 위한 방법

input size의 1/32까지 feature map 사이즈르 줄인 후, 다시 한 번에 키우게 되면 세세하고 정밀한(초기 레이어 값들)이 사라지게 된다. 그래서 FCN에서는 이런 부분을 해결하기위해 FCN8 , FCN16을 고안했다.

FCN16은 conv4를 거친 feature map을 skip connection을 통해 output에 더해주어 원래 사이즈의 1/16로 만든 다음 Deconv를 취해주는 것이다. 

FCN32은 conv3, 4를 거친 feature map을 skip connection을 통해 output에 더해주어 원래 사이즈의 1/8로 만든 다음 Deconv를 취해주는 것이다. 

predict1을 predict2와 사이즈를 맞추기위해 2배가 되도록 deconv1을 해준 후 , 둘을 pixelwise하게 더해준다. 

이 후 predict3과 사이즈를 맞추기위해 이전 과정의 output을 사이즈가 2배가 되게 deconv2 해주고 , 더해준다. 

그러면 원래 input size의 1/8이 된다. 

이때 Deconv3을 통해 원래 Input size에 맞게 키워준다.

 

5. Metric

Pixel Accuracy : pixel accuracy는 class에 상관없이 전체로 봤을 때, 예측한 것 중 맞춘 개수를 전체 픽셀 개수로 나눠주어서 구한다. 

Accuracy도 pixel accuracy가 있고, mean accuracy가 있다. 자세한 건 RefineNet 논문 리뷰 참고

Mean IoU : Multiple class에서의 IOU를 구하는 법으로 각 클래스 별 IOU를 구한다.

예전에 김진솔님 블로그에서 MIoU 설명을 본 적이 있었는데, 설명이 엄청 좋다. 혹시 염탐오신 캠퍼분들 여기까지 읽으셨다면 한 번 들어가서 보세용 *^^*

 

6. FCN 과 7x7

논문에서는 FC6을 1X1이 아닌 7X7로 진행한다. 그게 왜 문제가 되냐고? 모르겠다;

이미지의 크기가 변동되는게 왜 문제지? ;;;; 거 바뀔 수도 있는 거 아닌가?? 시간이 너무 늦었으니 내일 조원들한테 물어보고 그래도 모르겠으면 질문 게시판에 올려보자.

그리고 7X7 CONV가 어떤 장점이 있길래 꾸역꾸역 PADDING에 CROP까지 해가면서 한 걸까....?

 

 

 

📌 FCN 구현하기

import torch
import torch.nn as nn

class FCN(nn.Module):
    def __init__(self, num_classes, drop_r, bn_momentum , resolution):
        super(FCN , self).__init__()
        self.resolution = resolution

        # Backbone : VGG 19
        self.conv1 = self.make_block(3, 64 , 2, bn_momentum)
        self.conv2 = self.make_block(64, 128 , 2, bn_momentum)
        self.conv3 = self.make_block(128, 256 , 4, bn_momentum)
        self.conv4 = self.make_block(256, 512 , 4, bn_momentum)
        self.conv5 = self.make_block(512, 512 , 4, bn_momentum)

        # fc6
        self.fc6 = nn.Conv2d(512, 4096 ,1)
        self.relu6 = nn.ReLU(True)
        self.drop6 = nn.Dropout2d(drop_r)

        # fc7
        self.fc7 = nn.Conv2d(4096 , 4096, 1)
        self.relu7 = nn.ReLU(True)
        self.drop7 = nn.Dropout2d(drop_r)

        # fc7
        self.score = nn.Conv2d(4096,num_classes,1)  #[1,7,7,num_classes]
        
        # fcn-32
        self.upsample32 = nn.ConvTranspose2d(num_classes , num_classes , kernel_size = 64 , stride = 32 , padding = 16)

        # fcn-16
        self.pool4_conv = nn.Conv2d(512 , num_classes , kernel_size = 1 , stride = 1 , padding = 0)
        self.upsample16 = nn.ConvTranspose2d(num_classes , num_classes , kernel_size = 32 , stride = 16 , padding = 8)

        #fcn-8
        self.pool3_conv = nn.Conv2d(256 , num_classes , kernel_size = 1 , stride = 1 , padding = 0)
        self.upsample8 = nn.ConvTranspose2d(num_classes , num_classes , kernel_size = 16 , stride = 8 , padding = 4)
        
        # fcn-16 & fcn-8
        self.upsample_double = nn.ConvTranspose2d(num_classes , num_classes , kernel_size = 4 , stride = 2 , padding = 1)


    def forward(self, x):
      x = self.conv1(x)
      x = self.conv2(x)
      x = self.conv3(x)
      pool3 = self.pool3_conv(x)
      x = self.conv4(x)
      pool4 = self.pool4_conv(x)
      x = self.conv5(x)
      x = self.fc6(x)
      x = self.relu6(x)
      x = self.drop6(x)
      x = self.fc7(x)
      x = self.relu7(x)
      x = self.drop7(x)
      x = self.score(x)
      if (self.resolution == 32):
        x = self.upsample32(x)
        return x
      elif (self.resolution == 16):
        x = self.upsample_double(x) + pool4
        x = self.upsample16(x)
        return x
      else :
        x = self.upsample_double(x) + pool4
        x = self.upsample_double(x) + pool3
        x = self.upsample8(x)
        return x

    def make_block(self, in_channels ,out_channels ,repeat , bn_momentum):
      layers = []
      for i in range(repeat):
        if (i == 0) : 
          layers.append(nn.Conv2d(in_channels , out_channels , kernel_size = 3, padding = 1, stride = 1))
        else : 
          layers.append(nn.Conv2d(out_channels , out_channels , kernel_size = 3, padding = 1, stride = 1))
        layers.append(nn.BatchNorm2d(out_channels , momentum=bn_momentum))
        layers.append(nn.ReLU(True))

      layers.append(nn.MaxPool2d(kernel_size= 2, stride = 2))
      net = nn.Sequential(*layers)
      
      return net 

def FCN8(num_class):
    return FCN(num_class , 0.5 , 0.1 , 8) 

def FCN16(num_class ):
    return FCN(num_class , 0.5 , 0.1 , 16) 

def FCN32(num_class):
    return FCN(num_class , 0.5 , 0.1 , 32)

 

📌 피어세션

오늘 첫 피어세션 개시!

홍엽님 효진님 성배님 성훈님 유지님 나 이렇게 6명으로 구성되어있는데 확실히 컨택(?)으로 만든 조라서 그런 지 랜덤 피어세션보다는 분위기도 좋고, 진행도 매끄럽고 좋다 좋아~! 사람들도 다 열의가 넘친다. 홍엽님은 벌써 Baseline code를 완성하셨다 ;ㅂ; 대단한 사람. 많이 배워갈 예정이다. 

그리고 유지님은 알고보니 GIT 고수셨다. 🤯 Git clone말고는 몰라서 활용을 잘 못해서 문제였는데 이번 기회에 git을 좀 배워보자! 

이번 주 읽을 논문은 SETR ! 리더보드에 연연하기보다는 개인의 성장에 더 집중하기로 방향을 맞췄다.

 

제 2의 고향과도 같은 Ustage조원들과는 모여서 카카오 코테를 대비하기 위해서 알고리즘 문제풀이를 진행했다.

오늘 푼 문제는 등굣길 

 

코딩테스트 연습 - 등굣길

계속되는 폭우로 일부 지역이 물에 잠겼습니다. 물에 잠기지 않은 지역을 통해 학교를 가려고 합니다. 집에서 학교까지 가는 길은 m x n 크기의 격자모양으로 나타낼 수 있습니다. 아래 그림은 m =

programmers.co.kr

문제 읽자마자 풀이가 머리에서 자동으로 떠오르는 경험을 했다,,,! 백준 골드5 파이프 연결하기였나 그거 하위호환같다.

근데 프로그래머스 조작?이 익숙치가 않아서 제공되는 vector 어떻게 생겼는 지 구경한다고 시간 다 까먹어서 20분 걸렸다;;; 근데 이런 쉬운 문제는 안나올 거 같은데 내일은 카카오 기출을 풀자고 해봐야겠다. 최근에 삼성 준비한다고 구현만 주구장창 풀었더니 크루스칼 , 다익스트라 같은 알고리즘을 다 까먹었다. 큰 일이다.