본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

09 객체 탐지 (3) SSD

R-CNN 계열의 모델은 영역 제안과 객체 탐지가 분리되어있는 2 stage 모델들인데 SSD(Single Shot MultiBox Detector)는 이를 동시에 수행하는 1 stage 모델이다. 그래서 추론 속도가 빠르고 모델 크기도 가볍다. 상용화 관점에서 보면 적합한 모델이다. 

 

 SSD의 주요한 2가지 특징은 Multi Scale Feature Map과 Default Box이다. 

 

 

1. Multi Scale Feature Map

백본으로 사용하는 합성곱 모델에는 합성곱 층이 여러 개 존재한다. 특징 맵을 추출할 때 끝단에서만 추출하는 것이 아닌 중간 중간 여러 번 특징맵을 추출해서 사용하는 것이다. 이를 시각화한 모델 구조는 다음과 같다.

 

fig 1

 

화살표를 보면 총 6개의 크기가 38, 19, 10, 5, 3, 1인 특징 맵을 추출하는 것을 알 수 있다. 그리고 크기가 큰 특징 맵은 작은 객체를 인식하는데 사용되고, 크기가 작은 특징 맵은 큰 객체를 인식하는데 사용된다.

 

왜그러냐면 R-CNN 계열의 모델에서와 마찬가지로 특징 맵의 셀 하나당 앵커 박스를 여러개 그리는 방식을 취한다. 여기서는 앵커 박스가 아니라 default box라고 부르긴 하는데, 어쨋든 특징 맵의 셀 하나는 원본의 해당 위치의 지역적 정보를 축약하고 있다. 원본 이미지에 38 x 38 그리드를 그린다면 셀 하나는 작은 영역을 나타낼 것이다. 반면 원본 이미지에 3 x 3 그리드를 그린다면 셀 하나는 큰 영역을 나타낸다.

 

fig 2

 

위 그림을 보면 크기가 상대적으로 큰 8 x 8 특징 맵에서는 작은 객체인 고양이를 탐지하고, 크기가 작은 4 x 4 특징 맵에서는 큰 객체인 개를 탐지한다. 

 

그림 b와 c를 보면 셀 하나에 점선으로된 박스가 여러 개 있는 것을 확인할 수 있는데, 이것이 SSD에서 사용하는 default box이다.

 

추가적으로 첫 번째 그림의 화살표들을 보면 3 x 3(4 x (Classes + 4)) 이런식으로 되어있다. 4는 default box의 개수를 의미하고, 4는 (x, y, w, h)을 의미한다. Classes는 VOC2007 기준 배경 클래스까지 합쳐서 21이다. 

 

특징 맵 6개에 대해서 정리해보면

 

  • 38 x 38(4 x (Classes + 4)) = 5776 x (Classes + 4)
  • 19 x 19(6 x (Classes + 4)) = 2166 x (Classes + 4)
  • 10 x 10(6 x (Classes + 4)) = 600 x (Classes + 4)
  • 5 x 5(6 x (Classes + 4)) = 150 x (Classes + 4)
  • 3 x 3(4 x (Classes + 4)) = 36 x (Classes + 4)
  • 1 x 1(4 x (Classes + 4)) = 4 x (Classes + 4)

이를 다 더하면 8732 x (Classes + 4)가 되고, 생성되는 default box의 개수가 8732이라는 뜻이다.

 

 

2. Default Box

Default box의 크기는 입력 이미지의 크기와 특징 맵의 크기를 고려해 초기 크기를 설정한 뒤, 이후 다양한 스케일 값을 적용해 크기를 조정한다. 스케일 설정은 다음과 같이 계산된다.

 

$S_k = S_{min} + {S_{max} - S_{min} \over {m - 1}}(k - 1), k \in [1, m]$

 

일반적으로 $S_{min}=0.2$, $S_{max}=0.9$, $m=6$의 값을 설정한다. 이 값으로 6개의 특징 맵에 대한 스케일을 계산해보면 [0.2, 0.34, 0.48, 0.62, 0.76, 0.9]가 된다.

 

default box의 width와 height의 종횡비는 다음과 같이 결정된다.

 

$w_k = S_k \sqrt{a_r}$

 

위는 width에 대한 수식이고 다음은 height에 대한 수식이다.

 

