26.3 NMS, confidence és AP

26.3.1 1. Cél, előismeret és induló kérdés

A 26.2. Egy- és kétlépcsős detektor egységben a detektor nyers kimenetéről beszéltünk: régió- vagy anchor-alapú predikciók, osztálylogitok és dobozkoordináták együttese. Most azt vizsgáljuk, mi történik a modell kimenete és a publikálható detekciós lista között. Három fogalmat választunk szét: a modell confidence score-ját, a Non-Maximum Suppression (NMS) elnevezésű posztprocesszt és a benchmark-összegző Average Precision (AP) metrikát. Az első a modellé, a második a predikciók ritkításának algoritmusa, a harmadik a teljes tesztkészletre vonatkozó értékelési konvenció. A kettő nem mosható össze: egy jól kalibrált score nem teszi feleslegessé az NMS-t, és egyetlen score-küszöbön mért precision nem azonos az AP-vel.

Előismeret: az egy- és kétlépcsős detektorok felelősségi körei (26.2. Egy- és kétlépcsős detektor), a doboz mint félig nyílt xyxy intervallum és az IoU alapgondolata (26.1. Dobozkoordináta, IoU és lokalizációs loss), valamint a metrika és a kísérleti napló elválasztása (25.3. Kis adat és feature-alapú baseline). Visszautalunk a címkezaj problémájára is (22.3. Zajos és hiányos címke), mert a ground truth (GT) doboz pontatlansága közvetlenül befolyásolja az AP-t. A 14.3. Paraméterszám, MAC és aktivációmemória költségmodellje pedig releváns marad: az NMS páronkénti IoU-számítása a modell kiértékelésén túli költség.

Induló kérdés: ha a modell ugyanarra az objektumra két majdnem egyforma, egyaránt magas score-ú dobozt ad, miért nem lesz ettől kétszer akkora a mért teljesítménye?

26.3.2 2. Definíciók: doboz, pontszám, IoU, NMS, score threshold

Doboz (bounding box). Egy detekciós doboz négy koordinátából áll:

b=(xmin,ymin,xmax,ymax)∈[0,W]×[0,H]×[0,W]×[0,H],b=(x_{\min},y_{\min},x_{\max},y_{\max})\in[0,W]\times[0,H]\times[0,W]\times[0,H],

ahol W,HW,H a kép szélessége és magassága ugyanabban a koordinátarendszerben, és xmin<xmaxx_{\min}<x_{\max}, ymin<ymaxy_{\min}<y_{\max}. A koordináták lehetnek pixelértékek vagy [0,1][0,1]-re normalizált értékek, de egy kiértékelési egységen belül egyetlen konvenciót kell használni. A GlyphLab szerződéséhez hasonlóan a dobozt félig nyílt intervallumként értelmezzük: az xmin≤x<xmaxx_{\min}\le x<x_{\max}, ymin≤y<ymaxy_{\min}\le y<y_{\max} tartomány alkotja a dobozt. Területe:

A(b)=(xmax−xmin)(ymax−ymin).A(b)=(x_{\max}-x_{\min})(y_{\max}-y_{\min}).

A detekciós kimenetet egy kötegben P∈ℝB×4P\in\mathbb R^{B\times4} mátrixként és s∈ℝBs\in\mathbb R^B score-vektorként ábrázoljuk; BB a nyers detekciók száma. A score tartománya gyakran [0,1][0,1], de az NMS és az AP definíciójához elegendő, hogy a score-ok rendezhető valós számok legyenek.

Intersection over Union. Két doboz, aa és bb IoU-ja:

IoU(a,b)=A(a∩b)A(a∪b)=A(a∩b)A(a)+A(b)−A(a∩b),\operatorname{IoU}(a,b)=\frac{A(a\cap b)}{A(a\cup b)}=\frac{A(a\cap b)}{A(a)+A(b)-A(a\cap b)},

ahol a metszet szélessége max(0,min(xmax,x′max)−max(xmin,x′min))\max(0,\min(x_{\max},x'_{\max})-\max(x_{\min},x'_{\min})), magassága analóg módon számolódik. Ha A(a)+A(b)−A(a∩b)>0A(a)+A(b)-A(a\cap b)>0, a képlet direkt. Az üres-uniós szélsőesethez külön szabály kell: ha A(a)=A(b)=0A(a)=A(b)=0 és a=ba=b, akkor IoU(a,b)=1\operatorname{IoU}(a,b)=1; ha a két elfajuló doboz különbözik, IoU(a,b)=0\operatorname{IoU}(a,b)=0; ha pontosan az egyik területe nulla, a metszet területe nulla, tehát az IoU is nulla. Gyakorlati detektor-kimenetben a pozitív területű doboz a jellemző eset.

