23.4 Hibaszeletek és alkalmazási határ

23.4.1 Cél, előismeret és induló kérdés

A 23.3. Páros összehasonlítás és csoportszintű intervallum egységben a páros összehasonlítást és a scene-szintű, osztály-rétegzett bootstrap intervallumot vezettük be. Ott a metrika a teljes tesztkészletre vonatkozott. Itt a kérdés az: mely részhalmazokon romlik el a modell, és mekkora csoportméret alatt már nem tehetünk értelmes állítást. A 22.4. Augmentációs kísérlet és domainváltozás alfejezetben a clean és corrupt teszt eloszlását különítettük el; a 19.4. ConvNeXt és a recept/architektúra elválasztása arra tanított, hogy az architektúra és a tanítási recept hatását ne keverjük, amit most annyiban használunk, hogy a szeletelemzés a modellválasztástól független értékelési réteg. A 11.4. Egy teljes PyTorch tanítási iteráció logitokból származó döntési szabálya adja a döntési határ definícióját.

Induló becslés: lehet-e egy 80%-os tesztpontosságú modellnek olyan szelete, amelyen a pontosság 0%? A következő szakaszokban ezt formálisan megválaszoljuk.

23.4.2 Szelet, csoportméret és shift

Definíció (szelet). Legyen DD a teszt indexeinek halmaza, |D|=N|D|=N. Egy szelet S⊆DS\subseteq D, a gyakorlatban egy megfigyelhető attribútum (osztály, nézet, korrupció, scene-csoport) által indukált részhalmaz. A predikciók ŷ∈{0,…,K−1}N\hat y\in\{0,\ldots,K-1\}^N, a célok y∈{0,…,K−1}Ny\in\{0,\ldots,K-1\}^N. A szelet pontossága

acc(S)=1|S|∑i∈S𝟙[ŷi=yi],|S|>0. \mathrm{acc}(S)=\frac{1}{|S|}\sum_{i\in S}\mathbb{1}[\hat y_i=y_i], \qquad |S|>0.

Ha |S|=0|S|=0, az érték definiálatlan; az üres szeletet a modellkártyán „nincs adat” jelöléssel hagyjuk ki, nullával való feltöltés hamis eredmény lenne. A kódban a szeletet NN hosszú logikai maszkkal ábrázoljuk: mask[i] == True pontosan akkor, ha i∈Si\in S.

Definíció (csoportméret). A csoportméret a szelet elemszáma nS=|S|n_S=|S|. A GlyphLab tesztben osztályonként 10 scene és 20 kép van; egy osztály-korrupt keresztmetszet például n=20n=20. A bootstrap becslés szórása növekszik, ha nSn_S kicsi, ezért a jelentésben minden szelet mellé kiírjuk nSn_S-t, nemcsak a pontosságot.

Definíció (shift). Shiftnek nevezzük, ha a kiértékelési bemeneti eloszlás eltér a tanításitól. A GlyphLab corrupt tesztje ugyanazon scene/view párokat zajjal és fényességi gradienssel módosítja, tehát egy pontosan specifikált korrupciós eloszlást reprezentál; nem általános „valós világ” shiftet. A clean és corrupt teszt páros szerkezete miatt a romlást páronként számoljuk, ahogyan 22.4. Augmentációs kísérlet és domainváltozás rögzítette.

23.4.3 Hibataxonómia és döntési határ

Definíció (hibataxonómia). A hibataxonómia a téves predikciók rendezése három tengely mentén:

  1. Tévesztési cella: a confusion mátrix Cab=|{i:yi=a,ŷi=b}|C_{ab}=|\{i:y_i=a,\hat y_i=b\}|, a,b∈{0,1,2}a,b\in\{0,1,2\}, ahol a≠ba\neq b.
  2. Környezet: clean vagy corrupt.
  3. Csoport: a hibás példa scene_id azonosítója és view_id értéke.

