38.3 Végső teszt és páros korrupció

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

A 38.2. Modell, training és best checkpoint egység a validation loss alapján választotta ki a legjobb checkpointot, és rögzítette a modellarchitektúrát, az inicializálást és a tanítási protokollt. Itt ezt a befagyasztott modellt egyszer, a zárt tesztkészleten értékeljük. A modell a GlyphLab háromosztályos orientációfeladatán tanult: 00 = vertical, 11 = horizontal, 22 = diagonal. A teszt 30 független jelenetből áll, jelenetenként 2 zajos nézettel, tehát 60 kép; osztályonként 10 jelenet.

Előismeret: a 38.2. Modell, training és best checkpoint checkpoint formátuma, a 37.3. Linearitás, adjungált és határfeltétel egységből a modell determinisztikus kiértékelési szerződése, és a 34.3. Pruning és struktúra egységből az a szabály, hogy a betöltött state_dict kulcsainak pontosan egyezniük kell a deklarált architektúrával. A 26.3. NMS, confidence és AP egységben látott posztprocesszt itt nem használjuk, mert osztályozásnál az argmax a teljes kiértékelés.

Induló kérdés: ha a korrupt teszt ugyanazokat a jeleneteket tartalmazza, mint a clean teszt, miért hibás a két 60 képes mintát függetlenként kezelni?

38.3.2 Definíciók és a kiértékelési szerződés

Jelölje a teszt jeleneteit s=1,…,Ss=1,\ldots,S, ahol S=30S=30. Minden jelenetnek V=2V=2 nézete van; a vv-edik nézet képe Xs,v∈ℝ1×24×24X_{s,v}\in\mathbb R^{1\times24\times24}, float32, [0,1][0,1]. A jelenet osztálycímkéje cs∈{0,1,2}c_s\in\{0,1,2\}; mindkét nézet ehhez a címkéhez tartozik. A befagyasztott modell fθ:ℝ1×24×24→ℝ3f_\theta\colon\mathbb R^{1\times24\times24}\to\mathbb R^{3} logitokat ad, és a predikció

ŷs,v=argmaxk∈{0,1,2}fθ(Xs,v)k.\hat y_{s,v}=\arg\max_{k\in\{0,1,2\}}\,f_\theta(X_{s,v})_k .

A modell kiértékelés előtt eval() módban van, és a következtetés torch.no_grad() alatt fut, tehát a kimenet determinisztikus a paraméterezés szerint; nincs dropout- vagy BatchNorm-training véletlenség (a 37.3. Linearitás, adjungált és határfeltétel szerződésének megfelelően).

1. definíció (per-scene accuracy). A ss jelenetre számított accuracy

as=12∑v=01𝟏[ŷs,v=cs]∈{0,12,1}.a_s=\frac{1}{2}\sum_{v=0}^{1}\mathbf 1[\hat y_{s,v}=c_s]\;\in\;\{0,\tfrac12,1\} .

2. definíció (clean accuracy, group average, macro). A clean teszt per-image accuracyje

Accimg=130∑s=130as=160∑s=130∑v=01𝟏[ŷs,v=cs].\mathrm{Acc}_{img}=\frac{1}{30}\sum_{s=1}^{30}a_s=\frac{1}{60}\sum_{s=1}^{30}\sum_{v=0}^{1}\mathbf 1[\hat y_{s,v}=c_s] .

A group average a jelenet-accuracyk átlaga, acc¯scene=130∑sas\overline{\mathrm{acc}}_{scene}=\frac{1}{30}\sum_s a_s. A kk osztályra vett accuracy

Acck=120∑s:cs=k∑v=01𝟏[ŷs,v=k],\mathrm{Acc}_k=\frac{1}{20}\sum_{s:c_s=k}\sum_{v=0}^{1}\mathbf 1[\hat y_{s,v}=k],

és a macro accuracy Accmacro=13∑k=02Acck\mathrm{Acc}_{macro}=\frac13\sum_{k=0}^2\mathrm{Acc}_k. A group average és a per-image accuracy itt egyenlő, mert minden jelenetnek pontosan két nézete van; ezt alább lemmaként igazoljuk. A macro ezen a teszten pontosan egyenlő a per-image accuracyvel: mivel minden osztályhoz nk=20n_k=20 kép tartozik, Accmacro=13∑kck20=∑kck60=Accimg\mathrm{Acc}_{macro}=\frac13\sum_k\frac{c_k}{20}=\frac{\sum_kc_k}{60}=\mathrm{Acc}_{img}, ahol ckc_k a kk osztály helyes predikcióinak száma. A két összesítő csak kiegyensúlyozatlan osztálygyakoriságnál térhet el. A gyenge osztályt ezért itt nem a macro, hanem az osztályonkénti recall és a konfúziós mátrix mutatja meg, amelyet a labor külön kiír.

