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.
Definíció (modellcsomag). Egy osztályozó modellcsomag a következő öt komponens együttesét jelenti:
nn.Module
osztálydefiníció, amely a forward számítást meghatározza.state_dict): a
paraméterek és a tanítás során frissülő pufferek rendezett kulcs–érték
leképezése.Definíció (state_dict). A state_dict a
modul regisztrált paramétereinek és puffereinek rendezett leképezése:
,
ahol
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 , tartományú float32 tensor elemeihez az 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 , , .
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
,
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.
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 a modul forwardja, ahol a paramétertér, és tegyük fel, hogy az alábbi feltételek mindegyike teljesül:
state_dict kulcsai, alakjai
és értékei bitre egyeznek a referenciával.eval() állapotban van, és
a forward minden művelete determinisztikus függvény.Ekkor az új processz logitmátrixa bitre egyezik a referencia logitmátrixszal:
Bizonyítás. A forward 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.
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.
Az affin réteg a 3.2. Mátrixszorzás és affín
réteg szerint
.
Egy nn.Linear(2,1) modul state_dict-je két
kulcsot tartalmaz:
| Kulcs | Alak | Jelentés |
|---|---|---|
0.weight |
a súlymátrix | |
0.bias |
a biasvektor |
Legyen a kézi példában
A logit:
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.
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) |
||
pool = MaxPool2d(2) |
||
conv2 = Conv2d(8,16,3,padding=1) |
||
pool |
||
fc = Linear(16\cdot 36,3) |
Az összeg .
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.pyA 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.
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 maradtLokalizá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.
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
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.
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.
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.
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.