Focal loss: 크로스 엔트로피 (Cross-entropy)의 한 요소인 (1 p_t)^γ — 쉬운 예제에서는 손실을 깎아내고 어려운
요약
객체 탐지 모델 학습 시 발생하는 클래스 불균형 문제를 해결하기 위한 Focal Loss의 원리와 구현 방법을 설명합니다. 크로스 엔트로피의 한계를 지수 인자를 통해 보완하여 쉬운 예제의 손실 비중을 낮추는 메커니즘을 다룹니다.
핵심 포인트
- 쉬운 배경 예제가 전체 그래디언트를 지배하는 문제 해결
- (1-p_t)^γ 인자를 통해 난이도에 따른 손실 가중치 조절
- γ가 0일 경우 일반적인 크로스 엔트로피로 회복되는 일반화된 구조
- 클래스 빈도를 조절하는 α-balancing과 난이도를 조절하는 γ의 차이
RetinaNet과 같은 1단계 밀집 탐지기 (one-stage dense detector)를 학습시키다 보면 잔혹한 불균형에 직면하게 됩니다. 모든 이미지는 분류기 (classifier)에 약 10⁴–10⁵개의 후보 박스 (candidate boxes)를 쏟아붓는데, 이들 중 거의 대부분은 모델이 이미 정답을 맞히고 있는 쉬운 배경 (easy background)입니다. 일반적인 크로스 엔트로피 (cross-entropy) 하에서는 이러한 쉬운 부정 예제 (easy negatives) 각각이 여전히 작은 손실 (loss)을 기여하며, 그 수가 너무 많기 때문에 이들의 합이 소수의 어렵고 정보가 풍부한 전경 예제 (foreground examples)를 압도해 버립니다. 결국 그래디언트 (gradient)는 모델에게 아무것도 가르쳐주지 않는 예제들에 의해 지배됩니다. Focal loss는 하나의 곱셈 인자 (multiplicative factor)를 통해 정확히 이 문제를 해결하며, 저는 이 모든 과정을 실시간으로 계산하는 데모를 제작했습니다. 그 아이디어는 다음과 같습니다.
크로스 엔트로피 (Cross-entropy)는 결코 0이 되지 않으며, 그것이 문제입니다
모든 것은 모델이 '정답' 클래스에 할당한 확률인 p_t로 표현됩니다. 큰 p_t → 1은 쉬운 예제 (맞았고 확신함)를 의미하며, 작은 p_t → 0은 어려운 예제 (모델이 틀림)를 의미합니다. 크로스 엔트로피 (cross-entropy)는 단순히 −log(p_t)이며, p_t = 0.9로 잘 분류된 예제조차 여전히 −log(0.9) ≈ 0.105의 손실을 기여합니다.
ce(0.9) # 0.105 <- 매우 작지만, 이런 것이 약 10만 개 있음
ce(0.25) # 1.386 <- 실제로 중요한 예제
# 1000 * 0.105 = 105 >> 20 * 1.386 = 27.7
...
조절 인자 (modulating factor)는 '난이도'에 따라 가중치를 낮춥니다
크로스 엔트로피 (CE)에 (1−p_t)^γ를 곱합니다. 예제가 어려울 때 (p_t → 0) 인자는 → 1이 되어 손실에 영향을 주지 않습니다. 예제가 쉬울 때 (p_t → 1) 인자는 → 0이 되어 손실이 짓눌리게 됩니다. 지수 γ ("focusing")는 그 강도를 결정합니다.
def focal(pt, gamma):
return -(1.0 - pt) ** gamma * math.log(max(pt, 1e-12))
...
γ=0이면 정확히 일반적인 CE로 회복된다는 점에 주목하세요. 즉, Focal loss는 별개의 손실 함수가 아니라 엄격한 일반화 (generalisation)입니다. RetinaNet은 γ=2를 사용하는데, 이는 p_t=0.1인 예제는 거의 건드리지 않으면서 p_t=0.9인 예제의 가중치를 100배 낮춥니다.
α-balancing은 직교합니다 — 그것은 '클래스'에 가중치를 둡니다
집중(focusing) 기능 외에도, RetinaNet은 클래스별 가중치인 α_t를 유지합니다 (예: 희귀한 전경(foreground)의 경우 0.25). 이는 예제의 '어려움(difficulty)'이 아닌 클래스의 '빈도(frequency)'를 다루는 평면적인 스케일(flat scale)입니다. 즉, α는 전체 합계를 위아래로 조절할 뿐, 쉬운/어려운 예제의 구분(split)을 결코 바꾸지 않습니다. 오직 γ만이 그 역할을 수행합니다. 이 둘은 결합됩니다: FL = −α_t (1−p_t)^γ log(p_t).
그래디언트 반전(gradient flip) 관찰하기
이를 확인하는 가장 명확한 방법은 전체 집단을 집계하고, 각 그룹이 전체 손실(loss, 그래디언트(gradient)를 추적함)에서 차지하는 비중을 비교하는 것입니다:
def share(gamma):
easy = {'n':1000, 'pt':0.90} # 모델이 이미 맞춤
hard = {'n': 20, 'pt':0.25} # 모델이 틀림
...
실제로 배포하게 될 코드
실제 코드에서는 p_t를 먼저 형성한 다음 로그(log)를 취하지 않습니다. 대신 안정적인 커널(stable kernel)을 사용하여 로짓(logits)으로부터 시그모이드(sigmoid)/BCE를 계산하고 (1−p_t)^γ를 곱합니다. Torchvision은 이를 sigmoid_focal_loss로 제공하며, RetinaNet은 γ=2, α=0.25를 편향 초기화(bias-init) 트릭과 결합하여 사용합니다. 즉, 분류기(classifier)의 출력 편향(bias)을 π≈0.01일 때 −log((1−π)/π)로 시작하여, 첫 번째 에포크(epoch)에서 거대한 음성 다수 클래스로 인해 학습이 폭발하는 것을 방지합니다.
def sigmoid_focal_loss(logits, targets, alpha=0.25, gamma=2.0):
p = torch.sigmoid(logits)
ce = F.binary_cross_entropy_with_logits(logits, targets, reduction="none")
...
이는 레이블 스무딩(label smoothing, 과잉 확신을 억제하기 위해 타겟(targets)을 부드럽게 만드는 기법)과는 구별됩니다. Focal loss는 타겟(targets)은 그대로 유지하면서, '예제별 손실(per-example loss)을 어려움에 따라 재가중(reweight)'합니다. 단 한 줄의 차이지만, 이것이 단일 단계(single-stage) 탐지기가 단일 단계의 속도로 2단계(two-stage)의 정확도에 도달할 수 있게 만든 핵심입니다.
γ와 프로브(probe)를 조절하며 손실 곡선(loss curves)이 갈라지고 쉬운/어려운 막대 그래프가 반전되는 것을 확인해 보세요:
https://dev48v.infy.uk/dl/day47-focal-loss.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기