3. definíció (páros korrupciós különbség). A corrupted_test.npy a metadata teszt-sorainak sorrendjében tartalmazza ugyanazon jelenet–nézet párok zajos és fényességgradienssel rontott változatát, X̃s,v\tilde X_{s,v}-t. A korrupt jelenet-accuracy ãs=12∑v𝟏[argmaxkfθ(X̃s,v)k=cs]\tilde a_s=\frac12\sum_v\mathbf 1[\arg\max_k f_\theta(\tilde X_{s,v})_k=c_s], a páros különbség

ds=as−ãs∈{−1,−12,0,12,1},d‾=130∑s=130ds.d_s=a_s-\tilde a_s\;\in\;\{-1,-\tfrac12,0,\tfrac12,1\},\qquad \bar d=\frac{1}{30}\sum_{s=1}^{30}d_s .

A pozitív d‾\bar d azt jelenti, hogy a korrupció átlagosan rontja a jelenetszintű pontosságot; a párosság a jelenet nehézségét közös zajként tünteti el.

4. definíció (stratified scene bootstrap). Legyen Ik={s:cs=k}I_k=\{s:c_s=k\} a kk osztályú jelenetek indexhalmaza, |Ik|=10|I_k|=10. Adott BB replikációhoz és α\alpha-hoz:

  1. b=1,…,Bb=1,\ldots,B esetén minden k∈{0,1,2}k\in\{0,1,2\} osztályból húzzunk visszatevéssel |Ik|=10|I_k|=10 jelenetindexet az IkI_k halmazból; a három osztály húzásait összefűzve kapjuk az I(b)I^{(b)} bootstrap-mintát.
  2. Számítsuk ki a replikált statisztikát: d‾(b)=130∑s∈I(b)ds\bar d^{(b)}=\frac{1}{30}\sum_{s\in I^{(b)}}d_s.
  3. A percentilis intervallum [qα/2,q1−α/2][q_{\alpha/2},q_{1-\alpha/2}], ahol qpq_p a {d‾(1),…,d‾(B)}\{\bar d^{(1)},\ldots,\bar d^{(B)}\} pp-kvantilise.

Ez az intervallum feltételes a megfigyelt tesztadatra: azt közelíti, hogyan változna d‾\bar d, ha a teszt jeleneteit az osztályon belül újrahúznánk. Nem bizonyítunk általános bootstrap-konzisztenciatételt; a könyv szintjén ez heurisztikus újramintavételi eljárás, amelynek feltételes, ismételt mintavételi értelmezése az [S02] forrásra támaszkodik. A rétegzés biztosítja, hogy a bootstrap-mintában is osztályonként 10 jelenet marad.

38.3.3 Két igazolható azonosság

Lemma 1. Ha minden jelenetnek pontosan VV nézete van, akkor a group average megegyezik a per-image accuracyvel.

Bizonyítás. Legyen S=30S=30, V=2V=2. Ekkor

acc¯scene=1S∑s=1S1V∑v=1V𝟏[ŷs,v=cs]=1SV∑s=1S∑v=1V𝟏[ŷs,v=cs]=Accimg.\overline{\mathrm{acc}}_{scene}=\frac1S\sum_{s=1}^S\frac1V\sum_{v=1}^V\mathbf 1[\hat y_{s,v}=c_s]=\frac1{SV}\sum_{s=1}^S\sum_{v=1}^V\mathbf 1[\hat y_{s,v}=c_s]=\mathrm{Acc}_{img}.

A második egyenlőségnél a konstans 1V\frac1V kiemelhető, a harmadik az Accimg\mathrm{Acc}_{img} definíciója, mert az összes kép száma SVSV. Ha a jelenetek nézetszáma eltérne, az azonosság általában nem áll fenn; ez az F1 feladat ellenpéldája.

Lemma 2. A stratified scene bootstrap minden replikációban osztályonként pontosan |Ik||I_k| jelenetet tartalmaz, és a bootstrap-minta várható összetétele az eredeti osztályarányokat tükrözi.

