28.1 Tensor, device és autograd

28.1.1 Helyzet és előzetes becslés

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 x=(2.0,−1.0,0.5)x=(2.0,\,-1.0,\,0.5), a cél y=1.0y=1.0, mértékegység nélküli játékpélda. A modell lineáris predikciót ad:

ŷ=w1x1+w2x2+w3x3+b=w⋅x+b,\hat{y} = w_1 x_1 + w_2 x_2 + w_3 x_3 + b = w\cdot x + b,

ahol a négy tanulandó paraméter w=(1.0,−0.5,0.0)w=(1.0,\,-0.5,\,0.0) és b=0.25b=0.25. A regularizált négyzetes veszteség:

L(w,b)=(ŷ−y)2+λ(∑j=13wj2+b2),λ=0.1.L(w,b)=\left(\hat{y}-y\right)^2+\lambda\left(\sum_{j=1}^{3}w_j^2+b^2\right), \qquad \lambda=0.1.

Az előzetes becslés: ŷ=2.0⋅1.0−1.0⋅(−0.5)+0.5⋅0.0+0.25=2.75\hat{y}=2.0\cdot 1.0-1.0\cdot(-0.5)+0.5\cdot 0.0+0.25=2.75, így az illeszkedési tag 1.752=3.06251.75^2=3.0625, a regularizációs tag 0.1⋅(12+0.252)=0.106250.1\cdot(1^2+0.25^2)=0.10625, a teljes veszteség L=3.16875L=3.16875. A parciális deriváltak a láncszabállyal:

∂L∂wj=2(ŷ−y)xj+2λwj,∂L∂b=2(ŷ−y)+2λb.\frac{\partial L}{\partial w_j}=2(\hat{y}-y)x_j+2\lambda w_j, \qquad \frac{\partial L}{\partial b}=2(\hat{y}-y)+2\lambda b.

A konkrét számokkal ∇wL=(7.2,−3.6,1.75)\nabla_w L=(7.2,\,-3.6,\,1.75) és ∂L/∂b=3.55\partial L/\partial b=3.55. 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: ww esetén (3,)(3,) vektor, bb 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.

28.1.2 A tenzor: a gradiens hordozója

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.

28.1.3 Minimális munkapélda CPU-n

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.

28.1.4 Az eredmény értelmezése

A kézi ellenőrző számítás a gráf mentén halad. A láncszabály a ŷ=w⋅x+b\hat{y}=w\cdot x+b, Lfit=(ŷ−y)2L_{fit}=(\hat{y}-y)^2 és L=Lfit+λRL=L_{fit}+\lambda R úton:

A korábbi előzetes becslésben a regularizációs tagot pontatlanul 0.10625-re írtam; a pontos érték 0.1⋅(1+0.25+0+0.0625)=0.131250.1\cdot(1+0.25+0+0.0625)=0.13125, így L=3.19375L=3.19375, 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 2λwj2\lambda w_j tagok a bb esetén 0.2⋅0.25=0.050.2\cdot 0.25=0.05 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 10−910^{-9} nagyságrendű hibával teljesül; a numerikus szempontból a float64float64 típus az indokolt választás.

A shape-invariáns: a w.grad alakja megegyezik a w alakjával, (3,)(3,) vektor; a b.grad skalár. Ez minden paraméternél így van: a gradiens az a vektor, amellyel a veszteség helyileg nő.

28.1.5 Feladatok

  1. É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?

  2. Implementálás. Ugyanazon xx, yy, ww, bb, λ=0.1\lambda=0.1 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, ε=10−5\varepsilon=10^{-5} lépéssel becsülje mind a négy parciális deriváltat: ∂L/∂θi≈[L(θi+ε)−L(θi−ε)]/(2ε)\partial L/\partial \theta_i \approx [L(\theta_i+\varepsilon)-L(\theta_i-\varepsilon)]/(2\varepsilon). Hasonlítsa össze a képletezett és a numerikus értékeket, és adjon hibakorlátot.

  3. 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?

  4. 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 λ=0\lambda=0 lenne.

28.1.6 Megoldások és indoklás

  1. A harmadik komponens ∂L/∂w3=2(ŷ−y)x3+2λw3=2⋅1.75⋅0.5+0.2⋅0.0=1.75\partial L/\partial w_3=2(\hat{y}-y)x_3+2\lambda w_3=2\cdot 1.75\cdot 0.5+0.2\cdot 0.0=1.75. 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: b←b−η⋅3.55b \leftarrow b-\eta\cdot 3.55.

  2. 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 * b

    A 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 ε=10−5\varepsilon=10^{-5} mellett a simuló függvény hibája O(ε2)≈10−10O(\varepsilon^2)\approx 10^{-10} nagyságrendű, a kerekítési hiba pedig ennél nagyobb is lehet, de az egyezés abszolút értelemben 10−710^{-7} alatt várható.

  3. 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: 2λw=(0.2,−0.1,0.0)2\lambda w=(0.2,\,-0.1,\,0.0). 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.

  4. 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 10−410^{-4} nagyságrendű, ezért az atol-t is nagyvonalúan kell megválasztani. λ=0\lambda=0 esetén a regularizációs tag eltűnik, a gradiens (7.0,−3.5,1.75)(7.0,\,-3.5,\,1.75) és 3.53.5 lesz.

28.1.7 Gyakori tévedés és önellenőrzés

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ódik

A 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.

28.1.8 Rövid összefoglaló

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.