Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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ó).

Last change: , commit: a9a57eb