English · Español
04 — Modo inverso vs modo directo¶
🇪🇸 Hay dos formas de hacer autodiferenciación: forward-mode (propagar derivadas adelante junto con los valores) y reverse-mode (recorrer hacia atrás desde la pérdida). ML usa reverse-mode porque tiene una salida (la pérdida) y millones de entradas (los pesos). Esta página explica el porqué con una sola tabla.
El planteamiento¶
Tienes una función f: Rⁿ → Rᵐ. Quieres la Jacobiana — la matriz de todas las derivadas parciales ∂fᵢ/∂xⱼ, forma (m, n).
La diferenciación automática te da productos Jacobiana-vector (JVPs) o productos vector-Jacobiana (VJPs) uno a uno. La pregunta es: ¿cuál coincide con la forma de tu problema?
Forward-mode: JVP¶
Computa: (∂f/∂x) · v para un vector de dirección elegido v ∈ Rⁿ. Coste: ~1 forward pass.
Para obtener la Jacobiana completa, ejecutarías forward-mode n veces, con v fijado a cada vector base estándar e₁, e₂, ..., eₙ. Cada ejecución da una columna de la Jacobiana. Coste total: n forward passes.
Mecánica: junto con el valor de cada variable intermedia, también propaga una "tangente" — la derivada de ese intermedio respecto a la dirección de entrada elegida. La tangente se actualiza por regla de la cadena en forward-mode: si c = a · b y conocemos las tangentes ȧ = ∂a/∂x · v_input y ḃ = ∂b/∂x · v_input, entonces ċ = b · ȧ + a · ḃ.
Fácil de implementar. No se necesita grafo. Pero: una dirección de entrada a la vez.
Reverse-mode: VJP¶
Computa: w · (∂f/∂x) para un vector de dirección elegido w ∈ Rᵐ. Coste: ~1 forward + ~1 backward pass.
Para obtener la Jacobiana completa, ejecutarías reverse-mode m veces, con w fijado a cada eᵢ. Cada ejecución da una fila de la Jacobiana. Coste total: m pasos forward+backward.
Mecánica: construir un DAG durante el forward, luego recorrerlo en reverso aplicando derivadas locales — exactamente lo que construimos en §03.
Más difícil de implementar (grafo + topo-sort + closures). Pero: una dirección de salida a la vez.
La decisión¶
Para la Jacobiana de forma (m, n):
- Forward-mode es barato cuando n es pequeño — pocas entradas que variar.
- Reverse-mode es barato cuando m es pequeño — pocas salidas desde las que retro-propagar.
El caso ML¶
En ML, la función es:
m = 1(una pérdida escalar).n = número de parámetros— puede ser millones o miles de millones.
Coste de reverse-mode: ~1 forward + ~1 backward. Total: ~2× el coste del forward, independientemente de n.
Coste de forward-mode: n forward passes. Para un modelo de mil millones de parámetros: mil millones de forward passes por gradiente. Sin esperanza.
Reverse-mode gana por un factor de n/2 para cargas de ML. Por eso cada framework — PyTorch, JAX, TensorFlow, nuestro minigrad — usa reverse-mode por defecto.
Cuándo gana forward-mode¶
- Funciones con pocas entradas y muchas salidas. P. ej., una curva paramétrica 3-D con miles de propiedades computadas.
n=3, m=10000. Forward-mode: 3 forward passes. Reverse-mode: 10000 backward passes. - Derivadas de orden superior. Computar un producto Hessiano-vector (HVP) usa ambos — típicamente reverse-sobre-forward. JAX lo hace fácil; PyTorch lo soporta pero la ergonomía es más torpe.
- Actualizaciones in-place / sin grafo disponible. Forward-mode no necesita DAG. Útil en simuladores físicos donde construir un grafo de cada intermedio es prohibitivo.
Por qué paga "reverse-mode"¶
El DAG. Cada Value intermedio debe mantenerse vivo hasta que se ejecuta el backward, porque el backward lee .data de los padres. Para una red neuronal profunda con millones de activaciones, eso es mucha RAM.
En la Fase 18 introduciremos el checkpointing de activaciones: descartar algunas activaciones intermedias durante el forward, recomputarlas durante el backward. Cuesta ~30% más cómputo, ahorra 50%+ memoria. La técnica solo tiene sentido para autodiferenciación reverse-mode (forward-mode no tiene "memoria de intermedios" porque las tangentes fluyen hacia adelante en sincronía con los valores).
Para la Fase 7, nuestros grafos son diminutos (XOR-MLP tiene quizá 100 nodos en total). Sin preocupaciones de memoria. El punto es sentir el algoritmo.
La imagen única que explica todo¶
Coste para computar la Jacobiana:
forward-mode reverse-mode
n entradas, m salidas: O(n · cost_fwd) O(m · cost_fwd)
ML: n = muchos params, m = 1: O(n) O(1) ← gana reverse-mode
curva paramétrica: n=3, m=much: O(3) O(m) ← gana forward-mode
Esa es toda la elección. Reverse-mode para ML, forward-mode para la forma inusual.
¿Cómo se manifiesta esto en la Fase 7?¶
minigrad.scalar.Value.backward() es reverse-mode. No hay método forward() que propague tangentes. No hay jvp(). Tenemos una herramienta, la adecuada para ML.
Si Borja quiere jugar con forward-mode después (digamos, computando un HVP para análisis de curvatura), la técnica a añadir es "números duales" — una clase que envuelve un par (valor, tangente) y sobrecarga la aritmética para propagar ambos. Es un ejercicio divertido; no está en el alcance del currículo.
Derivadas de orden superior (vista previa, no implementación)¶
Para computar ∂²L/∂a² (una segunda derivada), puedes:
- Reverse-sobre-reverse: llamar
backward(), luego construir un segundo grafo sobre los gradientes mismos, luego llamarbackward()de nuevo. Funciona en teoría; requiere que los closures_backwardproduzcan ellos mismos objetosValue(actualmente actualizan.gradcomo floats crudos). Requiere refactorización. - Reverse-sobre-forward: empujar tangentes hacia adelante, luego backward sobre el resultado. Más barato para HVPs.
- Diferencias finitas: perturbar el parámetro con ε y rehacer backward.
PyTorch soporta (1) con el flag create_graph=True en backward. JAX soporta (2) muy limpiamente. minigrad.scalar no soporta ninguno — mantenemos grad como float. Documenta esto en el BLUEPRINT.
Escollos en el pensamiento de mezcla de modos¶
- No intentes hacer "forward-mode" fijando el grad de todos los parámetros menos uno a cero y viendo la respuesta. Eso es reverse-mode con semillas dispersas; no te da eficiencia forward-mode.
backward()siempre produce gradientes respecto a las hojas. No te da nativamente Jacobianas de intermedio-a-intermedio. Para obtener∂(intermedio)/∂(hoja), llamas aintermedio.backward()(tratando al intermedio como si fuera la pérdida).- Desvincular (
detach) un nodo rompe el flujo de gradientes. El.detach()de PyTorch hace unTensorque no contribuye al backward.minigrad.scalarno incluyedetach()(sin caso de uso al grano escalar). La Fase 8 lo considera.
Recapitulación en un párrafo¶
Forward-mode y reverse-mode son algoritmos duales para computar Jacobianas; el coste de forward-mode escala con el número de entradas, el de reverse-mode con el número de salidas. ML tiene una pérdida escalar y muchos parámetros, así que reverse-mode es la elección universal — coste ~2× el forward, independientemente del número de parámetros. La Fase 7 (y todas las fases posteriores) solo implementan reverse-mode. Forward-mode y derivadas de orden superior son interesantes pero explícitamente fuera de alcance. La imagen única: coste Jacobiano O(min(n, m)) — elige el modo que se alinea con cualquiera de las dimensiones que sea pequeña.
Fin de la teoría de la Fase 7. Pasa a lab/00-value-skeleton.md.