Score threshold. A score threshold τs≥0\tau_s\ge0 egy üzemeltetési szűrő: csak azokat a detekciókat tartjuk meg, amelyekre si≥τss_i\ge\tau_s. Ez a szűrő a modell kimenetének felhasználói értelmezését szolgálja, nem a benchmark-metrika része. Az AP-számítás ezzel szemben a rendezett detekciós listát feldolgozza anélkül, hogy egyetlen τs\tau_s-nél elvágná azt; a küszöb szerepét a rendezett sorrend összes lehetséges prefixe veszi át. Ezért az AP a score-rangsor összegzője, míg a score threshold egy adott működési pontot választ.

NMS (Non-Maximum Suppression). Legyen adott P∈ℝB×4P\in\mathbb R^{B\times4}, s∈ℝBs\in\mathbb R^B és τ∈[0,1]\tau\in[0,1] IoU-küszöb. Az NMS a következő determinisztikus algoritmus:

  1. Rendezzük a dobozokat csökkenő score szerint. Azonos score esetén a nagyobb területű doboz kerül előrébb; ha a terület is egyenlő, a kisebb index a korábbi. Így a rendezés teljes.
  2. A rendezett lista elejétől haladva minden még el nem nyomott dobozt kiválasztunk, és minden további, még el nem nyomott dobozt törlünk, amelyre a kiválasztottal számított IoU legalább τ\tau.
  3. A kiválasztott indexek halmaza az NMS kimenete, 𝒦⊆{1,…,B}\mathcal K\subseteq\{1,\dots,B\}.

A B=0B=0 bemenetre a kimenet üres. Az NMS nem változtat a dobozokon és a score-okon; csak eldönti, mely predikciókat közöljük. A τ\tau választása kompromisszum: a magas IoU-küszöb több, egymástól alig eltérő duplikátumot hagyhat meg, az alacsony küszöb pedig valódi, egymáshoz közel álló objektumokat nyomhat el. Az NMS a duplicate detection kezelésének egyik eszköze, de nem az egyetlen forrása a duplikátumoknak: ahogy a matching-definícióban látni fogjuk, az NMS után is lehet két detekció ugyanarra a GT-re.

Illesztés, TP/FP/FN. Legyen MM ground truth doboz egy adott képen, G∈ℝM×4G\in\mathbb R^{M\times4}, és legyen τIoU\tau_{\text{IoU}} a kiértékelési IoU-küszöb. A detekciókat csökkenő score szerint dolgozzuk fel. A ii-edik detekció akkor true positive (TP), ha létezik még hozzá nem rendelt GT-doboz, amellyel az IoU legalább τIoU\tau_{\text{IoU}}; ekkor a legnagyobb IoU-jú ilyen GT-t választjuk, és foglaltnak jelöljük. Ha egy GT-hez már korábban egy detekciót rendeltünk, a későbbi illeszkedő detekció false positive (FP). Ha nincs elérhető, kellően átfedő GT, a detekció FP. Egy GT, amelyhez egyetlen detekciót sem rendeltünk, false negative (FN). Ez a mohó párosítás a standard kiértékelési alap; a tie eseteket itt a legkisebb GT-index választásával oldjuk fel.

Precision, recall, AP. A feldolgozás kk-adik lépése után TPk,FPk\mathrm{TP}_k,\mathrm{FP}_k az addigi osztályozás, és

pk=TPkTPk+FPk,rk=TPkM.p_k=\frac{\mathrm{TP}_k}{\mathrm{TP}_k+\mathrm{FP}_k},\qquad r_k=\frac{\mathrm{TP}_k}{M}.

A M=0M=0 esethez külön konvenció kell; a jelen egységben feltesszük, hogy a kiértékelt képeken M>0M>0. A PR-görbe a (rk,pk)(r_k,p_k) pontokból áll. Az általunk használt all-point interpolált AP a következő:

APall=∑k=1D(rk−rk−1)p̂(rk),r0=0,p̂(rk)=maxj≥kpj,\operatorname{AP}_{\text{all}}=\sum_{k=1}^{D}(r_k-r_{k-1})\,\widehat p(r_k),\qquad r_0=0,\qquad \widehat p(r_k)=\max_{j\ge k}p_j,

ahol DD a feldolgozott detekciók száma, és az azonos recall-értékű pontokat az eredeti sorrendben tartjuk meg; ez utóbbi a jobb oldali maximális interpolációt alkalmazza. A szokásos 11 pontos VOC-konvenció ettől különbözik: ott a recall-tengely 0,0.1,…,10,0.1,\dots,1 pontjaiban vett interpolált precisioneket átlagoljuk. A publikációban mindig jelölni kell, melyik konvenciót használtuk; a numerikus értékek konvenciófüggők. A duplicate detection büntetése itt lép be: a második, ugyanarra a GT-re illeszkedő detekció FP-ként csökkenti a precisiont anélkül, hogy a recallt növelné.

26.3.3 3. Kézi levezetési kötelezettség: NMS és AP

26.3.3.1 NMS kézi példa

Legyen egy 40×4040\times40-es kép, öt nyers detekció a következő xyxy dobozokkal és score-okkal:

