English · Español
Break — Reemplazar += con = en _backward¶
🇪🇸 La ruptura canónica del autograd: cambia
parent.grad += ...porparent.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:
- Compara contra
torch.autograd.gradde PyTorch sobre la misma expresión. Reportaráa.grad = 4,c.grad = -12. La diferencia respecto a tu implementación revela la discrepancia inmediatamente. - Inspecciona
a.gradantes de que se ejecute el último_backward. Si instrumentas_backwardpara imprimira.gradantes y después de la asignación, verása.grad: 10 -> -6— es decir, el valor fue reemplazado, no aumentado. - 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
minigradde la Fase 7) — sus closures_backwardusan+=deliberadamente; leer el fuente tras revertir la ruptura es un ejercicio de confirmación de 5 minutos.