Bizonyítás. Az algoritmus definíció szerint minden kk-ra külön, visszatevéses húzást végez a rögzített IkI_k halmazból, pontosan |Ik||I_k| alkalommal. Ezért az I(b)I^{(b)}-beli kk osztályú elemek száma minden bb-re |Ik||I_k|. A várható osztályarány triviálisan |Ik|/∑j|Ij||I_k|/\sum_j|I_j|, mert a húzásszámok rögzítettek.

A bootstrap becslés várható értékének centráltságát az F2 feladatban kérjük egy konkrét, kis BB melletti eloszláson.

38.3.4 Kézi kis példa: négy jelenet és egzakt bootstrap

A teljes labor előtt egy 4 jelenetből, 2 osztályból álló példán kézzel számolunk. Az osztályok: {s1,s2}\{s_1,s_2\} a 0, {s3,s4}\{s_3,s_4\} az 1 osztály. A két nézetre adott predikciók:

jelenet ss osztály csc_s asa_s ãs\tilde a_s dsd_s
s1s_1 0 1 0.5 0.5
s2s_2 0 0.5 0 0.5
s3s_3 1 1 0.5 0.5
s4s_4 1 0.5 1 −0.5

A clean per-image accuracy: (1+0.5+1+0.5)/4=0.75(1+0.5+1+0.5)/4=0.75; a group average ugyanaz, mert V=2V=2. A macro: osztályonként (1+0.5)/2=0.75(1+0.5)/2=0.75 és (1+0.5)/2=0.75(1+0.5)/2=0.75, tehát Accmacro=0.75\mathrm{Acc}_{macro}=0.75. A korrupt group average (0.5+0+0.5+1)/4=0.5(0.5+0+0.5+1)/4=0.5, és d‾=0.75−0.5=0.25\bar d=0.75-0.5=0.25; a páros átlag (0.5+0.5+0.5−0.5)/4=0.25(0.5+0.5+0.5-0.5)/4=0.25, egyezik, ahogy a Lemma 1 mindkét feltételre garantálja.

A stratified bootstrap egzakt eloszlása ezen a kis példán kiszámítható. A 0 osztály mindkét húzása a {0.5,0.5}\{0.5,0.5\} halmazból mindig 0.5+0.5=1.00.5+0.5=1.0 összeget ad. Az 1 osztályban a {0.5,−0.5}\{0.5,-0.5\} halmazból visszatevéssel húzott két elem összege 1.01.0, 0.00.0 vagy −1.0-1.0, rendre 14,12,14\frac14,\frac12,\frac14 valószínűséggel. A négyelemű bootstrap-minta átlaga ezért

d‾(b)=1.0+(1. osztály összege)4,\bar d^{(b)}=\frac{1.0+\text{(1. osztály összege)}}4,

amelynek eloszlása: P(d‾(b)=0.5)=14P(\bar d^{(b)}=0.5)=\frac14, P(d‾(b)=0.25)=12P(\bar d^{(b)}=0.25)=\frac12, P(d‾(b)=0)=14P(\bar d^{(b)}=0)=\frac14. A 95%-os percentilis intervallum nagy BB esetén [0,0.5][0,0.5]. Ez a kézi eredmény futtatható ellenőrzésként szolgál a labor kódjához: a kódnak B=2000B=2000-rel megközelítőleg a [0,0.5][0,0.5] intervallumot kell visszaadnia.

38.3.5 Labor: befagyasztott modell, clean teszt és páros korrupció

Munkakönyvtár: lab/. A bemenet a 38.2. Modell, training és best checkpoint python run_project2.py futásának két választott checkpointja: checkpoints/best_val_cnn.pt és checkpoints/best_val_linear.pt. Mindkettő a 38.2. Modell, training és best checkpoint checkpointsémáját követi (model_state_dict, arch, seed, epoch, val_loss, params), és a szkript ugyanazzal a glyph_models.load_checkpoint függvénnyel tölti be őket, amellyel a 38.2. Modell, training és best checkpoint a mentést ellenőrizte: weights_only=True módon, szigorú kulcsegyezéssel, kézi átnevezés nélkül ([D09]). A 38.1. Adat, split és előzetes protokoll fő kérdése miatt a lineáris baseline is egyszer, ugyanazon a teszten értékelődik.

# lab/eval_test.py — n151: egyszeri teszt a befagyasztott checkpointokon.
import json, os
import numpy as np
import pandas as pd
import torch

from glyph_models import load_checkpoint

SEED = 20261020
B = 2000
ALPHA = 0.05
torch.manual_seed(SEED)
os.makedirs("results", exist_ok=True)     # a kimeneti könyvtár minden mentés előtt létezik

