Skip to content

English · Español

Lab 00 — Esqueleto de Value

Objetivo: escribir el esqueleto de la clase Value: constructor, repr, la fontanería _prev/_op/_backward, y el recorrido backward(). Sin operaciones aún. Este lab hace explícita la topología antes de que corra ninguna matemática.

Tiempo estimado: 90–120 minutos.

Prerrequisitos: lee theory/00..04. Las utilidades de la Fase 6 existen.


Lo que produces

  1. src/minigrad/scalar.py conteniendo el esqueleto de la clase Value:
  2. __init__(self, data: float, _prev: tuple = (), _op: str = "") -> None
  3. __repr__
  4. data: float, grad: float, _prev: tuple[Value, ...], _op: str, _backward: Callable[[], None]
  5. backward(self) -> None — topo-sort + recorrido inverso
  6. src/minigrad/scalar/BLUEPRINT.md — ya pre-escrito por Claude. Léelo ahora. Ábrelo en split con scalar.py.
  7. tests/test_scalar_skeleton.py — tres tests que pasan incluso sin operaciones implementadas:
  8. Value(2.5).data == 2.5
  9. repr(Value(2.5)) contiene "2.5"
  10. Value(2.5).backward() se ejecuta sin error y fija grad a 1.0
  11. Aún sin directorio experiments/ — los labs 01 y 02 producen experimentos.

TODOs

Bloque A — leer BLUEPRINT primero

  • Abre src/minigrad/scalar/BLUEPRINT.md. Lee las secciones "API surface" y "anti-goals" completas. ¿Desacuerdos? Indícalos en tu /phase-checkpoint; no diverjas silenciosamente.

Bloque B — esqueleto de la clase

  • Crea src/minigrad/__init__.py (vacío o con __all__ = ["Value"] después del paso C).
  • Crea src/minigrad/scalar.py. Añade la clase Value.
  • El constructor toma data: float, _prev: tuple[Value, ...] = (), _op: str = "". Inicializa self.data = float(data), self.grad = 0.0, self._prev = _prev, self._op = _op, self._backward = lambda: None.
  • __repr__ devuelve f"Value(data={self.data:.4f}, grad={self.grad:.4f})" o similar — tu elección, pero hazlo informativo.
  • Añade type hints en todos los atributos y métodos. mypy --strict debe pasar.
  • Aún sin operaciones. Resiste la tentación.

Bloque C — backward()

  • Implementa backward(self):
  • Construye el orden topológico. Usa un helper recursivo o una pila iterativa — tu elección. Documenta la elección.
  • Fija self.grad = 1.0 (semilla).
  • Recorre el topo invertido, llamando v._backward() para cada v.
  • Defensivo: assert self._backward is callable. (Siempre lo es, pero el assert documenta el invariante.)
  • Decide y documenta: ¿backward() resetea gradientes primero, o asume que el llamador los ha puesto a cero? Default de la Fase 7: NO resetea. El llamador (eventualmente el optimizador de la Fase 9) es responsable de zero_grad(). Anótalo en el docstring.

Bloque D — type y lint

  • mypy --strict src/minigrad/scalar.py — verde.
  • ruff check src/minigrad/scalar.py — verde.
  • ruff format src/minigrad/scalar.py — aplicado.

Bloque E — tests del esqueleto

  • tests/test_scalar_skeleton.py:
    def test_construction():
        v = Value(2.5)
        assert v.data == 2.5
        assert v.grad == 0.0
    
    def test_repr():
        v = Value(2.5)
        assert "2.5" in repr(v)
    
    def test_backward_on_leaf():
        v = Value(2.5)
        v.backward()
        assert v.grad == 1.0
    

Restricciones

  • Sin NumPy. scalar.py opera solo sobre float de Python. Se permite import math para exp, log, tanh en labs posteriores.
  • Sin PyTorch en src/. PyTorch es dependencia solo-de-test.
  • Callable[[], None] es el tipo correcto para _backward. Usa from collections.abc import Callable.
  • _prev como tupla, no lista. Las tuplas son hashables y señalan "fijado en construcción".
  • Sin mutación de _prev tras construcción. Hazlo _prev: tuple[Value, ...] (inmutable).

Condiciones de parada

Hecho cuando:

  1. mypy --strict y ruff ambos verdes en scalar.py.
  2. Los tres tests del esqueleto pasan.
  3. Puedes hacer from minigrad.scalar import Value en un shell de Python e instanciar Value(2.0).

Escollos

  • Referencia hacia adelante para type hint. _prev: tuple["Value", ...] (entrecomillado como string) porque la clase está a medio definir cuando se lee la anotación. O usa from __future__ import annotations al inicio del archivo.
  • Argumento default mutable. No escribas _prev: tuple = () y luego lo mutes. Las tuplas son inmutables así que está bien, pero si accidentalmente lo haces una list, has reproducido el bug de "default mutable arg" de Python.
  • self.grad: float = 0.0. Usa 0.0, no 0. De lo contrario la promoción int de NumPy/Python podría sorprenderte en tests.
  • Profundidad de recursión del topo-sort. El límite de recursión por defecto de Python es ~1000. Para los grafos de la Fase 7 (decenas de nodos), sin problema. Los bucles de entrenamiento de la Fase 18+ necesitarán un topo-sort iterativo o sys.setrecursionlimit(...). Documenta pero no optimices aún.

Cuándo consultar solutions/

Después de que los tres tests del esqueleto pasen. Luego solutions/00-value-skeleton-ref.md (en la apertura de fase) muestra la estructura de referencia y cualquier elección de estilo a considerar antes del lab 01.


Siguiente lab: lab/01-implement-ops.md.