Llevamos dos lecciones aplazando la gran pregunta: ¿cómo aprende sus pesos una red con capas ocultas? La regla del perceptrón no sirve (nadie le dice a una neurona oculta cuál era su salida "correcta") y en la lección anterior preparamos el ingrediente que faltaba: activaciones con derivada útil. En esta lección cerramos el círculo. Primero formalizaremos el forward pass (propagación hacia adelante) como una cadena de multiplicaciones matriciales, retomando nuestra red 3-4-2-1 de 29 parámetros. Después construiremos, de forma accesible, la noción de gradiente y la regla de la cadena, y con ellas entenderemos backpropagation: el algoritmo de 1986 que reparte la culpa del error entre todas las capas. Lo trazaremos a mano con un ejemplo numérico pequeño, veremos por qué de esta mecánica nacen los problemas de vanishing y exploding gradients, y terminaremos entrenando en numpy, desde cero, una red de 2 capas sobre datos ficticios de TecnoMarket.

Contenido

  1. Forward pass con matrices: la red 3-4-2-1 en acción
  2. El gradiente: la brújula del error
  3. La regla de la cadena, contada sin dolor
  4. Backpropagation: un ejemplo numérico trazado a mano
  5. Vanishing y exploding gradients
  6. Implementación completa en numpy: red de 2 capas para TecnoMarket

Forward pass con matrices: la red 3-4-2-1 en acción

El forward pass es el recorrido de los datos desde la entrada hasta la salida. Ya lo hicimos a pequeña escala con el MLP de XOR; ahora lo escribimos en general. Para una capa con matriz de pesos $W$ (una columna por neurona), sesgos $b$ y activación $f$:

$$z = xW + b \qquad a = f(z)$$

donde $z$ es la pre-activación (la suma ponderada de cada neurona) y $a$ la activación (lo que sale hacia la capa siguiente). Una red entera es simplemente esta operación repetida capa a capa. Recuperemos la red 3-4-2-1 que contamos a mano en el módulo 1 (29 parámetros: 16 + 10 + 3) y démosle vida:

import numpy as np

rng = np.random.default_rng(0)

# Pesos aleatorios pequeños (aún sin entrenar), con las formas de la red 3-4-2-1
W1, b1 = rng.normal(0, 0.5, (3, 4)), np.zeros(4)   # entrada(3) -> oculta1(4)
W2, b2 = rng.normal(0, 0.5, (4, 2)), np.zeros(2)   # oculta1(4) -> oculta2(2)
W3, b3 = rng.normal(0, 0.5, (2, 1)), np.zeros(1)   # oculta2(2) -> salida(1)

def relu(z):     return np.maximum(0, z)
def sigmoide(z): return 1 / (1 + np.exp(-z))

# Un pedido de TecnoMarket: [importe_norm, antiguedad_norm, discrepancia_direcciones]
x = np.array([[0.9, 0.1, 1.0]])          # forma (1, 3): 1 ejemplo, 3 características

a1 = relu(x @ W1 + b1)                   # forma (1, 4)
a2 = relu(a1 @ W2 + b2)                  # forma (1, 2)
y_hat = sigmoide(a2 @ W3 + b3)           # forma (1, 1): probabilidad de fraude
print(y_hat)                             # p. ej. [[0.55]] -> aún no sabe nada

Tres observaciones clave:

  • Las formas encajan como fichas de dominó: (1,3)·(3,4) → (1,4); (1,4)·(4,2) → (1,2); (1,2)·(2,1) → (1,1). Verificar .shape en cada paso es el mejor detector de errores.
  • Procesar un batch es gratis: si x tiene forma (32, 3) —32 pedidos a la vez—, el mismo código devuelve (32, 1) sin cambiar una línea. Esta es la razón práctica de organizar los datos en batches (vocabulario de 01-04) y de que las GPU brillen aquí.
  • La salida con pesos aleatorios ronda 0.5: la red aún es una moneda al aire. Entrenar consiste en mover esos 29 parámetros hasta que las salidas coincidan con las etiquetas.
