Ez az egység a tanítási kísérlet legkisebb ismételhető keretét építi meg: egy teljes epoch tanítása, a modell üzemmódjának váltása, a veszteség helyes aggregálása és a validáció szivárgásmentes kiértékelése. Előismeret a 10.4. Tanítási lépés és tanuló MLP tanítási lépése, a 11.4. Egy teljes PyTorch tanítási iteráció PyTorch-iteráció sorrendje és a 11.1. Tensor és autograd-életciklus autograd életciklusa.
Induló kérdés: egy öt mintás epoch két batchből áll. A három mintás batch átlagos vesztesége , a két mintás batché . A teljes epoch átlagos vesztesége vagy ? Becsülje meg, mielőtt a levezetést elolvassa.
Train/eval üzemmód. A PyTorch nn.Module
.training bool flaget hordoz. A model.train()
igazra, a model.eval() hamisra állítja. Az üzemmód az
állapotfüggő rétegek viselkedését szabályozza: a dropout train módban
véletlenül nulláz, eval módban identitás; a BatchNorm train módban a
batch-statisztikát használja és frissíti a running statisztikát, eval
módban a running statisztikát használja (18.2. Batch
normalization train és eval módban tárgyalja részletesen; a
keretjegyzet [D12] szerint a két statisztikakezelés nem felcserélhető).
A szabály akkor is kötelező, ha az aktuális modellben éppen nincs
állapotfüggő réteg, mert a későbbi bővítés nem írhatja át a
protokollt.
torch.inference_mode(). Ez nem
modell-üzemmód, hanem tenzor-kontextus: a blokkban létrehozott
tenzorokból nem épül autograd gráf, és a gráfot nem lehet utólag
kiegészíteni. Erősebb, mint a no_grad, mert a kilépés után
sem állítható vissza a gráfépítés (11.1. Tensor és
autograd-életciklus). A kiértékeléshez mindkét réteg kell: az
eval() a modul állapotát, az inference_mode a
memória- és gráffegyelmet kezeli.
Loss aggregation. A veszteségfüggvény mean redukcióval scalárt ad: egy logit tenzorból és darab int64 célból az egyedi veszteség átlagát. A tanítási lépésben a backward gradient az átlag deriváltja, ezért a minibatch gradiens az egyedi gradiensek átlaga (10.4. Tanítási lépés és tanuló MLP). Az epoch-loss ezzel szemben a teljes tanítóhalmazra vett monitoring metrika, nem a backprop bemenete.
Batch weighting és szivárgásmentesség (no leakage).
A DataLoader az epochot batch-ek particionálja; ha az utolsó batch
kisebb, a veszteségek összevetése súlyozást igényel. A
szivárgásmentesség azt jelenti: a validációs split egyetlen mintája sem
jelenik meg a tanítási veszteségben, a validációs forward nem frissíti a
paramétereket, és a validációs metrika nem választhat modellt az előre
rögzített protokoll megkerülésével. A GlyphLab adatban a split
scene_id szerint csoportosított, ezért egy jelenet két
nézete azonos splitbe kerül.
Állítás (epoch-loss mint súlyozott átlag). Legyen az epoch tanítóhalmaza , rögzített paraméter mellett. A batchek diszjunkt partíciót alkotnak, , . Minden batchre mean redukcióval definiáljuk
Ekkor a teljes epoch mean vesztesége
Bizonyítás. A partíció tulajdonság miatt , tehát a teljes összeg felbontható:
A belső összeg éppen , mert a batchre vett mean definícióját -vel visszaszorozva a batch összegét kapjuk:
Ez a bizonyítandó állítás. A partíció tulajdonság az első egyenlőségnél, a mean redukció definíciója a másodiknál lép be. Következmény: ha minden batch azonos méretű, akkor , így , az egyszerű átlag csak ebben a speciális esetben helyes.
A levezetés rögzített -ra vonatkozik. A tanítás közben a modell batch-ek között frissül, ezért a monitorozott epoch-loss a frissítési pálya veszteségeit átlagolja; a fenti súlyozási formula attól függetlenül érvényes, hogy a kiszámított batch-veszteségeket hogyan kombináljuk.
Legyen , ; , . A batch 1 egyedi veszteségei , így ; a batch 2 egyedi veszteségei , így . A súlyozott epoch-loss:
Az unweighted batchátlag ezzel szemben , ami nem a teljes mintára vett átlag. A félrevezető érték onnan jön, hogy a kisebb batch ugyanakkora súlyt kap, mint a nagyobb.
A labor egy n045_train_eval.py scriptet készít a
lab/ munkakönyvtárban. Bemenet a GlyphLab
data/images.npy, data/labels.npy és
data/metadata.csv; a splitet a metadata.csv
split oszlopa adja. A train 240, a val 60 képből áll; a kép
batch alakja
,
float32, a címke int64. A batch méret 64, így a train loader négy
batchre oszlik:
;
az utolsó kisebb batch a súlyozás kontrollja. A modell egy lapított MLP,
a veszteség CrossEntropyLoss mean redukcióval, logits
bemenettel, softmax nélkül ([D07] loss-szerződés). A közölt kimenet
számított szemléltetés, nem a laborban mért eredmény.
"""n045_train_eval.py -- train/eval üzemmód és súlyozott epoch-loss.
Munkakönyvtár: lab/
Futtatás: python n045_train_eval.py
A kód futási kimenete helyben ellenőrizendő; a közölt számok szemléltetők.
"""
import numpy as np
import pandas as pd
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
DEVICE = "cpu"
SEED = 20261020
BATCH_SIZE = 64
torch.manual_seed(SEED)
images = np.load("data/images.npy") # (360,1,24,24) float32
labels = np.load("data/labels.npy") # (360,) int64
meta = pd.read_csv("data/metadata.csv")
train_idx = meta[meta["split"] == "train"].index.to_numpy()
val_idx = meta[meta["split"] == "val"].index.to_numpy()
X_train = torch.from_numpy(images[train_idx])
y_train = torch.from_numpy(labels[train_idx])
X_val = torch.from_numpy(images[val_idx])
y_val = torch.from_numpy(labels[val_idx])
train_loader = DataLoader(TensorDataset(X_train, y_train),
batch_size=BATCH_SIZE, shuffle=True)
val_loader = DataLoader(TensorDataset(X_val, y_val),
batch_size=BATCH_SIZE, shuffle=False)
model = nn.Sequential(
nn.Flatten(),
nn.Linear(24 * 24, 64),
nn.ReLU(),
nn.Linear(64, 3),
)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
def train_one_epoch(model, loader, loss_fn, optimizer, device=DEVICE):
model.train()
total = 0.0
n = 0
for Xb, yb in loader:
Xb = Xb.to(device)
yb = yb.to(device)
optimizer.zero_grad()
loss = loss_fn(model(Xb), yb)
loss.backward()
optimizer.step()
total += loss.item() * yb.size(0)
n += yb.size(0)
return total / n
def evaluate(model, loader, loss_fn, device=DEVICE):
model.eval()
total = 0.0
n = 0
correct = 0
with torch.inference_mode():
for Xb, yb in loader:
Xb = Xb.to(device)
yb = yb.to(device)
logits = model(Xb)
total += loss_fn(logits, yb).item() * yb.size(0)
n += yb.size(0)
correct += (logits.argmax(dim=1) == yb).sum().item()
return total / n, correct / n
batch_sizes = []
for _, yb in train_loader:
batch_sizes.append(yb.size(0))
print("train batch méretek:", batch_sizes) # [64, 64, 64, 48]
assert batch_sizes == [64, 64, 64, 48]
assert sum(batch_sizes) == 240
for epoch in range(5):
train_loss = train_one_epoch(model, train_loader, loss_fn, optimizer)
val_loss, val_acc = evaluate(model, val_loader, loss_fn)
print(f"epoch {epoch+1}: train_loss={train_loss:.4f} "
f"val_loss={val_loss:.4f} val_acc={val_acc:.3f}")A függvények magja a súlyozás:
total += loss.item() * yb.size(0) és
n += yb.size(0), majd total / n. A kontroll
assert ellenőrzi, hogy az utolsó 48 mintás batch bekerült a
számításba; ha a DataLoadert drop_last=True-szal hívnánk, a
48 minta elveszne, és az epoch-loss nem a 240 mintára vonatkozna. A
validációs forward model.eval() alatt és
torch.inference_mode() blokkban fut, így sem
paramétergradiens, sem autograd-gráf nem keletkezik; a BatchNorm running
statisztikája sem frissül (a jelen MLP-ben nincs ilyen réteg, de a
protokoll a későbbi CNN-re is érvényes kell legyen).
Az értelmezéshez: a train loss csökkenése önmagában nem általánosítási bizonyíték; a val loss a 8.1. Kockázat, mintavétel és baseline empirikus kockázat becslése a val spliten. A 60 val minta miatt a görbe zajos, és egyetlen futás nem helyettesíti a seed-ismétlést. A [D02] dokumentáció szerint az azonos seed nem garantál platformok és verziók közötti bitazonos kimenetet, ezért a seed a protokoll része, nem a reprodukció egyetlen feltétele.
F1 (fogalmi, nehézség 1). Magyarázza el, miért kell
az evaluate függvényben egyszerre model.eval()
hívás és torch.inference_mode() blokk. Mi történne egy
dropout réteget tartalmazó modell kiértékelésekor, ha a
model.eval() hiányzik? Fogalmazza meg a két mechanizmus
különbségét egy-egy mondatban.
F2 (levezetési, nehézség 2). Legyen a tanítóhalmaz rögzített mellett darab méretű batch partíciója, a veszteség mean redukció. Vezesse le a
formulát. Jelölje meg, melyik lépés használja a partíció tulajdonságot, és ellenőrizze a szélsőesetet.
F3 (implementációs, nehézség 2). Bontsa ki a labor súlyozó logikáját önálló függvénnyé:
def weighted_epoch_loss(batch_losses, batch_sizes):
...A függvény két listából scalár epoch-loss-t ad. Input:
batch_losses = [2.0, 5.0],
batch_sizes = [3, 2]. Elvárt kimenet: 3.2,
tolerancia 1e-6. Adja meg a teljes importot és a
függvényt.
F4 (hibakeresési, nehézség 1). Egy társ ezt írta:
total += loss.item(), majd a végén
final = total / len(loader). Lokalizálja a hibát, számítsa
ki a kézi példán a hibás eredményt, és adja meg a javítást.
F5 (kísérleti, nehézség 3). Tervezzen GlyphLab kísérletet: az első öt epochban a train loss csökken-e, míg a val loss növekedésnek indul. Adja meg a kérdést, az adatot, a splitet, a seedet, a baseline-t, a metrikákat és a következtetés érvényességi korlátait.
F1 megoldás. Az eval() a modell
állapotflagjét hamisra állítja: a dropout eval módban identitás, ezért a
kiértékelés determinisztikus és a tanítási véletlen nullázás nem rontja
a predikciót. Ha elmarad, a dropout minden forwardnál más-más neuronokat
nulláz, így a val loss és pontosság zajossá válik, és a predikció nem
egyezik a későbbi éles használattal. Az inference_mode()
ezzel szemben az autograd gráf építését tiltja: az evaluate forwardja
alatt nem keletkezik gráf, így nem fogy memória, és véletlen
backward hívás sem szennyezhetné a paramétergradienst. A
kettő ortogonális: az egyik a modul viselkedését, a másik a
tenzor-könyvelést szabályozza.
F2 megoldás. A kiinduló definíció . Az epoch mean loss:
A partíció tulajdonság engedi a felbontást:
Ez az a lépés, amely a partíciót használja. A belső összeg a batch mean definícióját visszaszorozva , így
Szélsőeset: , ekkor , tehát , a formula visszaadja a teljes adatra vett mean loss-t. A levezetés feltétele, hogy a batch-ek páronként diszjunktak és lefedik a teljes epochot; a mean redukció definíciója rögzített -ra vonatkozik.
F3 megoldás.
def weighted_epoch_loss(batch_losses, batch_sizes):
"""Súlyozott epoch-loss a batch-veszteségek és batch-méretek listájából."""
total = sum(l * n for l, n in zip(batch_losses, batch_sizes))
return total / sum(batch_sizes)
result = weighted_epoch_loss([2.0, 5.0], [3, 2])
assert abs(result - 3.2) < 1e-6, result
print(f"súlyozott epoch-loss: {result:.6f}") # 3.200000Alakellenőrzés: minden
scalár,
pozitív egész; a nevező
;
a kimenet scalár float. A labor train_one_epoch függvénye
pontosan ezt a számítást végzi beágyazva, ahol a
loss.item() a batch mean loss scalárja.
F4 megoldás. A hiba a nevezőben van:
total / len(loader) a batch-veszteségek egyszerű átlaga,
nem a minták szerinti súlyozott átlag. A kézi példán
,
,
így a hibás érték
,
szemben a helyes
értékkel. A következmény: az epoch-loss torzított monitoring metrika, és
a különböző méretű utolsó batch indokolatlanul nagy súlyt kap. Javítás:
total += loss.item() * yb.size(0);
n += yb.size(0); return total / n.
Visszaellenőrzés a labor assertével: a batch méretek
,
a visszaadott érték a súlyozott képletből számolódik.
F5 megoldásminta. Kérdés: a train loss öt epoch
alatt monoton csökken-e, és a val loss a megfigyelési ablakban növekedni
kezd-e? Adat: GlyphLab data/images.npy,
data/labels.npy, split a data/metadata.csv
split oszlopából, a scene_id csoportosítás
megtartásával. Modell: a labor három osztályos MLP-je, SGD, lr
,
batch 64, seed 20261020, device CPU, float32. Baseline: a
leggyakoribb osztályt prediktáló konstans modell; a val pontossága itt
legfeljebb az osztályarány legnagyobbika, GlyphLab val spliten
osztályonként 10 kép, tehát
körüli felső korlát. Metrikák: súlyozott train és val cross-entropy, val
pontosság. Protokoll: a val loss-t nem használjuk modellválasztásra;
csak a tanítás után elemezzük a görbét. Korlát: a 60 val minta miatt a
görbe nagy varianciájú; egyetlen futás nem ad konfidencia-intervallumot,
több seed kellene az [S02] ismételt mintavételi értelmezéséhez; a train
loss a frissítés közbeni vándorló paraméterek vesztesége, nem állandó
-ra
vett epoch-loss.
Az unweighted batchátlag gyakori hibája:
total += loss.item(); return total / len(loader). Az öt
mintás,
felbontású példán a helyes
,
a hibás
;
a
eltérés nem kerekítési hiba, hanem strukturális súlyozási hiba. Ha
minden batch mérete egyenlő, a két képlet egybeesik, ezért a hiba kis
egyenletes batch-ek mellett rejtve maradhat, és az utolsó kisebb batch
esetén jelentkezik.
Ellenőrzés: a levezetésben
és
indexhatárok teljesek,
a partícióból következik, minden tag skalár. A kézi példa szélsőesete,
,
a formulát
-re
redukálja. A 11.1. Tensor és autograd-életciklus
visszahívása: az inference_mode erősebben zárja a
gráfépítést, mint a no_grad, ezért a kiértékeléshez ezt
használjuk. A 8.1. Kockázat, mintavétel és
baseline visszahívása: a val loss empirikus kockázat a val spliten,
nem populációs kockázat; a kis val minta becslési varianciája nem tűnik
el a seed rögzítésétől.
Az epoch tanítási vesztesége nem a batch-veszteségek egyszerű átlaga:
a különböző
méretű batchek mean loss-aiból csak a súlyozott aggregátum
adja a teljes mintára vett mean loss-t. A levezetés a partíció
tulajdonságon és a mean redukció definícióján áll, rögzített paraméter
mellett. A train mód a modell állapotát, az inference mode a gráfépítést
szabályozza; az evaluate ezért egyszerre hív model.eval()-t
és inference_mode blokkot. A validáció közben sem
paraméter, sem running statisztika nem frissül, így a val loss a
rögzített modell empirikus becslése a val spliten. A labor a súlyozást
az utolsó kisebb batch esetén asserttel is ellenőrzi.
A 12.2. Checkpoint és folytathatóság a checkpoint írását és folytathatóságát tárgyalja; ott az itt bevezetett eval mód a mentés előtti állapotkonzisztencia feltétele lesz, a súlyozott epoch-loss pedig a tanítási napló standard metrikája.