Skip to content

English · Español

Lab 00 — Derivar ∂ softmax/∂x y ∂ CE/∂x en papel, luego verificar numéricamente

🇪🇸 La página más importante de la Fase 4. Derivas a mano la Jacobiana de softmax (diag(p) - p p^T) y el resultado limpio softmax(x) - one_hot(y) para cross-entropy. Después, comparas con diferencias finitas.

Objetivo

Dos derivaciones en papel, luego una verificación numérica de una pantalla. Al final no deberías tener que "buscar" ∂ CE/∂x nunca más — es solo softmax(x) - one_hot(y), y habrás construido ese resultado tú mismo.

Planteamiento

  • Una página de cuaderno en blanco.
  • numpy, el log-sum-exp de la Fase 05.
  • Un vector de logits sintético de 5 elementos z = np.array([2.0, 1.0, 0.5, -1.0, 0.0]).

Tareas

Parte A — Derivar en papel (sin código)

  1. Derivada de softmax. Partiendo de p_i = exp(z_i) / Σ_k exp(z_k), deriva ∂p_i / ∂z_j tanto para i = j como para i ≠ j. Llega a:

$\(\frac{\partial p_i}{\partial z_j} = p_i (\delta_{ij} - p_j)\)$

Escribe la forma matricial explícitamente: J = diag(p) - p p^T.

  1. Cross-entropy + softmax compuestas. Sea L = -log p_y donde y es el índice de la clase verdadera. Deriva ∂L / ∂z_j. Pista: usa ∂L/∂z_j = Σ_i (∂L/∂p_i)(∂p_i/∂z_j). La mayoría de términos se anulan (∂L/∂p_i = 0 para i ≠ y; = -1/p_y para i = y). Tras sustitución y simplificación, deberías llegar a:

$\(\frac{\partial L}{\partial z_j} = p_j - \mathbb{1}[j = y]\)$

Es decir: ∂CE/∂z = softmax(z) - one_hot(y). Bello.

  1. Comprobación en papel. Para z = [2, 1, 0] e y = 0, calcula p = softmax(z) numéricamente a mano (solo las proporciones; no calcules los valores exp). Verifica que p_0 - 1 < 0 y p_1, p_2 > 0 — el gradiente empuja z_0 hacia arriba y z_1, z_2 hacia abajo, que es lo que queremos.

Parte B — Verificación numérica

  1. Implementa softmax (usa el log-sum-exp de la Fase 05):
def softmax(z):
    z_max = z.max()
    e = np.exp(z - z_max)
    return e / e.sum()
  1. Calcula la Jacobiana analíticamente:
def softmax_jacobian(z):
    p = softmax(z)
    return np.diag(p) - np.outer(p, p)
  1. Calcula la Jacobiana numéricamente mediante diferencias finitas centradas:
def softmax_jacobian_fd(z, h=1e-5):
    n = len(z)
    J = np.zeros((n, n))
    for j in range(n):
        e = np.zeros(n); e[j] = h
        J[:, j] = (softmax(z + e) - softmax(z - e)) / (2 * h)
    return J
  1. Compara. Para z = [2.0, 1.0, 0.5, -1.0, 0.0]:
J_analytical = softmax_jacobian(z)
J_numerical = softmax_jacobian_fd(z)
max_err = np.max(np.abs(J_analytical - J_numerical))
assert max_err < 1e-7, f"Jacobian mismatch: {max_err}"
  1. Mismo ejercicio para CE:
def ce_loss(z, y):
    return -np.log(softmax(z)[y])

def ce_grad(z, y):
    p = softmax(z)
    g = p.copy()
    g[y] -= 1.0
    return g

def ce_grad_fd(z, y, h=1e-5):
    g = np.zeros(len(z))
    for j in range(len(z)):
        e = np.zeros(len(z)); e[j] = h
        g[j] = (ce_loss(z + e, y) - ce_loss(z - e, y)) / (2 * h)
    return g

for y in range(5):
    diff = np.max(np.abs(ce_grad(z, y) - ce_grad_fd(z, y)))
    assert diff < 1e-7
  1. Imprime la Jacobiana. Para refuerzo visual, imprime J_analytical como matriz y verifica a ojo que la diagonal es p_i (1 - p_i) (máximo en el p más grande) y los off-diagonales son -p_i p_j (negativos pequeños).

Entregable

learners/borja/phase-04/lab-00-softmax-gradient.md que contiene: - Una foto o transcripción de la derivación en papel (ambas partes de la Parte A). - La salida del script de verificación (los mensajes assert o los máximos impresos). - Una reflexión de 3 frases: ¿la derivación se sintió mecánica o reveladora? ¿Dónde te atascaste?

Aceptación

  • Ambas derivaciones completadas en papel antes de escribir cualquier código.
  • softmax_jacobian coincide con diferencias finitas dentro de 1e-7.
  • ce_grad coincide con diferencias finitas dentro de 1e-7.
  • Reflexión escrita.

Escollos

  • Saltarse la parte en papel. El código es trivial una vez interiorizada la derivación; el valor de este lab es la derivación. Hazlo en papel.
  • Diferencias hacia adelante en lugar de centradas. Hacia adelante es O(h); centradas es O(h²). Con h = 1e-5, hacia adelante da ~1e-5 de error, centradas ~1e-10. Usa centradas.
  • h demasiado pequeño. h = 1e-12 desencadena cancelación catastrófica; obtendrás gradientes peores. 1e-5 es el punto óptimo para fp64; para fp32, usa 1e-3.
  • Usar np.log(softmax(z)) para cross-entropy. Numéricamente inestable. Usa log_softmax (forma logsumexp) de la Fase 05.
  • Olvidar que la Jacobiana de softmax es simétrica. diag(p) - p p^T es simétrica; si tu código produce una matriz no simétrica, tienes un bug.

Siguiente: 01-jacobian-by-hand.md