12.4 III. ellenőrző pont: önállóan tanuló kis háló

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

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?

12.4.2 Definíciók, jelölések és értelmezés

MLP a laborban. A bemenet GlyphLab-kép: X∈ℝN×1×24×24X\in\mathbb R^{N\times 1\times 24\times 24}, float32, értékkészlete [0,1][0,1]. A modell ezt ℝN×576\mathbb R^{N\times 576}-ra lapítja, majd

H=ReLU(XfW1⊤+𝟏Nb1⊤),Z=HW2⊤+𝟏Nb2⊤,H=\operatorname{ReLU}(X_fW_1^\top+\mathbf 1_N b_1^\top),\qquad Z=H W_2^\top+\mathbf 1_N b_2^\top,

ahol Xf∈ℝN×576X_f\in\mathbb R^{N\times 576}, W1∈ℝ32×576W_1\in\mathbb R^{32\times 576}, b1∈ℝ32b_1\in\mathbb R^{32}, W2∈ℝ3×32W_2\in\mathbb R^{3\times 32}, b2∈ℝ3b_2\in\mathbb R^{3}. A loss a stabil logits-alapú CrossEntropyLoss, cél y∈{0,1,2}Ny\in\{0,1,2\}^N, 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].

12.4.3 A levezetési kötelezettség: háromféle gradiens egyeztetése

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 X∈ℝN×dX\in\mathbb R^{N\times d}, W1∈ℝH×dW_1\in\mathbb R^{H\times d}, b1∈ℝHb_1\in\mathbb R^H, W2∈ℝK×HW_2\in\mathbb R^{K\times H}, b2∈ℝKb_2\in\mathbb R^{K}. Legyen A=XW1⊤+𝟏Nb1⊤A=XW_1^\top+\mathbf 1_Nb_1^\top, H1=ReLU(A)H_1=\operatorname{ReLU}(A), Z=H1W2⊤+𝟏Nb2⊤Z=H_1W_2^\top+\mathbf 1_Nb_2^\top, P=softmax(Z)P=\operatorname{softmax}(Z). A cél y∈{0,…,K−1}Ny\in\{0,\dots,K-1\}^N, a loss

L=1N∑n=1N(−logPn,yn).L=\frac1N\sum_{n=1}^N\left(-\log P_{n,y_n}\right).

Ha a vizsgált pontban minden Pn,k>0P_{n,k}>0, valamint H1H_1 minden olyan eleme pozitív, ahol a visszaterjesztett jel nem nulla, akkor

G=1N(P−Yonehot)∈ℝN×K,∇b2L=G⊤𝟏N,∇W2L=G⊤H1,G=\frac1N(P-Y_{\mathrm{onehot}})\in\mathbb R^{N\times K},\quad \nabla_{b_2}L=G^\top\mathbf 1_N,\quad \nabla_{W_2}L=G^\top H_1,

D=GW2∈ℝN×H,∇b1L=D⊤𝟏N,∇W1L=D⊤X.D=GW_2\in\mathbb R^{N\times H},\quad \nabla_{b_1}L=D^\top\mathbf 1_N,\quad \nabla_{W_1}L=D^\top X.

Ahol YonehotY_{\mathrm{onehot}} 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 P−YonehotP-Y_{\mathrm{onehot}} 7.2. Softmax és többosztályos cross-entropy, az átlagolás miatt osztunk NN-nel, tehát ∇ZL=G\nabla_ZL=G. Mivel Z=H1W2⊤+𝟏Nb2⊤Z=H_1W_2^\top+\mathbf 1_Nb_2^\top, a ZZ szerinti GG jelből a H1H_1-re visszaterjesztett jel D=GW2D=GW_2, a W2W_2 gradienséhez pedig G⊤H1G^\top H_1 tartozik; a bias a GG sorösszege. A ReLU a pozitív preaktivációjú pontokon differenciálható, deriváltja 11, ezért a D=D⊙𝟏H1>0D=D\odot\mathbf 1_{H_1>0} szorzás itt azonosság. Az affin A=XW1⊤+𝟏Nb1⊤A=XW_1^\top+\mathbf 1_Nb_1^\top leképezésnél a W1W_1 gradiense D⊤XD^\top X, a b1b_1 gradiense D⊤𝟏ND^\top\mathbf 1_N. Az alakok zárnak, mert D⊤XD^\top X mérete H×dH\times d, G⊤H1G^\top H_1 mérete K×HK\times H. ▫\square

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.

12.4.3.1 Kézi kis példa