Területek: A(A)=25A(A)=25, A(B)=100A(B)=100, A(C)=100A(C)=100, A(D)=100A(D)=100, A(E)=64A(E)=64. A rendezés a score, majd a terület, majd az index szabálya szerint: BB (0.9, terület 100), EE (0.9, terület 64), DD (0.85), AA (0.6), CC (0.5). Legyen τ=0.5\tau=0.5.

Az NMS kimenete {B,D,A,C}\{B,D,A,C\}, azaz az eredeti indexekkel {1,3,0,2}\{1,3,0,2\}. Figyeljük meg: az azonos score-ú EE kiesett, mert a nagyobb területű BB előbb dolgozódott fel; ez a tie-break szabály következménye. Az NMS tehát nem egyszerűen „a legmagasabb score-ú doboz mindig nyer”, hanem a térbeli elnyomás sorrendjétől függ.

26.3.3.2 AP kézi példa

Legyen egy (H,W)=(130,160)(H,W)=(130,160) méretű képen három GT-doboz:

g1=(10,10,50,50),g2=(100,20,150,60),g3=(30,80,80,120).g_1=(10,10,50,50),\quad g_2=(100,20,150,60),\quad g_3=(30,80,80,120).

Hat detekció, már score szerint rendezve (tegyük fel, hogy NMS után is így maradtak, például 0,90{,}9-es NMS-IoU-küszöb mellett; a második szándékosan egy megmaradó duplikátum g1g_1-re):

kk Detekció Score IoU a GT-kkel, illesztés
1 (12,12,48,48)(12,12,48,48) 0.95 g1g_1: 0.810.81 → TP
2 (11,10,49,52)(11,10,49,52) 0.90 g1g_1: 0.910.91, de g1g_1 már foglalt → FP
3 (100,22,149,59)(100,22,149,59) 0.85 g2g_2: 0.910.91 → TP
4 (5,5,20,20)(5,5,20,20) 0.80 legjobb g1g_1: 100/(1600+225−100)≈0,058<0,5100/(1600+225-100)\approx0{,}058<0{,}5 → FP
5 (29,81,79,119)(29,81,79,119) 0.70 g3g_3: 0.910.91 → TP
6 (0,0,9,9)(0,0,9,9) 0.60 nincs átfedő GT → FP

A τIoU=0.5\tau_{\text{IoU}}=0.5 melletti lépésenkénti értékek:

  1. TP=1,FP=0\mathrm{TP}=1,\mathrm{FP}=0, így p=1p=1, r=1/3r=1/3.
  2. TP=1,FP=1\mathrm{TP}=1,\mathrm{FP}=1, így p=1/2p=1/2, r=1/3r=1/3.
  3. TP=2,FP=1\mathrm{TP}=2,\mathrm{FP}=1, így p=2/3p=2/3, r=2/3r=2/3.
  4. TP=2,FP=2\mathrm{TP}=2,\mathrm{FP}=2, így p=1/2p=1/2, r=2/3r=2/3.
  5. TP=3,FP=2\mathrm{TP}=3,\mathrm{FP}=2, így p=3/5=0.6p=3/5=0.6, r=1r=1.
  6. TP=3,FP=3\mathrm{TP}=3,\mathrm{FP}=3, így p=1/2p=1/2, r=1r=1.

Az all-point képletben a jobb oldali maximumok:

Így

APall=13⋅1+(23−13)⋅23+(1−23)⋅35=13+29+15=15+10+945=3445≈0.7556.\operatorname{AP}_{\text{all}}=\frac13\cdot1+\left(\frac23-\frac13\right)\cdot\frac23+\left(1-\frac23\right)\cdot\frac35=\frac13+\frac29+\frac15=\frac{15+10+9}{45}=\frac{34}{45}\approx0.7556.

A kettes számú duplikátum rontotta a precisiont: a recall nem nőtt, de az FP eggyel növelte a nevezőt. Ha a 11 pontos VOC-interpolációval számolunk, az interpolált görbe pontjai: r∈[0,1/3]r\in[0,1/3]-ra p=1p=1; r∈(1/3,2/3]r\in(1/3,2/3]-ra p=2/3p=2/3; r∈(2/3,1]r\in(2/3,1]-ra p=3/5p=3/5. A 0,0.1,0.2,0.30,0.1,0.2,0.3 ponthoz p=1p=1, a 0.4,0.5,0.60.4,0.5,0.6 ponthoz p=2/3p=2/3, a 0.7,0.8,0.9,1.00.7,0.8,0.9,1.0 ponthoz p=3/5p=3/5 tartozik, így az átlag (4⋅1+3⋅(2/3)+4⋅(3/5))/11=(4+2+2.4)/11≈0.7636(4\cdot1+3\cdot(2/3)+4\cdot(3/5))/11=(4+2+2.4)/11\approx0.7636. A két konvenció közötti különbség 0.0080.008; nem hiba, hanem a definíciók eltérése.

Interpolált PR-görbe a kézi példából - A 3 GT / 6 detekciós kézi példából számolt all-point interpoláció; nem mérés.

