25.1 Teljes képosztályozó adatfolyam

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

Ez az alfejezet a képosztályozó teljes adatfolyamát rögzíti: a nyers GlyphLab-képtől a betanított modell három logitjáig, majd a logitból képzett osztályindexig és osztálynévig. A cél nem egy újabb architektúra bemutatása, hanem az, hogy a hallgató pontosan meg tudja mondani, milyen alakkal, milyen normalizálással, milyen címkeszerződéssel és milyen kiértékelési módban halad végig egy batch. Előismeret: a 20.4. V. ellenőrző pont: indokolt CNN-választás architektúra-döntési lapja, a 22.4. Augmentációs kísérlet és domainváltozás train/eval transzformációk szétválasztása, a 23.4. Hibaszeletek és alkalmazási határ hibaszeletek és a 24.4. VI. ellenőrző pont: megbízható CNN-training megbízható training-protokoll. Visszautalunk a 13.1. Kép mint diszkrét többcsatornás jel NCHW-konvenciójára, a 21.1. Inicializálás és varianciaterjedés inicializálási varianciaterjedésére és a 24.1. Tanulási görbe és hibadiagnózis hibadiagnosztikájára.

Induló kérdés. Egy betanított modell checkpointja a három osztály egyikét adja vissza. A GlyphLab képek már float32, NCHW, [0,1][0,1] tartományúak. Mely lépések maradnak a nyers tömb betöltése után, és hány paraméterrel jár az utolsó réteg? A becslést a 3. szakasz végén ellenőrizheti.

25.1.2 2. Az osztályozó adatfolyam szerződése

Definíció (osztályozási adatfolyam). Legyen a bemeneti tér 𝒳=ℝCin×H×W\mathcal X=\mathbb R^{C_{in}\times H\times W}, a célhalmaz 𝒴={0,…,K−1}\mathcal Y=\{0,\ldots,K-1\}, ahol a GlyphLab főútján Cin=1C_{in}=1, H=W=24H=W=24, K=3K=3. Egy osztályozó adatfolyam az alábbi komponensekből áll:

A konkrét tanulmányi modell legyen

fB:Conv1→16k=3,p=1→BN16→ReLU→MaxPool2→Conv16→32k=3,p=1→BN32→ReLU→MaxPool2, f_B:\quad \text{Conv}_{1\to16}^{k=3,p=1}\to\text{BN}_{16}\to\text{ReLU}\to\text{MaxPool}_2\to \text{Conv}_{16\to32}^{k=3,p=1}\to\text{BN}_{32}\to\text{ReLU}\to\text{MaxPool}_2,

majd a head

fH:Flatten1152→Linear1152→64→ReLU→Linear64→3. f_H:\quad \text{Flatten}_{1152}\to\text{Linear}_{1152\to64}\to\text{ReLU}\to\text{Linear}_{64\to3}.

25.1.3 3. Batch-végigkövetés: betöltéstől a logitsig

Lemma (a GlyphCNN shape-lánca). A fenti modellre, ha X∈ℝN×1×24×24X\in\mathbb R^{N\times1\times24\times24}, akkor a közbülső alakok:

(N,1,24,24)→Conv1(N,16,24,24)→Pool(N,16,12,12)→Conv2(N,32,12,12)→Pool(N,32,6,6) (N,1,24,24)\xrightarrow{\ \text{Conv1}\ }(N,16,24,24)\xrightarrow{\ \text{Pool}\ }(N,16,12,12) \xrightarrow{\ \text{Conv2}\ }(N,32,12,12)\xrightarrow{\ \text{Pool}\ }(N,32,6,6)

→Flatten(N,1152)→Head(N,64)→(N,3). \xrightarrow{\ \text{Flatten}\ }(N,1152)\xrightarrow{\ \text{Head}\ }(N,64)\xrightarrow{\ }(N,3).

Bizonyítás. A 14.1. Stride, padding, dilation és outputméret alfejezetben levezetett kimeneti méret