$ h_k = S_k / \sqrt{a_r} $

 

$a_r$의 값은 박스가 6개인 경우 [1, 2, 3, 1/2 ,1/3]을 쓰고 4개인 경우 [1, 2, 1/2]을 사용한다. 만약 2를 사용한다면 width가 height에 2배가 되는 직사각형 모양이 될 것이다. 6개인 경우에는 비율이 5개이고 4개인 경우에는 비율이 3개인데 나머지 1개는 크기가 좀 더 큰 정사각형을 사용한다.

 

1을 사용할 경우에는 스케일이 $S_k = \sqrt{S_k S_{k+1}}$인 조금 더 큰 박스를 추가적으로 생성한다. 위로 다시 올라가 fig 2를 보면 점선으로 된 박스 중 크기가 큰 정사각형과 크기가 작은 정사각형이 둘 다 있는 것을 확인할 수 있다.

 

 

3. Hard Negative Mining

SSD는 RPN이 없기 때문에 배경 영역을 미리 선별할 수 없어서 모델의 출력 값 중 대부분이 배경 영역을 차지한다. 따라서 Hard Negative Mining 기법을 도입해 배경 영역과 객체 영역의 불균형을 방지하여 학습을 원할히 하도록 한다.

 

G(Ground Truth)와 IoU 0.5이상이면 positive, 미만이면 negative로 default box를 라벨링한다. 이중에서 confidence loss가 높은 negative만 선택하여 positive : negative = 1 : 3의 비율을 유지하여 학습에 사용한다. (이 비율이 저자들이 optimal 하다고 주장)

 

 

4. 모델 실습

SSD 모델은 SSD300, SSD512, SSD-MobileNet, SSD-ResNet 등이 있다. SSD300, SSD512는 입력 이미지의 크기가 300x300 또는 500x500을 입력받는 모델이다.

 

여기서는 ResNet-34를 특징 추출 모델로 사용하는 SSD512로 실습해본다.

 

 

SSD512 특징 추출 네트워크 정의

  • resnet은 stem과 4개의 스테이지로 구성되어 있다. stem을 layer0, 각 스테이지를 layer1~4에 저장한다.
  • 세 번째 스테이지의 출력부터 분기를 나누기 위해 layer0~3을 features에 저장한다.
  • features의 출력 채널은 256이기 때문에 upsampling을 통해 512로 키워준다.
  • 그리고 나머지 특징 추출 부분들을 extra에 저장한다. 해당 부분을 통과한 뒤 특징 맵의 크기를 주석으로 표시해두었다.
  • forward 메서드에서 OrderedDict를 사용해 upsampling(1) + extra(6) 총 7개의 특징 맵을 반환한다.
class SSDBackbone(nn.Module):
  def __init__(self, backbone):
    super().__init__()

    layer0 = nn.Sequential(backbone.conv1, backbone.bn1, backbone.relu)
    layer1 = backbone.layer1
    layer2 = backbone.layer2
    layer3 = backbone.layer3
    layer4 = backbone.layer4

    self.features = nn.Sequential(layer0, layer1, layer2, layer3)
    self.upsampling = nn.Sequential(
        nn.Conv2d(256, 512, kernel_size=1), # 64, 64
        nn.ReLU(inplace=True),
    )
    self.extra = nn.ModuleList(
        [
            nn.Sequential(
                layer4,
                nn.Conv2d(512, 1024, kernel_size=1), # 32, 32
                nn.ReLU(inplace=True),
            ),
            nn.Sequential(
                nn.Conv2d(1024, 256, kernel_size=1),
                nn.ReLU(inplace=True),
                nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1), # 16, 16
                nn.ReLU(inplace=True),
            ),
            nn.Sequential(
                nn.Conv2d(512, 128, kernel_size=1),
                nn.ReLU(inplace=True),
                nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1), # 8, 8
                nn.ReLU(inplace=True),
            ),
            nn.Sequential(
                nn.Conv2d(256, 128, kernel_size=1),
                nn.ReLU(inplace=True),
                nn.Conv2d(128, 256, kernel_size=3), # 6, 6
                nn.ReLU(inplace=True),
            ),
            nn.Sequential(
                nn.Conv2d(256, 128, kernel_size=1),
                nn.ReLU(inplace=True),
                nn.Conv2d(128, 256, kernel_size=3), # 4, 4
                nn.ReLU(inplace=True),
            ),
            nn.Sequential(
                nn.Conv2d(256, 128, kernel_size=1),
                nn.ReLU(inplace=True),
                nn.Conv2d(128, 256, kernel_size=4), # 1, 1
                nn.ReLU(inplace=True),
            ),
        ]
    )
  def forward(self, x):
    x = self.features(x)
    output = [self.upsampling(x)]

    for block in self.extra:
      x = block(x)
      output.append(x)

    return OrderedDict([(str(i), v) for i, v in enumerate(output)])

 

 

