A la lliçó anterior vas obrir el capó de TensorFlow i vas descobrir que model.fit() és, en el fons, un bucle de GradientTape empaquetat. Avui coneixeràs el framework que va decidir no empaquetar aquest bucle: PyTorch. Creat per Meta (Facebook) i publicat el 2017, PyTorch és avui l'estàndard de facto en recerca —la gran majoria dels papers que llegiràs publiquen el seu codi en PyTorch— i una opció de primera línia a la indústria. La seva filosofia és donar-te el control explícit de cada pas. Per demostrar-te que ja saps gairebé tot el que cal, en aquesta lliçó reproduirem el mateix exemple de gradient de 06-01 i reescriurem en PyTorch la xarxa MNIST 784-128-64-10 de 02-05, entrenant-la fins a comparar resultats amb la versió Keras. L'equip de TecnoMarket vol parlar els dos idiomes; tu també.
Contingut
- Què és PyTorch i la seva filosofia
- Tensors de PyTorch: creació, operacions i dispositius
- Autograd: el GradientTape de PyTorch
- Construir models amb nn.Module
- El bucle d'entrenament explícit, línia a línia
- Avaluació amb torch.no_grad()
- Entrenar la xarxa MNIST i comparar amb Keras
Què és PyTorch i la seva filosofia
PyTorch va néixer als laboratoris de recerca de Meta com a successor "pythònic" de Torch (un framework en Lua). Tres idees en defineixen el caràcter:
- Define-by-run (definició dinàmica): el graf d'operacions es construeix mentre s'executa el codi Python, no abans. Cada forward pass pot ser diferent: pots fer servir
if, buclesfor, imprimir tensors intermedis amb un simpleprint. Se sent com numpy amb gradients. - Control explícit: res de
fit(). El bucle d'entrenament l'escrius tu. Més línies de codi, sí, però zero màgia: quan alguna cosa falla, veus exactament on. - Estàndard de facto en recerca: si al mòdul 5 et va picar la curiositat pels transformers i els LLMs, gairebé tot el seu codi obert (inclosa la llibreria
transformersde Hugging Face) està construït sobre PyTorch.
| Filosofia | Keras/TensorFlow | PyTorch |
|---|---|---|
| Lema implícit | "El que és comú ha de ser fàcil" | "Tot ha de ser explícit" |
| Bucle d'entrenament | Empaquetat a fit() |
L'escrius tu |
| Sensació en fer-lo servir | Conduir un automàtic | Conduir un manual |
Instal·lació (ja la vas fer a 01-05 amb pip install torch; afegeix-hi torchvision per als datasets de visió):
import torch
print(torch.__version__) # p. ex. 2.x
print(torch.cuda.is_available()) # True si hi ha GPU NVIDIA configuradaTensors de PyTorch: creació, operacions i dispositius
Els tensors de PyTorch són conceptualment idèntics als de TensorFlow. Canvia la sintaxi, no la idea:
import torch
escalar = torch.tensor(4.99) # preu d'un cable USB
vector = torch.tensor([120.0, 15.5, 899.0]) # preus de 3 productes
matriu = torch.rand(2, 3) # aleatori uniforme, shape (2,3)
zeros = torch.zeros(3, 3)
print(vector.shape) # torch.Size([3])
print(vector.dtype) # torch.float32 (mateix default que TF)Operacions, amb la seva equivalència:
| Operació | TensorFlow (06-01) | PyTorch |
|---|---|---|
| Producte matricial | tf.matmul(a, b) |
torch.matmul(a, b) o a @ b |
| Mitjana | tf.reduce_mean(a) |
a.mean() |
| Canviar la forma | tf.reshape(a, (4,1)) |
a.reshape(4, 1) o a.view(4, 1) |
| Convertir el tipus | tf.cast(a, tf.float32) |
a.float() o a.to(torch.float32) |
| A numpy | a.numpy() |
a.numpy() (idèntic) |
El broadcasting funciona amb les mateixes regles que vas veure a 06-01: dimensions iguals o de mida 1 són compatibles.
El pont amb numpy
import numpy as np
arr = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(arr) # numpy -> tensor (comparteixen memòria!)
arr2 = t.numpy() # tensor -> numpy (també comparteixen memòria)
t[0] = 99.0
print(arr[0]) # 99.0 -> canviar-ne un canvia l'altreQue comparteixin memòria és eficient (no hi ha còpia) però pot sorprendre: si no vols el vincle, fes servir torch.tensor(arr) (còpia) en lloc de from_numpy.
Dispositius: .to(device)
A PyTorch moure dades entre CPU i GPU és explícit (a TensorFlow era automàtic). El patró universal que veuràs a tot el codi PyTorch:
device = "cuda" if torch.cuda.is_available() else "cpu"
x = torch.rand(32, 784)
x = x.to(device) # moure el tensor a la GPU (o quedar-se a la CPU)
# model.to(device) # els models es mouen igualRegla d'or: model i dades han de ser al mateix dispositiu, o PyTorch llançarà un error del tipus Expected all tensors to be on the same device. És dels errors més comuns en començar.
Autograd: el GradientTape de PyTorch
PyTorch també grava les operacions per derivar automàticament, però sense cinta explícita: n'hi ha prou de marcar un tensor amb requires_grad=True i totes les operacions que s'hi fan queden registrades. Reproduïm el mateix exemple de 06-01 —minimitzar L(w) = (w - 4)²— per comparar els dos estils cara a cara:
import torch
w = torch.tensor(0.0, requires_grad=True) # ~ tf.Variable(0.0)
loss = (w - 4.0) ** 2 # el graf es construeix sol, sense "with tape"
loss.backward() # ~ tape.gradient(loss, w): deriva cap enrere
print(w.grad) # tensor(-8.) -> el mateix -8.0 que a 06-01Diferències clau respecte a GradientTape:
- No hi ha context
with: qualsevol operació sobre un tensor ambrequires_grad=Truees grava sempre. backward()es crida sobre la pèrdua, i els gradients apareixen a l'atribut.gradde cada tensor fulla.- Els gradients s'acumulen: si crides
backward()dues vegades sense netejar,.gradconté la suma. Per això tot bucle PyTorch neteja gradients a cada pas.
El descens del gradient complet, bessó del de 06-01:
w = torch.tensor(0.0, requires_grad=True)
lr = 0.1
for pas in range(30):
loss = (w - 4.0) ** 2
loss.backward() # calcula dL/dw i el deixa a w.grad
with torch.no_grad(): # actualitzar SENSE gravar aquesta operació
w -= lr * w.grad # w = w - lr * gradient
w.grad.zero_() # netejar! si no, s'acumularia
print(w.item()) # ~3.995: convergeix a 4, igual que numpy (02-03) i TF (06-01)Tres versions del mateix algorisme, tres nivells d'automatització:
| Versió | Derivada | Actualització | Bucle |
|---|---|---|---|
| numpy (02-03) | A mà | A mà | A mà |
| TensorFlow (06-01) | Automàtica (tape) | A mà (assign_sub) |
A mà |
| PyTorch (avui) | Automàtica (autograd) | A mà (després la farà optimizer.step()) |
A mà |
Keras fit() |
Automàtica | Automàtica | Automàtic |
Construir models amb nn.Module
A PyTorch els models es construeixen sempre amb subclassing —la tercera API que vas veure a 06-01 aquí és la forma estàndard—. S'hereta de nn.Module, es declaren les capes a __init__ i es connecten a forward(). Reescriguem la xarxa MNIST 784-128-64-10 de 02-05:
import torch
from torch import nn
class XarxaMNIST(nn.Module):
def __init__(self):
super().__init__()
self.capa1 = nn.Linear(784, 128) # ~ Dense(128) sobre entrada 784
self.capa2 = nn.Linear(128, 64) # ~ Dense(64)
self.sortida = nn.Linear(64, 10) # ~ Dense(10)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.capa1(x))
x = self.relu(self.capa2(x))
return self.sortida(x) # logits SENSE softmax (vegeu la nota)
model = XarxaMNIST()
print(model) # imprimeix l'estructura, com summary() a KerasObservacions importants:
nn.Linear(784, 128)demana entrada i sortida explícites; Keras inferia l'entrada de la dada. Més escriptura, menys sorpreses.- No hi ha softmax a la sortida. A PyTorch, la funció de pèrdua
nn.CrossEntropyLossespera logits crus (aplica log-softmax internament per estabilitat numèrica). És l'equivalent exacte de compilar a Keras ambSparseCategoricalCrossentropy(from_logits=True). Posar softmax + CrossEntropyLoss és un error clàssic que degrada l'entrenament. - No crides mai
forward()directament: crides el model,model(x), i PyTorch invocaforwardamb la maquinària de hooks necessària. - Comptar paràmetres:
sum(p.numel() for p in model.parameters())→ 109.386, exactament els mateixos que la versió Keras de 02-05 (784·128+128 + 128·64+64 + 64·10+10).
El bucle d'entrenament explícit, línia a línia
Aquí hi ha la gran diferència amb Keras. Primer, les dades amb DataLoader (el bessó de tf.data):
from torch.utils.data import DataLoader, TensorDataset
from torchvision import datasets, transforms
transform = transforms.ToTensor() # PIL -> tensor float32 en [0,1]
train_ds = datasets.MNIST(root="data", train=True, download=True, transform=transform)
test_ds = datasets.MNIST(root="data", train=False, download=True, transform=transform)
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) # ~ shuffle+batch de tf.data
test_loader = DataLoader(test_ds, batch_size=32)I ara el bucle complet, comentat línia a línia:
device = "cuda" if torch.cuda.is_available() else "cpu"
model = XarxaMNIST().to(device) # model al dispositiu
loss_fn = nn.CrossEntropyLoss() # ~ sparse_categorical_crossentropy
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # ~ optimizer="adam"
for epoch in range(5): # ~ epochs=5 a fit()
model.train() # mode entrenament (afecta dropout/BN)
for x_batch, y_batch in train_loader: # ~ fit() itera els lots per tu
x_batch = x_batch.view(x_batch.size(0), -1) # (32,1,28,28) -> (32,784), el flatten
x_batch, y_batch = x_batch.to(device), y_batch.to(device)
logits = model(x_batch) # 1. FORWARD: prediccions
loss = loss_fn(logits, y_batch) # 2. PÈRDUA: quant ens equivoquem
optimizer.zero_grad() # 3. NETEJAR gradients acumulats
loss.backward() # 4. BACKWARD: repartir la culpa (02-03)
optimizer.step() # 5. ACTUALITZAR pesos amb Adam
print(f"Època {epoch+1}: pèrdua darrer lot = {loss.item():.4f}")Desglossament dels cinc passos del cor del bucle:
- Forward (
model(x_batch)): les dades travessen la xarxa i en surten els logits. És la propagació cap endavant de 02-03. - Pèrdua (
loss_fn(...)): un únic número que mesura l'error del lot, com a 02-04. optimizer.zero_grad(): posa a zero els.gradde tots els paràmetres. Recorda: autograd acumula; sense aquesta línia, cada pas sumaria els gradients de tots els passos anteriors i l'entrenament divergiria.loss.backward(): backpropagation. Calcula el gradient de la pèrdua respecte a cadascun dels 109.386 paràmetres.optimizer.step(): Adam actualitza cada paràmetre fent servir el seu.grad. És elw.assign_sub(lr * grad)de 06-01, amb la sofisticació d'Adam (02-04).
Què automatitza Keras i què exposa PyTorch
| Tasca | Keras fit() |
PyTorch |
|---|---|---|
| Iterar èpoques i lots | Automàtic | for epoch... / for x, y in loader |
| Moure dades a la GPU | Automàtic | .to(device) manual |
| Forward | Automàtic | model(x) |
| Càlcul de la pèrdua | Automàtic (la dones a compile) |
loss_fn(logits, y) |
| Netejar gradients | Automàtic | optimizer.zero_grad() |
| Backward | Automàtic | loss.backward() |
| Actualitzar pesos | Automàtic | optimizer.step() |
| Mètriques i barra de progrés | Automàtic | Les calcules/imprimeixes tu |
| Mode train/eval (dropout, BN) | Automàtic | model.train() / model.eval() |
Cap columna no és "millor": Keras et dona velocitat per al que és estàndard; PyTorch et dona transparència per al que no ho és. Quan a 05-01 vas veure l'esquelet de l'entrenament d'una GAN (dues xarxes, dos optimitzadors alternant-se), aquesta mena de bucle a mida és on l'estil explícit de PyTorch brilla.
Avaluació amb torch.no_grad()
En avaluar no volem entrenar: no calen gradients. torch.no_grad() desactiva la gravació d'autograd, estalviant memòria i temps:
model.eval() # mode avaluació (dropout off, BN congelada)
correctes, total = 0, 0
with torch.no_grad(): # res del que hi ha a dins no es grava
for x_batch, y_batch in test_loader:
x_batch = x_batch.view(x_batch.size(0), -1).to(device)
y_batch = y_batch.to(device)
logits = model(x_batch)
preds = logits.argmax(dim=1) # classe amb el logit més alt
correctes += (preds == y_batch).sum().item()
total += y_batch.size(0)
print(f"Precisió en test: {correctes / total:.4f}")Dues coses diferents que se solen confondre:
model.eval()canvia el comportament de certes capes (dropout deixa d'apagar neurones, batch norm fa servir les seves estadístiques acumulades — ho vas veure a 05-04).torch.no_grad()desactiva el càlcul de gradients.
En avaluació es fan servir totes dues. És l'equivalent del que model.evaluate() de Keras fa tot sol.
Entrenar la xarxa MNIST i comparar amb Keras
Executant el bucle anterior 5 èpoques (el mateix pressupost que a 02-05), un resultat típic:
| Keras (02-05) | PyTorch (avui) | |
|---|---|---|
| Arquitectura | 784-128-64-10, ReLU | Idèntica |
| Paràmetres | 109.386 | 109.386 |
| Optimitzador / pèrdua | Adam / crossentropy | Adam / CrossEntropyLoss |
| Precisió en test (~5 èpoques) | ~97,7 % | ~97,5 – 97,9 % |
| Línies de codi d'entrenament | ~5 (compile + fit) |
~25 (bucle explícit) |
La conclusió que l'equip de TecnoMarket apunta a la seva pissarra: mateixa xarxa, mateixes dades, mateix resultat. Les petites diferències decimals venen de la inicialització aleatòria i de l'ordre dels lots, no del framework. El que canvia és l'experiència de desenvolupament, i això és exactament el que compararem sistemàticament a la propera lliçó.
Errors Comuns i Consells
- Oblidar
optimizer.zero_grad(): l'error número u. Els gradients s'acumulen, la pèrdua balla o explota i no hi ha cap missatge d'error que t'avisi. Si el teu entrenament PyTorch "no convergeix", revisa això primer. - Softmax + CrossEntropyLoss:
nn.CrossEntropyLossja inclou el softmax (logarítmic). Afegirnn.Softmaxa la sortida entrena malament en silenci. Sortida = logits crus. - Model i dades en dispositius diferents:
Expected all tensors to be on the same device. Mou-los tots dos amb.to(device); el model un cop, les dades a cada lot. - Oblidar
model.eval()en avaluar: amb el dropout actiu en test, la precisió mesurada serà pitjor que la real. I a l'inrevés, oblidarmodel.train()en tornar a entrenar. .item()vs. el tensor:lossés un tensor connectat al graf; per registrar-ne el valor fes servirloss.item(). Acumular tensors en una llista durant moltes èpoques reté el graf i esgota la memòria.- Consell: quan una shape no quadri, aprofita el define-by-run: posa
print(x.shape)dins deforward(). Aquesta depuració immediata és un dels grans plaers de PyTorch.
Exercicis
Exercici 1: gradient a mà i amb autograd
Calcula amb autograd el gradient de L(w, b) = (3*w + b - 10)**2 a w=1.0, b=0.0 (el mateix de l'exercici 2 de 06-01). Comprova que obtens -42 i -14, idèntics a la versió GradientTape. Després executa 200 passos de descens amb lr 0.01 fent servir torch.no_grad() per actualitzar.
Exercici 2: la xarxa de ressenyes en nn.Module
TecnoMarket vol el classificador binari de ressenyes (positiva/negativa) com a mòdul PyTorch: entrada de 200 característiques (la ressenya ja vectoritzada, com a 04-03), capes ocultes de 64 i 32 amb ReLU, i sortida d'1 logit. Escriu la classe ClassificadorRessenyes(nn.Module) i indica quina funció de pèrdua faries servir i per què la sortida no porta sigmoide.
Exercici 3: llegir el bucle
Sense executar-lo, explica què fallaria (i amb quin símptoma) en aquest bucle:
for x, y in train_loader:
logits = model(x)
loss = loss_fn(logits, y)
loss.backward()
optimizer.step()Solucions
Solució 1:
import torch
w = torch.tensor(1.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)
loss = (3.0 * w + b - 10.0) ** 2
loss.backward()
print(w.grad.item(), b.grad.item()) # -42.0, -14.0 (idèntic a 06-01)
lr = 0.01
for _ in range(200):
loss = (3.0 * w + b - 10.0) ** 2
if w.grad is not None:
w.grad.zero_(); b.grad.zero_() # netejar abans de recalcular
loss.backward()
with torch.no_grad():
w -= lr * w.grad
b -= lr * b.grad
print(loss.item()) # ~0.0Mateix resultat que amb GradientTape: els dos autograds calculen la mateixa matemàtica de 02-03.
Solució 2:
from torch import nn
class ClassificadorRessenyes(nn.Module):
def __init__(self):
super().__init__()
self.capa1 = nn.Linear(200, 64)
self.capa2 = nn.Linear(64, 32)
self.sortida = nn.Linear(32, 1) # 1 logit per a classificació binària
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.capa1(x))
x = self.relu(self.capa2(x))
return self.sortida(x)Pèrdua: nn.BCEWithLogitsLoss. Igual que CrossEntropyLoss amb el softmax, aquesta pèrdua inclou la sigmoide internament (amb més estabilitat numèrica), així que la xarxa ha de retornar el logit cru. És l'equivalent de BinaryCrossentropy(from_logits=True) a Keras.
Solució 3: Hi falten tres coses:
optimizer.zero_grad(): els gradients s'acumulen pas a pas; símptoma: la pèrdua deixa de baixar o divergeix sense errors visibles.- Aplanament i dispositiu: si
xve de MNIST amb shape(32, 1, 28, 28)i el model espera(32, 784), hi haurà un error de shapes ann.Linear; i si el model és a la GPU i les dades no, error de dispositius. model.train()abans del bucle (rellevant si el model té dropout/BN).
L'ordre correcte del nucli és: forward → pèrdua → zero_grad() → backward() → step().
Conclusió
Ja parles els dos idiomes. PyTorch t'ha mostrat l'altre estil: tensors gairebé idèntics als de TensorFlow (amb moviment explícit .to(device) i pont directe amb numpy), autograd que resol el mateix gradient que GradientTape amb requires_grad + backward(), models com a classes nn.Module (el subclassing de 06-01 com a norma, no com a opció) i, sobretot, el bucle d'entrenament explícit: forward, pèrdua, zero_grad, backward, step — els cinc passos que Keras empaqueta a fit(). La xarxa MNIST de 02-05 reescrita en PyTorch assoleix la mateixa precisió: el framework no canvia la matemàtica, canvia qui escriu cada peça.
La pregunta inevitable és: quin fer servir? A la propera lliçó farem la comparació honesta —filosofia, depuració, ecosistemes, indústria vs. recerca— i veurem què triaria l'equip de TecnoMarket i per què, amb una conclusió tranquil·litzadora: els conceptes que has après viatgen amb tu, sigui quina sigui l'eina.
Curs de Deep Learning
Mòdul 1: Introducció al Deep Learning
- Què és el Deep Learning?
- Història i evolució del Deep Learning
- Aplicacions del Deep Learning
- Conceptes bàsics de xarxes neuronals
- Preparació de l'entorn de treball
Mòdul 2: Fonaments de Xarxes Neuronals
- Perceptró i Perceptró Multicapa
- Funció d'activació
- Propagació cap endavant i cap enrere
- Optimització i funció de pèrdua
- La teva primera xarxa neuronal completa
Mòdul 3: Xarxes Neuronals Convolucionals (CNN)
- Introducció a les CNN
- Capes convolucionals i de pooling
- Arquitectures populars de CNN
- Aplicacions de CNN en reconeixement d'imatges
Mòdul 4: Xarxes Neuronals Recurrents (RNN)
- Introducció a les RNN
- LSTM i GRU
- Aplicacions de RNN en processament del llenguatge natural
- Seqüències i sèries temporals
Mòdul 5: Tècniques Avançades en Deep Learning
- Xarxes Generatives Adversàries (GAN)
- Autoencoders
- Transfer Learning
- Regularització i tècniques de millora
- Mecanismes d'atenció i Transformers
Mòdul 6: Eines i Frameworks
- Introducció a TensorFlow
- Introducció a PyTorch
- Comparació de frameworks
- Entorns de desenvolupament i recursos addicionals
- Desar, carregar i desplegar models
Mòdul 7: Projectes Pràctics
- Classificació d'imatges amb CNN
- Generació de text amb RNN
- Detecció d'anomalies amb Autoencoders
- Creació d'una GAN per a generació d'imatges
- Fine-tuning d'un model preentrenat
