Lección 7

Autograd: la clase Value

El corazón de train2.py. La clase Value es un número normal, pero con un superpoder: recuerda exactamente cómo fue calculado, para poder propagar gradientes automáticamente.

1. El GPS que puede deshacer el camino

Un GPS normal sabe dónde estás ahora. Pero también recuerda cada giro que tomaste para llegar aquí. Si necesitas "deshacer" el recorrido, puede guiarte de vuelta al origen paso por paso.

La clase Value hace algo parecido con los cálculos. No solo guarda el resultado numérico — guarda de dónde vino ese número. Y cuando llamas a backward(), recorre el camino en reversa propagando gradientes.

🗺️ Value en una frase: es un número con memoria. data es el número actual. grad es cuánto cambia el resultado final si este número cambia. _children son los números que lo produjeron. _local_grads son los gradientes locales de las operaciones.

2. Los cuatro campos de un Value

Anatomía · Un objeto Value
Anatomía de la clase Value con sus cuatro campos Caja central mostrando data, grad, _children y _local_grads de un objeto Value con valor 6. Value data 6.0 ← el número grad 0.0 ← empieza en 0 _children [Value(2), Value(3)] _local_grads [3.0, 2.0] ← d(a×b)/da=b, d/db=a

Este Value representa c = a × b = 2 × 3 = 6. Los gradientes locales son 3.0 (respecto a a) y 2.0 (respecto a b) porque d(a×b)/da = b = 3 y d(a×b)/db = a = 2.

¿Qué es un gradiente local?

El gradiente local de una operación respecto a uno de sus inputs es: "si ese input sube 1, ¿cuánto sube el output de esta operación?"

✍️ Gradientes locales de operaciones básicas
Para c = a × b:
  Si a sube 1, c sube en b  →  gradiente local de c respecto a a = b
  Si b sube 1, c sube en a  →  gradiente local de c respecto a b = a

Con a=2, b=3:
  gradiente local respecto a a = 3
  gradiente local respecto a b = 2

Para L = c + a:
  Si c sube 1, L sube 1  →  gradiente local respecto a c = 1
  Si a sube 1, L sube 1  →  gradiente local respecto a a = 1

Para y = exp(x):
  gradiente local = exp(x) = y  (el exponencial se deriva a sí mismo)

Para y = log(x):
  gradiente local = 1/x

Para y = ReLU(x):
  gradiente local = 1 si x > 0, else 0
      

3. Las operaciones que se registran

La clase Value sobreescribe los operadores de Python (+, *, etc.) para que cada operación no solo calcule el resultado, sino que también guarde los gradientes locales.

class Value:
  __slots__ = ('data', 'grad', '_children', '_local_grads')

  def __init__(self, data):
      self.data = data
      self.grad = 0.0          # empieza en 0
      self._children = []     # de dónde viene
      self._local_grads = []  # gradientes locales

  def __mul__(self, other):
      out = Value(self.data * other.data)
      out._children = [self, other]
      out._local_grads = [other.data, self.data]  # d/da = b, d/db = a
      return out

  def __add__(self, other):
      out = Value(self.data + other.data)
      out._children = [self, other]
      out._local_grads = [1.0, 1.0]  # d(a+b)/da = 1, d(a+b)/db = 1
      return out

Cuando escribes c = a * b, Python llama a a.__mul__(b) que crea un nuevo Value con data=6, guarda que vino de [a, b] y que los gradientes locales son [b.data, a.data] = [3, 2].

4. backward(): el recorrido al revés

Cuando llamas a L.backward(), el método recorre el grafo en orden topológico inverso (de L hacia los inputs) y propaga los gradientes:

✍️ backward() paso a paso para L = (a×b) + a, a=2, b=3
Llamamos: L.backward()

Paso 0: L.grad = 1.0
  (el output siempre empieza con gradiente 1)

Paso 1: Propagamos desde L hacia sus hijos [c, a]:
  c.grad += L.grad × 1.0 = 1.0 × 1.0 = 1.0
  a.grad += L.grad × 1.0 = 1.0 × 1.0 = 1.0  ← primer aporte de 'a'

Paso 2: Propagamos desde c hacia sus hijos [a, b]:
  a.grad += c.grad × b.data = 1.0 × 3 = 3.0  ← segundo aporte de 'a'
  b.grad += c.grad × a.data = 1.0 × 2 = 2.0

Resultado final:
  a.grad = 1.0 + 3.0 = 4.0  ✅ significa: si a sube 1, L sube 4
  b.grad = 2.0             ✅ significa: si b sube 1, L sube 2

Verificación manual:
  L(a=2, b=3) = (2×3) + 2 = 8
  L(a=3, b=3) = (3×3) + 3 = 12  →  L subió 4 cuando a subió 1 ✅
  L(a=2, b=4) = (2×4) + 2 = 10  →  L subió 2 cuando b subió 1 ✅
      

🎮 Pruébalo: visualiza el backward

Haz clic en "Siguiente paso" para ver cómo se propagan los gradientes en el grafo L = (a×b) + a.

5. Lo que aprendiste

En esta lección:
  • Value es un número con 4 campos: data (el número), grad (el gradiente), _children (de dónde vino) y _local_grads (derivadas de la operación).
  • Cada operación (+, ×, exp, log) guarda automáticamente los gradientes locales.
  • backward() recorre el grafo en reversa aplicando la regla de la cadena acumulando gradientes en cada nodo.
  • Si un nodo aparece múltiples veces (como 'a' en L = a×b + a), su gradiente es la suma de todos los aportes.