Descenso por gradiente y backpropagation en redes neuronales

Capítulo 5

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

El gradiente: qué mide y por qué guía el ajuste de pesos

Entrenar una red neuronal consiste en ajustar parámetros (pesos y sesgos) para minimizar una función de pérdida. El mecanismo matemático que indica cómo cambiar esos parámetros es el gradiente: un vector de derivadas parciales que apunta en la dirección de mayor aumento de la pérdida. Por tanto, para reducirla, nos movemos en la dirección opuesta.

Si agrupamos todos los parámetros en un vector θ (pesos y sesgos), el gradiente es ∇θ L(θ). La actualización básica de descenso por gradiente es:

θ ← θ − η ∇θ L(θ)

donde η es la tasa de aprendizaje (learning rate). La derivada es crucial porque cuantifica sensibilidad: si ∂L/∂w es grande, pequeños cambios en w alteran mucho la pérdida; si es pequeña, el parámetro afecta poco (o está en una zona “plana”).

Ejemplo mínimo: una neurona y una pérdida

Considera una neurona con salida ŷ y un objetivo y. Sea z = w·x + b y ŷ = f(z). Para una pérdida escalar L(ŷ, y), la derivada respecto a w se obtiene encadenando derivadas:

∂L/∂w = (∂L/∂ŷ) (∂ŷ/∂z) (∂z/∂w)

y como ∂z/∂w = x, aparece el patrón típico: gradiente = “error local” × entrada. Esto se generaliza a redes profundas: cada capa recibe un “error” (gradiente) y lo distribuye hacia atrás.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Backpropagation: regla de la cadena aplicada de forma sistemática

Backpropagation es un procedimiento eficiente para calcular ∇θ L en redes multicapa. No es un optimizador; es el método para obtener gradientes. Se basa en reutilizar resultados del paso hacia adelante y aplicar la regla de la cadena desde la salida hacia las capas anteriores.

Notación para una red multicapa

Para una red con capas l = 1..L:

  • a^0 = x (entrada)
  • z^l = W^l a^{l-1} + b^l (pre-activación)
  • a^l = f^l(z^l) (activación)
  • ŷ = a^L (salida)

La pérdida para un ejemplo es L(ŷ, y). En entrenamiento con lotes, se usa la media sobre ejemplos.

Paso 1: forward pass (activaciones)

Se calculan z^l y a^l capa por capa. En práctica, se guardan z^l y/o a^l porque se reutilizan en el backward.

a = x  # a^0
for l in 1..L:
  z[l] = W[l] @ a + b[l]
  a = f[l](z[l])
  a_cache[l] = a
  z_cache[l] = z[l]
ŷ = a

Paso 2: error en la salida (gradiente inicial)

El backward arranca calculando el gradiente de la pérdida respecto a la salida: ∂L/∂a^L. Luego se convierte en gradiente respecto a z^L usando la derivada de la activación:

δ^L = ∂L/∂z^L = (∂L/∂a^L) ⊙ f'^L(z^L)

donde es producto elemento a elemento. A δ^l se le suele llamar “delta” o “error de la capa”.

Paso 3: propagación hacia atrás (capas internas)

Para una capa interna l, el gradiente respecto a su pre-activación se obtiene propagando el delta desde la capa siguiente:

δ^l = ( (W^{l+1})^T δ^{l+1} ) ⊙ f'^l(z^l)

Interpretación: (W^{l+1})^T δ^{l+1} reparte el “error” de la capa siguiente hacia las neuronas actuales, y f'^l(z^l) ajusta por la sensibilidad local de la activación.

Paso 4: gradientes de parámetros y actualización

Una vez que tienes δ^l, los gradientes de W^l y b^l se obtienen con fórmulas simples:

  • ∂L/∂W^l = δ^l (a^{l-1})^T
  • ∂L/∂b^l = δ^l (o suma sobre el batch)

Luego se actualiza con descenso por gradiente (o una variante):

W[l] ← W[l] − η dW[l]
b[l] ← b[l] − η db[l]

Guía práctica paso a paso: implementar backprop en una MLP (vectorizado)

La siguiente guía asume un batch de tamaño B, donde X tiene forma (d, B) y las activaciones a^l tienen forma (n_l, B). Esta convención facilita el uso de multiplicación matricial.

1) Inicializa parámetros

  • W^l de forma (n_l, n_{l-1})
  • b^l de forma (n_l, 1) (se difunde a B)

2) Forward (guardar cachés)

caches = []
a = X
for l in 1..L:
  z = W[l] @ a + b[l]
  a_next = f[l](z)
  caches.append((a, z))  # guarda a^{l-1} y z^l
  a = a_next
Yhat = a

3) Backward (deltas y gradientes)

Primero, el delta de salida. La forma exacta de ∂L/∂a^L depende de la pérdida; aquí lo representamos como una función dLoss_dYhat(Yhat, Y).

dWs = [None]* (L+1)
dbs = [None]* (L+1)

# salida
(a_prev, zL) = caches[L-1]
dA = dLoss_dYhat(Yhat, Y)
dZ = dA ⊙ fprime[L](zL)

dW = (dZ @ a_prev.T) / B
db = sum(dZ, axis=1, keepdims=True) / B

dWs[L] = dW
dbs[L] = db

