English · Español
Lab 03 — Gradcheck por diferencias finitas para el autograd escalar¶
Prerrequisito: lab 01 (
Valuey operaciones implementadas). Opcional pero recomendado: lab 02 (el MLP entrenado da expresiones no triviales para gradcheck). Objetivo: construir una utilidad de gradcheck de 30 líneas que compara los gradientes analíticos de tuminigradcontra diferencias finitas centradas sobre una batería de expresiones. El entregable es un CLI que falla ruidosamente cuando el backward de cualquier operación no cuadra.Tiempo estimado: 60–90 minutos. Corre en segundos en el i5-8250U.
§1 Por qué existe este lab¶
El lab 01 testeó cada operación contra un gradiente esperado derivado a mano. El lab 02 entrenó un MLP y confió en que la pérdida bajando significa que los gradientes son correctos. Ambos son útiles pero ninguno captura un bug sutil de signo en una operación que no testeaste a mano.
Un gradcheck es la defensa más barata y general contra esa clase de bug: compara el gradiente analítico df/dx (tu Value._backward) contra una estimación numérica (f(x + ε) - f(x - ε)) / (2ε). Deben coincidir dentro de una pequeña tolerancia. Si no, el backward de tu operación está mal — o tu forward, o tu topo-sort, o tu acumulación +=.
PyTorch incluye exactamente esta utilidad (torch.autograd.gradcheck). Construirás una versión escalar diminuta de ella.
§2 Lo que produces¶
Un único archivo src/minigrad/gradcheck.py (~50 líneas), con una función pública:
def gradcheck(
f: Callable[..., Value],
inputs: tuple[Value, ...],
*,
eps: float = 1e-5,
atol: float = 1e-4,
rtol: float = 1e-3,
) -> bool:
"""Return True iff every input's analytic gradient matches the
central-difference numerical gradient within tolerance.
Raises AssertionError with a diagnostic message on failure.
"""
Más tests/test_gradcheck.py ejercitando cada operación del lab 01 más tres expresiones compuestas:
- El diamante de theory/03: L = (a*b + c) * (a - c).
- Una cadena: L = relu(a * b + c) (usa relu si la implementaste; si no, usa tanh).
- Una división: L = a / (b + 1.0) para cazar bugs de signo en __truediv__.
§3 Pasos¶
- Esboza el algoritmo en papel primero. Para cada input
x_i: - Guarda
x_i.data. Fíjalo ax_i.data + ε, ejecutaf(inputs), registraf_plus. Restaura. - Fija
x_i.data = x_i.data - ε, ejecutaf(inputs), registraf_minus. Restaura. - Gradiente numérico:
(f_plus - f_minus) / (2ε). - Pon a cero todos los
.grad, ejecutaf(inputs).backward(). Gradiente analítico:x_i.grad. - Compara con
abs(num - analytic) <= atol + rtol * abs(num). -
En fallo, imprime: índice del input, esperado (numérico), obtenido (analítico), diff absoluta, diff relativa.
-
Implementa. Anota tipos. ~50 líneas.
-
Escribe los casos de test. Cada test crea
Values frescos, define unfpequeño, llamagradcheck, compruebaTrue. Para cada operación en tu biblioteca, escribe al menos un test de gradcheck. -
Rompe y confirma. Introduce intencionalmente un bug de signo en
__sub__._backward(p. ej.,a.grad += out.grad, faltando el-1para el operando derecho). Ejecuta gradcheck; confirma que falla con un mensaje de error claro. Luego revierte.
§4 Condiciones de parada¶
-
uv run pytest tests/test_gradcheck.pypasa (todos los gradchecks de operaciones + las tres expresiones compuestas). -
mypy --strict src/minigrad/gradcheck.pypasa. -
ruff check src/minigrad/gradcheck.pypasa. - Ejecutaste el experimento del "bug de signo intencional", confirmaste que gradcheck lo cazó, y revertiste. Documenta el mensaje del bug que viste en
learners/borja/phase-07/notes/gradcheck.md. - Escribiste un párrafo corto (4-6 líneas) explicando por qué
eps = 1e-5es un default razonable y qué pasa si eliges1e-12o1e-2(cancelación vs truncamiento). - Commit:
lab: phase-07 add scalar gradcheck CLI and tests.
§5 Pistas¶
- Restaura
.datatras cada perturbación ε — si lo olvidas, las operaciones posteriores ven el valor perturbado y la estimación numérica es errónea. - Pon a cero todos los gradientes antes de cada pase analítico. Un
.gradpersistente de un test previo envenena al siguiente. - Para inputs de los que la expresión no depende, gradcheck debe reportar
0.0analítico y0.0numérico. Asegúrate de que tu test no incluye inputs "fantasma" por accidente. - Elección de tolerancia:
atol = 1e-4,rtol = 1e-3funciona paraeps = 1e-5en expresiones escalares típicas con magnitudes deValue~O(1). Si necesitas tolerancia más estrecha, elige un ε mayor; si tu expresión tiene magnitudes ~O(1e6), subeatolproporcionalmente. - Mensaje de error bonito:
§6 Lo que habrás aprendido¶
- El patrón gradcheck: numérico vs analítico, diferencias centradas, diseño de tolerancia.
- Por qué las diferencias finitas son una comprobación de cordura, no una herramienta primaria (el "punto dulce" de
epses estrecho). - Una utilidad reutilizable a la que volverás en la Fase 8 (gradcheck vectorizado sobre tensores).
- La mentalidad de "testea el gradiente, no la pérdida" — el bucle de entrenamiento de la Fase 16 lo reutilizará como puerta de CI antes de cada ejecución larga.
§7 Referencias¶
- El fuente de
torch.autograd.gradcheckde PyTorch (~200 líneas) es el análogo de producción; leerlo después de este lab son 15 minutos provechosos. - Bishop, Pattern Recognition and Machine Learning, §5.3.5 — derivación de comprobaciones de gradiente por diferencias finitas y sus modos de fallo.