Skip to content

English · Español

Lab 02 — Log-sum-exp y entropía cruzada estable desde logits

Lee theory/04-log-sum-exp-and-stability.md. No consultes solutions/.

Objetivo

Implementar logsumexp, log_softmax y cross_entropy_from_logits con disciplina completa de estabilidad numérica. Demostrar que las implementaciones ingenuas fallan en entradas adversariales mientras que las estables tienen éxito.

Configuración

Continúa en src/phase05/probability.py.

Tareas

Tarea 1 — implementaciones ingenuas (para que las veas fallar)

Implementa primero las versiones ingenuas:

def logsumexp_naive(z): return np.log(np.exp(z).sum())
def log_softmax_naive(z): return np.log(np.exp(z) / np.exp(z).sum())
def cross_entropy_naive(z, y_star): return -np.log(np.exp(z) / np.exp(z).sum())[y_star]

Pruébalas sobre las siguientes entradas y documenta qué ocurre:

Entrada \(z\) Resultado esperado Resultado ingenuo
[0, 0, 0] sensato debería funcionar
[1, 2, 3] sensato debería funcionar
[1000, 1001, 1002] debería ser sensato pero no lo será overflow → inf / NaN
[-1000, -999, -998] debería ser sensato pero no lo será underflow → 0 → log -inf

Tarea 2 — logsumexp estable

Implementa la versión estable (restar el máximo antes de exp). Vuelve a ejecutar las 4 entradas de la Tarea 1; las 4 deberían producir ahora salidas finitas y correctas. Verifica contra scipy.special.logsumexp.

Tarea 3 — log_softmax estable

Mismo ejercicio para log_softmax. Referencia: scipy.special.log_softmax.

Tarea 4 — cross_entropy_from_logits estable

def cross_entropy_from_logits(z, y_star):
    """Stable CE from raw logits. Equivalent to PyTorch's F.cross_entropy on a single example."""
    return -log_softmax(z)[y_star]

Verifica sobre un pequeño batch sintético.

Tarea 5 — property tests

Añade en tests/test_phase05_logsumexp.py:

  1. Invarianza por desplazamiento. Para cualquier \(c \in \mathbb{R}\): logsumexp(z + c) == logsumexp(z) + c dentro de tolerancia.
  2. Invarianza por desplazamiento del softmax. Para cualquier \(c\): log_softmax(z + c) es igual a log_softmax(z) (porque la constante se cancela).
  3. Sanidad de reducción. log_softmax(z).sum() == log_softmax([z, z]).sum() / 2 * 2 — es decir, el resultado está bien definido por fila.
  4. Paridad con referencia. Compara contra scipy.special.log_softmax en una batería de entradas (uniforme, puntiaguda, grande, pequeña, negativa).

Tarea 6 — mide velocidad

logsumexp sobre forma (B, V) = (64, 600):

  1. Mide el tiempo de la versión estable en NumPy.
  2. Mide scipy.special.logsumexp.
  3. Mide la versión ingenua rota (sólo por contexto — aunque produciría NaN en logits reales, es una comparación útil sobre entradas seguras).

Guarda las mediciones en experiments/<date>-phase-05-logsumexp/timings.csv.

Aceptación

  • Las 4 entradas de la Tarea 1 documentadas (la ingenua falla como se predijo).
  • Las implementaciones estables pasan sobre las 4 entradas.
  • Los property tests pasan.
  • Paridad con referencia de scipy dentro de 1e-12.
  • Tiempos capturados.

Escollos esperados

  • np.exp(1002) == np.inf en float64; verás RuntimeWarning: overflow encountered in exp — ese es el punto. No silencies el warning; es diagnóstico.
  • np.log(0.0) == -np.inf; la multiplicación río abajo por 0 da NaN. La versión estable evita esto por completo al no calcular nunca np.log de exponentes subfluidos.
  • Al restar el máximo, vigila la semántica de ejes: z.max(axis=-1, keepdims=True) para z en batch.
  • El cross_entropy_from_logits estable está fusionado — nunca calcules log-softmax luego indexes luego log; sólo -log_softmax(z)[y_star]. F.cross_entropy de PyTorch hace la misma fusión bajo el capó.

Siguiente: 03-calibration.md