A 4.2. Parciális derivált és gradiens alfejezetben egy skalár függvény gradiensét értelmeztük. A neurális réteg azonban tipikusan vektorból vektort képez: egy affin réteg -ből -be, egy aktiváció réteg szintén -ből -be. A tanítás során a veszteség skalár, tehát a visszafelé haladó számításnak „csak” egy-egy irány menti szorzatot kell elvégeznie, nem kell felépítenie a teljes mátrix deriváltat.
Induló becslés: ha , akkor a teljes Jacobi-mátrix számot tartalmazna. Ha viszont csak egy kimeneti vektort vetítünk vissza, a kívánt bemeneti gradiens maga is csupán szám. A kérdés: milyen mátrix–vektor szorzat és milyen transzponálási konvenció adja helyesen ezt az olcsóbb számítást?
A cél egy pontos, alakhelyes nyelv: Jacobi-mátrix, kompozíció, JVP, VJP, batch-redukció, valamint az affin réteg súly-, bias- és inputgradiensének levezetése. Előfeltétel a 4.2. Parciális derivált és gradiens parciális derivált- és gradiensfogalma; a 3.3. Vetítés, legkisebb négyzetek és kondicionáltság alfejezetből a legkisebb négyzetes normálegyenletre is röviden visszautalunk.
Definíció (Jacobi-mátrix). Legyen nyílt, , és legyen differenciálható az pontban. Ekkor az Jacobi-mátrixa
A mátrix sora az output, oszlopa az input komponenshez tartozik. A mátrix -edik sora az skalár függvény gradiensének transzponáltja:
A differenciálhatóság definíciója éppen azt jelenti, hogy a maradékos lineáris közelítés minden irányból jó:
Alakellenőrzés: mérete , mérete , tehát mérete , azonos alakjával. Ha , akkor egy sor, és , ami visszaadja a 4.2. Parciális derivált és gradiens gradiensét.
A rétegzett háló matematikailag kompozíció: a következő réteg bemenete az előző réteg kimenete. A láncszabály mondja meg, hogyan szorzódnak a Jacobi-mátrixok.
Tétel (többváltozós láncszabály). Legyen és nyílt, differenciálható -ban, differenciálható -ben. Ekkor differenciálható -ben, és
A szorzat alakjai: , , ezért a szorzat -beli.
Bizonyításvázlat. A bizonyítás a differenciálhatóság elsőrendű maradékos alakját használja. Legyen , és írjuk
Legyen . Mivel differenciálható -ben,
Behelyettesítve értékét:
Az első maradéktag nyilván , mert az. A másodikhoz azt használjuk, hogy : folytonos lineáris leképezés, ezért alkalmas mátrixnormával , és korlátos elég kis -ra. Így , amikor , tehát , miközben korlátos. Ezért a teljes maradék , ami éppen a kívánt lineáris közelítést adja. A teljes - kidolgozás a szokásos elemi kalkulus bizonyítás; a fenti vázlat a saját levezetés, a feltételek a kimondott differenciálhatósági pontok.
A tétel feltételei elégségesek, de nem szükségesek. Példa: konstans, . Ekkor nem differenciálható -ban, ezért a láncszabály képlete nem alkalmazható; a mégis azonosan nulla, tehát differenciálható. Ezért a tétel használatakor mindig ellenőrizni kell, hogy a belső függvény kimeneti pontjában a külső függvény tényleg differenciálható-e.
Definíció (JVP). Adott és adott irányvektor esetén a Jacobian–vector product
Ez az irány menti lineáris megváltozás: .
Definíció (VJP). Adott kimeneti vektor esetén a vector–Jacobi product
A megegyezésünk szerint az absztrakt vektorok oszlopvektorok, ezért a VJP-ben a Jacobi-mátrix transzponáltja áll. A mátrixok mérete: , , tehát az eredmény .
A két művelet nem szinonima. A JVP egy inputirányt visz előre, a VJP egy outputgradienst visz hátra. Ha , a JVP lehet olcsóbb irány; a klasszikus backpropagation egy skalár veszteség gradiense miatt viszont VJP-kből épül fel. A kapcsolatot a láncszabály adja: legyen differenciálható, és . Skalár kimenetű -ra a Jacobi-mátrix sor, transzponáltja a gradiens:
Itt , tehát .
Batch és reduction. A tanításban a veszteség általában egy batch-átlag:
ahol egy megfigyelés kimenete. Legyen a batchelt kimenet, és ; a -edik sora . Az átlagolás a láncszabályon kívüli, de a -ben lévő faktor:
Ha valaki sum redukcióval számol, akkor
,
és
.
A fontos szabály: a redukció nem a Jacobi-mátrix
transzponálásába épül, hanem a kimeneti gradiens skálájába. Az autograd
keretrendszer ezt a
értéket szolgáltatja a hátramenetben, és a réteg feladata már csak az
adott
-hez
tartozó VJP-összeg kiszámítása.
Most teljesítjük a fejezet fő levezetési kötelezettségét. Legyen batchelt bemenet, súly, bias, és
ahol a bias sorirányban broadcast; komponensenként
Ez a torch.nn.Linear súlyalakja is: weight
mérete (out_features, in_features), a forward pedig
x @ weight.T + bias.
Legyen skalár, és tegyük fel, hogy mint függvénye differenciálható. Jelölje
Az , és gradienseit külön-külön vezetjük le.
Inputgradiens. Az csak az -edik sor kimenetén keresztül hat. A láncszabály:
Oszlopvektoros alakban egy megfigyelésre: , ahol oszlopvektor. A teljes batch mátrixalakja:
Súlygradiens. A súly minden sorhoz hozzájárul, ezért a gradiens az szerinti külső összeg. Mivel
a láncszabály:
Tehát
Biasgradiens. A az összes megfigyelés eleméhez ugyanazzal az együtthatóval járul hozzá:
Mátrixos írásmóddal ez a oszlopösszege:
PyTorch/Numpy írásmódban: db = G.sum(axis=0).
A három eredmény egységes konvenciója: az absztrakt oszlopvektoros VJP , a batch-soros tárolás viszont . Aki a kettőt keveri, transzponálási hibát kap; erre a tipikus tévedésnél visszatérünk.
Az speciális esetben , tehát például ; a 3.3. Vetítés, legkisebb négyzetek és kondicionáltság normálegyenletének alakja is ugyanezt a VJP-irányt mutatja és választással.
Legyen
A parciális deriváltak:
ezért
Válasszuk a kimeneti gradiens értékét -nak. A VJP:
A következő kód ugyanezt explicit Jacobi-mátrixszal és
PyTorch-autograddal is ellenőrzi. A torch.dot(z,g) skalár
veszteséget képez, tehát az autograd a láncszabály szerinti
VJP-t adja vissza.
# lab/n015/vjp_lab.py
import torch
torch.manual_seed(20261020) # a példa determinisztikus; véletlen nincs benne
DTYPE = torch.float64
DEVICE = torch.device("cpu")
x = torch.tensor([1.0, 2.0], dtype=DTYPE, device=DEVICE, requires_grad=True)
g = torch.tensor([1.0, -2.0, 3.0], dtype=DTYPE, device=DEVICE)
def f(u: torch.Tensor) -> torch.Tensor:
return torch.stack([
u[0],
u[0] + u[1] ** 2,
u[0] * u[1],
])
z = f(x)
# Az x = (1,2) pontban kézzel levezetett Jacobi-mátrix:
J = torch.tensor(
[
[1.0, 0.0],
[1.0, 2.0 * x[1].item()],
[x[1].item(), x[0].item()],
],
dtype=DTYPE,
device=DEVICE,
)
vjp_explicit = J.T @ g
# loss = g·f(x), ezért grad_x(loss) = J_f(x)^T g:
loss = torch.dot(z, g)
vjp_autograd, = torch.autograd.grad(loss, x)
print("J =")
print(J)
print("vjp_explicit =", vjp_explicit)
print("vjp_autograd =", vjp_autograd)
print("max_abs_diff =", (vjp_explicit - vjp_autograd).abs().max().item())Futtatás a lab/n015 munkakönyvtárból:
python lab/n015/vjp_lab.py
Várt kimenet (a fenti kézi számításból, nem a jelen írás során futtatott naplóból):
J =
tensor([[1., 0.],
[1., 4.],
[2., 1.]], dtype=torch.float64)
vjp_explicit = tensor([5., -5.], dtype=torch.float64)
vjp_autograd = tensor([5., -5.], dtype=torch.float64)
max_abs_diff = 0.0
A labor kódja a fenti lab/n015/vjp_lab.py.
x=(1,2) és
g=(1,-2,3), mindkettő float64; nincs külső
adatfájl, a GlyphLab itt nem szükséges.torch; a labor a CPU-autograd
utat használja, device="cpu".torch.manual_seed(20261020), torch.float64,
CPU. A seed itt csak a protokoll része, mert a kód nem használ véletlen
számot.lab/n015/vjp_lab.py, futtatási parancs fent.vjp_explicit a kézzel
felírt
,
a vjp_autograd a
skalár veszteség gradiense. Alaktolerancia: mindkettő (2,);
f64 esetén az eltérésnek nulla közeli értéknek kell lennie, elfogadott
numerikus korlát atol=1e-12, rtol=1e-12. A
kiírt max_abs_diff a várható példán 0.A megadott kimenet számított, nem mért laborjelentés: a kódot a tanulólaborban külön kell futtatni, és az eltérést a fenti toleranciával rögzíteni.
F1, fogalmi, nehézség 1. Legyen , , . Adja meg alakját, a JVP-t , és a VJP-t esetén. Ne feledje megnevezni, melyik művelet melyik Jacobi-irányt használja.
F2, levezetési, nehézség 2. Legyen
Számítsa ki mátrixot és a vektorhoz tartozó VJP-t. Minden lépést indokoljon, és ellenőrizze az alakokat.
F3, implementációs, nehézség 2. Írjon NumPy-függvényt
def affine_grads(X: np.ndarray, W: np.ndarray, b: np.ndarray, G: np.ndarray):
...amely float64 bemenetekkel visszaadja a
dX, dW, db hármast az
,
,
,
konvencióban. Tesztelje az
esetet:
,
,
,
.
Adja meg a három elvárt mátrixot/vektort.
F4, hibakeresési, nehézség 1. Az alábbi kód a batchelt affin réteg inputgradiensét szeretné visszaadni ismeretében:
def buggy_dX(X, W, G):
# X: N x d, W: m x d, G: N x m
return W.T @ GKeresse meg a hibát, magyarázza el az okát, következményét, és javítsa a kódot. Ellenőrizze a javítást az F3 numerikus példáján.
F5, kísérleti/transzfer, nehézség 3. Tervezzen CPU-s PyTorch-mérést annak eldöntésére, hogy az , leképezésnél a teljes Jacobi-mátrix anyagiasítása vagy az autograd-VJP használ-e kisebb csúcsmemóriát és időt. Nincs szükség tényleges futtatásra; adjon protokollt, baseline-t, változó tényezőt, metrikát, ellenőrző egyezést és érvényességi korlátot. Jelölje meg, mi lenne a nyers eredménytábla két sora.
F1 megoldás. affin, ezért a Jacobi-mátrix konstans: . Sor–oszlop konvenció: az 1. sor az , a 2. sor az parciális deriváltjait tartalmazza.
JVP: , mérete .
VJP: , mérete . Az első a Jacobi-mátrixszal, a második annak transzponáltjával szoroz.
F2 megoldás. Parciális deriváltak:
Ezért a Jacobi-mátrix:
A VJP:
Alakellenőrzés: mérete , mérete , az eredmény , valóban az inputtér gradiense.
F3 megoldás. A szükséges kód:
# lab/n015/f3_solution.py
import numpy as np
def affine_grads(X, W, b, G):
assert X.dtype == np.float64 and W.dtype == np.float64
assert G.dtype == np.float64 and b.dtype == np.float64
dX = G @ W
dW = G.T @ X
db = G.sum(axis=0)
return dX, dW, db
X = np.array([[1.0, 0.0, 2.0],
[0.0, 1.0, 1.0]], dtype=np.float64)
W = np.array([[1.0, 2.0, 0.0],
[0.0, 1.0, 2.0]], dtype=np.float64)
b = np.array([-1.0, 1.0], dtype=np.float64)
G = np.array([[2.0, -1.0],
[0.0, -3.0]], dtype=np.float64)
dX, dW, db = affine_grads(X, W, b, G)
print(dX)
print(dW)
print(db)Kézi mátrixszorzással:
, ahol mérete , mérete , tehát :
, mérete :
A biasgradiens a oszlopösszege:
A kód a b értékét a gradiensben nem használja, ami
helyes: a biasgradiens független a bias aktuális értékétől.
Szélsőeset-ellenőrzés:
esetén mindhárom gradiens nulla, a függvény (N×d, m×d, m)
alakkal tér vissza;
esetén db=G[0,:].
F4 megoldás. A hiba oka: a kód a batch-soros
G@W irány helyett W.T@G-t ír. Az absztrakt
oszlopvektoros szabály valóban
egy megfigyelésre, de a batchelt soros tárolásban a transzponálás után a
teljes mátrixszorzat G@W; a
sorai a
vektorok. A W.T@G általában alakhibát ad, mert
(d×m) @ (N×m) nem szorozható össze, kivéve a véletlenül
egybeeső dimenziós eseteket. Következmény: futásidejű broadcast/shape
hiba, vagy ha a méretek egybeesnek, akkor numerikusan rossz gradiens,
amely a súlyfrissítést elrontja.
Javítás:
def buggy_dX_corrected(X, W, G):
return G @ W # N x dEllenőrzés az F3 példáján: G@W a fenti dX
értéket adja, és az np.testing.assert_allclose f64 alatt
atol=1e-12 határral elfogadja.
F5 megoldásminta. A kérdés erőforrás-típusú transzfer: a teljes Jacobi-mátrix f64 elem, azaz bájt, nagyságrendileg 8 MiB csak a mátrixra; a VJP egyetlen hosszú vektort ad vissza. A terv:
Kérdés: azonos matematikai eredményt feltételezve, a Jacobi-anyagiasítás vagy a VJP kisebb csúcsmemóriájú-e CPU-n.
Baseline:
J = torch.autograd.functional.jacobian(f, x), majd
v1 = J.T @ g; ez anyagiasítja a teljes mátrixot.
Változat: y = f(x),
loss = torch.dot(y,g),
v2, = torch.autograd.grad(loss,x).
Fix keret: dtype=float64, CPU,
torch.manual_seed(20261020),
véletlen
mátrix,
és
rögzített vektor; warmup háromszor, mérés ötvenszer.
Metrika: medián és interkvartilis terjedelem a
falidőből, valamint a folyamat csúcsmemóriája (ru_maxrss,
Linuxon KiB-ban); minden ismétlés előtt új folyamat a nyomkövető
overhead elkerülésére. A csúcsmemória lekérdezése:
import resource
peak_kib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrssKontroll:
torch.allclose(v1,v2,rtol=1e-12,atol=1e-12) igaz legyen, és
mindkét output alakja (1000,).
Értelmezési korlát: az eredmény erre a toy
leképezésre, e framework-verzióra és CPU-ra érvényes; a mérés
overheadje, a kernelindítási küszöb és a
szimmetria miatt nem általános GPU-, batch- vagy termelési állítás. A
nyers táblában a két módszer wall_time_median,
wall_time_iqr, ru_maxrss_kib értéke, valamint
a kontroll allclose eredménye szerepel.
A leggyakoribb hiba a transzponálási konvenció elhallgatása. Legyen , ahol
Ekkor , tehát a helyes VJP
Ha valaki tévesen
-t
számít, az
-t
kap. A példa azért fontos, mert
,
tehát az alak hiba nélkül is elfedi a tévedést: mindkét eredmény két
hosszú vektor, mégis a második a JVP, nem a VJP. Batch-soros tárolásnál
a fenti affin szabály szerint dX=G@W, nem
W.T@G; a kettő egyetlen megfigyelés oszlopgradiensére
átszámítva
.
Önellenőrzés ehhez a fejezethez: minden láncszabály-szorzatnál a szomszédos Jacobi-méreteknek illeszkedniük kell ( és ); a VJP bemenete -hosszú, kimenete -hosszú; a mean-redukció -je a -ben van, nem a -ben. Ha a kód olyan gradiensvektort ad, amelynek hossza a kívánt helyett, akkor szinte biztosan JVP-t számoltunk VJP helyett.
Visszahívás: a 4.2. Parciális derivált és gradiens alfejezetben a gradiens oszlopvektor volt; itt ezért lett a láncszabályból . A 3.3. Vetítés, legkisebb négyzetek és kondicionáltság legkisebb négyzetes gradiense, , ugyanennek a VJP-formának a speciális esete: , , és a kimeneti gradiens az reziduum.
A Jacobi-mátrix az deriváltjának mátrixa, sora az output, oszlopa az input. Differenciálható kompozícióban a Jacobikat a sorrendben szorozzuk. A backprop nem a teljes Jacobit építi fel, hanem a VJP-t: , ahol . Az affin rétegre esetén a gradiensek: , , és a oszlopösszege. A mean-redukció szorzója a -ben van, nem a transzponálási szabályban. A kidolgozott f64 példa várt VJP-értéke , és az autograd ellenőrzés ugyanezt a irányt teszteli.
A 4.4. I. ellenőrző pont: matematika és kód megfelelése ellenőrző pont a matematika és kód megfelelését vizsgálja. Ott a most levezetett affin gradiensképleteket, a JVP/VJP irányt és a numerikus toleranciát egy összetettebb, kis dimenziós ellenőrzési feladaton kell együtt használni.