English · Español
Lab 00 — Esqueleto de Parameter y Module¶
Objetivo: implementa
Parameter(una subclase de 2 líneas deTensor) yModule(la clase base de ~25 líneas con registro vía__setattr__,parameters()recursivo,zero_grad,state_dict,load_state_dicty__repr__). Dejamypy --strictlimpio. Escribe los tests estructurales que prueban que la mecánica de registro funciona.Tiempo estimado: 90–120 minutos.
Prereqs: Fase 8 cerrada (
minitorch.Tensordisponible). Teoría 00 + 01 leída.
Qué produces¶
Un nuevo módulo src/minimodel/nn/ con:
__init__.pyre-exportandoParameter,Module.module.pyconteniendo ambas clases.
Y tests/test_module_skeleton.py con los tests de la mecánica de registro.
🇪🇸 Toda la fase 9 descansa sobre este lab. Si
Moduleno descubre los parámetros correctamente, niLinearniSequentialni los optimizadores funcionan. Saca 90 minutos para que pase limpio antes de pasar al Lab 01.
TODOs¶
Bloque A — Parameter¶
En src/minimodel/nn/module.py:
- Define
Parameter(Tensor): __init__(self, data, requires_grad: bool = True)— llama asuper().__init__(data, requires_grad=requires_grad).- Nada más.
Bloque B — clase base Module¶
- Define
Module: __init__(self) -> None:- Usa
object.__setattr__(self, "_parameters", {})yobject.__setattr__(self, "_modules", {}). - También
object.__setattr__(self, "training", True).
- Usa
__setattr__(self, name, value) -> None:- Haz pop de
nameen ambos_parametersy_modules(maneja el caso de re-asignación). - Si
isinstance(value, Parameter): registra en_parameters[name]. - Sino si
isinstance(value, Module): registra en_modules[name]. - En todos los casos, también
object.__setattr__(self, name, value).
- Haz pop de
parameters(self) -> Iterator[Parameter]:- Yield from
_parameters.values(). - Para cada submódulo en
_modules.values(), recursivamenteyield from submodule.parameters().
- Yield from
zero_grad(self) -> None:- Para cada
penself.parameters(), ponp.grad = None.
- Para cada
state_dict(self, prefix: str = "") -> dict[str, np.ndarray]:- Dict plano; las claves son rutas separadas por puntos.
- Recorre
_modulesrecursivamente.
load_state_dict(self, state, prefix: str = "") -> None:- Copia in-place
state[key]alParameter.dataque coincida. - Recorre
_modulesrecursivamente.
- Copia in-place
train(self, mode: bool = True) -> Noneyeval(self) -> None: ponself.training; stub para la Fase 9 (no-op semánticamente, pero el atributo debe existir).forward(self, *args, **kwargs) -> Any: lanzaNotImplementedError.__call__(self, *args, **kwargs): despacha aself.forward(*args, **kwargs).__repr__(self) -> str: emite un print estilo árbol de submódulos y shapes de parámetros.
Tests¶
En tests/test_module_skeleton.py:
Bloque C — registro básico¶
-
test_parameter_is_tensor: -
test_module_init_no_parameters: -
test_single_parameter_registered: -
test_submodule_registered:(Nota: el orden de yield esclass Inner(Module): def __init__(self): super().__init__() self.w = Parameter(np.zeros(3)) class Outer(Module): def __init__(self): super().__init__() self.inner = Inner() self.b = Parameter(np.zeros(1)) m = Outer() params = list(m.parameters()) assert len(params) == 2 assert params[0] is m.b # direct Parameter first assert params[1] is m.inner.w # then submodule's parameter_parametersy luego_modules. Fija esta convención.)
Bloque D — re-asignación / sobreescritura¶
-
test_parameter_reassign_replaces_registration: -
test_parameter_overwritten_with_module: Ponself.w = Parameter(...), luegoself.w = Linear(2, 3)(puedes usar una clase stub temporal para Linear aquí — el Lab 01 construye el real). Verifica que_parametersya no tiene"w"y_modulessí. -
test_parameter_overwritten_with_none: Ponself.w = Parameter(...), luegoself.w = None. Verifica que_parametersya no tiene"w", yself.w is None.
Bloque E — zero_grad, state_dict, load_state_dict¶
-
test_zero_grad_clears_grads: Construye un módulo con 2 parámetros. Asigna manualmentep.grad = np.ones_like(p.data). Llama am.zero_grad(). Comprueba que todos losgradde los parámetros sonNone. -
test_state_dict_keys: -
test_load_state_dict_roundtrip: m1 = Outer(). Capturastate = m1.state_dict().m2 = Outer()(init aleatorio distinto).m2.load_state_dict(state).- Comprueba
m1.b.data is not m2.b.data(objetos distintos). - Comprueba
np.array_equal(m1.b.data, m2.b.data)(mismos valores).
Bloque F — casos límite¶
-
Documenta esto como comportamiento esperado (embeddings atados). La reflexión del Lab 02 pregunta si deduplicar.test_shared_parameter_yields_twice: Dos atributos apuntando al mismoParameter: -
test_repr_does_not_crash:repr(M())devuelve una string y no recurre infinitamente. -
test_call_dispatches_to_forward: SubclasaModule, sobreescribeforward(self, x)para devolverx * 2. Verificam(5) == 10. -
test_forward_not_implemented_raises:Module()(any_arg)debe lanzarNotImplementedError.
Bloque G — mypy --strict limpio¶
- Sin tipos
Anyexcepto donde aparecen en la API upstream deTensor. -
parametersestá tipadoIterator[Parameter]. -
state_dictestá tipadodict[str, np.ndarray]. -
forwardestá tipadoAny(placeholder; subclases lo estrechan).
Restricciones¶
- Sin
Linear, sinSequential, sin activaciones. Esos son el Lab 01. Modulees la única base abstracta. No introduzcasLayer,Container,LossModuleetc. KISS — la Fase 10+ puede refactorizar si es necesario.- Sin PyTorch en
src/minimodel/. PyTorch vive solo entests/(algunos tests de integración del Lab 02 lo usan). __init_subclass__está prohibido. La astucia del framework está limitada a__setattr__. Sin metaclases, sin decoradores de clase.
Escollos¶
- Recursión de
__setattr__. SiModule.__setattr__escribe víaself.X = value(asignación normal), se dispara a sí mismo. Usa siempreobject.__setattr__(self, name, value)para el almacenamiento real. super().__init__()olvidado. Pruébalo construyendo una subclase que olvide llamar a super, luego asignando un Parameter — debe lanzarAttributeErrorporque_parametersno existe. Documenta el error esperado.parameters()devuelve un generador, no una lista. Los consumidores deben convertir (list(model.parameters())) si necesitan iterar dos veces. La clase base del optimizador hace la conversión internamente.- Colisiones de clave en
state_dict. Si dos submódulos tienen el mismo nombre (no debería ocurrir pero podría vía subclasing extraño), el segundo sobreescribe al primero. Añade un test (test_state_dict_no_collision) que asegure claves únicas.
Condiciones de parada¶
Hecho cuando:
Parameteres una subclase de 2 líneas.Modulees ≤ 60 líneas incluyendo type hints y el repr.- Los 12+ tests anteriores están en verde.
mypy --strict src/minimodel/nn/module.pylimpio.ruff check src/minimodel/nn/module.pylimpio.- Puedes explicar por qué usamos
object.__setattr__en__init__(no en el cuerpo de__setattr__).
Cuándo consultar solutions/¶
Tras pasar todos los tests. solutions/00-parameter-and-module-skeleton-ref.md (en la apertura de fase) compara tu Module contra la implementación canónica y señala dónde la astucia puede ser incluso más corta (o más larga, con mejores mensajes de error).
Siguiente lab: lab/01-linear-and-activations.md.