SSD512 모델 생성

  • 박스 생성은 DefaultBoxGenerator를 사용한다.
  • aspect_ratio : 각 특징 맵 레벨에서 생성되는 박스의 가로 세로 비율을 정의한다. [2]는 4개를 생성하고, [2, 3]은 6개를 생성한다.
  • scales : 박스의 스케일을 정의
  • steps : 박스를 배치하는 간격을 결정
  • 특징 맵을 7개 사용하기 때문에 모든 인자는 7개이다.
backbone_base = models.resnet34(weights="ResNet34_Weights.IMAGENET1K_V1")
backbone = SSDBackbone(backbone_base)

anchor_generator = models.detection.anchor_utils.DefaultBoxGenerator(
    aspect_ratios=[[2], [2, 3], [2, 3], [2, 3], [2, 3], [2], [2]],
    scales=[0.07, 0.15, 0.33, 0.51, 0.69, 0.87, 1.05, 1.20],
    steps=[8, 16, 32, 64, 100, 300, 512],
)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = models.detection.ssd.SSD(
    backbone=backbone,
    anchor_generator=anchor_generator,
    size=(512, 512),
    num_classes=3
).to(device)

 

 

사용하는 데이터셋과 학습 및 평가 코드는 Faster R-CNN을 실습했을 때와 동일하다. 단 epoch만 5에서 10으로 늘려주었다.

2024.02.20 - [책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습] - 09 객체 탐지 (2) Faster R-CNN pytorch 실습

 

09 객체 탐지 (2) Faster R-CNN pytorch 실습

MS COCO(Microsoft Common Objects in Context) 데이터셋으로 Faster R-CNN 모델을 미세 조정해본다. MS COCO 데이터셋은 Bounding Box 탐지, 객체 분할 및 캡션 생성을 위한 데이터를 제공한다. 이 데이터셋은 약 328,000

ai-junha.tistory.com

 

 

학습 결과는 다음과 같다.

Epoch 1: 100%|██████████| 608/608 [01:18<00:00,  7.73it/s]
epoch:    1, cost: 6.222
Epoch 2: 100%|██████████| 608/608 [01:17<00:00,  7.88it/s]
epoch:    2, cost: 5.201
Epoch 3: 100%|██████████| 608/608 [01:16<00:00,  7.90it/s]
epoch:    3, cost: 4.700
Epoch 4: 100%|██████████| 608/608 [01:16<00:00,  7.90it/s]
epoch:    4, cost: 4.278
Epoch 5: 100%|██████████| 608/608 [01:16<00:00,  7.90it/s]
epoch:    5, cost: 3.946
Epoch 6: 100%|██████████| 608/608 [01:16<00:00,  7.90it/s]
epoch:    6, cost: 3.295
Epoch 7: 100%|██████████| 608/608 [01:16<00:00,  7.90it/s]
epoch:    7, cost: 3.089
Epoch 8: 100%|██████████| 608/608 [01:16<00:00,  7.91it/s]
epoch:    8, cost: 2.943
Epoch 9: 100%|██████████| 608/608 [01:17<00:00,  7.89it/s]
epoch:    9, cost: 2.813
Epoch 10: 100%|██████████| 608/608 [01:16<00:00,  7.90it/s]epoch:   10, cost: 2.673

 

 

다음은 모델의 성능 평가이다.

 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.285
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.621
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.184
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.048
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.315
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.290
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.378
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.444
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.450
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.105
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.399
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.499

 

Faster R-CNN 떄와 마찬가지로 적은 데이터셋, epoch으로 학습해서 그런지 성능이 좋지는 않다.

 

다음은 추론을 시각화한 것이다.