graph LR
    X[x - 1x3] -->|"W1 (3x4)"| A1[a1 - 1x4]
    A1 -->|"W2 (4x2)"| A2[a2 - 1x2]
    A2 -->|"W3 (2x1)"| Y[y_hat - 1x1]

El gradiente: la brújula del error

Para entrenar necesitamos medir el error con un número. Usaremos por ahora el error cuadrático $L = (\hat{y} - y)^2$ (las funciones de pérdida en detalle son el tema de la próxima lección; con esta nos basta).

La pregunta central del aprendizaje es: si muevo un peso concreto $w$ un poquito, ¿el error sube o baja, y cuánto? La respuesta es la derivada parcial $\frac{\partial L}{\partial w}$. El vector con todas esas derivadas —una por parámetro— es el gradiente, y apunta en la dirección en que el error crece más deprisa. Por tanto, para reducir el error caminamos en dirección contraria:

$$w \leftarrow w - \eta \frac{\partial L}{\partial w}$$

Esto es el descenso de gradiente, y es la versión matemática exacta de la analogía del grifo de la ducha: la derivada dice hacia dónde girar (¿está saliendo demasiado caliente o demasiado fría?) y $\eta$ (la tasa de aprendizaje) cuánto girar cada vez. Compárala con la regla del perceptrón de 02-01: aquella era un caso particular rígido; esta funciona para cualquier red derivable.

El problema práctico: nuestra pequeña red 3-4-2-1 tiene 29 derivadas que calcular, y una red real tiene millones. Calcular cada una por separado sería inviable. Backpropagation es, simplemente, la manera eficiente de calcularlas todas de una pasada, reutilizando cálculos. Y su motor es la regla de la cadena.

La regla de la cadena, contada sin dolor

Piensa en una cadena de causas y efectos dentro de la red:

el peso $w_1$ afecta a la pre-activación $z_1$, que afecta a la activación $h$, que afecta a la salida $\hat{y}$, que afecta al error $L$.

La regla de la cadena dice que el efecto total de $w_1$ sobre $L$ es el producto de los efectos de cada eslabón:

$$\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_2} \cdot \frac{\partial z_2}{\partial h} \cdot \frac{\partial h}{\partial z_1} \cdot \frac{\partial z_1}{\partial w_1}$$

Una analogía TecnoMarket: si el departamento de compras negocia mal un precio (causa), el coste del producto sube, el margen baja, el beneficio trimestral cae. ¿Cuánta culpa tiene compras del resultado trimestral? La culpa se propaga multiplicándose eslabón a eslabón por la cadena. Backpropagation hace exactamente eso: empieza por el error final y va repartiendo culpa hacia atrás, capa por capa — de ahí el nombre, propagación hacia atrás. Y es eficiente porque los primeros factores de la cadena (los cercanos a la salida) se calculan una vez y se reutilizan para todos los pesos anteriores.

Solo necesitas tres derivadas elementales, todas ya conocidas:

Eslabón Derivada Lectura
$L = (\hat{y}-y)^2$ respecto a $\hat{y}$ $2(\hat{y}-y)$ Cuanto más lejos del objetivo, más culpa entra
Sigmoide $\sigma(z)$ respecto a $z$ $\sigma(z)(1-\sigma(z))$ La calculaste en el ejercicio 1 de la lección anterior
$z = wh + b$ respecto a $w$, $b$, $h$ $h$, $1$, $w$ La culpa de un peso es proporcional a la señal que le llegó

Backpropagation: un ejemplo numérico trazado a mano

