10.2 Affín réteg és aktiváció backward

10.2.1 Cél, előismeret és induló kérdés

Ebben az alfejezetben a 10.1. Számítási gráf és lokális derivált alfejezetben felépített visszaterjesztési gépezet két konkrét építőkövét vezetjük le: az affin réteg és a ReLU-aktiváció backwardját. Az előismeret a lokális VJP, a topologikus sorrend és a gradiensösszegzési lemma. Nem használunk sem PyTorch autogradot, sem kész optimalizálót; minden képletet NumPy-műveletként is megadunk.

Induló kérdés: egy NN elemű minibatchre számolt átlagolt veszteség után a biasgradiens a bejövő gradiens oszlopainak összege vagy átlaga? Mielőtt tovább olvasna, becsülje meg: ha N=4N=4, és a bejövő upstream gradiens minden sora azonos, mekkora a helyes db a sorösszeghez képest?

10.2.2 Affin réteg: definíció, VJP és alakok

Az affin réteg a könyv konvenciója szerint

Z=XW⊤+b, Z = XW^\top + b,

ahol X∈ℝN×dX\in\mathbb R^{N\times d} a batch, W∈ℝm×dW\in\mathbb R^{m\times d} a súly, b∈ℝmb\in\mathbb R^m a bias, Z∈ℝN×mZ\in\mathbb R^{N\times m} a pre-aktiváció. Egy megfigyelés tehát az XX mátrix egy sora, a bb vektort a NumPy soronként szórja ki (broadcast).

A backward bemenete a ZZ-re érkező upstream gradiens G=∇ZL∈ℝN×mG=\nabla_Z L\in\mathbb R^{N\times m}, ahol L=ℓ(Z)L=\ell(Z) a skalár veszteség. A levezetendő gradiens alakja mindig megegyezik a paraméter vagy bemenet alakjával, a könyv oszlop-VJP-konvenciója szerint.

Lemma (affin réteg VJP-je). Legyen Z=XW⊤+bZ=XW^\top+b a fenti alakokkal, és legyen ℓ:ℝN×m→ℝ\ell:\mathbb R^{N\times m}\to\mathbb R differenciálható. Ekkor G=∇ZLG=\nabla_Z L mellett

∇WL=G⊤X,∇XL=GW,∇bL=∑n=1NGn,:. \nabla_W L = G^\top X,\qquad \nabla_X L = G W,\qquad \nabla_b L = \sum_{n=1}^N G_{n,:}.

Bizonyítás. A skalár láncszabályt koordinátánként alkalmazzuk. Mivel

Znk=∑r=1dXnrWkr+bk, Z_{nk} = \sum_{r=1}^{d} X_{nr}W_{kr} + b_k,

a WijW_{ij} szerinti parciális derivált ∂Znk/∂Wij=δkiXnj\partial Z_{nk}/\partial W_{ij}=\delta_{ki}X_{nj}, ezért

∂L∂Wij=∑n=1N∑k=1mGnkδkiXnj=∑n=1NGniXnj=(G⊤X)ij. \frac{\partial L}{\partial W_{ij}} = \sum_{n=1}^N\sum_{k=1}^m G_{nk}\delta_{ki}X_{nj} = \sum_{n=1}^N G_{ni}X_{nj} = (G^\top X)_{ij}.

A biasra ∂Znk/∂bj=δkj\partial Z_{nk}/\partial b_j=\delta_{kj}, így

∂L∂bj=∑n=1N∑k=1mGnkδkj=∑n=1NGnj, \frac{\partial L}{\partial b_j} = \sum_{n=1}^N\sum_{k=1}^m G_{nk}\delta_{kj} = \sum_{n=1}^N G_{nj},

ami éppen a GG oszlopösszege. Végül ∂Znk/∂Xij=δniWkj\partial Z_{nk}/\partial X_{ij}=\delta_{ni}W_{kj}, tehát

∂L∂Xij=∑n=1N∑k=1mGnkδniWkj=∑k=1mGikWkj=(GW)ij, \frac{\partial L}{\partial X_{ij}} = \sum_{n=1}^N\sum_{k=1}^m G_{nk}\delta_{ni}W_{kj} = \sum_{k=1}^m G_{ik}W_{kj} = (GW)_{ij},

