În 2016 Meta a lansat PyTorch și aproape nimeni nu-l folosea. Framework-ul de deep learning implicit era TensorFlow, care avea bugetul de marketing al Google în spate și o trăsătură numită „static graphs” pe care marketing-ul o numea un beneficiu. Până în 2019 lumea ML academică defectase în masă către PyTorch. Până în 2022 industria urmase. Până în 2026 situația e tranșată: PyTorch este standardul. Lucrările noi de cercetare livrează implementări de referință în PyTorch. Întregul ecosistem Hugging Face e PyTorch-first. Marii furnizori de cloud au cu toții suport PyTorch de prima clasă. JAX are o nișă printre cercetătorii care țin obsesiv la performanța la nivel de compilator și echipele interne ale Google. TensorFlow trăiește mai departe în produsele Google și o coadă lungă de proiecte legacy, dar dacă pornești un proiect nou de deep learning azi și nu ai un motiv specific să alegi altceva, alegi PyTorch.
Lecția asta e despre PyTorch-ul pe care îl vei folosi efectiv zi de zi. Tensori. Autograd. API-ul nn.Module. Optimizers. DataLoader-ul. Apoi vom defini o mică rețea de clasificare și vom diseca fiecare bucată.
De ce a castigat PyTorch
Experiența TensorFlow 1.x, în 2017, era: definește un graf computațional static, compilează-l, apoi alimentează-l cu date. Debugging-ul însemna să tipărești formele tensorilor inserându-le în graf ca side effects. Control flow însemna ops speciale TensorFlow precum tf.cond. Toată chestia se simțea ca scrisul într-un limbaj diferit care întâmplător împărțea sintaxa Python.
Pariul lui PyTorch a fost: fii pur și simplu o bibliotecă Python. Tensorii sunt obiecte pe care le poți tipări, slici și trece în jur. Graful se construiește singur pe măsură ce apelezi operații și e aruncat când ai terminat. Dacă vrei un loop for în rețeaua ta, scrii un loop for. Dacă vrei să tipărești valori intermediare, apelezi print. Framework-ul s-a simțit ca NumPy cu trăsături în plus. Cercetătorii l-au preferat imediat. TensorFlow a recuperat în cele din urmă cu eager execution în 2.x, dar până atunci războiul se terminase.
Lecția aici nu e îngust despre framework-uri de deep learning. E despre design de bibliotecă: fă lucrul ușor să fie ușor, chiar la un cost de performanță, și lumea te va alege pe tine. JAX, framework-ul care a venit după PyTorch de la Google, a făcut un pariu diferit, puritatea funcțională și compilarea JIT îi dau avantaje de viteză pe anumite workload-uri, iar JAX și-a câștigat o nișă reală, dar limitată.
Tensori: NumPy plus trei lucruri
Un PyTorch tensor e exact ceea ce NumPy a numit ndarray din lecția 43, cu trei adăugiri: suport GPU, automatic differentiation și un API ușor diferit. Poți muta un tensor pe un GPU și operațiile pe el vor rula pe GPU. Poți cere oricărui tensor să urmărească gradienți, iar PyTorch va înregistra fiecare operație pe care o faci cu el.
import torch
# Creation, mostly mirroring NumPy
a = torch.tensor([1.0, 2.0, 3.0])
b = torch.zeros((3, 4))
c = torch.ones((2, 2))
d = torch.randn((100, 10)) # standard normal
e = torch.arange(10)
f = torch.eye(5) # identity matrix
# Element-wise math, broadcasting, indexing — all NumPy-shaped
print(a + 1)
print(d.shape, d.dtype) # torch.Size([100, 10]) torch.float32
print(d[0, :3])
print(d.mean(dim=0)) # mean along axis 0 → shape (10,)
Valorile implicite pentru dtype sunt diferite față de NumPy. PyTorch are implicit float32 pentru float-uri (NumPy are implicit float64). Pentru deep learning, float32 e implicitul corect, float64 e de două ori mai lent și de două ori memoria și aproape niciodată nu îți dă un model semnificativ mai bun. Rămâi la implicituri.
Devices: CPU, CUDA, MPS
Tensorii trăiesc pe un device. Implicit, CPU. Ca să folosești un GPU, mută tensorul:
device = torch.device(
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu"
)
print(f"Using device: {device}")
x = torch.randn(1000, 1000).to(device)
y = torch.randn(1000, 1000).to(device)
z = x @ y # matmul on the device
Boilerplate-ul de mai sus e pattern-ul corect în 2026. cuda sunt GPU-uri NVIDIA (cazul de producție). mps sunt Metal Performance Shaders de la Apple Silicon, merge pe Mac-urile din seria M, suficient de rapid pentru dezvoltare și modele mici, încă necompetitiv cu NVIDIA pentru antrenare serioasă. Fallback-ul cpu există pentru medii fără GPU.
Operațiile cer ca toți tensorii să fie pe același device. Un tensor pe CPU plus un tensor pe GPU e o eroare. Cea mai comună sesiune de debugging a oricărui utilizator nou de PyTorch e să uite să apeleze .to(device) pe model sau pe date.
Autograd: masinaria de gradienti
Orice tensor cu requires_grad=True urmărește fiecare operație la care participă. Când apelezi .backward() pe un scalar care depinde de el, PyTorch parcurge operațiile înregistrate invers și calculează gradienți ai scalarului față de fiecare tensor care avea requires_grad=True.
x = torch.tensor(3.0, requires_grad=True)
y = x ** 2 + 2 * x + 1 # y = (x+1)^2
y.backward() # compute dy/dx
print(x.grad) # 2*(x+1) = 8.0
Asta e toată mașinăria antrenării. Nu scrii derivate. Scrii forward pass-ul, calculezi un loss (un scalar), apelezi loss.backward() și PyTorch completează atributul .grad al fiecărui parametru. Optimizer-ul folosește apoi acei gradienți ca să actualizeze parametrii.
Un detaliu subtil, dar important: gradienții se acumulează. Dacă apelezi .backward() de două ori fără să zerorizezi gradienții între, primești suma celor doi gradienți în .grad. De aia fiecare training loop apelează optimizer.zero_grad() la început. E o capcană pentru începători și o trăsătură pentru utilizare avansată (gradient accumulation peste micro-batch-uri).
nn.Module: unde definesti retele
Ai putea defini o rețea ca o funcție liberă și un sac de tensori. Nu ar trebui. Clasa de bază nn.Module îți dă management de parametri, mutare pe device și serializare gratis. Două metode: __init__ declară layerele, forward definește calculul.
import torch.nn as nn
import torch.nn.functional as F
class MLP(nn.Module):
def __init__(self, in_dim: int, hidden_dim: int, n_classes: int):
super().__init__()
self.fc1 = nn.Linear(in_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
self.fc3 = nn.Linear(hidden_dim, n_classes)
self.dropout = nn.Dropout(p=0.2)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = F.relu(self.fc2(x))
x = self.dropout(x)
return self.fc3(x) # logits, not probabilities
model = MLP(in_dim=20, hidden_dim=128, n_classes=3).to(device)
print(model)
print(f"Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}")
Câteva lucruri de semnalat. nn.Linear(in, out) e exact y = x @ W.T + b cu W de formă (out, in), layer-ul tău standard fully-connected. Funcțiile de activare trăiesc în torch.nn.functional (importat ca F prin convenție) sau ca module în nn.ReLU() dacă preferi. nn.Dropout e un regularizator care zerorizează aleatoriu activări în timpul antrenării și nu face nimic în timpul evaluării. Metoda forward returnează logits, scoruri brute, nu probabilități. Funcțiile loss din PyTorch așteaptă logits și aplică softmax intern; făcând softmax tu însuți în forward și apoi din nou în loss e un bug clasic.
Pentru stive simple unde nu ai nevoie de logică custom, nn.Sequential e mai scurt:
model = nn.Sequential(
nn.Linear(20, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 3),
).to(device)
Folosesc nn.Module pentru orice mă aștept să crească. nn.Sequential pentru prototipuri de aruncat.
Optimizers si losses
Optimizer-ul deține parametrii modelului și știe cum să-i actualizeze dați fiind gradienți. Alegerile standard în 2026:
torch.optim.SGDcu momentum: clasic, încă folosit pentru computer vision.torch.optim.Adam: implicitul pentru majoritatea lucrurilor. Robust, convergență rapidă, iertător la learning rates proaste.torch.optim.AdamW: Adam cu weight decay corect. Standardul pentru transformere și majoritatea antrenărilor moderne.
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)
Funcția loss e un callable care ia (predicții, ținte) și returnează un scalar. Cele două pe care le vei folosi 80% din timp:
nn.CrossEntropyLosspentru clasificare multi-clasă. Ia logits de formă(batch, n_classes)și etichete întregi de clasă de formă(batch,).nn.MSELosspentru regresie.nn.BCEWithLogitsLosspentru clasificare binară (binary cross-entropy care ia logits, stabil numeric).
criterion = nn.CrossEntropyLoss()
DataLoaders: aducerea batch-urilor de date
Deep learning-ul antrenează pe batch-uri. Ai nevoie de un obiect care îți dă (inputs, targets) de o dimensiune fixă de batch, opțional shuffluit, opțional pe mai multe procese worker. Dataset și DataLoader din PyTorch îți dau asta.
from torch.utils.data import TensorDataset, DataLoader
# Imagine we already have these as tensors
X_train = torch.randn(10000, 20)
y_train = torch.randint(0, 3, (10000,))
train_ds = TensorDataset(X_train, y_train)
train_loader = DataLoader(
train_ds,
batch_size=64,
shuffle=True,
num_workers=2, # parallel data loading processes
pin_memory=True, # faster CPU→GPU transfer
)
for inputs, targets in train_loader:
inputs, targets = inputs.to(device), targets.to(device)
# ... training step ...
break
Pentru dataset-uri reale, imagini pe disc, text dintr-un fișier, orice are nevoie de preprocesare, faci subclasă la Dataset și definești __len__ și __getitem__. DataLoader-ul îl învelește. Vom folosi TensorDataset pentru exemplele de jucărie din modulul ăsta; training loop-ul lecției 57 presupune că ai un DataLoader funcțional.
torch.compile: accelerarea din 2.x
PyTorch 2.0, lansat în 2023, a introdus torch.compile. Învelești modelul tău și PyTorch face JIT-compile graf-ului computațional pentru hardware-ul tău:
model = torch.compile(model) # one line
În mod tipic obții o accelerare de antrenare de 1.5 până la 3x pe GPU-uri moderne. Există avertismente, prima iterație e lentă din cauza compilării, formele dinamice sunt încă brute, dar pentru rulări stabile de antrenare în producție, torch.compile e în esență performanță gratuită. Până în 2026 e implicitul în majoritatea proiectelor serioase.
Cand are sens JAX in schimb
Nu alege JAX ca primul tău framework de deep learning. PyTorch e implicitul corect. Dar vei auzi de JAX, deci versiunea scurtă:
JAX e funcțional. Modelele sunt funcții pure; parametrii sunt pytrees explicite trecute în și afară. JAX compilează agresiv prin XLA, ceea ce îi dă avantaje serioase de viteză pe TPU-uri și pe workload-uri uriașe batch-uite cu forme statice. Google folosește JAX intens intern. Unele grupuri de cercetare îl preferă. Trade-off-ul e că stilul funcțional și modelul compilation-first sunt mai puțin ergonomice pentru cod de cercetare dezordonat cu control flow dinamic. Rămâi la PyTorch decât dacă ai un motiv specific și echipa care să susțină alegerea.
Ce avem pana acum
Avem tensori. Avem un model. Avem un optimizer și o funcție loss. Avem un DataLoader. Avem totul cu excepția loop-ului care le leagă. Lecția 57 e acel loop, cele cinci linii care constituie pasul efectiv de antrenare, contabilitatea din jurul lor care face un sistem real de antrenare și alternativele de framework (Lightning, Hugging Face Trainer) care îți permit să sari peste boilerplate când nu ai nevoie de el.