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,
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.
Definíció (osztályozási adatfolyam). Legyen a bemeneti tér , a célhalmaz , ahol a GlyphLab főútján , , . Egy osztályozó adatfolyam az alábbi komponensekből áll:
data/images.npy alakja
float32; data/labels.npy alakja
int64; data/metadata.csv rekordonként
index, scene_id, view_id, label, split, xmin, ymin, xmax, ymax
mezőket ad. A split jelenetszinten független (120/30/30 jelenet).CrossEntropyLoss-t használ; a cél int64 osztályindex, és a
modellben nincs softmax ([D07]).A konkrét tanulmányi modell legyen
majd a head
Lemma (a GlyphCNN shape-lánca). A fenti modellre, ha , akkor a közbülső alakok:
Bizonyítás. A 14.1. Stride, padding, dilation és outputméret alfejezetben levezetett kimeneti méret
szerint a Conv1-re , , , , tehát ; a MaxPool , , , tehát . A Conv2-re ugyanez , a második pool . A kilapítás skalárt ad mintánként, a lineáris rétegek pedig a mátrixszorzás definíciójából és alakot.
A modell paraméterszáma, esetén:
| Réteg | Paraméterek |
|---|---|
| Conv2d(1,16,3) + bias | |
| BatchNorm2d(16) | |
| Conv2d(16,32,3) + bias | |
| BatchNorm2d(32) | |
| Linear(1152,64) + bias | |
| Linear(64,3) + bias |
Összesen tanítható skalár. Ez a kis modell a GlyphLab 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.
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")
PYA 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.
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.pyA 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 diagonalA 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
;
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.
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
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 volt, de az új szkript confusion matrixának accuracy értéke . 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 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.
F1. Az inference-szerződésbe csak determinisztikus,
címkemegőrző leképezések kerülhetnek: a GlyphLab esetében a
float32 NCHW
tartomány már adott, esetleg a rögzített
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
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: . Az utolsó aktivációs tér , tehát a kilapított dimenzió . A head első lineáris rétegének bemenete ezért .
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
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
.
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
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
pool
,
tehát
-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.
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
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
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.
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
,
;
a tanulmányi modell két konvolúciós blokk után
jellemzőt lapít
dimenzióra, majd két lineáris réteggel három logitot ad. A batch
alaklánca:
.
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.
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
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.
GlyphCNN architektúra és a batch-alaklevezetés
e könyv 14.1. Stride, padding, dilation és
outputméret shape-képletére épül; a fejezeti [D13] katalógus ebben
az alfejezetben nem használt, mert a főút előtanítás nélküli, saját kis
háló.