33.1 Inference-szerződés és modellcsomag

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

A VI. ellenőrző pont után rendelkezünk egy betanított, kiértékelt modellel és egy checkpointtal. Ebben az egységben azt a szerződést rögzítjük, amely nélkül a súlyfájl önmagában nem modell: az architektúra, a preprocessing, a labeltérkép, a verzió, a device és az eval-állapot együtt alkotja az inference-szerződést. A cél az, hogy a hallgató a saját CNN-súlyt egy új, független processzben be tudja tölteni, és a visszatöltött modell logitjait kontrollpredikcióval ellenőrizze. Előismeret: a 12.4. III. ellenőrző pont: önállóan tanuló kis háló checkpoint-fogalma, a 23.4. Hibaszeletek és alkalmazási határ kiértékelési szerződése, a 25.4. Nyílt halmaz és ismeretlen input tartományon kívüli input kezelése, valamint a 21.1. Inicializálás és varianciaterjedés seed/reprodukálhatósági fogalma.

Induló kérdés: egy glyph_cnn.pt fájl megérkezik egy másik gépre. Elég-e a fájlkiterjesztés és a fájlnév ahhoz, hogy a betöltött modell ugyanazt a predikciót adja, mint a tanító processzben? Becslés: nem. A kiterjesztés nem biztonsági és nem szemantikai garancia; a predikcióhoz a teljes szerződést ismerni kell.

33.1.2 2. Inference-szerződés: definíció, komponensek és jelölések

Definíció (modellcsomag). Egy osztályozó modellcsomag a következő öt komponens együttesét jelenti:

  1. Architektúra: az nn.Module osztálydefiníció, amely a forward számítást meghatározza.
  2. Súly- és állapottérkép (state_dict): a paraméterek és a tanítás során frissülő pufferek rendezett kulcs–érték leképezése.
  3. Preprocessing-szerződés: az input séma, dtype, tartomány és bármely előfeldolgozási lépés.
  4. Labeltérkép: a KK kimeneti index és a szemantikus osztály megfeleltetése.
  5. Verzió: a modellváltozat, valamint a tanítási split és a kiértékelési protokoll azonosítója.

Definíció (state_dict). A state_dict a modul regisztrált paramétereinek és puffereinek rendezett leképezése: 𝚜𝚝𝚊𝚝𝚎_𝚍𝚒𝚌𝚝:𝒦→ℝd1×⋯×dr\texttt{state\_dict}: \mathcal{K}\to\mathbb{R}^{d_1\times\cdots\times d_r}, ahol 𝒦\mathcal{K} a rétegnév-mintázatú kulcsok halmaza, például conv1.weight, conv2.bias, fc.weight. A kulcsok a moduldefinícióból származnak; a fájlnév nem hordozza őket. A betöltés strict=True módban hibát ad, ha a kulcshalmaz vagy valamelyik alak nem egyezik.

Definíció (inference-szerződés). Az inference-szerződés az a leképezés, amely az érvényes inputtér X⊆ℝN×1×24×24X\subseteq\mathbb R^{N\times 1\times 24\times 24}, [0,1][0,1] tartományú float32 tensor elemeihez az N×3N\times 3 logitmátrixot rendeli. A logitok nem valószínűségek; a softmax a 7.2. Softmax és többosztályos cross-entropy szerint a döntéshez külön lépés. A labeltérkép 0↦vertical0\mapsto\text{vertical}, 1↦horizontal1\mapsto\text{horizontal}, 2↦diagonal2\mapsto\text{diagonal}.

A trusted input fogalma két különböző helyen lép fel. Egyrészt a checkpoint betöltése deszerializációs kockázatot hordoz: a torch.load a Python pickle mechanizmusára épül, ezért nem megbízható forrásból származó fájlból tetszőleges kódot futtathat. A weights_only=True argumentum ezt a deszerializációt tenzorokra, primitív típusokra és egyszerű konténerekre korlátozza — a hivatalos oktatóoldal [D09] ezt a state_dict betöltés ajánlott módjaként tárgyalja. Másrészt a modell bemenete is szerződés alá esik: az input tartománya [0,1][0,1], alakja (N,1,24,24), dtype float32. A szerződésen kívüli inputról a 25.4. Nyílt halmaz és ismeretlen input mondja ki, hogy a softmax-konfidencia önmagában nem érvényességi bizonyíték.

A device a számítás helyét rögzíti. A labor CPU-t használ; a betöltés map_location="cpu" argumentummal akkor is CPU-ra helyezi a tenzorokat, ha a checkpoint GPU-n készült. A train/eval állapot a modul viselkedését váltja: dropout és batch normalization másként számol a két módban. A BatchNorm running állapota a state_dict puffereiben van, de a forward módját a model.eval() hívás állítja be.

33.1.3 3. Ekvivalenciafeltétel a kontrollpredikcióhoz

A levezetési kötelezettség pontos állítást kíván arról, mikor egyezik meg az új processzben kapott logit a referenciával.

Tétel (kontrollpredikció bitazonossága). Legyen f:ℝN×1×H×W×Θ→ℝN×Kf:\mathbb R^{N\times 1\times H\times W}\times\Theta\to\mathbb R^{N\times K} a modul forwardja, ahol Θ\Theta a paramétertér, és tegyük fel, hogy az alábbi feltételek mindegyike teljesül:

Ekkor az új processz logitmátrixa bitre egyezik a referencia logitmátrixszal: Znew=f(xnew,θnew)=f(xref,θref)=Zref.Z_{\text{new}}=f(x_{\text{new}},\theta_{\text{new}})=f(x_{\text{ref}},\theta_{\text{ref}})=Z_{\text{ref}}.

Bizonyítás. A forward ff a modul rétegeinek függvénykompozíciója. Az (A4) feltétel miatt minden tag determinisztikus: ugyanahhoz az input–paraméter párhoz pontosan egy kimenet tartozik. Az (A3) és (A2) feltétel garantálja, hogy a függvény argumentumai bitre megegyeznek a referenciafutáséval, az (A1) és (A5) feltétel pedig azt, hogy a számítás során nincs olyan átalakítás (dtype-konverzió, device-áthelyezés, félpontos kerekítés), amely a biteket megváltoztatná. A függvényérték egyértelműsége miatt a két kimenet azonos. ◼\blacksquare

Ez a bizonyítás tudatosan nem mély állítás: a feltételek pontosan azok a pontok, ahol a gyakorlatban a predikció eltérni szokott. Ha bármelyik feltétel sérül, a tétel nem alkalmazható. Két tipikus sérülés: (A4) sérül, ha dropoutos hálót train() módban futtatunk, mert a maszk véletlenszerű; (A5) sérül, ha a forward GPU-ra vagy bfloat16-ra költözik, mert a redukciók sorrendje és a kerekítés eltérhet. Ilyenkor legfeljebb numerikus közelséget állíthatunk, bitazonosságot nem.

33.1.4 4. Eredeti kézi kis példa: state_dict egy lineáris modulon

Az affin réteg a 3.2. Mátrixszorzás és affín réteg szerint z=Wx+bz=Wx+b. Egy nn.Linear(2,1) modul state_dict-je két kulcsot tartalmaz:

Kulcs Alak Jelentés
0.weight (1,2)(1,2) a WW súlymátrix
0.bias (1)(1) a bb biasvektor

Legyen a kézi példában W=(0,51,5),b=−0,5,x=(2−1).W=\begin{pmatrix}0{,}5 & 1{,}5\end{pmatrix},\qquad b=-0{,}5,\qquad x=\begin{pmatrix}2\\-1\end{pmatrix}. A logit: z=0,5⋅2+1,5⋅(−1)−0,5=1−1,5−0,5=−1.z=0{,}5\cdot 2+1{,}5\cdot(-1)-0{,}5=1-1{,}5-0{,}5=-1. A mentett state_dict tehát nem „a modell”, hanem csak a paraméterek:

OrderedDict([
    ('0.weight', tensor([[0.5000, 1.5000]])),
    ('0.bias',   tensor([-0.5000]))
])

Ugyanez a state_dict egy másik, nemlineáris moduldefinícióhoz betöltve más függvényt ad, vagy strict=True módban kulcshibával leáll. Ez a kézi példa mutatja, hogy a súlyfájl és az architektúra csak együtt határoz meg predikciót.

33.1.5 5. Labor: új processzben visszatöltés és logitellenőrzés

Munkakönyvtár: lab. A GlyphLab adat a data/images.npy fájlban van a kezdőlabor szerződése szerint. A labor három fájlt használ: a moduldefiníciót, egy referencia-előállítót és egy új processzben futó ellenőrzőt.

Architektúra. A GlyphCNN megegyezik a VI. ellenőrző pontnál használt, 2979 paraméteres kis CNN-nel. A rétegek:

Réteg Kimenet Paraméterszám
conv1 = Conv2d(1,8,3,padding=1) (N,8,24,24)(N,8,24,24) 8⋅(1⋅9)+8=808\cdot(1\cdot 9)+8=80
pool = MaxPool2d(2) (N,8,12,12)(N,8,12,12) 00
conv2 = Conv2d(8,16,3,padding=1) (N,16,12,12)(N,16,12,12) 16⋅(8⋅9)+16=116816\cdot(8\cdot 9)+16=1168
pool (N,16,6,6)(N,16,6,6) 00
fc = Linear(16\cdot 36,3) (N,3)(N,3) 3⋅576+3=17313\cdot 576+3=1731

Az összeg 80+1168+1731=297980+1168+1731=2979.

Fájl: n129/glyph_cnn.py

import torch
from torch import nn

class GlyphCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
        self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
        self.pool = nn.MaxPool2d(2)
        self.fc = nn.Linear(16 * 6 * 6, 3)

    def forward(self, x):
        x = torch.relu(self.conv1(x))
        x = self.pool(x)
        x = torch.relu(self.conv2(x))
        x = self.pool(x)
        x = x.reshape(x.size(0), -1)
        return self.fc(x)

Fájl: n129/make_reference.py — a referenciafolyamat: inicializálás rögzített seeddel, kontrollinput a test split első képéből (images[300:301], mert az indexelés a metadatasorrendet követi), eval mód, forward, mentés.

import json, os
import numpy as np
import torch
from glyph_cnn import GlyphCNN

torch.manual_seed(20261020)  # csak a referencia inicializáláshoz

model = GlyphCNN()
model.eval()

images = np.load("data/images.npy")          # (360,1,24,24), float32
control = torch.from_numpy(images[300:301].copy())  # (1,1,24,24), CPU float32

with torch.no_grad():
    reference_logits = model(control)        # (1,3)

os.makedirs("n129/checkpoints", exist_ok=True)
torch.save(model.state_dict(), "n129/checkpoints/glyph_cnn.pt")
np.save("n129/control_input.npy", control.numpy())
np.save("n129/reference_logits.npy", reference_logits.numpy())

manifest = {
    "model_version": "glyphcnn-1.0",
    "architecture": "GlyphCNN",
    "input": {"shape": [1, 1, 24, 24], "dtype": "float32", "domain": [0.0, 1.0]},
    "labels": {"0": "vertical", "1": "horizontal", "2": "diagonal"},
    "preprocessing": "none; raw NCHW float32 image",
    "checkpoint": "n129/checkpoints/glyph_cnn.pt",
    "control_input": "n129/control_input.npy",
    "reference_logits": "n129/reference_logits.npy"
}
with open("n129/manifest.json", "w", encoding="utf-8") as f:
    json.dump(manifest, f, indent=2)

print("saved reference logits:", reference_logits.numpy())

Fájl: n129/verify_inference.py — ez egy új processz: a moduldefiníciót újraimportálja, a checkpointot weights_only=True móddal tölti, és bitazonosságot ellenőriz.

import json
import numpy as np
import torch
from glyph_cnn import GlyphCNN

with open("n129/manifest.json", encoding="utf-8") as f:
    manifest = json.load(f)

if manifest["architecture"] != "GlyphCNN":
    raise SystemExit("architecture mismatch in manifest")

model = GlyphCNN()  # új példány, új processzben
ckpt = torch.load(manifest["checkpoint"], map_location="cpu", weights_only=True)
model.load_state_dict(ckpt)  # strict=True: kulcs- és alakellenőrzés
model.eval()

control = torch.from_numpy(np.load("n129/control_input.npy"))
reference = torch.from_numpy(np.load("n129/reference_logits.npy"))

with torch.no_grad():
    reloaded = model(control)

bitwise_equal = torch.equal(reloaded, reference)
max_abs_diff = (reloaded - reference).abs().max().item()

print("architecture_ok: True")
print("state_dict_keys:", sorted(ckpt.keys()))
print("bitwise_equal:", bitwise_equal)
print("max_abs_diff:", max_abs_diff)
print("reference logits:", reference.numpy())
print("reloaded  logits:", reloaded.numpy())

if not bitwise_equal:
    # Numerikus tartalékellenőrzés, ha a környezet mégis eltérő lebegőpontos utat adna
    tol = 1e-6
    print("bitwise equality failed; numerical allclose:",
          torch.allclose(reloaded, reference, atol=tol, rtol=0.0))

Futtatási parancsok a lab könyvtárból:

python n129/make_reference.py
python n129/verify_inference.py

A második parancs külön processz, és nem osztozik az első memóriáján. A numerikus kimenet a futtatás előtt jelölt szemléltetés, nem mért eredmény: a tényleges logitértékeket a seedelt inicializálás határozza meg, a konzolon rögzítendők. A várt ellenőrzési sor CPU float32 esetén bitwise_equal: True és max_abs_diff: 0.0; ha valamelyik feltétel sérül, a hibakeresési lista a dtype/device/eval hármast vizsgálja. A seed a betöltés utáni eval forwardhoz már nem szükséges, mert a forward nem használ véletlent.

33.1.6 6. Feladatok

F1. Fogalmi — a kiterjesztés nem biztonsági garancia (nehézség: 2). Magyarázza el, miért nem következik a .pt vagy .pth fájlkiterjesztésből a betöltés biztonsága. Különböztesse meg a weights_only=True deszerializációs korlátját és a súlyok szakmai megbízhatóságát; adjon egy konkrét ellenpéldát mindkét hiányra. Elvárt átadandó: saját szavas magyarázat és két kis példa, legfeljebb egy oldal.

F2. Levezetési — a tétel feltételeinek megsértése (nehézség: 2). A 3. szakasz tételének (A4) és (A5) feltételét egy-egy konkrét esetben megsértik: (a) dropoutos háló train() módban, (b) ugyanaz a súly GPU-float32-re helyezve. Mindkét esetre nevezze meg, melyik feltétel sérül, milyen típusú eltérés keletkezik (sztochasztikus vagy numerikus), és miért nem alkalmazható a bitazonossági következtetés. Elvárt átadandó: két rövid, feltételre hivatkozó érvelés.

F3. Implementációs — manifest-ellenőrző (nehézség: 2). Írjon validate_manifest.py függvényt, amely a manifest.json architecture, input.shape, input.dtype és labels mezőit összeveti (i) a betöltött state_dict kulcshalmazával, (ii) a kontrollinput tensor alakjával és dtype-jával, anélkül hogy forwardot számolna. Elvárt átadandó: teljes kód, egy helyes és egy szándékosan hibás manifestre adott tesztkimenet.

F4. Hibakeresési — két csendes hiba (nehézség: 2). Adott a következő részlet:

model = GlyphCNNBN()               # batch normalizációt is tartalmazó változat
state = torch.load(path)           # weights_only nélkül
model.load_state_dict(state)
logits = model(single_image)       # train módban maradt

Lokalizálja a két hibát, írja le mindkettő következményét, és adja meg a javítást úgy, hogy a bitazonossági tétel feltételei helyreálljanak. Elvárt átadandó: ok–következmény–javítás–visszaellenőrzés négyes mindkét hibára.

F5. Kísérlettervezési — device vagy dtype? (nehézség: 3). Tervezzen kontrollfutást annak eldöntésére, hogy egy megfigyelt numerikus eltérés a CPU→GPU áthelyezésnek vagy a float32→bfloat16 konverziónak tulajdonítható-e. Adjon meg figyelési egységet, baseline-t, faktorokat, metrikát, ismétlésszámot és következtetési határt. Elvárt átadandó: protokollterv, nem feltétlenül futtatott eredmény.

33.1.7 7. Megoldások és indoklás

F1. A torch.save a Python pickle protokolljára épül; a kiterjesztés csak fájlnév-konvenció. Egy evil.pt tartalmazhat olyan pickle-objektumot, amely betöltéskor kódot futtat; weights_only=True esetén a deszerializáló tenzorokra és primitív értékekre korlátozódik, ezért az ilyen fájl hibaüzenettel elutasítható. Ez azonban nem jelenti a súlyok szakmai megbízhatóságát: egy másik architektúra state_dict-je, vagy egy ellenségesen módosított, de érvényes tenzorokból álló súly betölthető, és hibás vagy manipulált predikciót ad. A szakmai megbízhatóság forrásellenőrzés, manifest, kulcsegyezés és kontrollpredikció kérdése.

F2. (a) A dropout forwardja véletlen maszkot sorsol, tehát a forward nem determinisztikus függvény: (A4) sérül. Az eltérés sztochasztikus; két futás ugyanazon input és súly mellett is eltérő logitot ad. (b) A GPU-kernel redukciós sorrendje és a lebegőpontos fúziók eltérhetnek a CPU-úttól: (A5) sérül, az eltérés numerikus. A tétel konklúziója bitazonosság, amelyhez mind az öt feltétel együttes teljesülése szükséges; egyik megsértése esetén csak allclose típusú közelség vizsgálható.

F3. Egy lehetséges megoldás:

import json
import numpy as np
import torch

def validate_manifest(manifest_path, ckpt_path, input_path):
    with open(manifest_path, encoding="utf-8") as f:
        m = json.load(f)
    ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True)
    x = torch.from_numpy(np.load(input_path))

    expected_keys = {"conv1.weight", "conv1.bias",
                     "conv2.weight", "conv2.bias",
                     "fc.weight", "fc.bias"}
    key_ok = set(ckpt.keys()) == expected_keys
    arch_ok = m["architecture"] == "GlyphCNN"
    shape_ok = (tuple(m["input"]["shape"]) == tuple(x.shape))
    dtype_ok = (m["input"]["dtype"] == str(x.dtype).replace("torch.", ""))
    labels_ok = (m["labels"] == {"0": "vertical", "1": "horizontal", "2": "diagonal"})
    return {"arch_ok": arch_ok, "key_ok": key_ok,
            "shape_ok": shape_ok, "dtype_ok": dtype_ok, "labels_ok": labels_ok}

if __name__ == "__main__":
    result = validate_manifest("n129/manifest.json",
                               "n129/checkpoints/glyph_cnn.pt",
                               "n129/control_input.npy")
    for k, v in result.items():
        print(k, v)
    assert all(result.values())

A kulcshalmaz-ellenőrzés a moduldefinícióból származtatott várt halmazt használ; a hibás manifestnél például a labels "1" értékét "vertical"-re állítva a labels_ok hamis lesz.

F4. Első hiba: a torch.load weights_only nélkül fut, ami nem megbízható fájl esetén kódfuttatási kockázat; következménye biztonsági, nem numerikus. Javítás: torch.load(path, map_location="cpu", weights_only=True). Második hiba: a modul train() módban maradt. A batch normalizációs réteg train módban a bemenő minibatch statisztikáját használja a state_dict-ben tárolt futó átlag helyett; egyetlen kép esetén a normalizálás a kép saját statisztikájából számolódik, így a logit eltér a referenciafolyamattól. Javítás: model.eval() a forward előtt. Visszaellenőrzés: model.training értéke False, a BN-réteg track_running_stats pufferei nem változnak a forward alatt, és a logit bitazonos a referenciával.

F5. Egy lehetséges terv. Kérdés: az eltérést a device vagy a dtype okozza. Baseline: CPU-float32 referencia-logit, a tétel szerint bitazonos önmagával. Faktorok: device kétszintű (CPU, CUDA) és dtype kétszintű (float32, bfloat16); a 2×22\times2 rács minden cellájában ugyanaz a state_dict és ugyanaz a 10 kontrollinput. Metrika: max_abs_diff a CPU-float32 referenciához, valamint bitazonos találatok aránya. Minden cellában 10 forward, a seed nem befolyásolja az eval forwardot, de az ismétlés a kernel-változatok stabilitását méri. Következtetési határ: ha a CPU-bfloat16 cella is nagy eltérést mutat, a dtype a domináns magyarázat; ha a GPU-float32 cella tér el, de a CPU-float32 nem, a device útvonala a valószínű ok. A következtetés konzisztenciamintázat, nem ok-okozati bizonyíték; egyetlen GPU-verzió nem általánosít más hardverre.

33.1.8 8. Tipikus tévedés, önellenőrzés és visszahívás

Tipikus tévedés. „A .pt kiterjesztés garantálja, hogy a fájl biztonságos, mert a PyTorch saját formátuma.” Ellenpélda: a .pt fájl pickle, amely tetszőleges Python objektumot szerializálhat; a kiterjesztés semmit sem ír elő a tartalomról. A weights_only=True a deszerializációt korlátozza, de a betöltött súlyok ettől még származhatnak más architektúrából, más adattartományból vagy módosított forrásból. A biztonság és a modellhelyesség két, egymástól független ellenőrzés.

Önellenőrzés. (1) Fel tudja sorolni a modellcsomag öt komponensét anélkül, hogy a súlyfájlt a modellel azonosítaná? (2) A load_state_dict melyik argumentuma ellenőrzi a kulcs- és alakilleszkedést, és mi történik eltérésnél? (3) Miért nem elegendő a seed a betöltött modell reprodukálhatóságához, ha a modult train() módban hagyjuk dropoutos réteggel? (4) A bitazonossági tétel melyik feltétele sérül, ha a kontrollinputot kép helyett véletlen tenzorral helyettesítjük?

Korábbi fogalom visszahívása. A 32.1. Szekvenciamodell és változó hossz szekvenciaállapota megmutatja, hogy a forward alatt képződő rejtett állapot nem része a state_dict-nek; ezzel szemben a batch normalizáció futó átlaga igen. A 29.1. Lineáris és nemlineáris autoencoder autoencoder-szerződése ugyanígy input- és output-sémát rögzített: a súlyok csak a hozzájuk tartozó leképezéssel együtt értelmesek. A 21.1. Inicializálás és varianciaterjedés inicializálási seedje a referencia-előállítást teszi reprodukálhatóvá, de a betöltés utáni determinisztikus eval forward már nem igényel seedet.

33.1.9 9. Rövid összefoglaló

A modellcsomag a state_dict-en túl az architektúrát, az input sémát, a labeltérképet és a verziót is rögzíti. A weights_only=True betöltés csak a deszerializációt korlátozza; a súlyok helyességét a manifest, a kulcsegyezés és a kontrollpredikció ellenőrzi. A kontrollpredikció ekvivalenciatétele kimondja: determinisztikus forward, bitazonos input és paraméterek, azonos dtype, device és eval-állapot mellett a logitok bitre egyeznek. A labor egy új processzben visszatöltött GlyphCNN-t vet össze a referencia logitokkal. A gyakori hiba, hogy a fájlkiterjesztést biztonsági garanciának tekintik.

33.1.10 Következő alkalmazás

A 33.2. Batch és kis szolgáltatás egységben ezt a szerződést batch-feldolgozású, kis szolgáltatásarchitektúrába helyezzük: a bemeneti köteg méretét, az inference-szolgáltatás hívási szerződését és a késleltetés helyes mérését vizsgáljuk.

33.1.11 Felhasznált források