A Tensor, device és autograd alfejezetben megismert tenzor, dtype, device és autograd most egyetlen ismétlődő munkafolyamatba áll össze: a tanítási ciklusba. A ciklus szereposztása egyszerű, de a részek felcserélése vagy elhagyása csendes, nehezen észrevehető hibákat okozhat.
Egy kis szintetikus regressziós feladatot oldunk meg: a bemenet
,
két folytonos feature-rel; a cél
,
folytonos érték, mértékegység nélküli szimulált szám. A megfigyelési
egység egy sor, tehát egy
pár. A generáló folyamat lineáris:
,
ahol
független,
szórású normális zaj. A cél egy PyTorch lineáris réteg betanítása
minibatch-gradiens módszerrel, majd ugyanazon
train/test felosztáson összehasonlítani a
sklearn.linear_model.LinearRegression baseline-nal.
Előzetes becslés: a lineáris baseline teszt RMSE-je a zaj miatt körül várható, így a jól konvergált PyTorch modell teszt RMSE-jének is 0.5 közelében kell lennie. A két modell közötti eltérés elsősorban az optimalizáló befejezetlenségéből és a numerikus különbségből adódik; a split azonossága az összehasonlítás feltétele.
Dataset. A torch.utils.data.Dataset egy
indexelhető gyűjtemény: tudnia kell a méretét (__len__) és
az
-edik
elemet (__getitem__). Nem kötelező az egész adatot tenzorrá
alakítani, de kis CPU-s példában ez a legegyszerűbb. A targetet
alakúra hozzuk, mert a veszteségfüggvény a predikcióval azonos shape-et
vár.
class RegressionDataset(Dataset):
def __init__(self, X, y):
self.X = torch.tensor(X, dtype=torch.float32)
self.y = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
def __len__(self):
return len(self.y)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]DataLoader. A DataLoader a Datasetet batch-ekre
osztja, és opcionálisan keveri (shuffle=True). A batch a
tanításhoz használt részhalmaz; az epoch a teljes tanítóhalmaz egyszeri
végigjárása. Ha batch_size=32, akkor egy 960 soros
tanítóhalmazból egy epoch 30 batchből áll. A shuffle minden
epoch elején új sorrendet állít elő, ezért a batch-összetétel epochról
epochra változik. Fontos: a splitet már a Dataset létrehozása előtt
elvégeztük; a DataLoader nem végez modellértékelési felosztást.
Veszteség és sztochasztikus gradiens. A teljes tanítóhalmazra vett átlagos négyzetes hiba
ahol a modell összes paramétere, lineáris modellnél , , . Egy batchre
A gradiens a parciális deriváltak vektora, például a -re:
A minibatch-gradiens a teljes adathalmaz gradiensének véletlen mintából számolt, torzítatlan becslése; ezért „sztochasztikus”. Minél kisebb a batch, annál zajosabb a becslés, de annál kevesebb memória kell; a nagyobb batch simább, de azonos epochszám alatt kevesebb paraméterfrissítést jelent.
Optimizer, zero_grad, step. Az optimizer tárolja a paraméterekre vonatkozó állapotot, és elvégzi a frissítést:
ahol
a tanulási sebesség (learning rate),
pedig az aktuális batch veszteségének gradiense. A
loss.backward() a gradienst hozzáadja a
paraméterek .grad mezőjéhez, ezért minden batch előtt
optimizer.zero_grad() hívás kell. Ha ezt elhagyjuk, a
korábbi batch gradiensei halmozódnak, és a lépés nem az adott batch
veszteségét minimalizálja.
train/eval mód. A model.train() és
model.eval() a modul training állapotát
állítja. A lineáris rétegnél nincs hatása, de a Dropout és
a BatchNorm rétegeknél igen: tanítás közben a dropout
véletlenszerűen nulláz, kiértékeléskor viszont identitásként viselkedik;
a BatchNorm tanítás közben a batch statisztikákat, kiértékeléskor a futó
statisztikákat használja. Ezt a későbbi ellenpélda mutatja.
A példa CPU-n fut. A torch és scikit-learn
csomagot a fejezet bővítő környezetébe kell telepíteni; az Interpreter és virtuális környezet alfejezetben
bemutatott projektmódszerrel külön környezetet érdemes használni. Az
alábbi kódot train_loop.py néven mentve, a munkakönyvtárból
a python train_loop.py paranccsal futtathatjuk.
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
# 1. Szintetikus adat
rng = np.random.default_rng(42)
n = 1200
X = rng.normal(0.0, 1.0, size=(n, 2))
y = 1.8 * X[:, 0] - 0.7 * X[:, 1] + 0.3 + rng.normal(0.0, 0.5, size=n)
# 2. Azonos split mindkét modellhez
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 3. Dataset és DataLoader
class RegressionDataset(Dataset):
def __init__(self, X_data, y_data):
self.X = torch.tensor(X_data, dtype=torch.float32)
self.y = torch.tensor(y_data, dtype=torch.float32).unsqueeze(1)
def __len__(self):
return len(self.y)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
train_ds = RegressionDataset(X_train, y_train)
test_ds = RegressionDataset(X_test, y_test)
# A DataLoader reprodukálható keveréséhez saját generátort adunk
torch_gen = torch.Generator().manual_seed(0)
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, generator=torch_gen)
# 4. Modell, veszteség, optimalizáló
torch.manual_seed(0)
model = nn.Linear(2, 1)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 5. Tanítási ciklus
n_epochs = 40
for epoch in range(n_epochs):
model.train()
epoch_loss = 0.0
for Xb, yb in train_loader:
optimizer.zero_grad()
pred = model(Xb)
loss = loss_fn(pred, yb)
loss.backward()
optimizer.step()
epoch_loss += loss.item() * len(yb)
epoch_loss /= len(train_ds)
# 6. Kiértékelés eval módban, gradiensgráf nélkül
model.eval()
with torch.no_grad():
pred_train = model(train_ds.X)
pred_test = model(test_ds.X)
pt_train_rmse = torch.sqrt(loss_fn(pred_train, train_ds.y)).item()
pt_test_rmse = torch.sqrt(loss_fn(pred_test, test_ds.y)).item()
# 7. sklearn baseline ugyanazon a spliten
lr = LinearRegression().fit(X_train, y_train)
sk_test_rmse = float(np.sqrt(mean_squared_error(y_test, lr.predict(X_test))))
print(f"PyTorch train RMSE: {pt_train_rmse:.3f}")
print(f"PyTorch test RMSE: {pt_test_rmse:.3f}")
print(f"sklearn test RMSE: {sk_test_rmse:.3f}")A TensorBoard-naplózás opcionális kiegészítő. Az
nn.MSELoss átlagos négyzetes hibát ad; az RMSE ennek gyöke,
ezért az epoch átlagveszteségéből a gyököt vonva kapjuk a train RMSE-t.
A torch.no_grad() blokk alatt a PyTorch nem építi a
számítási gráfot, így a kiértékelés gyorsabb és nem foglal fölösleges
memóriát. A futás előtt a „nem futtatott” jelleg miatt az alábbi kimenet
szemléltető, a seedtől és a konvergenciától függő:
PyTorch train RMSE: 0.502
PyTorch test RMSE: 0.508
sklearn test RMSE: 0.511
A TensorBoardhoz tartozó minimális vázlat:
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/linear_baseline")
# a tanítási ciklusban, minden epoch után:
writer.add_scalar("loss/train_epoch", epoch_loss, epoch)
writer.close()A görbe megtekintéséhez külön terminálablakban, a projektgyökérből:
tensorboard --logdir runs. A pontos telepítési és indítási
lépések a helyi Python-környezettől függenek; a tensorboard
csomagot a futtató környezetbe külön kell telepíteni.
A szemléltető RMSE-értékek azt mutatják, hogy a PyTorch lineáris
réteg és a sklearn lineáris regresszió ugyanazon a spliten
közel azonos hibát ad. Ez várható, mert mindkettő ugyanazt a lineáris
hipotézist és négyzetes veszteséget használja; a sklearn
zárt alakú legkisebb négyzetek megoldást számol, a PyTorch pedig
iteratív gradienslépésekkel közelíti ugyanazt a minimumot. A teszt RMSE
0.5 körüli értéke a generáló zaj szórásának felel meg: ezt a hibát akkor
sem tudjuk eltüntetni, ha a súlyokat pontosan visszanyerjük, mert a cél
magában hordozza a
szórású zajt.
A közel egyezés azonban nem igazolja, hogy a tanítási ciklus minden eleme helyes; ha például a teszt sorok beszivárognának a tanításba, a hiba akár ennél is alacsonyabbnak tűnhetne. A regressziós súly értéke itt nem oksági következtetés: a Tesztválasztás és hatásméret tanulsága szerint a becsült együttható nem bizonyítja, hogy a feature okozza a cél változását.
A model.train() és model.eval() eltérő
viselkedést okozhat dropout és batch normalization mellett; validáció
során használjunk torch.no_grad() vagy
torch.inference_mode() kontextust.
Értelmezés. Egy tanítási ciklus két batchből
áll, a tanulási sebesség
.
Az első batch gradiense
,
a másodiké
.
Számold ki a paraméter teljes változását (a) helyes
zero_grad hívással, (b) a zero_grad
elhagyásával. Mi a hiba tartalmi jelentése?
Implementálás. Írj
eval_rmse(model, loader, loss_fn) függvényt, amely
model.eval() módban, torch.no_grad() alatt
kiszámolja egy DataLoader adatainak RMSE-jét. A függvény ne építsen
gradiensgráfot. Figyelj arra, hogy az utolsó batch mérete kisebb is
lehet.
Hibadiagnózis. Egy tanuló a következőt írta:
X_all = torch.tensor(X, dtype=torch.float32)
y_all = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
train_ds = TensorDataset(X_all, y_all) # az egész X, nem X_train
# ...
sk_model = LinearRegression().fit(X_train, y_train)Nevezd meg azt a két problémát, amely az összehasonlítást érvénytelenné teszi, és add meg a javítást.
batch_size=1, batch_size=32 és
batch_size=512 értékekkel, azonos lr=0.01,
n_epochs=40 mellett. Rögzítsd az utolsó epoch train
RMSE-jét és a teszt RMSE-t. Melyik beállításnál várható a legzajosabb
tanítási görbe, és miért nem garantálja a legnagyobb batch a legjobb
teszt RMSE-t? A válaszban nevezd meg a rögzítendő feltételeket is.Helyes zero_grad esetén az első frissítés
,
a második
,
összesen
.
Elhagyás esetén az első lépés után is
,
de a második batch előtt a gradiens nem nullázódik, így a
.grad értéke
lesz, és a második frissítés
.
Az összes változás
a helyes
helyett. A hiba az, hogy a frissítés nem az aktuális batch veszteségének
gradiensét, hanem a felhalmozott gradienst használja; a modell rosszabb
lépést tesz, mintha két batch együttes, de nem skálázott veszteségét
minimalizálná.
Példa megoldás:
def eval_rmse(model, loader, loss_fn):
model.eval()
total_sq = 0.0
n = 0
with torch.no_grad():
for Xb, yb in loader:
pred = model(Xb)
total_sq += loss_fn(pred, yb).item() * len(yb)
n += len(yb)
return (total_sq / n) ** 0.5Az invariáns: a loss_fn(pred, yb) a batch átlagos
MSE-je, ezért a len(yb)-vel való szorzás adja a batch
négyzetes hibák összegét; a teljes RMSE ennek a teljes elemszámra vett
átlagából számított gyök. Tolerancia: a függvénynek a tanításhoz
használt teljes epoch-veszteség számításával azonos eredményt kell adnia
ugyanazon adat és modell esetén. A hívás után a tanítás folytatásához
model.train() szükséges.
Az első probléma a tesztadat beszivárgása: a
train_ds az egész X-et tartalmazza, tehát a
PyTorch modell olyan sorokon is tanul, amelyek a tesztben szerepelnek.
Ez optimista torzítást okoz, és sérti a Tanítás,
validáció és érintetlen teszt alfejezetben megfogalmazott érintetlen
teszt elvét. A második probléma az összehasonlítás ellentmondásossága: a
sklearn modell X_train-ből tanul, a PyTorch
modell X-ből, így a két modell nem ugyanazon a
tanítóhalmazon értékelhető. Javítás: mindkét modell kizárólag
X_train-ből tanuljon, a tesztelés pedig kizárólag
X_test-ből; a TensorDataset-et a
train_test_split eredményéből kell felépíteni.
Azonos adatgeneráló folyamat és szokásos mintavételi feltételek
mellett a batch_size=1 tipikusan nagyobb varianciájú
gradiensbecslést ad, ezért a tanítási görbe erősen ugrál, és az utolsó
epoch train RMSE-je kevésbé informatív. A batch_size=512
simább gradienslépéseket ad, de a körülbelül 960 soros tanítóhalmazon
egy epoch alatt csak egy vagy két frissítés történik; 40 epoch alatt ez
lényegesen kevesebb lépés, mint a 32-es batchnél. Ezért előfordulhat,
hogy a legnagyobb batch nem éri el ugyanazt a megoldást. Az
összehasonlításhoz rögzíteni kell a seedet, az epochok számát vagy a
frissítések számát, a tanulási sebességet, a hardvert és a
könyvtárverziókat; az egyetlen futásból származó RMSE-különbség nem
önmagában bizonyíték.
A model.eval() elmaradása akkor okoz feltűnő hibát, ha a
modell tartalmaz dropout vagy BatchNorm réteget. Példaként egy extrém,
didaktikus dropout:
class DropoutModel(nn.Module):
def __init__(self):
super().__init__()
self.drop = nn.Dropout(p=1.0)
self.linear = nn.Linear(1, 1)
def forward(self, x):
return self.linear(self.drop(x))Tanítási módban a p=1.0 dropout minden bemeneti elemet
nulláz, ezért a modell csak a bias-t tanulja; kiértékelési módban a
dropout kikapcsol, és a bemenet átjut. Ha a tesztelésnél elmarad az
eval(), a tesztpredikciók a tanítási viselkedést követik,
és az RMSE eltérhet a helyes értéktől. Az eltérés
iránya nem általánosítható: a dropout miatt egyes mintákon romolhat,
másokon akár javulhat is a mérőszám. A szokásos
–
tartományban a hatás kevésbé feltűnő, de az értékelési protokoll így is
hibás. A lineáris példánkban nincs dropout, így a hiba ott nem
jelentkezett volna; a train/eval szokást mégis minden modellnél be kell
tartani.
Önellenőrzés: a torch.no_grad() nem helyettesíti az
eval() hívást. A no_grad csak a gradiensgráf
építését kapcsolja ki, de a dropout és BatchNorm train-viselkedése a
modul training állapotától függ. A kettő külön célt
szolgál, és teszteléskor általában együtt van rá szükség.
A tanítási ciklus egy epoch során DataLoader-batch-eken iterál,
minden batch előtt nullázza a gradienst, kiszámítja a veszteséget, a
backward hívással gradienst számol, végül az optimizer
frissíti a paramétereket. A batch-gradiens a teljes adat gradiensének
zajos becslése; a batch méret a gradiens zaját és a lépésszámot is
befolyásolja. A kiértékelést eval módban és gradiensgráf nélkül
végezzük, mert a Dropout és BatchNorm viselkedése eltér tanítás közben.
A lineáris réteg ugyanazon a spliten közel a sklearn
lineáris baseline hibáját éri el, ami az optimalizálás és az invariáns
ellenőrzésére használható. A következő lépés a generalizáció és a
gyorsító választás felé vezet.