[Computer Vision] 5(2). 빠른 객체 탐지 알고리즘 YOLO

9 minute read


빠른 객체 탐지 알고리즘 YOLO

최신 버전인 YOLOv3는 크기가 256x256인 이미지에 대해 최신 GPU에서 초당 170 프레임의 속도로 실행될 수 있다.


YOLO 소개


YOLO의 강점과 한계

2015년에 최초로 공개된 YOLO는 속도가 빠른 것으로 유명하다. 그렇지만 최근 Faster R-CNN이 정확도 측면에서 YOLO를 능가했다. 게다가 YOLO는 객체를 탐지하는 방식 때문에 작은 크기의 물건을 탐지하는 데 어려움을 겪는다. 또한 대부분의 딥러닝 모델과 마찬가지로 훈련 데이터셋에서 너무 많이 벗어난 객체를 적절히 탐지하는 일에도 어려움을 겪는다. 그럼에도 불구하고 이 아키텍쳐는 꾸준히 진화하고 있으며, 이러한 문제점들을 해결하고 있다.


YOLO의 주요 개념

YOLO의 핵심 아이디어는 객체 탐지를 단일 회귀 문제로 다시 구성하는 것이다. 이는 슬라이딩 윈도우나 다른 복잡한 기법을 사용하는 대신 입력 이미지를을 w x h 그리드로 나누는 것이다.

KakaoTalk_20210817_184010999

그리드의 각 부분에 대해 B개의 경계 상자를 정의한다. 그런 다음 각 경계 상자에 대해 다음을 예측한다. 여기서 생성될 수 있는 박스는 w x h x B 개이다.

  • 상자의 중심
  • 상자의 너비와 높이
  • 이 상자가 객체를 포함하고 있을 확률
  • 객체의 클래스

이 모든 예측은 숫자이므로 객체 탐지 문제를 회귀 문제로 변환했다.


실제로 YOLO에서 사용하는 개념은 이보다 더 복잡하다.

  • 그리드의 한 부분에 여러 객체가 있다면 어떻게 될까?
  • 하나의 객체가 그리드의 여러 부분에 걸쳐 있다면 어떻게 될까?
  • 모델 훈련 손실을 어떻게 정할까?

위 의문점들을 해결하기 위해 YOLO 아키텍처를 좀 더 깊이있게 살펴보자.



YOLO로 추론하기


추론훈련으로 나누어 모델을 자세히 설명한다.

추론은 이미지 입력을 받아 결과를 계산하는 절차이고, 훈련은 모델의 가중치를 학습하는 과정이다. 설명을 단순히 하기 위해 추론부터 설명한다.


YOLO 백본

대부분의 이미지 탐지 모델처럼 YOLO는 백본 모델을 기반으로 한다. 이 모델의 역할은 마지막 계층에서 사용될 의미있는 특징을 이미지로부터 추출하는 것이다. 따라서 이를 이미지 분류 모델에서 흔히 사용되는 용어인 특징 추출기라고도 부른다.

KakaoTalk_20210817_184040936

특징 추출기로는 어떤 아키텍처도 사용할 수 있다. 최종 모델의 성능은 특징 추출기 아키텍처로 무엇을 사용했는지에 따라 크게 달라진다.

백본의 마지막 계층은 크기가 w x h x D 인 특징 볼륨을 출력하는데, 각각 그리드의 너비, 그리드의 높이, 특징 볼륨의 깊이이다. 예를 들어 VGG-16의 경우 D=512이다.

그리드 크기인 w x h는 다음 두 요인에 따라 달라진다.

  • 전체 특징 추출기의 보폭: VGG-16의 보폭은 16으로, 출력된 특징 볼륨이 입력 이미지보다 16배 작다는 뜻이다.
  • 입력 이미지 크기: 특징 볼륨 크기는 이미지 크기에 비례하므로 입력 크기가 작을수록 그리드 크기가 달라진다.

YOLO의 마지막 계층은 입력으로 특징 볼륨을 받는다. 이 마지막 계층은 크기가 1x1인 합성곱 필터로 구성되어 특징 볼륨의 공간 구조에 영향을 주지 않으면서 깊이를 변화시킨다.


