Saltar a contenido

1. Metodología de Retropropagación

La retropropagación es el algoritmo fundamental para entrenar redes neuronales. Formalizada por primera vez en los años 80 y popularizada por Rumelhart, Hinton y Williams 1, esta elegante aplicación de la regla de la cadena permite calcular gradientes de forma eficiente en redes multicapa, haciendo que el aprendizaje profundo sea viable en la práctica.

En esencia, la retropropagación resuelve un problema fundamental: ¿cómo calculamos eficientemente el gradiente de una función de pérdida con respecto a cada peso en una red que podría tener millones de parámetros? La respuesta consiste en propagar sistemáticamente señales de error hacia atrás a través de la red, reutilizando cálculos intermedios para evitar operaciones redundantes.

1.1 El Problema de Aprendizaje

Considere una red neuronal como una función parametrizada \(f(x; \theta)\) que transforma entradas \(x\) en salidas \(\hat{y}\). Los parámetros \(\theta\) incluyen todos los pesos y sesgos de todas las capas. Nuestro objetivo es encontrar los parámetros que minimicen una función de pérdida:

\[ \begin{equation} \mathcal{L}(\theta) = \frac{1}{N} \sum_{i=1}^{N} L(y_i, f(x_i; \theta)) \label{eq:loss_function} \end{equation} \]

donde:

  • \(N\) es el número de ejemplos de entrenamiento
  • \(L(y, \hat{y})\) es la pérdida para una sola predicción (por ejemplo, error cuadrático medio o entropía cruzada)
  • \(y_i\) es la etiqueta verdadera para la entrada \(x_i\)

Para minimizar esta pérdida mediante descenso de gradiente, necesitamos calcular:

\[ \begin{equation} \frac{\partial \mathcal{L}}{\partial \theta} = \frac{1}{N} \sum_{i=1}^{N} \frac{\partial L(y_i, f(x_i; \theta))}{\partial \theta} \label{eq:gradient_objective} \end{equation} \]

La retropropagación proporciona un algoritmo eficiente para calcular estos gradientes.

1.2 Arquitectura de Red

Construyamos nuestra comprensión con una red multicapa de propagación hacia adelante. Para la capa \(l\) (donde \(l = 1, 2, \ldots, L\)):

Preactivación (suma ponderada):

\[ \begin{equation} z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)} \label{eq:pre_activation} \end{equation} \]

Activación (no linealidad):

\[ \begin{equation} a^{(l)} = g^{(l)}(z^{(l)}) \label{eq:activation} \end{equation} \]

donde:

  • \(W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}}\) es la matriz de pesos para la capa \(l\)
  • \(b^{(l)} \in \mathbb{R}^{n_l}\) es el vector de sesgos
  • \(a^{(l-1)}\) es la activación de la capa anterior (con \(a^{(0)} = x\) como entrada)
  • \(g^{(l)}(\cdot)\) es la función de activación (por ejemplo, ReLU, sigmoide, tanh)
  • \(n_l\) es el número de neuronas en la capa \(l\)

La salida final es \(\hat{y} = a^{(L)}\), y la pérdida para un solo ejemplo es:

\[ \begin{equation} L = L(y, a^{(L)}) \label{eq:single_loss} \end{equation} \]

1.3 El Paso Hacia Adelante

Antes de poder propagar errores hacia atrás, primero debemos calcular el paso hacia adelante, es decir, evaluar las predicciones de la red. El procedimiento es sencillo:

  1. Inicializar: Establecer \(a^{(0)} = x\) (la entrada)
  2. Para cada capa \(l = 1, 2, \ldots, L\):
  3. Calcular \(z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}\)
  4. Calcular \(a^{(l)} = g^{(l)}(z^{(l)})\)
  5. Salida: \(\hat{y} = a^{(L)}\)
  6. Calcular la pérdida: \(L = L(y, \hat{y})\)

Durante este paso, almacenamos todos los valores intermedios \(z^{(l)}\) y \(a^{(l)}\), que serán cruciales para el paso hacia atrás.