# capas internas
for l in (L-1)..1:
  (a_prev, z) = caches[l-1]
  dA = W[l+1].T @ dZ
  dZ = dA ⊙ fprime[l](z)
  dW = (dZ @ a_prev.T) / B
  db = sum(dZ, axis=1, keepdims=True) / B
  dWs[l] = dW
  dbs[l] = db

4) Actualiza parámetros

for l in 1..L:
  W[l] -= η * dWs[l]
  b[l] -= η * dbs[l]

Detalles prácticos importantes: (1) promediar por B estabiliza la escala del gradiente al cambiar el tamaño del batch; (2) verificar dimensiones en cada multiplicación evita errores silenciosos; (3) guardar a^{l-1} y z^l es suficiente para el backward.

Variantes de optimización según cómo se usa el dataset

El cálculo del gradiente puede hacerse con distintos tamaños de lote. Esto cambia el coste computacional por actualización y el “ruido” del gradiente.

VarianteCómo calcula el gradienteVentajasInconvenientes
Batch (full-batch)Usa todo el dataset en cada actualizaciónGradiente estable; trayectoria suaveMuy costoso en datasets grandes; menos actualizaciones por época
Mini-batchUsa un subconjunto (p.ej., 32–512 ejemplos)Buen equilibrio; eficiente en GPU; gradiente suficientemente estableRequiere elegir tamaño de batch; algo de ruido
SGD (stochastic)Usa 1 ejemplo por actualizaciónActualizaciones muy frecuentes; puede escapar de ciertos mínimos localesGradiente muy ruidoso; puede oscilar y requerir más pasos

Esqueleto de entrenamiento para mini-batch

for epoch in 1..E:
  baraja(dataset)
  for (X_batch, Y_batch) en mini_batches:
    Yhat = forward(X_batch)
    grads = backward(Yhat, Y_batch)
    update(params, grads, η)

En plataformas como Coursera, edX o Udacity es común ver esta estructura con mini-batches porque refleja el flujo real de entrenamiento en frameworks modernos.

Comprobación de implementación: gradiente numérico vs analítico

Una fuente típica de errores al programar backprop “a mano” es un signo incorrecto, una dimensión mal alineada o una derivada de activación equivocada. Para detectar esto, se usa gradient checking: comparar el gradiente analítico (backprop) con una aproximación numérica por diferencias finitas.

Idea: aproximación por diferencias centradas

Para un parámetro escalar θ_i:

∂L/∂θ_i ≈ ( L(θ_i + ε) − L(θ_i − ε) ) / (2ε)

Con ε pequeño (por ejemplo, 1e-5). La versión centrada suele ser más precisa que la diferencia hacia adelante.

Procedimiento paso a paso

  • Elige un batch pequeño (p.ej., B=2) para que la comprobación sea rápida.
  • Ejecuta forward y calcula la pérdida L(θ).
  • Ejecuta backprop y guarda el gradiente analítico g_analítico.
  • Para un subconjunto de parámetros (no todos), perturba cada uno con y −ε, recalcula la pérdida y estima g_numérico.
  • Compara con un error relativo.

Métrica de comparación recomendada

rel_error = |g_num − g_an| / max(1e-8, |g_num| + |g_an|)

Como regla práctica, valores del orden de 1e-6 a 1e-4 suelen indicar que la implementación es correcta (depende de la escala de la pérdida y de ε).

Pseudocódigo de gradient checking (parcial)

ε = 1e-5

# 1) gradiente analítico
loss = compute_loss(forward(X), Y)
grads = backward(X, Y)  # contiene dW, db

# 2) gradiente numérico para algunos índices
for (param_name, idx) en sample_indices(params):
  θ = params[param_name][idx]

  params[param_name][idx] = θ + ε
  loss_plus = compute_loss(forward(X), Y)

  params[param_name][idx] = θ - ε
  loss_minus = compute_loss(forward(X), Y)

  params[param_name][idx] = θ  # restaurar

  g_num = (loss_plus - loss_minus) / (2*ε)
  g_an = grads[param_name][idx]

  rel = abs(g_num - g_an) / max(1e-8, abs(g_num) + abs(g_an))
  report(param_name, idx, g_num, g_an, rel)

Buenas prácticas al validar

  • Desactiva regularizaciones estocásticas (p.ej., dropout) durante la comprobación para que la pérdida sea determinista.
  • Usa el mismo batch y no barajes datos entre evaluaciones de loss_plus y loss_minus.
  • Comprueba primero una red muy pequeña (pocas capas y neuronas) antes de escalar.
  • Si el error relativo es alto, revisa: derivadas de activación, promediado por batch, y transpuestas en W^T durante la propagación hacia atrás.

Ahora responde el ejercicio sobre el contenido:

¿Cuál es el propósito principal de backpropagation durante el entrenamiento de una red neuronal multicapa?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Backpropagation no es un optimizador: su función es obtener ∇θL de manera eficiente. Lo hace propagando hacia atrás el “error” (deltas) mediante la regla de la cadena, para luego actualizar parámetros con descenso por gradiente u otra variante.

Siguiente capítulo

Redes neuronales multicapa y capacidad de aproximación

Arrow Right Icon
Portada de libro electrónico gratuitaIntroducción a las Redes Neuronales: Del Perceptrón al Deep Learning
38%

Introducción a las Redes Neuronales: Del Perceptrón al Deep Learning

Nuevo curso

13 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.