YOLO의 계층 출력

YOLO의 마지막 출력은 w x h x M 행렬로, 여기에서 w x h는 그리드 크기이며 M은 공식 Bx(C+5)에 해당 한다. B와 C는 다음과 같다.

  • B: 그리드 셀 당 경계 상자 개수
  • C: 클래스 개수

C+5인 이유는 경계 상자마다 클래스의 개수에 더해 5개의 숫자를 더 예측해야 하기 때문이다.

  • tx, ty: 경계 상자의 중심 좌표
  • tw, th: 경계 상자의 너비와 높이
  • c: 객체가 경계 상자 안에 있다고 확인하는 신뢰도
  • p1, p2, …, pC: 경계 상자가 클래스 1, 2, …, C 의 객체를 포함할 확률

이 출력 행렬을 사용해 최종 경계 상자를 계산하는데, 그 전에 중요한 개념인 앵커 박스를 소개한다.


앵커 박스 소개

tx, ty, tw, th 가 경계 상자의 좌표를 계산하는 데 사용된다고 설명했다. (x, y, w, h)를 직접 예측하지 않는 이유는 실제로 객체의 크기는 매우 다양해서 크기의 차이가 큰 객체에 대해 제대로 예측하지 못하기 때문이다.

실제로 YOLOv1에서는 (x, y, w, h)를 예측하는 방식을 사용했고, 이 문제를 해결하기 위해 YOLOv2부터 도입한 것이 앵커박스이다.

앵커 박스는 네트워크를 훈련시키기 전에 결정되는 일련의 경계 상자 크기다.

앵커 박스의 집합은 일반적으로 작으며 실제로 3~25 사이의 다양한 크기를 갖는다. 그러한 상자가 모든 객체와 정확하게 일치할 수는 없으므로 네트워크는 가장 근접한 앵커 박스를 개선하는데 사용(가장 근접한 앵커 박스를 기준으로 객체에 맞게 높이나 너비를 변화)한다. 이것이 tx, ty, tw, th 가 필요한 이유로, 앵커 박스 보정에 해당한다.

KakaoTalk_20210817_202804218

앵커 박스의 크기는 모델을 훈련시키기 전에 데이터를 분석해서 선택해야 한다. 예를 들어 보행자와 같은 객체에 있어서는 세로로 긴 직사각형을, 사과와 같은 객체에 있어서는 정사각형을 사용한다.


YOLO가 앵커 박스를 개선하는 방법

실제로 YOLOv2는 다음 공식을 사용해 최종 경계 상자 좌표를 각각 계산한다.

KakaoTalk_20210817_234755623

  • tx, ty, tw, th는 마지막 계층의 출력이다.
  • bx, by, bw, bh는 각각 예측된(정교화된) 경계 상자의 위치와 크기다.
  • pw, ph는 앵커 박스의 원래 크기를 나타낸다.
  • cx, cy는 현재 그리드 셀의 좌표다. (상단 왼쪽 상자는 (0,0), 상단 오른쪽 상자는 (w-1, 0), 하단 왼쪽 상자는 (0, h-1))

신경망의 출력은 원시 숫자로 이뤄진 행렬로 경계 상자 목록으로 변환돼야 한다. 코드를 단순화하면 다음과 같다.

boxes = []
for row in range(grid_height): # 각 행에 대해
    for col in range(grid_width): # 각 열에 대해
        for b in range(num_box):    # 각 앵커 박스에 대해
            tx, ty, tw, th = network_output[row, col, b, :4]
            box_confidence = network_output[row, col, b, 4]
            classes_scores = network_output[row, col, b, 5:]
            
            bx = sigmoid(tx) + col
            by = sigmoid(ty) + row
            
            # anchor_boxes는 각 앵커 박스의 크기를 포함한 딕셔너리의 리스트임.
            bw = anchor_boxes[b]['w'] * np.exp(tw)
            bh = anchor_boxes[b]['h'] * np.exp(th)
            
            # 보정된 앵커 박스(정교화된 경계 상자)로 변환
            boxes.append((bx, by, bw, bh, box_confidence, classes_scores))

위 코드는 추론할 때마다 이미지에 대한 경계 상자를 계산하기 위해 실행된다. 이 상자를 표시하기 전에 사후 처리 연산이 하나 더 필요하다.


상자를 사후처리하기

결국 예측된 경계 상자의 좌표와 크기와 함께 신뢰도와 클래스 확률을 얻게 된다. 이제 신뢰도를 클래스 확률과 곱하고 높은 확률만 유지하게 임곗값을 설정하면 된다.

# 신뢰도는 부동소수점, 클래스는 크기가 NUM_CLASSES인 배열
final_scores = box_confidence * classes_scores

OBJECT_THRESHOLD = 0.3
# 필터는 bool 값의 배열, 숫자가 임곗값보다 높으면 True
filter = classes_Scores >= OBJECT_THRESHOLD

filtered_scores = class_scores * filter

다음은 위 코드의 연산을 간단한 샘플로 보여준 예로, 임곗값은 0.3, 상자 신뢰도(이 특정 상자에 대해)는 0.5로 정했다.

CLASS_LABELS dog airplane bird elephant
class_scores 0.7 0.8 0.001 0.1
final_scores 0.35 0.4 0.0005 0.05
filtered_scores 0.35 0.4 0 0

filtered_scores가 null이 아닌 값을 포함한다면 임곗값보다 큰 클래스가 최소 하나가 있다는 것을 뜻한다.

점수가 가장 높은 클래스를 유지한다.

class_id = np.argmax(filtered_scores)
class_label = CLASS_LABELS[class_id]

위 코드에서 class_label은 ‘airplane’이다.

이 필터링 연산을 그리드 내 모든 경계 상자에 적용하면 결국 예측을 그리기 위해 필요한 모든 정보를 얻게 된다.

KakaoTalk_20210817_205439010

위의 왼쪽 사진을 보면 수많은 경계 상자가 겹쳐있는데, 이는 객체가 여러 그리드 셀에 걸쳐 있으므로 한 번 이상 탐지되기 때문이다.

이를 보정하기 위해 사후 처리 파이프라인의 마지막 단계로 비최댓값 억제(non-maximum suppression, NMS)가 필요하다.


NMS

NMS의 기본 아이디어는 확률이 가장 높은 상자와 겹치는 상자들을 제거하는 것이다. 따라서 최댓값을 갖지 않는 상자들을 제거한다.

그러기 위해서는 확률 기준으로 모든 상자를 정렬하고 먼저 가장 확률이 높은 상자를 취한다. 그런 다음 각 상자에 대해 다른 모든 상자와의 IOU를 계산한다.

하나의 상자와 그 밖의 상자 사이의 IOU를 계산한 다음, 특정 임곗값(일반적으로 0.5~0.9 사이)을 넘는 상자들을 제거한다. (특정 임곗값은 넘는 상자들은 확률이 가장 높은 상자가 가리키는 객체와 같은 객체를 가리킬 확률이 높으므로)

# Pseudo Code
sorted_boxes = sort_boxes_by_confidence(boxes)
ids_to_suppress = []

for maximum_box in sorted_box:
    for idx, box in enumerate(boxes):
        iou = compute_iou(maximum_box, box)
        if iou > iou_threshold:
            ids_to_suppress.append(idx)
            
processed_boxes = np.delete(boxes, ids_to_suppress)

✋ 실제로 텐서플로는 자체적으로 구현한 NMS인 tf.image.non_max_suppression(boxes,…)를 제공하며 이 함수를 사용할 것을 추천한다. 또한 NMS는 대부분의 객체 탐지 모델의 사후 처리 파이프라인에서 사용된다는 점을 알아두자.

NMS를 수행하면 위의 사진에서 오른쪽과 같은 결과를 얻을 수 있다.


YOLO 추론 요약

종합해보면, YOLO 추론은 몇 가지 작은 단계로 구성된다.

  1. 특징 추출기에서 생성된 특징 볼륨(wxhxD) ➡ (1x1xD 필터)
  2. 그리드의 한 위치에서의 1xM 출력 ➡ (정교화된 경계 상자값 계산)
  3. 경계 상자 리스트 ➡ (threshold + NMS)
  4. 경계 상자 리스트

