10.4 Tanítási lépés és tanuló MLP

10.4.1 Cél, előismeret és induló becslés

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 L(w)=12((w−1)2+(w−3)2)L(w)=\frac12\bigl((w-1)^2+(w-3)^2\bigr) két megfigyelés átlagos MSE-vesztesége, w0=0w_0=0 és α=0.5\alpha=0.5. Mennyi lesz w1w_1 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 55-ről 11-re csökken.

10.4.2 A tanítási lépés objektumai

Legyen θ∈ℝp\theta\in\mathbb R^p a modell összes paraméterének vektora, α>0\alpha>0 a tanulási ráta, és T∈ℕT\in\mathbb N a lépések száma. A tanítóhalmaz Dtrain={(xi,yi)}i=1ND_{\mathrm{train}}=\{(x_i,y_i)\}_{i=1}^N, a holdout DvalD_{\mathrm{val}} a tanításból kizárt, független ellenőrző adat.

Definíció 40.1 (minibatch veszteség). Egy B⊆{1,…,N}B\subseteq\{1,\ldots,N\} indexhalmazhoz, M=|B|M=|B|, a minibatch veszteség

LB(θ)=1M∑i∈Bℓ(fθ(xi),yi),L_B(\theta)=\frac1M\sum_{i\in B}\ell(f_\theta(x_i),y_i),

ahol ℓ\ell az egyedi példa vesztesége, fθf_\theta a modell előrejelzése. A teljes batch a B={1,…,N}B=\{1,\ldots,N\} 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

θt+1=θt−αgt,gt=∇θLB(θt).\theta_{t+1}=\theta_t-\alpha g_t,\qquad g_t=\nabla_\theta L_B(\theta_t).

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 mt,vtm_t,v_t mennyiségek.

Definíció 40.4 (loss history). A tanítás során feljegyzett L̂t\hat L_t é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.

10.4.3 Levezetési kötelezettség: mi változik egy minibatch frissítés előtt és után

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 ℓi(θ)=ℓ(fθ(xi),yi)\ell_i(\theta)=\ell(f_\theta(x_i),y_i) differenciálható a θ\theta pontban, akkor

∇θLB(θ)=1M∑i∈B∇θℓi(θ).\nabla_\theta L_B(\theta)=\frac1M\sum_{i\in B}\nabla_\theta\ell_i(\theta).

Az SGD-frissítés után

θt+1=θt−α∇θLB(θt).\theta_{t+1}=\theta_t-\alpha\nabla_\theta L_B(\theta_t).

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. ▫\square

Ez a lemma rögzíti a skálát: a minibatch gradiens az egyedi gradiensek átlaga, nem összege. A labor kódban a δ[2]=2M(ŷ−y)\delta^{[2]}=\frac2M(\hat y-y) tényező pontosan ezt hordozza.

Most a konkrét minibatch. Legyen M=2M=2, a modell ŷ=w\hat y=w, célok y1=1y_1=1, y2=3y_2=3. Az átlagos MSE

LB(w)=12((w−1)2+(w−3)2).L_B(w)=\frac12\bigl((w-1)^2+(w-3)^2\bigr).

A gradiens tagonként:

∂LB∂w=12(2(w−1)+2(w−3))=2w−4.\frac{\partial L_B}{\partial w}=\frac12\bigl(2(w-1)+2(w-3)\bigr)=2w-4.

w0=0w_0=0 esetén g0=−4g_0=-4, tehát a frissítés

w1=w0−αg0=0−0.5⋅(−4)=2.w_1=w_0-\alpha g_0=0-0.5\cdot(-4)=2.

A veszteség előtte és utána:

LB(w0)=12(1+9)=5,LB(w1)=12(1+1)=1.L_B(w_0)=\frac12(1+9)=5,\qquad L_B(w_1)=\frac12(1+1)=1.

A frissítés előtti és utáni állapot:

Mennyiség Frissítés előtt Frissítés után
ww 00 22
gB(w)=2w−4g_B(w)=2w-4 −4-4 00
LB(w)L_B(w) 55 11

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.

10.4.4 Kézi kis példa: háromlépéses frissítés

Ugyanazon a feladaton α=0.25\alpha=0.25 mellett:

  1. w0=0w_0=0, g0=−4g_0=-4, w1=0+1=1w_1=0+1=1;
  2. w1=1w_1=1, g1=2−4=−2g_1=2-4=-2, w2=1+0.5=1.5w_2=1+0.5=1.5;
  3. w2=1.5w_2=1.5, g2=3−4=−1g_2=3-4=-1, w3=1.5+0.25=1.75w_3=1.5+0.25=1.75.

A veszteségek: LB(0)=5L_B(0)=5, LB(1)=12(0+4)=2L_B(1)=\frac12(0+4)=2, LB(1.5)=12(0.25+2.25)=1.25L_B(1.5)=\frac12(0.25+2.25)=1.25, LB(1.75)=12(0.5625+1.5625)=1.0625L_B(1.75)=\frac12(0.5625+1.5625)=1.0625. 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.

10.4.5 Labor: kis MLP szintetikus regresszión

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, x∈ℝ2x\in\mathbb R^2, y=sin(2x1)+0.5x22+εy=\sin(2x_1)+0.5x_2^2+\varepsilon, ε∼N(0,0.052)\varepsilon\sim N(0,0.05^2). Split: Ntrain=200N_{\mathrm{train}}=200, Nval=50N_{\mathrm{val}}=50. Modell: d=2d=2, rejtett szélesség h=16h=16, tanh aktiváció, lineáris kimenet. Loss: átlagos MSE. Optimalizáló: minibatch SGD, α=0.01\alpha=0.01, M=16M=16, 300300 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 0.20.2 környékéről 0.010.01 alá, a val loss 0.20.2 környékéről 0.020.02–0.050.05 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.

10.4.6 Feladatok

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 M=3M=3, célok y=(1,2,4)y=(1,2,4), modell ŷ=w\hat y=w, veszteség L(w)=1M∑i=13(yi−w)2L(w)=\frac1M\sum_{i=1}^3(y_i-w)^2, w0=0w_0=0, α=0.25\alpha=0.25. Számítsa ki g0g_0, w1w_1, L(w0)L(w_0) és L(w1)L(w_1) pontos értékét. Ellenőrizze, hogy L(w1)<L(w0)L(w_1)<L(w_0).

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=16M=16 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: M∈{1,8,64}M\in\{1,8,64\}. 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.

10.4.7 Megoldások és indoklás

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 gt+gt−1g_t+g_{t-1} 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 L(w)=13((1−w)2+(2−w)2+(4−w)2)L(w)=\frac13\bigl((1-w)^2+(2-w)^2+(4-w)^2\bigr). Deriválás:

L′(w)=13(2(w−1)+2(w−2)+2(w−4))=23(3w−7)=2w−143.L'(w)=\frac13\bigl(2(w-1)+2(w-2)+2(w-4)\bigr)=\frac23(3w-7)=2w-\frac{14}{3}.

w0=0w_0=0: g0=−143g_0=-\frac{14}{3}, w1=0−0.25⋅(−143)=1412=76w_1=0-0.25\cdot(-\frac{14}{3})=\frac{14}{12}=\frac76. A veszteségek: L(w0)=13(1+4+16)=7L(w_0)=\frac13(1+4+16)=7; L(w1)=13((−16)2+(56)2+(176)2)=315108=3512≈2.92<7L(w_1)=\frac13\bigl((-\frac16)^2+(\frac56)^2+(\frac{17}{6})^2\bigr)=\frac{315}{108}=\frac{35}{12}\approx2.92<7. Alakellenőrzés: minden mennyiség skalár; a gradiens dimenziója megegyezik ww 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, loss

Teszt: válasszunk egy M=16M=16 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 α\alpha 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:

10.4.8 Tipikus tévedés, önellenőrzés és visszahívás

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.

10.4.9 Rövid összefoglaló

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 1M\frac1M tényező explicite megjelenik. A frissítés θt+1=θt−αgt\theta_{t+1}=\theta_t-\alpha g_t; a konkrét kézi példában w0=0w_0=0, g0=−4g_0=-4 és α=0.5\alpha=0.5 mellett w1=2w_1=2, a veszteség 55-ről 11-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.

10.4.10 Következő alkalmazás és források

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.