Entrenamiento de Redes Neuronales
Cómo aprende una red neuronal: desde el descenso del gradiente hasta la retropropagación. Domina el algoritmo que hace posible todo el deep learning moderno.
El problema de optimización
Entrenar una red neuronal es, en esencia, un problema de optimización. Tenemos una función de pérdida (o loss function) que mide lo mal que lo está haciendo nuestro modelo, y queremos encontrar los valores de los parámetros (pesos y biases) que minimizan esa función.
Formalmente, dado un dataset de entrenamiento \(\{(\mathbf{x}_i, y_i)\}_{i=1}^{N}\), buscamos los parámetros \(\boldsymbol{\theta}\) que minimizan:
donde \(f(\mathbf{x}; \boldsymbol{\theta})\) es la predicción del modelo (por ejemplo, un MLP), \(\mathcal{L}\) es la función de pérdida por muestra (MSE, cross-entropy, etc.) y \(\boldsymbol{\theta}\) agrupa todos los pesos y biases de la red.
Idea fundamental: Una red neuronal moderna puede tener millones (o miles de millones) de parámetros. No podemos resolver \(\nabla J = 0\) analíticamente. Necesitamos un método iterativo: el descenso del gradiente.
¿Por qué no resolver analíticamente?
En regresión lineal, minimizar el MSE tiene una solución cerrada (la ecuación normal). Pero en redes neuronales, la composición de funciones no lineales crea un paisaje de pérdida (loss landscape) con:
- No convexidad: múltiples mínimos locales y puntos de silla.
- Alta dimensionalidad: el espacio de parámetros tiene millones de dimensiones.
- Interdependencia: cada parámetro afecta a la pérdida a través de múltiples caminos.
Descenso del gradiente
El descenso del gradiente (Gradient Descent, GD) es el algoritmo que permite navegar el paisaje de pérdida hacia un mínimo. La idea es sencilla: en cada paso, calculamos la dirección de máximo crecimiento de la función (el gradiente) y nos movemos en la dirección opuesta.
Intuición: la analogía del montañero
Imagina que estás en la cima de una montaña con los ojos vendados y quieres llegar al valle. ¿Qué haces? En cada paso, palpas el suelo alrededor para detectar la pendiente más pronunciada y das un paso colina abajo. Eso es exactamente lo que hace el descenso del gradiente.
La regla de actualización
En cada iteración \(t\), actualizamos todos los parámetros simultáneamente:
donde:
- \(\boldsymbol{\theta}_t\): vector de parámetros en la iteración \(t\).
- \(\eta\) (learning rate): tasa de aprendizaje, un escalar positivo que controla el tamaño del paso.
- \(\nabla_{\boldsymbol{\theta}} J\): el gradiente de la función de coste respecto a todos los parámetros.
El signo negativo es crucial: el gradiente apunta en la dirección de máximo crecimiento, así que nos movemos en la dirección opuesta (máximo decrecimiento) para minimizar la pérdida.
Gradient Descent por lotes (Batch GD)
La versión original del algoritmo calcula el gradiente usando todo el dataset en cada iteración:
Vamos a desgranar esta fórmula. Lo que dice es: para saber en qué dirección ajustar los parámetros \(\boldsymbol{\theta}\), pasamos cada una de las \(N\) muestras del dataset por la red, calculamos cuánto contribuye cada muestra al error (el gradiente individual \(\nabla_{\boldsymbol{\theta}} \mathcal{L}_i\)), y promediamos todos esos gradientes. El resultado es un vector que apunta en la dirección media de máximo crecimiento de la pérdida. Al movernos en la dirección opuesta (con el signo menos de la regla de actualización), reducimos la pérdida promedio sobre todo el dataset.
Esta media tiene una propiedad importante: es el gradiente exacto de la función de coste \(J\), sin ninguna aproximación. Cada paso va en la dirección óptima para reducir la pérdida total. Sin embargo, el precio es alto: si tu dataset tiene 1 millón de muestras, cada único paso de actualización requiere hacer 1 millón de forward passes y 1 millón de backward passes. En datasets modernos de imágenes, texto o audio, esto hace que el Batch GD puro sea prohibitivamente lento y, en muchos casos, ni siquiera cabe en la memoria de la GPU. Aquí es donde entran las variantes estocásticas que veremos en la siguiente sección.
Ventajas:
- Gradiente exacto → convergencia estable y predecible.
- Usa toda la información disponible en cada paso.
Desventajas:
- Muy costoso: con millones de muestras, un solo paso requiere evaluar toda la red N veces.
- No cabe en memoria si el dataset es muy grande.
- Puede quedar atrapado en mínimos locales poco profundos (sin ruido que le ayude a escapar).
SGD y Mini-Batches
La solución al coste computacional de Batch GD es no usar todo el dataset en cada paso. En lugar de eso, estimamos el gradiente con un subconjunto aleatorio de muestras. Esto nos da tres variantes:
Stochastic Gradient Descent (SGD)
En SGD puro, calculamos el gradiente con una sola muestra aleatoria en cada iteración:
donde \(i\) se elige aleatoriamente del dataset en cada paso. La ventaja es que cada actualización es extremadamente barata (una sola muestra); la desventaja es que el gradiente de una sola muestra puede ser una estimación muy ruidosa del gradiente real, haciendo que la trayectoria de optimización sea errática.
Mini-Batch Gradient Descent
En la práctica, usamos mini-batches: subconjuntos de tamaño \(B\) (típicamente 32, 64, 128 o 256 muestras):
El mini-batch es el punto dulce entre ambos extremos. Con \(B = 32\) o \(B = 64\), el gradiente estimado es suficientemente representativo del gradiente real (la varianza se reduce como \(1/\sqrt{B}\)), y además las operaciones se vectorizan eficientemente en GPU gracias al paralelismo sobre tensores. Un mini-batch de 32 muestras no tarda 32× más que una sola: las multiplicaciones matriciales en batch son casi tan rápidas como una individual, gracias a la arquitectura SIMT de las GPUs.
En la práctica: cuando la gente dice «SGD» en deep learning, casi siempre se refiere a mini-batch gradient descent. El SGD puro (muestra a muestra) casi nunca se usa. Además, los optimizadores modernos como Adam, AdamW o SGD con momentum se basan en mini-batch SGD pero añaden acumulación de momentos y adaptación de la tasa por parámetro.
Comparativa de las tres variantes
| Variante | Tamaño del lote | Ruido | Velocidad | Uso de memoria |
|---|---|---|---|---|
| Batch GD | \(N\) (todo) | Ninguno | Lento | Muy alto |
| SGD puro | 1 | Muy alto | Rápido por paso | Mínimo |
| Mini-batch GD | \(B\) (32-256) | Moderado | Rápido | Moderado |
Tasa de aprendizaje y tamaño del batch
Tasa de aprendizaje (η)
La tasa de aprendizaje es probablemente el hiperparámetro más importante en el entrenamiento de redes neuronales. Controla cuánto cambian los pesos en cada paso. Encontrar un buen valor no es trivial y se aborda con técnicas de búsqueda de hiperparámetros o con schedulers que ajustan \(\eta\) dinámicamente durante el entrenamiento.
- η demasiado grande: los pasos son enormes, la pérdida oscila o diverge. El modelo "salta" por encima de los mínimos.
- η demasiado pequeño: la convergencia es extremadamente lenta. El modelo puede quedar atrapado en mínimos locales poco profundos.
- η adecuado: convergencia rápida y estable hacia un buen mínimo.
Tamaño del batch (B)
El tamaño del batch tiene efectos profundos en el entrenamiento:
| Batch grande | Batch pequeño |
|---|---|
| Gradiente más preciso | Gradiente más ruidoso (actúa como regularización) |
| Mejor uso del paralelismo de la GPU | Menos memoria requerida |
| Tiende a converger a mínimos más agudos (peor generalización) | Tiende a encontrar mínimos más planos (mejor generalización) |
| Menos actualizaciones por epoch | Más actualizaciones por epoch |
Regla práctica: Si aumentas el batch, aumenta proporcionalmente la tasa de aprendizaje. Esta linear scaling rule (Goyal et al., 2017) mantiene la varianza del gradiente constante y se usa rutinariamente en el entrenamiento distribuido de modelos grandes.
Epochs, iteraciones y el bucle de entrenamiento
Definiciones clave
- Iteración (step): una actualización de los pesos usando un mini-batch.
- Epoch: una pasada completa por todo el dataset de entrenamiento.
- Iteraciones por epoch: \(\lceil N / B \rceil\), donde \(N\) = muestras y \(B\) = batch size.
Ejemplo: Con un dataset de 50,000 muestras y un batch de 64, cada epoch tiene \(\lceil 50000/64 \rceil = 782\) iteraciones. Si entrenamos 100 epochs, el modelo verá cada muestra ~100 veces y realizará 78,200 actualizaciones de pesos.
El bucle de entrenamiento
Este es el flujo que sigue cualquier entrenamiento de red neuronal. Fíjate en que hay dos bucles anidados: un bucle externo de epochs (pasadas completas por el dataset) y un bucle interno de batches (subconjuntos de datos). En cada iteración del bucle interno se ejecuta un ciclo completo de forward → loss → backward → actualización. Al agotar todos los batches, se completa un epoch.
Inicializar los parámetros \(\boldsymbol{\theta}_0\) aleatoriamente (Xavier, He, etc.).
Para cada epoch \(e = 1, 2, \ldots, E\):
• Mezclar (shuffle) el dataset aleatoriamente.
• Dividir en \(\lceil N/B \rceil\) mini-batches de tamaño \(B\).
Para cada mini-batch \(\mathcal{B}\) (se repite \(\lceil N/B \rceil\) veces por epoch):
a) Forward pass: calcular \(\hat{y}_i = f(\mathbf{x}_i; \boldsymbol{\theta})\) para cada muestra del batch.
b) Calcular la pérdida: \(J = \frac{1}{B}\sum_{i \in \mathcal{B}} \mathcal{L}(\hat{y}_i, y_i)\)
c) Backward pass: calcular \(\nabla_{\boldsymbol{\theta}} J\) mediante backpropagation.
d) Actualizar: \(\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta \cdot \nabla_{\boldsymbol{\theta}} J\)
Evaluar en el conjunto de validación al final de cada epoch. Si la pérdida de validación deja de mejorar, detener el entrenamiento (early stopping, una técnica de regularización que se explica en profundidad en el módulo de Técnicas de Entrenamiento → Regularización).
Forward pass (propagación directa)
El forward pass es el proceso de calcular la salida de la red dados unos datos de entrada. Los datos fluyen capa por capa, desde la entrada hasta la salida, aplicando en cada capa una transformación lineal seguida de una no linealidad.
Para cada capa \(l = 1, 2, \ldots, L\)
donde:
- \(\mathbf{a}^{(0)} = \mathbf{x}\): la entrada a la red.
- \(\mathbf{z}^{(l)}\): la pre-activación (salida lineal) de la capa \(l\).
- \(\mathbf{a}^{(l)}\): la activación (salida no lineal) de la capa \(l\).
- \(\mathbf{W}^{(l)}\): la matriz de pesos de la capa \(l\).
- \(\mathbf{b}^{(l)}\): el vector de biases de la capa \(l\).
- \(\sigma^{(l)}\): la función de activación (ReLU, sigmoid, etc.).
La predicción final es \(\hat{\mathbf{y}} = \mathbf{a}^{(L)}\). Es crucial guardar todos los valores intermedios (\(\mathbf{z}^{(l)}, \mathbf{a}^{(l)}\)) porque los necesitaremos para el backward pass. Esto explica por qué el entrenamiento consume más memoria que la inferencia: durante el forward pass en entrenamiento, se almacena un grafo computacional completo con todas las activaciones intermedias. En inferencia, cada capa puede descartar su entrada una vez calculada la salida, pero en entrenamiento no, porque la regla de la cadena necesita acceder a esos valores para calcular los gradientes.
Nota sobre eficiencia: En la práctica, el forward pass procesa un mini-batch completo de forma matricial (no muestra por muestra), aprovechando las operaciones sobre tensores. PyTorch y TensorFlow optimizan estas operaciones usando BLAS/cuBLAS en GPU, alcanzando TFLOPs de rendimiento.
Backpropagation: la regla de la cadena en acción
El backpropagation (retropropagación, abreviado backprop) es el algoritmo que calcula eficientemente los gradientes de la función de pérdida respecto a cada parámetro de la red. Fue popularizado por Rumelhart, Hinton y Williams en 1986 y es el motor fundamental del deep learning.
Backpropagation no es un optimizador. Es un algoritmo para calcular gradientes. El optimizador (SGD, Adam, etc.) es el que usa esos gradientes para actualizar los pesos.
La regla de la cadena
El corazón del backpropagation es la regla de la cadena del cálculo diferencial. Si tenemos funciones compuestas \(y = f(g(x))\), la derivada es:
En una red neuronal, la salida es una composición de muchas funciones:
Aplicar la regla de la cadena a esta composición de forma eficiente — sin recalcular gradientes que ya hemos computado — es exactamente lo que hace backpropagation.
El error de retropropagación (δ)
Definimos el error de retropropagación de la neurona \(i\) en la capa \(l\) como:
Es decir, cuánto cambia la pérdida total cuando cambia la pre-activación de esa neurona.
Paso 1: δ de la capa de salida
Para la última capa (\(l = L\)):
donde \(\odot\) es el producto elemento a elemento (Hadamard) y \(\sigma'\) es la derivada de la función de activación.
Paso 2: Retropropagar δ capa por capa
Para cada capa oculta \(l = L-1, L-2, \ldots, 1\):
Observa la elegancia: para calcular \(\delta^{(l)}\), solo necesitamos \(\delta^{(l+1)}\) (que ya calculamos) y los pesos de la capa siguiente \(\mathbf{W}^{(l+1)}\). El gradiente «fluye hacia atrás» por los mismos pesos que se usaron en el forward pass, pero transpuestos.
Esta propagación multiplicativa tiene un efecto colateral: si los pesos o las derivadas de la activación son sistemáticamente menores (o mayores) que 1, los gradientes se reducen (o amplifican) exponencialmente al retroceder por muchas capas. Este es el problema del vanishing/exploding gradient, que se trata en profundidad en Estabilidad del entrenamiento.
Paso 3: Calcular los gradientes de los parámetros
Una vez tenemos todos los \(\delta^{(l)}\), los gradientes de pesos y biases son:
Es decir: el gradiente de cada peso \(W^{(l)}_{ij}\) es simplemente el error retropropagado \(\delta^{(l)}_i\) multiplicado por la activación de la neurona que lo alimenta \(a^{(l-1)}_j\). Elegante y eficiente.
🧪 Prueba la retropropagación interactiva →Cálculo de gradientes capa por capa
Vamos a aplicar todo lo anterior a un ejemplo concreto: una red con 2 entradas, una capa oculta de 2 neuronas (ReLU) y 1 salida (sigmoide), usando binary cross-entropy.
Arquitectura: 2 → 2 → 1
Ejemplo numérico completo
Supongamos estos valores iniciales (simplificados):
| Parámetro | Valor |
|---|---|
| Entrada \(\mathbf{x}\) | [0.5, 0.8] |
| Etiqueta \(y\) | 1 |
| \(\mathbf{W}^{(1)}\) | [[0.4, 0.3], [0.2, 0.6]] |
| \(\mathbf{b}^{(1)}\) | [0.1, 0.1] |
| \(\mathbf{W}^{(2)}\) | [[0.5, 0.7]] |
| \(\mathbf{b}^{(2)}\) | [0.2] |
Capa 1 (oculta, ReLU):
\(z^{(1)}_1 = 0.4 \times 0.5 + 0.3 \times 0.8 + 0.1 = 0.54\)
\(z^{(1)}_2 = 0.2 \times 0.5 + 0.6 \times 0.8 + 0.1 = 0.68\)
\(a^{(1)}_1 = \text{ReLU}(0.54) = 0.54\)
\(a^{(1)}_2 = \text{ReLU}(0.68) = 0.68\)
Capa 2 (salida, sigmoid):
\(z^{(2)}_1 = 0.5 \times 0.54 + 0.7 \times 0.68 + 0.2 = 0.946\)
\(\hat{y} = a^{(2)}_1 = \sigma(0.946) = 0.7203\)
Pérdida (BCE):
\(J = -[1 \cdot \ln(0.7203) + 0 \cdot \ln(1-0.7203)] = 0.3279\)
Delta de la capa de salida:
\(\frac{\partial J}{\partial a^{(2)}} = \frac{a^{(2)} - y}{a^{(2)}(1-a^{(2)})} = \frac{0.7203 - 1}{0.7203 \times 0.2797} = -1.389\)
\(\sigma'(z^{(2)}) = 0.7203 \times (1 - 0.7203) = 0.2015\)
\(\delta^{(2)} = -1.389 \times 0.2015 = -0.2797\)
Con BCE+sigmoid, esto se simplifica a: \(\delta^{(2)} = \hat{y} - y = 0.7203 - 1 = -0.2797\)
Gradientes de W⁽²⁾ y b⁽²⁾:
\(\frac{\partial J}{\partial W^{(2)}_{11}} = \delta^{(2)} \cdot a^{(1)}_1 = -0.2797 \times 0.54 = -0.1511\)
\(\frac{\partial J}{\partial W^{(2)}_{12}} = \delta^{(2)} \cdot a^{(1)}_2 = -0.2797 \times 0.68 = -0.1902\)
\(\frac{\partial J}{\partial b^{(2)}} = \delta^{(2)} = -0.2797\)
Retropropagar delta a la capa oculta:
\(\delta^{(1)}_1 = W^{(2)}_{11} \cdot \delta^{(2)} \cdot \text{ReLU}'(z^{(1)}_1) = 0.5 \times (-0.2797) \times 1 = -0.1399\)
\(\delta^{(1)}_2 = W^{(2)}_{12} \cdot \delta^{(2)} \cdot \text{ReLU}'(z^{(1)}_2) = 0.7 \times (-0.2797) \times 1 = -0.1958\)
Gradientes de W⁽¹⁾ y b⁽¹⁾:
\(\frac{\partial J}{\partial W^{(1)}_{11}} = \delta^{(1)}_1 \cdot x_1 = -0.1399 \times 0.5 = -0.0699\)
\(\frac{\partial J}{\partial W^{(1)}_{12}} = \delta^{(1)}_1 \cdot x_2 = -0.1399 \times 0.8 = -0.1119\)
\(\frac{\partial J}{\partial W^{(1)}_{21}} = \delta^{(1)}_2 \cdot x_1 = -0.1958 \times 0.5 = -0.0979\)
\(\frac{\partial J}{\partial W^{(1)}_{22}} = \delta^{(1)}_2 \cdot x_2 = -0.1958 \times 0.8 = -0.1566\)
Actualización con η = 0.1:
\(W^{(2)}_{11} = 0.5 - 0.1 \times (-0.1511) = 0.5151\)
\(W^{(2)}_{12} = 0.7 - 0.1 \times (-0.1902) = 0.7190\)
Tras esta actualización, la pérdida sería menor que los 0.3279 iniciales. Repitiendo este proceso miles de veces, la red converge a un mínimo de la función de pérdida.
Complejidad computacional
Una propiedad notable del backpropagation es su eficiencia:
- El forward pass tiene coste \(\mathcal{O}(\text{parámetros})\).
- El backward pass tiene el mismo orden de complejidad que el forward pass.
- Calcular todos los gradientes cuesta ≈2× el coste de una predicción.
Sin backpropagation, calcular el gradiente de cada peso requeriría una perturbación individual: \(\mathcal{O}(\text{parámetros}^2)\). Para una red con 1M de parámetros, backprop es un millón de veces más eficiente.
El ciclo completo de entrenamiento
Ahora que entendemos la complejidad de cada paso (forward + backward ≈ 2× forward), podemos calcular el coste total del entrenamiento. Recordemos que el dataset se divide en mini-batches de tamaño \(B\), cada uno de los cuales produce una actualización de pesos. El número total de actualizaciones (pasos de backpropagation) en todo el entrenamiento es:
donde \(E\) es el número de epochs, \(N\) es el tamaño del dataset y \(B\) es el tamaño del batch. Este número puede ser sorprendentemente grande: un modelo entrenado en ImageNet (1.28M imágenes) con batch 256 durante 90 epochs realiza \(90 \times 5000 = 450{,}000\) pasos de backpropagation. Para un LLM como LLaMA-2 entrenado con 2 billones de tokens, la cifra se dispara a millones de pasos.
Es importante entender esta relación porque las decisiones de diseño están conectadas: un batch más grande reduce el número de pasos por epoch (y por tanto el tiempo por epoch), pero puede requerir más epochs para converger y necesita más memoria GPU. Un batch más pequeño da más pasos (más actualizaciones), lo que en principio permite una convergencia más rápida en epochs, pero cada paso es más ruidoso.
Referencias y lecturas complementarias:
- Rumelhart, Hinton & Williams, «Learning representations by back-propagating errors» (1986)
- Baydin et al., «Automatic Differentiation in Machine Learning: a Survey» (2018)
- Goodfellow, Bengio & Courville, Deep Learning, capítulos 6-8
- Goyal et al., «Accurate, Large Minibatch SGD» (2017) — la linear scaling rule