Ez az egység a 11.3. Dataset és DataLoader a
split után adatbetöltőjét kapcsolja össze a tanítási ciklussal. Egy
batchre felírjuk a teljes PyTorch-lépést: zero_grad,
forward, loss, backward, gradiens-ellenőrzés,
optimizer.step. A cél annak pontos megértése, hogy az
autograd által előállított gradiens mikor és hogyan válik
paraméterfrissítéssé.
Előismeret: 11.3. Dataset és DataLoader a split
után Dataset/DataLoader, 11.2. nn.Module és
paraméterregisztráció nn.Module, 11.1. Tensor és autograd-életciklus
autograd-életciklus. Visszautalás: 10.4. Tanítási
lépés és tanuló MLP a kézzel írt tanítási lépés, 7.4. Numerikus stabilitás és jelentésmegőrző
implementáció a stabil loss-számítás.
Induló kérdés: ha loss.backward() sikeresen lefutott,
ettől miért maradhat a súly változatlan?
Egy batchre a következő hívássorrendet rögzítjük:
1. optimizer.zero_grad() # a .grad tárolók nullázása
2. logits = model(x) # forward
3. loss = criterion(logits, y) # skalár loss
4. loss.backward() # gradiens a levelek .grad mezőjében
5. (opcionális) gradiens-ellenőrzés / clip
6. optimizer.step() # paraméterfrissítés
Az 1. lépés azért előzi meg a 4.-et, mert a backward a
.grad mezőben akkumulál, nem felülír. Egy elmaradt
zero_grad hatására több batch gradiense összeadódik, ami
nem az aktuális minibatch SGD-frissítés.
Logits. Az affin réteg aktiváció előtti kimenete . Egy osztályozó fejben ahol , , . A logit nem valószínűség, és a célvektor osztályindexekből áll.
Loss. A főúton a [D07] szerinti
CrossEntropyLoss-t használjuk logits bemenettel és int64
célindexekkel; softmaxot nem hívunk külön. A
reduction="mean" konvenció:
A skalár loss egy zéró dimenziós
tensor. A 7.4. Numerikus stabilitás és
jelentésmegőrző implementáció alfejezetben tárgyalt
logsumexp-stabilizálást a könyvtári implementáció végzi; a levezetésben
a matematikai képlettel dolgozunk.
zero_grad. Az optimizer minden
hozzárendelt paraméter .grad tárolóját nullázza vagy
None-ra állítja. A pontos viselkedés a
set_to_none opciótól függ; a labor a visszaellenőrzésben a
gradiens értékét, nem a None-kezelést
teszteli.
backward. A zéró dimenziós lossból
kiindulva reverse-mode autograddal kiszámítja a gráf leveleinek
gradiensét. A levelek itt a modell paraméterei, tehát
p.grad alakja megegyezik p alakjával. A
backward után a számítási gráf alapértelmezésben
felszabadul.
optimizer.step. Az éppen tárolt
.grad értékek alapján frissíti a paramétereket. Az alap
torch.optim.SGD momentum nélkül a
frissítést végzi, ahol
a lr,
.
Véges gradiens (finite gradient). Egy paraméter
gradiensét végesnek nevezzük, ha minden eleme véges lebegőpontos érték,
azaz torch.isfinite(g).all() igaz. A NaN és a
nem véges; ilyen értékkel az SGD-frissítés is nem véges paramétereket
adna, ezért a lépés előtt megállítjuk a futást.
backward gradiens és az SGD-frissítés levezetéseA levezetési kötelezettséghez először a veszteség logits szerinti gradiensét vezetjük le, majd ebből a lineáris réteg paramétergradienseit.
Állítás (elemi levezetés). Legyen , és legyen . A fenti mellett
Levezetés. Rögzítsük az megfigyelést. A softmax deriváltja mert a tört deriváltjában a számláló és a nevező tagjai adják a két tagot. Mivel , Ha , akkor , és a hányados . Ha , akkor a derivált , és a hányados . A két eset együtt: Az megfigyelésre átlagolt loss miatt az szorzó megjelenik.
Legyen Mivel , a láncszabályból: A második képletből látszik, hogy ebben az utolsó lineáris rétegben a bias gradiens azonosan nulla, ha minden osztályhoz ugyanaz a bias tolás tartozik: a softmaxot és a loss-t invariánsan hagyja. Ez a konstanseltolási invariancia közvetlen következménye a 7.4. Numerikus stabilitás és jelentésmegőrző implementáció alfejezetben tárgyalt logsumexp-tulajdonságnak.
A PyTorch backward implementációjától tehát azt várjuk,
hogy a fenti két mátrixot írja a model.weight.grad és
model.bias.grad mezőkbe. Az alábbi f64-kontroll ezt
numerikusan ellenőrzi; a numerikus egyezés nem bizonyítja az
implementáció általános helyességét, de független hibakeresési
bizonyíték.
Legyen , , , , .
A logitok: A softmax kerekítve: Ebből A súlygradiens kerekítve: és a fenti invariancia miatt. Az SGD-frissítés:
Az alábbi szkript a fenti számítást float64 pontosságon,
CPU-n ellenőrzi, majd a véges-gradiens védelmet is bemutatja. A kód
futtatható a lab munkakönyvtárból; a kézirat most nem állít
elvégzett futást, a labor során a két True és a tervezett
RuntimeError a várt kimenet.
# lab/n044_train_step.py
import torch
torch.manual_seed(20261020)
torch.set_default_dtype(torch.float64)
device = torch.device("cpu")
d, K, N = 3, 2, 2
x = torch.tensor([[0.5, 1.0, -0.5],
[-1.0, 0.25, 2.0]], device=device)
y = torch.tensor([0, 1], dtype=torch.int64, device=device)
model = torch.nn.Linear(d, K, bias=True).to(device)
with torch.no_grad():
model.weight.copy_(torch.tensor([[0.2, -0.4, 0.1],
[0.3, 0.15, -0.2]], device=device))
model.bias.copy_(torch.tensor([0.05, -0.05], device=device))
criterion = torch.nn.CrossEntropyLoss(reduction="mean")
optimizer = torch.optim.SGD(model.parameters(), lr=0.25)
# A lépés sorrendje
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
# Kézi gradiens a levezetett képletből
p = torch.softmax(logits, dim=1)
y_onehot = torch.nn.functional.one_hot(y, num_classes=K).to(torch.float64)
G = (p - y_onehot) / N
expected_grad_w = G.T @ x
expected_grad_b = G.sum(dim=0)
print(torch.allclose(model.weight.grad, expected_grad_w, rtol=1e-10, atol=1e-10))
print(torch.allclose(model.bias.grad, expected_grad_b, rtol=1e-10, atol=1e-10))
# Várt SGD-frissítés ellenőrzése
with torch.no_grad():
w0 = model.weight.data.clone()
b0 = model.bias.data.clone()
expected_w = w0 - 0.25 * expected_grad_w
expected_b = b0 - 0.25 * expected_grad_b
optimizer.step()
print(torch.allclose(model.weight.data, expected_w, rtol=1e-10, atol=1e-10))
print(torch.allclose(model.bias.data, expected_b, rtol=1e-10, atol=1e-10))
# Véges-gradiens védelem: a step előtt állítsuk meg a nem véges futást
def assert_finite_grad(model):
for name, p in model.named_parameters():
if p.grad is None:
continue
if not torch.isfinite(p.grad).all():
raise RuntimeError(f"Nem véges gradiens a {name} paraméterben.")
model.zero_grad()
# Új forward gráf: különben a torch.full_like leválasztaná a logitsot,
# és a bad_loss.backward() már a differenciálás előtt RuntimeError-ral leállna.
bad_logits = model(x) * float("nan")
bad_loss = criterion(bad_logits, y)
bad_loss.backward()
try:
assert_finite_grad(model)
except RuntimeError as e:
print("Tervezett leállás:", e)A bad_logits az új model(x) gráfjához
kapcsolódik, így a bad_loss.backward() végrehajtható, és a
paraméterek gradiense NaN lesz. Egy önálló
torch.full_like(logits, float("nan")) tensorral ez nem
működne, mert a veszteségnek nem lenne differenciálható útja a
modellparaméterekig. A assert_finite_grad hívás a kivételt
dobja, ezért az optimizer.step nem fut le, és a paraméterek
nem válnak NaN-né a lépésben. A védelmet mindig a
step előtt kell meghívni.
Tévedés: „Az optimizer automatikusan lefut, amint a
loss.backward() sikeres volt.” Ez hamis: a
backward csak a gradiens tárolókat tölti fel; a
paraméterfrissítés külön optimizer.step() hívás.
Ellenpélda:
logits = model(x)
loss = criterion(logits, y)
optimizer.zero_grad()
loss.backward()
# NINCS optimizer.step()
print(model.weight.data is not None) # True, a súly létezik
print(model.weight.grad is not None) # True, a gradiens kiszámult
# A súly értéke mégis változatlan marad.A két True ellenére a paraméter nem frissült, mert a
frissítést semmi sem hívta. Ennek ellenőrzése a laborban: mentsük le a
súlyt a backward előtt, majd hasonlítsuk össze a lépés
utáni értékkel; hiányzó step esetén a két tensor bitre
egyezik.
F1 (fogalmi, nehézség 1). Adja meg a tanítási lépés
hat hívásának helyes sorrendjét, és magyarázza el, miért nem mindegy,
hogy zero_grad a backward előtt vagy után
szerepel. Illusztráljon egy rossz sorrendet kis gondolatkísérlettel.
Elvárt eredmény: a sorrend és az akkumuláció következménye.
F2 (levezetési, nehézség 2). Az
,
,
esetben legyen
,
,
,
,
.
Számítsa ki
,
,
a backward után várható
és
értékeket, majd a frissített
és
paramétereket. Kerekítsen 4 tizedesjegyre. Elvárt eredmény: a két
gradiens és a két frissített paraméter.
F3 (implementációs, nehézség 2). Írjon olyan
train_step(model, x, y, criterion, optimizer) függvényt,
amely a megfelelő sorrendben elvégzi a lépést, a step előtt
minden paraméter gradiensét végesnek ellenőrzi, és nem véges esetén
RuntimeError-t dob. Térjen vissza a loss
item() értékével. Tesztelje az F2 példáján. Elvárt
eredmény: teljes kód és egy sikeres hívás kimenete.
F4 (hibakeresési, nehézség 1). Az alábbi kódban a
loss nem változik több lépés után sem, bár a gradiens
None-tól különböző. Hol a hiba, mi a következménye, és
hogyan javítja?
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
# néhány diagnosztikai nyomtatás
print(loss.item())Elvárt eredmény: a hiba azonosítása, a kimaradt hívás neve, a javítás és az ellenőrzés leírása.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen egybatch-es diagnosztikai kísérletet, amely meggyőződik arról, hogy a véges-gradiens védelem valóban megállítja a futást, mielőtt a paraméterek nem véges értéket kapnának. Adja meg: kérdés, adat, baseline, változó tényező, metrika, a várható kivétel típusa és a következtetési korlát. Elvárt eredmény: korrekt protokoll, alternatív megoldás és a korlát megnevezése.
F1. Helyes sorrend:
optimizer.zero_grad(), logits = model(x),
loss = criterion(logits, y), loss.backward(),
gradiens-ellenőrzés, optimizer.step(). A
backward a .grad mezőt a kiszámolt gradienssel
bővíti. Ha a zero_grad a backward
után futna le, a frissítéshez szükséges gradiens törlődne, és az SGD
súlyváltozása nulla lenne. Ha a zero_grad teljesen elmarad,
két egymás utáni batch gradiense összeadódik, és a lépés nem az aktuális
batch átlagának, hanem az összegének megfelelően frissít. A rossz
sorrendre példa: backward után zero_grad, majd
step; a gradiens mindig nulla.
F2. A logit:
A softmax:
,
,
összeg
,
így
és
,
ezért
.
A súlygradiens oszlopvektor:
a biasgradiens
.
A frissítés
:
Alakellenőrzés:
,
.
A f64 kódban torch.allclose rtol=1e-10, atol=1e-10 mellett
egyezik a backward eredményével.
F3. Egy lehetséges megoldás:
def train_step(model, x, y, criterion, optimizer):
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
for name, p in model.named_parameters():
if p.grad is None:
raise RuntimeError(f"{name}: hiányzó gradiens")
if not torch.isfinite(p.grad).all():
raise RuntimeError(f"{name}: nem véges gradiens")
optimizer.step()
return loss.item()Az F2 példájára hívva a visszatérés a megfelelő loss
item(), a modell súlya pedig a kiszámolt
értékre módosul. A zero_grad a backward előtt, a
step a végső ellenőrzés után van, így akár hiányzó gradiens
(p.grad is None), akár NaN/inf
megállítja a futást.
F4. A hiba a hiányzó optimizer.step()
hívás. A backward feltölti a gradienseket, a loss
kiszámítása és a gradiens léte tényleges, de a paraméterek nem
frissülnek, ezért a következő forward ugyanazt a loss-t adja. A javítás
a diagnosztikai nyomtatás után egy optimizer.step() hívás.
Ellenőrzés: a step előtt elmentett
w_before = model.weight.data.clone() és a step
utáni w_after nem egyezik bitre; a loss a következő
forwardon csökken. A javított kód minden iterációban megismétli a
zero_grad–forward–loss–backward–step
sort.
F5. Egy tervezhető kísérlet:
NaN
logitokat adunk a loss-hoz, így a gradiens nem véges lesz.RuntimeError), és a paraméterek értéke a védelem után
megegyezik a lépés előtti értékkel.NaN injektálás
determinisztikus teszt, de nem bizonyítja, hogy éles adatokon minden nem
véges okot a védelem időben elkap; a túl nagy tanulási ráta okozta
inf például már a paraméterfrissítés után is csak a
következő lépésben jelenhet meg. A labor ezért a gradienst a
step előtt ellenőrzi, a loss és a paraméterek
isfinite állapotát pedig a lépés után is naplózza.Önellenőrzés: a backward után a súlygradiens alakja
megegyezik a súly alakjával
,
a biasgradiens
;
a kézi példában a biasgradiens nulla a softmax konstanseltolási
invarianciája miatt. A numerikus összevetést f64-ben,
rtol=1e-10, atol=1e-10 értékkel végezzük. A 10.4. Tanítási lépés és tanuló MLP alfejezetben
kézzel végzett három művelet itt pontosan a zero_grad,
backward és step hármasnak felel meg; a 7.4. Numerikus stabilitás és jelentésmegőrző
implementáció alfejezetben tárgyalt stabil loss itt élesben is véd
az overflow ellen, de a gradiens végessége külön futás előtti
ellenőrzés.
A PyTorch-tanítási lépés sorrendje szerződés: zero_grad,
forward, loss, backward, véges-gradiens ellenőrzés, majd
optimizer.step. A backward a közvetlen
lineáris réteg plusz CrossEntropyLoss esetén a
súly- és
biasgradienst állítja elő, ahol
.
Az SGD-lépés
,
és a labor f64-ben numerikusan összeveti a levezetett értékkel. A
backward sikere önmagában nem frissíti a paramétereket; a
step előtti isfinite ellenőrzés a
NaN/inf gradiensből eredő hibát állítja meg. A
következő 12.1. Tanítás, validáció és kiértékelési
mód egység a teljes training-validáció és eval mód keretét építi
erre a lépésre.