Módulo 5 · Redes neuronales
Tensores y autograd
En esta páginaEl problema
El problema
Un sensor entrega lecturas x y un instrumento de referencia mide el valor real y de las mismas muestras. Buscas un factor de calibración w para que w · x se acerque a y. Las tres mediciones llegan en un arreglo NumPy:
| muestra | lectura x | referencia y |
|---|---|---|
| M-1 | 1.0 | 2.0 |
| M-2 | 2.0 | 4.0 |
| M-3 | 3.0 | 7.0 |
Con w = 1, el error cuadrático medio es ((1 − 2)² + (2 − 4)² + (3 − 7)²) / 3 = 7. Para bajarlo necesitas saber si conviene aumentar o disminuir w y qué tan rápido cambia la pérdida alrededor de w = 1.
Atributos de un tensor
Un tensor de PyTorch es un arreglo de n dimensiones, igual que un ndarray. Cada tensor registra su forma (shape), su tipo (dtype) y el dispositivo donde viven sus valores (device).
Pregunta. Escribe la forma y el tipo de enteros, decimales y desde_numpy. Decide también en qué dispositivo queda decimales.
import numpy as np
import torch
mediciones = np.array([[1.0, 2.0], [2.0, 4.0], [3.0, 7.0]])
enteros = torch.tensor([1, 2, 3])
decimales = torch.tensor([1.0, 2.0, 3.0])
desde_numpy = torch.tensor(mediciones)
print(enteros.shape, enteros.dtype)
print(decimales.shape, decimales.dtype)
print(desde_numpy.shape, desde_numpy.dtype)
print(decimales.device)
La salida esperada está oculta.
Operaciones y broadcasting
Las operaciones elemento a elemento, las reducciones y el broadcasting siguen las mismas reglas que en NumPy. Las reducciones aceptan dim, el equivalente de axis. .float() convierte un tensor a float32.
Pregunta. Escribe la forma de w0, la de residuos y la de perdida0.
x = desde_numpy[:, 0].float()
y = desde_numpy[:, 1].float()
w0 = torch.tensor(1.0)
residuos = w0 * x - y
perdida0 = (residuos**2).mean()
print(w0.shape)
print(residuos, residuos.shape)
print(perdida0)
La salida esperada está oculta.
El broadcasting también permite evaluar varios valores de w a la vez. Una columna de forma (4, 1) contra x de forma (3,) produce una tabla de errores.
Pregunta. Escribe la forma de errores y la de perdidas. Decide cuál de los cuatro candidatos tiene la pérdida más baja.
candidatos = torch.tensor([[0.5], [1.0], [2.0], [2.5]])
errores = (candidatos * x - y) ** 2
perdidas = errores.mean(dim=1)
print(errores.shape)
print(perdidas.shape)
print(perdidas.round(decimals=3))
La salida esperada está oculta.
Gradiente con autograd
requires_grad=True le pide a PyTorch que registre cada operación que usa ese tensor. backward() sobre un resultado escalar recorre ese registro. Después, w.grad contiene la derivada del resultado respecto a w, evaluada en el valor actual de w.
Pregunta. Con w = 1, ¿w.grad será positivo o negativo? Piensa qué pasa con la pérdida si w aumenta un poco.
w = torch.tensor(1.0, requires_grad=True)
perdida = ((w * x - y) ** 2).mean()
perdida.backward()
print(type(perdida.grad_fn).__name__)
print(round(perdida.item(), 3))
print(round(w.grad.item(), 3))
La salida esperada está oculta.
Una pendiente también se puede aproximar sin autograd. Se evalúa la pérdida un poco a la derecha y un poco a la izquierda de w = 1 y se divide la diferencia entre la distancia.
Pregunta. ¿El cociente coincidirá con w.grad hasta el segundo decimal?
def perdida_en(valor):
return ((valor * x - y) ** 2).mean().item()
h = 0.01
pendiente = (perdida_en(1 + h) - perdida_en(1 - h)) / (2 * h)
print(round(pendiente, 2))
La salida esperada está oculta.
En este punto, w.grad todavía guarda el −11.333 del primer backward().
Pregunta. Se calcula la misma pérdida otra vez y se llama backward() sin limpiar w.grad. Escribe el primer valor impreso. Después, escribe el valor tras poner w.grad = None y repetir.
perdida = ((w * x - y) ** 2).mean()
perdida.backward()
print(round(w.grad.item(), 3))
w.grad = None
perdida = ((w * x - y) ** 2).mean()
perdida.backward()
print(round(w.grad.item(), 3))
La salida esperada está oculta.
Cálculos sin registro
Mientras w tenga requires_grad=True, cada operación que lo usa queda registrada. Cambiar el propio w también sería una operación registrada.
Pregunta. ¿Qué pasa si se intenta cambiar w directamente con w -= 0.1 * w.grad?
try:
w -= 0.1 * w.grad
except RuntimeError as error:
print(error)
La salida esperada está oculta.
Dentro de with torch.no_grad(): las operaciones no se registran. Ahí se puede cambiar w y calcular valores que no se van a derivar. El factor 0.1 fija el tamaño del cambio.
Pregunta. ¿w sube o baja? ¿La pérdida nueva será menor que 7? ¿Cuál de prediccion y nueva_perdida tendrá requires_grad en True?
with torch.no_grad():
w -= 0.1 * w.grad
prediccion = w * torch.tensor(4.0)
nueva_perdida = ((w * x - y) ** 2).mean()
print(round(w.item(), 3))
print(round(nueva_perdida.item(), 3))
print(prediccion.requires_grad, nueva_perdida.requires_grad)
La salida esperada está oculta.
Conversión con NumPy
Hay dos formas de pasar un arreglo NumPy a PyTorch: torch.tensor y torch.from_numpy.
Pregunta. Después de cambiar mediciones[0, 1] a 2.5, ¿qué valor muestra cada tensor en la posición [0, 1]? ¿Qué tipo tiene compartido?
compartido = torch.from_numpy(mediciones)
copiado = torch.tensor(mediciones)
mediciones[0, 1] = 2.5
print(compartido[0, 1].item())
print(copiado[0, 1].item())
print(compartido.dtype)
La salida esperada está oculta.
El camino inverso es .numpy(). Para un tensor en CPU, el arreglo resultante también comparte memoria con el tensor.
Pregunta. ¿Funciona w.numpy() si w tiene requires_grad=True?
try:
w.numpy()
except RuntimeError as error:
print(error)
w_np = w.detach().numpy()
print(type(w_np).__name__, w_np.dtype, round(float(w_np), 3))
La salida esperada está oculta.
Práctica
Traza guiada
Antes de procesar, escribe la forma y el tipo de a, el valor y la forma de s, y el valor de c.grad.
a = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
b = torch.tensor([10.0, 20.0, 30.0])
c = torch.tensor(2.0, requires_grad=True)
s = (a + b).sum(dim=0)
f = c**3
f.backward()
Comprueba con print. c.grad debe coincidir con la derivada de c³ evaluada en c = 2. Si s no tiene la forma que predijiste, revisa qué eje elimina dim=0.
Valor inicial distinto
Repite el cálculo de «Gradiente con autograd» con w = 3.0 y los mismos x y y. Antes de procesar, predice el signo de w.grad. Después, cambia w dentro de torch.no_grad() con el factor 0.1 y comprueba con assert que la pérdida nueva es menor que la anterior. Si w.grad y el cambio de w tienen el mismo signo, revisa el signo de la resta.
Pérdida propia
Construye un arreglo NumPy de cinco pares (x, y) con np.random.default_rng(0). Escribe un cálculo que:
- convierta las columnas con
torch.from_numpyy registre sudtype - cree
wconrequires_grad=Truey el mismodtypeque los datos - calcule el error cuadrático medio y su gradiente con
backward() - compare
w.gradcon la pendiente aproximada por diferencias mediante unassertcon tolerancia1e-3 - devuelva
wa NumPy condetach()
Reto opcional
Crea y con requires_grad=True en lugar de w, y deja w como un número fijo igual a 1. Antes de llamar a backward(), predice la forma de y.grad y el signo de cada uno de sus tres valores.
Errores que revisar
- Si
requires_grad=TruelanzaOnly Tensors of floating point and complex dtype can require gradients, el tensor es entero. Créalo con decimales o condtype=torch.float32. - Si
backward()lanzagrad can be implicitly created only for scalar outputs, el resultado tiene más de un valor. Redúcelo con.mean()o.sum()antes. - Si
w.grades el doble o el triple de lo esperado, se acumularon variosbackward(). Ponw.grad = Noneantes de cada cálculo nuevo. - Si un producto de matrices lanza
expected m1 and m2 to have the same dtype, un tensor esfloat32y el otrofloat64. Revisa los que vienen de NumPy y convierte con.float(). - Si un tensor cambia sin que lo hayas tocado, revisa si salió de
torch.from_numpyo de.numpy(). Ambos comparten memoria con el arreglo original.
Cierre
Explica qué número guarda w.grad después de perdida.backward() y por qué el cambio de w se escribe dentro de torch.no_grad().
Lista de comprobación
- ¿Qué
dtypeproducetorch.tensorcon decimales de Python y cuál con un arreglo NumPyfloat64? - ¿Qué indica el signo de
w.gradsobre la pérdida siwaumenta un poco? - ¿Por qué un segundo
backward()sin limpiarw.gradcambia el valor guardado? - ¿Qué diferencia hay entre
torch.tensorytorch.from_numpycuando el arreglo original cambia?
Documentación oficial
- Tensors: creación, atributos, operaciones y puente con NumPy.
- Automatic differentiation with torch.autograd:
requires_grad,backward(),.grady desactivación del registro. - Tensor attributes: tipos
dtypey dispositivosdevice. - torch.no_grad: contexto que desactiva el cálculo de gradientes.
- torch.from_numpy: tensor que comparte memoria con un arreglo NumPy.