Hout=⌊H+2p−ks⌋+1 H_{out}=\left\lfloor\frac{H+2p-k}{s}\right\rfloor+1

szerint a Conv1-re H=24H=24, p=1p=1, k=3k=3, s=1s=1, tehát Hout=24H_{out}=24; a MaxPool k=2k=2, s=2s=2, p=0p=0, tehát 1212. A Conv2-re ugyanez 12→1212\to12, a második pool 12→612\to6. A kilapítás 32⋅6⋅6=115232\cdot6\cdot6=1152 skalárt ad mintánként, a lineáris rétegek pedig a mátrixszorzás definíciójából (N,1152)→(N,64)(N,1152)\to(N,64) és (N,64)→(N,3)(N,64)\to(N,3) alakot. ▫\square

A modell paraméterszáma, K=3K=3 esetén:

Réteg Paraméterek
Conv2d(1,16,3) + bias 16⋅9+16=16016\cdot9+16=160
BatchNorm2d(16) 3232
Conv2d(16,32,3) + bias 16⋅32⋅9+32=464016\cdot32\cdot9+32=4640
BatchNorm2d(32) 6464
Linear(1152,64) + bias 1152⋅64+64=737921152\cdot64+64=73792
Linear(64,3) + bias 64⋅3+3=19564\cdot3+3=195

Összesen 7888378\,883 tanítható skalár. Ez a kis modell a GlyphLab N=240N=240 tanítóképére szánt, előtanítás nélküli főút; a TorchVision előtanított modelljei ([D13]) itt nem szükségesek.

25.1.4 4. Kézi kis példa és ellenőrzés

A kézi ellenőrzéshez nem kell adathalmazt betölteni: a modellt véletlen inputon definiáljuk, és assert-ekkel rögzítjük az alakokat.

# lab/model_def.py
import torch
import torch.nn as nn

class GlyphCNN(nn.Module):
    def __init__(self, num_classes: int = 3):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 16, 3, padding=1), nn.BatchNorm2d(16), nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
            nn.MaxPool2d(2),
        )
        self.head = nn.Sequential(
            nn.Linear(32 * 6 * 6, 64), nn.ReLU(), nn.Linear(64, num_classes),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.head(torch.flatten(self.features(x), 1))
# A lab/ munkakönyvtárból:
python - <<'PY'
import torch
from model_def import GlyphCNN
torch.manual_seed(20261020)
model = GlyphCNN()
x = torch.randn(8, 1, 24, 24, dtype=torch.float32)
feat = model.features(x)
z = model(x)
print(feats.shape)   # torch.Size([8, 32, 6, 6])
print(z.shape)       # torch.Size([8, 3])
print(sum(p.numel() for p in model.parameters()))  # 78883
assert feat.shape == (8, 32, 6, 6)
assert z.shape == (8, 3)
assert sum(p.numel() for p in model.parameters()) == 78883
print("shape check passed")
PY

A fenti kód futtatása a hallgató gépén ellenőrzi a lemmát; ebben a kéziratban a shape check passed sor számított, nem mért output.

25.1.5 5. Labor: GlyphLab osztályozó és új inputra inference-parancs

A labor a lab/ munkakönyvtárban fut, a GlyphLab eredeti adatfájljaival. A tanítási script betölti a képeket és a célokat, a metadata.csv split oszlopa alapján train/val indexeket képez, a train splitet jelenetszintű felosztásként kezeli, és a best checkpoint mentésekor rögzíti a label mappinget is.

# lab/train_classifier.py
import numpy as np, pandas as pd
import torch
from torch.utils.data import DataLoader, TensorDataset
from model_def import GlyphCNN

torch.manual_seed(20261020)
device = torch.device("cpu")
X = np.load("data/images.npy")          # (360,1,24,24) float32
y = np.load("data/labels.npy")          # (360,) int64
meta = pd.read_csv("data/metadata.csv")
tr = meta.index[meta.split == "train"].to_numpy()
va = meta.index[meta.split == "val"].to_numpy()

Xt, yt = torch.from_numpy(X[tr]), torch.from_numpy(y[tr])
Xv, yv = torch.from_numpy(X[va]), torch.from_numpy(y[va])
dl = DataLoader(TensorDataset(Xt, yt), batch_size=32, shuffle=True)

model = GlyphCNN().to(device)
opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-4)
lossf = torch.nn.CrossEntropyLoss()

