Skip to content

English · Español

Break — Reemplazar += con = en _backward

🇪🇸 La ruptura canónica del autograd: cambia parent.grad += ... por parent.grad = .... Los tests lineales pasan; el test de la "diamante" falla con un número que parece "casi correcto", lo que lo hace especialmente didáctico.

Objetivo: closures _backward de Value en src/minigrad/scalar.py, o tu equivalente escrito a mano.

Hipótesis

El aprendiz predice: "Reemplazar += con = hará que cualquier cómputo donde una variable aparezca solo una vez siga funcionando (el closure se ejecuta una vez, la asignación es la única contribución), pero producirá silenciosamente gradientes erróneos en patrones de diamante (la variable aparece dos veces) — manteniendo solo la última contribución. El test de diamante de theory/03-worked-backprop.md es el caso de fallo canónico."

La ruptura

En cada closure _backward, cambia:

 def _backward():
-    a.grad += local_grad_a * out.grad
-    b.grad += local_grad_b * out.grad
+    a.grad = local_grad_a * out.grad
+    b.grad = local_grad_b * out.grad

Aplica esto a todas las operaciones (add, mul, sub, etc.) — las rupturas parciales son peores que las completas porque hacen el diagnóstico aún más difícil.

Procedimiento de ejecución

El test de diamante de la teoría §03:

uv run python -c "
from src.minigrad.scalar import Value

a = Value(2.0, label='a')
b = Value(3.0, label='b')
c = Value(4.0, label='c')

# L = (a*b + c) * (a - c)
L = (a * b + c) * (a - c)
L.backward()

print(f'a.grad = {a.grad}  (expected 4)')
print(f'b.grad = {b.grad}  (expected -4)')
print(f'c.grad = {c.grad}  (expected -12)')
"

Para comparación, un test lineal (no diamante):

uv run python -c "
from src.minigrad.scalar import Value

x = Value(3.0)
y = Value(5.0)
# z = x*y + x — x appears twice but in a simple shape; still a diamond.
# Use a strictly linear chain instead:
z = x * y          # x appears ONCE
z.backward()
print(f'x.grad = {x.grad}  (expected 5)')
print(f'y.grad = {y.grad}  (expected 3)')
"

Modo de fallo esperado

Con la ruptura aplicada:

Linear (one occurrence):
  x.grad = 5         <-- correct
  y.grad = 3         <-- correct

Diamond:
  a.grad = -6        <-- WRONG, should be 4 (kept only the last contribution from ab._backward)
  b.grad = -4        <-- correct (b appears once)
  c.grad = -2        <-- WRONG, should be -12 (kept only the last from e._backward)

Sutil: a.grad = -6 es la contribución del camino (a·b + c) únicamente; el camino (a - c) fue sobrescrito. c.grad = -2 es la contribución de e = ab + c; la contribución de f = a - c fue sobrescrita.

Dos de tres gradientes están mal, pero los valores parecen plausibles (no nan, no enormes). El bug llega a producción si no tienes un test de diamante.

Diagnóstico

Solo desde los logs:

  1. Compara contra torch.autograd.grad de PyTorch sobre la misma expresión. Reportará a.grad = 4, c.grad = -12. La diferencia respecto a tu implementación revela la discrepancia inmediatamente.
  2. Inspecciona a.grad antes de que se ejecute el último _backward. Si instrumentas _backward para imprimir a.grad antes y después de la asignación, verás a.grad: 10 -> -6 — es decir, el valor fue reemplazado, no aumentado.
  3. Ejecuta el test estándar de theory/03. Ese test es el diamante computado a mano; existe precisamente para cazar este bug.

Lección

Todo el sentido de la AD en modo inverso es que cada aparición de una variable en el grafo de cómputo contribuye un término a su gradiente (regla de la cadena vía suma sobre caminos). La estructura de datos que convierte la "suma sobre caminos" en código es += sobre .grad. Reemplaza += con = y pierdes la semántica de suma sobre caminos. Las matemáticas se siguen aplicando, pero solo el último camino contribuye.

Esta es también la razón por la que zero_grad debe llamarse entre pasos de entrenamiento: el += que habilita la acumulación de diamante también significa que los gradientes del paso N persisten al paso N+1 a menos que se limpien explícitamente. El mismo operador, dos modos de fallo.

Referencias

  • Griewank & Walther, Evaluating Derivatives, §3 (el enunciado formal de la AD en modo inverso como suma sobre caminos).
  • Karpathy, micrograd (la base de código sobre la que se modela minigrad de la Fase 7) — sus closures _backward usan += deliberadamente; leer el fuente tras revertir la ruptura es un ejercicio de confirmación de 5 minutos.