M_AI 입니다. 이전 앵커 박스의 추가적인 설명과 데이터셋 준비에 방법에 이어서,
이제 RetinaNet의 핵심인 Loss function의 설명과 코드를 설명하도록 하겠습니다!
이전글 : [RetinaNet] 3. Prepare the Dataset
이 글을 읽으면 좋을 것 같은 사람들!
- 기본적인 분류(classfication) 모델을 Tensorflow 2.x 버전으로 구현할 줄 아는 사람
- Clssification을 넘어서 Object detection을 하길 원하는데 오픈 소스를 봐도 이해가 가질 않는 사람.
- 오픈 소스를 이해하더라도 입맛대로 수정하는 방법을 모르는 사람.
본 게시글의 목적
※ 본 게시글은 딥러닝 모델의 상세한 이론적 설명보다는 모델 구조를 바탕으로 코드 분석하고 이해하는 데에 목적에 있습니다!
※ 상세한 이론을 원하면 다른 곳에서 찾아보시길 바랍니다!
본 게시글의 특징
① Framework : Tensorflow 2.x
② 학습 환경
- OS : Ubuntu
- GPU : RX570 (AMD)
③ Batch size = 1
④ Kaggle의 RSNA Pneumonia Detection Challenge 데이터셋 사용하여 Chest X-ray에서 폐렴 검출 (Pneumonia detection) 목적.
https://www.kaggle.com/c/rsna-pneumonia-detection-challenge
⑤ 게시글에서는 반말
4. Loss Function
Object detection에서 loss function은 2개의 loss function이 합쳐진 것으로, 배경(background)인지 객체인지(foreground)인지 판단하는 classification loss function과 박스 좌표를 예측하는 box regression loss
function이 있다.
각기에 대한 설명은 다음과 같다.
4.1. Explaning the Loss Function
4.1.1. Classification Loss Function
분류 loss function이 RetinaNet에서 가장 큰 부분을 차지하고 있다고 과언이 아니다. 논문[1]의 제목에서 언급되었다시피, ‘focal loss’가 여기서 설명이 된다.
여태 RetinaNet을 설명하면서 배경과 객체가 있는 부분을 분류해야 한다고 했다. 하지만 object detection에서는 배경이 있는 영역이 객체가 있는 부분보다 상당히 많기 때문에, 여기서 데이터 불균형(imbalance)가 발생하여 배경이 우세한 쪽이 편향 학습이 일어난다.
즉, Negativa 앵커 박스가 Positive 앵커 박스가 많아 편향 학습 발생!
기본적으로 딥러닝에서는 loss를 최소화하는 방향으로 학습이 진행되기에, 예측이 잘 되는 것은 더 잘되도록 학습이 된다. 여기서는 데이터가 많은 부분이(negative 앵커 박스) 쉽게 예측이 되기에, 데이터가 적은 부분(positive 앵커 박스)는 예측하기가 어려워진다.
이를 해결하기 위해서 등장한 function이 ‘focal loss’이다.
이는 간략히 설명하자면 이진 분류에 사용되는 cross entropy(CE)의 변형인데, CE의 수식은 아래와 같다.
다음은 focal loss의 수식이다.
CE와 차이 나는 점은 앞에 알파가 곱해진 것과 제곱이 2가 아닌 감마로 임의의 숫자로 설정 가능하다는 점이다.
이것이 의미하는 점은 쉽게 예측해서 맞을 수 있는 것의 비중을 줄이고, 예측하기 어려워 틀리는 것에 대한 비중을 상대적으로 늘려 적절한 밸런스를 유지하면서 학습하도록 한다. 여기서 알파는 0.25, 감마는 CE와 동일한 2를 사용한다.
더 나아가, 모든 앵커 박스에 대해서 loss를 계산하는 것이 아닌, 위에서 언급한 negative, positive 박스에만 loss 계산하고, ignorane 박스는 loss = 0으로 만들어 무시한다.
4.1.2. Box Regression Loss Function
box regression loss function은 예측한 박스 좌표가 생성한 앵커 박스 좌표와의 loss를 계산하는 식으로, smooth L1을 사용한다. 수식은 다음과 같다.
위의 수식을 해석하면 |x| < 1 부분에서는 곡선이고 이외 영역은 직선 형태이다. 이는 loss가 작을 경우에는 맞는 것으로 판단하여, 빠르게 loss가 줄어든다.
여기서는 객체가 있는 positive 앵커 박스만 찾으면 되므로, positive에 대해서만 loss를 계산하고 나머지 박스에는 0으로 만들어 제외한다.
4.2. Code Analysis
본 코드는 keras 것과 동일하다. (Figure 23)은 위에서 설명한 loss를 코드로 나타낸 것이다. 각 loss function은 클래스로 구현되어 있다는 것을 명심하길 바란다.
4.1.1. Classification Loss Function
(Figure 23)에서 검정 박스 내에 있는 것이 Classification loss function이다.
(본 내용과 별개로 지속적으로 텐서플로우 함수가 상당히 등장하는데, 이는 배열 데이터를 반복문 없이 빠른 연산이 가능하므로 텐서플로우 함수에 빨리 익숙해지길 바란다.)
4.1.1절에서 설명하지 않은 것에서 코드에 등장하는 것이 sigmoid인데, 딥러닝에서 이진 분류를 배웠다면 CE와 더불어 이진 분류에서의 활성 함수는 sigmoid가 사용된다는 것을 알 것이다. 사실 이 부분은 2장 모델에서 등장해야 하지만, focal loss의 연산 특수성 때문에 loss function 코드에서 등장한다. 또한 CE 연산을 위해 tf.nn.sigmoid_cross_entropy_with_logits이 등장함을 알아두자.
이후 최종 loss function 클래스에서 ignorance 박스는 무시된다는 것 또한 코드로 나타나있다.
4.1.2. Box Regression Loss Function
(Figure 23)에서 빨간 박스 내에 있는 것이 Box regression loss function이며, smooth L1 수식 그대로 표현되어있다. 아래 최종 loss function 클래스에서는 positive 박스만 loss에 고려된다는 것이 표현되어있다.
맨 아래 class RetinaNet에서는 위의 두 loss function 클래스를 아우르며, RetinaNet의 최종 output인 예측값에서 인덱싱한다. 그 중에서
cls_labels = tf.one_hot(
tf.cast(y_true[:, :, 4], dtype=tf.int32),
depth=self._num_classes,
dtype=tf.float32,
)
이와 같은 코드가 존재하는데, 3장 데이터셋 준비에서 Label Encoding을 설명했을 것이다. 정확히는 3.1.3에서 한 영상 데이터에 대해 (1, N, 4+Num_classes)가 아닌 (1, N, 4+1) 형태의 레이블이 생성된다고 언급하였다.
또한, RetinaNet 모델에서 나온 최종 예측값 형태(차원)은 (1, N, 4+Num_classes)인데, 데이터셋을 준비할 때는
(1, N, 4+1)라고 한 이유는 Loss function 코드에서 one-hot 인코딩 벡터화가 진행된다고 하였다. 이에 대한 설명이 위의 코드다. 마지막 인덱스 4를 인덱싱하여 one-hot 으로 변환하여, classification loss를 계산한다.
아래 코드는 위의 Loss function 코드가 어떻게 작동하는 지에 보여주기 위해 따로 만든 테스트 코드로,
필히 코드와 테스트 결과 비교하며 이해하길 바란다.
## Classification Test
a = tf.cast([[0.3, 0.4], [0.7, 0.4]], dtype = "float32")
y_true = tf.cast([0, 1.0], dtype = "float32")
print(f"y_true : {y_true}")
label = tf.one_hot(
tf.cast(y_true, dtype=tf.int32),
depth=2,
dtype=tf.float32,
)
print(f"label\n{label}\n")
alpha = tf.where(tf.equal(label, 1.0), 0.25, (1.0 - 0.25))
print(f"alpha\n{alpha}\n")
cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=label, logits=a)
print(f"cross_entropy\n{cross_entropy}\n")
probs = tf.nn.sigmoid(a)
print(f"probs(sigmoid(y_pred))\n{probs}\n")
pt = tf.where(tf.equal(label, 1.0), probs, 1 - probs)
print(f"pt\n{pt}\n")
loss = alpha * tf.pow(1.0 - pt, 2) * cross_entropy
loss = tf.reduce_sum(loss, axis=-1)
print(f"Focal loss\n{loss}\n")
positive_mask = tf.cast(tf.greater(y_true, -1.0), dtype=tf.float32)
negative_mask = tf.cast(tf.equal(y_true, -1.0), dtype=tf.float32)
ignore_mask = tf.cast(tf.equal(y_true, -2.0), dtype=tf.float32)
loss = tf.where(tf.equal(ignore_mask, 1.0), 0.0, loss) # ignore_mask 무시
print(f"P+N_loss\n{loss}\n")
#normalizer = tf.reduce_sum(positive_mask, axis=-1)
num_positive_anchor = tf.reduce_sum(positive_mask, axis=-1)
normalizer = tf.where(tf.equal(num_positive_anchor, 0), 1, num_positive_anchor)
print(f"Normalizer\n{normalizer}\n")
loss = tf.math.divide_no_nan(tf.reduce_sum(loss, axis=-1), normalizer)
print(f"fanal_loss\n{loss}")
#########################################################################
## Box test
print("\n\n#########################################################################\nBox\n")
pred, true = [[65, 66, 70, 50], [120, 125, 30, 50]], [[65, 80, 50, 64], [90, 150, 20, 40]]
pred, true = tf.cast(pred, dtype = "float32"), tf.cast(true, dtype = "float32")
difference = true - pred
print(f"difference\n{difference}")
absolute_difference = tf.abs(difference)
squared_difference = difference ** 2
loss = tf.where(
tf.less(absolute_difference, 15.0),
0.5 * squared_difference,
absolute_difference - 0.5,
)
print(f"\nloss\n{loss}")
loss = tf.reduce_sum(loss, axis=-1)
print(f"\nSmooth L1 loss\n{loss}")
loss = tf.where(tf.equal(positive_mask, 1.0), loss, 0.0)
print(f"\nP_loss\n{loss}")
loss = tf.math.divide_no_nan(tf.reduce_sum(loss, axis=-1), normalizer)
print(f"\nfanal_loss\n{loss}")
끝
Reference
[1] "Focal Loss for Dense Object Detection", "Tsung-Yi Lin and Priya Goyal and Ross Girshick and Kaiming He and Piotr Dollár", 2018, arXiv, cs.CV
[2] "Deep Residual Learning for Image Recognition", "Kaiming He and Xiangyu Zhang and Shaoqing Ren and Jian Sun", 2015, arXiv, cs.CV
[3] "Feature Pyramid Networks for Object Detection", "Tsung-Yi Lin and Piotr Dollár and Ross Girshick and Kaiming He and Bharath Hariharan and Serge Belongie", 2017, arXiv, cs.CV
[4] “Object Detection with RetinaNet”, “Srihari Humbarwadi”, 2020.05.17., keras, “https://keras.io/examples/vision/retinanet/”
[5] Resnet50, http://ethereon.github.io/netscope/#/gist/db945b393d40bfa26006
[6] “ResNet50 구현해보기”, “eremo2002”, 2019.01.23., https://eremo2002.tistory.com/76
[7] "tf.keras.applications.ResNet50", https://www.tensorflow.org/api_docs/python/tf/keras/applications/ResNet50
[8] "25편 CLAHE", "옥수별", 2015.11.18 , https://blog.naver.com/samsjang/220543360864
[9] "Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks", "Shaoqing Ren, Kaiming He, Ross Girshick, Jian Sun", 2016, arXiv, cs.CV
[10] "SSD: Single Shot MultiBox Detector", "Wei Liu, Dragomir Anguelov, Dumitru Erhan, Christian Szegedy, Scott Reed, Cheng-Yang Fu, Alexander C. Berg", 2015, arXiv, cs.CV
[11] "Bounding Box Encoding and Decoding in Object Detection", "Lei Mao", https://leimao.github.io/blog/Bounding-Box-Encoding-Decoding/
[12] "40.1 제너레이터와 yield 알아보기", 코딩 도장, https://dojang.io/mod/page/view.php?id=2412
[13] "tf.data.Dataset", Tensorflow, https://www.tensorflow.org/api_docs/python/tf/data/Dataset
[14] "대용량 훈련 데이터 처리 - Generator로 TF Dataset 만들기", "새옹지인", https://jins-sw.tistory.com/14
'1. 모델 분석 > Object Detection' 카테고리의 다른 글
[RetinaNet] 5. Training the Model and Result (0) | 2021.05.31 |
---|---|
[RetinaNet] 3. Prepare the Dataset (0) | 2021.05.31 |
[RetinaNet] 2. Explaining overall RetinaNet model and Code analysis (1) | 2021.05.31 |
[RetinaNet] 1. Anchor Box (2) | 2021.05.31 |