A Projekt III. a 39.2. Encoder–decoder vagy lokalizációs fej alfejezetben egy bináris, egyobjektumos szegmentáló fejet definiált: a háló minden 24×24-es GlyphLab-képhez egy logit-maszkot ad, amelyből küszöböléssel bináris előrejelzés lesz. Ebben az alfejezetben azt a kérdést járjuk körül, hogyan ellenőrizzük ezt a kimenetet geometriailag és statisztikailag úgy, hogy az állítás ne csak a nagy háttérterület miatt tűnjön jónak.
A szükséges előismeret: a maszk mint
bináris mátrix, a half-open doboz 26.1.
Dobozkoordináta, IoU és lokalizációs loss alfejezetből, valamint az
IoU/Dice alapfogalmak 27.3. Dice, IoU és
aggregálás alfejezetből. A GlyphLab anotációja minden képhez
tartalmazza az előtér maszkját; a masks.npy alakja
,
uint8, értéke 0 vagy 1.
Induló ellenőrző kérdés: ha egy 24×24-es képen a GT maszk 24 előtérpixelt tartalmaz (4,17%), és a modell üres maszkot ad, akkor a pixel accuracy . Jó eredmény-e ez? A kérdésre a válasz nem, és a különbség az alfejezet központi témája.
Rögzítsünk egyetlen képre egy GT maszkot és egy bináris predikciót . A pixelpárok négy hibakategóriába sorolhatók:
Mind a négy mennyiség nemnegatív egész, összegük
.
A maszkok uint8 típusból bool-lá alakíthatók,
így a fenti összegek a NumPy logical_and,
logical_not műveleteivel számíthatók.
A bináris szegmentálás két szokásos metrikája:
A 27.3. Dice, IoU és aggregálás alfejezetben bizonyítottuk, hogy nemüres, pozitív metszetű halmazokra , és rögzítettük az üres–üres konvenciót: ha , azaz a két maszk egyaránt üres, akkor képpáronként. Ha a nevező nulla, de a kategóriák nem mind nullák, a hányados 0. A projektben a GT maszk a generátor szerződése miatt nem üres, de a predikció lehet üres, ezért az üres eset kezelése kötelező.
A pixel accuracy
egyszerű, de háttérdomináns képen torzít: az induló példa accuracy mellett és , mert . A szegmentáló modell kiértékelésénél ezért az IoU/Dice az elsődleges metrika, az accuracy-t csak a hibakategóriák mellett közöljük.
A doboz az előtér pixeleinek legkisebb befoglaló téglalapja. Az indexelés nullától kezdődik, a doboz félig nyílt xyxy intervallum:
és ugyanígy , . Az intervallum vízszintesen és függőlegesen. A doboz területe
A félig nyílt konvenció nem esztétikai kérdés: két doboz metszetének
szélessége
,
amely pontosan akkor pozitív, ha a két intervallum valódi átfedésben
van. Ha valaki
-et
adna a pixelindexhez, a terület és a metszet konzisztenciája megtörne. A
GlyphLab metadata.csv-ben szereplő
pontosan ezt a konvenciót követi.
A centroid, azaz a maszk tömegközéppontja:
ahol az előtérpixelek száma. A koordináták valós számok, a tengelysorrend , mert a tömb első indexe a sor. A centroid a doboz négy sarkánál finomabb pozícióinformációt ad: a doboz a szélső pontokra, a centroid a tömeg eloszlására érzékeny.
A projektben a centroid-hibát is használjuk, ezért nem elegendő azt mondani, hogy „a centroid eltolódik az eltolással”. Állításként és bizonyításként rögzítjük.
4.1. lemma (centroid eltolási ekvivarianciája). Legyen , , és . Ekkor minden esetén
ahol .
Bizonyítás. Az eltolás bijekció és között, ezért . A definíciót alkalmazva:
Az összeget komponensenként szétbontva:
A második egyenlőségnél használtuk, hogy az összeg második tagja konstans tagú: minden elemhez ugyanazt a vektort adjuk, ezért az összeg .
Szélsőeset. Üres maszkra
nem definiált; a kódban None értékkel jelöljük. Ha az
eltolás a képkeretből kilóg, akkor a levágott, megfigyelt halmaz nem
,
hanem annak a képkerettel vett metszete, így a lemma nem alkalmazható. A
centroid-hiba tehát eltolás-invariáns a kereten belül, de a határnál a
csonkolás új geometriai hibát hoz létre.
4.2. következmény (a centroid-hiba eltolás-invarianciája). Legyen két nemüres maszk, és olyan, hogy és is a képkeretben marad. Ekkor
Bizonyítás. A 4.1. lemmát kétszer alkalmazva , mert a tag kiejti egymást. A norma megőrzi az egyenlőséget.
Ez a következmény a projekt metrikájának matematikai azonossága: a centroid-hiba a két maszk együttes eltolására érzéketlen, így tisztán a relatív pozícióhibát méri. A bizonyítás a koordináták egy-egy értelmű eltolásán (bijekción) alapul: az eltolás a maszk előtérpixeleit egy-egy értelműen képezi le, így a pixelek száma nem változik, és minden koordinátájuk ugyanazzal a vektorral tolódik. A centroid a maszkra nézve általában nem lineáris: , a nevező is a bemenettől függ; például a maszk pozitív skalárral szorzása nem skálázza a centroidot. Linearitás legfeljebb rögzített számú pont koordinátáinak átlagolására mondható ki, és az eltolási ekvivariancia nem ebből, hanem a fenti bijekcióból következik. A doboz ugyanígy ekvivariáns az eltolásra, de mivel minimumra és maximumra épül, a pixelek helyzetének kis változására érzékenyebben reagál.
Vegyünk egy 6×8-as rácsot. A GT maszk egy 3×2-es téglalap:
0 0 0 0 0 0 0 0
0 0 1 1 0 0 0 0
0 0 1 1 0 0 0 0
0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
A predikció az alsó sort elveszti, de a felső kettőt megtartja:
0 0 0 0 0 0 0 0
0 0 1 1 0 0 0 0
0 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
Kézzel: (a közös ), , (a GT pixelei hiányoznak), . Így
A GT doboz: , , , , terület . A pred doboz: , , , , terület . A metszet területe , az unió , tehát a box-IoU .
A GT centroid:
A pred centroid:
A centroid-hiba
Ez a kézi számpélda lesz a labor numerikus ellenőrzése.
A labor egy NumPy-alapú kiértékelő modult valósít meg, amelyet a 39.4. Alkalmazási riport és átadás exportja és
auditja is importál; ezért a dobozkonvenció egyetlen helyen, egy névvel
ellátott Box(xmin, ymin, xmax, ymax) struktúrában és a
validate_box központi validátorban rögzített. A sorrend
mindenhol a metadata.csv xyxy sorrendje; a maszkból számolt
dobozt a kód mind a 360 képre összeveti a metadata dobozával, és egy
aszimmetrikus doboz teljes láncát (maszk → doboz → terület → JSON-export
→ visszaolvasás) is ellenőrzi. A maszkon mért IoU neve
mask_iou, a dobozon mérté bbox_iou. A
munkakönyvtár lab/project3; a GlyphLab adat a
lab/data alatt van.
# lab/project3/n155_geom_control.py — n155 geometriai metrika kontroll, xyxy dobozokkal.
"""Egyetlen dobozkonvenció: Box(xmin, ymin, xmax, ymax), nullától indexelt,
félig nyílt [xmin,xmax) x [ymin,ymax); üres maszk szentinelje (-1,-1,-1,-1)."""
from __future__ import annotations
import json
from pathlib import Path
from typing import NamedTuple
import numpy as np
import pandas as pd
DATA = Path(__file__).resolve().parents[1] / "data"
H = W = 24
class Box(NamedTuple):
xmin: int
ymin: int
xmax: int
ymax: int
EMPTY = Box(-1, -1, -1, -1)
def validate_box(b: Box, h: int = H, w: int = W) -> Box:
"""Központi validátor: a szentinel vagy érvényes, nemüres, a képen belüli doboz."""
b = Box(*map(int, b))
if b == EMPTY:
return b
if not (0 <= b.xmin < b.xmax <= w and 0 <= b.ymin < b.ymax <= h):
raise ValueError(f"érvénytelen xyxy doboz: {tuple(b)}")
return b
def conf_counts(pred: np.ndarray, target: np.ndarray) -> dict[str, int]:
p = pred.astype(bool)
t = target.astype(bool)
assert p.shape == t.shape, (p.shape, t.shape)
tp = int(np.logical_and(p, t).sum())
fp = int(np.logical_and(p, ~t).sum())
fn = int(np.logical_and(~p, t).sum())
tn = int(np.logical_and(~p, ~t).sum())
return {"TP": tp, "FP": fp, "FN": fn, "TN": tn}
def bbox(mask: np.ndarray) -> Box:
"""Maszkból xyxy doboz; üres maszkra a szentinel."""
ys, xs = np.nonzero(np.squeeze(mask))
if ys.size == 0:
return EMPTY
return validate_box(Box(int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1))
def box_area(b: Box) -> int:
b = validate_box(b)
return 0 if b == EMPTY else (b.xmax - b.xmin) * (b.ymax - b.ymin)
def box_iou(a: Box, b: Box) -> float:
"""bbox-IoU; két üres doboz 1, pontosan egy üres 0 (az n153 üreseset-szerződése)."""
a, b = validate_box(a), validate_box(b)
if a == EMPTY or b == EMPTY:
return 1.0 if a == b else 0.0
iw = max(0, min(a.xmax, b.xmax) - max(a.xmin, b.xmin))
ih = max(0, min(a.ymax, b.ymax) - max(a.ymin, b.ymin))
inter = iw * ih
return inter / (box_area(a) + box_area(b) - inter)
def centroid(mask: np.ndarray) -> tuple[float, float] | None:
ys, xs = np.nonzero(np.squeeze(mask))
if ys.size == 0:
return None
return float(ys.mean()), float(xs.mean())
def metric_dict(pred: np.ndarray, target: np.ndarray) -> dict:
c = conf_counts(pred, target)
tp, fp, fn, tn = c["TP"], c["FP"], c["FN"], c["TN"]
out = {**c,
"accuracy": (tp + tn) / (tp + fp + fn + tn),
"mask_iou": tp / (tp + fp + fn) if (tp + fp + fn) else 1.0,
"dice": 2 * tp / (2 * tp + fp + fn) if (2 * tp + fp + fn) else 1.0,
"bbox_iou": box_iou(bbox(pred), bbox(target)),
"n_pixels": int(pred.size)}
cp, ct = centroid(pred), centroid(target)
out["centroid_error"] = (float(np.hypot(ct[0] - cp[0], ct[1] - cp[1]))
if cp is not None and ct is not None else None)
return out
def main() -> None:
target = np.array([
[0,0,0,0,0,0,0,0],
[0,0,1,1,0,0,0,0],
[0,0,1,1,0,0,0,0],
[0,0,1,1,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]], dtype=np.uint8)
pred = np.array([
[0,0,0,0,0,0,0,0],
[0,0,1,1,0,0,0,0],
[0,0,0,1,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0]], dtype=np.uint8)
out_dir = Path("n155_control")
out_dir.mkdir(exist_ok=True)
np.save(out_dir / "control_pred.npy", pred)
np.save(out_dir / "control_target.npy", target)
report = metric_dict(pred, target)
assert abs(report["mask_iou"] - 0.5) < 1e-12
assert abs(report["dice"] - 2/3) < 1e-12
assert abs(report["accuracy"] - 45/48) < 1e-12
assert abs(report["bbox_iou"] - 2/3) < 1e-12
assert abs(report["centroid_error"] - np.sqrt(17)/6) < 1e-12
# Aszimmetrikus doboz teljes lánca: maszk -> bbox -> terület -> export -> visszaolvasás.
m = np.zeros((H, W), dtype=np.uint8)
m[5:9, 2:7] = 1 # sorok 5..8, oszlopok 2..6
b = bbox(m)
assert b == Box(xmin=2, ymin=5, xmax=7, ymax=9) and box_area(b) == 20
assert Box(*json.loads(json.dumps(list(b)))) == b
assert box_iou(b, Box(2, 5, 7, 9)) == 1.0 and box_iou(b, Box(5, 2, 9, 7)) < 1.0
# A metadata.csv xyxy oszlopai minden képre egyeznek a maszkból számolt dobozzal.
masks = np.load(DATA / "masks.npy").astype(np.uint8)
meta = pd.read_csv(DATA / "metadata.csv")
for r in meta.itertuples():
assert bbox(masks[r.index]) == Box(r.xmin, r.ymin, r.xmax, r.ymax), r.index
report["metadata_bbox_check"] = f"{len(meta)} kép, mind egyezik"
report["foreground_ratio_by_split"] = {
split: float(masks[g["index"].to_numpy()].mean()) for split, g in meta.groupby("split")}
(out_dir / "n155_control_report.json").write_text(
json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps(report, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()Futtatás:
cd lab/project3
python n155_geom_control.pyA kód a n155_control/control_pred.npy és
n155_control/control_target.npy fájlokat menti, majd a
n155_control/n155_control_report.json jelentést írja. Az
ellenőrzés tűrése
,
mert a számítások egész alapúak, a hibák pedig kis nevezőjű racionális
számok. Az aszimmetrikus kontrolldoboz a
téglalap, azaz Box(xmin=2, ymin=5, xmax=7, ymax=9),
területe
;
ha valaki a koordinátákat
sorrendben adná át, a validátor vagy az összevetés azonnal hibát jelez,
nem csak a saját függvényei egymás közötti tesztje. Futási
jegyzőkönyv (eredeti GlyphLab-adat): minden assert
teljesült, a metadata_bbox_check értéke „360 kép, mind
egyezik”, az előtérarány splitenként train
,
val
,
test
.
A valós predikciókra a modult a 39.4. Alkalmazási
riport és átadás export_delivery.py és
report_audit.py szkriptje alkalmazza.
A GlyphLab megfigyelési egysége a scene_id: egy jelenethez két zajos nézet tartozik, és a két nézet nem független. A 38.3. Végső teszt és páros korrupció alfejezetben bizonyított csoportátlag-lemma szerint, ha minden csoportban azonos megfigyelésszám van, a csoportátlag megegyezik a per-image átlaggal. Itt minden scene-hez view tartozik, tehát a per-scene és a per-image átlag numerikusan egybeesik; a scene-alapú megfogalmazás mégis kötelező, mert a bootstrap és a hibaintervallum csak scene-egységekre értelmes.
Legyen a tiszta tesztkép és a korrupt pár IoU-ja. A korrupt teszt ugyanazon scene/view sorrendet követi, így a párosítás egyértelmű. A páros különbség
a scene-szintű különbség pedig
Az átlagos hatás . A párosítás azért lényeges, mert a két nézet közös jelenetbeli pozíciója és vastagsága korrelációt visz a mérésbe; ha a clean és corrupt méréseket független mintaként kezelnénk, a jelenetek közötti szórást tévesen a beavatkozás bizonytalanságához adnánk.
Az intervallumot a scene-szintű értékek osztályonként rétegzett bootstrapjével számítjuk: minden orientációs osztályból külön-külön, visszatevéssel húzunk 10 scene-t, kiszámítjuk a bootstrap-mintát, és a 2,5–97,5 percentilist adjuk. Az [S02] módszertani kerete szerint ez feltételes, ismételt mintavételi értelmezésű intervallum; nem ok-okozati garancia, és 30 scene mellett a szélek bizonytalanok.
Az osztályarány két szinten jelenik meg. A jelenetek három orientációs osztálya a generátor szerint kiegyensúlyozott, ezért a bootstrapet rétegezzük. A pixelosztály azonban nem az: az előtér aránya a 24×24-es képen lényegesen kisebb, mint a háttéré, ezért a plain BCE loss és a pixel accuracy háttértorzított lehet. A per-scene átlag minden jelenetet egyenlő súllyal vesz, a globális pixel-IoU viszont a nagyobb előtérterületű képek felé húz.
A leggyakoribb tévedés a pixel accuracy önálló használata háttérdomináns szegmentálásnál. Ellenpélda: legyen a kép 24×24, a GT maszk 24 előtérpixelt tartalmaz, a predikció pedig az azonosan nulla maszk. Ekkor , , , , tehát
A 95,83%-os accuracy azt sugallhatná, hogy a modell kiváló, miközben a foreground szegmentálás teljesen használhatatlan: egyetlen előtérpixelt sem talál meg. A helyes eljárás az IoU/Dice, valamint a foreground recall és foreground precision együttes közlése.
A második ellenpélda a doboz és a maszk összekeverésére figyelmeztet: legyen a GT doboz , a pred doboz ugyanez, de a GT maszk a doboz bal, a pred maszk a doboz jobb felét fedi. Ekkor a box-IoU , a pixel-IoU . A doboz tehát lokalizációs összefoglaló, nem helyettesíti a pixelszintű metrikát.
Ellenőrző kérdések a továbbhaladás előtt:
A válaszok röviden: a négy skalár összege
;
a félig nyílt konvenció őrzi a metszet–unió formulát; a centroid az
átlag linearitása miatt tolódik el, ahogy a 4.1. lemma mutatja; a
mintavételi egység a scene_id;
,
ha
de
,
azaz a két maszk diszjunkt előterű.
Visszahívás: a 27.3. Dice, IoU és aggregálás alfejezetben bevezetett azonosság és az üres konvenció itt is érvényes; a 38.3. Végső teszt és páros korrupció scene-bootstrapje adja a páros különbség intervallumát; a 35.3. Perturbáció és adversarial példa tanulsága pedig az, hogy a clean és a perturbált bemenet metrikája két külön kockázat, nem egyetlen „robusztusság” szám.
F1 (fogalmi, nehézség 1). Definiálja a half-open doboz területét, és számítsa ki a doboz területét. Miért hibás a diszkrét képlet? Elvárt eredmény: a helyes terület, a hibás képlet értéke és az indoklás.
F2 (levezetési, nehézség 2). A 4.1. lemma felhasználásával bizonyítsa a 4.2. következményt. Adjon olyan eltolást, amelyre a lemma nem alkalmazható egy 24×24-es képen, és indokolja a kimaradást. Elvárt eredmény: teljes levezetés és konkrét számértékű ellenpélda.
F3 (implementációs, nehézség 2). Írjon függvényt,
amely két azonos alakú uint8 maszkra visszaadja a TP, FP,
FN, TN, IoU, Dice ötöst. Tesztelje az üres–üres esetre és arra az
esetre, amikor a pred minden pixelén 1, a GT pedig azonosan 0. Elvárt
eredmény: futtatható kód és a két teszteset számértékei.
F4 (hibakeresési, nehézség 2). Egy mérést végző hallgató a szegmentáló modelljét „nagyon pontosnak” nevezi, mert a validációs accuracy 0,9375. A tényleges predikció minden képen üres maszk. Hogyan lehetséges ez? Számítsa ki a 4×4-es esetet, ahol a GT-ben egyetlen előtérpixel van, és adja meg a javítást. Elvárt eredmény: az ok, a TM/FP/FN/TM számok, valamint a javasolt metrika.
F5 (kísérlettervezési, nehézség 3). Tervezze meg a Projekt III. páros korrupciós kísérletét a szegmentáló modellre. Adja meg a kérdést, a megfigyelési egységet, a metrikát, a statisztikát, a bootstrap stratégiát és a következtetés érvényességi korlátját. Elvárt eredmény: protokoll, amely a 38.3. Végső teszt és páros korrupció és a jelen alfejezet jelöléseit használja.
F1. A helyes terület . A hibás formula . A fél-nyílt intervallum a folytonos élhosszat méri: az intervallum hossza a végpontkülönbség, függetlenül attól, hogy a rácson hány egész index esik bele. A a bennfoglalt rácspontok számát adná, ami a metszet–unió geometriai területformulához nem konzisztens.
F2. A 4.1. lemma szerint
A norma mindkét oldalon kifejezhető, ezért a hiba egyenlő. A feltételek: és nem üres, és a , halmazok a keretben maradnak. Például a 24×24-es kép esetén az irányba tol: ha a maszk maximális -indexe 12, akkor , a keret csonkít. Az így megfigyelt halmaz nem , tehát a lemma előfeltétele sérül; a centroid-hiba már nem feltétlenül eltolás-invariáns.
F3. Az alábbi kód a labor conf_counts
és metric_dict függvényeire építhető; a két teszteset a
NumPy-ben:
def conf_counts(pred, target):
p = pred.astype(bool); t = target.astype(bool)
return (int((p & t).sum()), int((p & ~t).sum()),
int((~p & t).sum()), int((~p & ~t).sum()))
# üres-üres: 2x2
tp, fp, fn, tn = conf_counts(np.zeros((2,2), np.uint8),
np.zeros((2,2), np.uint8))
iou = 1.0 if (tp+fp+fn) == 0 else tp/(tp+fp+fn)
dice = 1.0 if (2*tp+fp+fn) == 0 else 2*tp/(2*tp+fp+fn)
assert (tp,fp,fn,tn,iou,dice) == (0,0,0,4,1.0,1.0)
# pred minden 1, target 0
tp, fp, fn, tn = conf_counts(np.ones((2,2), np.uint8),
np.zeros((2,2), np.uint8))
iou = 1.0 if (tp+fp+fn) == 0 else tp/(tp+fp+fn)
dice = 1.0 if (2*tp+fp+fn) == 0 else 2*tp/(2*tp+fp+fn)
assert (tp,fp,fn,tn,iou,dice) == (0,4,0,0,0.0,0.0)Az alakellenőrzés az assert pred.shape == target.shape;
a maszk uint8, a műveletek bool-lá alakítás
után egész összeget adnak.
F4. Az ok a háttérdominancia: 16 pixelből a GT-ben csak 1 az előtér, a pred üres. Ekkor TP=0, FP=0, FN=1, TN=15; accuracy , IoU , Dice . A pontosság a TN tömegétől függ, ezért egy triviális üres predikció is magas értéket kap. A javítás: a döntési metrika legyen IoU vagy Dice, és mindig közöljük a foreground recallt és precisiont; ha a modell üres maszkot ad, a recall 0, ami azonnal jelzi a hibát.
F5. Protokollterv. Kérdés: hogyan változik a befagyasztott szegmentáló modell IoU-ja a GlyphLab corrupt tesztjén? Hipotézis: a zaj és fényességgradiens az előtér határán rontja a maszkot. Megfigyelési egység: scene/view pár; a clean és corrupt image ugyanazon scene/view, ezért a párosítás rögzített. Metrika: ; hatás: , scene-átlag . Statisztika: . Bootstrap: 1000 ismétlés, osztályonként 10 scene visszatevéssel, 2,5–97,5 percentilis. Korlát: a toy adat nem valós kép; a CI csak a rögzített corrupt előállításra vonatkozik; a modellválasztás a validációs spliten már lezárult.
A szegmentáló modell kiértékelése nem egyetlen számon múlik. A TP, FP, FN, TN hibakategóriákból számított IoU és Dice érzékeny az előtérhibára, míg a pixel accuracy háttérdominancia mellett félrevezető. A half-open doboz a lokalizációt, a centroid a tömegközéppont-eltérést méri; utóbbiról beláttuk, hogy a maszkok közös eltolására invariáns. A GlyphLab értékelése scene-szinten aggregál, a clean–corrupt összevetés páros, a bootstrap egysége a scene_id. A korrekt protokoll az IoU/Dice mellett közli a hibakategóriákat és az osztályarányt, így a triviális üres predikció nem kaphat hamisan magas érdemjegyet.
A következő alkalmazás, 39.4. Alkalmazási riport és átadás, az alkalmazási riport és átadás: a most definiált metrikákat és páros intervallumot a modellkártya, a hibakorlátok és a felhasználási szerződés nyelvezetévé alakítja.
Felhasznált források: