지난 글에서 이미지 분류 파이프라인을 완성했다. 분류는 이미지 전체에 하나의 레이블을 붙이는 것이었다. 객체 탐지(Object Detection)는 한 발 더 나아간다: 이미지에서 여러 물체의 위치와 클래스를 동시에 찾아야 한다.
한 발이라고 했지만 실제로는 문제의 성격이 바뀐다. 분류는 입력 하나에 출력 하나가 대응하는 함수를 배우는 일이라 손실 함수도 하나면 됐다. 탐지는 출력의 개수가 입력마다 다르다. 사람이 셋 있는 사진에는 박스가 셋이고 아무도 없는 사진에는 박스가 없다. 그래서 탐지 모델은 "몇 개를 낼 것인가"를 먼저 정하지 못한 채, 미리 정한 수천 개의 후보를 모두 예측한 뒤 대부분을 배경으로 버리는 방식을 택한다. 이 구조 하나가 탐지의 어려움 대부분을 만든다 — 후보를 어떻게 놓을지, 어느 후보에 어느 정답을 붙일지, 압도적으로 많은 배경 후보를 손실에서 어떻게 다룰지가 전부 여기서 나온다.
탐지 문제의 출력
박스·클래스·신뢰도
객체 탐지 모델의 출력은 [(x1,y1,x2,y2,class,confidence), ...] 형식의 예측 박스 목록이다.
(x1,y1),(x2,y2): 경계 박스의 좌상단·우하단 좌표class: 물체 클래스 (사람, 자동차, 고양이 등)confidence: 탐지 신뢰도 (0~1)
좌표를 어떤 꼴로 둘지도 선택이다. xyxy는 사람이 읽기 좋고 IoU 계산이 간단하다. 반면 학습 때는 중심과 크기를 쓰는 cxcywh가 흔하다 — 중심 좌표는 앵커 중심에서의 이동량으로, 크기는 앵커 크기에 대한 비율의 로그로 회귀시키면 값의 범위가 좁아져 학습이 안정된다. 최근의 앵커 프리 모델은 격자 점에서 네 변까지의 거리 네 개(ltrb)를 직접 회귀하기도 한다. 어느 꼴을 쓰든 평가 직전에는 xyxy로 되돌린다.
신뢰도는 "이 박스가 맞을 확률"처럼 읽히지만 실제로는 그렇게 보정된 값이 아니다. 대부분의 탐지기에서 신뢰도는 분류 점수이거나 분류 점수와 객체성 점수의 곱이고, 박스 좌표가 얼마나 정확한지는 거의 반영하지 않는다. 그래서 신뢰도 0.9짜리 박스가 IoU 0.4로 어긋나 있는 일이 흔하다. 뒤에서 볼 라벨 할당 전략들이 굳이 분류 점수와 박스 품질을 곱해 정렬하는 이유가 이것이다.
IoU
예측 박스와 실제 박스가 얼마나 겹치는지 측정하는 지표다. 교집합 넓이를 합집합 넓이로 나눈다.
def compute_iou(box1, box2):
"""
box: [x1, y1, x2, y2]
"""
# 교집합 좌표
inter_x1 = max(box1[0], box2[0])
inter_y1 = max(box1[1], box2[1])
inter_x2 = min(box1[2], box2[2])
inter_y2 = min(box1[3], box2[3])
inter_area = max(0, inter_x2 - inter_x1) * \
max(0, inter_y2 - inter_y1)
area1 = (box1[2]-box1[0]) * (box1[3]-box1[1])
area2 = (box2[2]-box2[0]) * (box2[3]-box2[1])
union_area = area1 + area2 - inter_area
return inter_area / (union_area + 1e-8)
IoU가 0.5 이상이면 탐지 성공으로 간주하는 것이 오래된 표준이다(PASCAL VOC). COCO 데이터셋은 IoU 0.5부터 0.95까지 0.05 간격으로 열 번 재서 평균을 낸다. 이 차이가 생각보다 크다. 0.5 기준은 "물체를 찾았는가"를 묻지만 0.75 기준부터는 "박스를 얼마나 정확히 그렸는가"를 묻는다. 같은 모델이 IoU 0.5에서 55, 0.75에서 38을 받는 일이 흔하고, 그 격차가 곧 회귀 손실을 손볼 여지다.
IoU에는 한 가지 성질이 있다. 두 박스가 아예 겹치지 않으면 값이 항상 0이라 "얼마나 멀리 빗나갔는지"를 구분하지 못한다. 그래서 IoU를 그대로 손실로 쓰면 초기 학습에서 기울기가 0인 구간이 생긴다. 뒤에서 볼 GIoU·CIoU는 정확히 이 구멍을 메우려고 나온 변형이다.
NMS
탐지기는 같은 물체에 대해 여러 박스를 예측한다. 격자 점 여럿이 같은 물체를 보고 있으니 당연한 일이다. NMS(Non-Maximum Suppression)는 신뢰도가 가장 높은 박스를 남기고 그와 많이 겹치는 나머지를 지운다.
def nms(boxes, scores, iou_threshold=0.5):
"""
boxes: (N, 4) - [x1,y1,x2,y2]
scores: (N,) - confidence score
"""
# 신뢰도 내림차순 정렬
order = scores.argsort(descending=True)
keep = []
while order.numel() > 0:
# 가장 높은 신뢰도 박스 선택
i = order[0].item()
keep.append(i)
if order.numel() == 1:
break
# 나머지 박스들과 IoU 계산
remaining = order[1:]
ious = compute_iou_batch(boxes[i], boxes[remaining])
# IoU < 임계값인 것만 유지
mask = ious < iou_threshold
order = remaining[mask]
return keep
NMS는 학습이 아니라 후처리라 임계값을 사람이 정해야 하고, 그 값 하나가 결과를 크게 흔든다. 임계값을 낮추면 중복은 깨끗이 지워지지만 서로 붙어 선 두 사람 중 하나가 함께 지워진다. 높이면 밀집 장면은 살아나지만 물체 하나에 박스가 셋씩 남는다. 밀집 장면이 많은 데이터에서는 완전히 지우는 대신 신뢰도만 깎는 Soft-NMS를 쓰기도 한다. NMS는 클래스마다 따로 도는 것이 기본이다 — 사람 박스와 가방 박스가 겹쳤다고 하나를 지우면 안 되기 때문이다.
앵커와 앵커 프리
앵커 스케일과 종횡비
앵커는 특징 맵의 격자 점마다 미리 놓아 두는 기준 박스다. 모델은 박스 좌표를 맨땅에서 만들지 않고 "이 앵커를 얼마나 옮기고 얼마나 늘릴 것인가"만 회귀한다. 회귀 대상의 범위가 좁아지므로 학습이 훨씬 안정된다.
문제는 앵커의 크기와 종횡비를 누가 정하느냐다. Faster R-CNN은 스케일 셋과 종횡비 셋을 곱해 격자 점마다 아홉 개를 두었는데, 이 값들은 PASCAL VOC의 물체 분포를 보고 손으로 고른 것이다. 데이터가 바뀌면 그대로 쓸 수 없다. 위성 사진의 차량은 대부분 작고 정사각형에 가깝고, 문서 스캔의 표는 가로로 극단적으로 길다. 이런 데이터에 기본 앵커를 그대로 쓰면 정답 박스와 IoU 0.5를 넘는 앵커가 아예 없는 물체가 생기고, 그 물체는 학습 내내 한 번도 양성 샘플을 만들지 못한다.
그래서 앵커는 데이터에서 뽑는다. 학습셋의 모든 정답 박스를 너비·높이 두 값으로 놓고 k-means를 돌리되, 거리로는 유클리드가 아니라 를 쓴다. 큰 박스의 픽셀 오차가 작은 박스보다 크게 잡히는 것을 막기 위해서다. YOLOv2가 이 방법을 처음 썼고 이후 자동 앵커 계산이 표준이 되었다. 뽑은 뒤에는 반드시 커버리지를 확인한다 — 정답 박스 중 어느 앵커와도 IoU 0.4를 넘지 못하는 비율이 1%를 넘으면 앵커 수나 스케일 범위를 다시 잡아야 한다.
중심점 회귀
앵커를 쓰면 하이퍼파라미터가 늘고, 앵커 수만큼 후보가 늘어 계산과 메모리가 커진다. 앵커 프리 계열은 그 전제를 버린다.
FCOS는 특징 맵의 각 점이 정답 박스 안에 들어가면 그 점을 양성으로 삼고, 그 점에서 박스의 네 변까지의 거리 를 직접 회귀한다. 앵커도 IoU 임계값도 없다. 대신 박스 중심에서 멀리 떨어진 점이 만드는 헐거운 박스가 문제인데, FCOS는 중심다움을 재는 별도의 출력을 두어 점수를 곱해 준다. CenterNet은 더 멀리 간다 — 물체의 중심 한 점만 히트맵의 극대점으로 예측하고 거기서 너비·높이를 회귀한다. 극대점 하나가 물체 하나이므로 NMS 자체가 필요 없고, 후처리가 3×3 최대 풀링 한 번으로 끝난다.
비용과 취약점
| 앵커 기반 | 앵커 프리 | |
|---|---|---|
| 하이퍼파라미터 | 스케일·종횡비·IoU 임계값 | 거의 없음 |
| 후보 수 | 격자 점 × 앵커 수 | 격자 점 수 |
| 극단 종횡비 | 앵커를 추가하면 대응 가능 | 회귀 범위가 넓어져 불리 |
| 밀집 장면 | NMS 임계값 조정 필요 | 중심점이 겹치면 하나를 놓침 |
앵커 프리가 무조건 낫지는 않다. 중심점이 같은 위치에 놓이는 두 물체는 CenterNet 계열이 구조적으로 하나만 낸다. 반대로 앵커 기반은 종횡비 1:8 같은 극단적 물체도 앵커를 하나 더 추가해 대응할 수 있다. 지금 널리 쓰는 YOLOv8·YOLOv10은 앵커 프리를 택했지만, 그 선택이 성능을 올린 것은 앵커를 없앴기 때문이라기보다 다음에 볼 라벨 할당을 함께 바꿨기 때문이다.
라벨 할당
고정 임계값의 한계
라벨 할당은 "수천 개의 후보 중 어느 것을 이 정답 박스의 양성으로 삼을 것인가"를 정하는 규칙이다. 오래된 방식은 단순하다 — 정답과 IoU 0.5를 넘는 앵커는 전부 양성, 0.4 미만은 음성, 사이는 무시. 이 규칙에는 두 가지 문제가 있다.
첫째, 물체 크기에 따라 양성 개수가 심하게 달라진다. 큰 물체는 IoU 0.5를 넘는 앵커가 수십 개 생기지만 작은 물체는 하나도 없을 수 있다. 앵커가 조금만 어긋나도 작은 박스의 IoU는 급격히 떨어지기 때문이다. 결과적으로 큰 물체는 학습 신호를 수십 배로 받고 작은 물체는 거의 못 받는다.
둘째, IoU가 높은 앵커가 반드시 분류하기 좋은 자리는 아니다. 물체의 가장자리에 걸친 앵커가 IoU는 높은데 그 위치의 특징 벡터에는 배경이 절반이라 분류가 어려운 경우가 있다. 임계값 하나로는 이 구분을 할 수 없다.
ATSS
ATSS(Adaptive Training Sample Selection)는 임계값을 물체마다 데이터에서 계산한다. 절차는 이렇다. 정답 박스마다 특징 피라미드의 각 레벨에서 중심이 가장 가까운 후보 k개씩을 모으고, 그 후보들과 정답의 IoU를 구해 평균 과 표준편차 를 낸다. 그리고 임계값을 로 잡아 그보다 높은 후보만 양성으로 삼는다.
이 한 줄이 앞의 두 문제를 동시에 푼다. 작은 물체는 후보들의 IoU가 전반적으로 낮으므로 평균도 낮아져 임계값이 함께 내려가고, 결국 물체 크기와 무관하게 비슷한 수의 양성이 뽑힌다. 표준편차를 더하는 것은 "이 물체에 유난히 잘 맞는 후보들"만 고르는 장치다 — 분산이 크다는 것은 잘 맞는 후보와 아닌 후보가 뚜렷이 갈린다는 뜻이므로 기준을 높이고, 분산이 작으면 다들 비슷하니 기준을 낮춘다. ATSS 논문이 보인 결과 중 인상적인 것은, 이 할당 규칙 하나만 통일하면 앵커 기반과 앵커 프리의 성능 차이가 거의 사라진다는 점이었다.
TaskAlignedAssigner
YOLOv8이 쓰는 TAL(Task Alignment Learning)은 한 걸음 더 나아가 분류와 회귀를 함께 본다. 후보마다 정렬 점수를
로 계산한다. 는 정답 클래스의 분류 점수, 는 예측 박스와 정답의 IoU다. 점수가 높으면서 박스도 잘 맞는 후보만 위로 올라오고, 그중 상위 k개를 양성으로 삼는다.
이 설계의 목적은 앞서 말한 "신뢰도와 박스 품질의 어긋남"을 학습 단계에서 미리 없애는 것이다. 추론 때 NMS는 신뢰도로 정렬하므로, 신뢰도가 높은 박스가 실제로 잘 맞아야 NMS가 옳은 것을 남긴다. TAL은 그 조건을 만족하는 후보에만 높은 목표값을 주어 두 점수를 붙여 놓는다. 할당이 예측에 의존하므로 학습 초기에는 점수가 무의미해 불안정한데, 대개 앞 몇 에폭은 IoU 기반 할당으로 워밍업한 뒤 전환한다.
탐지 손실의 구성
Focal Loss
탐지의 후보 수만 개 중 양성은 보통 수십 개다. 나머지는 전부 배경이고, 그중 대부분은 하늘이나 아스팔트처럼 배경인 것이 너무 명백해서 이미 잘 맞히고 있는 것들이다. 이 쉬운 음성들의 손실 값은 개별로는 작지만 개수가 압도적이라 합치면 양성의 손실을 덮어 버린다.
Focal Loss는 잘 맞히고 있는 샘플의 손실을 깎는다. 정답 클래스의 예측 확률을 라 할 때
가 0.99인 쉬운 음성은 가 에서 0.0001이 되어 손실이 만분의 일로 줄고, 가 0.3인 어려운 샘플은 거의 그대로 남는다. 결과적으로 기울기의 대부분이 애매한 샘플로 간다. 이전에는 배경 후보를 무작위로 골라 양성 대 음성을 1:3으로 맞추는 샘플링을 썼는데, Focal Loss는 그 샘플링 없이 모든 후보를 쓰면서도 균형을 맞춘다는 점이 달랐다.
를 키우면 쉬운 샘플을 더 강하게 누르지만 너무 키우면 학습 신호 자체가 얇아진다. 원 논문의 , 조합이 대부분의 데이터에서 그대로 잘 듣고, 클래스 불균형이 극단적일 때만 를 만져 본다.
박스 회귀 손실
박스 쪽 손실은 세 세대를 거쳤다. 처음에는 네 좌표에 각각 Smooth L1을 걸었다. 계산이 간단하지만 문제가 둘 있다 — 네 좌표를 독립으로 다루므로 "박스 하나가 얼마나 잘 맞는가"를 직접 최적화하지 않고, 큰 박스의 좌표 오차가 작은 박스보다 크게 잡혀 크기에 따라 가중치가 달라진다.
그래서 평가 지표인 IoU를 그대로 손실로 쓰자는 발상이 나왔다. 다만 앞서 말했듯 겹치지 않는 두 박스는 IoU가 0이라 기울기가 없다. GIoU는 두 박스를 모두 감싸는 최소 박스 를 도입해
로 고친다. 겹치지 않아도 두 박스가 멀수록 의 빈 공간이 커지므로 "가까이 오라"는 기울기가 생긴다. CIoU는 여기에 중심 거리 항과 종횡비 일치 항을 더해 수렴을 더 빠르게 만든다. 실무에서 고를 때는 대개 CIoU 또는 그 변형을 기본으로 두되, 극단 종횡비가 많은 데이터에서는 종횡비 항이 오히려 불안정할 수 있으니 GIoU와 나란히 재 본다.
최근 모델은 좌표를 하나의 값으로 회귀하는 대신 이산 분포로 예측하기도 한다(Distribution Focal Loss). 경계가 흐릿한 물체에서 "여기쯤"이라는 불확실성을 분포의 폭으로 표현할 수 있고, 기댓값을 취하면 실수 좌표가 나온다.
탐지기 계열
Faster R-CNN
Faster R-CNN은 두 단계로 탐지한다. RPN(Region Proposal Network)이 물체가 있을 법한 영역을 2,000개쯤 제안하고, 그 각 영역을 RoI Align으로 고정 크기 특징으로 잘라 분류와 박스 정밀화를 한 번 더 한다. 두 번 거치므로 느리지만, 두 번째 단계가 후보를 이미 좁혀 놓은 상태에서 판단하므로 정확도가 높다.
import torchvision
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
# 사전학습 Faster R-CNN 로드
model = fasterrcnn_resnet50_fpn(pretrained=True)
# 클래스 수 교체 (COCO 91개 → 커스텀)
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(
in_features, num_classes=5 # 배경 포함
)
model = model.cuda()
# 학습 루프
model.train()
for images, targets in data_loader:
images = [img.cuda() for img in images]
targets = [{k: v.cuda() for k, v in t.items()} for t in targets]
# torchvision detection 모델은 targets를 직접 받음
loss_dict = model(images, targets)
losses = sum(loss_dict.values())
optimizer.zero_grad()
losses.backward()
optimizer.step()
# 추론
model.eval()
with torch.no_grad():
predictions = model(images) # [{'boxes','labels','scores'}, ...]
YOLOv8
YOLO는 이미지를 단 한 번의 순전파로 모든 박스를 예측한다. 제안 단계가 없으므로 빠르고, 앞서 본 Focal Loss와 라벨 할당 개선이 쌓이면서 정확도 격차도 크게 줄었다.
# pip install ultralytics
from ultralytics import YOLO
# 사전학습 모델 로드
model = YOLO('yolov8n.pt') # nano (가장 빠름)
# 추론
results = model('image.jpg')
for result in results:
boxes = result.boxes.xyxy # (N, 4) 박스 좌표
confs = result.boxes.conf # (N,) 신뢰도
cls = result.boxes.cls # (N,) 클래스 인덱스
print(f"탐지된 물체: {len(boxes)}개")
result.save('output.jpg') # 결과 저장
# 커스텀 데이터 학습
model = YOLO('yolov8n.pt')
results = model.train(
data='dataset.yaml', # 데이터셋 설명 파일
epochs=100,
imgsz=640,
batch=16,
device='cuda'
)
데이터셋 YAML은 경로와 클래스 이름만 적는다.
path: /data/custom
train: images/train
val: images/val
nc: 3
names: ['cat', 'dog', 'bird']
다중 스케일과 FPN
작은 물체와 큰 물체를 동시에 잘 탐지하려면 FPN(Feature Pyramid Network)이 필요하다. 깊은 층의 특징 맵은 의미 정보가 풍부하지만 해상도가 낮아 작은 물체의 위치를 잃고, 얕은 층은 해상도가 높지만 의미 정보가 얕다. FPN은 깊은 층을 위로 업샘플링해 얕은 층과 더하는 방식으로 두 성질을 한 피라미드에 담는다.
# FPN 개념 (torchvision에 내장)
from torchvision.ops import FeaturePyramidNetwork
# ResNet의 여러 스테이지 출력을 피라미드로 합침
fpn = FeaturePyramidNetwork(
in_channels_list=[256, 512, 1024, 2048],
out_channels=256
)
# P2 (stride=4): 작은 물체
# P3 (stride=8): 중간 물체
# P4 (stride=16): 큰 물체
# P5 (stride=32): 매우 큰 물체
어느 레벨이 어느 물체를 맡을지는 물체 크기로 나누는 것이 기본이지만, 앞서 본 ATSS·TAL 같은 할당 규칙은 레벨 선택까지 데이터에 맡긴다.
DETR과 집합 예측
DETR은 지금까지의 구성 요소를 거의 다 지운다. 앵커도 없고 NMS도 없다. 학습 가능한 쿼리 벡터 100개를 두고 트랜스포머 디코더가 그 각각을 하나의 박스로 바꾼다. 핵심은 손실 쪽이다 — 예측 100개와 정답 몇 개를 헝가리안 알고리즘으로 일대일 매칭한 뒤, 매칭된 쌍에만 분류·회귀 손실을 걸고 나머지는 "물체 없음"으로 학습시킨다.
일대일 매칭이라 한 물체에 예측이 하나만 붙고, 그래서 NMS가 필요 없다. 대신 대가가 있다. 매칭이 학습 초기에 매번 바뀌어 어느 쿼리가 어느 영역을 맡을지 정해지지 않으므로 수렴이 극단적으로 느리다 — 원 논문은 500에폭을 돌렸고, 같은 정확도를 Faster R-CNN은 12에폭에 얻는다. Deformable DETR은 어텐션이 전체 픽셀 대신 학습된 소수의 참조점만 보게 바꿔 이 문제를 크게 줄였고(50에폭 수준), 이후 DINO 계열이 노이즈를 섞은 정답 쿼리를 함께 넣어 매칭을 안정시키면서 COCO 최상위권을 차지했다.
| 상황 | 추천 |
|---|---|
| 정확도 최우선 (의료, 위성) | Faster R-CNN, DINO |
| 실시간 처리 (CCTV, 자율주행) | YOLOv8/9/10 |
| 엣지 기기 (모바일, Jetson) | YOLO-NAS, PP-YOLO |
| 빠른 프로토타입 | YOLOv8 (Ultralytics) |
mAP
PR 곡선과 AP
mAP를 "정확도 비슷한 것"으로 뭉뚱그려 읽으면 결과를 개선할 방향이 안 보인다. 계산 절차를 한 번 따라가면 이 숫자가 무엇에 민감한지가 드러난다.
먼저 클래스 하나를 골라, 그 클래스의 모든 예측 박스를 신뢰도 내림차순으로 세운다. 위에서부터 하나씩 내려가며 각 박스가 아직 매칭되지 않은 정답과 IoU 임계값을 넘으면 TP, 아니면 FP로 찍는다. 이미 다른 박스가 가져간 정답과 겹친 것도 FP다 — 중복 탐지를 벌하는 자리다. 그렇게 내려가면서 매 지점의 정밀도와 재현율을 기록하면 PR 곡선이 나온다. AP는 이 곡선 아래 넓이이고, COCO는 재현율 축을 101등분해 각 지점의 최대 정밀도를 평균한다.
이 절차에서 읽어야 할 것은 두 가지다. 첫째, 신뢰도의 순서만 중요하고 절대값은 상관없다 — 모든 신뢰도를 0.1씩 낮춰도 AP는 그대로다. 그래서 "임계값을 조정해 mAP를 올린다"는 말은 성립하지 않는다. 둘째, 신뢰도가 낮은 예측을 잘라 내면 AP는 대체로 떨어진다. 곡선의 오른쪽 끝, 즉 높은 재현율 구간이 사라지기 때문이다. 평가 때는 임계값을 0.001처럼 아주 낮게 두고, 실제 서비스에 쓸 임계값은 그와 별개로 정밀도·재현율 요구에 맞춰 따로 고른다.
COCO의 AP 변형
# torchmetrics로 mAP 계산
from torchmetrics.detection import MeanAveragePrecision
metric = MeanAveragePrecision(iou_type='bbox')
preds = [{
'boxes': torch.tensor([[100,100,200,200]], dtype=torch.float),
'scores': torch.tensor([0.9]),
'labels': torch.tensor([0])
}]
targets = [{
'boxes': torch.tensor([[110,110,210,210]], dtype=torch.float),
'labels': torch.tensor([0])
}]
metric.update(preds, targets)
result = metric.compute()
print(f"mAP@50: {result['map_50']:.3f}")
print(f"mAP@50:95: {result['map']:.3f}")
COCO가 함께 내주는 여러 숫자는 각각 다른 질문에 답한다. AP50과 AP75의 격차가 크면 물체는 잘 찾는데 박스가 헐겁다는 뜻이라 회귀 손실과 라벨 할당을 본다. APs가 APm·APl보다 크게 낮으면 작은 물체 문제이므로 입력 해상도와 피라미드 얕은 레벨을 본다. AR(평균 재현율)이 AP보다 훨씬 높으면 찾기는 다 찾았는데 순위가 엉켜 있다는 뜻이고, 이때는 분류 점수와 박스 품질의 정렬 문제일 가능성이 크다.
탐지의 약점
작은 물체와 밀집·가림
작은 물체는 거의 모든 탐지기의 약점이다. 640×640으로 리사이즈한 이미지에서 20픽셀짜리 물체는 stride 8 특징 맵에서 2.5칸에 불과하고, 그보다 깊은 레벨에서는 한 칸도 안 된다. 대응은 세 가지다 — 입력 해상도를 올리거나, 원본을 타일로 잘라 각각 추론한 뒤 결과를 합치거나(위성·드론 영상에서 표준적인 방법이다), 피라미드에 더 얕은 레벨을 추가한다. 셋 다 비용을 낸다는 점이 공통이다.
밀집과 가림은 NMS가 걸리는 자리다. 사람이 빽빽한 장면에서 IoU 0.5 임계값은 겹쳐 선 두 사람 중 하나를 지운다. 임계값을 0.65로 올리면 살아나지만 중복이 늘고, Soft-NMS로 완전 삭제 대신 점수 감쇠를 쓰면 절충이 된다. 근본적으로는 DETR 계열처럼 일대일 매칭으로 학습해 NMS를 없애는 쪽이 이 문제에 강하다.
극단 종횡비와 도메인 시프트
종횡비 1:10짜리 물체 — 전선, 문서의 표 줄, 긴 간판 — 는 앵커가 없으면 회귀 범위를 크게 벗어나고, 있어도 IoU가 종횡비에 민감해 할당이 흔들린다. 회전 박스를 쓰는 것이 정답인 경우도 많다.
도메인 시프트는 평가에서 잘 안 보인다. 낮에 찍은 데이터로 학습해 밤에 배포하거나, 카메라 한 대의 영상으로 학습해 다른 렌즈에 쓰면 mAP가 절반으로 떨어지는 일이 흔하다. 검증셋을 학습셋과 같은 분포에서 무작위로 쪼개면 이 하락이 절대 드러나지 않는다. 검증셋은 촬영 조건·장소·시간대 단위로 나누는 것이 안전하다.
데이터 점검 순서
모델을 바꾸기 전에 데이터를 먼저 보는 편이 거의 항상 이득이다. 순서는 이렇다.
- 박스 라벨 규약을 문장으로 못 박는다. 가려진 물체를 어디까지 그릴 것인가(보이는 부분만인가, 추정한 전체인가), 잘린 물체는 몇 퍼센트부터 라벨할 것인가, 군중은 개별로 그릴 것인가 묶어서 그릴 것인가. 이 셋만 정해도 라벨러 간 편차가 크게 준다.
- 라벨 품질을 점검한다. 같은 이미지를 두 사람에게 주고 IoU를 재 보면 사람 사이의 일치도가 나온다. 그 값이 0.8이면 모델의 AP75 상한이 대략 거기다.
- 크기 분포를 그려 본다. 정답 박스 넓이의 히스토그램을 보고 입력 해상도와 앵커·피라미드 레벨을 맞춘다. 여기서 이미 작은 물체가 대부분이라는 것이 보이면 해상도부터 올린다.
- 클래스 빈도를 센다. 꼬리 클래스가 전체의 1% 미만이면 그 클래스의 AP는 통계적으로 흔들린다. 클래스를 합치거나, 그 클래스만 오버샘플링하거나, 아예 평가에서 분리해 따로 본다.
객체 탐지는 이미지 분류와 달리 위치 정보를 다루므로 훨씬 복잡하다. 이 복잡성은 다음 단계, 픽셀 단위로 분류하는 세그멘테이션에서 한 번 더 깊어진다 — 박스가 아니라 마스크를 다루면 라벨 할당과 손실 설계가 통째로 다시 짜인다.
읽어주셔서 감사합니다. 😊