for epoch in range(20):
    model.train()
    for xb, yb in dl:
        xb, yb = xb.to(device), yb.to(device)
        opt.zero_grad()
        loss = lossf(model(xb), yb)
        loss.backward()
        opt.step()
    model.eval()
    with torch.inference_mode():
        vacc = (model(Xv.to(device)).argmax(1) == yv.to(device)).float().mean().item()
    print(f"epoch {epoch+1:02d} val_acc={vacc:.4f}")

torch.save({
    "model": model.state_dict(),
    "arch": "GlyphCNN",
    "input_schema": "float32 NCHW 1x24x24 [0,1]",
    "labels": {0: "vertical", 1: "horizontal", 2: "diagonal"},
}, "glyph_cls.pt")
print("saved glyph_cls.pt")

Futtatás:

cd lab
python train_classifier.py

A val_acc sorok a hallgató saját futtatásában keletkeznek; itt nem állítunk mért értéket. A végső tesztet a protokoll szerint csak a best checkpoint kiválasztása után, befagyasztott modellen értékeljük (22.4. Augmentációs kísérlet és domainváltozás, 23.4. Hibaszeletek és alkalmazási határ).

Az új inputra szolgáló inference-parancs egy korábban nem látott, helyben előállított mintát készít:

# lab/infer.py
import torch, argparse
from model_def import GlyphCNN
LABELS = {0: "vertical", 1: "horizontal", 2: "diagonal"}

def make_sample(kind: str) -> torch.Tensor:
    x = torch.zeros(1, 1, 24, 24, dtype=torch.float32)
    if kind == "vertical":
        x[..., 6:18, 10:14] = 1.0
    elif kind == "horizontal":
        x[..., 10:14, 6:18] = 1.0
    elif kind == "diagonal":
        idx = torch.arange(24)
        x[..., idx, idx] = 1.0
    return x

p = argparse.ArgumentParser()
p.add_argument("--kind", choices=["vertical", "horizontal", "diagonal"], required=True)
p.add_argument("--ckpt", default="glyph_cls.pt")
a = p.parse_args()

ckpt = torch.load(a.ckpt, weights_only=True)
model = GlyphCNN()
model.load_state_dict(ckpt["model"])
model.eval()
x = make_sample(a.kind)
with torch.inference_mode():
    z = model(x)
    prob = z.softmax(dim=1)
    yhat = int(z.argmax(dim=1).item())
print("logits:", z.numpy())
print("probs:", prob.numpy())
print("predicted:", ckpt.get("labels", LABELS)[yhat])
python infer.py --kind diagonal

A kimenet jelölt szemléltetés, mert a kézirat készítésekor nem futtattunk kódot:

logits: [[ 0.85 -1.02  2.31]]
probs:  [[0.19 0.03 0.78]]
predicted: diagonal

Értelmezés: a softmax pozitív, összegük 11; az argmax a legnagyobb logithoz tartozó indexet adja, a név pedig a mentett labels mezőből jön, nem a logit abszolút nagyságából.

25.1.6 6. Feladatok

F1 (fogalmi, nehézség 1, átadandó: 3–4 mondatos válasz). Melyik transzformáció lehet része az inference-szerződésnek, és melyik nem? Indokoljon egy-egy példával a GlyphLab főúton.

F2 (levezetési, nehézség 2, átadandó: alaklánc és flatten dimenzió). Vezesse le a 3. szakasz shape-képletével: egy N×1×32×32N\times1\times32\times32 bemenetre Conv2d(1,16,3,padding=1), MaxPool2d(2), Conv2d(16,24,3,padding=1), MaxPool2d(2) után mekkora a kilapított jellemződimenzó?

F3 (implementációs, nehézség 2, átadandó: futtatható függvény). Írjon predict_from_array(x_np) függvényt, amely egy (24,24) float32 NumPy-tömböt vár, és (osztályindex, valószínűségvektor) párt ad vissza a fenti modellből. A modell betöltése legyen a függvényen kívül, model.eval() és torch.inference_mode() használatával.

F4 (hibakeresési, nehézség 2, átadandó: ok, következmény, javítás, kontroll). A hallgató a mentett checkpointot egy másik szkriptben így értékeli:

NAMES = {0: "horizontal", 1: "vertical", 2: "diagonal"}

A tanítási val accuracy 0,980{,}98 volt, de az új szkript confusion matrixának accuracy értéke 0,420{,}42. Hol a hiba?

F5 (kísérleti/transzfer, nehézség 3, átadandó: protokollvázlat). Tervezzen protokollt a főút átvitelére: az új procedurális adatgenerátor 32×3232\times32 RGB képeket ad, ugyancsak vertical/horizontal/diagonal címkékkel. Mi változik a preprocessingben, a backbone első rétegében és a head bemeneti dimenziójában? Adjon baseline-t és validálási elvet.

25.1.7 7. Megoldások és indoklás

F1. Az inference-szerződésbe csak determinisztikus, címkemegőrző leképezések kerülhetnek: a GlyphLab esetében a float32 NCHW [0,1][0,1] tartomány már adott, esetleg a rögzített μ,σ\mu,\sigma normalizálás. A véletlen transzláció, forgatás vagy zaj csak a tanítási augmentáció része; ha a betanított modell kiértékelésekor is véletlenítünk, a P(Y∣X)P(Y\mid X) becslés nem a szerződött bemenetre vonatkozik, és a clean/corrupt párosítás (22.4. Augmentációs kísérlet és domainváltozás) is felborul.

F2. Az egyes lépések: 32→Conv32→Pool16→Conv16→Pool832\xrightarrow{\text{Conv}}32\xrightarrow{\text{Pool}}16\xrightarrow{\text{Conv}}16\xrightarrow{\text{Pool}}8. Az utolsó aktivációs tér N×24×8×8N\times24\times8\times8, tehát a kilapított dimenzió 24⋅8⋅8=153624\cdot8\cdot8=1536. A head első lineáris rétegének bemenete ezért 15361536.

F3.

import torch
from model_def import GlyphCNN

model = GlyphCNN()
model.load_state_dict(torch.load("glyph_cls.pt", weights_only=True)["model"])
model.eval()
LABELS = {0: "vertical", 1: "horizontal", 2: "diagonal"}

def predict_from_array(x_np):
    x = torch.from_numpy(x_np).float().unsqueeze(0).unsqueeze(0)  # (1,1,24,24)
    with torch.inference_mode():
        z = model(x)
        p = torch.softmax(z, dim=1)
        yhat = int(z.argmax(dim=1).item())
    return yhat, p.numpy()[0]

Alakellenőrzés: a bemenet (24,24) float32-ból (1,1,24,24) lesz, a kimenet valószínűségvektor (3,); az argmax a {0,1,2}\{0,1,2\} halmazból választ.

F4. Ok: a NAMES szótár nem azonos a modell tanításakor használt label mappinggel; a checkpoint labels mezője {0: vertical, 1: horizontal, 2: diagonal}, tehát a kiértékelő a 0 indexet tévesen „horizontal”-ként kezeli. Következmény: a confusion matrix sor/oszlop nevei elcsúsznak, az aggregált accuracy a nevek alapján hamisan alacsony lesz, miközben a nyers indexekre számolt pontosság 0,980{,}98. Javítás: a kiértékelő szkript a checkpoint mentett labels mezőjét olvassa be, vagy a külső rögzített szerződés szerint ugyanazt a mappinget használja. Kontroll: számoljuk ki a nyers indexekre a (pred == y).mean() értéket; ennek egyeznie kell a tanítási val accuracy-vel.

F5. Protokollvázlat: az új adatgenerátor minden jelenetét 32×32 RGB-ben menti; a képeket float32 [0,1] NCHW alakra konvertáljuk, és az új adatra becsült csatornánkénti μ,σ\mu,\sigma normalizálást rögzítünk. A backbone első konvolúciója Conv2d(3,16,3,padding=1) lesz, a head előtti alak 32→2×32\xrightarrow{2\times} pool 88, tehát 24⋅8⋅8=153624\cdot8\cdot8=1536-ról induló lineáris réteg. Baseline: az eredeti 24×24 modellre resizelt (pl. bilineáris) képek kiértékelése, változó tényező: a natív 32×32 bemenetre tanított modell. A split jelenetszintű rétegzett; metrika osztályszintű accuracy, a bizonytalanságot scene-rétegzett bootstrap CI-vel ([S02] csak a feltételes ismételt mintavételi értelmezéshez, a GlyphLabra vonatkozó csoportos tervet a 23.4. Hibaszeletek és alkalmazási határ alfejezetben tárgyaltak szerint). Az eredmény csak erre a mesterséges shiftre érvényes, természetes képi általánosításra nem.

25.1.8 8. Tipikus tévedés és ellenpélda

A leggyakoribb hiba a label index–név leképezés kezelése. A checkpoint önmagában csak az indexeket adja vissza; ha a felhasználó külső szótárat rendel, az nem a betanított modell szerződése. Legyen egy modell, amelyet a {0↦diagonal,1↦vertical,2↦horizontal}\{0\mapsto\text{diagonal}, 1\mapsto\text{vertical}, 2\mapsto\text{horizontal}\} mappinggel tanítottak. Ha a kliens az F4-beli NAMES = {0: "horizontal", ...} szótárt használja, egy olyan bemenetre, ahol a valódi osztály diagonal, a modell z0z_0 logitja lesz a legnagyobb; a kliens NAMES[0] alapján „horizontal”-t jelent, tehát a numerikus index helyes, a közölt név mégis téves. A mapping ezért a mentési rekord kötelező eleme.

Önellenőrzés. A 3. szakaszban minden közbülső alak és a paraméterszám explicit; a CrossEntropyLoss bemenete logits, a cél int64; a normalizálás minden fázisban azonos; a checkpoint az architektúra, a bemeneti séma és a label mapping mellett lett mentve. NCHW-konvenció a 13.1. Kép mint diszkrét többcsatornás jel szerint, az inicializálási skála a 21.1. Inicializálás és varianciaterjedés szerint marad a kis modell stable tartományában.

25.1.9 Rövid összefoglaló

Az osztályozó adatfolyam a dataset schema, a determinisztikus preprocessing, a backbone, a head, a logits és a label mapping együttes szerződése. A GlyphLabon a bemenet float32 NCHW 1×24×241\times24\times24, [0,1][0,1]; a tanulmányi modell két konvolúciós blokk után 32×6×632\times6\times6 jellemzőt lapít 11521152 dimenzióra, majd két lineáris réteggel három logitot ad. A batch alaklánca: (N,1,24,24)→(N,16,12,12)→(N,32,6,6)→(N,1152)→(N,3)(N,1,24,24)\to(N,16,12,12)\to(N,32,6,6)\to(N,1152)\to(N,3). A végrehajtott művelet: CrossEntropyLoss logitokból, int64 cél; a modellben nincs softmax. A mentett checkpoint magában foglalja az architektúrát, a bemeneti sémát és a label mappinget, mert a logitok indexeiből a nevek nem rekonstruálhatók.

25.1.10 Következő alkalmazás

A 25.2. Multilabel és hierarchikus cél a bináris, többosztályos és multilabel célok közötti eltérést vizsgálja: a multilabel fej kimenete (N,K)(N,K) logits lesz BCEWithLogitsLoss-szal és {0,1}^K céllal, ahol a jelen alfejezetben rögzített adatfolyam- és checkpoint-szerződés mintájára kell a maszk- és threshold konvenciókat is menteni.

25.1.11 Felhasznált források