Skip to content

English · Español

00 — Motivación: por qué el autograd escalar es el punto de entrada correcto

🇪🇸 La retropropagación (backprop) tiene fama de oscura. Lo es… si la aprendes desde transformers gigantes. Si la aprendes desde un único número — una Value que envuelve un float y guarda quién es su padre — la regla de la cadena deja de ser magia y se vuelve un recorrido inverso por un grafo. Esta fase mete esa intuición en hueso.


Lo que habrás construido al terminar esta fase

Una clase Python Value que envuelve un único float de Python. Puedes hacer:

a = Value(2.0)
b = Value(-3.0)
c = Value(10.0)
d = a * b + c.tanh()
d.backward()

print(a.grad)   # ∂d/∂a, computado automáticamente
print(b.grad)   # ∂d/∂b
print(c.grad)   # ∂d/∂c

…y los tres gradientes son correctos, computados por un recorrido topológico inverso de un DAG que Value construyó durante el forward pass. Sin NumPy. Sin PyTorch. ~150 líneas de código del propio Borja.

Esto es, en miniatura, exactamente lo que hace PyTorch. Las diferencias son escala (tensores en vez de floats), rendimiento (kernels C/CUDA) y ergonomía (módulos, optimizadores). La idea es idéntica.

Por qué "constrúyelo una vez al grano escalar"

Hay un camino pedagógico común que dice "usa PyTorch primero, entiéndelo después". Funciona para usuarios que nunca necesitarán entender la retropropagación. Para Borja (y para el contrato pedagógico de este currículo — CLAUDE.md §0.4), es el camino equivocado. La razón:

El autograd tensorial tiene tres fuentes de complejidad apiladas una sobre otra.

  1. El algoritmo de autograd en sí (DAG, recorrido inverso, regla de la cadena).
  2. La mecánica de NumPy (formas, broadcasting, strides — la Fase 6 los cubrió).
  3. Derivaciones de gradientes por operación (backward de matmul, backward de softmax — álgebra lineal no trivial).

Si depuras un gradiente equivocado en PyTorch el primer día, no sabes cuál de los tres te está mordiendo. ¿Fue el topo-sort? ¿El reverso del broadcast? ¿Derivaste mal el backward de matmul?

El autograd escalar tiene solo la primera fuente de complejidad. Sin formas (todo es un float). Sin broadcasting. Sin derivaciones por operación más allá de lo que cabe en una sola hoja de papel (+ - * / etc. son triviales). Todo lo que va mal en esta fase es el algoritmo de autograd. Aprenderás exactamente cómo se ve eso — cómo se siente la clase correcta de bug, qué clase de test lo captura.

Cuando la Fase 8 añada NumPy y derivadas con forma tensorial, esas serán las nuevas fuentes de bugs. Podrás aislarlos porque ya confías en el algoritmo en sí.

Qué es la retropropagación, en dos párrafos

Forward pass: escribes d = a * b + c.tanh(). Python evalúa esto de izquierda a derecha (precedencia de operadores aparte) y produce un número. Por el camino, cada operador crea un nodo Value que recuerda a sus padres y una etiqueta de operación. Cuando se asigna d, existe un DAG en memoria: d sabe que fue hecho de "a*b" y "c.tanh()" vía suma; el nodo * sabe que fue hecho de a y b; el nodo tanh sabe que fue hecho de c. Cinco objetos Value, cinco aristas, una forma.

Backward pass: d.backward() hace dos cosas. Primero, recorre el DAG desde d hacia afuera (padres-de-padres-de-…) para producir un orden topológico. Luego recorre ese orden en reverso, empezando por fijar d.grad = 1.0 (la semilla: ∂d/∂d = 1), y en cada nodo aplica una pequeña regla local que añade la contribución de este nodo a los atributos .grad de sus padres. Al final del recorrido inverso, cada nodo — incluyendo a, b, c — tiene su .grad fijado en la derivada parcial de d respecto a él.

La "pequeña regla local" en cada nodo es la derivada local de esa operación. Para la multiplicación c = a*b: el gradiente de c se multiplica por b para contribuir al gradiente de a, y por a para contribuir al gradiente de b — porque ∂c/∂a = b y ∂c/∂b = a. Cada operación tiene una regla así. Hay aproximadamente diez y todas caben en una página.

Eso es todo. Eso es toda la retropropagación, siempre, en cualquier framework, a cualquier escala.

