A 10.3. Gradiensellenőrzés és numerikus hibamérce egységben a backward lépést független numerikus differenciával ellenőriztük. Most ebből a lépésből tanítási hurkot építünk: a modell paramétereit ismételten frissítjük a minibatch gradiens alapján. A cél a három művelet pontos szétválasztása: gradienstörlés, backpropagation és optimizerlépés. A fogalom az autograd-keretrendszerek viselkedését is előkészíti, de itt még kézi NumPy-kódban dolgozunk.
Előismeret: a gradiens definíciója 4.2. Parciális derivált és gradiens, a minibatch gradiens mint becslés 6.3. Sztochasztikus és minibatch gradiens, a momentumos és adaptív frissítési szabályok 6.4. Momentum és adaptív optimizálók, valamint az aktivációk lokális deriváltjai 9.4. Aktivációk és gradiensáramlás. A backward képleteket a 10.2. Affín réteg és aktiváció backward alfejezetből ismertnek tekintjük.
Induló kérdés. Legyen két megfigyelés átlagos MSE-vesztesége, és . Mennyi lesz egy gradienslépés után? A választ a 40.3 szakaszban számítjuk ki; jó becslés a 2, és az ottani példa azt is megmutatja, hogy a veszteség -ről -re csökken.
Legyen a modell összes paraméterének vektora, a tanulási ráta, és a lépések száma. A tanítóhalmaz , a holdout a tanításból kizárt, független ellenőrző adat.
Definíció 40.1 (minibatch veszteség). Egy indexhalmazhoz, , a minibatch veszteség
ahol az egyedi példa vesztesége, a modell előrejelzése. A teljes batch a speciális eset.
Definíció 40.2 (zero gradient). Az
autograd-keretrendszerekben minden paraméterhez egy gradiens-tároló
tartozik; a .backward() hívás az új gradienst a tárolóhoz
hozzáadja. A zero_grad() ezt a tárolót nullázza a
backward előtt. A kézi NumPy-kódban ilyen tároló nincs: a backward
függvény közvetlenül visszaadott gradienstömbjeit a lépés végén
eldobjuk, így ott a „törlés” a felülírásban valósul meg.
Definíció 40.3 (optimizerlépés). A sima SGD-frissítés
Momentumos vagy adaptív szabály esetén az optimizer állapotváltozókat is frissít; ezek a 6.4. Momentum és adaptív optimizálók alfejezetben tárgyalt mennyiségek.
Definíció 40.4 (loss history). A tanítás során feljegyzett értékek sorozata. Ez lehet minibatch loss, epoch-átlagolt tanítási loss vagy holdout loss; a három nem felcserélhető, mert a minibatch loss fluktuál, a holdout loss pedig a tanításból kizárt adaton mért célfüggvény.
Definíció 40.5 (holdout és small overfit). A holdout a tanításban nem látott adat; a tanítási és holdout loss együttes görbéje a túlillesztés alapdiagnózisa. Ha a tanítási loss közel nullára csökken, miközben a holdout loss nő, akkor a modell az empirikus kockázatot csökkenti ugyan, de a tanítóhalmaz sajátosságait memorizálja. A small overfit ilyen ellenőrzés kis tanítóhalmazon: a modellnek képesnek kell lennie a train loss jelentős csökkentésére, különben a tanítási lépés vagy a kapacitás hibás.
Először az általános állítás, majd a konkrét számpélda.
Lemma 40.1 (minibatch gradiens és frissítés). Ha minden differenciálható a pontban, akkor
Az SGD-frissítés után
Bizonyítás. Az átlag konstans szorzó, a véges összeg deriváltja a deriváltak összege a 4.2. Parciális derivált és gradiens gradiensdefiníciójából. A frissítési képlet a gradienslépés definíciója.
Ez a lemma rögzíti a skálát: a minibatch gradiens az egyedi gradiensek átlaga, nem összege. A labor kódban a tényező pontosan ezt hordozza.
Most a konkrét minibatch. Legyen , a modell , célok , . Az átlagos MSE
A gradiens tagonként:
esetén , tehát a frissítés
A veszteség előtte és utána:
A frissítés előtti és utáni állapot:
| Mennyiség | Frissítés előtt | Frissítés után |
|---|---|---|
Ez a példa azt is mutatja, hogy a frissítés utáni gradiens általában nem nulla; itt véletlenül nulla, mert a két cél átlagába konvergáltunk. Ez nem általános tulajdonság, hanem ennek a konkrét kvadratikus feladatnak a speciális esete.
Ugyanazon a feladaton mellett:
A veszteségek: , , , . A loss history monoton csökken, mert a célfüggvény konvex kvadratikus; ez nem általános garantált viselkedés mély hálókra.
Protokoll. Kérdés: a kézzel írt minibatch SGD
csökkenti-e a holdout veszteséget egy nemlineáris regressziós feladaton?
Adat: eredeti szintetikus minta,
,
,
.
Split:
,
.
Modell:
,
rejtett szélesség
,
tanh aktiváció, lineáris kimenet. Loss: átlagos MSE. Optimalizáló:
minibatch SGD,
,
,
epoch. A paramétereket float64-ben tároljuk, CPU-n futunk.
Seedek: adat 4201, keverés 4202, inicializálás
4203.
A teljes kód egyetlen ellenőrizhető munkamenetben,
lab/n040_train_mlp.py munkakönyvtárral:
"""n040 labor: kézi MLP tanítás minibatch SGD-vel, holdouttal."""
import numpy as np
# --- adat: eredeti szintetikus regresszió, seed rögzítve ---
rng_data = np.random.default_rng(4201)
N_train, N_val, d, h = 200, 50, 2, 16
X_train = rng_data.uniform(-2.0, 2.0, size=(N_train, d))
y_train = np.sin(2.0 * X_train[:, 0:1]) + 0.5 * X_train[:, 1:2]**2
y_train = y_train + rng_data.normal(0.0, 0.05, size=y_train.shape)
X_val = rng_data.uniform(-2.0, 2.0, size=(N_val, d))
y_val = np.sin(2.0 * X_val[:, 0:1]) + 0.5 * X_val[:, 1:2]**2
y_val = y_val + rng_data.normal(0.0, 0.05, size=y_val.shape)
# --- inicializálás ---
rng_init = np.random.default_rng(4203)
W1 = rng_init.normal(0.0, 0.3, size=(h, d)) # (h,d), a konvenció szerint
b1 = np.zeros(h)
W2 = rng_init.normal(0.0, 0.3, size=(1, h)) # (1,h)
b2 = np.zeros(1)
def forward_loss(X, Y, W1, b1, W2, b2):
Z1 = X @ W1.T + b1 # (M,h)
A1 = np.tanh(Z1)
Yhat = A1 @ W2.T + b2 # (M,1)
return np.mean((Yhat - Y)**2)
def backward(X, Y, W1, b1, W2, b2):
M = X.shape[0]
Z1 = X @ W1.T + b1
A1 = np.tanh(Z1)
Yhat = A1 @ W2.T + b2
dL_dZ2 = 2.0 / M * (Yhat - Y) # (M,1), az átlag deriváltja
dW2 = dL_dZ2.T @ A1 # (1,h)
db2 = dL_dZ2.sum(axis=0) # (1,)
dA1 = dL_dZ2 @ W2 # (M,h)
dZ1 = dA1 * (1.0 - A1**2) # tanh lokális derivált
dW1 = dZ1.T @ X # (h,d)
db1 = dZ1.sum(axis=0) # (h,)
loss = np.mean((Yhat - Y)**2)
return loss, dW1, db1, dW2, db2
# --- tanítás: epochonként keverés, minibatch-ekre osztás ---
rng_train = np.random.default_rng(4202)
epochs, batch_size, lr = 300, 16, 0.01
train_hist, val_hist = [], []
for epoch in range(epochs):
perm = rng_train.permutation(N_train)
X_shuf, y_shuf = X_train[perm], y_train[perm]
epoch_loss = 0.0
for start in range(0, N_train, batch_size):
idx = slice(start, start + batch_size)
Xb, yb = X_shuf[idx], y_shuf[idx]
loss, dW1, db1, dW2, db2 = backward(Xb, yb, W1, b1, W2, b2)
# optimizerlépés: paraméterfrissítés, nincs akkumulátor, ezért nincs zero_grad
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
epoch_loss += loss * yb.shape[0]
train_hist.append(epoch_loss / N_train)
val_hist.append(forward_loss(X_val, y_val, W1, b1, W2, b2))
if epoch % 50 == 0 or epoch == epochs - 1:
print(f"epoch {epoch:3d} train={train_hist[-1]:.4f} val={val_hist[-1]:.4f}")
print(f"final W1 norm: {np.linalg.norm(W1):.3f}, W2 norm: {np.linalg.norm(W2):.3f}")Futtatás: python n040_train_mlp.py. A kimenet itt
jelölt szemléltetés, mert ez a kézirat nem futtatta a
kódot; a hallgatói környezetben a konkrét értékeket a seed rögzíti.
Várható trend: a train loss
környékéről
alá, a val loss
környékéről
–
közé csökken; a val loss a tanítási zaj miatt nem éri el a train loss-t,
és a görbe késői szakaszán enyhe emelkedés is előfordulhat.
Eredményértelmezés. A frissítési lépés működését a train loss csökkenése igazolja; a holdout loss azt mutatja, hogy a csökkenés nem pusztán a tanítóhalmaz memorizálása. Ha a val loss a train loss csökkenése ellenére tartósan nő, akkor small overfit áll fenn; ezt a 12.3. Sanity check és kis mintára túlillesztés sanity check fogja célzottan vizsgálni.
F1 (fogalmi, nehézség 1). Magyarázza el, miért kell
a gradiens-tárolót nullázni az autograd-keretrendszerekben a backward
előtt, és miért nincs erre külön utasítás a fenti NumPy-kódban. Térjen
ki a += akkumuláció következményére.
F2 (levezetési, nehézség 1). Legyen , célok , modell , veszteség , , . Számítsa ki , , és pontos értékét. Ellenőrizze, hogy .
F3 (implementációs, nehézség 2). Írjon
train_step(W1, b1, W2, b2, Xb, yb, lr) függvényt, amely egy
minibatchre elvégzi a forward/backward lépést, frissíti a paramétereket,
és visszaadja az új paramétereket, valamint a frissítés
előtti minibatch loss-t. Tesztelje a labor adat egy
méretű minibatchén úgy, hogy a visszaadott paraméterekkel újra
kiszámítja a loss-t, és ellenőrzi, hogy az kisebb-e a visszaadott
loss-nál.
F4 (hibakeresési, nehézség 2). A labor kód egy
változata a validációs loss számításához tévedésből a
X_train, y_train tömböket adja át a
forward_loss függvénynek. Lokalizálja a hibát, magyarázza
meg a következményt, adja meg a javítást, és írja le, milyen numerikus
viszony várható a javítás után a train és a val loss között.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen kísérletet a minibatch méret hatásának vizsgálatára a labor feladatán: . Adja meg a rögzített és változó tényezőket, a metrikát, a baseline-t, az ismétlések és seedek kezelését, valamint a következtetés érvényességi korlátját. Előre megadott győztest ne állítson.
F1. Az autograd .backward() a
láncszabályból kapott gradienst a paraméter .grad mezőjéhez
hozzáadja, nem felülírja. Ha az előző lépés gradiense
ott maradt, akkor a frissítés
irányába történik, ami hibás lépésközt ad. A zero_grad()
ezt a mezőt nullázza. A NumPy-kódban a backward függvény új
tömböket ad vissza; a dW1 = ... értékadás felülírja a
változót, és a frissítés után nem tárolunk gradienst. Az akkumuláció
tehát nem jön létre; a „zero gradient” a felülírásban implicit.
F2. A veszteség . Deriválás:
: , . A veszteségek: ; . Alakellenőrzés: minden mennyiség skalár; a gradiens dimenziója megegyezik dimenziójával.
F3. Teljes függvény:
def train_step(W1, b1, W2, b2, Xb, yb, lr):
"""Egy minibatch lépés; visszaadja az új paramétereket és a régi loss-t."""
loss, dW1, db1, dW2, db2 = backward(Xb, yb, W1, b1, W2, b2)
W1_new = W1 - lr * dW1
b1_new = b1 - lr * db1
W2_new = W2 - lr * dW2
b2_new = b2 - lr * db2
return W1_new, b1_new, W2_new, b2_new, lossTeszt: válasszunk egy
minibatch-et a tanítóhalmazból, számítsuk ki a régi loss-t, hívjuk a
függvényt lr=0.01-gyel, majd az új paraméterekkel számítsuk
újra a loss-t. A várt eredmény: az új loss kisebb, mert a SGD lépés a
gradienssel ellentétes irányba mozdul; nem garantált, de ezen a
feladaton kis
mellett tipikusan teljesül. A visszaadott loss-nak pontosan egyeznie
kell a lépés előtti forward_loss értékével.
F4. A hiba a val loss számításánál a bemeneti
tömbökben van: a forward_loss(X_train, y_train, ...) hívás
a tanítóhalmazt méri újra. Következmény: a val_hist lista
nem holdout loss, hanem train loss, tehát a görbe nem mutatja a
túlillesztést; a kísérlet hamisan „jó” általánosítást jelez. Javítás:
forward_loss(X_val, y_val, W1, b1, W2, b2). Ellenőrzés: a
javított val loss általában nagyobb, mint a train loss, mert a holdoutot
a modell nem látta; a két görbe szétválása a túlillesztés jele
lehet.
F5. Egy lehetséges terv:
4201); a split rögzített.Tipikus tévedés. „A modell a teljes adaton tanul, és ugyanazon értékel, tehát a loss history általánosítási teljesítményt mutat.” Ez hibás: a train loss az empirikus kockázatot méri, amely a memorizálással is csökkenthető. A F4 hibája pontosan ezt a csapdát valósítja meg: a holdout nélküli görbe alacsony, de semmit sem mond az új adaton várható hibáról. A laborban a val loss szétválása a helyes diagnózis.
Önellenőrzés. (1) Mi a minibatch gradiens képlete? (2) Miért kell a gradiens-tárolót nullázni a backward előtt? (3) Mit mér a loss history, ha a veszteséget a tanítóhalmazon számoljuk? (4) Igaz-e, hogy egy gradienslépés után a gradiens a frissített pontban mindig nulla? (Válasz: nem; a 40.3 példában véletlenül nulla, mert a kvadratikus cél minimumába értünk.)
Visszahívás. A gradiens a veszteség legmeredekebb növekedési iránya 4.2. Parciális derivált és gradiens, a minibatch gradiens a teljes gradiens zajos becslése 6.3. Sztochasztikus és minibatch gradiens, a momentumos frissítés állapotot visz 6.4. Momentum és adaptív optimizálók, a ReLU nullpontja pedig a numerikus gradiensellenőrzésnél kink-ként viselkedett 10.3. Gradiensellenőrzés és numerikus hibamérce.
A tanítási lépés három elkülönülő műveletből áll: a gradiens-tároló nullázása, a backpropagation által előállított minibatch gradiens, valamint a paraméterfrissítés. A minibatch gradiens az egyedi gradiensek átlaga, ezért a backward képletekben az tényező explicite megjelenik. A frissítés ; a konkrét kézi példában , és mellett , a veszteség -ről -re csökken. A loss history és a holdout loss együttesen diagnosztizálja a túlillesztést: a teljes adaton mért alacsony veszteség nem általánosítási bizonyíték. A labor kézzel írt NumPy MLP-t tanít minibatch SGD-vel, rögzített seedekkel és független holdouttal.
A 11.1. Tensor és autograd-életciklus
egységben a PyTorch autograd-életciklus következik; ott a
zero_grad, backward és
optimizer.step hívások a fenti három művelet közvetlen
megfelelői.