English · Español
Lab 01 — Implementar las operaciones¶
Objetivo: dar cuerpo a
Valuecon+ - * / ** exp log relu tanh. Cada operación obtiene un forward (crea el nodo + registra padres) y un closure_backward(contribuye a los gradientes de los padres vía la derivada local).Tiempo estimado: 3–4 horas.
Prerrequisitos: lab 00. Lee
theory/02-op-derivatives.mdhasta que puedas recitar la tabla de memoria.
Lo que produces¶
src/minigrad/scalar.pyextendido con todas las operaciones implementadas.tests/test_scalar_autograd.py— tests por operación contrastados contra PyTorch FP64 con tolerancia 1e-9.tests/test_scalar_graph.py— test de dependencia en diamante detheory/03.
TODOs¶
Bloque A — operaciones binarias vía dunders¶
Implementa estas en Value:
-
__add__(self, other). EnvuelveotherenValue(other)si es un número. -
__radd__(self, other)— para que3 + Value(2)funcione. -
__mul__(self, other),__rmul__(self, other). -
__neg__(self)— definido comoself * -1, o nativamente. -
__sub__(self, other)—self + (-other). -
__rsub__(self, other)—(-self) + other. -
__truediv__(self, other)—self * other ** -1. Requiere__pow__. -
__rtruediv__(self, other). -
__pow__(self, n)— requiere quensea unintofloatde Python, NO unValue. LanzaTypeErroren otro caso.
Cada operación:
- Calcula el
datadel forward. - Crea
out = Value(data, _prev=(self, other), _op=symbol). - Define el closure
_backwardcapturando los padres y la tabla de derivada local detheory/02. - Fija
out._backward = _backward. - Devuelve
out.
Bloque B — operaciones unarias como métodos¶
-
exp(self) -> Value—out.data = math.exp(self.data),_backwardañadeout.data * out.gradaself.grad. -
log(self) -> Value—out.data = math.log(self.data),_backwardañade(1/self.data) * out.grad. Lanza conself.data <= 0. -
relu(self) -> Value—out.data = max(0.0, self.data),_backwardañade(1.0 if self.data > 0 else 0.0) * out.grad. Documenta la elección de sub-gradiente en el docstring. -
tanh(self) -> Value—out.data = math.tanh(self.data),_backwardañade(1 - out.data**2) * out.grad.
Bloque C — tests por operación¶
En tests/test_scalar_autograd.py, para cada operación escribe un test con esta forma (pseudocódigo — Borja rellena los cuerpos):
def test_op_NAME():
# Configura las entradas.
a = Value(2.5)
b = Value(-1.3)
# Forward en minigrad.
c = a OP b # o a.OP(b) para unaria
c.backward()
# Forward + backward en PyTorch FP64 como oráculo.
ta = torch.tensor(2.5, dtype=torch.float64, requires_grad=True)
tb = torch.tensor(-1.3, dtype=torch.float64, requires_grad=True)
tc = ta OP tb
tc.backward()
# Comparar.
assert abs(c.data - tc.item()) < 1e-9
assert abs(a.grad - ta.grad.item()) < 1e-9
assert abs(b.grad - tb.grad.item()) < 1e-9
Claude ha proporcionado la lista de tests como comentarios en test_scalar_autograd.py. Rellena los cuerpos. Un test por operación, más algunos casos límite:
- ReLU en
a.data == 0(convención de sub-gradiente). 1 / Value(2.0)— camino de__rtruediv__.Value(2.0) ** 3yValue(2.0) ** 0.5— operación de potencia con exponente float.log(Value(2.0))yexp(Value(0.5)).
Bloque D — test del diamante¶
En tests/test_scalar_graph.py:
def test_diamond_accumulation():
a = Value(2.0)
b = Value(3.0)
c = Value(4.0)
L = (a*b + c) * (a - c)
L.backward()
assert math.isclose(L.data, -20.0)
assert math.isclose(a.grad, 4.0)
assert math.isclose(b.grad, -4.0)
assert math.isclose(c.grad, -12.0)
Este es el ejemplo trabajado de theory/03. Si este test pasa, tu acumulación += funciona en patrones de diamante. Si falla, casi seguramente usaste = en algún sitio dentro de _backward.
Bloque E — test trampa de closures¶
Para cazar el bug común "capturé la variable equivocada en un bucle":
def test_closure_captures_correctly():
values = [Value(float(i)) for i in range(5)]
total = values[0]
for v in values[1:]:
total = total + v
total.backward()
# Cada valor contribuyó equitativamente a la suma.
for v in values:
assert math.isclose(v.grad, 1.0)
Si escribiste _backward como una lambda capturando la variable del bucle descuidadamente, este test falla (todos los v.grad salvo el último serán 0 o erróneos). El arreglo es capturar padres en tiempo de creación de la operación dentro de una función no-bucle — lo cual la forma de método por operación ya hace correctamente. Pero testéalo.
Bloque F — propiedad de contraste¶
Opcional pero recomendado: usa hypothesis para generar pequeñas expresiones aleatorias y contrastarlas contra PyTorch. Esto es sobre todo asunto de la Fase 8; para la Fase 7, los tests por operación + el test del diamante son suficientes.
Restricciones¶
- Tolerancia de PyTorch 1e-9. FP64 debería coincidir hasta ~1e-12; 1e-9 deja margen.
- Mínimo un test por operación. No agrupes "test todas las ops" en un mega-test.
- Sin import de
numpy. Usamathparaexp,log,tanh. - Type hints obligatorios en todos los nuevos métodos y variables libres de closures.
- Todos los tests deben pasar bajo
pytest -x(parar en el primer fallo — saca bugs a la luz más rápido).
Condiciones de parada¶
Hecho cuando:
- Las diez operaciones implementadas en
scalar.py. - Tests por operación verdes para las diez operaciones.
- Test del diamante verde.
- Test de captura de closure verde.
mypy --strictyrufflimpios.
Escollos¶
- Orden de argumentos de
__radd__.__radd__(self, other)se llama cuando se evalúaother + selfyother.__add__(self)devolvióNotImplemented. Así queotheres el operando izquierdo. Para operaciones conmutativas (add, mul) no importa; para resta y división sí. Value(0.0) ** 0.0**0en Python es1. Matemáticamente debatible. Nuestro__pow__debe seguir la convención de Python; PyTorch hace lo mismo.log(Value(0)). Debe lanzar. Si computas en forwardmath.log(0)obtienes-infy el backward obtieneinfde1/0. Decide: lanzar en forward, o dejar que se propague ainf/nan. Default de la Fase 7: lanzar. Documenta.relu(Value(0)). Testea específicamente quegrad == 0.0en este punto.- Igualdad de
Value. No sobrescribas__eq__— eso haríaValue(2) == Value(2)verdadero y rompería cosas comoif v in some_set. Usa la igualdad por identidad por defecto. __hash__está bien. El hash por defecto de object es basado en identidad;Valuees no hashable... no, en realidad el__hash__por defecto funciona para cualquier clase. Lo necesitamos para elsetdevisiteden el topo-sort.- Importar PyTorch en tests es lento. ~1s. Está bien para una suite de tests; solo no te sorprendas.
Cuándo consultar solutions/¶
Después de que todos los tests listados pasen. Luego solutions/01-implement-ops-ref.md (en la apertura de fase) muestra la estructura canónica de cada operación para comparación.
Siguiente lab: lab/02-train-xor.md.