Vamos a hacer una iteración completa, con números, sobre la red más pequeña posible con capa oculta: 1 entrada → 1 neurona oculta (sigmoide) → 1 salida (sigmoide). Datos: entrada $x = 1.0$, etiqueta $y = 1$ (es fraude). Pesos iniciales: $w_1 = 0.6$, $b_1 = 0$, $w_2 = 0.4$, $b_2 = 0$. Tasa de aprendizaje $\eta = 0.5$.

Forward pass (guardamos todos los valores intermedios: los necesitaremos a la vuelta):

Paso Cálculo Valor
$z_1 = w_1 x + b_1$ $0.6 \times 1.0$ $0.600$
$h = \sigma(z_1)$ $\sigma(0.6)$ $0.646$
$z_2 = w_2 h + b_2$ $0.4 \times 0.646$ $0.258$
$\hat{y} = \sigma(z_2)$ $\sigma(0.258)$ $0.564$
$L = (\hat{y}-y)^2$ $(0.564-1)^2$ $0.190$

La red dice "56 % de probabilidad de fraude" cuando debería decir casi 100 %. Repartamos la culpa.

Backward pass, de la salida hacia la entrada:

  1. Culpa de la salida: $\frac{\partial L}{\partial \hat{y}} = 2(\hat{y}-y) = 2(0.564-1) = -0.872$ (negativa: hay que subir $\hat{y}$).
  2. Atravesamos la sigmoide de salida: $\frac{\partial \hat{y}}{\partial z_2} = \hat{y}(1-\hat{y}) = 0.564 \times 0.436 = 0.246$. Acumulado: $\delta_2 = -0.872 \times 0.246 = -0.214$.
  3. Culpas de los parámetros de la capa 2: $\frac{\partial L}{\partial w_2} = \delta_2 \cdot h = -0.214 \times 0.646 = -0.139$; $\frac{\partial L}{\partial b_2} = \delta_2 = -0.214$.
  4. La culpa que baja hacia la capa oculta viaja por el peso: $\frac{\partial L}{\partial h} = \delta_2 \cdot w_2 = -0.214 \times 0.4 = -0.086$.
  5. Atravesamos la sigmoide oculta: $\frac{\partial h}{\partial z_1} = h(1-h) = 0.646 \times 0.354 = 0.229$. Acumulado: $\delta_1 = -0.086 \times 0.229 = -0.020$.
  6. Culpas de la capa 1: $\frac{\partial L}{\partial w_1} = \delta_1 \cdot x = -0.020$; $\frac{\partial L}{\partial b_1} = \delta_1 = -0.020$.

Actualización ($w \leftarrow w - \eta \cdot \text{gradiente}$):

$$w_2 = 0.4 - 0.5(-0.139) = 0.470 \qquad b_2 = 0 - 0.5(-0.214) = 0.107$$ $$w_1 = 0.6 - 0.5(-0.020) = 0.610 \qquad b_1 = 0 - 0.5(-0.020) = 0.010$$

Comprobación: repitiendo el forward con los pesos nuevos, $\hat{y}$ sube de $0.564$ a $\approx 0.594$ y la pérdida baja de $0.190$ a $\approx 0.165$. La red ha aprendido un poquito. Repetir esto miles de veces (sobre muchos ejemplos) es entrenar.

Fíjate en un detalle revelador del paso 3 frente al paso 6: el gradiente de $w_2$ ($-0.139$) es unas 7 veces mayor que el de $w_1$ ($-0.020$). La capa más lejana de la salida recibe menos señal de corrección. Ese detalle es la puerta del siguiente apartado.

Vanishing y exploding gradients

Cada vez que la culpa retrocede una capa, se multiplica por dos cosas: la derivada de la activación y los pesos. En nuestro trazado, atravesar cada sigmoide multiplicó la señal por ~0.23–0.25. Recuerda de la lección anterior que la derivada de la sigmoide nunca supera 0.25. Entonces, en una red de 10 capas sigmoides:

$$0.25^{10} \approx 0.00000095$$

La culpa que llega a las primeras capas es prácticamente cero: no aprenden. Es el vanishing gradient (desvanecimiento del gradiente), y es la razón histórica de que durante los 90 fuera casi imposible entrenar redes profundas, y una de las razones del triunfo de ReLU (su derivada es 1 en la zona positiva: la señal no se encoge al atravesarla).

El fenómeno inverso también existe: si los pesos son grandes (mayores que 1 en magnitud), la señal se amplifica en cada capa y los gradientes se disparan a valores enormes — exploding gradient — produciendo actualizaciones gigantes que destrozan el entrenamiento (verás pérdidas que saltan a nan). Es especialmente típico de las redes recurrentes, donde la "profundidad" es la longitud de la secuencia (lo retomaremos en el módulo 4), y las técnicas modernas de mitigación se tratan en el módulo 5.

Problema Causa Síntoma Mitigación (avance)
Vanishing Derivadas < 1 multiplicadas muchas veces Las primeras capas no cambian; la pérdida se estanca ReLU, buena inicialización, arquitecturas del módulo 5
Exploding Pesos grandes multiplicados muchas veces Pérdida oscila salvajemente o se hace nan Bajar $\eta$, recorte de gradiente (módulo 5)

Implementación completa en numpy: red de 2 capas para TecnoMarket

Juntemos todo: una red 3-8-1 (3 entradas, 8 ocultas con sigmoide, 1 salida sigmoide) entrenada con descenso de gradiente para estimar la probabilidad de fraude de un pedido. Es el salto de calidad respecto al perceptrón de 02-01: entradas continuas, salida con matices y capas ocultas entrenadas de verdad.

Primero, datos ficticios de TecnoMarket generados sintéticamente (400 pedidos):

import numpy as np

rng = np.random.default_rng(7)
n = 400

importe    = rng.uniform(0, 1, n)      # importe normalizado (0 = 0 EUR, 1 = 2000 EUR)
antiguedad = rng.uniform(0, 1, n)      # antiguedad de la cuenta (0 = nueva, 1 = 10 anios)
discrep    = rng.integers(0, 2, n)     # 1 si direcciones de envio/facturacion difieren

# Regla oculta (que la red debera descubrir): riesgo alto si el pedido es caro
# Y la cuenta es nueva, agravado por la discrepancia de direcciones
riesgo = 2.2*importe - 2.0*antiguedad + 1.2*discrep - 0.6
y = (riesgo + rng.normal(0, 0.3, n) > 0).astype(float).reshape(-1, 1)

X = np.column_stack([importe, antiguedad, discrep])   # forma (400, 3)

Ahora la red. Todo el backward pass es la versión matricial exacta de los 6 pasos que trazamos a mano:

def sigmoide(z): return 1 / (1 + np.exp(-z))

# Inicializacion: valores pequenos aleatorios (nunca ceros en las ocultas)
W1 = rng.normal(0, 0.5, (3, 8)); b1 = np.zeros((1, 8))
W2 = rng.normal(0, 0.5, (8, 1)); b2 = np.zeros((1, 1))

eta, epocas = 0.5, 3000
m = X.shape[0]                              # numero de ejemplos

for epoca in range(epocas):
    # ---- FORWARD: guardamos los intermedios para el backward ----
    z1 = X @ W1 + b1                        # (400, 8)
    h  = sigmoide(z1)                       # (400, 8)
    z2 = h @ W2 + b2                        # (400, 1)
    y_hat = sigmoide(z2)                    # (400, 1)
    perdida = np.mean((y_hat - y) ** 2)     # error cuadratico medio

    # ---- BACKWARD: los mismos 6 pasos del trazado a mano ----
    d_yhat = 2 * (y_hat - y) / m            # paso 1 (el /m promedia el batch)
    delta2 = d_yhat * y_hat * (1 - y_hat)   # paso 2: atravesar sigmoide de salida
    dW2 = h.T @ delta2                      # paso 3: culpa de W2 (8, 1)
    db2 = delta2.sum(axis=0, keepdims=True)
    d_h = delta2 @ W2.T                     # paso 4: la culpa baja por los pesos
    delta1 = d_h * h * (1 - h)              # paso 5: atravesar sigmoide oculta
    dW1 = X.T @ delta1                      # paso 6: culpa de W1 (3, 8)
    db1 = delta1.sum(axis=0, keepdims=True)

    # ---- ACTUALIZACION: descenso de gradiente ----
    W1 -= eta * dW1; b1 -= eta * db1
    W2 -= eta * dW2; b2 -= eta * db2

    if epoca % 500 == 0:
        acierto = np.mean((y_hat > 0.5) == y)
        print(f"Epoca {epoca:4d} | perdida {perdida:.4f} | acierto {acierto:.2%}")

Salida típica:

Epoca    0 | perdida 0.2531 | acierto 52.25%
Epoca  500 | perdida 0.1025 | acierto 86.50%
Epoca 1000 | perdida 0.0668 | acierto 91.75%
Epoca 2000 | perdida 0.0525 | acierto 93.50%
Epoca 2500 | perdida 0.0499 | acierto 94.00%

De moneda al aire (52 %) a un clasificador decente (94 %), sin que nadie programara la regla de riesgo: la red la ha extraído de los datos vía forward + backward + actualización. Probémosla como haría el equipo antifraude:

def prob_fraude(pedido):
    h = sigmoide(pedido @ W1 + b1)
    return sigmoide(h @ W2 + b2)[0, 0]

print(prob_fraude(np.array([[0.95, 0.05, 1]])))  # caro, cuenta nueva, direcciones distintas -> ~0.98
print(prob_fraude(np.array([[0.30, 0.90, 0]])))  # barato, cliente veterano -> ~0.02

Las correspondencias código ↔ trazado a mano merecen una segunda lectura: h.T @ delta2 es "la culpa de un peso es la señal que le llegó por la culpa de su neurona", sumada sobre los 400 ejemplos del batch; delta2 @ W2.T es "la culpa baja hacia la capa anterior viajando por los pesos". Si entiendes esas dos líneas, entiendes backpropagation.

Errores Comunes y Consejos

  • No guardar los valores intermedios del forward. El backward necesita h, y_hat, etc. Si los recalculas o los pierdes, o el código se complica o se vuelve lento. Estructura siempre: forward guarda → backward consume.
  • Inicializar todos los pesos a cero. En el perceptrón funcionaba; con capas ocultas es fatal: todas las neuronas de una capa reciben el mismo gradiente, aprenden lo mismo y la capa se comporta como una sola neurona (el problema de la simetría). Inicializa con valores aleatorios pequeños.
  • Confundir el signo de la actualización. Es $w \mathrel{-}= \eta \cdot dW$ (restar: descender). Un += accidental hace que la pérdida suba y es un despiste sorprendentemente frecuente.
  • Errores de dimensiones en el backward. Regla de oro: el gradiente dW debe tener exactamente la misma forma que W. Comprueba dW1.shape == W1.shape como test rápido.
  • Pérdida que se vuelve nan. Casi siempre exploding gradient o tasa de aprendizaje demasiado alta. Baja $\eta$ un orden de magnitud y revisa la inicialización.
  • Verificar el gradiente numéricamente cuando dudes. Aproxima $\frac{\partial L}{\partial w} \approx \frac{L(w+\epsilon) - L(w-\epsilon)}{2\epsilon}$ con $\epsilon = 10^{-5}$ para un peso concreto y compáralo con tu backward. Si no coinciden en varias cifras, hay un bug en el backward.

Ejercicios

  1. Segunda iteración a mano. Continúa el ejemplo trazado a mano: con los pesos actualizados ($w_1=0.610$, $b_1=0.010$, $w_2=0.470$, $b_2=0.107$), haz el forward completo y calcula la nueva pérdida. Comprueba que ha bajado respecto a 0.190.
  2. Sentir el vanishing gradient. En la red numpy de TecnoMarket, cambia la arquitectura a 4 capas ocultas sigmoides de 8 neuronas (en lugar de 1) y entrena con los mismos hiperparámetros. Imprime en la época 0 la magnitud media de los gradientes de la primera y de la última capa oculta (np.abs(dW).mean()). ¿Cuántas veces más pequeño es el de la primera capa? ¿Qué pasa con la velocidad de aprendizaje?
  3. Tasa de aprendizaje extrema. Vuelve a la red 3-8-1 original y entrena con eta = 20. Describe qué observas en la pérdida y explícalo con el vocabulario de esta lección. ¿Y con eta = 0.001?

Soluciones

Ejercicio 1. Forward con los pesos nuevos: $z_1 = 0.610 \times 1.0 + 0.010 = 0.620$; $h = \sigma(0.620) = 0.650$; $z_2 = 0.470 \times 0.650 + 0.107 = 0.413$; $\hat{y} = \sigma(0.413) = 0.602$; $L = (0.602 - 1)^2 = 0.158$. La pérdida baja de 0.190 a ≈0.158 (los decimales exactos pueden variar levemente según el redondeo que arrastres): el descenso de gradiente ha funcionado.

Ejercicio 2. Al apilar 4 capas sigmoides, en la época 0 el gradiente medio de la primera capa suele ser entre 20 y 100 veces menor que el de la última (cada sigmoide atravesada multiplica la señal por ≤ 0.25, más el efecto de los pesos pequeños). El entrenamiento se vuelve mucho más lento y la pérdida puede quedarse estancada cerca de su valor inicial durante cientos de épocas: estás viendo el vanishing gradient en vivo. Sustituir las sigmoides ocultas por ReLU (y su derivada, (z > 0).astype(float)) mejora la situación notablemente.

Ejercicio 3. Con eta = 20 la pérdida oscila salvajemente o crece hasta estabilizarse en un valor malo (o directamente aparece nan si los gradientes explotan): cada actualización es un giro de grifo tan grande que saltamos de "helada" a "hirviendo" sin pasar por el punto bueno. Con eta = 0.001 la pérdida baja de forma monótona pero lentísima: tras 3000 épocas el acierto apenas ha mejorado. La tasa de aprendizaje es un compromiso; en la próxima lección veremos técnicas (y optimizadores) que lo gestionan mejor que un valor fijo.

Conclusión

Ya tienes el corazón del deep learning completo: el forward pass convierte entradas en predicciones mediante multiplicaciones matriciales encadenadas; la pérdida resume el error en un número; el gradiente dice hacia dónde mover cada parámetro; la regla de la cadena permite calcularlo capa a capa repartiendo la culpa hacia atrás — eso es backpropagation, el algoritmo que en 1986 sacó a las redes de su primer invierno; y el descenso de gradiente aplica la corrección, giro suave de grifo a giro suave de grifo. Lo has visto en fórmulas, trazado a mano número a número, y funcionando en numpy sobre pedidos de TecnoMarket. También has visto que la propia mecánica multiplicativa del backward engendra sus patologías: gradientes que se desvanecen o explotan.

En nuestro entrenamiento hemos usado la pérdida y el optimizador más simples posibles: error cuadrático y descenso de gradiente puro sobre todos los datos a la vez. La próxima lección examina esas dos elecciones con lupa: qué funciones de pérdida existen y cuál corresponde a cada problema, y qué optimizadores (mini-batch, momentum, RMSprop, Adam) hacen que el descenso sea más rápido y estable. Con eso tendrás todas las piezas para montar tu primera red completa en Keras al final del módulo.

Curso de Deep Learning

Módulo 1: Introducción a Deep Learning

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados