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.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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]
ŷ = aPaso 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^lde forma(n_l, n_{l-1})b^lde forma(n_l, 1)(se difunde aB)
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 = a3) 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] = db4) 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.
| Variante | Cómo calcula el gradiente | Ventajas | Inconvenientes |
|---|---|---|---|
| Batch (full-batch) | Usa todo el dataset en cada actualización | Gradiente estable; trayectoria suave | Muy costoso en datasets grandes; menos actualizaciones por época |
| Mini-batch | Usa un subconjunto (p.ej., 32–512 ejemplos) | Buen equilibrio; eficiente en GPU; gradiente suficientemente estable | Requiere elegir tamaño de batch; algo de ruido |
| SGD (stochastic) | Usa 1 ejemplo por actualización | Actualizaciones muy frecuentes; puede escapar de ciertos mínimos locales | Gradiente 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 estimag_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_plusyloss_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^Tdurante la propagación hacia atrás.