ami a három állítást bizonyítja. ▫\square

A levezetés saját kidolgozás, a mátrixalakú láncszabály általános konvencióját követi; a B01 általános mátrixkalkulus hátterét használtuk, de a konkrét képleteket ebben az alakban magunk ellenőriztük.

Alakellenőrzés: G⊤XG^\top X alakja (m×N)(N×d)=m×d(m\times N)(N\times d)=m\times d, egyezik WW-vel; GWGW alakja (N×m)(m×d)=N×d(N\times m)(m\times d)=N\times d, egyezik XX-szel; a biasgradiens (m,)(m,). Azt is érdemes kimondani, hogy bb konkrét értéke nem jelenik meg egyik gradiensben sem, mert a +b+b transzláció Jacobi-mátrixa az identitás; a bb csak a forward ZZ értékén keresztül hat a további rétegek gradiensére.

10.2.3 Broadcast backward és a biasgradiens összege

A b∈ℝmb\in\mathbb R^m a forwardban NN példányban használatos: minden batchsorhoz ugyanazt a vektort adjuk. A 10.1. Számítási gráf és lokális derivált elágazási lemma szerint a közös paraméter gradiense a különböző ágak hozzájárulásainak összege. Egy ág egy nn sor, ahonnan Gn,:G_{n,:} érkezik, tehát

∇bL=G⊤𝟏N=∑n=1NGn,:, \nabla_b L = G^\top \mathbf 1_N = \sum_{n=1}^N G_{n,:},

ahol 𝟏N\mathbf 1_N a csupa egyes oszlopvektor. Ez a db = G.sum(axis=0) művelet.

Általános szabályként: ha egy forward műveletben egy kisebb alakú tenzort a batchdimenzió mentén sugároztunk ki, akkor a backward bejövő gradienst a kibővített tengelyek mentén összegezni kell, hogy visszakapjuk az eredeti alakot. Itt a 0. tengely a kibővített batchtengely.

A batch mean a reduction szintjén dől el, nem a biasgradiens képletében. Ha

Lmean=1N∑n=1Nℓn(Zn,:),akkorGn,:=1N∇zℓn(Zn,:). L_{\mathrm{mean}}=\frac1N\sum_{n=1}^N \ell_n(Z_{n,:}), \quad\text{akkor}\quad G_{n,:}=\frac1N\nabla_z\ell_n(Z_{n,:}).

A ∇bL\nabla_b L ekkor is ∑nGn,:\sum_n G_{n,:}, tehát a biasgradiens automatikusan tartalmazza az 1/N1/N skálát. A sum reduction esetén Gn,:=∇zℓnG_{n,:}=\nabla_z\ell_n, így minden gradiens pontosan NN-szerese az átlagos változatnak. A képlet tehát nem változik, csak a bejövő GG skálája; ezt a gyakori tévedésnél ismét elővesszük.

10.2.4 ReLU backward

A 9.2. MLP és nemlinearitás alfejezetben bevezetett ReLU elemenkénti művelet: h(x)=max(0,x)h(x)=\max(0,x). A hh differenciálható minden x≠0x\neq 0 pontban, és