또는

  1. 입력 이미지를 받아 CNN 백본을 사용해 특징 볼륨을 계산한다.
  2. 합성곱 계층을 사용해 앵커 박스 보정. 객체성 점수, 클래스 확률을 계산한다.
  3. 이 출력을 사용해 경계 상자의 좌표를 계산한다.
  4. 임곗값보다 낮은 상자는 걸러내고 남은 상자는 비최댓값 억제 기법을 사용해 사후 처리한다.

전체 프로세스가 합성곱, 필터링 연산으로 구성되기 때문에 이 네트워크는 크기와 비율에 상관없이 어떤 어미지라도 받을 수 있다. 따라서 이 네트워크는 매우 유연하다.



YOLO로 훈련시키기


앞에서 YOLO의 추론 절차를 알아봤다. 온라인에서 제공된 사전 훈련된 가중치를 사용함으로써 모델을 바로 인스턴스화하고 예측을 생성하는 것이 가능하다. 그러나 특정 데이터셋에서 모델을 훈련시켜야 할 수 있다. 이번에는 YOLO의 훈련 절차를 따라가본다.


YOLO 백본 훈련 방법

앞서 언급했듯이 YOLO 모델은 백본(backbone)과 YOLO 헤드(YOLO head)라는 두 개의 주요 부분으로 구성된다. 백본으로는 많은 아키텍처를 적용할 수 있다.

케라스는 네트워크에서 사전 훈련된 백본을 사용하는 일을 매우 쉽게 해준다.

input_image = Input(shape=(IMAGE_H, IMAGE_W, 3))
true_boxes = Input(shape=(1, 1, 1, TRUE_BOX_BUFFER, 4))

inception = InceptionV3(input_shape=(IMAGE_H, IMAGE_G, 3), 
                        weights = 'imagenet', include_top=False)

features = inception(input_image)
GRID_H, GRID_W = inception.get_output_shape_at(-1)[1:3]
# (grid_h, grid_w) 출력
output = Conv2D(BOX * (4 + 1 + CLASS), 
                (1,1), 
                strides=(1,1), 
                padding='same', 
                name='DetectionLayer', 
                kernel_initializer='lecun_normal')(features)

output = Reshape((GRID_H, GRID_W, BOX, 4+1+CLASS))(output)

위 아키텍쳐는 논문에서 제시한 아키텍쳐이고, 가장 좋은 결과를 낸다. 그렇지만 모바일 환경에서 모델을 실행하려면 더 작은 모델을 사용해야 할 것이다.


YOLO 손실

마지막 계층의 출력이 상당히 일반적이지 않아서, 그에 대응하는 손실 또한 매우 복잡하다. 이를 설명하기 위해 손실을 여러 부분으로 나눌 수 있는데, 각각은 마지막 계층에서 반환되는 출력의 한 종류에 해당한다.

  • 경계 상자 좌표와 크기
  • 객체가 경계 상자 안에 있을 신뢰도
  • 클래스에 대한 점수

이 손실에 대한 기본 개념은 오차가 높을 때 손실도 높아야 한다는 것이다. 그래야 손실이 부정확한 값에 패널티를 부과할 것이다. 그렇지만 그렇게 하는 것이 타당할 때에만 패널티를 부과해야 한다. 만약 경계 상자에 아무 객체도 포함돼 있지 않으면 어차피 사용하지 않기 때문에 해당 좌표에 패널티를 부과해서는 안된다.

KakaoTalk_20210817_230317008


경계 상자 손실

손실의 첫번째와 두번째 항은 네트워크가 경계 상자 좌표와 크기를 예측하기 위해 가중치를 학습하는 데 가이드를 제공한다.

  • 람다: 손실에 가중치를 부여한다. 이것은 훈련하는 동안 경계 상자 좌표에 얼마만큼의 중요도를 부과할 지를 반영한다.
  • 시그마: 오른쪽 옆에 나오는 것들의 합계를 계산한다. 이 경우, 그리드 각 부분에 대해(i=0 부터 i=S2까지) 합계를 내고 그리드의 이 부분에 포함된 각 상자에 대해(0부터 B까지) 합계를 낸다.
  • 1obj: ‘객체에 대한 지시 함수’는 그리드의 i번째 부분의 j번째 경계 상자가 해당 객체를 탐지한다면 1이 되는 함수다. 여기에서 ‘탐지한다면’의 의미는 다음 단락에서 설명한다.
  • xi, yi, wi, hi: 경계 상자 크기와 좌표에 해당한다. 예측값(네트워크 출력)과 목표값(또는 실제값) 사이의 차이를 구한다. 여기에서 예측값은 햇(^)을 쓰고 있다.
  • xi, yi는 차이를 제곱해 양수를 만든다.
  • wi, hi의 제곱근의 차이를 제곱한다. 이렇게 함으로써 크기가 작은 경계 상자에 대한 오차에 크기가 큰 경계 상자에 대한 오차보다 더 큰 패널티를 부과한다.

이 손실의 핵심은 지시 함수(indicator function)다. 해당 상자가 객체를 탐지할 때만 좌표가 정확하다. YOLOv2에서는 탐지된 객체를 포함하면서 IoU가 가장 높은 앵커 박스가 해당 객체를 탐지한 것으로 간주한다.


객체 신뢰도 손실

손실의 세번째와 네번째 항은 경계 상자가 객체를 포함하는 지 여부를 예측하기 위해 가중치를 학습하도록 네트워크를 가르친다.

  • Cij: 그리드의 i번째 부분에서 j번째 상자에 객체(어떤 종류라도)가 포함될 신뢰도.
  • 1noobj: 그리드의 i번째 부분에서 j번째 경계 상자에서 객체가 ‘탐지되지 않을 때’ 1이 되는 함수

1noobj에서 ‘탐지되지 않을 때’라는 뜻은 ‘상자가 어떤 객체도 탐지하지 못하’면서 ‘상자가 여느 객체 경계 상자와도너무 많이 겹치지 않은’ 것을 의미한다.

실제로 위치 (i,j)의 경계 상자마다 각각의 실제 상자에 대해 IoU를 계산한다. 이 IoU가 특정 임곗값(일반적으로 0.6)을 넘으면 1noobj는 0이다. 이 개념은 객체를 포함하고 있으나 앞서 말한 객체를 탐지하지 않은 상자에 패널티를 부과하는 것을 피하기 위해서 고안되었다.


분류 손실

손실의 마지막 다섯번째 항은 네트워크가 각 경계 상자에 대해 적절한 클래스를 예측하게 해준다.

YOLO 논문에서는 L2 손실을 이용하지만 교차-엔트로피(cross-entropy)를 사용하는 구현도 많다.


전체 YOLO 손실

전체 YOLO 손실은 앞서 자세히 설명한 3개의 손실의 합이다. 이 세 항을 결합함으로써 전체 손실은 경계 상자 좌표 계선, 객체성 점수, 클래스 예측에 대한 오차에 패널티를 부과한다. 이 오차를 역전파함으로써 정확한 경계 상자를 예측하도록 YOLO 네트워크를 훈련시킬 수 있다.


훈련 기법

손실이 발산되는 것을 막고 우수한 성능을 얻기 위해 사용되는 몇 가지 훈련 기법을 알아본다.

  • 데이터 보강드롭아웃을 사용한다. 이 두 기법이 없다면 네트워크는 훈련 데이터에서 과적합되어 일반화될 수 없게 된다.
  • 또 다른 기법으로는 다중-척도 훈련(multi-scale training)이 있다. n개의 분기마다 네트워크 입력이 다른 크기로 바뀐다. 이렇게 함으로써 네트워크는 다양한 입력 차원에서 정확하게 예측하는 법을 학습한다.
  • 대부분의 탐지 네트워크처럼 YOLO는 이미지 분류 작업에 대해 사전 훈련되어 있다.
  • 이 논문에서 언급하지는 않지만, 공식 YOLO 구현에서는 번-인(burn-in)을 사용한다. 이 기법은 손실 폭발을 피하기 위해 훈련 초기에 학습률을 감소시킨다.

Leave a comment