Módulo 5 · Redes neuronales
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 A | interruptor B | foco |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
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.
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()))
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 acertadas | perceptrones |
|---|---|
| 0 | 0 |
| 1 | 1290 |
| 2 | 7507 |
| 3 | 1203 |
| 4 | 0 |
(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 exigeb ≤ 0(0, 1)→ 1 exigew₂ + b > 0(1, 0)→ 1 exigew₁ + b > 0(1, 1)→ 0 exigew₁ + 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.
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)
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?
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))
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?
w_prueba = torch.tensor([1.0, 1.0], requires_grad=True)
salida_prueba = escalon(X @ w_prueba - 0.5)
print(salida_prueba.requires_grad)
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.
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())
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:
- derivada de la pérdida:
∂L/∂ŷ = 2(ŷ − y) - capa de salida:
∂L/∂w₂ = ∂L/∂ŷ · h, y pasa hacia atrás∂L/∂h = ∂L/∂ŷ · w₂ - capa oculta:
∂L/∂w₁ = ∂L/∂h · x
Pregunta. Calcula los cuatro valores. ¿Cuál de los dos pesos tiene el gradiente de mayor magnitud?
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())
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?
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())
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?
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())
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.
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:
- haga la pasada hacia adelante y guarde
h₁,h₂yŷ - calcule a mano, de la salida hacia la entrada, los gradientes de
w₃,w₂yw₁ - repita el cálculo con
requires_grad=Trueybackward() - 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 @ W1lanzamat1 and mat2 shapes cannot be multiplied (4x2 and 3x2),W1no tiene una fila por entrada. Las filas deW1deben coincidir con las columnas deX, y sus columnas son las unidades de la capa. - Si
H @ W2lanza"addmv_impl_cpu" not implemented for 'Bool',Hquedó como booleanos. Convierte la comparación con.float(), como haceescalon. - Si
backward()lanzaelement 0 of tensors does not require grad and does not have a grad_fn, la pérdida pasó por una comparación comoz > 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.gradesNone,hno es una hoja. Llamah.retain_grad()antes debackward().
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
Hy por qué su forma es(4, 2)? - ¿Por qué el gradiente de
w₂se calcula antes que el dew₁? - ¿Qué derivada pasa la capa de salida hacia la capa oculta y por qué número se multiplica ahí?
- ¿Por qué
backward()no puede atravesarz > 0?
Documentación oficial
- Automatic differentiation with torch.autograd: grafo de cómputo,
backward()y gradientes de parámetros. - A Gentle Introduction to torch.autograd: retropropagación en una red neuronal y regla de la cadena en autograd.
- torch.Tensor.retain_grad: conservar el gradiente de un tensor que no es hoja.
- torch.matmul: producto de matrices y reglas de forma.
- Neural network models (supervised): perceptrón multicapa, capas ocultas y retropropagación.