Por qué esto escala (y por qué la Fase 8 es "más de lo mismo")

El Value escalar que construyes en la Fase 7 se reemplazará en la Fase 8 por un Tensor que envuelve un array NumPy. Las cinco cosas que son lo mismo:

  • Estructura del DAG (nodos + padres + etiqueta de operación).
  • El forward construye el DAG.
  • Backward = topo-sort + recorrido inverso.
  • Cada operación contribuye una regla local.
  • _backward es un closure que captura a los padres y aplica la regla.

Las cinco cosas que cambian:

  • data es un ndarray, no un float.
  • grad es un ndarray con la misma forma que data, no un float.
  • Las reglas locales se vuelven con forma tensorial (p. ej., backward de matmul).
  • El broadcasting necesita ser invertido en el backward (suma a lo largo de los ejes de broadcast).
  • Los tests por operación necesitan gradcheck (diferencias finitas) además de los contrastes con PyTorch.

La Fase 7 clava la primera lista. La Fase 8 añade la segunda. La división de fase es deliberada: una fuente de complejidad a la vez.

Por qué un proyecto pequeño, incluso al grano escalar

Value son ~150 líneas. El entrenamiento del XOR-MLP en experiments/07-train-xor/ son ~50 líneas. El visualizador del grafo son ~30. Los tests son ~200. Total: ~430 líneas.

¿Es eso "pequeño"? Comparado con PyTorch (~3M líneas), sí. Comparado con micrograd (la referencia de Karpathy, ~100 líneas de Value), es del mismo orden. El sentido de construirlo pequeño es: en ningún momento tienes una caja negra. Cada línea que escribes es tuya; cada línea que lees es corta. Cuando la retropropagación produce un número del que desconfías, puedes recorrerlo con pdb y ver, operación a operación, exactamente qué pasó.

El ejemplo XOR también es deliberadamente pequeño. XOR no es una tarea seria de aprendizaje — es un dataset de 4 puntos que un MLP diminuto puede memorizar en segundos. El sentido es ver que la retropropagación, de extremo a extremo, funciona: forward, pérdida, backward, actualización de parámetros, repetir. Cuando la pérdida pasa de 0.7 a 0.001 en 500 pasos usando solo tu clase Value, el proyecto está desbloqueado. La Fase 8 puede empezar.

Qué pedagogía honra esta fase

CLAUDE.md §0.2: Borja escribe la implementación; Claude andamia.

Eso significa:

  • Claude escribe src/minigrad/scalar/BLUEPRINT.md — propósito, API, alternativas, anti-objetivos.
  • Claude escribe theory/ — derivaciones, ejemplos trabajados.
  • Claude escribe lab/ — enunciados de problemas con TODOs y restricciones, sin respuestas.
  • Claude escribe stubs de tests fallidos en tests/test_scalar_autograd.py — lista de operaciones a cubrir, tolerancia esperada, oráculo de comparación.
  • Borja escribe src/minigrad/scalar.py. El cuerpo real de la clase.
  • Borja rellena los cuerpos de los tests (Claude proporcionó los comentarios).
  • Borja decide cuestiones de diseño como "¿tanh es nativo o vía exp?" (los defaults están documentados, pero la decisión es suya).

Si Borja se atasca, el camino es: releer la teoría, escribir un test más pequeño, mirar el ejemplo trabajado en theory/03, preguntar al subagente math-reviewer. No "mirar la solución".

solutions/ se escribirá en la apertura de fase, después de que las decisiones de API de la fase previa de Borja sean visibles (según addendum §A12). Hasta entonces, solutions/ está vacío.

Recapitulación en un párrafo

El autograd escalar es el contexto más pequeño posible en el que la retropropagación está plenamente viva. Al envolver un único float en una clase Value que registra a sus padres y un closure de derivada local, el forward pass construye un DAG y el backward pass — un recorrido topológico inverso aplicando la regla de la cadena — rellena .grad en cada nodo. ~150 líneas, sin NumPy, sin PyTorch. Cuando la Fase 8 lo lleva a tensores, el algoritmo no cambia; solo el tipo de dato, las derivadas por operación y la inversión del broadcasting son nuevos. La Fase 7 clava el algoritmo. Constrúyelo una vez al grano escalar, y nunca más volverás a preguntarte qué está haciendo .backward() bajo el capó.


Siguiente: 01-computation-graphs.md