26.3.4 4. Minimális kód és ellenőrizhető munkamenet

A következő NumPy-implementáció CPU-n fut, float32 dtype-tal, véletlen komponens nélkül, ezért seed-re nincs szükség. A munkakönyvtár: lab.

# lab/n103_nms_lab.py
import numpy as np


def box_area(boxes: np.ndarray) -> np.ndarray:
    boxes = np.asarray(boxes, dtype=np.float32)
    return (boxes[..., 2] - boxes[..., 0]) * (boxes[..., 3] - boxes[..., 1])


def compute_iou(a: np.ndarray, b: np.ndarray) -> float:
    ix1 = float(max(a[0], b[0]))
    iy1 = float(max(a[1], b[1]))
    ix2 = float(min(a[2], b[2]))
    iy2 = float(min(a[3], b[3]))
    iw = max(0.0, ix2 - ix1)
    ih = max(0.0, iy2 - iy1)
    inter = iw * ih
    aa = float((a[2] - a[0]) * (a[3] - a[1]))
    ba = float((b[2] - b[0]) * (b[3] - b[1]))
    union = aa + ba - inter
    if union > 0.0:
        return inter / union
    if aa == 0.0 and ba == 0.0:
        return 1.0 if np.allclose(a, b, atol=0.0) else 0.0
    return 0.0


def nms(boxes: np.ndarray, scores: np.ndarray, iou_threshold: float = 0.5) -> np.ndarray:
    boxes = np.asarray(boxes, dtype=np.float32)
    scores = np.asarray(scores, dtype=np.float32)
    B = boxes.shape[0]
    areas = box_area(boxes)
    order = sorted(range(B), key=lambda i: (-float(scores[i]), -float(areas[i]), i))
    keep = []
    suppressed = np.zeros(B, dtype=bool)
    for i in order:
        if suppressed[i]:
            continue
        keep.append(i)
        for j in order:
            if suppressed[j] or j == i:
                continue
            if compute_iou(boxes[i], boxes[j]) >= iou_threshold:
                suppressed[j] = True
    return np.array(keep, dtype=np.int64)


def match_detections(det_boxes, det_scores, gt_boxes, iou_threshold=0.5):
    det_boxes = np.asarray(det_boxes, dtype=np.float32)
    det_scores = np.asarray(det_scores, dtype=np.float32)
    gt_boxes = np.asarray(gt_boxes, dtype=np.float32)
    D = det_boxes.shape[0]
    M = gt_boxes.shape[0]
    areas = box_area(det_boxes)
    order = sorted(range(D), key=lambda i: (-float(det_scores[i]), -float(areas[i]), i))
    tp = np.zeros(D, dtype=bool)
    assigned = np.zeros(M, dtype=bool)
    for i in order:
        best_j = -1
        best_iou = -1.0
        for j in range(M):
            if assigned[j]:
                continue
            iou = compute_iou(det_boxes[i], gt_boxes[j])
            if iou >= iou_threshold and iou > best_iou:
                best_iou = iou
                best_j = j
        if best_j >= 0:
            tp[i] = True
            assigned[best_j] = True
    return tp, order


def all_point_ap(tp, order, n_gt):
    tp_cum = 0
    fp_cum = 0
    precisions = []
    recalls = []
    for idx in order:
        if tp[idx]:
            tp_cum += 1
        else:
            fp_cum += 1
        precisions.append(tp_cum / (tp_cum + fp_cum))
        recalls.append(tp_cum / n_gt)
    ap = 0.0
    prev = 0.0
    for k, r in enumerate(recalls):
        if r > prev:
            ap += (r - prev) * max(precisions[k:])
            prev = r
    return ap


def main():
    nms_boxes = np.array([
        [0, 0, 5, 5],
        [0, 0, 10, 10],
        [20, 20, 30, 30],
        [2, 2, 12, 12],
        [1, 1, 9, 9],
    ], dtype=np.float32)
    nms_scores = np.array([0.6, 0.9, 0.5, 0.85, 0.9], dtype=np.float32)
    keep = nms(nms_boxes, nms_scores, 0.5)
    print("NMS keep (eredeti indexek):", keep.tolist())
    print("Ures NMS:", nms(np.empty((0, 4), np.float32), np.empty(0, np.float32)).tolist())

    gt = np.array([
        [10, 10, 50, 50],
        [100, 20, 150, 60],
        [30, 80, 80, 120],
    ], dtype=np.float32)
    det_boxes = np.array([
        [12, 12, 48, 48],
        [11, 10, 49, 52],
        [100, 22, 149, 59],
        [5, 5, 20, 20],
        [29, 81, 79, 119],
        [0, 0, 9, 9],
    ], dtype=np.float32)
    det_scores = np.array([0.95, 0.90, 0.85, 0.80, 0.70, 0.60], dtype=np.float32)
    tp, order = match_detections(det_boxes, det_scores, gt, 0.5)
    ap = all_point_ap(tp, order, gt.shape[0])
    print("All-point AP:", ap)
    print("TP sorrend szerint:", [int(tp[i]) for i in order])


if __name__ == "__main__":
    main()

A futtatás parancsa: python n103_nms_lab.py. Az alábbi munkamenet-output a fenti kézi számításból származó jelölt elvárt kimenet, mert a kéziratkészítés során kódot nem futtattam:

NMS keep (eredeti indexek): [1, 3, 0, 2]
Ures NMS: []
All-point AP: 0.75555557
TP sorrend szerint: [1, 0, 1, 0, 1, 0]

Ha az ellenőrző futtatás ettől 10−610^{-6} abszolút hibánál jobban eltér, akkor nem a konvenció, hanem a kód hibás; az AP-érték kerekítve 0.75560.7556.

26.3.4.1 Kiterjesztés: több kép, több osztály, mAP

A fenti match_detections és all_point_ap szándékosan egyképes, egyosztályos didaktikai példa: nem kap image_id-t és osztálycímkét. Valódi detektálási kiértékelésnél egy detekció csak azonos képű és azonos osztályú GT-hez illeszthető, egy osztály AP-jét pedig az összes kép detekcióinak globális score-sorrendjéből kell számolni, nem a képenkénti AP-k átlagaként. A mAP az osztályonkénti AP-k átlaga.

Három AP-konvenció él egymás mellett, és a számszerű eredmények nem összevethetők:

Az NMS szintén konvenciófüggő: osztályonkénti NMS-nél csak azonos osztályú dobozok nyomják el egymást (a detektorok szokásos alapesete), class-agnostic NMS-nél bármely két átfedő doboz; az utóbbi két különböző osztályú, egymást átfedő objektum egyikét eldobhatja. A választott változatot a kiértékelési jegyzőkönyvben rögzíteni kell.

# lab/n103_map.py — többképes, többosztályos AP és mAP (az n103_nms_lab.py függvényeire épül)
import numpy as np
from n103_nms_lab import compute_iou


def class_ap(dets, gts, cls, iou_threshold=0.5):
    """Egy osztály AP-je az összes képen: a detekciók globális score-sorrendben,
    illesztés csak azonos image_id-jú és osztályú GT-hez (all-point interpoláció)."""
    d = sorted([x for x in dets if x["cls"] == cls], key=lambda x: -x["score"])
    g = [x for x in gts if x["cls"] == cls]
    used = [False] * len(g)
    tp = []
    for det in d:
        best, best_iou = -1, iou_threshold
        for j, gt in enumerate(g):
            if used[j] or gt["image_id"] != det["image_id"]:
                continue
            iou = compute_iou(det["box"], gt["box"])
            if iou >= best_iou:
                best, best_iou = j, iou
        if best >= 0:
            used[best] = True
        tp.append(best >= 0)
    tp = np.array(tp, dtype=float)
    prec = np.cumsum(tp) / np.arange(1, len(tp) + 1)
    rec = np.cumsum(tp) / len(g)
    ap, prev = 0.0, 0.0
    for k in range(len(tp)):
        ap += (rec[k] - prev) * prec[k:].max()          # jobb oldali maximum
        prev = rec[k]
    return ap


def mean_ap(dets, gts, iou_threshold=0.5):
    classes = sorted({g["cls"] for g in gts})
    aps = {c: class_ap(dets, gts, c, iou_threshold) for c in classes}
    return aps, float(np.mean(list(aps.values())))


def box(x, y, w=10, h=10):
    return np.array([x, y, x + w, y + h], dtype=np.float32)


# Két kép, két osztály.
gts = [{"image_id": 0, "cls": 0, "box": box(0, 0)},
       {"image_id": 0, "cls": 1, "box": box(30, 30)},
       {"image_id": 1, "cls": 0, "box": box(0, 0)},
       {"image_id": 1, "cls": 0, "box": box(50, 50)}]
dets = [{"image_id": 0, "cls": 0, "score": 0.9, "box": box(1, 1)},      # TP
        {"image_id": 0, "cls": 0, "score": 0.8, "box": box(60, 0)},     # FP
        {"image_id": 1, "cls": 0, "score": 0.7, "box": box(0, 1)},      # TP
        {"image_id": 1, "cls": 0, "score": 0.6, "box": box(51, 50)},    # TP
        {"image_id": 0, "cls": 1, "score": 0.5, "box": box(30, 31)},    # TP
        {"image_id": 1, "cls": 1, "score": 0.95, "box": box(0, 0)}]     # FP: más osztály
aps, m = mean_ap(dets, gts)
print("osztályonkénti AP:", {c: round(float(a), 4) for c, a in aps.items()}, " mAP:", round(m, 4))

# A 0. osztály képenkénti AP-jeinek átlaga nem a globális osztály-AP.
per_image = [class_ap([d for d in dets if d["image_id"] == i],
                      [g for g in gts if g["image_id"] == i], 0) for i in (0, 1)]
print("0. osztály képenként:", [round(float(a), 4) for a in per_image],
      " átlaguk:", round(float(np.mean(per_image)), 4), " globális:", round(float(aps[0]), 4))

A lab-profilban (NumPy 2.3.5) mért kimenet:

osztályonkénti AP: {0: 0.8333, 1: 0.5}  mAP: 0.6667
0. osztály képenként: [1.0, 1.0]  átlaguk: 1.0  globális: 0.8333

A 0. osztály mindkét képen külön-külön tökéletes AP-t kap (a hibás detekció az első képen a helyes után jön), a globális rangsorban viszont a 0,8 score-ú FP megelőzi a második kép két TP-jét, ezért a globális osztály-AP 13⋅1+13⋅0,75+13⋅0,75=0,8333\tfrac13\cdot1+\tfrac13\cdot0{,}75+\tfrac13\cdot0{,}75=0{,}8333. A képenkénti AP-k átlaga tehát általában nem a benchmark-AP. Az 1. osztály AP-je 0,50{,}5: a második képen adott, más osztályú GT-re eső 0,95-ös detekció FP, mert az illesztés osztályon belül történik.

26.3.5 5. Konkrét labor és eredményértelmezés

A labor célja annak ellenőrzése, hogy a tiszta Python/Numpy NMS ugyanazt a sorrendet és elnyomást adja, mint a kézi definíció. A laborprotokoll:

  1. Input: a fenti nms_boxes és nms_scores ötöse, τ=0.5\tau=0.5; külön üres bemenet; a 3.2. példa GT- és detekciós dobozai.
  2. Futtatás: cd lab && python n103_nms_lab.py, CPU, float32, seed nem értelmezett.
  3. Ellenőrzés: az NMS kimenet pontosan [1,3,0,2], az üres [], az AP 0.755555570.75555557 a float32 kerekítés mellett. Az azonos score-jú BB és EE közül EE törlődik, mert a nagyobb területű BB előbb került sorra; ez a területi tie-break szándékolt következménye.
  4. Értelmezés: a labor azt mutatja, hogy a teljes pipeline három része elkülönül: a det_scores a modell kimenete; az nms a posztprocessz, amely nem változtat a score-on; az all_point_ap a matching után a teljes rangsort összegzi. A hatodik, alacsony score-ú FP nem rontja az AP-t, mert az interpoláció a későbbi pontoknál a maximális precisiont használja; a második, duplikált detekció viszont rontja, mert a recall növelése nélkül növeli az FP-ket.

Ez a labor nem teljes detektor-értékelés, hanem a definíciók numerikus ellenőrzése. A jövőbeli TorchVision-alapú detektálási laborhoz a [D18] hivatkozást külön kell tartalmilag ellenőrizni; jelen egység nem támaszkodik rá implementációs állításban.

26.3.6 6. Feladatok

F1. (fogalmi, nehézség 1, előfeltétel: 2. szakasz) Definiálja az IoU-t egy pozitív területű és egy elfajuló dobozra. Miért van szükség a félig nyílt xyxy konvenció megkülönböztetésére a területszámításban? Adjon meg egy párt, ahol a metszet üres, de a két doboz koordinátái átfedni látszanak a zárt intervallum-értelmezés szerint.

F2. (levezetési/bizonyítási, nehézség 2, előfeltétel: 3.2. szakasz) Igazolja, hogy az all-point AP invariáns a score-vektor minden szigorúan monoton növekvő transzformációjára, ha nincsenek azonos score-ok. Számítsa ki a 11 pontos VOC-AP-t a 3.2. példa adataiból, minden köztes lépéssel.

F3. (implementációs, nehézség 2, előfeltétel: 4. szakasz) Módosítsa az nms függvényt lineáris soft-NMS-szé: ha egy kiválasztott MM doboz egy még megmaradt ii dobozt elnyomna (IoU≥τ\operatorname{IoU}\ge\tau), akkor si←si(1−IoU(M,bi))s_i\leftarrow s_i(1-\operatorname{IoU}(M,b_i)); a dobozt csak akkor dobja el, ha az új score 0.10.1 alá esik. A külső ciklus mindig a pillanatnyi legmagasabb score-ú megmaradt dobozt vegye. Futtassa a 4. szakasz öt dobozán, és adja meg a megmaradt sorrendet és a végső score-okat.

F4. (hibakeresési, nehézség 2, előfeltétel: 3.2. szakasz) Egy hallgató a recall nevezőjébe a képhez tartozó detekciók számát írja, azaz rk=TPk/Dr_k=\mathrm{TP}_k/D és nem TPk/M\mathrm{TP}_k/M. Lokalizálja a hibát, írja le a következményét akkor, ha egy képen M=3M=3 GT és D=6D=6 detekció van, és mutassa meg a javítás utáni visszaellenőrzést a 3.2. példán.

F5. (kísérleti/transzfer, nehézség 3, előfeltétel: 25.3. Kis adat és feature-alapú baseline, 26.2. Egy- és kétlépcsős detektor) Tervezzen kísérletet arra, hogyan változik az AP, ha az NMS IoU-küszöbét τ∈{0.3,0.5,0.7}\tau\in\{0.3,0.5,0.7\} értékekre állítjuk. A rendelkezésre álló adat 20 procedurális kép, képenként 2-5 GT-dobozzal, ugyanazzal a rögzített, NMS előtti detektor-kimenettel. Adjon meg protokollt: megfigyelési egység, split, baseline, változó tényező, metrika, az összehasonlítás típusa és a következtetés érvényességi korlátja.

26.3.7 7. Megoldások és indoklás

F1. Pozitív területű aa és elfajuló bb esetén A(b)=0A(b)=0, így a metszet területe is nulla, és IoU(a,b)=0/(A(a)+0−0)=0\operatorname{IoU}(a,b)=0/(A(a)+0-0)=0. A félig nyílt konvenció azért fontos, mert a [xmin,xmax)[x_{\min},x_{\max}) értelmezésben az egymást csak határon érintő dobozok metszete nulla hosszúságú, tehát nulla területű. Zárt intervallumokkal a határvonalon osztozó dobozoknak látszólag közös területük lenne. Példa: a=(0,0,10,10)a=(0,0,10,10) és b=(10,0,20,10)b=(10,0,20,10); félig nyíltan az x=10x=10 nem tartozik aa-hoz, így a metszet üres, területe 00. Zárt értelmezésben a {10}×[0,10)\{10\}\times[0,10) együttes egy egyenes szakasz, ami festett pixelrácson értelmezhető másként; a numerikus IoU-konvenció a félig nyílt definíciót rögzíti.

F2. Legyen f:ℝ→ℝf:\mathbb R\to\mathbb R szigorúan monoton növekvő, és s′i=f(si)s'_i=f(s_i). Mivel nincs tie, si>sj⇔f(si)>f(sj)s_i>s_j\iff f(s_i)>f(s_j), tehát a rendezett sorrend azonos. A matching csak a sorrendtől és a dobozoktól függ, a score-értékektől nem; a TP/FP sorozat azonos. Ezért a (rk,pk)(r_k,p_k) pontok azonosak, és APall(s′)=APall(s)\operatorname{AP}_{\text{all}}(s')=\operatorname{AP}_{\text{all}}(s). A 11 pontos VOC-számítás a 3.2. szakasz végén: az interpolált görbe r=0,0.1,0.2,0.3r=0,0.1,0.2,0.3 pontban p=1p=1; r=0.4,0.5,0.6r=0.4,0.5,0.6 pontban p=2/3p=2/3; r=0.7,0.8,0.9,1.0r=0.7,0.8,0.9,1.0 pontban p=3/5p=3/5. Az átlag 8.4/11≈0.76368.4/11\approx0.7636.

F3. A soft-NMS implementációjának vázlata:

def soft_nms(boxes, scores, iou_threshold=0.5, score_drop=0.1):
    boxes = np.asarray(boxes, dtype=np.float32)
    scores = np.asarray(scores, dtype=np.float64).copy()
    B = boxes.shape[0]
    areas = box_area(boxes)
    suppressed = np.zeros(B, dtype=bool)
    keep = []
    for _ in range(B):
        candidates = [(idx, scores[idx], -float(areas[idx]), idx)
                      for idx in range(B) if not suppressed[idx]]
        if not candidates:
            break
        i = max(candidates, key=lambda x: (x[1], x[2], x[3]))[0]
        keep.append(i)
        suppressed[i] = True
        for j in range(B):
            if suppressed[j]:
                continue
            iou = compute_iou(boxes[i], boxes[j])
            if iou >= iou_threshold:
                scores[j] *= (1.0 - iou)
                if scores[j] < score_drop:
                    suppressed[j] = True
    return keep, scores[keep]

A 4. szakasz öt dobozára: először BB kerül kiválasztásra. Ekkor sE←0.9⋅(1−0.64)=0.324s_E\leftarrow0.9\cdot(1-0.64)=0.324; sD←0.85⋅(1−0.4706)≈0.45s_D\leftarrow0.85\cdot(1-0.4706)\approx0.45; sA←0.6⋅(1−0.25)=0.45s_A\leftarrow0.6\cdot(1-0.25)=0.45; sC=0.5s_C=0.5. A következő választás a legmagasabb score alapján CC (0.5) lesz, nem DD vagy AA; CC nem nyom el senkit. Ezután DD és AA holtversenyben 0.45-nél; a területi tie-break miatt DD előbb, és IoU(D,A)=0.0776<0.5\operatorname{IoU}(D,A)=0.0776<0.5, tehát AA nem módosul. Végül AA és EE marad; EE score-ja 0.324, AA-é 0.45. A sorrend: B,C,D,A,EB,C,D,A,E, végső score-ok: 0.9,0.5,0.45,0.45,0.3240.9,0.5,0.45,0.45,0.324. A lineáris soft-NMS tehát megőrizhet olyan dobozt is, amelyet a kemény NMS eldobott; ez a fő viselkedésbeli különbség.

F4. A hiba a recall definíciójában van: a nevező MM, a GT-k száma, nem DD. Ha D>MD>M, a hallgató képlete alulbecsüli a recallt, és hamis „van még találatlan objektum” képet ad akkor is, amikor minden GT-t megtaláltunk. A 3.2. példában a lépésenkénti recallok 1/6,1/6,2/6,2/6,3/6,3/61/6,1/6,2/6,2/6,3/6,3/6 lennének 1/3,1/3,2/3,2/3,1,11/3,1/3,2/3,2/3,1,1 helyett; az AP hamisan alacsonyabb lenne, mert a görbe sosem éri el az 1-et. A javítás a nevező MM-re cserélése; a visszaellenőrzés a fenti r5=1r_5=1 érték visszaállítása és az AP 0.75560.7556 reprodukálása.

F5. Protokollvázlat. Kérdés: van-e mérhető különbség az AP-ben a három IoU-küszöb között? Hipotézis: nincs előre rögzített nyertes; az alacsonyabb τ\tau agresszívebben ritkít. Megfigyelési egység: egy kép; a 20 képet 10/10 train/val splitre osztjuk a detektort nem tanítjuk, csak a rögzített predikciókat értékeljük. Változó tényező: τ∈{0.3,0.5,0.7}\tau\in\{0.3,0.5,0.7\}. Baseline: NMS nélküli nyers lista, amelynek AP-je az induló referencia. Metrika: all-point AP, τIoU=0.5\tau_{\text{IoU}}=0.5. Az összehasonlítás páros, ugyanazon a 10 val-képen, ezért a különbség szignifikanciája a képek közötti szórással értelmezhető; bootstrap esetén a jelenet, nem a doboz legyen a független mintavételi egység. Korlát: a 10 kép nem elegendő általános detektálási benchmarkhoz; az eredmény csak a saját procedurális adatra vonatkozik, és a detektor-modell rögzítése miatt a tanulási bizonytalanságot nem méri.

26.3.8 8. Tipikus tévedés ellenpéldával

A leggyakoribb hiba az AP és az egyetlen score-küszöbön mért precision összekeverése. Ellenpélda a 3.2. adatsorból: ha a felhasználó τs=0.6\tau_s=0.6-ot választ, mind a hat detekció bekerül, így a precision 3/6=0.53/6=0.5. Ha τs=0.95\tau_s=0.95-öt választ, csak az első doboz kerül be, a precision 1/1=11/1=1. Ugyanarra a modellre a két működési pont látványosan eltérő precisiont ad, miközben a modell rangsora nem változott. Az AP éppen azért használja a teljes rendezett listát, hogy ne egyetlen küszöbhöz kössük a modell minőségét. A hiba következménye: a „modellünk precisionje 11” állítás 0.950.95-ös küszöbnél igaz is lehet, de elhallgatja a két duplikátumot és a kihagyott GT-ket. A javított megfogalmazás: „az adott τs\tau_s melletti precision 11, az all-point AP 0.75560.7556”.

Önellenőrzés: az NMS kimenetben a duplikátumok száma τ\tau monoton csökkenő függvénye, de a megmaradó dobozok indexsorrendje nem feltétlenül score-csökkenő, mert az elnyomás a feldolgozási sorrendtől függ. A PR-pontok pkp_k értéke nem monoton: a kézi példában 1→0.5→0.667→0.5→0.6→0.51\to0.5\to0.667\to0.5\to0.6\to0.5; a pmaxp_{\max}-interpoláció éppen ezt simítja. Visszahívás: a 14.3. Paraméterszám, MAC és aktivációmemória szerint a MAC a modell előrehaladásának költsége; az NMS O(B2)O(B^2) IoU-számítása ettől független posztprocessz-költség, nagy BB esetén külön mérni kell.

26.3.9 Rövid összefoglaló

Az NMS a nyers detekciók posztprocessze: score, majd terület és index szerinti determinisztikus sorrendben kiválaszt, és a τ\tau-nál jobban átfedő, alacsonyabb prioritású dobozokat elnyomja. A score threshold ezzel ellentétben egyetlen működési pontot vág le; az AP a teljes rangsorból indul. A detekciós matching során minden GT-hez legfeljebb egy TP tartozhat; a második illeszkedő detekció FP, ez a duplicate detection büntetése. Az all-point AP a PR-görbe alatti, jobb oldali maximális precisionnel interpolált terület. A kézi példában az AP 0.75560.7556, a 11 pontos VOC-változat 0.76360.7636; a kettő közötti eltérés konvenció, nem hiba.

26.3.10 Következő alkalmazás és ténylegesen használt források

A 26.4. Detektálási pipeline és hibaelemzés egység a teljes detektálási pipeline-t és a hibaelemzést vizsgálja: az itteni NMS, score threshold és AP ott kapcsolódik össze a választott architektúra, az adatfolyam és a tévesztési esetek rendszeres elemzésével.