A taxonómia tehát minden hibás indexhez egy rekordot rendel; ez operatív osztályozás, nem ok-okozati magyarázat. A belőle képzett összegzés például „a corrupt környezetben a vertical→diagonal tévesztések száma 7, ebből 5 a 12-es scene mindkét nézetéből származik”. Az ilyen mondat leíró állítás; azt, hogy miért éppen az a scene hibázik, külön vizsgálat nélkül nem állítjuk.

Definíció (döntési határ). A modell logitokat ad: z(x)∈ℝKz(x)\in\mathbb R^K, és

ŷ(x)=argmaxkzk(x). \hat y(x)=\arg\max_{k} z_k(x).

A döntési határ azon xx pontok halmaza, ahol az argmax nem egyértelmű, azaz létezik k≠jk\neq j, hogy zk(x)=zj(x)=maxlzl(x)z_k(x)=z_j(x)=\max_l z_l(x). Ha a shift az input eloszlást a határ felé tolja, a hibavalószínűség nőhet. Ez utóbbi heurisztika: abból, hogy egy szelet hibái a határhoz közeliek, nem következik bizonyítottan, hogy a shift okozta őket.

23.4.4 Összesített pontosság és szeletszerződés: ellenpélda

Állítás. Legyen DD teszt-indexhalmaz, |D|=N|D|=N, és S⊆DS\subseteq D nemüres. Ha minden i∈Si\in S téves, akkor

acc(D)≤1−|S|N. \mathrm{acc}(D)\le 1-\frac{|S|}{N}.

Bizonyítás. A hibák száma ED=∑i∈D𝟙[ŷi≠yi]E_D=\sum_{i\in D}\mathbb{1}[\hat y_i\neq y_i]. Feltétel szerint ED≥|S|E_D\ge |S|. Ezért

acc(D)=1−EDN≤1−|S|N. \mathrm{acc}(D)=1-\frac{E_D}{N}\le 1-\frac{|S|}{N}.

Egyenlőség pontosan akkor áll fenn, ha ED=|S|E_D=|S|, vagyis a D\SD\setminus S halmaz minden eleme helyes. Az állításból következik a levezetési kötelezettség ellenpéldája: N=100N=100, |S|=5|S|=5 mellett, ha SS minden eleme hibás és a többi 95 helyes, akkor acc(D)=0,95\mathrm{acc}(D)=0{,}95, miközben acc(S)=0\mathrm{acc}(S)=0. A globális pontosság tehát nem garantál semmilyen alsó korlátot egy tetszőleges szeletre; a globális érték a szeletpontosságok méret szerint súlyozott átlaga, az átlag pedig elfedheti a kis súlyú, teljesen hibás csoportot.

23.4.5 Kézi példa és minimális kód

Legyen N=10N=10, K=3K=3, és két szelet: a clean az első 8, a corrupt az utolsó 2 index.

Szelet Méret Helyes Pontosság
Összes 10 8 0,80
clean 8 8 1,00
corrupt 2 0 0,00

A globális 0,80 mellett a corrupt szelet 0,00; ez a fenti állítás egyenlőségi esete, hiszen 1−2/10=0,801-2/10=0{,}80.

# workdir: lab/
import numpy as np

def slice_accuracy(y_true, y_pred, mask):
    """Visszaadja: (correct, size, acc); acc None, ha a szelet üres."""
    mask = np.asarray(mask, dtype=bool)
    n = int(np.sum(mask))
    if n == 0:
        return 0, 0, None
    correct = int(np.sum(y_true[mask] == y_pred[mask]))
    return correct, n, correct / n

y_true = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 2], dtype=np.int64)
y_pred = np.array([0, 0, 0, 1, 1, 1, 2, 2, 0, 0], dtype=np.int64)
clean = np.array([True] * 8 + [False] * 2)
corrupt = ~clean

print("clean  :", slice_accuracy(y_true, y_pred, clean))
print("corrupt :", slice_accuracy(y_true, y_pred, corrupt))
print("overall :", np.mean(y_true == y_pred))

Várt kimenet: clean : (8, 8, 1.0), corrupt : (2, 0, 0.0), overall : 0.8.

23.4.6 Labor: hibataxonómia és modellkártya

