Tensors de PyTorch
PyTorch guarda les dades i els pesos de les xarxes en tensors. Un tensor és un array com els de NumPy: una graella de nombres del mateix tipus, amb una forma, que s’indexa igual i amb què s’opera sense bucles. Té dues diferències que importen:
- les xarxes treballen amb un tipus concret,
float32, i PyTorch no barreja tipus; - un tensor pot seguir les operacions que s’hi fan, i així PyTorch en calcula les derivades per entrenar.
import numpy as np
import torch
from torch import nn
Crear tensors
Des d’una llista o d’un array
torch.tensor fa el mateix que np.array. shape, ndim i dtype també hi són:
punts = [[0.5, 1.0], [2.0, -1.0], [0.0, 3.0], [1.5, 0.5]] # 4 punts, 2 coordenades
X_t = torch.tensor(punts)
print(X_t)
print('shape:', X_t.shape, '| ndim:', X_t.ndim, '| dtype:', X_t.dtype)
tensor([[ 0.5000, 1.0000],
[ 2.0000, -1.0000],
[ 0.0000, 3.0000],
[ 1.5000, 0.5000]])
shape: torch.Size([4, 2]) | ndim: 2 | dtype: torch.float32
Les dades solen arribar en un array de NumPy, que torch.tensor també accepta. dtype en tria
el tipus, i .numpy() fa el camí invers. Quan tens les mateixes dades en tots dos formats, el
tensor porta el nom de l’array amb _t al final (y i y_t): així el nom diu sempre amb quina
biblioteca treballes.
y = np.array([1, 0, 1, 0])
y_t = torch.tensor(y, dtype=torch.float32)
print(y.dtype, '→', y_t.dtype)
print(y_t.numpy()) # i de tornada a NumPy
int64 → torch.float32
[1. 0. 1. 0.]
Sempre float32
Els pesos d’una capa de PyTorch són float32, i NumPy crea els decimals en float64. Si no
dius el tipus en convertir, la capa no accepta les dades:
X = np.array(punts) # NumPy: float64 per defecte
neurona = nn.Linear(2, 1) # els pesos de PyTorch: float32
neurona(torch.tensor(X)) # float64 contra float32
RuntimeError: mat1 and mat2 must have the same dtype, but got Double and Float
Double és float64 i Float és float32. Per això les dades d’una xarxa es converteixen
sempre amb dtype=torch.float32, etiquetes incloses:
X_t = torch.tensor(X, dtype=torch.float32)
print(X_t.dtype)
torch.float32
Operar amb tensors
Com a NumPy
Les operacions element a element, la indexació i les agregacions funcionen com a NumPy. A les
agregacions, l’eix es diu dim en lloc d’axis:
print(X_t * 2) # element a element
print(X_t[:, 0]) # la columna 0: totes les files
print(X_t.mean(dim=0)) # la mitjana de cada columna: dim fa el paper d'axis
tensor([[ 1., 2.],
[ 4., -2.],
[ 0., 6.],
[ 3., 1.]])
tensor([0.5000, 2.0000, 0.0000, 1.5000])
tensor([1.0000, 0.8750])
El producte de matrius, @
@ és el producte matricial, com a NumPy. Amb una taula de
punts i un vector de pesos, és la suma ponderada d’una neurona,
\(z = w_1 x_1 + w_2 x_2 + b\), per a tots els punts alhora:
w = torch.tensor([2.0, -1.0]) # un pes per coordenada
b = 0.5
z = X_t @ w + b # la suma ponderada de la neurona, per als 4 punts alhora
print(z)
print(torch.sigmoid(z)) # les probabilitats
tensor([ 0.5000, 5.5000, -2.5000, 3.0000])
tensor([0.6225, 0.9959, 0.0759, 0.9526])
El primer punt, (0.5, 1.0): 2 · 0.5 - 1 · 1.0 + 0.5 = 0.5. Una capa nn.Linear fa exactament
aquest càlcul, amb els seus pesos.
La forma
Etiquetes en columna
Una xarxa sempre rep i retorna lots: la sortida té una fila per mostra, en una columna.
Les etiquetes, en canvi, solen arribar en fila. .reshape(-1, 1) les posa en columna, com a
NumPy, i .squeeze() fa el contrari: treu
els eixos de mida 1.
print(y_t.shape) # una etiqueta per punt
print(y_t.reshape(-1, 1).shape) # en columna: 4 files i 1 columna
torch.manual_seed(0)
neurona = nn.Linear(2, 1)
sortida = neurona(X_t)
print(sortida.shape) # la sortida d'una xarxa: sempre en columna
print(sortida.squeeze().shape) # squeeze treu els eixos de mida 1
torch.Size([4])
torch.Size([4, 1])
torch.Size([4, 1])
torch.Size([4])
La funció de pèrdua compara la sortida i les etiquetes posició a posició, i vol la mateixa forma:
loss_fn = nn.BCEWithLogitsLoss()
loss_fn(sortida, y_t) # etiquetes en fila, sortida en columna
ValueError: Target size (torch.Size([4])) must be the same as input size (torch.Size([4, 1]))
y_col = y_t.reshape(-1, 1)
print(loss_fn(sortida, y_col))
tensor(0.5220, grad_fn=<BinaryCrossEntropyWithLogitsBackward0>)
Comparar i comptar
De booleans a números
Com a NumPy, una comparació dona una màscara de True i
False. Però PyTorch no en fa la mitjana:
positius = z > 0
print(positius)
positius.mean()
tensor([ True, True, False, True])
RuntimeError: mean(): could not infer output dtype. Input dtype must be either a floating point or complex dtype. Got: Bool
.float() converteix True en 1 i False en 0, com .astype(int) a NumPy. La mitjana d’uns
i zeros és la proporció de True:
print(positius.float()) # True → 1, False → 0
print(positius.float().mean()) # la proporció de True: encara un tensor
print(positius.float().mean().item()) # el número de Python
tensor([1., 1., 0., 1.])
tensor(0.7500)
0.75
El número, fora del tensor: item
Una agregació de PyTorch retorna un tensor d’un sol element, no un número. .item() en treu el
número de Python: el que vols imprimir amb format, desar en una llista o comparar.
Gradients
Seguir les operacions: requires_grad
Un tensor creat amb requires_grad=True registra totes les operacions que s’hi fan. Quan
calcules una pèrdua a partir d’ell, .backward() en calcula la derivada respecte de cada
element, i la deixa a .grad. És la diferenciació automàtica
que entrena les xarxes:
w = torch.tensor([2.0, -1.0], requires_grad=True) # PyTorch en seguirà les operacions
z = X_t @ w + 0.5
perdua = loss_fn(z, y_t)
perdua.backward() # calcula la derivada de la pèrdua respecte de w
print(w.grad)
tensor([ 0.8080, -0.9174])
w.grad diu cap on creix la pèrdua si mous cada pes: el descens del gradient mou els pesos en
sentit contrari. Els pesos d’una capa ja porten requires_grad activat:
print(neurona.weight.requires_grad) # els pesos d'una capa ja el porten activat
True
Els gradients s’acumulen: cada .backward() suma els nous als que ja hi havia. Per això el
bucle d’entrenament els esborra amb optimizer.zero_grad() abans de cada pas.
Calcular sense seguir: no_grad
Per avaluar o predir no cal cap derivada. torch.no_grad() és un
gestor de context: el bloc del with s’executa sense
seguir les operacions, i en sortir-ne tot torna a ser com abans:
with torch.no_grad():
z = X_t @ w + 0.5 # dins del bloc, PyTorch no segueix res
print(z.requires_grad)
False
Estalvia memòria i temps, i a les xarxes va sempre amb model.eval()
(§Modes d’entrenament i avaluació).