Módulo 5 · Redes neuronales

Lección 2 de 7

Perceptrón y backpropagation

En esta páginaEl problema

El problema

Un foco de escalera tiene dos interruptores, A y B, uno al pie y otro en lo alto de la escalera. Cada interruptor está en posición 0 o 1. El foco enciende cuando exactamente un interruptor está en 1. Esta regla se llama XOR (o exclusivo):

interruptor Ainterruptor Bfoco
000
011
101
110

Quieres una unidad de cálculo que reciba (A, B) y devuelva el estado del foco. La unidad más simple multiplica cada entrada por un peso, suma y compara el resultado con un umbral. Si esa unidad no basta, necesitas saber qué añadir y cómo calcular el gradiente de la pérdida respecto a cada peso cuando hay más de una capa.

Perceptrón

Un perceptrón recibe entradas x₁, …, xₙ, calcula la suma ponderada w₁·x₁ + … + wₙ·xₙ y la compara con un umbral θ. Devuelve 1 si la suma supera el umbral y 0 en otro caso. Con b = −θ, la regla se escribe w · x + b > 0. El número b se llama sesgo.

Con las cuatro combinaciones en las filas de X, el producto X @ w calcula las cuatro sumas ponderadas a la vez.

Pregunta. Con pesos (1, 1) y umbral 0.5, escribe los cuatro valores de z, las cuatro salidas y cuántas coinciden con la columna foco.

Python 3 · ejemplo reproducible
import torch

X = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
foco = torch.tensor([0.0, 1.0, 1.0, 0.0])

w = torch.tensor([1.0, 1.0])
b = torch.tensor(-0.5)

z = X @ w + b
salida = (z > 0).float()

print(z)
print(salida)
print(int((salida == foco).sum()))
Salida esperada

La salida esperada está oculta.

XOR y separación lineal

La frontera w₁·A + w₂·B + b = 0 es una recta en el plano de las entradas. Un perceptrón devuelve 1 de un lado de la recta y 0 del otro. Cambiar los pesos y el sesgo mueve esa recta.

Una búsqueda ensaya 10 000 perceptrones con pesos y sesgos al azar, guardados en pesos_azar (un par por fila) y sesgos_azar. Z = X @ pesos_azar.T + sesgos_azar los evalúa sobre las cuatro filas de una sola vez, y cada perceptrón se califica por cuántas filas acierta.

Pregunta. Escribe la forma de Z. Decide cuántos aciertos logra el mejor de los 10 000 perceptrones.

Ver resultado

Z tiene forma (4, 10000): una fila por combinación de entradas y una columna por perceptrón. Cada perceptrón se compara con las cuatro filas de XOR y se cuenta cuántas acierta, de 0 a 4. La tabla agrupa los 10 000 perceptrones según ese número. Por ejemplo, la fila con 2 y 7507 dice que 7507 perceptrones aciertan exactamente dos de las cuatro filas.

filas acertadasperceptrones
00
11290
27507
31203
40
Lectura. El mejor resultado es tres de cuatro: 1203 perceptrones lo logran, como el de pesos (1, 1) de la sección anterior. Ninguno acierta las cuatro filas y ninguno las falla todas. Los dos hechos van juntos: si un perceptrón fallara las cuatro, cambiar el signo de sus pesos y de su sesgo invertiría cada respuesta y daría uno que las acierta todas. La mayoría acierta dos, igual que una regla que siempre respondiera 0: XOR tiene dos filas con cada valor.

Una búsqueda al azar es evidencia, no prueba. La prueba sale de las cuatro condiciones que tendría que cumplir un perceptrón:

  • (0, 0) → 0 exige b ≤ 0
  • (0, 1) → 1 exige w₂ + b > 0
  • (1, 0) → 1 exige w₁ + b > 0
  • (1, 1) → 0 exige w₁ + w₂ + b ≤ 0

Sumando la segunda y la tercera, w₁ + w₂ + b > −b. Por la primera, −b ≥ 0, así que w₁ + w₂ + b > 0. Eso contradice la cuarta. Ninguna recta deja (0, 1) y (1, 0) de un lado y (0, 0) y (1, 1) del otro: XOR no es linealmente separable.

Red de dos capas

Una capa son varios perceptrones que leen las mismas entradas. Los pesos de cada unidad forman una columna de W1, y b1 guarda un sesgo por unidad. Así, X @ W1 + b1 tiene una fila por combinación y una columna por unidad. La segunda capa recibe las salidas de la primera en lugar de X. La primera capa queda entre la entrada y la salida, y se llama capa oculta.

La primera unidad oculta usa pesos (1, 1) y umbral 0.5. La segunda usa los mismos pesos y umbral 1.5.

Pregunta. Escribe la forma de H y sus cuatro filas. Decide qué regla calcula cada columna.

Python 3 · ejemplo reproducible
def escalon(z):
    return (z > 0).float()


W1 = torch.tensor([[1.0, 1.0], [1.0, 1.0]])
b1 = torch.tensor([-0.5, -1.5])

H = escalon(X @ W1 + b1)

print(H.shape)
print(H)
Salida esperada

La salida esperada está oculta.

La unidad de salida recibe H con pesos (1, −1) y umbral 0.5: suma el valor de O y resta el de Y.

Pregunta. Escribe las cuatro salidas de la red. ¿Coinciden con la columna foco?

Python 3 · ejemplo reproducible
W2 = torch.tensor([1.0, -1.0])
b2 = torch.tensor(-0.5)

salida_red = escalon(H @ W2 + b2)

print(salida_red)
print(torch.equal(salida_red, foco))
Salida esperada

La salida esperada está oculta.

Retropropagación

Los pesos de la red XOR se eligieron a mano. Para aprenderlos de los datos hace falta el gradiente de la pérdida respecto a cada peso. backward() necesita que todas las operaciones entre los pesos y la pérdida estén registradas.

Pregunta. w_prueba tiene requires_grad=True. ¿La salida del escalón conserva el registro?

Python 3 · ejemplo reproducible
w_prueba = torch.tensor([1.0, 1.0], requires_grad=True)
salida_prueba = escalon(X @ w_prueba - 0.5)

print(salida_prueba.requires_grad)
Salida esperada

La salida esperada está oculta.

Para seguir el recorrido del gradiente basta una cadena de dos capas con un peso cada una y sin escalón:

  • capa oculta: h = w₁ · x
  • capa de salida: ŷ = w₂ · h
  • pérdida: L = (ŷ − y)²

En el código, ŷ se escribe y_hat y L se guarda en perdida.

Pregunta. Con x = 1.5, y = 3, w₁ = 0.5 y w₂ = 2, calcula h, ŷ y L.

Python 3 · ejemplo reproducible
x = torch.tensor(1.5)
y = torch.tensor(3.0)
w1 = torch.tensor(0.5)
w2 = torch.tensor(2.0)

h = w1 * x
y_hat = w2 * h
perdida = (y_hat - y) ** 2

print(h.item(), y_hat.item(), perdida.item())
Salida esperada

La salida esperada está oculta.

L depende de w₁ a través de h y de ŷ. La regla de la cadena multiplica las derivadas de cada paso: ∂L/∂w₁ = ∂L/∂ŷ · ∂ŷ/∂h · ∂h/∂w₁. La lección Derivadas y gradientes desarrolla la regla de la cadena con más detalle. La retropropagación (backpropagation) aplica esa regla de la salida hacia la entrada, capa por capa:

  1. derivada de la pérdida: ∂L/∂ŷ = 2(ŷ − y)
  2. capa de salida: ∂L/∂w₂ = ∂L/∂ŷ · h, y pasa hacia atrás ∂L/∂h = ∂L/∂ŷ · w₂
  3. capa oculta: ∂L/∂w₁ = ∂L/∂h · x

Pregunta. Calcula los cuatro valores. ¿Cuál de los dos pesos tiene el gradiente de mayor magnitud?

Python 3 · ejemplo reproducible
d_y_hat = 2 * (y_hat - y)
d_w2 = d_y_hat * h
d_h = d_y_hat * w2
d_w1 = d_h * x

print(d_y_hat.item(), d_w2.item(), d_h.item(), d_w1.item())
Salida esperada

La salida esperada está oculta.

Gradientes con autograd

Ahora w1 y w2 se crean con requires_grad=True, se repite la pasada hacia adelante y se llama perdida.backward().

Pregunta. Escribe los valores de w1.grad y w2.grad. ¿Coincidirán con d_w1 y d_w2?

Python 3 · ejemplo reproducible
w1 = torch.tensor(0.5, requires_grad=True)
w2 = torch.tensor(2.0, requires_grad=True)

h = w1 * x
y_hat = w2 * h
perdida = (y_hat - y) ** 2
perdida.backward()

print(w1.grad.item(), w2.grad.item())
print(torch.isclose(w1.grad, d_w1).item(), torch.isclose(w2.grad, d_w2).item())
Salida esperada

La salida esperada está oculta.

Durante ese recorrido autograd también calcula ∂L/∂h, pero no lo guarda: h no es una hoja, porque lo produjo una operación. h.retain_grad(), llamado antes de backward(), pide conservarlo en h.grad.

Pregunta. Después de limpiar los gradientes y repetir el cálculo, ¿qué valor tendrá h.grad?

Python 3 · ejemplo reproducible
w1.grad = None
w2.grad = None

h = w1 * x
h.retain_grad()
perdida = (w2 * h - y) ** 2
perdida.backward()

print(h.grad.item(), d_h.item())
Salida esperada

La salida esperada está oculta.

Práctica

Traza guiada

Antes de procesar, escribe los cuatro valores de z_traza y las cuatro salidas. Decide qué regla lógica calcula este perceptrón.

Python 3 · ejemplo reproducible
w_traza = torch.tensor([2.0, -1.0])
b_traza = torch.tensor(-0.5)

z_traza = X @ w_traza + b_traza
salida_traza = escalon(z_traza)

Comprueba con print. Si una salida no coincide, revisa en qué fila la suma z_traza cambia de signo.

Salida XNOR

XNOR enciende el foco cuando los dos interruptores están en la misma posición. Conserva W1, b1 y H de la red XOR y cambia solo W2 y b2. Antes de procesar, escribe los nuevos pesos y el umbral y justifícalos con las tres filas distintas de H. Comprueba con assert torch.equal(…, 1 - foco). Si falla, revisa qué punto de (h₁, h₂) debe quedar del lado del 0.

Cadena de tres capas

Extiende la cadena de «Retropropagación» con una tercera capa: h₁ = w₁ · x, h₂ = w₂ · h₁, ŷ = w₃ · h₂ y L = (ŷ − y)². Usa x = 1.0, y = 2.0, w₁ = 0.5, w₂ = 1.5 y w₃ = 2.0. Escribe un cálculo que:

  1. haga la pasada hacia adelante y guarde h₁, h₂ y ŷ
  2. calcule a mano, de la salida hacia la entrada, los gradientes de w₃, w₂ y w₁
  3. repita el cálculo con requires_grad=True y backward()
  4. compruebe cada par con assert torch.isclose(…)

Antes de procesar, predice cuál de los tres gradientes tiene mayor magnitud.

Reto opcional

Construye otra red de dos capas con escalón para XOR. La primera unidad oculta calcula la regla O; la segunda calcula NO-Y, que vale 0 solo cuando los dos interruptores están en 1. La unidad de salida calcula la regla Y sobre H. Elige W1, b1, W2 y b2, escribe antes de procesar las cuatro filas de la nueva H y comprueba con assert torch.equal(…, foco). Si falla, revisa en qué fila de H la unidad NO-Y no da lo que predijiste.

Errores que revisar

  • Si X @ W1 lanza mat1 and mat2 shapes cannot be multiplied (4x2 and 3x2), W1 no tiene una fila por entrada. Las filas de W1 deben coincidir con las columnas de X, y sus columnas son las unidades de la capa.
  • Si H @ W2 lanza "addmv_impl_cpu" not implemented for 'Bool', H quedó como booleanos. Convierte la comparación con .float(), como hace escalon.
  • Si backward() lanza element 0 of tensors does not require grad and does not have a grad_fn, la pérdida pasó por una comparación como z > 0. Esa operación corta el registro.
  • Si tu gradiente de w₁ a mano no coincide con autograd, revisa ∂L/∂h. La capa de salida pasa hacia atrás ∂L/∂ŷ · w₂, no ∂L/∂ŷ · h.
  • Si h.grad es None, h no es una hoja. Llama h.retain_grad() antes de backward().

Cierre

Explica por qué la capa oculta permite que una sola unidad de salida reproduzca XOR y qué valores de la pasada hacia adelante usa el gradiente de w₁.

Lista de comprobación

  • ¿Qué cuatro condiciones tendría que cumplir un perceptrón para reproducir XOR y por qué se contradicen?
  • ¿Qué representa cada columna de H y por qué su forma es (4, 2)?
  • ¿Por qué el gradiente de w₂ se calcula antes que el de w₁?
  • ¿Qué derivada pasa la capa de salida hacia la capa oculta y por qué número se multiplica ahí?
  • ¿Por qué backward() no puede atravesar z > 0?

Documentación oficial

Cargando progreso…