https://gmongaras.medium.com/yolox-explanation-how-does-yolox-work-3e5c89f2bf78
YOLOX Explanation — How Does YOLOX Work?
The second article in my YOLOX explanation series where I talk about the YOLOX model from a general perspective.
gmongaras.medium.com
위 글의 한국어 번역입니다.
Darknet-53 - The YOLOX Backbone
YOLOv3 알고리즘은 많은 객체 탐지 알고리즘들의 기반이며 YOLOX 역시 이를 기반으로 합니다. YOLOv3로 들어가기 전에, 지난 글에서 간단하게 설명했던 YOLOv1의 동작을 알고 있다고 가정하겠습니다.
[CoIn] YOLOX Explanation — What is YOLO and What Makes It Special?
https://gmongaras.medium.com/yolox-explanation-what-is-yolox-and-what-makes-it-special-c01f6a8a0830 YOLOX Explanation — What is YOLO and What Makes It SpecialThe first article in my YOLOX explanation series where I introduce the original YOLO modelgm
hw-hk.tistory.com
YOLOv3 알고리즘은 원래의 YOLO와 전체적으로는 매우 비슷합니다. 하지만 몇몇 작지만 영향이 큰 변경점들을 도입합니다. YOLOv3의 주요 변화는 Darknet-53 이라 불린는 매우 큰 백본(backbone)을 사용한다는 점입니다(백본, backbone이란 데이터가 처음 통과하는 unspecialized, 즉, general한 구조를 말합니다). 이 backbone 아키텍처는 1x1 conv, residual connection, 3x3 conv를 활용해 매우 강력하게 이미지의 특징들을 추출합니다.
이 backbone은 YOLOX에서도 사용되며 구조는 다음과 같습니다:

YOLOv3에서 예측을 얻는 첫 단계는 이미지를 이 backbone에 통과시켜 데이터를 인코딩(encoding)하고 그 다음 YOLOv 헤드(head)가 최종 예측을 만들 수 있도록 하는 것입니다(backbone과 달리 head는 예측을 수행하도록 "특화"된 아키텍처입니다).
YOLOv3 모델의 head는 기본적으로 YOLOv1의 헤드와 거의 같습니다. 하지만 YOLOX의 경우 head를 완전히 바꾸었기 때문에 YOLOv1과 v3사이의 head차이는 그리 중요하게 다루지 않겠습니다. YOLOv1의 최종 예측은 가로/세로 차원이 서로 다른 예측들이고, 깊이 차원이 예측하는 다양한 특징들을 담는 거대한 3차원 tensor였음을 기억하면 됩니다.
backbone 모델은 FPN(feature pyramid network)라는 것을 사용합니다.
https://herbwood.tistory.com/18
FPN 논문(Feature Pyramid Networks for Object Detection) 리뷰
이번 포스팅에서는 FPN 논문(Feature Pyramid Networks for Object Detection)을 리뷰해보도록 하겠습니다. 이미지 내 존재하는 다양한 크기의 객체를 인식하는 것은 Object dection task의 핵심적인 문제입니다. 모
herbwood.tistory.com
FPN은 서로 다른 가로/세로 스케일에서 이미지의 정보를 추출합니다. 이를 Darknet과 함께 하려면 일명 transition states라고 불리는 곳에서 출력을 가져와 각 스케일 당 출력으로 사용합니다. 즉, 여러 개의 출력을 만들어내는 것입니다. 아래는 이 동작 방식으로 보여주는 도식입니다:

요지는, Darknet-53 backbone(이하 FPN)이 서로 다른 스케일에서 세 가지 예측을 출력한다는 것입니다:
(1) 256 channels (first transition output)
(2) 512 channels (second transition output)
(3) 1024 channels (third transition output)
이 각각의 출력은 서로 다른 스케일의 정보를 추출합니다. 채널 수가 증가할수록 이미지의 가로/세로 해상도가 감소합니다. 그래서 256 channels transition output은 더 작은 스케일의 특징을 추출하고, 1024 channel transition output은 원본 이미지로부터 가져올 수 있는 정보가 더 적기 때문에 더 큰 스케일의 특징을 추출합니다.
YOLOv3 Head vs. YOLOX head
YOLOv3 backbone과 YOLOX backbone은 동일하지만, head는 서로 다릅니다. 아래 그림은 두 head의 차이를 보여줍니다:

아래는 이해를 돕기 위해 표시를 덧댄 버전입니다:

이 그림이 말하는 바는, YOLOv3 head와 YOLOX head 모두의 입력은 FPN backbone에서 나온 3가지 스케일의 출력이라는 것입니다. 두 head의 출력 형태는 본질적으로 동일하며 (HxWxFeature) 차원을 갖습니다. YOLOX는 분리형(decoupled) head를 사용한다는 점에서 차이가 있습니다. 따라서 YOLOX의 출력은 모든 정보를 하나의 거대한 tensor에 담는 대신 서로 다른 정보를 담는 3개의 tensor로 나오게 됩니다.
YOLOX가 출력하는 세 개의 tensor는 YOLOv3의 거대한 텐서가 담던 정보와 동일합니다:
Cls: 각 bbox의 클래스
Reg: bbox의 4요소(x, y, w, h)
IoU(Obj): 그 박스에 객체가 있을 신뢰도
원래 출력과 마찬가지로, 출력의 가로/세로의 각 픽셀은 서로 다른 bbox 예측 하나를 의미합니다. 따라서 총 예측의 개수는 HxW개 입니다.
위에서 나열한 출력은 FPN의 단일 출력에 대해서만 해당합니다. FPN은 세 가지 출력이 있고, 이들이 YOLOv3와 YOLOX의 head로 들어갑니다. 즉, 각 head에서 실제로는 하나가 아니라 세 가지 출력이 나옵니다. 그래서 YOLOv3의 출력은 (3xHxWxFeature)가 되고, YOLOX의 출력은 Cls, Reg, IoU 각각을 3개 스케일로 내므로 총 9개의 출력이 됩니다.
Switching To An Anchor-free Model
YOLOX의 가장 중요한 변화는 앵커(anchor)를 쓰지 않는 것이며, 반면 YOLOv3는 앵커에 크게 의존합니다.
What is an anchor?
앵커는 본질적으로 네트워크를 돕기 위해 미리 정의된 bbox의 형태입니다. 이전 YOLO알고리즘들은 bbox를 직접 예측하는 대신 미리 정의된 앵커 박스로부터의 오프셋(offset)을 예측했습니다. 예를 들어, 앵커 박스의 길이/너비가 100/50이고 모델이 10과 15를 예측했다면, 최종 bbox는 앵커 박스로부터의 오프셋을 반영해 길이/너비가 110과 65가 됩니다.
The Problem with Anchor Boxes
앵커 박스는 사실상 추가 파라미터입니다. 앵커를 몇 개 쓸 것인가? 앵커의 크기는 어떠게 할 것인가? 이런 질문들은 더 많은 하이퍼파라미터 튜닝을 요구하고 모델의 다양성을 떨어뜨립니다.
How Does YOLOX Fix The Anchor Box Problem?
YOLOX는 간단히 앵커 박스 오프셋을 예측하는 대신, bbox의 치수, 크기 자체를 직접 예측합니다. 이를 위해서 위에서 설명한 decoupled head를 사용합니다. 추가로 스트라이딩(striding)이라는 방식을 사용합니다.
Striding
YOLOX는 YOLOv3만을 기반으로 하는 것이 아니라, FCOS(또 다른 bbox 모델, YOLO 시리즈는 아님)에도 기반을 두었습니다. FCOS는 striding을 사용해 모델을 돕는데, 모델이 좌상단 (0,0)에서 우하단 (1024,1024)까지 어디에나 bbox를 예측해야 한다고 생각해보면, 이산 공간에서는 가능한 위치가 1,048,576개나 되어 예측 범위가 지나치게 넓어 제대로 학습할 수 없습니다.
striding은 이 문제를 해결하며, 모델이 이미지의 좌상단 기준이 아니라 오프셋 기준으로 예측하도록 해줍니다. 기본 아이디어는 모델이 예측을 수행할 세 가지 스케일에 맞춰 이미지를 그리드(grid)로 분할하는 것입니다. 예를 들어, 그리드는 아래처럼 생겼을 수 있습니다:

이 그리드를 사용해, 각 예측을 그리드의 교차점들에 할당할 수 있습니다. YOLOX의 예측이 좋은 점은 예측이 이미 가로x세로 형태라는 점입니다. 따라서 출력을 그리드의 고유한 점에 직접 매핑하고, 그 그리드 점을 오프셋으로 사용해 bbox를 스케일링 할 수 있습니다.
위 그리드는 그리드의 교차점 간 거리를 의미하는 stride를 정해 만들 수 있습니다. YOLOX에서는 각 FPN 레벨에 대해 32, 16, 8의 stride를 사용합니다. stride 32를 256x256 이미지에 적용하면 각 축에 256/32 = 8개의 교차점, 총 64개의 교차점이 생깁니다.
예시로 위에서 정의한 YOLOX FPN stride를 사용해 아래의 이미지에 그리드를 얹어보겠습니다:

다음 이미지는 곰 사진 위에 그리드가 오버레이 된 모습입니다:

이미지의 각 교차점을 앵커 포인트(anchor point)라고 부릅니다. 앞서 설명한 앵커 박스와는 다른 개념입니다. 여기서의 앵커 포인트는 예측의 x, y 위치를 이동시키는 오프셋이고, (YOLOX가 없앤) 이전의 앵커 박스는 예측의 w, h 부분을 위한 미리 정의된 박스였습니다. 앵커 박스는 튜닝해야할 추가 하이퍼-파라미터라서 좋지 않지만, 앵커 포인트는 추가 파라미터가 필요 없으므로 문제가 없습니다.
이미지에서의 앵커 위치는 다음 공식으로 구합니다:
x = s/2 + s*i
y = s/2 + s*j
여기서 s는 stride, i는 x축 i-th 교차점, j는 y축 j-th 교차점을 말합니다.
YOLOX에서는 그리드 점을 bbox의 좌상단 오프셋으로 사용합니다. 예측된 bbox (p_x, p_y, p_w, p_h)를 실제 이미지 좌표 (l_x, l_y, l_w, l_h)로 매핑할 때, 이 예측이 속한 그리드 교차점이 (x, y)이고 현재 FPN 레벨의 stride가 s라면 다음 공식을 사용합니다:
l_x = p_x + x
l_y = p_y + y
l_w = s*e^(p_w)
l_h = s*e^(p_h)
예측된 점 (p_x, p_y)에 앵커(할당된 x, y점)를 더해 예측 위치를 이동시킵니다. 또한 폭과 높이를 지수 함수를 사용해 음수가 되지 않도록 하면서 이미지의 stride에 맞춰 비정규화합니다(p_x와 p_y는 할당된 anchor point에서로부터의 "오프셋"을, 폭과 높이는 stride 크기에 대한 "비율"을 예측하는 것입니다).
예를 들어, stride = 32의 곰 이미지를 다시 보면, 이 예측의 앵커 포인트가 (i,j) = (2,1)이라면, 이미지에서 다음과 같은 점을 볼 수 있습니다:

그리드 상의 좌표는 (2,1)이지만 pixel 좌표로는 다음과 같습니다:
x = 32/2 + 32*2 = 16 + 64 = 80
y = 32/2 + 32*1 = 16 + 32 = 48
모델이 (20, 15, 0.2, 0.3)을 예측했다면, bbox는 다음과 같이 계산됩니다:
l_x = 20 + 80 = 100
l_y = 15 + 48 = 63
l_w = 32*e^(0.2) = 39
l_h = 32*e^(0.3) = 43
따라서 최종 이미지는 아래와 같습니다:

Label Assignment
모든 예측이 동일한 것은 아닙니다. 어떤 것들은 예측이 명백하게 틀려서 아예 모델이 그런 것들까지 학습하길 원하지 않을 수 있습니다(YOLO는 HxW개 만큼의 예측을 만들어내기 때문에 명백히 틀린 예측들의 개수가 많고, 이들을 걸러내는 작업이 필요한 것입니다). 이렇게 좋은 예측과 나쁜 예측을 구분하기 위해, YOLOX는 SimOTA라는 동적 레이블 할당(dynamic label assignment) 기법을 사용합니다.
SimOTA는 다음 글에서 자세히 설명하겠지만, 지금 필요한 핵심은 이것입니다:
""좋은" 예측(정답 bbox를 제대로 둘러싼 예측)은 양성(positive)으로, "나쁜" 예측(배경을 둘러싼 예측)은 음성(negative)으로 라벨링된다."
음성 라벨 예측을 그냥 버리지는 않습니다. 세 손실 함수(cls, reg, IoU)중 하나가 이 음성 예측을 사용하기 때문입니다. 반면, 양성 라벨 예측은 매우 중요합니다. 각 앵커가 어느 정답 bbox로 최적화해야하는지 알려주기 때문입니다. SimOTA는 단순히 양성/음성을 정해ㅐ줄 뿐 아니라, 이미지 내 양성으로 라벨된 각 앵커에 대해 정답 bbox 매칭까지 수행합니다.
Loss Functions - Evaluating YOLOX
YOLOX모델에는 세 가지 출력이 있고, 각각은 최적화 방식이 달라 자기 전용 손실 함수를 갖습니다.
Class Optimization
YOLOX에 따르면, 클래스 출력의 텐서 형태는 HxWxC입니다. 즉, 각 예측마다, 모델은 길이 C의 벡터를 예측하는 것입니다. 여기서 C는 선택 가능한 클래스 수입니다. 따라서 각 원소는 해당 클래스일 확률(신뢰도), 즉 그 박스 안의 클래스가 그 클래스일 것이라는 모델의 확신 정도를 나타냅니다.
이를 최적화하기 위해, 각 앵커/예측에 대한 정답 bbox의 클래스를 원-핫(one-hot) 벡터로 인코딩해 사용할 수 있습니다. 각 예측마다 원-핫 벡터는 길이가 C이며, 모델이 맞히길 원하는 클래스 위치에 1, 나머지에 0을 둡니다. 예를 들어 클래스가 네 개고 두 번째 클래스를 정답으로 하고 싶다면, 벡터는 다음과 같을 수 있습니다:
pred1: [0.45, 0.25, 0.05, 0.25] # The model is most confident in the 1st class
pred2: [0.25, 0.25, 0.25, 0.25] # The model is not confident in any class
pred3: [0.1, 0.7, 0.1, 0.1] # The model is very confident in the second class
labels: [0, 1, 0, 0] # 1 in the second location which is what we want the model to predict
이 예측들을 최적화하기 위해, 예측과 정답 레이블을 BCE(Binary Cross Entropy) 손실에 넣을 수 있습니다. 구체적으로는, 모든 양성 예측에 대해 BCE with logits를 사용합니다. 참고로 이 손실에서는 음성 라벨 예측은 사용하지 않습니다.
원-핫 벡터를 쓰는 이유는, 정답 클래스의 값은 1에 가깝게, 나머지는 0에 가깝게 학습시키기 위함입니다. 모델은 단일 값을 예측하는 것이 아니라 모든 클래스에 대한 분포를 예측합니다. 그러므로 단일 값만 최적화하는 대신, 모델이 예측한 전체 분포를 함께 최적화 해야합니다.
Regression Optimization
회귀(bbox) 출력의 최적화는 클래스보다 조금 더 까다롭습니다. 회귀 출력의 형태는 HxWx4이며, 각 예측은 (x,y,w,h)입니다. 표면적으로는 회귀 과제이니 MSE(Mean Squared Error)가 좋아 보일 수 있습니다. 실제로 YOLOv3는 이와 비슷한 SSE(Sum of Squared Error)를 사용합니다. 하지만 이런 지표들은 학습 샘플의 회귀 타깃에 오버피팅을 유발할 수 있습니다?
Intersection Over Union(IoU)
이 문제를 해결하기 위해, YOLOX는 IoU라는 평가 지표를 사용합니다.
IoU는 예측 박스와 정답 박스를 비교해 계산합니다.
먼저 두 박스의 교집합을 구하고, 다음으로 합집합을 구합니다.
마지막으로 IoU = 교집합 / 합집합 을 계산합니다.
좀 더 들어가보면, 먼저 교집합은 합집합보다 클 수 없고, 최소 교집합은 0이므로 다음 제약이 성립합니다:
0 ≤ I ≤ U
교집합이 0일 때, 합집합은 두 박스 면적의 합 (A1 + A2)이고, 교집합이 100%일 때 합집합은 한 박스의 면적 (A1)이므로 다음이 성립합니다:
A₁ ≤ U ≤ A₁ + A₂
따라서 교집합이 커질 수록 IoU는 1에 가까워지고, 교집합이 작아질수록, IoU는 0에 가까워집니다. 즉 IoU의 범위는:
0 ≤ IoU ≤ 1
이때 모델은 IoU를 최대화하길 원합니다(교집합이 두 박스를 온전히 덮길 원하므로). 하지만 경사하강법은 손실을 최소화하므로, 보통 1 - IoU를 손실로 사용해 같은 정보를 최소화 문제로 바꿉니다. 이렇게 하면 교집합이 0%에 가까워질수록 손실은 커지고, 100%에 가까울수록 손실은 작아집니다.
Evaluating the Regression Output
실제로는 GIoU(Generic IoU)를 사용합니다. GIoU는 IoU와 유사하지만 값의 범위가 -1부터 1입니다. IoU 값의 문제는 IoU가 0인 경우가 모두 동일하게 취급되어 추가적인 정보가 없다는 점입니다. GIoU는 여기에 추가 정보를 인코딩해 IoU가 0이어도 0이 아닌 값을 주면서 더 매끄러운 함수를 제공합니다. 참고로 이 손실에서도 음성 라벨 예측은 사용하지 않습니다.
IoU/Objectness Loss
박스 안에 객체가 있다고 판단하면 objectness 점수가 1에 가깝게, 없다고 판단하면 0에 가깝게, 불확실하면 그 사이가 되길 원합니다. 따라서 0과 1 사이에 있으면서, 박스가 객체를 완벽히 덮을수록 1에 가깝고, 전혀 덮지 못하면 0에 가까운 함수를 원합니다. 여기에 IoU가 딱 맞습니다. 특히 GIoU가 아니라 IoU를 쓰는 이유는, GIoU의 범위는 2(-1~1)이지만, IoU의 범위는 1(0~1)이기 때문입니다.
클래스 손실과 유사하게, objectness 예측 최적화에도 BCE를 사용합니다. 단일 예측을 최적화할 때 두 가지 경우를 고려합니다:
(1) 양성으로 라벨된 예측들
(2) 음성으로 라벨된 예측들
양성의 경우, SimOTA가 이미 예측에 대응하는 정답 박스를 할당해두므로, 예측 박스 vs. 정답 박스의 IoU를 구해, 모델이 맞히길 바라는 목표값으로 삼습니다. 그런 다음, 예측 objectness와 그 IoU 값을 BCE with logits에 넣어 해당 예측의 손실을 계산합니다.
음성의 경우에는 문제가 있습니다. objectness 손실이 "나쁜 예측은 낮게, 좋은 예측은 높게"를 배우도록 나쁜 예측도 학습시키고 싶습니다(나쁜 예측의 개수가 좋은 예측 대비 훨씬 많기 때문에, 이를 학습에 이용하는 것이 빠른 수렴을 돕습니다). 하지만 SimOTA는 음성에는 정답 박스를 할당하지 않습니다. 그럼 음성의 정답값을 어떻게 만들까요?
한 가지 단순한 방법은 모든 음성 예측의 objectness 정답을 0으로 주는 것입니다. 하지만 이 전략의 문제는, 음성 중에도 정도의 차이가 있다는 점입니다. 모든 음성이 똑같이 나쁘진 않습니다.
더 나은 방법은 이미지 내 모든 정답 박스를 살펴보는 것입니다. 음성으로 라벨된 예측 박스와 모든 정답 박스 사이의 IoU를 계산한 다음, 그중 최대값을 택히(즉, 그 예측 박스가 가장 많이 겹치는 정답) 그 IoU 값을 해당 예측의 정답 objectness로 할당합니다. 그리고 예측 objectness vs. 할당된 IoU를 BCE with logits에 넣어 이 음성 예측의 손실을 계산합니다.
Final Loss Function
최종 손실은 위 세 손실을 조합한 것이며, 대략 다음과 같이 정의됩니다:

reg_weight은 다른 손실 대비 회귀 손실의 비중을 조절하는 가중치이며, 저자들은 5.0을 사용합니다.
Making Inferences
YOLOX의 추론 과정은 대부분의 머신러닝 모델과 유사하지만, 반드시 다뤄야 할 중요한 문제가 있습니다. 이는 YOLOX가 매우 많은 bbox를 내놓는다는 점입니다. 그리고 그 중 대부분이 좋은 예측이 아니라는 점입니다. 이를 처리하기 위해, 모델의 출력은 두 단계 가지치기(pruning)을 거칩니다:
(1) objectness threshold보다 낮은 예측을 제거합니다(실제 구현에서는 0.5미만 제거).
(2) Soft Nonmax Supression(Soft-NMS)로 예측을 한 번 더 정제합니다.
이 두 단계를 거치면, 소수의 예측만 남고 이것들이 모델의 최종 예측이 됩니다.
Nonmax Suppression
NMS는 이미지 안의 정답을 몰라도 bbox를 효과적으로 가지치기하는 좋은 방법입니다. 아래 그림처럼 서로 큰 겹침을 가진 예측들을 제거하는 식으로 동작합니다:

NMS는 겹치는 박스들 사이의 IoU를 사용해 겹침이 큰 것들을 제거하고, 최종적으로 하나의 대표 박스만을 남깁니다. 아래는 해당 글의 저자가 작성한 Soft-NMS의 pseudocode입니다:
Definitions:
B - The predicted bounding boxes with shape (x, y, w, h)
S - The confidence score for each bounding box (objectness)
C - The class for each boudning box
score_thresh - The score threshold to remove boxes
IoU_thresh - The IoU threshold to update scores
softNMS(B, S, C, score_thresh, IoU_thresh):
D = [] <- Boudning boxes we want to keep for all images
for img in imgs:
b = B[img]
s = S[img]
d = []
while b not empty:
Get the bounding box with the highest score and save it
m = argmax(s)
M = b[m]
d.append(M)
Remove the bounding box with the highest score from the lists
Get the mean of all confidence scores
mean_scores = mean(s)
Get the IoU between M and all b
IoU = IoU_funct(M, b)
Update all scores, s, where the IoU > IoU_thresh
idx = argwhere(IoU > IoU_thresh)
s[idx] = s[idx]*e^(-(IoU[idx]**2)/mean_scores)
Remove the bounding boxes from b where s < score_thresh
b = b[s >= score_thresh]
Save the bounding boxes for this image
D.append(d)
return D
이를 다시 정리하면 다음과 같습니다:
(1) objectness 점수가 가장 높은 박스를 선택
(2) 선택한 예측을 목록에서 제거
(3) 남아 있는 예측들의 objectness 평균을 계산
(4) 선택 박스 M과 나머지 b들 사이의 IoU 계산
(5) IoU가 높은 박스들의 점수를 업데이트
(6) 점수가 score_threshold 미만인 박스 제거
(7) b가 빌 때까지 (1)-(6)을 반복
점수 없데이는 공식은 원 논문(soft-NMS) 4쪽에 있는 식 가운데 하나를 사용합니다.
다음 글에서는 SimOTA가 동적 레이블 할당을 어떻게 수행하는지 다루겠습니다.