# --- adat: NCHW float32, int64 címkék; a korrupt teszt a metadata test-sorrendjében
X = torch.from_numpy(np.load("data/images.npy")).to(torch.float32)
labels = np.load("data/labels.npy").astype(np.int64)
X_corrupt = torch.from_numpy(np.load("data/corrupted_test.npy")).to(torch.float32)
meta = pd.read_csv("data/metadata.csv")
test_idx = meta[meta["split"] == "test"].index.to_numpy()          # (60,)
assert (meta["index"].to_numpy() == np.arange(len(meta))).all()
test_meta = meta.iloc[test_idx].reset_index(drop=True)
y_test = labels[test_idx]                                          # (60,)

# --- a n150 kimenete kézi átnevezés nélkül: ugyanaz a séma és ugyanaz a modellmodul
cnn, cnn_ckpt = load_checkpoint("checkpoints/best_val_cnn.pt")
lin, lin_ckpt = load_checkpoint("checkpoints/best_val_linear.pt")

@torch.no_grad()
def predict(net, batch):
    return torch.argmax(net(batch), dim=1).numpy().astype(np.int64)  # logit -> argmax

def per_scene_accuracy(pred, y, rows_meta):
    """Jelenetenkénti accuracy; a scene_id és az osztály ugyanabban a sorrendben."""
    groups = rows_meta.groupby("scene_id", sort=True).indices        # scene_id -> sorok
    scene_ids = list(groups)
    acc = np.array([np.mean(pred[r] == y[r]) for r in groups.values()], dtype=np.float64)
    class_ids = np.array([y[r][0] for r in groups.values()], dtype=np.int64)
    assert all(len(set(y[r])) == 1 for r in groups.values())       # egy jelenet, egy címke
    return scene_ids, acc, class_ids

# kis kontroll: két jelenet, két-két nézet -> hossz 2
_m = pd.DataFrame({"scene_id": ["b", "a", "b", "a"]})
_s, _a, _c = per_scene_accuracy(np.array([0, 1, 1, 1]), np.array([0, 1, 0, 1]), _m)
assert _s == ["a", "b"] and _a.tolist() == [1.0, 0.5] and _c.tolist() == [1, 0]

def scene_bootstrap_ci(d, class_ids, B, alpha, seed):
    rng = np.random.default_rng(seed)
    pools = [np.where(class_ids == k)[0] for k in np.unique(class_ids)]
    reps = np.array([d[np.concatenate([rng.choice(p, size=len(p), replace=True)
                                       for p in pools])].mean() for _ in range(B)])
    lo, hi = np.quantile(reps, [alpha / 2, 1 - alpha / 2])
    return float(lo), float(hi)

report = {"checkpoints": {"cnn": {k: cnn_ckpt[k] for k in ("seed", "epoch", "val_loss", "params")},
                          "linear": {k: lin_ckpt[k] for k in ("seed", "epoch", "val_loss", "params")}}}
scene = {}
for name, net in (("cnn", cnn), ("linear", lin)):
    p_clean, p_corrupt = predict(net, X[test_idx]), predict(net, X_corrupt)
    sids, a_clean, class_ids = per_scene_accuracy(p_clean, y_test, test_meta)
    _, a_corrupt, _ = per_scene_accuracy(p_corrupt, y_test, test_meta)
    assert a_clean.shape == (30,) and set(np.unique(a_clean)) <= {0.0, 0.5, 1.0}
    d = a_clean - a_corrupt
    assert np.isclose(d.mean(), np.mean(p_clean == y_test) - np.mean(p_corrupt == y_test))
    scene[name] = a_clean
    report[name] = {
        "clean_img": float(np.mean(p_clean == y_test)),
        "corrupt_img": float(np.mean(p_corrupt == y_test)),
        "macro": float(np.mean([np.mean(p_clean[y_test == k] == k) for k in range(3)])),
        "paired_mean": float(d.mean()),
        "paired_ci": scene_bootstrap_ci(d, class_ids, B, ALPHA, SEED),
        "scene_ids": sids, "a_clean": a_clean.tolist(), "a_corrupt": a_corrupt.tolist()}
    # kiegyensúlyozott teszt (20-20-20): a macro pontosan a per-image accuracy
    assert np.bincount(y_test).tolist() == [20, 20, 20]
    assert abs(report[name]["macro"] - report[name]["clean_img"]) < 1e-12
    report[name]["confusion_clean"] = [[int(np.sum((y_test == a) & (p_clean == b))) for b in range(3)]
                                       for a in range(3)]          # sor: valódi, oszlop: predikált
    if name == "cnn":
        np.save("results/n151_pred_clean.npy", p_clean)
        np.save("results/n151_pred_corrupt.npy", p_corrupt)
        with torch.no_grad():                    # a nyers logitok is a riport részei
            np.save("results/n151_logits_clean.npy", net(X[test_idx]).numpy().astype(np.float32))
        np.save("results/n151_test_index.npy", test_idx.astype(np.int64))
        err = np.where(p_clean != y_test)[0]
        report["cnn"]["failures"] = [[int(j), str(test_meta.scene_id[j]),
                                      int(test_meta.view_id[j]), int(y_test[j]), int(p_clean[j])]
                                     for j in err[:5]]

