Ez az egység a III. ellenőrző pont: a hallgató egy kézzel levezetett gradienssel, egy NumPy-referenciával, egy autograd-ellenőrzéssel, valamint egy teljes, futtatható MLP-kísérlettel ad át reprodukálható csomagot. A 12.3. Sanity check és kis mintára túlillesztés sanity check után most a teljes training–validáció–checkpoint–teszt ciklust zárjuk le.
Értékelés. A közös 0–4 rubrikát, a PASS-szabályt és a hibatípusonkénti visszautakat az 1.4. Tanulási diagnózis és kísérleti napló „Tanulási főút és ellenőrző pontok” szakasza adja meg; itt csak az eltérés: a PASS-hoz a visszatöltött checkpoint kimenetének egyezését is ellenőrizni kell (lásd az F4 megoldását); a súlypont a D2 és a D4.
Előfeltétel: 12.3. Sanity check és kis mintára túlillesztés, valamint a 11.4. Egy teljes PyTorch tanítási iteráció PyTorch-lépés és a 8.4. II. ellenőrző pont: korrekt tanulási állítás független teszt fogalma. Nem feltételezünk GPU-t, külső súlyt vagy előtanított modellt.
Induló kérdés: meg tudja-e adni a választott kis háló paramétergradienseit három független úton úgy, hogy az egyezés numerikusan ellenőrizhető, és a tanított modellből friss inputra futtatható prediction készül?
MLP a laborban. A bemenet GlyphLab-kép: , float32, értékkészlete . A modell ezt -ra lapítja, majd
ahol
,
,
,
,
.
A loss a stabil logits-alapú CrossEntropyLoss, cél
,
int64.
Held-out metric. A val split a
checkpoint kiválasztásához használt, a test split csak a
kiválasztás után egyszer értékelhető. Így a test a modellválasztás
szempontjából visszatartott. A GlyphLab splitje scene-szintű: egy scene
minden view-ja azonos splitbe esik.
train/eval. A modell Dropout(0.1)
réteget tartalmaz, ezért a train és eval mód nem felcserélhető. Az
eval() kikapcsolja a dropout véletlen maszkját; az
inference_mode ezen felül kikapcsolja az
autograd-könyvelést. A checkpoint mentése és a tesztelés ezért
eval módban történik.
Checkpoint. A kísérlet a legjobb
val_loss-hoz tartozó state_dict-et menti. A
betöltés deklarált architektúrával és weights_only=True
opcióval történik [D09].
Az alábbi lemmát a kis kézi példára alkalmazzuk; ez a fejezet levezetési kötelezettsége.
Lemma (kétlineáris MLP lokális gradiensvisszaterjesztése). Legyen , , , , . Legyen , , , . A cél , a loss
Ha a vizsgált pontban minden , valamint minden olyan eleme pozitív, ahol a visszaterjesztett jel nem nulla, akkor
Ahol soronként a célosztály one-hot vektora.
Bizonyítás (saját levezetés, VJP-lánc). A softmax + cross-entropy kombinált deriváltja logitok szerint 7.2. Softmax és többosztályos cross-entropy, az átlagolás miatt osztunk -nel, tehát . Mivel , a szerinti jelből a -re visszaterjesztett jel , a gradienséhez pedig tartozik; a bias a sorösszege. A ReLU a pozitív preaktivációjú pontokon differenciálható, deriváltja , ezért a szorzás itt azonosság. Az affin leképezésnél a gradiense , a gradiense . Az alakok zárnak, mert mérete , mérete .
Megjegyzés: a ReLU nullponti nem-differenciálhatósága kívül esik e példán; általános esetben a könyvtár által választott konvenciót a 9.4. Aktivációk és gradiensáramlás alfejezetben tárgyalja a szöveg.
Legyen , , , :
A preaktiváció , mindkettő pozitív, ezért . A logitok:
A softmax-sorok közelítőleg:
így . A lemmából, -vel:
| Mennyiség | Alak | Számított közelítő érték |
|---|---|---|
| ; | ||
A fenti értékek kerekített kézi számpélda, nem gépi futási lelet; a kódos ellenőrzésnek a levezetésből számolt tizedesjegyekig egyeznie kell.
A következő munkamenet a levezetett NumPy-backwardot a kézi adatra,
majd ugyanezt autograddal veti össze. Munkakönyvtár:
lab/n048.
# gradient_threeway_n048.py
import numpy as np
import torch
def stable_softmax(z):
z = z - z.max(axis=1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=1, keepdims=True)
def mlp_forward_backward(x, y, w1, b1, w2, b2):
# x: (N,d), y: (N,), w1: (H,d), b1: (H,),
# w2: (K,H), b2: (K,)
a = x @ w1.T + b1 # (N,H)
h = relu = np.maximum(a, 0.0)
z = h @ w2.T + b2 # (N,K)
p = stable_softmax(z)
N = x.shape[0]
y_onehot = np.zeros_like(p)
y_onehot[np.arange(N), y] = 1.0
G = (p - y_onehot) / N # (N,K)
d_w2 = G.T @ h # (K,H)
d_b2 = G.sum(axis=0) # (K,)
dh = (G @ w2) * (a > 0) # (N,H), ReLU-maszk
d_w1 = dh.T @ x # (H,d)
d_b1 = dh.sum(axis=0) # (H,)
loss = -np.log(p[np.arange(N), y]).mean()
return (d_w1, d_b1, d_w2, d_b2), loss
x = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64)
y = np.array([0, 1], dtype=np.int64)
w1 = np.array([[0.5, -0.25]], dtype=np.float64)
b1 = np.array([0.1], dtype=np.float64)
w2 = np.array([[0.2], [-0.3]], dtype=np.float64)
b2 = np.array([0.0, 0.1], dtype=np.float64)
np_grads, np_loss = mlp_forward_backward(x, y, w1, b1, w2, b2)
x_t = torch.tensor(x, dtype=torch.float64)
y_t = torch.tensor(y, dtype=torch.long)
w1_t = torch.tensor(w1, dtype=torch.float64, requires_grad=True)
b1_t = torch.tensor(b1, dtype=torch.float64, requires_grad=True)
w2_t = torch.tensor(w2, dtype=torch.float64, requires_grad=True)
b2_t = torch.tensor(b2, dtype=torch.float64, requires_grad=True)
z_t = torch.relu(x_t @ w1_t.T + b1_t) @ w2_t.T + b2_t
loss_t = torch.nn.functional.cross_entropy(z_t, y_t)
loss_t.backward()
for np_g, torch_g, name in [
(np_grads[0], w1_t.grad, "w1"),
(np_grads[1], b1_t.grad, "b1"),
(np_grads[2], w2_t.grad, "w2"),
(np_grads[3], b2_t.grad, "b2"),
]:
np.testing.assert_allclose(np_g, torch_g.detach().numpy(),
rtol=1e-10, atol=1e-10,
err_msg=name)
print(f"{name}: NumPy={np_g} | autograd={torch_g.detach().numpy()} | max_abs_diff={np.max(np.abs(np_g - torch_g.detach().numpy())):.2e}")
print(f"loss: numpy={np_loss:.8f}, autograd={loss_t.item():.8f}")Az ellenőrzés tolerance-értéke f64 esetén
rtol=1e-10, atol=1e-10; ez a kézi kerekített táblázatnál
szigorúbb, mert mindkét kód a levezetésből számol. A középső
differenciás független kontrollt a 10.3.
Gradiensellenőrzés és numerikus hibamérce protokoll szerint
ugyanezen a ponton futtathatjuk, de ez a háromutas egyeztetés a lemmát
és az implementációt közvetlenül kapcsolja össze.
Munkakönyvtár lab/n048, adat ../data.
Input: images.npy, labels.npy,
metadata.csv. dtype float32, device CPU. Az
értékelés a test splitet csak a legjobb checkpoint kiválasztása után
végzi.
# train_mlp_n048.py
import argparse, csv, json, os
import numpy as np
import torch
from torch.utils.data import DataLoader, Dataset
class GlyphMLP(torch.nn.Module):
def __init__(self, in_dim=576, hidden=32, classes=3, p=0.1):
super().__init__()
self.net = torch.nn.Sequential(
torch.nn.Flatten(),
torch.nn.Linear(in_dim, hidden),
torch.nn.ReLU(),
torch.nn.Dropout(p),
torch.nn.Linear(hidden, classes),
)
def forward(self, x):
return self.net(x)
class ArrayDataset(Dataset):
def __init__(self, x, y):
assert x.ndim == 4 and x.shape[1] == 1
self.x = torch.from_numpy(np.asarray(x, dtype=np.float32))
self.y = torch.from_numpy(np.asarray(y, dtype=np.int64))
def __len__(self):
return len(self.y)
def __getitem__(self, i):
return self.x[i], self.y[i]
def load_split(data_dir):
# Csak a metadata.csv-ból bontunk; így a scene_id szerinti
# függetlenséget ellenőrizni tudjuk.
with open(os.path.join(data_dir, "metadata.csv"), newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
idx = {"train": [], "val": [], "test": []}
scenes = {"train": set(), "val": set(), "test": set()}
for i, r in enumerate(rows):
s = r["split"]
if s not in idx:
raise ValueError(f"ismeretlen split: {s}")
idx[s].append(i)
scenes[s].add(r["scene_id"])
assert scenes["train"].isdisjoint(scenes["val"])
assert scenes["train"].isdisjoint(scenes["test"])
assert scenes["val"].isdisjoint(scenes["test"])
return idx, rows
@torch.inference_mode()
def evaluate(model, loader):
model.eval()
total, correct, loss_sum = 0, 0, 0.0
for x, y in loader:
logits = model(x)
loss_sum += torch.nn.functional.cross_entropy(
logits, y, reduction="sum").item()
correct += (logits.argmax(1) == y).sum().item()
total += len(y)
return loss_sum / total, correct / total
def main():
p = argparse.ArgumentParser()
p.add_argument("--data-dir", default="../data")
p.add_argument("--out-dir", default="artifacts_n048")
p.add_argument("--epochs", type=int, default=60)
p.add_argument("--batch-size", type=int, default=32)
p.add_argument("--lr", type=float, default=1e-3)
p.add_argument("--weight-decay", type=float, default=1e-4)
p.add_argument("--seed", type=int, default=20261020)
args = p.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
torch.manual_seed(args.seed)
np.random.seed(args.seed)
images = np.load(os.path.join(args.data_dir, "images.npy"))
labels = np.load(os.path.join(args.data_dir, "labels.npy"))
assert images.dtype == np.float32, images.dtype
assert np.all((images >= 0.0) & (images <= 1.0))
idx, rows = load_split(args.data_dir)
loaders = {}
for split in ("train", "val", "test"):
ds = ArrayDataset(images[idx[split]], labels[idx[split]])
g = torch.Generator().manual_seed(args.seed)
shuffle = split == "train"
loaders[split] = DataLoader(
ds, batch_size=args.batch_size, shuffle=shuffle, generator=g)
model = GlyphMLP()
optimizer = torch.optim.AdamW(
model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
best_val = float("inf")
best_path = os.path.join(args.out_dir, "best_model.pt")
history = []
for epoch in range(1, args.epochs + 1):
model.train()
total_loss, count = 0.0, 0
for x, y in loaders["train"]:
optimizer.zero_grad(set_to_none=True)
loss = torch.nn.functional.cross_entropy(model(x), y)
if not torch.isfinite(loss):
raise RuntimeError(f"nem véges loss az epoch {epoch} során")
loss.backward()
optimizer.step()
total_loss += loss.item() * len(y)
count += len(y)
train_loss = total_loss / count
val_loss, val_acc = evaluate(model, loaders["val"])
history.append({"epoch": epoch, "train_loss": train_loss,
"val_loss": val_loss, "val_acc": val_acc})
if val_loss < best_val:
best_val = val_loss
torch.save(model.state_dict(), best_path)
print(f"{epoch:03d} train={train_loss:.4f} "
f"val={val_loss:.4f} acc={val_acc:.4f}")
state = torch.load(best_path, map_location="cpu", weights_only=True)
model.load_state_dict(state)
model.eval()
test_loss, test_acc = evaluate(model, loaders["test"])
result = {
"protocol": "n048-III-checkpoint",
"seed": args.seed,
"best_val_loss": best_val,
"test_loss": test_loss,
"test_accuracy": test_acc,
"history_val_acc": [h["val_acc"] for h in history],
"history_train_loss": [h["train_loss"] for h in history],
"command": "python train_mlp_n048.py ...",
}
with open(os.path.join(args.out_dir, "results.json"), "w", encoding="utf-8") as f:
json.dump(result, f, indent=2)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()Futtatási parancs a lab/n048 könyvtárból:
python train_mlp_n048.py --data-dir ../data --out-dir artifacts_n048 --epochs 60 --seed 20261020Ellenőrzés és értelmezés. A futás előtt a kód a
dtype-ot, a
tartományt és a split scene-diszjunktságát ellenőrzi. A
results.json rögzíti a seedet, a legjobb val_loss-t és a
test pontosságát. A véletlen baseline class-balanced osztályok mellett
;
egészséges kísérletben a train loss csökken, a val_acc érdemben
meghaladja ezt, és a test kiértékelés csak a rögzített checkpoint
betöltése után történik. A jelen kézirat ezt a futást nem
állítja elvégzettnek; a fenti results.json mezők
csak a tényleges futtatás után tölthetők fel mért értékkel.
# predict_fresh_n048.py
import argparse, csv, os
import numpy as np
import torch
def main():
p = argparse.ArgumentParser()
p.add_argument("--input", required=True)
p.add_argument("--checkpoint", required=True)
p.add_argument("--out", required=True)
args = p.parse_args()
x = np.load(args.input)
assert x.ndim == 4 and x.shape[1:] == (1, 24, 24), x.shape
assert x.dtype == np.float32, x.dtype
assert np.all(np.isfinite(x)) and np.all((x >= 0.0) & (x <= 1.0))
model = torch.nn.Sequential(
torch.nn.Flatten(),
torch.nn.Linear(576, 32),
torch.nn.ReLU(),
torch.nn.Dropout(0.1), # eval módban inaktív
torch.nn.Linear(32, 3),
)
state = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
model.load_state_dict(state)
model.eval()
with torch.inference_mode():
logits = model(torch.from_numpy(x))
prob = torch.softmax(logits, dim=1).numpy()
pred = prob.argmax(axis=1)
with open(args.out, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["index", "predicted_class", "prob_0", "prob_1", "prob_2"])
for i in range(len(x)):
w.writerow([i, int(pred[i]), *[float(v) for v in prob[i]]])
print(f"predikciók: {args.out}")
if __name__ == "__main__":
main()A parancs friss, a tanító spliten kívüli inputra:
python predict_fresh_n048.py \
--input ../data/fresh_examples.npy \
--checkpoint artifacts_n048/best_model.pt \
--out artifacts_n048/fresh_predictions.csvA friss input sémája: (N,1,24,24), float32,
.
A script ezt és a deklarált architektúrát ellenőrzi, így idegen
állapotszótár nem tölthető be hiba nélkül.
Korlátjegyzék. (1) A test accuracy pontbecslés, nem intervallum; CI-hez az 5.3. Mintavételi bizonytalanság és intervallum és az S02 feltételei szerint külön mintavételi terv kell. (2) A seed, verzió és split rögzítése növeli az ismételhetőséget, de [D02] szerint platformok és verziók között nem garantál bitazonosságot. (3) A modell GlyphLab-orientációra tanult; valós képi általánosításra nem állítható. (4) A friss input eloszlásváltását a jelen checkpoint önmagában nem detektálja. (5) A GPU, AMP, augmentáció és pretraining kívül esik a főúton.
model.eval(), torch.inference_mode() és a
train mód között a fenti MLP-nél? Adjunk olyan esetet, ahol az
eval() kihagyása torzítja a val_loss-t.gradient_threeway_n048.py programot; adja át a négy
max_abs_diff értéket és a loss egyezését, valamint
indokolja, miért rtol=1e-10, atol=1e-10 a f64-es összevetés
határa.torch.load(path) a
weights_only=True nélkül, valamint a mentés
torch.save(model, path). Mi a két hiba lehetséges
következménye, és hogyan ellenőrizhető a javítás?F1. A train módban a Dropout(0.1)
véletlen maszkkal nullázza a rejtett egységek egy részét, ezért a
forward kimenet mintánként eltér. Az eval() kikapcsolja a
maszkot, az inference_mode ezen túl megakadályozza a számítási gráf
építését. Ha a validáció is train módban futna, a dropout véletlen zajt
adna hozzá, és a val_loss nem a kiértékeléskori tényleges modellt mérné;
ugyanazon modul hívással a val_loss realizációnként ingadozna. A
javítás: minden nem tanító kiértékelés előtt
model.eval().
F2. Ha , akkor változatlan, mert a logitok megváltozását a kiszámításánál figyelembe kell venni; a feladat kulcsa, hogy a -gradiens . Újraszámolva a logitok: az első sor , a második . A softmax-sorok , közelítéssel, mellett -sorok: és . Így . Alak-ellenőrzés: , , eredmény . A numerikus tizedesjegyek kerekítettek; a pontos érték a softmax újraszámításából következik.
F3. A futtatás után várt ellenőrzés: mind a négy
max_abs_diff
nagyságrendű vagy kisebb, a loss különbsége pedig legfeljebb
.
A f64-es NumPy és autograd alapműveletei ezen a kis példán tipikusan
ennél is pontosabbak; a rtol=1e-10, atol=1e-10 az
invertált/anomalisztikus mátrixokból adódó felhalmozódást szándékosan
kizárja, miközben az eltérést nem fojtja el túl laza tűréssel. Ha
például a d_w1 nem
,
hanem
lenne, az alak eltérése már korábban hibát adna, mert a shape-ek nem
zárnának.
F4. A torch.load
weights_only nélkül nemcsak tenzort, hanem deszerializálhat
más Python-objektumokat is; idegen vagy manipulált checkpointnál ez
biztonsági kockázat. A második hiba,
torch.save(model, path), a teljes modulobjektumot menti,
ami importálhatósági és verziófüggő csatolást visz be, és nem különíti
el a state_dictet. Következmény: a kísérlet nem reprodukálható más
környezetben, és a betöltés hibája a modell definíciójának változásakor
nehezen lokalizálható. Javítás:
torch.save(model.state_dict(), ...), majd a mentett
state_dict betöltése deklarált modellbe
torch.load(..., weights_only=True) és
load_state_dict. Ellenőrzés: a betöltött modell kimenete
ugyanazon fix bemenetre allclose a mentés előtti modellel,
eval módban.
F5. Egy lehetséges terv: készítsünk 100 új procedurális jelenetet vastagabb vonallal és nagyobb zajjal, a címkézést ugyanazzal a szabállyal, mint a GlyphLab. A kiértékelés a befagyasztott checkpointkal történik; nem futtatunk rajta finomhangolást. Baseline: a záró GlyphLab-teszt accuracy, valamint a véletlen . Metrika: accuracy és confusion matrix a három orientációra. Összehasonlítás páronként ugyanazon checkpointkal, így a modelltényező rögzített. A következtetés határa: ha az accuracy csökken, az domain shiftre utal, de nem azonosítja, hogy a vastagság vagy a zaj okolható-e; ehhez faktoronként egy-egy módosított adatkészlet kellene. A futási napló rögzíti a generátor seedjét, a scene_id szerinti egységet és a nyers predikciókat.
Tipikus tévedés ellenpéldával. „A korábbi eredmény
újrakiírása nem újrafuttatás.” Ha a results.json-ban a test
accuracy-t kézzel javítják egy kívánt értékre, de a
best_model.pt és a predikciók nem változnak, akkor a lelet
nem hordoz reprodukálható bizonyítékot. A modell mentett súlya ezt
teleplezi: a checkpointból újraértékelt pontosság nem egyezik a fájlba
írt számmal. A megoldás az, hogy minden szám a betöltött checkpointból,
azonos scripttel, a nyers predikciókkal együtt keletkezzen.
Önellenőrzés. (1) Minden gradient-check shape zár-e
a lemmában? (2) A betöltött test pontosság a best_val_loss
szerinti checkpointból jött-e, nem az utolsó epochból? (3) A friss input
script ellenőrzi-e a dtype-ot,
-t
és a shape-et a modellhívás előtt?
Visszahívás. A 11.4. Egy teljes
PyTorch tanítási iteráció szerint a zero_grad → forward
→ loss → backward → step sorrend, valamint a
step előtti isfinite kontroll itt is
feltétlenül érvényes. A 8.4. II. ellenőrző pont:
korrekt tanulási állítás szerint a test csak a modellválasztás
lezárása után szolgál független mértéknek.
A III. ellenőrző pont a levezetést, a numerikus egyezést és a kísérletet egyetlen csomagban kapcsolja össze. A kézi példa egy kétlineáris MLP-re a softmax–cross-entropy jeléből VJP-lánccal levezeti a súly- és biasgradienseket; a NumPy- és autograd-megvalósítás f64-ben, tűréssel egyezik. A labor egy 32 rejtett egységes, dropoutos GlyphLab-MLP-t tanít, validációs loss alapján checkpointot ment, majd a test splitet csak a betöltött legjobb modellen értékeli. A friss inputos prediction script a bemeneti sémát és az architektúrát ellenőrzi. A mért eredményt a labor futtatásának kell rögzítenie.
A 13.1. Kép mint diszkrét többcsatornás jel a képet diszkrét többcsatornás jelként vezeti be, és ezzel a konvolúciós művelet előkészítését kezdi meg.