1.4 El Paso Hacia Atrás: Derivando la Retropropagación

Ahora viene la parte elegante. Queremos calcular \(\frac{\partial L}{\partial W^{(l)}}\) y \(\frac{\partial L}{\partial b^{(l)}}\) para cada capa \(l\). La idea clave consiste en trabajar hacia atrás desde la capa de salida, calculando y almacenando gradientes intermedios.

1.4.1 Error de la Capa de Salida

Para la capa final \(L\), comenzamos calculando cómo cambia la pérdida con respecto a la preactivación:

\[ \begin{equation} \delta^{(L)} = \frac{\partial L}{\partial z^{(L)}} \label{eq:output_delta} \end{equation} \]

Por la regla de la cadena:

\[ \begin{equation} \delta^{(L)} = \frac{\partial L}{\partial a^{(L)}} \odot \frac{\partial a^{(L)}}{\partial z^{(L)}} = \frac{\partial L}{\partial a^{(L)}} \odot g'^{(L)}(z^{(L)}) \label{eq:output_delta_chain} \end{equation} \]

donde \(\odot\) denota multiplicación elemento a elemento (producto de Hadamard), y \(g'^{(L)}\) es la derivada de la función de activación.

Para combinaciones comunes de función de pérdida y activación, esta expresión se simplifica:

Error cuadrático medio con salida lineal:

\[ \begin{equation} \delta^{(L)} = a^{(L)} - y \label{eq:mse_delta} \end{equation} \]

Entropía cruzada con salida softmax:

\[ \begin{equation} \delta^{(L)} = a^{(L)} - y \label{eq:softmax_delta} \end{equation} \]

(donde \(y\) es un vector con codificación one-hot)

1.4.2 Errores de Capas Ocultas

Para las capas ocultas (\(l < L\)), propagamos el error hacia atrás usando la regla de la cadena. El error en la capa \(l\) depende del error en la capa \(l+1\):

\[ \begin{equation} \delta^{(l)} = \frac{\partial L}{\partial z^{(l)}} \label{eq:hidden_delta_definition} \end{equation} \]

Como \(z^{(l)}\) afecta a la pérdida a través de \(a^{(l)}\), que a su vez afecta a \(z^{(l+1)}\):

\[ \begin{equation} \frac{\partial L}{\partial z^{(l)}} = \frac{\partial L}{\partial z^{(l+1)}} \cdot \frac{\partial z^{(l+1)}}{\partial a^{(l)}} \cdot \frac{\partial a^{(l)}}{\partial z^{(l)}} \label{eq:chain_rule_layers} \end{equation} \]

De la ecuación \(\ref{eq:pre_activation}\) sabemos que \(z^{(l+1)} = W^{(l+1)} a^{(l)} + b^{(l+1)}\), por lo que:

\[ \begin{equation} \frac{\partial z^{(l+1)}}{\partial a^{(l)}} = W^{(l+1)} \label{eq:dz_da} \end{equation} \]

Y de la ecuación \(\ref{eq:activation}\):

\[ \begin{equation} \frac{\partial a^{(l)}}{\partial z^{(l)}} = g'^{(l)}(z^{(l)}) \label{eq:da_dz} \end{equation} \]

Combinando estas expresiones, obtenemos la ecuación de retropropagación para capas ocultas:

\[ \begin{equation} \delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot g'^{(l)}(z^{(l)}) \label{eq:backprop_recursion} \end{equation} \]

Este es el núcleo de la retropropagación: los errores de la capa \(l+1\) se propagan hacia atrás a través de los pesos, modulados por la derivada de la función de activación.

1.4.3 Gradientes con Respecto a Parámetros

Una vez que tenemos \(\delta^{(l)}\) para cada capa, calcular los gradientes es sencillo:

Gradiente con respecto a los pesos:

\[ \begin{equation} \frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T \label{eq:weight_gradient} \end{equation} \]

Gradiente con respecto a los sesgos:

\[ \begin{equation} \frac{\partial L}{\partial b^{(l)}} = \delta^{(l)} \label{eq:bias_gradient} \end{equation} \]

Estas fórmulas surgen directamente de la regla de la cadena aplicada a las ecuaciones \(\ref{eq:pre_activation}\) y \(\ref{eq:activation}\).

1.5 El Algoritmo Completo de Retropropagación

Reuniendo todos los elementos, este es el algoritmo completo para un solo ejemplo de entrenamiento:

1.5.1 Algoritmo: Retropropagación

Entrada: Ejemplo de entrenamiento \((x, y)\), parámetros de la red \(\{W^{(l)}, b^{(l)}\}_{l=1}^L\)

Salida: Gradientes \(\{\frac{\partial L}{\partial W^{(l)}}, \frac{\partial L}{\partial b^{(l)}}\}_{l=1}^L\)

Paso hacia adelante:

  1. Establecer \(a^{(0)} = x\)
  2. Para \(l = 1\) hasta \(L\):
  3. \(z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}\)
  4. \(a^{(l)} = g^{(l)}(z^{(l)})\)
  5. Calcular la pérdida: \(L = L(y, a^{(L)})\)

Paso hacia atrás:

  1. Calcular el error de salida: \(\delta^{(L)} = \frac{\partial L}{\partial a^{(L)}} \odot g'^{(L)}(z^{(L)})\)
  2. Para \(l = L-1\) descendiendo hasta \(1\):
  3. \(\delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot g'^{(l)}(z^{(l)})\)
  4. Para \(l = 1\) hasta \(L\):
  5. \(\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T\)
  6. \(\frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}\)

1.6 Eficiencia Computacional

La retropropagación es notablemente eficiente. Sin ella, calcular los gradientes de forma ingenua requeriría \(O(W^2)\) operaciones, donde \(W\) es el número total de pesos. La retropropagación reduce esta complejidad a \(O(W)\) al reutilizar cálculos intermedios.

Idea clave: Cada gradiente intermedio se calcula exactamente una vez y se reutiliza en todos los cálculos posteriores. Esto es programación dinámica aplicada a la diferenciación.

Para una red con \(L\) capas y \(n\) neuronas por capa:

  • Paso hacia adelante: \(O(Ln^2)\) operaciones
  • Paso hacia atrás: \(O(Ln^2)\) operaciones
  • Total: \(O(Ln^2)\) — ¡el mismo orden que el paso hacia adelante!

1.7 Un Ejemplo Detallado: Red de Dos Capas

Veamos un ejemplo concreto con una red de dos capas (una capa oculta).

Arquitectura:

  • Capa de entrada: 2 neuronas (\(x \in \mathbb{R}^2\))
  • Capa oculta: 3 neuronas con activación sigmoide
  • Capa de salida: 1 neurona con activación sigmoide
  • Pérdida: error cuadrático medio

Ejemplo de entrenamiento: \(x = [0.5, 1.0]^T\), \(y = 0.8\)

Pesos y sesgos iniciales (elegidos aleatoriamente para ilustrar el procedimiento):

\[ W^{(1)} = \begin{bmatrix} 0.1 & 0.4 \\ 0.3 & 0.2 \\ 0.5 & 0.6 \end{bmatrix}, \quad b^{(1)} = \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \end{bmatrix} \]
\[ W^{(2)} = \begin{bmatrix} 0.7 & 0.8 & 0.9 \end{bmatrix}, \quad b^{(2)} = [0.4] \]

1.7.1 Paso Hacia Adelante

Capa 1 (oculta):

\[ z^{(1)} = W^{(1)} x + b^{(1)} = \begin{bmatrix} 0.1 & 0.4 \\ 0.3 & 0.2 \\ 0.5 & 0.6 \end{bmatrix} \begin{bmatrix} 0.5 \\ 1.0 \end{bmatrix} + \begin{bmatrix} 0.1 \\ 0.2 \\ 0.3 \end{bmatrix} = \begin{bmatrix} 0.55 \\ 0.55 \\ 1.15 \end{bmatrix} \]
\[ a^{(1)} = \sigma(z^{(1)}) = \begin{bmatrix} 0.634 \\ 0.634 \\ 0.759 \end{bmatrix} \]

(donde \(\sigma(z) = \frac{1}{1 + e^{-z}}\) es la función sigmoide)

Capa 2 (salida):

\[ z^{(2)} = W^{(2)} a^{(1)} + b^{(2)} = [0.7 \ 0.8 \ 0.9] \begin{bmatrix} 0.634 \\ 0.634 \\ 0.759 \end{bmatrix} + 0.4 = 1.625 \]
\[ a^{(2)} = \sigma(z^{(2)}) = \sigma(1.625) = 0.836 \]

Pérdida:

\[ L = \frac{1}{2}(y - a^{(2)})^2 = \frac{1}{2}(0.8 - 0.836)^2 = 0.000648 \]

1.7.2 Paso Hacia Atrás

Error de la capa de salida (\(\delta^{(2)}\)):

Para la pérdida MSE y la activación sigmoide:

\[ \frac{\partial L}{\partial a^{(2)}} = -(y - a^{(2)}) = -(0.8 - 0.836) = 0.036 \]
\[ \sigma'(z) = \sigma(z)(1 - \sigma(z)) = 0.836 \times (1 - 0.836) = 0.137 \]
\[ \delta^{(2)} = 0.036 \times 0.137 = 0.00494 \]

Error de la capa oculta (\(\delta^{(1)}\)):

\[ \delta^{(1)} = (W^{(2)})^T \delta^{(2)} \odot \sigma'(z^{(1)}) \]
\[ (W^{(2)})^T \delta^{(2)} = \begin{bmatrix} 0.7 \\ 0.8 \\ 0.9 \end{bmatrix} \times 0.00494 = \begin{bmatrix} 0.00346 \\ 0.00395 \\ 0.00445 \end{bmatrix} \]
\[ \sigma'(z^{(1)}) = a^{(1)} \odot (1 - a^{(1)}) = \begin{bmatrix} 0.232 \\ 0.232 \\ 0.183 \end{bmatrix} \]
\[ \delta^{(1)} = \begin{bmatrix} 0.00346 \\ 0.00395 \\ 0.00445 \end{bmatrix} \odot \begin{bmatrix} 0.232 \\ 0.232 \\ 0.183 \end{bmatrix} = \begin{bmatrix} 0.000803 \\ 0.000917 \\ 0.000814 \end{bmatrix} \]

Gradientes:

Para \(W^{(2)}\):

\[ \begin{aligned} \frac{\partial L}{\partial W^{(2)}} &= \delta^{(2)} (a^{(1)})^T \\ &= 0.00494 \times \begin{bmatrix} 0.634 & 0.634 & 0.759 \end{bmatrix} \\ &= \begin{bmatrix} 0.00313 & 0.00313 & 0.00375 \end{bmatrix} \end{aligned} \]

Para \(b^{(2)}\):

\[ \frac{\partial L}{\partial b^{(2)}} = \delta^{(2)} = 0.00494 \]

Para \(W^{(1)}\):

\[ \frac{\partial L}{\partial W^{(1)}} = \delta^{(1)} (a^{(0)})^T = \begin{bmatrix} 0.000803 \\ 0.000917 \\ 0.000814 \end{bmatrix} \begin{bmatrix} 0.5 & 1.0 \end{bmatrix} = \begin{bmatrix} 0.000402 & 0.000803 \\ 0.000459 & 0.000917 \\ 0.000407 & 0.000814 \end{bmatrix} \]

Para \(b^{(1)}\):

\[ \frac{\partial L}{\partial b^{(1)}} = \delta^{(1)} = \begin{bmatrix} 0.000803 \\ 0.000917 \\ 0.000814 \end{bmatrix} \]

1.8 Implementación en Python

A continuación se presenta una implementación clara y vectorizada de la retropropagación:

import numpy as np

class NeuralNetwork:
    """
    A simple feedforward neural network with backpropagation.
    """

    def __init__(self, layer_sizes, activation='relu'):
        """
        Initialize network with random weights.

        Parameters:
        - layer_sizes: list of layer sizes [input_size, hidden1, hidden2, ..., output_size]
        - activation: 'relu', 'sigmoid', or 'tanh'
        """
        self.layer_sizes = layer_sizes
        self.num_layers = len(layer_sizes)
        self.activation_name = activation

        # Initialize weights and biases with He initialization for ReLU
        self.weights = []
        self.biases = []

        for i in range(1, self.num_layers):
            # He initialization: good for ReLU
            w = np.random.randn(layer_sizes[i], layer_sizes[i-1]) * np.sqrt(2.0 / layer_sizes[i-1])
            b = np.zeros((layer_sizes[i], 1))

            self.weights.append(w)
            self.biases.append(b)

    def activation(self, z):
        """Apply activation function."""
        if self.activation_name == 'relu':
            return np.maximum(0, z)
        elif self.activation_name == 'sigmoid':
            return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
        elif self.activation_name == 'tanh':
            return np.tanh(z)

    def activation_derivative(self, z):
        """Compute derivative of activation function."""
        if self.activation_name == 'relu':
            return (z > 0).astype(float)
        elif self.activation_name == 'sigmoid':
            s = self.activation(z)
            return s * (1 - s)
        elif self.activation_name == 'tanh':
            return 1 - np.tanh(z)**2

    def forward(self, x):
        """
        Forward pass through the network.

        Parameters:
        - x: input vector of shape (input_size, 1)

        Returns:
        - activations: list of activations for each layer
        - z_values: list of pre-activation values for each layer
        """
        activations = [x]
        z_values = []

        a = x
        for i in range(self.num_layers - 1):
            z = self.weights[i] @ a + self.biases[i]
            z_values.append(z)

            # Use linear activation for output layer, activation function for hidden layers
            if i == self.num_layers - 2:  # Output layer
                a = z  # Linear output for regression
            else:
                a = self.activation(z)

            activations.append(a)

        return activations, z_values

    def backward(self, x, y, activations, z_values):
        """
        Backward pass: compute gradients using backpropagation.

        Parameters:
        - x: input vector
        - y: true output
        - activations: list from forward pass
        - z_values: list from forward pass

        Returns:
        - weight_gradients: list of gradients for weights
        - bias_gradients: list of gradients for biases
        """
        weight_gradients = [None] * (self.num_layers - 1)
        bias_gradients = [None] * (self.num_layers - 1)

        # Output layer error (assuming MSE loss and linear output)
        delta = activations[-1] - y

        # Backpropagate through layers
        for l in range(self.num_layers - 2, -1, -1):
            # Compute gradients for this layer
            weight_gradients[l] = delta @ activations[l].T
            bias_gradients[l] = delta

            # Propagate error to previous layer (if not input layer)
            if l > 0:
                delta = (self.weights[l].T @ delta) * self.activation_derivative(z_values[l-1])

        return weight_gradients, bias_gradients

    def train_step(self, x, y, learning_rate=0.01):
        """
        Perform one training step: forward pass, backward pass, update weights.

        Parameters:
        - x: input vector (column vector)
        - y: true output (scalar or column vector)
        - learning_rate: step size for gradient descent

        Returns:
        - loss: mean squared error for this example
        """
        # Ensure inputs are column vectors
        x = x.reshape(-1, 1)
        y = np.array(y).reshape(-1, 1)

        # Forward pass
        activations, z_values = self.forward(x)

        # Compute loss (MSE)
        loss = 0.5 * np.sum((activations[-1] - y)**2)

        # Backward pass
        weight_grads, bias_grads = self.backward(x, y, activations, z_values)

        # Update weights and biases
        for i in range(self.num_layers - 1):
            self.weights[i] -= learning_rate * weight_grads[i]
            self.biases[i] -= learning_rate * bias_grads[i]

        return loss

    def predict(self, x):
        """Make a prediction for input x."""
        x = x.reshape(-1, 1)
        activations, _ = self.forward(x)
        return activations[-1]


# Example: Train on XOR problem
if __name__ == "__main__":
    # XOR dataset
    X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]).T  # Shape: (2, 4)
    y = np.array([[0], [1], [1], [0]]).T              # Shape: (1, 4)

    # Create network: 2 inputs, 4 hidden neurons, 1 output
    nn = NeuralNetwork([2, 4, 1], activation='sigmoid')

    # Training loop
    epochs = 5000
    print("Training on XOR problem...")
    for epoch in range(epochs):
        total_loss = 0
        for i in range(4):
            x_i = X[:, i:i+1]
            y_i = y[:, i:i+1]
            loss = nn.train_step(x_i, y_i, learning_rate=0.5)
            total_loss += loss

        if epoch % 1000 == 0:
            print(f"Epoch {epoch:4d}, Loss: {total_loss:.6f}")

    # Test predictions
    print("\nPredictions after training:")
    for i in range(4):
        x_i = X[:, i]
        pred = nn.predict(x_i)
        print(f"Input: {x_i}, True: {y[0, i]:.1f}, Predicted: {pred[0, 0]:.4f}")

Salida:

Training on XOR problem...
Epoch    0, Loss: 0.847213
Epoch 1000, Loss: 0.006891
Epoch 2000, Loss: 0.002456
Epoch 3000, Loss: 0.001339
Epoch 4000, Loss: 0.000889

Predictions after training:
Input: [0 0], True: 0.0, Predicted: 0.0187
Input: [0 1], True: 1.0, Predicted: 0.9795
Input: [1 0], True: 1.0, Predicted: 0.9801
Input: [1 1], True: 0.0, Predicted: 0.0213

1.9 Funciones de Activación Comunes y Sus Derivadas

La retropropagación requiere calcular \(g'(z)\) para cada función de activación. Estas son las más comunes:

Función de activación Definición Derivada
ReLU \(g(z) = \max(0, z)\) \(g'(z) = \begin{cases} 1 & z > 0 \\ 0 & z \leq 0 \end{cases}\)
Sigmoide \(g(z) = \frac{1}{1 + e^{-z}}\) \(g'(z) = g(z)(1 - g(z))\)
Tanh \(g(z) = \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}\) \(g'(z) = 1 - \tanh^2(z)\)
Leaky ReLU \(g(z) = \max(\alpha z, z)\), donde \(\alpha \approx 0.01\) \(g'(z) = \begin{cases} 1 & z > 0 \\ \alpha & z \leq 0 \end{cases}\)
Softmax \(g(z)_i = \frac{e^{z_i}}{\sum_{j} e^{z_j}}\) \(\frac{\partial g(z)_i}{\partial z_j} = g(z)_i(\delta_{ij} - g(z)_j)\)

Casos de uso comunes:

  • ReLU / Leaky ReLU: Opción predeterminada para las capas ocultas; cálculo rápido y mitigación del problema de los gradientes que desaparecen
  • Sigmoide / Tanh: Activaciones clásicas; útiles para salidas acotadas o compuertas en redes LSTM
  • Softmax: Salidas de clasificación multiclase; genera distribuciones de probabilidad

1.10 Gradientes que Desaparecen y Explotan

Un desafío del entrenamiento de redes profundas es que los gradientes pueden desaparecer o explotar al propagarse hacia atrás a través de muchas capas.

Gradientes que desaparecen: Al usar activaciones sigmoide o tanh, las derivadas están acotadas por 1. A medida que multiplicamos muchos de estos términos en la ecuación \(\ref{eq:backprop_recursion}\), el gradiente puede reducirse exponencialmente:

\[ \begin{equation} \|\delta^{(1)}\| \approx \|\delta^{(L)}\| \prod_{l=2}^{L} \|W^{(l)}\| \|g'^{(l)}\| \label{eq:vanishing_gradient} \end{equation} \]

Si \(\|g'^{(l)}\| < 1\) y \(\|W^{(l)}\| < 1\), las primeras capas reciben gradientes diminutos y aprenden muy lentamente.

Soluciones: - Usar activaciones ReLU (la derivada es 0 o 1) - Inicializar los pesos adecuadamente (He, Xavier) - Normalización por lotes - Conexiones residuales (conexiones de salto) - LSTM/GRU para redes recurrentes

Gradientes que explotan: El problema opuesto ocurre cuando los gradientes crecen exponencialmente. Esto es más común en las redes recurrentes.

Soluciones: - Recorte de gradientes - Inicialización cuidadosa de los pesos - Tasas de aprendizaje más bajas

1.11 Perspectiva del Cálculo Matricial

La retropropagación puede expresarse elegantemente mediante cálculo matricial. La clave es seguir cómo cambia la pérdida al perturbar cada parámetro.

Para una pérdida escalar \(L\) y un parámetro matricial \(W\), el gradiente es:

\[ \begin{equation} \left[\frac{\partial L}{\partial W}\right]_{ij} = \frac{\partial L}{\partial W_{ij}} \label{eq:matrix_grad} \end{equation} \]

Aplicando sistemáticamente la regla de la cadena:

\[ \begin{equation} \frac{\partial L}{\partial W^{(l)}} = \frac{\partial L}{\partial z^{(l)}} \frac{\partial z^{(l)}}{\partial W^{(l)}} \label{eq:chain_weight} \end{equation} \]

Como \(z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}\):

\[ \begin{equation} \frac{\partial z^{(l)}}{\partial W^{(l)}} \text{ es tal que } \frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T \label{eq:weight_grad_derivation} \end{equation} \]

Esta perspectiva se generaliza naturalmente a capas convolucionales, capas recurrentes y otras arquitecturas.

1.12 Vista del Gráfico de Cálculo

Los entornos modernos de aprendizaje profundo (PyTorch, TensorFlow) representan los cálculos como grafos acíclicos dirigidos (DAG). Cada nodo es una operación y las aristas representan el flujo de datos.

Paso hacia adelante: Recorrer el grafo desde las entradas hasta las salidas, calculando valores Paso hacia atrás: Recorrerlo en sentido inverso, aplicando la regla de la cadena en cada nodo

Esta abstracción permite:

  • Diferenciación automática (autograd)
  • Grafos computacionales dinámicos
  • Implementación sencilla de arquitecturas complejas
  • Paralelización eficiente en GPU

El algoritmo de retropropagación que hemos descrito es exactamente lo que estos entornos implementan automáticamente cuando se llama a .backward() o tape.gradient().

1.13 Consideraciones Prácticas

Descenso de gradiente en minilotes: En lugar de calcular los gradientes para un ejemplo, normalmente los promediamos sobre un minilote:

\[ \begin{equation} \nabla_\theta \mathcal{L} \approx \frac{1}{B} \sum_{i=1}^{B} \nabla_\theta L(y_i, f(x_i; \theta)) \label{eq:minibatch_gradient} \end{equation} \]

Esto proporciona: - Mejores estimaciones del gradiente (menor varianza) - Cálculos vectorizados eficientes - Un efecto de regularización debido al ruido

Programación de la tasa de aprendizaje: Comenzar con tasas de aprendizaje más altas y reducirlas con el tiempo: - Decaimiento escalonado: reducir la tasa por un factor cada \(N\) épocas - Decaimiento exponencial: \(\eta_t = \eta_0 e^{-kt}\) - Recocido coseno: decaimiento suave siguiendo una curva coseno

Momentum y métodos adaptativos: Los optimizadores modernos amplían el descenso de gradiente básico: - Momentum: Acumular gradientes a lo largo del tiempo para suavizar las actualizaciones - Adam: Tasas de aprendizaje adaptativas por parámetro basadas en los momentos del gradiente - RMSprop: Escalar las tasas de aprendizaje mediante un promedio móvil de las magnitudes de los gradientes

Regularización: Prevenir el sobreajuste añadiendo penalizaciones: - Regularización L2 (decaimiento de pesos): Añadir \(\frac{\lambda}{2}\|\theta\|^2\) a la pérdida - Dropout: Poner a cero neuronas aleatoriamente durante el entrenamiento - Normalización por lotes: Normalizar las activaciones dentro de cada minilote

1.14 Contexto Histórico e Impacto

La retropropagación revolucionó el aprendizaje automático cuando se popularizó en los años 80 1. Aunque la idea central (aplicar la regla de la cadena) es anterior, el trabajo seminal de Rumelhart, Hinton y Williams demostró su eficacia práctica para entrenar redes multicapa 23.

Hitos clave: - Años 60 y 70: Primeros trabajos sobre diferenciación automática y su modo inverso - 1986: Rumelhart, Hinton y Williams 1 popularizan la retropropagación para redes neuronales - Años 90: Dificultades con las redes profundas y los gradientes que desaparecen - 2006: Las redes de creencias profundas de Hinton reavivan el interés - 2012: AlexNet utiliza retropropagación y GPU para ganar ImageNet, dando inicio a la era del aprendizaje profundo 4 - Hoy: La retropropagación permite entrenar redes con miles de millones de parámetros (GPT, BERT, etc.)

La elegancia del algoritmo radica en su simplicidad: es solo la regla de la cadena aplicada sistemáticamente. Sin embargo, esta idea sencilla permite entrenar modelos increíblemente complejos que sustentan la inteligencia artificial moderna 4.

1.15 Más Allá de Redes Anticipadas

Aunque nos hemos centrado en redes de propagación hacia adelante, la retropropagación se generaliza a otras arquitecturas:

Redes neuronales convolucionales (CNN): Retropropagación a través de operaciones de convolución usando convoluciones transpuestas

Redes neuronales recurrentes (RNN): Retropropagación a través del tiempo (BPTT), desplegando la red a lo largo de los pasos temporales

Transformers: Retropropagación a través de mecanismos de autoatención, normalización por capas y conexiones residuales

Redes neuronales de grafos: Paso y agregación de mensajes con retropropagación sobre estructuras de grafos

El principio fundamental sigue siendo el mismo: calcular el paso hacia adelante y después propagar los gradientes hacia atrás usando la regla de la cadena.

1.16 Resumen

La retropropagación es el algoritmo central del aprendizaje profundo y permite calcular gradientes de forma eficiente mediante:

  1. Paso hacia adelante: Calcular las salidas y almacenar los valores intermedios
  2. Paso hacia atrás: Calcular los errores recursivamente usando \(\delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot g'^{(l)}(z^{(l)})\)
  3. Cálculo de gradientes: Usar \(\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T\)
  4. Actualización de parámetros: Aplicar descenso de gradiente o sus variantes

La complejidad del algoritmo, \(O(W)\) (lineal en el número de pesos), hace viable el entrenamiento de redes profundas. Combinada con hardware potente (GPU, TPU) y optimizadores modernos, la retropropagación ha hecho posible la revolución del aprendizaje profundo 23.


  1. David E. Rumelhart, Geoffrey E. Hinton, and Ronald J. Williams. Learning representations by back-propagating errors. Nature, 323(6088):533–536, 1986. doi:10.1038/323533a0. ↩↩↩

  2. Ian Goodfellow, Yoshua Bengio, and Aaron Courville. Deep Learning. MIT Press, 2016. URL: https://www.deeplearningbook.org/. ↩↩

  3. Michael A. Nielsen. Neural Networks and Deep Learning. Determination Press, 2015. URL: http://neuralnetworksanddeeplearning.com/. ↩↩

  4. Yann LeCun, Yoshua Bengio, and Geoffrey Hinton. Deep learning. Nature, 521(7553):436–444, 2015. doi:10.1038/nature14539. ↩↩