# a n149 fő kérdése: CNN clean accuracy vs. lineáris baseline, jelenetszintű páros CI
diff = scene["cnn"] - scene["linear"]
report["cnn_minus_linear"] = {"mean": float(diff.mean()),
                              "ci": scene_bootstrap_ci(diff, class_ids, B, ALPHA, SEED)}
with open("results/n151_report.json", "w") as f:
    json.dump(report, f, indent=2)
for name in ("cnn", "linear"):
    r = report[name]
    print(f"{name}: clean {r['clean_img']:.3f}  corrupt {r['corrupt_img']:.3f}  "
          f"macro {r['macro']:.3f}  clean-corrupt {r['paired_mean']:.3f} "
          f"CI [{r['paired_ci'][0]:.3f}, {r['paired_ci'][1]:.3f}]")
c = report["cnn_minus_linear"]
print(f"cnn - linear (clean): {c['mean']:.3f} CI [{c['ci'][0]:.3f}, {c['ci'][1]:.3f}]")
print("cnn failures (row, scene, view, truth, pred):", report["cnn"]["failures"])
print("cnn confusion (clean):", report["cnn"]["confusion_clean"])

A per_scene_accuracy a jelenetek számával egyező hosszú tömböt ad vissza, és vele együtt, ugyanabban a sorrendben, a scene_id-ket és a jelenetek osztályát. A szkript eleji kis kontroll (két jelenet, két-két nézet) ezt ellenőrzi; a főúton a kimenet (30,) alakú, minden értéke a {0;0,5;1}\{0;\ 0{,}5;\ 1\} halmazban van, és a dd átlaga egyezik a per-image clean és corrupt accuracy különbségével (ez a Lemma 1 következménye, a kód assert-tel ellenőrzi).

A futás parancsa a lab/ könyvtárból: python eval_test.py. Futási jegyzőkönyv (az eredeti GlyphLab-adat, a 38.2. Modell, training és best checkpoint fenti futásának checkpointjai, CPU, PyTorch 2.14.1+cpu; két futás azonos kimenetet adott):

cnn: clean 1.000  corrupt 0.733  macro 1.000  clean-corrupt 0.267 CI [0.167, 0.383]
linear: clean 0.217  corrupt 0.250  macro 0.217  clean-corrupt -0.033 CI [-0.133, 0.067]
cnn - linear (clean): 0.783 CI [0.667, 0.900]
cnn failures (row, scene, view, truth, pred): []
cnn confusion (clean): [[20, 0, 0], [0, 20, 0], [0, 0, 20]]

Az eredményértelmezés négy rétege: (1) a CNN clean accuracyje a befagyasztott modell egyszeri, választás utáni tesztje, nem új modellválasztási kritérium; itt mind a 60 tesztkép helyes, ami a 30 jelenetes, szintetikus teszten nem jelent hibátlan általánosítást. (2) A macro itt definíció szerint egyenlő a clean accuracyvel (kiegyensúlyozott osztályok); az osztályonkénti képet a konfúziós mátrix adja, amely a CNN-nél diagonális (minden osztály 20/20). (3) A páros bootstrap intervallum a korrupció hatásának jelenetszintű bizonytalanságát adja: a [0,167;0,383][0{,}167;\ 0{,}383] intervallum nem tartalmazza a nullát, tehát a megfigyelt 30 jeleneten a korrupció konzisztensen ront; ez feltételes állítás a 30 jelenetre, nem általánosít a GlyphLabon túlra. (4) A 38.1. Adat, split és előzetes protokoll fő kérdésére a CNN és a lineáris baseline jelenetenkénti clean accuracyjének páros különbsége felel: 0,7830{,}783, intervalluma [0,667;0,900][0{,}667;\ 0{,}900]. A lineáris baseline a véletlen szint körül marad (clean 0,2170{,}217), és a korrupció sem ront rajta érdemben; ami nincs, azt a zaj sem rontja el.

