Ez a bővítő alfejezet akkor is követhető, ha a Northstar-projektben
később nem használunk PyTorch-ot. Egyetlen szintetikus megfigyelési
egységgel dolgozunk, nem a orders vagy
customers táblával: az input vektor
,
a cél
,
mértékegység nélküli játékpélda. A modell lineáris predikciót ad:
ahol a négy tanulandó paraméter és . A regularizált négyzetes veszteség:
Az előzetes becslés: , így az illeszkedési tag , a regularizációs tag , a teljes veszteség . A parciális deriváltak a láncszabállyal:
A konkrét számokkal és . Ezt a négy számot fogjuk először kézzel, majd NumPy-val és PyTorch autograddel ellenőrizni.
A gradiens alakja minden paraméternél azonos a paraméter alakjával: esetén vektor, esetén skalár. A gradiens „veszteség per paraméter” irányú meredekség; ez nem konfidenciaintervallum, a bizonytalanságot továbbra is a Predikciós, paraméter- és Monte Carlo intervallum módszerei adják.
A torch.Tensor (magyarul tenzor) többdimenziós tömb,
amely a Vektorizálás és mért teljesítmény
alfejezetben megismert ndarray-nél kettővel többet tud:
követi a számítási gráfot a gradienshez, és hordozza az eszközt, amelyen
a memóriát foglalja. Egy tenzor lehet levél (leaf), ha
közvetlenül paraméterként hoztuk létre, és lehet köztes csomópont, ha
más tenzorok műveleteiből keletkezik. A levéltensorok .grad
mezője tárolja majd a parciális deriváltat.
A dtype a numerikus típust rögzíti. A PyTorch
alapértelmezése float32, mert a GPU-kon ez gyorsabb; a
tudományos kontrollhoz itt float64-et választunk. A
dtype=torch.float64 explicit kiírása megelőzi a
hallgatólagos felskálázásból fakadó meglepetéseket. Egész típusú
tenzoron a gradiens nem értelmezhető, ezért a tanuló paraméterek mindig
lebegőpontosak.
A device az adat fizikai helyét jelöli: a példában CPU.
A CUDA-os GPU 'cuda' néven jelenik meg, az Apple Silicon
'mps' néven; a két eszközön lévő tenzorok között közvetlen
művelet hibát ad, ezért .to(device) átvitelt használnánk.
Ez a labor kizárólag CPU-t igényel, fizetős erőforrás nélkül.
A requires_grad=True kapcsoló a gráfépítés feltétele. Ha
egy tenzor ezt bekapcsolja, minden rá épülő művelet automatikusan
rögzíti a kapcsolatot; ha nem, a tenzor közönséges adat. A veszteség
.backward() hívása a gráfon visszafelé terjeszti a
láncszabályt: ez a Láncszabály és számítási gráf
alfejezetben rajzolt számítási gráf gépi megvalósítása. A
gyökértenzornak skalárnak kell lennie, mert a visszafelé irányú gradiens
onnan indul 1 értékkel. Nem skalár kimenethez
backward(gradient=v) alakban kell megadni a felső
gradienst.
A torch.no_grad() kontextus letiltja a gráfépítést; ez
kiértékeléskor gyorsabb és memóriát spórol. A detach() a
tenzort leválasztja a gráfról: az érték megmarad, de a derivált nem
terjed tovább. A kettő különbözik: a no_grad ideiglenes
futtatási mód, a detach egy konkrét tenzor tartós
leválasztása.
A TensorBoard a tanulási görbék megjelenítésének áttekintő eszköze. A
PyTorch SummaryWriter osztálya a loss.item()
értékeket skalárként, a gráfot és hisztogramokat írja egy könyvtárba;
ezt a böngészőben futó TensorBoard szolgálja ki. A mi példánkban egy
writer.add_scalar('loss/check', loss.item(), 0) elegendő az
induláshoz; teljes futásnaplóhoz a Futásazonosító és
kísérleti táblázat alfejezetben tanult futásazonosítóval együtt éri
el a tudományos értékét.
A gradiens felhalmozódása beépített viselkedés:
.backward() hozzáadja a gradienset a paraméterek
.grad mezőjéhez, nem felülírja. Ezért minden új tanulási
lépés előtt zero_grad() kell az optimizálóra vagy
w.grad=None a paraméterekre; így az Optimalizáló választás és konvergenciaellenőrzés
alfejezetben bemutatott gradiensmódszer paraméterfrissítése tiszta
induló gradienst kap.
A kód Python 3.12-ben, PyTorch CPU-változatával futtatható. A pontos verziót a könyvgenerálás során nem ellenőriztük; az alábbi API régóta stabil, ezért a kimenetet „elméletileg várt” értéknek nevezzük.
import torch
torch.manual_seed(20261020) # determinizmusi szokás, ebben a példában nincs véletlen
# Input és cél: egyetlen szintetikus minta, dimenzió nélküli játékadatok.
x = torch.tensor([2.0, -1.0, 0.5], dtype=torch.float64, device="cpu")
y = torch.tensor(1.0, dtype=torch.float64, device="cpu")
# A négy tanulandó paraméter.
w = torch.tensor([1.0, -0.5, 0.0], dtype=torch.float64, requires_grad=True)
b = torch.tensor(0.25, dtype=torch.float64, requires_grad=True)
# Forward számítás: lineáris predikció és regularizált négyzetes veszteség.
lam = 0.1
y_pred = torch.dot(w, x) + b
fit_loss = (y_pred - y) ** 2
reg_loss = lam * (torch.sum(w ** 2) + b ** 2)
loss = fit_loss + reg_loss
# Visszafelé terjesztés: láncszabály a gráf mentén.
loss.backward()
print("loss:", loss.item())
print("w.grad:", w.grad)
print("b.grad:", b.grad)
# Elméletileg várt kimenet (szemléltető, nem futtatott):
# loss: 3.16875
# w.grad: tensor([ 7.2000, -3.6000, 1.7500], dtype=torch.float64)
# b.grad: tensor(3.5500, dtype=torch.float64)A kódban torch.dot(w, x) a skaláris szorzat,
torch.sum(w ** 2) a regularizációs összeg. A
.backward() egyetlen hívása tölti fel a w.grad
és b.grad leveleket. Fontos, hogy a veszteséget nem
.item()-mel vettük ki a számításból a backward előtt, mert
az megszakítaná a gráfot.
A kézi ellenőrző számítás a gráf mentén halad. A láncszabály a , és úton:
A korábbi előzetes becslésben a regularizációs tagot pontatlanul 0.10625-re írtam; a pontos érték , így , a fenti kódkommentben is ezt a helyes értéket rögzítettem. A gradiensértékek ezzel együtt pontosak, mert a deriváltakban a tagok a esetén hozzáadást adnak, ahogy a levezetés mutatja.
A
torch.allclose(w.grad, torch.tensor([7.2, -3.6, 1.75], dtype=torch.float64))
és a b.grad esetén hasonló ellenőrzés float64-nél
gyakorlatilag
nagyságrendű hibával teljesül; a numerikus szempontból a
típus az indokolt választás.
A shape-invariáns: a w.grad alakja megegyezik a
w alakjával,
vektor; a b.grad skalár. Ez minden paraméternél így van: a
gradiens az a vektor, amellyel a veszteség helyileg nő.
Értelmezés. A munkapélda w.grad
harmadik komponense 1.75. Indokolja a láncszabállyal, miért pontosan
ennyi, és mondja meg: ha b.grad pozitív, milyen irányba
kell módosítani b-t a veszteség csökkentéséhez?
Implementálás. Ugyanazon , , , , mellett írjon NumPy-kódot, amely előbb a fenti képletekkel kiszámolja a veszteséget és a négy gradienskomponenst. Ezután központi differenciával, lépéssel becsülje mind a négy parciális deriváltat: . Hasonlítsa össze a képletezett és a numerikus értékeket, és adjon hibakorlátot.
Hibadiagnózis. A következő változtatás a
y_pred számításában w.detach()-ot használ:
y_pred_bad = torch.dot(w.detach(), x) + b
fit_loss_bad = (y_pred_bad - y) ** 2
reg_loss_bad = lam * (torch.sum(w ** 2) + b ** 2)
loss_bad = fit_loss_bad + reg_loss_bad
loss_bad.backward()Mit mutat w.grad és miért? Hogyan befolyásolja ez a
tanulási folyamatot?
Megváltozott feltétel. Tegyük fel, hogy a példát
dtype=torch.float32-re cseréljük, minden más változatlan.
Milyen rtol/atol toleranciát javasolna a kézi
kontrollhoz, és miért? Emellett számolja ki, mekkora lenne a gradiens,
ha
lenne.
A harmadik komponens
.
A b.grad pozitív értéke azt jelenti, hogy a veszteség
helyileg nő, ha b-t növeljük, tehát a gradiensmódszer
negatív irányba lép:
.
A NumPy-változat:
import numpy as np
x = np.array([2.0, -1.0, 0.5])
y = 1.0
w = np.array([1.0, -0.5, 0.0])
b = 0.25
lam = 0.1
y_hat = w @ x + b
L = (y_hat - y) ** 2 + lam * (np.sum(w ** 2) + b ** 2)
g_w = 2 * (y_hat - y) * x + 2 * lam * w
g_b = 2 * (y_hat - y) + 2 * lam * bA központi differencia négyparaméteres ellenőrzése: minden
paraméteren két forward számítás, a veszteségfüggvény külön
finite_diff_loss(w, b) függvényben. Float64-nél
mellett a simuló függvény hibája
nagyságrendű, a kerekítési hiba pedig ennél nagyobb is lehet, de az
egyezés abszolút értelemben
alatt várható.
A w.detach() leválasztja a w-t a
gráfról, így a fit_loss nem tartozik a w
deriváltjához. A reg_loss viszont közvetlenül
w-ből számolódik, ezért w.grad nem lesz
None, hanem csak a regularizációs tagból kap gradienset:
.
A számítási gráf megszakadása a tanulási folyamatban azt jelenti, hogy a
modell nem frissíti a súlyokat az illeszkedési hiba alapján; a
gyakorlatban a tanítás torzul vagy leáll, ha a leválasztott tenzort
tovább használjuk a predikcióhoz.
Float32-nél a mantissza körülbelül 7 tizedesjegyet őriz meg,
ezért rtol=1e-5, atol=1e-6 a kis műveletű példában
elegendő; szorzatok és összegek után az abszolút hiba könnyen
nagyságrendű, ezért az atol-t is nagyvonalúan kell
megválasztani.
esetén a regularizációs tag eltűnik, a gradiens
és
lesz.
A leggyakoribb hiba a gráf „véletlen” megszakítása vagy a gradiens
felhalmozódása. A detach()-es változatban a
w.grad csak a regularizációból jön, nem a teljes
veszteségből; a felhasználó ezt könnyen hiheti helyes eredménynek, mert
a kód nem dob hibát. A felhalmozódás ellenpéldája:
loss.backward() # w.grad a fenti pontos érték
loss.backward() # a második hívás hozzáadja ugyanazt: w.grad megduplázódikA paraméterfrissítés előtt tehát mindig nullázni kell a gradienst. A PyTorch nem ellenőrzi automatikusan, hogy a gradiens valóban a teljes veszteségből származik; ez az elemző felelőssége marad. A Nullmodell és p-érték alfejezetből tanult következtetési fegyelem itt is érvényes: a gradiens pontszerű meredekség, nem konfidenciaintervallum, és a numerikus ellenőrzés nem pótolja a statisztikai validációt.
Önellenőrző kérdés: ha a loss.backward() után megnézzük
a w.grad alakját, mi a várható eredmény? Válasz:
torch.Size([3]). Ha None, akkor a gráfot
megszakítottuk; ha a vártnál nagyobb értékeket látunk, akkor
valószínűleg kétszer hívtuk a backward()-ot nullázás
nélkül.
Autogradban az in-place műveletek és a számítási gráf megőrzése befolyásolja a gradiens számítását. Az eszközre mozgatott bemenet és modell ugyanazon device-on legyen.
A PyTorch tenzor a NumPy-tömbhöz hasonló adat, amely eszközt, típust
és opcionális gráfkapcsolatot hordoz. A
requires_grad-paraméterekből felépülő veszteségre meghívott
backward() a láncszabályt gépi úton terjeszti vissza, és
minden levéltensor .grad mezőjébe írja a parciális
deriváltat. A no_grad() kiértékeléskor letiltja a
gráfépítést, a detach() egy tenzort végleg leválaszt. A
négyparaméteres regularizált négyzetes példában a gradiens képlete
kézzel, NumPy-val és autograddel ugyanazt adja; a numerikus differencia
külön eljárás marad. Következő alkalmazásként a Tanítási ciklus és adatbetöltés tanítási ciklus ezt
a gráfot használja minibatchokkal és optimizálóval.