Cél. A hallgató a saját, befagyasztott GlyphLab-modelljének clean és corrupt predikcióiból szeletelemzést és hibataxonómiát készít, majd JSON modellkártyát állít elő. A teszt nem befolyásolhatja a modellválasztást: a checkpointot kizárólag a validációs loss alapján választottuk, a clean és corrupt kiértékelés ezután, egyszer történik.

Input. data/ könyvtár a GlyphLab szerződés szerint; pred_labels.npy alak (60,) int64, corr_pred_labels.npy alak (60,) int64, mindkettő a metadata.csv test sorainak sorrendjében.

# workdir: lab/
# make_model_card.py
import argparse, json
import numpy as np
import pandas as pd

def scene_resample_indices(meta, rng):
    idxs = []
    for cls in range(3):
        scenes = meta[meta.label == cls].scene_id.unique()
        chosen = rng.choice(scenes, size=len(scenes), replace=True)
        for sc in chosen:
            sub = meta[(meta.label == cls) & (meta.scene_id == sc)]
            idxs.append(sub.index.to_numpy())
    return np.concatenate(idxs)

def bootstrap_ci(meta, y_true, y_pred, rng, B=2000):
    stats = []
    for _ in range(B):
        idx = scene_resample_indices(meta, rng)
        stats.append(np.mean(y_true[idx] == y_pred[idx]))
    return float(np.percentile(stats, 2.5)), float(np.percentile(stats, 97.5))

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--pred", required=True)
    ap.add_argument("--corr-pred", required=True)
    ap.add_argument("--out", default="model_card.json")
    ap.add_argument("--data-dir", default="data")
    ap.add_argument("--seed", type=int, default=20261020)
    a = ap.parse_args()

    meta = pd.read_csv(f"{a.data_dir}/metadata.csv")
    y_all = np.load(f"{a.data_dir}/labels.npy")
    test_meta = meta[meta.split == "test"].copy()
    idx = test_meta.index.to_numpy()
    y_true = y_all[idx]
    y_clean = np.load(a.pred).astype(np.int64)
    y_corr = np.load(a.corr_pred).astype(np.int64)
    rng = np.random.default_rng(a.seed)

    card = {
        "selection": "best val loss only",
        "overall": {
            "clean_acc": float(np.mean(y_true == y_clean)),
            "corrupt_acc": float(np.mean(y_true == y_corr)),
            "paired_drop": float(np.mean(y_true == y_clean)
                                 - np.mean(y_true == y_corr)),
        },
        "slices": {},
        "failure_taxonomy": {},
        "status": "measured by the student run",
    }

    for cls in range(3):
        mask = (test_meta.label == cls).to_numpy()
        acc = np.mean(y_true[mask] == y_clean[mask])
        lo, hi = bootstrap_ci(test_meta[mask], y_true[mask],
                              y_clean[mask], rng)
        card["slices"][f"clean_class_{cls}"] = {
            "n": int(mask.sum()), "acc": float(acc), "ci95": [lo, hi]}

    lo, hi = bootstrap_ci(test_meta, y_true, y_corr, rng)
    card["slices"]["corrupt_all"] = {
        "n": len(y_true),
        "acc": float(np.mean(y_true == y_corr)),
        "ci95": [lo, hi]}

    for env, yp in [("clean", y_clean), ("corrupt", y_corr)]:
        C = np.zeros((3, 3), dtype=int)
        for a in range(3):
            for b in range(3):
                C[a, b] = int(np.sum((y_true == a) & (yp == b)))
        card["failure_taxonomy"][env] = C.tolist()

    with open(a.out, "w", encoding="utf-8") as f:
        json.dump(card, f, indent=2)
    print(card["overall"])

if __name__ == "__main__":
    main()

Futtatási parancs:

python make_model_card.py --pred pred_labels.npy \
  --corr-pred corr_pred_labels.npy --out model_card.json --data-dir data

Ellenőrzés. A JSON minden acc értéke [0,1][0,1]-ben legyen; minden n pozitív; a CI határok monotonok (lo≤hilo\le hi). A confusion mátrixok sorösszege a clean esetben osztályonként 20, a corrupt esetben ugyancsak 20. Jelölt szemléltetés, nem mért futás: a várható JSON szerkezete például

{
  "overall": {"clean_acc": 0.82, "corrupt_acc": 0.61, "paired_drop": 0.21},
  "slices": {
    "clean_class_0": {"n": 20, "acc": 0.90, "ci95": [0.80, 1.00]},
    "clean_class_1": {"n": 20, "acc": 0.75, "ci95": [0.60, 0.90]},
    "clean_class_2": {"n": 20, "acc": 0.80, "ci95": [0.65, 0.95]},
    "corrupt_all": {"n": 60, "acc": 0.61, "ci95": [0.50, 0.72]}
  },
  "failure_taxonomy": {"clean": [[18,1,1],[2,15,3],[1,3,16]]},
  "status": "illustrative values, not a measured run"
}

Értelmezés. Ha a corrupt CI nem tartalmazza a clean pontosságot, a páros romlás a definiált korrupciós eloszláson jelentős; ez nem általános domain-shift garancia. A kis csoportméretű szeletek CI-je széles; azokból nagy állítást tenni tilos.

23.4.7 Feladatok

1. F1 fogalmi, nehézség 1. Definiálja a szelet pontosságát, és indokolja, miért nem jelentünk értéket üres szeletre. Adjon egy üres szelet példát a GlyphLab teszt metadata alapján. Átadandó: képlet, az üres eset szabálya, egy konkrét üres mask.

2. F2 levezetési/bizonyítási, nehézség 2. Bizonyítsa be a fenti Állítást, és adja meg az egyenlőség pontos feltételét. Ellenőrizze a N=100N=100, |S|=5|S|=5 példán, hogy a globális pontosság maximuma 0,950{,}95. Átadandó: teljes bizonyítás és a maximum indoklása.

3. F3 implementációs, nehézség 2. Írjon NumPy függvényt slice_accuracy(y_true, y_pred, mask) néven, amely üres szeletre (0, 0, None) értéket ad. Számítsa ki a kézi példa mindhárom sorát 64 bites egészekkel. Átadandó: kód, futtatási parancs, a három sor kimenete.

4. F4 hibakeresési, nehézség 2. Az alábbi függvény hibás:

def slice_accuracy_bad(y_true, y_pred, mask):
    return np.mean(y_true == y_pred)  # a mask elveszik

Lokalizálja a hibát, írja le a következményét a modellkártyán, javítsa, és a kézi példán mutassa meg, hogy a helyes függvény a corrupt szeletre 0,0-t ad, a hibás pedig 0,8-at. Átadandó: ok, következmény, javított kód, ellenőrző futtatás kimenete.

5. F5 kísérleti/transzfer, nehézség 3. Tervezzen szeletelemzést a GlyphLab test view_id értékei szerint (0 és 1 nézet). Adja meg a megfigyelési egységet, a bootstrap mintavételi egységet, a baseline-t, a metrikát és a következtetési korlátot. Átadandó: féloldalas protokoll, amely kitér arra, hogy a két view ugyanazon scene-hez tartozik.

23.4.8 Megoldások és indoklás

1. A szelet pontossága acc(S)=1|S|∑i∈S𝟙[ŷi=yi]\mathrm{acc}(S)=\frac1{|S|}\sum_{i\in S}\mathbb{1}[\hat y_i=y_i], |S|>0|S|>0. Üres szeletre a nevező nulla, a tört definiálatlan; a 0-val jelölés hamis „minden rossz” állítást sugallna. Üres szelet a GlyphLab-ban például a split == "train" mask a test predikciókra; vagy egy nem létező scene_id szűrés, például metadata.scene_id == 999 a testben.

2. Az Állítás bizonyítása a fenti levezetés. Egyenlőség pontosan akkor, ha ED=|S|E_D=|S|, azaz D\SD\setminus S minden eleme helyes. A 100/5100/5 példán: a hibaszám legalább 5, ezért a pontosság legfeljebb 1−5/100=0,951-5/100=0{,}95; ez elérhető, ha a maradék 95 mind helyes, tehát a maximum pontosan 0,950{,}95.

3. A kézi példa függvénye a fenti kód. Az np.int64 miatt a shape (10,)(10,); a kimenet:

clean   : (8, 8, 1.0)
corrupt : (2, 0, 0.0)
overall : 0.8

4. A hiba oka: a függvény nem alkalmazza a maskot, így a slice-ra vonatkozó kérdésre a teljes minta pontosságát adja. Következmény: a modellkártya minden szeletsorában ugyanaz a globális érték jelenik meg, a kis hibás szelet eltűnik. Javítás a 3. feladat slice_accuracy függvénye. A kézi példán a hibás függvény a corrupt maskra is 0,80{,}8-at ad, a helyes pedig (2,0,0.0)(2,0,0.0)-t.

5. Protokollváz: split: a rögzített test 60 képe; megfigyelési egység: kép, de a bootstrap mintavételi egység scene_id, osztályonként rétegezve (osztályonként 10 scene visszatevéssel). Csoportosítás: view_id == 0 és view_id == 1, mindkét csoport mérete 30. Baseline: a clean teszt teljes pontossága. Metrika: szeletpontosság és scene-bootstrap 95% CI; a két view összehasonlítása páros, mert minden scene mindkét view-t tartalmazza, ezért a view-különbségre scene-szintű páros bootstrap vagy előjel-teszt kell. Következtetési korlát: ha a CI-k átfednek, nem állítunk view-függő különbséget; a view nem új független eloszlás, hanem ugyanazon jelenet két zajrealizációja.

23.4.9 Tipikus tévedés és önellenőrzés

Tipikus tévedés. Kis szeletből nagy általános állítást tenni. Ellenpélda: egy 5 elemű corrupt szeleten mért 0% pontosságból azt állítani, hogy „a modell használhatatlan a corrupt környezetben”. A scene-bootstrap 95% CI ilyen kis csoporton rendkívül széles lehet; egy másik, független corrupt mintán a pontosság lehet 80% is. A kis minta nagy varianciájú becslést ad; a korrekt állítás a CI-vel együtt fogalmazható meg: „a pontosság becslése 0, de a 95% CI [0, 60%], ezért a használhatatlanság nem igazolt”.

Önellenőrzés. (1) A szelet definíciója a teszt indexeire épül-e, nem a tanítóra? (2) Üres szelet esetén None szerepel-e? (3) A CI scene-szintű bootstrapből származik-e? (4) A corrupt kiértékelés a befagyasztott modellen, modellválasztás után történt-e? Ha bármelyik nem, a modellkártya állítása nem érvényes.

Visszahívás. A 23.3. Páros összehasonlítás és csoportszintű intervallum bootstrap egysége a scene_id volt, osztályonkénti rétegzéssel; ezt a szeletek CI-jében is megtartottuk. A 22.4. Augmentációs kísérlet és domainváltozás clean/corrupt eloszlás-megkülönböztetése adja a környezeti szeletet. A 11.4. Egy teljes PyTorch tanítási iteráció logit–döntés szerződése a döntési határ definíciójának alapja.

23.4.10 Rövid összefoglaló

Az összesített pontosság a szeletek feletti súlyozott átlag; jó globális érték mellett is létezhet nulla pontosságú kis szelet, mert a hiba koncentrálódhat. A modellkártya ezért clean/korrupt, osztály- és nézetszintű bontást, csoportméreteket és scene-rétegzett bootstrap intervallumokat közöl. A kis csoportból levont állítás a széles CI miatt bizonytalan. A corrupt teszt csak a befagyasztott modell kiválasztása után értékelhető, és a shift csupán az adott korrupciós eloszlást reprezentálja. A hibataxonómia a hibákat cella, környezet és csoport szerint rendezi; a döntési határ közelsége heurisztika, nem bizonyított ok-okozat.

A következő 24.1. Tanulási görbe és hibadiagnózis egység a tanulási görbét és a hibadiagnózist vizsgálja; az itteni szeleteredmények ott azt segítik eldönteni, hogy a hiba adat-, modell- vagy optimalizálási eredetű-e.

23.4.11 Felhasznált források