Ez az alfejezet azt a kérdést rendezi, mit jelent valójában
„elmentettük a modellt”. A cél: megkülönböztetni a csak következtetésre
alkalmas súlyállapotot a tanítás folytathatóságát biztosító teljes
állapottól, és megadni, milyen feltételek mellett ad egy megszakított
tanítás a megszakítás nélkülivel azonos következő frissítést.
Előismeret: a 12.1. Tanítás, validáció és
kiértékelési mód tanítási iterációja és eval-módja, a 11.2. nn.Module és paraméterregisztráció
state_dict- és bufferfogalma. Induló becslés: elég-e a
paraméterek mentése ahhoz, hogy a tanítás ott folytatódjon, ahol
abbamaradt? Momentumos optimalizáló esetén a válasz nem, és ezt alább
egy számpélda pontosan megmutatja.
Definíció. Egy modell state_dict-je
(állapotszótár) rendezett kulcs–érték leképezés, amely a modell összes
Parameter és perzisztens (alapértelmezett,
persistent=True jelzéssel regisztrált) buffer értékét
tartalmazza; a persistent=False bufferek kimaradnak belőle.
A kulcsok a modulhierarchiát tükrözik, például egy
nn.Sequential(nn.Linear(3,4), nn.ReLU(), nn.Linear(4,1))
modellben:
0.weight alakja
,
0.bias alakja
,2.weight alakja
,
2.bias alakja
.BatchNorm-os modellben a bufferek is a state_dict-ben
vannak: running_mean alakja
,
running_var alakja
,
num_batches_tracked egy int64 tenzor. A
state_dict értékei tenzorok; a kulcsok stringek.
Inference-only állapot. Ha csak a modell
state_dict-jét mentjük, a modell újra betölthető és
model.eval() módban előre számítható, de a tanítási
folyamat belső állapota elveszett. Az optimizer momentum- vagy
Adam-pufferei, az ütemező számlálója és a véletlenszám-generátor
állapota nincs benne.
Teljes tanítási állapot. A folytathatósághoz az alábbi komponensek együttes mentése szükséges:
model.state_dict() —
tanuló paraméterek és nem tanuló bufferek.optimizer.state_dict() — param_groups (például
lr, betas, eps,
weight_decay) és a paraméterenkénti belső puffer, például
momentumos SGD-nél momentum_buffer, Adamnél
exp_avg, exp_avg_sq és step.scheduler.state_dict() — a tanulási ráta ütemezésének belső
pozíciója.torch.randperm,
inicializálás) torch.get_rng_state(); ha a
DataLoader saját generátort kap
(DataLoader(..., generator=g)), a
g.get_state(); NumPy új API-nál
(rng = np.random.default_rng(seed)) az
rng.bit_generator.state, a régi globális API-nál
np.random.get_state(); a Python random
moduljánál random.getstate(). Amit a kód nem használ, azt
nem kell menteni; amit használ, de nem mentünk, az a folytatás után más
véletlen sorozatot ad.epoch,
global_step, valamint a napló szerinti
best_val_loss és best_step a legjobb modell
kiválasztásához.num_workers>0, a workerek előre
betöltött (prefetch), de még fel nem dolgozott batch-ei miatt az
iterátor állapota nem menthető közvetlenül: az elhasznált batchszámból
kell újraépíteni, a worker-seedeket pedig a mentett generátorállapotból
rögzíteni.torch.cuda.get_rng_state_all(); kevert pontosságnál a
GradScaler állapota, scaler.state_dict()
(aktuális skála, növekedési számláló). Enélkül a folytatás első lépései
más skálával futnak, és más lépéseket dobnak el, mint a megszakítás
nélküli futás.A tankönyv CPU-s főútján az 1–6. pont a követelmény; a 7. pont csak a GPU-s, AMP-t használó bővített útra vonatkozik.
Az epoch nem tanult állapot, hanem pozíciószámláló: azt
mondja meg, hányadik teljes adatbejárásnál tartunk. A
best snapshot ettől különböző fogalom: a validációs
veszteség szerint eddig legjobb modell rögzített, független
másolata. A helyes másolat
{k: v.detach().clone() for k, v in model.state_dict().items()}
vagy copy.deepcopy(model.state_dict()). A sima referencia
megtartása hiba, mert a state_dict() tenzorai a modell
paramétereinek háttértárolójára mutatnak, így a következő
optimizer.step() in-place frissítése a „legjobb” másolatot
is megváltoztatja.
Legyen egy tanítási lépés az
függvény, ahol a paraméterek és bufferek együttese, az optimizer belső állapota, az RNG-állapot, a batch-gradiens, pedig az aktuális tanulási ráta.
Lemma (azonos következő frissítés platformon belül). Ha az által végrehajtott lebegőpontos műveletek sorrendje, dtype-ja és eszköze két futásban azonos, determinisztikus, és a mindegyike bitazonos a két futásban, akkor a következő frissítés eredménye is bitazonos ugyanazon a platformon és könyvtárverzión.
Bizonyításvázlat. Ez a determinisztikus függvény definíciójából közvetlenül következik: azonos bemenet azonos kimenetet ad. A valódi, nem triviális tartalom a feltételrendszerben van. A float összeadás nem asszociatív, ezért a bitazonossághoz a műveletek azonos sorrendje is kell, nem csak az azonos matematikai összeg. A [D02] dokumentáció ezt a platformok közötti irányba is kiterjeszti: a seed önmagában nem garantál bitazonosságot más platformon, verzióban vagy kernelválasztásnál. A lemma tehát nem keresztplatformos ígéret; ez a tankönyv kísérleti főútján kis CPU-s, azonos környezetű futásokra vonatkozik.
A szükséges állapot és feltételek listája együtt adja a választ a fejezet levezetési kötelezettségére:
exp_avg/exp_avg_sq/step,
valamint a paramétercsoport-beállítások.param_groups['lr']; schedulerrel annak állapota is.torch.use_deterministic_algorithms(True).Momentumos SGD, PyTorch-konvenció: a momentum-puffer, a gradiens, a tanulási ráta, a momentum, dampening és weight decay nélkül:
Legyen , , , . Az első batch gradiense , a másodiké .
Az eltérés . A különbség a következő lépésekben tovább halmozódik. Ez az ellenpélda mutatja, hogy az inference-súly nem elégséges a tanítás folytatásához.
A labor munkakönyvtára lab/n046_checkpoint/. A cél annak
kimutatása, hogy a teljes állapotból folytatott kis CPU-futás
paramétereiben és veszteségében azonos a kontrollal. A script
szándékosan nem használ DataLoader-t:
teljes batch-tel dolgozik, így a [D08] iterátorállapot kérdése nem
terheli a reprodukciót. Az adat 16 darab 3-dimenziós szintetikus
bemenet, a modell a 11.2. nn.Module és
paraméterregisztráció alfejezetből ismert 3-4-1 MLP, a loss MSE, az
optimizer momentumos SGD.
import os
import torch
from torch import nn, optim
HERE = "lab/n046_checkpoint"
os.makedirs(HERE, exist_ok=True)
SEED = 20261020
DTYPE = torch.float32
DEVICE = torch.device("cpu")
LR = 0.5
MOMENTUM = 0.9
STEPS_A = 2
TOTAL = 4
torch.manual_seed(SEED)
torch.use_deterministic_algorithms(True)
X = torch.linspace(-1.0, 1.0, 48, dtype=DTYPE).reshape(16, 3)
Y = (1.5 * X[:, 0] + 0.5 * X[:, 1] - 0.7 * X[:, 2] + 0.2).unsqueeze(1)
def make_model():
torch.manual_seed(SEED)
return nn.Sequential(nn.Linear(3, 4), nn.ReLU(), nn.Linear(4, 1)).to(DEVICE)
def one_step(model, optimizer):
model.train()
optimizer.zero_grad()
loss = torch.nn.functional.mse_loss(model(X), Y)
loss.backward()
optimizer.step()
return loss.item()
# 1. Kontroll: négy lépés egyben.
model_cont = make_model()
opt_cont = optim.SGD(model_cont.parameters(), lr=LR, momentum=MOMENTUM)
losses_cont = [one_step(model_cont, opt_cont) for _ in range(TOTAL)]
# 2. Checkpoint-ág: két lépés, teljes állapot mentése.
model_a = make_model()
opt_a = optim.SGD(model_a.parameters(), lr=LR, momentum=MOMENTUM)
losses_a = [one_step(model_a, opt_a) for _ in range(STEPS_A)]
cp = {
"model": model_a.state_dict(),
"optimizer": opt_a.state_dict(),
"epoch": 0,
"step": STEPS_A,
"rng": torch.get_rng_state(),
}
torch.save(cp, f"{HERE}/checkpoint.pt")
# 3. Új folyamat szimulálása: friss modell és optimizer, betöltés.
model_b = make_model()
opt_b = optim.SGD(model_b.parameters(), lr=LR, momentum=MOMENTUM)
cp2 = torch.load(f"{HERE}/checkpoint.pt", weights_only=False)
model_b.load_state_dict(cp2["model"])
opt_b.load_state_dict(cp2["optimizer"])
torch.set_rng_state(cp2["rng"])
losses_b = [one_step(model_b, opt_b) for _ in range(TOTAL - STEPS_A)]
losses_chk = losses_a + losses_b
# 4. Bitazonossági ellenőrzés a paramétereken.
max_abs = max((p1.detach() - p2.detach()).abs().max().item()
for p1, p2 in zip(model_cont.parameters(), model_b.parameters()))
print("control_losses:", [f"{v:.6f}" for v in losses_cont])
print("resumed_losses:", [f"{v:.6f}" for v in losses_chk])
print(f"max_abs_diff={max_abs:.3e}")
assert max_abs < 1e-6
# 5. A referenciamásolat hibájának bemutatása.
snapshot_ref = model_b.state_dict() # referencia, nem független másolat
one_step(model_b, opt_b) # in-place mutáció
print("reference_is_mutated:",
snapshot_ref["0.weight"].equal(model_b[0].weight))
snapshot_best = {k: v.detach().clone() for k, v in model_b.state_dict().items()}
one_step(model_b, opt_b)
print("clone_is_independent:",
not snapshot_best["0.weight"].equal(model_b[0].weight))Futtatási parancs a munkakönyvtárból:
python checkpoint_lab.py. Az átadandó eredmény a
lab/n046_checkpoint/checkpoint.pt fájl, a
control_losses és resumed_losses listák, a
max_abs_diff érték, valamint a
reference_is_mutated és clone_is_independent
logikai értékek. Az alábbi kimenet a kód logikája alapján várt,
nem e kéziratban elvégzett futtatás:
<table> <tr><th>Ellenőrzés</th><th>Várt érték</th><th>Értelmezés</th></tr> <tr><td><code>max_abs_diff</code></td><td><code>< 1e-6</code>, e felépítésben <code>0.000e+00</code></td><td>bitazonos folytatás azonos platformon</td></tr> <tr><td><code>reference_is_mutated</code></td><td><code>True</code></td><td>a referencia követi a mutációt</td></tr> <tr><td><code>clone_is_independent</code></td><td><code>True</code></td><td>a mély másolat független</td></tr> </table>
A weights_only=False itt azért szükséges, mert a teljes
tanítási állapot nem csak tenzorokat tartalmaz: az optimizer
state része int és float metaadatokat is tárol. [D09]
hangsúlyozza, hogy nem megbízható forrásból származó adatnál a
weights_only=True a biztonságos alapbeállítás; a labor
saját, megbízható checkpointot állít elő, ezért a teljes objektum
visszaállítása itt indokolt.
F1 (fogalmi, nehézség 1). Fogalmazd meg, mi a
különbség az inference-only state_dict és a teljes tanítási
állapot között, és sorolj fel legalább négy komponenst az utóbbiból.
Elvárt átadandó: definíció, komponenslista és egy példa, amelyben a
hiányzó komponens megváltoztatná a következő frissítést.
F2 (levezetési, nehézség 2). A momentumos SGD képleteivel számold ki a , , , sorozatot a , , gradiensekre. Add meg a 3. lépés előtti teljes állapotot, majd hasonlítsd össze a helyes és a csak -ből indított hibás folytatást. Elvárt átadandó: képletek, minden közbülső érték és a két közötti eltérés.
F3 (implementációs, nehézség 2). Írj egy
save_training_state(model, optimizer, scheduler, epoch, step, path)
függvényt, amely a teljes állapotot menti, és egy
load_training_state(path, build_fn) függvényt, amely friss
modellt, optimizert és schedulert épít a build_fn hívással,
majd betölti az állapotot. A labor MLP-jén ellenőrizd a betöltés utáni
folytatást. Elvárt átadandó: futtatható kód, mentett fájl,
max_abs_diff < 1e-6.
F4 (hibakeresési, nehézség 2). Adott a hiba:
best_state = model.state_dict() elmentése a validáció után,
majd további tanítási lépések, és a best_state a
kiértékeléskor az új súlyokat tartalmazza. Lokalizáld az okot, írd le a
következményt, add meg a javítást, és olyan ellenőrzést, amely a hiba
jelenlétét kimutatja. Elvárt átadandó:
ok–következmény–javítás–ellenőrzés lánc, data_ptr vagy
torch.equal alapú teszt.
F5 (kísérleti/transzfer, nehézség 3). Tervezz kísérletet, amely 4 különböző seed mellett vizsgálja, hogy a checkpointból folytatott futás veszteséggörbéje statisztikailag egyenértékű-e a megszakítás nélküli kontrolléval. Adj protokollt: kérdés, adat, megfigyelési egység, baseline, változó, metrika, erőforrás, a CI értelmezése és a következtetési korlát. Elvárt átadandó: 8-10 lépéses terv, alternatív metrika indoklása és a [S02]-höz illeszkedő feltételes értelmezés.
F1 megoldás. Az inference-only állapot a modell
state_dict-je: paraméterek és bufferek, tehát a súlyok és a
futó statisztikák. Predictionre elég, de a tanítási folyamat nem
folytatható belőle. A teljes állapot ezen felül tartalmazza: (i)
optimizer state_dict (momentum/Adam-pufferek,
step, paramétercsoportok), (ii) scheduler
state_dict, (iii) RNG-állapot, (iv) pozíció
(epoch, global_step), szükség esetén (v)
adatbetöltő-pozíció. Példa: ha csak a modell state_dict-jét
mentjük momentumos SGD-nél, az újraindulás
pufferrel számol, így az első folytatott lépés más irányú, mint a
megszakítás nélküli futásban.
F2 megoldás. A frissítés: , .
Eltérés: . Alakellenőrzés: minden mennyiség skalár, a gradiensek és a puffer azonos mértékegységű; a puffer elhagyása a gradiens előzményét nullázza.
F3 megoldás.
import os, torch
from torch import nn, optim
HERE = "lab/n046_checkpoint"
os.makedirs(HERE, exist_ok=True)
def save_training_state(model, optimizer, scheduler, epoch, step, path):
state = {
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"scheduler": scheduler.state_dict() if scheduler is not None else None,
"epoch": epoch,
"step": step,
"rng": torch.get_rng_state(),
}
torch.save(state, path)
def load_training_state(path, build_fn):
state = torch.load(path, weights_only=False)
model, optimizer, scheduler = build_fn()
model.load_state_dict(state["model"])
optimizer.load_state_dict(state["optimizer"])
if scheduler is not None and state["scheduler"] is not None:
scheduler.load_state_dict(state["scheduler"])
torch.set_rng_state(state["rng"])
return model, optimizer, scheduler, state["epoch"], state["step"]A build_fn a labor make_model()-jét és az
SGD-konstrukciót adja vissza. Az ellenőrzés a labor 3-4. lépését
ismétli: a visszatöltött modellen két további lépés után a paraméterek
max abszolút eltérése a kontrolltól
.
F4 megoldás. Ok: a model.state_dict()
shallow másolat, az értéktenzorok a modell paramétereinek
háttértárolójára mutatnak; az optimizer.step() jellemzően
in-place (add_) frissít, ezért a tárolt
best_state is mutálódik. Következmény: a „legjobb” modell
nem a kiválasztási pillanat súlyait őrzi, hanem a későbbi, rosszabb
validációs állapotot, tehát a tesztkiértékelés optimistává vagy
torzítottá válik. Javítás:
copy.deepcopy(model.state_dict()) vagy
{k: v.detach().clone() for k, v in model.state_dict().items()}.
Ellenőrzés: a másolat data_ptr()-e térjen el a
paraméterétől, vagy egy in-place lépés után
snapshot["0.weight"] ne legyen torch.equal a
model[0].weight-tel. A labor 5. pontja éppen ezt a negatív
és pozitív tesztet mutatja.
F5 megoldásminta. (1) Kérdés: a teljes állapotból
folytatott futás végső val loss-ja azonos eloszlású-e a megszakítás
nélküli kontrolléval. (2) Adat: GlyphLab data/images.npy,
data/labels.npy, train split osztályonkénti
jelenet-struktúrával; a validálás a val spliten, scene-szintű
felosztással. (3) Megfigyelési egység: egy seed által definiált teljes
futás; a képek nem függetlenek a scene-ek miatt. (4) Baseline:
megszakítás nélküli 4 epoch. (5) Kezelés: 2 epoch után mentés, új
modell/optimizer építés, betöltés, további 2 epoch. (6) Metrika:
seedenként párosított különbség a kontroll és a folytatott futás végső
val loss-ában, és max_abs_diff a paramétereken; a
bitazonosság egy seeden belül, a statisztikai egyenértékűség a seedek
közötti páros különbségek átlagával vizsgálható. (7) Intervallum: a
páros különbségek átlagára
-alapú
CI a [S02] feltételes, ismételt mintavételi értelmezésével; 4 seed kis
minta, ezért az intervallum széles és csak iránymutató. (8) Erőforrás:
CPU, kis MLP, előtanított súly nélkül; a négy seed ismétlése becsült
helyi futásidővel. (9) Következtetési korlát: az eredmény csak a
vizsgált platformra, verzióra és adatra vonatkozik; nem bizonyít
keresztplatformos bitazonosságot, és nem általánosít más
architektúrára.
A klasszikus hiba a best_state = model.state_dict()
referencia tárolása. A fenti labor 5. pontja méri: egy további lépés
után reference_is_mutated igaz, mert a tárolt tenzor
ugyanarra a memóriaterületre mutat. Önellenőrző kérdések: (1) Miért nem
elegendő a súlymentés momentumos SGD-nél? (2) A betöltésnél miért kell
az optimizer state_dict-je is? (3) A seed-platform
bitazonosságot garantál? Válaszok: (1) a momentum-puffer az előző
gradiensek állapotát hordozza; (2) mert a frissítés pufferből indul; (3)
nem, a [D02] szerint a seed nem garantál platformok közötti
bitazonosságot. Visszahívás: a 11.2. nn.Module és
paraméterregisztráció state_dict-je a paramétereket és
puffereket együtt kezeli; a 8.2. Véges
hipotézisosztály egyenletes korlátja konfidenciaintervallum-logikája
pedig arra figyelmeztet, hogy a seedek közötti szórás és a betanított
modell kockázatának bizonytalansága külön fogalom, a bitazonosság
egyetlen seeden belül sem statisztikai garancia a generalizációra.
A checkpoint nem egyszerűen a súlyok mentése. A csak következtetési
state_dict a paramétereket és buffereket tartalmazza, de a
tanítás folytatásához az optimizer pufferei, az ütemező állapota, az
RNG-állapot és a pozíció is szükséges. A folytathatósági lemma szerint
azonos platformon, dtype-on és műveletsorrend mellett a teljes állapot
bitazonossága azonos következő frissítést ad; platformok között ez nem
garantált. A labor kétlépéses megszakítással és újraépített modellel
ellenőrzi a paraméterek azonosságát, és kimutatja, hogy a
state_dict referencia hiba, a detach().clone()
független másolat.
A 12.3. Sanity check és kis mintára túlillesztés sanity check ezt a folytathatóságot használja fel: a mentett állapotból kiindulva kis mintára való túlillesztéssel ellenőrzi, hogy a modell és az optimizer együtt valóban képes tanulni, mielőtt hosszabb kísérletet indítanánk.