h′(x)={1,x>0,0,x<0. h'(x)=\begin{cases}1,&x>0,\\0,&x<0.\end{cases}

Az x=0x=0 pontban klasszikus derivált nincs; ott a szubderivált a [0,1][0,1] intervallum. A NumPy és a PyTorch implementáció egyetlen értéket választ konvencióként, nevezetesen 00-t. Ennek megfelelően a backward

∇XL=𝟏[X>0]⊙G, \nabla_X L = \mathbf 1[X>0] \odot G,

ahol ⊙\odot az elemenkénti szorzat, az indikátor pedig a mentett pre-aktivációból képzett maszk. Elemenkénti aktiváció esetén a Jacobi-mátrix diagonális, ezért a VJP éppen ez az elemenkénti szorzat.

A törésponti konvenció miatt a nulla pre-aktivációjú elemhez tartozó gradiens a fenti szabály szerint nulla. Ez nem azt jelenti, hogy a nullpontbeli derivált nulla volna, hanem hogy az optimalizáló implementáció egy szubderiváltat választ. A 10.3. Gradiensellenőrzés és numerikus hibamérce gradiensellenőrzésénél ezért mindig nullától távoli vizsgálati pontot használunk; a labor adatait úgy választjuk, hogy minden pre-aktiváció pozitív legyen, így a töréspont nem zavarja a numerikus összevetést.

10.2.5 Kézi kis példa

Legyen N=2N=2, d=3d=3, m=2m=2,

X=(102011),W=(120011),b=(10), X=\begin{pmatrix}1&0&2\\0&1&1\end{pmatrix},\quad W=\begin{pmatrix}1&2&0\\0&1&1\end{pmatrix},\quad b=\begin{pmatrix}1\\0\end{pmatrix},

és érkezzen a bejövő gradiens

G=(1234). G=\begin{pmatrix}1&2\\3&4\end{pmatrix}.

Aktor

dW=G⊤X=(1324)(102011)=(135248), dW=G^\top X =\begin{pmatrix}1&3\\2&4\end{pmatrix} \begin{pmatrix}1&0&2\\0&1&1\end{pmatrix} =\begin{pmatrix}1&3&5\\2&4&8\end{pmatrix},

db=G1,:+G2,:=(1+3,2+4)=(4,6), db=G_{1,:}+G_{2,:}=(1+3,\;2+4)=(4,6),

dX=GW=(1234)(120011)=(1423104). dX=GW =\begin{pmatrix}1&2\\3&4\end{pmatrix} \begin{pmatrix}1&2&0\\0&1&1\end{pmatrix} =\begin{pmatrix}1&4&2\\3&10&4\end{pmatrix}.

A dWdW alakja (2,3)(2,3), a dXdX alakja (2,3)(2,3), a dbdb alakja (2,)(2,); mindegyik a megfelelő paraméter alakjával egyezik. A b=(1,0)⊤b=(1,0)^\top értéke egyik eredményben sem szerepel, ahogy a lemma állítja.

10.2.6 Konkrét labor: két rétegből álló NumPy-MLP

A labor egy d=4d=4 bemenetű, m=5m=5 rejtett neuronú, K=2K=2 kimenetű MLP-t épít fel: affin réteg, ReLU, affin réteg, majd batch-átlagolt fél-MSE loss. A bemenet determinisztikus, ezért nincs RNG-seed; minden tenzor float64, eszköz CPU. A teljes futtatható kód:

# lab_n038.py
# Munkakönyvtár: a fejezet lab könyvtára.
# Futtatás: python lab_n038.py
import numpy as np

def relu(x):
    return np.maximum(0.0, x)

def linear_forward(X, W, b):
    Z = X @ W.T + b
    cache = (X, W)          # b a backwardhoz nem kell
    return Z, cache

def relu_forward(Z):
    H = relu(Z)
    cache = Z               # a maszkot a pre-aktivaciobol kepezzuk
    return H, cache

def linear_backward(G, cache):
    X, W = cache
    dW = G.T @ X
    db = G.sum(axis=0)      # biasgradiens: batch szerinti osszeg
    dX = G @ W
    return dX, dW, db

def relu_backward(dH, cache):
    Z = cache
    dZ = dH * (Z > 0)       # ReLU-maszk; a nullponti konvencio 0
    return dZ

# Determinisztikus kis bemenet, float64 CPU.
X = np.array([[1.,2.,3.,4.],
              [5.,6.,7.,8.],
              [9.,10.,11.,12.]], dtype=np.float64)      # (3,4)
W1 = np.array([[0.1,0.2,0.3,0.4],
               [0.5,0.6,0.7,0.8],
               [0.9,1.0,1.1,1.2],
               [1.3,1.4,1.5,1.6],
               [1.7,1.8,1.9,2.0]], dtype=np.float64)    # (5,4)
b1 = np.array([0.01,-0.02,0.03,-0.04,0.05], dtype=np.float64)
W2 = np.array([[0.1,0.2,0.3,0.4,0.5],
               [-0.1,-0.2,0.1,0.2,0.3]], dtype=np.float64)  # (2,5)
b2 = np.array([0.0,0.1], dtype=np.float64)
y = np.array([[1.,0.],
              [0.,1.],
              [1.,1.]], dtype=np.float64)               # (3,2)

# Forward.
Z1, cache1 = linear_forward(X, W1, b1)   # (3,5)
H1, rcache = relu_forward(Z1)            # (3,5)
Z2, cache2 = linear_forward(H1, W2, b2)  # (3,2)

# Loss: batch-mean, felenkenti MSE.  L = 1/(2NK) * sum (Z2-y)^2
N, K = y.shape
L = 0.5 * np.mean((Z2 - y) ** 2)

# Backward; a mean reduction skalaja az upstream G2-ben van.
G2 = (Z2 - y) / (N * K)
dH1, dW2, db2 = linear_backward(G2, cache2)
dZ1 = relu_backward(dH1, rcache)
dX, dW1, db1 = linear_backward(dZ1, cache1)

print("L=", L)
print("Z1", Z1.shape, "H1", H1.shape, "Z2", Z2.shape)
print("dW1", dW1.shape, "db1", db1.shape, "dX", dX.shape)
print("dW2", dW2.shape, "db2", db2.shape)

# Fuggetlen numerikus ellenorzes centralis differenciaval.
def loss_of(params):
    A, c, B, d = params
    return 0.5 * np.mean((relu(X @ A.T + c) @ B.T + d - y) ** 2)

base_params = [W1.copy(), b1.copy(), W2.copy(), b2.copy()]
analytical = [dW1, db1, dW2, db2]
eps = 1e-6
max_err = 0.0
for p, g in zip(base_params, analytical):
    pf = p.reshape(-1)
    gf = g.reshape(-1)
    for idx in range(pf.size):
        old = pf[idx]
        pf[idx] = old - eps
        lm = loss_of(base_params)
        pf[idx] = old + eps
        lp = loss_of(base_params)
        pf[idx] = old
        num = (lp - lm) / (2.0 * eps)
        max_err = max(max_err, abs(num - gf[idx]))
print(f"max_abs_grad_err={max_err:.3e}")
print("max_abs_grad_err < 1e-8:", max_err < 1e-8)

A fenti kód az alfejezet képleteiből összeállított, futtatásra kész munkamenet; az itt közölt kimeneti szerkezet számított illusztráció, nem a jelen kéziratírás során mért futás. A várt alakok: Z1, H1 (3,5)(3,5), Z2 (3,2)(3,2), dW1 (5,4)(5,4), db1 (5,)(5,), dX (3,4)(3,4), dW2 (2,5)(2,5), db2 (2,)(2,). A választott X,W1,b1X,W_1,b_1 mellett minden pre-aktiváció pozitív, ezért a ReLU-töréspont nem befolyásolja a numerikus gradienst. A véges differencia középső lépése ε=10−6\varepsilon=10^{-6}, a várt maximális abszolút eltérés sima ponton 10−810^{-8} alatti; az átadandó laboreredmény a fenti kimenet és a max_abs_grad_err < 1e-8 logikai igazságérték.

10.2.7 Feladatok

F1 (fogalmi, nehézség 1, előfeltétel: 10.1.–10.2.). Magyarázza el, miért ∇bL=∑nGn,:\nabla_bL=\sum_nG_{n,:} és miért nem a 1N∑nGn,:\frac1N\sum_nG_{n,:} átlag, amikor a veszteség batch-átlag. Adjon N=2N=2-re olyan GG-t, amellyel a két képlet különböző eredményt ad. Átadandó: magyarázat, a különbség konkrét számértéke.

F2 (levezetési, nehézség 2, előfeltétel: a lemma). Legyen

X=(1234),W=(1002),b=(1,−1)⊤,G=(21−13). X=\begin{pmatrix}1&2\\3&4\end{pmatrix},\quad W=\begin{pmatrix}1&0\\0&2\end{pmatrix},\quad b=(1,-1)^\top,\quad G=\begin{pmatrix}2&1\\-1&3\end{pmatrix}.

Számítsa ki ∇WL\nabla_WL, ∇bL\nabla_bL és ∇XL\nabla_XL értékét az affin VJP-lemma alapján, és ellenőrizze mindhárom alakot. Mutassa meg, hogy bb értéke nem jelenik meg az eredményben. Átadandó: minden mátrixművelet, a három eredmény és alakellenőrzés.

F3 (implementációs, nehézség 2, előfeltétel: NumPy). Implementálja a linear_backward függvényt a fenti labor szerint, futtassa az F2 bemenetén, és nyomtassa ki a három gradienst. Átadandó: futtatható kód, dtype és az F2-vel egyező három kimenet.

F4 (hibakeresési, nehézség 2). A következő hibás backwardot kapja:

def linear_backward_wrong(G, cache):
    X, W = cache
    dW = X.T @ G
    db = G.sum(axis=0)
    dX = W.T @ G
    return dX, dW, db

Hol a hiba, mi a következménye, és hogyan ellenőrizné a javítást? Átadandó: a hiba lokalizálása, a helyes két sor, valamint egy shape- vagy numerikus teszt, amely megfogja a hibát.

F5 (kísérleti/transzfer, nehézség 2). Tervezzen kis kontrollkísérletet a labor adatain: ugyanazt a forwardot, ugyanazt a lossfüggvényt, de egyszer sum, egyszer mean reductionnel. A változó tényező csak a reduction; a metrika a biasgradiens sum/mean aránya. Mi a várt arány, milyen numerikus toleranciát ír elő, és mi a következtetés érvényességi határa? Átadandó: protokoll, várt arány, tolerancia, egy érvényességi korlát.

10.2.8 Megoldások és indoklás

F1. A bb vektort a forward minden batchsorhoz hozzáadja, tehát a paraméter NN különböző ágon jelenik meg a számítási gráfban. A 10.1. Számítási gráf és lokális derivált gradiensösszegzési lemma szerint a közös paraméter gradiense az ágak hozzájárulásainak összege, minden ágból Gn,:G_{n,:} érkezik. A mean reduction nem a biasgradiens képzésénél lép be, hanem már a bejövő GG skálájában: Gn,:=1N∇zℓnG_{n,:}=\frac1N\nabla_z\ell_n. Ha tehát LmeanL_{\mathrm{mean}} gradiensét kaptuk, a további átlagolás dupla 1/N1/N-es skálázást adna. Legyen N=2N=2, G=(2468)G=\begin{pmatrix}2&4\\6&8\end{pmatrix}; a helyes ∇bL=(8,12)\nabla_bL=(8,12), a téves átlag (4,6)(4,6), a kettő aránya 1/N=1/21/N=1/2.

F2. Számolás:

∇WL=G⊤X=(2−113)(1234)=(2−34−41+92+12)=(−101014), \nabla_WL=G^\top X =\begin{pmatrix}2&-1\\1&3\end{pmatrix} \begin{pmatrix}1&2\\3&4\end{pmatrix} =\begin{pmatrix}2-3&4-4\\1+9&2+12\end{pmatrix} =\begin{pmatrix}-1&0\\10&14\end{pmatrix},

∇bL=G1,:+G2,:=(2−1,1+3)=(1,4), \nabla_bL=G_{1,:}+G_{2,:}=(2-1,\;1+3)=(1,4),

∇XL=GW=(21−13)(1002)=(22−16). \nabla_XL=GW =\begin{pmatrix}2&1\\-1&3\end{pmatrix} \begin{pmatrix}1&0\\0&2\end{pmatrix} =\begin{pmatrix}2&2\\-1&6\end{pmatrix}.

Az alakok sorrendben (2,2)(2,2), (2,)(2,), (2,2)(2,2), mindegyik a megfelelő objektum alakja. Mivel ∂Z/∂W\partial Z/\partial W és ∂Z/∂X\partial Z/\partial X nem függ a bias konstanstól, b=(1,−1)⊤b=(1,-1)^\top valóban nem jelenik meg.

F3. A labor linear_backward függvényét használva az F2 bemenetén dW == [[-1,0],[10,14]], db == [1,4], dX == [[2,2],[-1,6]]; a dtype float64. Az egyezőséget np.allclose ellenőrzi alapértelmezett, szigorúbb rtol=1e-9, atol=1e-12 mellett.

F4. A hibás dW = X.T @ G a helyes irányú szorzás transzponáltja: a forward Z=XW⊤Z=XW^\top, ezért a súlygradiens G⊤XG^\top X, nem X⊤GX^\top G. A hibás dX = W.T @ G szintén felcseréli a sor-oszlop irányt; a lemma szerint dX=GWdX=GW. A következmény kettős: egyrészt a hibás alak sok esetben nem (m,d)(m,d) vagy (N,d)(N,d), másrészt ha N=dN=d vagy m=dm=d miatt az alak véletlenül stimmel, a számérték akkor is hibás. A javítás:

dW = G.T @ X
dX = G @ W

Visszaellenőrzés: shape-assert mindkét értékre, majd a labor középső differenciás vizsgálata; a hibás változat a numerikus gradienssel nem egyezik, a javított 10−810^{-8} alatti abszolút eltérést ad.

F5. Protokoll: rögzített laborbemenet (X,y,W1,b1,W2,b2)(X,y,W_1,b_1,W_2,b_2), egyetlen változó a loss aggregálása, sum vagy mean. Metrika: db2db_2 minden elemének sum/mean hányadosa, valamint ugyanez dW2dW_2-re. A várt arány pontosan N=3N=3, mert a differenciálás lineáris, és Gsum=NGmeanG^{sum}=NG^{mean}. Numerikus tolerancia: minden hányados |r−3|≤10−12|r-3|\le 10^{-12} legyen float64, az osztás numerikus kerekítése miatt. Érvényességi korlát: az állítás csak a loss lineáris aggregálására vonatkozik; ha a reduction mellé például osztálysúlyozás vagy label smoothing is lép, a két bejövő gradiens aránya már nem konstans NN, hanem elemenként eltérhet.

10.2.9 Tipikus tévedés ellenpéldával

A leggyakoribb tévedés a biasgradiens összegének és az átlagolt loss skálájának összekeverése. Legyen N=2N=2, a bejövő átlagolt gradiens G=(48812)G=\begin{pmatrix}4&8\\8&12\end{pmatrix}. A helyes ∇bL=(12,20)\nabla_bL=(12,20). Aki a „batch-átlag” szó miatt G.mean(axis=0)-t használ, (6,10)(6,10)-et kap. Ez nem kicsi numerikus zaj, hanem pontosan kétszeres relatív hiba: a bias frissítése fele akkora lépést tesz, mint kellene. Ha viszont a forward loss sum reductionnel készült, a bejövő gradiens már ennek megfelelően nagyobb, de a backwardban akkor is sum(axis=0) kell; a reduction a GG előállításánál dönt el, nem a bias összegzésénél. Az ellenőrzés egyszerű: N=1N=1 esetén az összeg és az átlag egybeesik, így a hibás kód azonnal látszik egy egyelemű batch numerikus gradiensével.

10.2.10 Önellenőrzés és visszahívás

10.2.11 Rövid összefoglaló

Az affin réteg Z=XW⊤+bZ=XW^\top+b backwardja a bejövő GG mellett dW=G⊤XdW=G^\top X, db=∑nGn,:db=\sum_nG_{n,:} és dX=GWdX=GW. A lemmát koordinátánkénti láncszabállyal bizonyítottuk, és minden alakot ellenőriztünk. A biasgradiens a broadcast miatt batch szerinti összeg; a mean loss 1/N1/N skálája már a bejövő GG-ben van, ezért a backward képlet reduction-független összeget használ. A ReLU backwardja a mentett pre-aktivációból képzett X>0X>0 maszkkal szoroz, a nullpontban nulla konvencióval. A labor két rétegből álló NumPy-MLP-t épít, és minden paraméter gradiensét középső differenciával ellenőrzi 10−810^{-8} alatti abszolút hibakorláttal.

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

A 10.3. Gradiensellenőrzés és numerikus hibamérce egységben ezt a moduláris VJP-készletet teljes gradiensellenőrzési protokollá alakítjuk: tárgyaljuk az ε\varepsilon választását, az abszolút/relatív hiba mércéjét és a ReLU-töréspont kikerülését. A 10.4. Tanítási lépés és tanuló MLP ezután a tanítási lépést és egy valóban tanuló MLP-t mutat be.

<a id=“forrasok”></a>