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.
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.
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.
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.
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?"
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
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].
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:
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 ✅
Haz clic en "Siguiente paso" para ver cómo se propagan los gradientes en el grafo L = (a×b) + a.
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).backward() recorre el grafo en reversa aplicando la regla de la cadena acumulando gradientes en cada nodo.