Legyen N=2N=2, d=2d=2, H=1H=1, K=2K=2:

X=(1234),y=(0,1),W1=(0,5−0,25),b1=0,1,X=\begin{pmatrix}1&2\\3&4\end{pmatrix},\quad y=(0,1),\quad W_1=\begin{pmatrix}0{,}5&-0{,}25\end{pmatrix},\quad b_1=0{,}1,

W2=(0,2−0,3),b2=(0,00,1).W_2=\begin{pmatrix}0{,}2\\-0{,}3\end{pmatrix},\quad b_2=\begin{pmatrix}0{,}0\\0{,}1\end{pmatrix}.

A preaktiváció XW1⊤+b1=(0,1;0,6)XW_1^\top+b_1=(0{,}1;0{,}6), mindkettő pozitív, ezért H1=(0,1;0,6)H_1=(0{,}1;0{,}6). A logitok:

Z=(0,020,070,12−0,08).Z=\begin{pmatrix}0{,}02&0{,}07\\0{,}12&-0{,}08\end{pmatrix}.

A softmax-sorok közelítőleg:

P1≈(0,487503;0,512497),P2≈(0,549834;0,450166),P_1\approx(0{,}487503;0{,}512497),\qquad P_2\approx(0{,}549834;0{,}450166),

így L≈0,7584L\approx 0{,}7584. A lemmából, N=2N=2-vel:

Mennyiség Alak Számított közelítő érték
GG 2×22\times 2 (−0,256249,0,256249)(-0{,}256249,0{,}256249); (0,274917,−0,274917)(0{,}274917,-0{,}274917)
∇W2L\nabla_{W_2}L 2×12\times1 (0,139325;−0,139325)(0{,}139325;-0{,}139325)
∇b2L\nabla_{b_2}L 22 (0,018668;−0,018668)(0{,}018668;-0{,}018668)
DD 2×12\times1 (−0,128125;0,137459)(-0{,}128125;0{,}137459)
∇W1L\nabla_{W_1}L 1×21\times2 (0,284251;0,293585)(0{,}284251;0{,}293585)
∇b1L\nabla_{b_1}L 11 (0,009334)(0{,}009334)

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.

12.4.3.2 Háromutas ellenőrzés kódja

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.

12.4.4 Labor: reprodukálható MLP-kísérlet checkpointtal

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 20261020

Ellenőrzés és értelmezés. A futás előtt a kód a dtype-ot, a [0,1][0,1] 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 1/31/3; 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.

12.4.4.1 Friss inputos inference parancs

# 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.csv

A friss input sémája: (N,1,24,24), float32, [0,1][0,1]. 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.

12.4.5 Feladatok

12.4.6 Megoldások és indoklá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 W2=(0,3;−0,3)⊤W_2=(0{,}3;-0{,}3)^\top, akkor GG változatlan, mert a logitok megváltozását a PP kiszámításánál figyelembe kell venni; a feladat kulcsa, hogy a b2b_2-gradiens G⊤𝟏NG^\top\mathbf 1_N. Újraszámolva a logitok: az első sor (0,03;0,07)(0{,}03;0{,}07), a második (0,18;−0,08)(0{,}18;-0{,}08). A softmax-sorok P1=(0,49;0,51)P_1=(0{,}49;0{,}51), P2=(0,5646;0,4354)P_2=(0{,}5646;0{,}4354) közelítéssel, y=(0,1)y=(0,1) mellett GG-sorok: (−0,255;0,255)(-0{,}255;0{,}255) és (0,2823;−0,2823)(0{,}2823;-0{,}2823). Így ∇b2L=(0,0273;−0,0273)\nabla_{b_2}L=(0{,}0273;-0{,}0273). Alak-ellenőrzés: G∈ℝ2×2G\in\mathbb R^{2\times2}, 𝟏N∈ℝ2\mathbf 1_N\in\mathbb R^2, eredmény ℝ2\mathbb R^2. 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 10−1210^{-12} nagyságrendű vagy kisebb, a loss különbsége pedig legfeljebb 10−1210^{-12}. 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 D⊤XD^\top X, hanem X⊤DX^\top D 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 1/31/3. 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.

12.4.7 Tipikus tévedés, önellenőrzés és visszahívás

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, [0,1][0,1]-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.

12.4.8 Rövid összefoglaló

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 P−YP-Y jeléből VJP-lánccal levezeti a súly- és biasgradienseket; a NumPy- és autograd-megvalósítás f64-ben, 10−1010^{-10} 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.

12.4.9 Következő alkalmazás

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.

12.4.10 Felhasznált források