38.3.6 Gyakori tévedés: a korrupt teszt nem független minta

A korrupt változat ugyanazon 30 jelenet és ugyanazon 60 nézet zajos változata. Aki a clean és korrupt 60 képet két független mintaként kezeli, és külön image-szintű bootstrapet vagy kétmintás tesztet futtat, az a jelenetek közös nehézségét figyelmen kívül hagyja. A helyes egység a jelenet, és a helyes összehasonlítás a dsd_s páros különbség.

Ellenpélda az unpaired elemzésre. Vegyünk négy jelenetet az alábbi clean és korrupt jelenet-accuracykkal:

jelenet asa_s ãs\tilde a_s dsd_s
1 1 0.5 0.5
2 1 0.5 0.5
3 0.5 0 0.5
4 0.5 0 0.5

A páros dsd_s minden jelenetre ugyanaz a 0.50.5, ezért a páros bootstrap eloszlása degenerált: minden replikáció átlaga 0.50.5, az intervallum [0.5,0.5][0.5,0.5], amely nem tartalmazza a nullát. Ha viszont valaki a két feltételt függetlennek tekinti, és a négy clean meg négy korrupt jelenet-accuracyt két független mintaként hasonlítja össze, akkor a clean minta (1,1,0.5,0.5)(1,1,0.5,0.5) és a korrupt (0.5,0.5,0,0)(0.5,0.5,0,0) szórása 0.250.25 körüli, a kétmintás Welch-féle közelítés standard hibája nagy, és a különbség konfidencia-intervalluma tartalmazhatja a nullát. A tévedés következménye: egy valójában minden jeleneten azonos, konzisztens romlást nem ismerünk fel, mert a jelenetek eltérő alapnehézségét az unpaired eljárás zajként, nem pedig közös hatásként kezeli. A páros összehasonlítás ezért nem stilisztikai választás, hanem a GlyphLab adatszerződéséből következő követelmény.

38.3.7 Feladatok

F1 (fogalmi, nehézség 1). Definiálja külön a per-image és a per-scene átlagos accuracyt, és adjon olyan 2 jelenetes példát, ahol az egyik jelenetnek 2, a másiknak 3 nézete van, és a két mennyiség nem egyenlő. Átadandó: definíció, a példa táblázata, mindkét érték kiszámítva, és egy mondat arról, mikor egyenlők.

F2 (levezetési, nehézség 2). A kézi példa 4 jelenetére és a stratified bootstrap egzakt eloszlására igazolja, hogy a bootstrap-átlag várható értéke egyenlő a megfigyelt d‾=0.25\bar d=0.25-tel. Átadandó: a {d‾(b)}\{\bar d^{(b)}\} eloszlás felsorolása, a várható érték levezetése, és annak ellenőrzése, hogy a percentilis intervallum [0,0.5][0,0.5] tényleg a definíció szerinti 95%-os intervallum.

F3 (implementációs, nehézség 2). Írjon stratified_scene_bootstrap(d, class_ids, B, alpha, seed) függvényt, amely a fenti 3 osztályos esetre visszaadja a percentilis intervallumot; a függvény ne importáljon külső statisztikai csomagot, csak NumPy-t. Ellenőrizze a kézi példán B=2000B=2000, seed=20261020 paraméterrel; a visszaadott intervallum közelítse a [0,0.5][0,0.5] értéket ±0.1 toleranciával. Átadandó: futtatható kód, seed, az ellenőrző hívás és a kapott intervallum.

F4 (hibakeresési, nehézség 2). Az alábbi kódrészlet a clean–korrupt különbség konfidenciaintervallumát akarja számolni, de unpaired módon:

def unpaired_diff_ci(clean_acc, corrupt_acc, alpha=0.05):
    n = len(clean_acc)
    diff = clean_acc.mean() - corrupt_acc.mean()
    se = (clean_acc.std(ddof=1)**2 / n +
          corrupt_acc.std(ddof=1)**2 / n) ** 0.5
    z = 1.96
    return diff - z*se, diff + z*se

Lokalizálja a hibát a négy jelenetes példán, adja meg a téves következményt, és írja át páros dsd_s-alapú bootstrapre. Átadandó: a hiba oka, a példán a helytelen és a helyes intervallum, valamint a javított kód.

F5 (kísérlettervezési, nehézség 3). Tervezzen protokollt egy új, nem a GlyphLab csomagban lévő korrupció, például erős Gauss-elmosás hatásának vizsgálatára ugyanezen a 30 tesztjeleneten. Határozza meg a nullhipotézist, a metrikát, a bootstrap egységet, a replikációszámot és azt a döntési szabályt, amellyel a nullát elutasítja. Adjon egy érvényességi korlátot arra, hogy a következtetés miért nem általánosítható új jelenetekre. Átadandó: protokoll szövegesen és pszeudokódban, a döntési szabály képlete, valamint a korlát indoklása.

38.3.8 Megoldások és indoklás

F1 megoldás. A per-image accuracy 1∑sVs∑s,v𝟏[ŷs,v=cs]\frac{1}{\sum_s V_s}\sum_{s,v}\mathbf 1[\hat y_{s,v}=c_s], a per-scene átlag 1S∑s1Vs∑v𝟏[ŷs,v=cs]\frac1S\sum_s\frac1{V_s}\sum_v\mathbf 1[\hat y_{s,v}=c_s]. Példa: 1. jelenet V1=2V_1=2, mindkét nézet helyes; 2. jelenet V2=3V_2=3, egy nézet helyes. Per-image: 2+12+3=35=0.6\frac{2+1}{2+3}=\frac35=0.6. Per-scene átlag: 12(1+13)=12⋅43=23≈0.667\frac12(1+\frac13)=\frac12\cdot\frac43=\frac23\approx0.667. Nem egyenlők. Egyenlők akkor, ha minden jelenet nézetszáma azonos; ez a Lemma 1.

F2 megoldás. A d‾(b)\bar d^{(b)} lehetséges értékei 0.50.5, 0.250.25, 00 rendre 14\frac14, 12\frac12, 14\frac14 valószínűséggel. Várható érték:

𝔼*[d‾(b)]=0.5⋅14+0.25⋅12+0⋅14=0.125+0.125=0.25=d‾.\mathbb E^*[\bar d^{(b)}]=0.5\cdot\tfrac14+0.25\cdot\tfrac12+0\cdot\tfrac14=0.125+0.125=0.25=\bar d.

A rendezett eloszlás kumulatív valószínűségei: P(d‾(b)≤0)=14P(\bar d^{(b)}\le 0)=\frac14, P(d‾(b)≤0.25)=34P(\bar d^{(b)}\le 0.25)=\frac34, P(d‾(b)≤0.5)=1P(\bar d^{(b)}\le 0.5)=1. A 2.5%-os kvantilis 00, mert a 00 érték valószínűsége 14>0.025\frac14>0.025; a 97.5%-os kvantilis 0.50.5, mert P(d‾(b)≤0.25)=0.75<0.975P(\bar d^{(b)}\le 0.25)=0.75<0.975, és a 0.50.5 már eléri az 1-et. Az intervallum tehát [0,0.5][0,0.5].

F3 megoldás.

import numpy as np

def stratified_scene_bootstrap(d, class_ids, B=2000, alpha=0.05, seed=20261020):
    rng = np.random.default_rng(seed)
    n_classes = int(np.max(class_ids)) + 1
    reps = np.empty(B, dtype=np.float64)
    for b in range(B):
        rows = []
        for k in range(n_classes):
            pool = np.where(class_ids == k)[0]
            rows.append(rng.choice(pool, size=len(pool), replace=True))
        reps[b] = d[np.concatenate(rows)].mean()
    return np.quantile(reps, [alpha/2, 1-alpha/2])

d = np.array([0.5, 0.5, 0.5, -0.5])
class_ids = np.array([0, 0, 1, 1])
lo, hi = stratified_scene_bootstrap(d, class_ids)
print(lo, hi)   # várt közelítőleg: 0.0 0.5

Alakellenőrzés: d alakja (S,), class_ids alakja (S,), a visszatérési érték (2,). A class_ids maximális értéke plusz egy adja az osztályok számát; a példában 2, a teljes laborban 3. A tolerancia ±0.1 a percentilisekre, mert B=2000B=2000 mellett a diszkrét eloszlás közelítése már stabil.

F4 megoldás. A hiba oka: a kód a clean_acc és corrupt_acc tömböket két független mintaként kezeli, és a jelenetek közötti szórást mindkét feltételben teljes mértékben beleszámítja a standard hibába; a páros szerkezetet elhagyja. A négy jelenetes példán a helytelen unpaired közelítés nagy szórással olyan intervallumot adhat, amely tartalmazza a nullát, pedig minden ds=0.5d_s=0.5. A helyes javítás:

