Capítulo 3

Backpropagation

3.1 Diferenciación Automática

El backpropagation es la regla de la cadena aplicada eficientemente a redes de computación. Dos modos:

Forward Mode (AD)

Propaga derivadas hacia adelante con las entradas. Eficiente para funciones con pocas entradas y muchas salidas.

Reverse Mode (AD)

El backpropagation. Calcula el gradiente propagando hacia atrás desde la salida. Eficiente para ML: muchas entradas, una salida (pérdida).

3.2 Red Simple: Diagrama de Computación

Para f(x)=σ(wx+b)f(x) = \sigma(wx + b) donde σ(z)=1/(1+ez)\sigma(z) = 1/(1+e^{-z}):

  1. Forward: z=wx+b,  a=σ(z)z = wx + b, \; a = \sigma(z)
  2. Backward: LaLz=Laσ(z)Lw=Lzx\frac{\partial L}{\partial a} \to \frac{\partial L}{\partial z} = \frac{\partial L}{\partial a} \sigma'(z) \to \frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} x
python
import numpy as np

# Forward + Backward manual
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def sigmoid_prime(z):
    s = sigmoid(z)
    return s * (1 - s)

x, w, b = 2.0, 0.5, 0.1
y_true = 1.0  # etiqueta

# Forward
z = w * x + b
a = sigmoid(z)
L = - (y_true * np.log(a) + (1 - y_true) * np.log(1 - a))  # BCE

# Backward
dL_da = -(y_true / a) + ((1 - y_true) / (1 - a))
da_dz = sigmoid_prime(z)
dz_dw = x
dz_db = 1.0

dL_dz = dL_da * da_dz
dL_dw = dL_dz * dz_dw
dL_db = dL_dz * dz_db

print(f"Pérdida: {L:.4f}")
print(f"∂L/∂w = {dL_dw:.4f}")
print(f"∂L/∂b = {dL_db:.4f}")

# Con autograd (JAX)
# import jax.numpy as jnp
# from jax import grad

3.3 Backprop en una Red Multicapa

Para una red con capas ocultas, el gradiente se propaga capa por capa usando la regla de la cadena. Cada capa tiene un paso forward y backward.

python
import numpy as np

class RedNeuronal:
    def __init__(self, n_input, n_hidden, n_output):
        self.W1 = np.random.randn(n_input, n_hidden) * 0.01
        self.b1 = np.zeros(n_hidden)
        self.W2 = np.random.randn(n_hidden, n_output) * 0.01
        self.b2 = np.zeros(n_output)

    def forward(self, X):
        self.z1 = X @ self.W1 + self.b1
        self.a1 = np.maximum(0, self.z1)  # ReLU
        self.z2 = self.a1 @ self.W2 + self.b2
        # Softmax implícito para clasificación
        exp = np.exp(self.z2 - self.z2.max(axis=1, keepdims=True))
        self.a2 = exp / exp.sum(axis=1, keepdims=True)
        return self.a2

    def backward(self, X, y, lr=0.01):
        m = X.shape[0]
        # Gradiente capa de salida (cross-entropy + softmax)
        dz2 = self.a2 - y
        dW2 = self.a1.T @ dz2 / m
        db2 = dz2.mean(axis=0)
        # Gradiente capa oculta
        da1 = dz2 @ self.W2.T
        dz1 = da1 * (self.z1 > 0)  # derivada de ReLU
        dW1 = X.T @ dz1 / m
        db1 = dz1.mean(axis=0)
        # Actualizar
        self.W2 -= lr * dW2
        self.b2 -= lr * db2
        self.W1 -= lr * dW1
        self.b1 -= lr * db1

# Ejemplo: XOR
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([[0],[1],[1],[0]])
y_onehot = np.eye(2)[y.flatten()]

red = RedNeuronal(2, 4, 2)
for i in range(1000):
    red.forward(X)
    red.backward(X, y_onehot, lr=0.5)

pred = red.forward(X).argmax(axis=1)
print(f"Predicciones: {pred}")
print(f"Reales:       {y.flatten()}")

3.4 Frameworks de Diferenciación Automática

JAX

grad(f)\texttt{grad(f)} devuelve la función gradiente. Composición: grad(grad(f))\texttt{grad(grad(f))} para segunda derivada.

PyTorch

tensor.backward()\texttt{tensor.backward()} computa gradientes vía autograd. Cada tensor guarda su .grad\texttt{.grad}.

TensorFlow/Keras

GradientTape\texttt{GradientTape} graba operaciones para diferenciación automática backwards.