지난시간에 Loss를 계산하기 위해 각 Grid 별로 Ground Truth를 정규화하는 과정을 알아보았다.
이제 그 결과와 Prediction 결과로 YOLO Loss를 구하는 과정을 구현된 코드와 결과를 보면서 알아보겠다.
이 코드는 ultralytics / yolov5 > utils > loss.py 에 구현되어있다.
코드를 보기 전에 먼저 설명하면,
YOLO Loss는
1. Classes Loss
2. Objectness Loss
3. Location Loss 의 합으로 이루어져있다.
각 Scale(v5는 3개, v6는 4개, ...), 각 Grid에 대해 모두 구해서 더한다.
Classes Loss는 Class를 잘 찾기 위한 Loss이고
Objectness Loss는 해당 Grid안에 물체가 있을지 없을지에 대한 Loss이며
Location Loss는 Box의 Center Point(X,Y)와 Width, Height를 잘 찾기 위한 Regression Loss이다.
Classes Loss와 Objectness Loss는 이진 분류 문제를 풀 때 사용하는 Binary Cross Entopy에 Sigmoid Layer를 추가한 torch.nn.BCEWithLogitsLoss() 를 사용한다.
이는 YOLO v3에서부터 사용한 Loss이며 Multi-Label Classification 문제에 사용되는 Loss이다.
즉, 모든 Class의 합을 1로 만들어서 가장 값이 높은 하나의 Class만 결과가 되는 것이 아니라,
각 Class가 모두 0~1 사이 값을 가져 일정 Threshold 이상이 되면 두 개 이상의 Class도 결과가 될 수 있는 것이다.
예로 어떤 물체가 사람이면서 동시에 여성일 수도 있다.
이럴 때 Label도
사람 여성 강아지 고양이
[ 1 1 0 0 ]
의 형태가 된다.
물론 Multi Label Classification이 아닐 때도 사용 가능하다.
두 Loss 모두 원한다면 Focal Loss를 사용할 수도 있다. hyper parameter에 따라 0이면 그냥 BCE Loss를, 그 이상이면 Focal Loss를 사용한다.
Focal Loss는 Class Imbalabce 문제를 해결하기 위한 Loss로,
Classes Loss에서는 말 그대로 Class들 간 데이터 개수 차이를 완화하기 위한 것으로 작용하고,
Objectness Loss에서는 물체가 있고/없고의 Imbalance를 해결하기 위해 작용한다.
이에 대한 자세한 설명은 YOLOv1~v6를 리뷰하는 포스팅에 다 설명해 놓았다. (1-2. Sementic Distribution Bias 참고)
단, Objectness Loss는 더 작은 이물을 찾는 Scale에 더 큰 가중치를 두어 더한다.
예로 Scale이 3개인 YOLO v5에서는 다음과 같이 4.0, 1.0, 0.4의 가중치를 곱해서 더하고,
$$ L_{obj} = 4.0*L^{small}_{obj} + 1.0*L^{medium}_{obj} + 0.4*L^{large}_{obj} $$
Scale이 4개인 YOLO v6에서는 Small부터 차례로 4.0, 1.0, 0.25, 0.06을 곱해서 더한다.
다음으로 Location Loss는 CIoU Loss를 사용하며, 보통 Regression에서 사용하는 MSE가 아닌 IoU 기반의 Loss를 사용하는 이유와 CIoU가 무엇인지도 YOLO v1~v6를 리뷰하는 포스팅에서 다 설명해놓았다. (1-3. BBox Loss Function 참고)
세 Loss를 구한 뒤 모두 더하는데, 이 때도 가중치를 곱해서 더한다. (가중치는 hyperparameter)
이제 실제 구현된 코드로 알아보겠다.
사용 모델과 이미지, Label은 모두 지난 포스팅(build_targets())에서 사용한 것과 같은 것을 사용한다.
이미지에 대한 설명은 모두 지난 포스팅에있다.
먼저 필요한 Library들을 Import한다.
개발자가 구현해 놓은 것들도 사용하기 위해 yolov5 폴더 경로를 sys.path.insert()로 입력해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import sys
sys.path.insert(0, 'Path of yolov5 directory')
import os
import cv2
from models.yolo import Model
import numpy as np
import torch
import torch.nn as nn
from utils.metrics import bbox_iou
from utils.torch_utils import de_parallel, select_device
from utils.loss import ComputeLoss
from models.experimental import attempt_load
|
cs |
target(label)과 prediction 결과로 loss를 계산하므로,
prediction 결과를 뽑기위한 Model과 이미지를 불러온다.
학습한 본인의 Model을 불러온다. 없다면 개발자가 제공하는 COCO Dataset에 학습시킨 모델을 불러와도 좋다.
본인은 직접 학습한 YOLOv6 모델을 사용한다.
1
2
3
|
weights = "Path your Weights(.pt)"
model = attempt_load(weights, inplace=True, fuse=True)
device = select_device('cpu')
|
cs |
이미지를 불러오고, Model에 Input으로 넣을 수 있도록 전처리한다.
1
2
3
4
5
6
7
8
9
|
img = cv2.imread("Path your image(jpg, png, ...)")
img = cv2.resize(img, (128,128), interpolation=cv2.INTER_AREA)
im = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
im = np.ascontiguousarray(im)
im = torch.from_numpy(im)
im = im.float()
im /= 255
|
cs |
불러온 이미지에 맞는 Label도 불러온다.
1
2
3
4
5
|
labels = np.loadtxt("Path of your txt file", ndmin = 2)
labels_out = torch.zeros((1, 6))
labels_out[:, 1:] = torch.from_numpy(labels)
targets = labels_out # image, class, xywh
|
cs |
Model에 불러온 이미지를 넣어 Prediction 결과를 생성한다.
1
2
|
pred = model(im[None])
p = pred[1]
|
cs |
tuple 형태로 떨어지는 두 개의 결과 중 두번째걸 사용한다. 이에 대한 설명은 바로 이전 포스팅(build_targets())에 있다.
이제 target(label)과 prediction 결과로 Loss를 계산한다.
Loss를 계산하는 ComputeLoss는 Class로 구현되어있다.
그 중 생성자인 __init__() 부터 살펴보겠다.
실제 구현된 내용은 다음과 같다.
먼저 필요한 변수들을 선언한다.
학습할 때 hyper parameter는 yolov5 > data > hyps > hyp.scratch-low.yaml을 사용했다.
smooth_BCE()는 Class Label을 Smoothing하는 역할을 한다. 예로 [1, 0, 0, 0]을 [0.9, 0.1, 0, 0] 이렇게 바꿔준다.
이는 서로 다른 Class간에 연관성을 표현하기 위함이고 모델을 더 Robust하게 만드는 효과가 있다고 한다.
(단, 본인 모델은 학습할 때 이 기능을 사용하지 않았다.)
만약, hyper parameter에서 fl_gamma가 0보다 크다면 focal loss를 사용한다. (본인은 사용하지 않았다.)
다음으로 모델에서 마지막 Detection Layer만 따로 빼준다.
anchor, class 개수 등 정보를 파악하기 위함이다.
이제 나머지 변수들도 선언해준다.
balance가 위에서 설명했던 각 Scale별 가중치이다. 본인 모델은 YOLOv6로 Detection Scale이 4개이므로 각각 4.0, 1.0, 0.25, 0.06을 사용한다.
anchors에 대해 설명하면,
본인이 사용한 모델은 YOLOv6의 small 모델로, 모델 archtecture와 사용한 anchors는 yolov5 > models > hub > yolov5s6.yaml에서 확인할 수 있다.
모델에 사용된 anchor는 다음과 같다.
4개 Scale이고 각 Scale별로 3개씩 anchor box의 width와 height를 나타내고 있다.
각 Scale의 Stride가 8, 16, 32, 64이므로
[19, 27] / 8 = [2.375, 3,375]
[44, 40] / 8 = [5.5, 5]
...
이다. 이는 위에 m.anchors의 결과와 동일함을 알 수 있다.
이제 실제로 loss를 계산하는 __call__() 부분으로 넘어가면, 구현된 내용은 다음과 같다.
build_targets() 에 대한 내용도 같이 구현되어있으나, 이는 지난 포스팅에서 설명한 내용으로 넘어가겠다.
먼저 classes loss, localization loss(box loss), objectness loss의 결과를 담을 변수를 선언한다.
그리고, build_targets()에 label을 input으로 넣어
Ground Truth를 포함하는 grid cell 3개와,
거기에 맞게 normalization된 box coordination,
그리고 해당 box를 찾기에 가장 적절한 anchor를 각 Scale별로 계산한다.
(이에 대한 자세한 설명은 이전 포스팅 참고)
그리고 for loop로 들어가 각 Scale에 대해 loss를 계산하고 더해준다.
어차피 본인이 넣어준 이미지에 물체는 작아서 첫 번째 Scale에만 해당하므로 i=0일때만 확인해보겠다.
prediction 결과 중 첫 번째 Scale에 해당하는 결과의 shape은 [1, 3, 16, 16, 10]이다.
한 장의 이미지만 input으로 넣었으므로 첫 번째 값은 1이고,
총 3개의 anchor가 있으므로 두 번째 값은 3이다.
output grid가 16by16이므로 세번째, 네 번째 값은 16이고,
본인 task는 5개의 class를 가지고 있으므로 class 5개 + box(x,y,w,h, conf_score) 5개 = 총 10이다.
위 build_targets() 에서 ground truth를 포함하는 3개 Grid에 대한 위치를 가지고 있는 indices 결과를 각 변수에 넣어주고
objectness loss를 계산하기 위한 tobj 변수를 선언한다.
b는 image_id 같은 개념이고,
a는 정답 class,
gj, gi는 GT를 포함한 grid가 몇 번째 grid인지를 나타내고 있다.
3개 grid cell이 target이므로 b.shape[0]은 3이다.
prediction 결과 중 GT를 포함하는 3개 grid cell에 대한 예측값만 가져온다.
xy 좌표, wh, class probality를 따로 담아준다.
이제 Localization Loss(Box Loss)를 계산한다.
YOLOv2와 v3에서는 BBox의 x,y,w,h를 다음과 같이 계산했다.
(pw와 ph가 기존 anchor box의 width, heigth이다.)
YOLOv5와 v6는 이를 좀 변형하여 다음과 같이 계산한다.
tx, ty, tw, th를 학습해서 bx, by, bw, bh를 만드는 건 똑같으나,
1. 기존에는 center point(x,y)를 0~1 값으로 사용했다면, v5/v6에서는 2를 곱하고 0.5를 빼서 -0.5~1.5 값으로 사용한다.
이유는 0 또는 1을 더 잘 얻기 위함이라는데 이건 잘 모르겠다..
2. width와 heigth도 기존에는 exp 에서 -> sigmoid취해 2를 곱하고 제곱하는 것으로 바뀌었다.
exp를 사용하면 w, h값이 무한대로 커질 수 있기 때문이다.
이미지를 모델에 넣어 예측한 $t_x, t_y, t_w, t_h$를 $b_x, b_y, b_w, b_h$로 바꾸고 GT와 CIoU를 구해 1-CIoU를 Box Loss로 사용한다.
다음으로 Objectness Loss이다.
Objectness Loss는 각 grid cell별로 그곳에 물체가 있을지, 없을지에 대한 Loss이다.
예측 결과에서는 Confidence Loss가 확률로써 그 역할을 한다.
prediction shape과 동일한 shape을 가지고, 0으로 채워준 tobj 중 GT를 포함하고있는 세 개 grid cell 위치에 위에서 계산한 IoU값을 채워준다.
그리고 prediction 결과 중 confidence score와 BCE Loss를 구해주고,
위에서 설명한대로 각 Scale별로 가중치를 곱해서 더해준다.(balance가 그 역할)
마지막으로 Classes Loss이다.
이 task는 5개 class를 가지고있고, 해당 이미지는 첫 번째 class가 정답이므로 아래와 같이 정답에 대한 t 변수를 만들어주고
예측값 중 class probality와 BCE Loss를 계산한다.
이렇게 i=0부터 i=3까지 위 과정을 반복하여 모든 Scale에 대해 Loss를 구해 더한다.
그리고 나서 hyper parameter에 설정한대로 class, obj, box loss에 가중치를 곱해 모두 더하면 loss 계산 끝이다.
'AI > Object Detection' 카테고리의 다른 글
[Object Detection] YOLO Define Optimal Anchor Box :: YOLO v5, YOLO v6 autoanchor (0) | 2022.08.30 |
---|---|
[Object Detection] YOLOv5, YOLOv6 Loss 구하는 과정 중 build_targets() 이해하기 (2) | 2022.08.04 |
[Object Detection] YOLO v1 ~ v6 비교(2) (8) | 2022.06.23 |
[Object Detection] YOLO v1 ~ v6 비교(1) (6) | 2022.06.23 |
[Python] Object Detection Mosaic Augmentation :: YOLO v5 (2) | 2022.06.09 |