def paired_bootstrap_ci(a_clean, a_corrupt, class_ids, B=2000, alpha=0.05, seed=20261020):
    d = a_clean - a_corrupt
    return stratified_scene_bootstrap(d, class_ids, B, alpha, seed)

A példán a_clean = [1,1,0.5,0.5], a_corrupt = [0.5,0.5,0,0], class_ids = [0,0,1,1]. A páros bootstrap a degenerált ds=0.5d_s=0.5 miatt minden replikációra 0.50.5-öt ad, az intervallum [0.5,0.5][0.5,0.5]; a hiba kijavítva kimutatja a konzisztens romlást.

F5 megoldás. Protokoll: nullhipotézis H0:𝔼[ds]=0H_0\colon\mathbb E[d_s]=0, ahol dsd_s a clean és az elmosott predikció jelenetszintű accuracy-különbsége. Metrika: d‾\bar d. Megfigyelési egység: jelenet, nem kép. Eljárás: az új korrupt képeket a 30 tesztjelenet mindkét nézetére előállítjuk; a befagyasztott modellt futtatjuk; kiszámítjuk dsd_s-t; stratified scene bootstrap, B=2000B=2000, α=0.05\alpha=0.05; a nullát akkor utasítjuk el, ha a percentilis intervallum nem tartalmazza a nullát. Pszeudokód:

repeat b = 1..B:
    for each class k:
        resample |I_k| scene indices from I_k with replacement
    compute mean of d_s on the resampled set
interval = (2.5%, 97.5%) quantile of the B means
reject H0 if interval excludes 0

Érvényességi korlát: az eljárás feltételes a 30 jelenetre, és azt kérdezi, hogy a megfigyelt jelenetek újramintavételezésén a hatás konzisztens-e; nem bizonyítja, hogy új, független jeleneteken is ugyanaz a hatás, mert a jelenetgeneráló eloszlásról nincs független mintánk. A következtetés ezért GlyphLab-specifikus, nem általános látási állítás.

38.3.9 Önellenőrzés és visszahívás

Az átadás előtt ellenőrizze: (1) a pred_clean és pred_corrupt alakja pontosan (60,) és int64; (2) a state_dict betöltése weights_only=True módon történt, és a kulcshalmaz egyezik a modellével ([D09]); (3) a modell eval() módban van, nincs torch.set_grad_enabled(True); (4) a korrupt sorrend a metadata teszt-sorrendjével azonos, ezért a d_s párosítás helyes; (5) a bootstrap a jeleneteken, nem a képeken resamplel, és a rétegzés osztályonként 10 jelenetet tart; (6) a végső tesztet csak a 38.2. Modell, training és best checkpoint modellválasztása után értékeltük ki.

Visszahívás a 37.3. Linearitás, adjungált és határfeltétel egységből: a konvolúciós modell kiértékelésben lineáris, ezért a predikció a paraméterezett súlyokkal determinisztikus; a 34.3. Pruning és struktúra egységből: ha a 38.2. Modell, training és best checkpoint checkpoint előtt pruning történt, a betöltött architektúrának a pruned változattal kell egyeznie, különben kulcsméret-hiba lép fel; a 26.3. NMS, confidence és AP egységből: a tesztkiértékelés előtt semmilyen küszöböt vagy modellt nem hangolunk a teszteredményre.

38.3.10 Rövid összefoglaló

A befagyasztott modell zárt tesztjén a clean accuracyt, a jelenetszintű csoportátlagot és a macro accuracyt definiáltuk; azonos nézetszám mellett a csoportátlag megegyezik a per-image accuracyvel, amit lemmaként bizonyítottunk. A korrupt teszt ugyanazon jelenet–nézet párokat tartalmazza, ezért a hatást páros dsd_s különbséggel mérjük. A rétegzett jelenet-bootstrap osztályonként visszatevéssel őrzi az osztályarányt, és feltételes percentilis intervallumot ad d‾\bar d-re. A kézi négy jelenetes példán az egzakt bootstrap-eloszlás [0,0.5][0,0.5]-öt ad, ami a labor kódjának ellenőrzési alapja. A párosság elhagyása konzisztens romlást rejthet el.

38.3.11 Következő alkalmazás és források

A 38.4. Mentett modellből független inference egység a mentett modellből független inference-t vizsgál: ott az itt előállított n151_pred_clean.npy és n151_report.json a modellcsomag részeként újra betölthető és egyezésvizsgálat alá vonható lesz. Az itt bemutatott páros és rétegzett értékelési logika a végső vizsgafeladatban is visszatér.

Felhasznált források: