Módulo 5 · Redes neuronales

Lección 1 de 7

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:

muestralectura xreferencia y
M-11.02.0
M-22.04.0
M-33.07.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.

Python 3 · ejemplo reproducible
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)
Salida esperada

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.

Python 3 · ejemplo reproducible
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)
Salida esperada

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.

Python 3 · ejemplo reproducible
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))
Salida esperada

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.

Python 3 · ejemplo reproducible
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))
Salida esperada

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?

Python 3 · ejemplo reproducible
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))
Salida esperada

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.

Python 3 · ejemplo reproducible
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))
Salida esperada

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?

Python 3 · ejemplo reproducible
try:
    w -= 0.1 * w.grad
except RuntimeError as error:
    print(error)
Salida esperada

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?

Python 3 · ejemplo reproducible
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)
Salida esperada

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?

Python 3 · ejemplo reproducible
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)
Salida esperada

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?

Python 3 · ejemplo reproducible
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))
Salida esperada

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.

Python 3 · ejemplo reproducible
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:

  1. convierta las columnas con torch.from_numpy y registre su dtype
  2. cree w con requires_grad=True y el mismo dtype que los datos
  3. calcule el error cuadrático medio y su gradiente con backward()
  4. compare w.grad con la pendiente aproximada por diferencias mediante un assert con tolerancia 1e-3
  5. devuelva w a NumPy con detach()

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=True lanza Only Tensors of floating point and complex dtype can require gradients, el tensor es entero. Créalo con decimales o con dtype=torch.float32.
  • Si backward() lanza grad 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.grad es el doble o el triple de lo esperado, se acumularon varios backward(). Pon w.grad = None antes de cada cálculo nuevo.
  • Si un producto de matrices lanza expected m1 and m2 to have the same dtype, un tensor es float32 y el otro float64. 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_numpy o 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é dtype produce torch.tensor con decimales de Python y cuál con un arreglo NumPy float64?
  • ¿Qué indica el signo de w.grad sobre la pérdida si w aumenta un poco?
  • ¿Por qué un segundo backward() sin limpiar w.grad cambia el valor guardado?
  • ¿Qué diferencia hay entre torch.tensor y torch.from_numpy cuando el arreglo original cambia?

Documentación oficial

Cargando progreso…