English · Español
Lab 01 — Módulos Linear y de activación¶
Objetivo: implementa
Linear,ReLU,Tanh,SigmoidySoftmaxcomo subclases delModuleque construiste en el Lab 00. Cada una es un envoltorio delgado alrededor de ops deminitorch.Tensor; juntas te dan todo lo necesario para el MLP del Lab 03. ~50 LOC por Borja.Tiempo estimado: 90–120 minutos.
Prereqs: Lab 00 cerrado (
ParameteryModuleen verde). Teoría 02 leída.
🇪🇸 Una vez que
Moduledescubre parámetros, unaLinearcabe en quince líneas y las activaciones en cuatro cada una. La única decisión interesante es la inicialización (Kaiming-uniform por defecto, la Fase 10 la derivará) y la estabilidad numérica del softmax (restar el máximo antes delexp). Todo lo demás es plomería sobreTensor.
Qué produces¶
Un nuevo conjunto de ficheros en src/minimodel/nn/:
linear.py—Linear(in_features, out_features, bias=True).activations.py—ReLU,Tanh,Sigmoid,Softmax(dim=-1).__init__.pyre-exporta actualizado para incluir los nuevos símbolos.
Y tests/test_linear_and_activations.py con tests de shape, gradcheck y state-dict.
TODOs¶
Bloque A — Linear¶
En src/minimodel/nn/linear.py:
- Importa
math,numpy as np,Tensordesdeminitorch, yParameter, Moduledesde.module. - Define
Linear(Module):
class Linear(Module):
"""Affine layer: y = x @ W.T + b. Stores weight as (out, in) per PyTorch."""
def __init__(self, in_features: int, out_features: int, bias: bool = True) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
# Kaiming-uniform default; Phase 10 will derive the magnitude.
bound = 1.0 / math.sqrt(in_features)
# TODO: initialize self.weight as Parameter of shape (out_features, in_features)
# uniform in [-bound, +bound].
raise NotImplementedError
# TODO: if bias, set self.bias = Parameter(np.zeros(out_features));
# else set self.bias = None (do NOT register as Parameter).
def forward(self, x: Tensor) -> Tensor:
# Input x has shape (..., in_features). Output has shape (..., out_features).
# TODO: y = x @ self.weight.transpose((1, 0))
# TODO: if self.bias is not None, y = y + self.bias
raise NotImplementedError
- Nota el caso sin bias:
self.bias = Nonedebe evitar queModule.__setattr__lo registre (no es unParameterni unModule, así que cae al almacenamiento plano de atributo).
Bloque B — ReLU, Tanh, Sigmoid¶
En src/minimodel/nn/activations.py:
- Cada uno es un
Modulede un método. Sin parámetros;parameters()no produce nada.
class ReLU(Module):
def forward(self, x: Tensor) -> Tensor:
# TODO: return x.relu() (or x.maximum(0) if relu() lives elsewhere).
raise NotImplementedError
class Tanh(Module):
def forward(self, x: Tensor) -> Tensor:
# TODO: return x.tanh()
raise NotImplementedError
class Sigmoid(Module):
def forward(self, x: Tensor) -> Tensor:
# TODO: return x.sigmoid()
raise NotImplementedError
Si minitorch.Tensor carece de alguno de relu, tanh, sigmoid, anota el hueco en tu diario y añade un TODO en minitorch — NO implementes la op dentro de activations.py. Las activaciones son módulos; la matemática vive en la librería de tensores.
Bloque C — Softmax¶
-
Softmax(dim: int = -1)necesita el truco de restar el máximo para estabilidad numérica:
class Softmax(Module):
def __init__(self, dim: int = -1) -> None:
super().__init__()
self.dim = dim
def forward(self, x: Tensor) -> Tensor:
# Numerical-stability trick: subtract the per-row max BEFORE exp.
# x_shifted = x - x.max(dim=self.dim, keepdim=True)
# exp_x = x_shifted.exp()
# return exp_x / exp_x.sum(dim=self.dim, keepdim=True)
raise NotImplementedError
- El "restar el máximo" no es una optimización — previene que
exp(large_number)haga overflow en FP32 (≈ 88.7 es el techo). Sin ello, un logit de 100 devuelveinfy el gradiente esnan.
Bloque D — re-exports de __init__.py¶
En src/minimodel/nn/__init__.py:
- Añade
from .linear import Linear. - Añade
from .activations import ReLU, Tanh, Sigmoid, Softmax. - Mantén
__all__ordenado y explícito.
Tests¶
En tests/test_linear_and_activations.py:
Bloque E — shape y parámetros de Linear¶
-
test_linear_shapes_batched: -
test_linear_parameters_count: -
test_linear_no_bias: -
test_linear_higher_rank_input: Entrada con shape(B, T, in)→ salida(B, T, out). Verifica que eltranspose + matmulhace broadcast en la dim final.
Bloque F — Gradcheck¶
-
test_relu_gradcheck: Gradiente de diferencia finita numérico vs autograd paraReLU. Muestrea 10 entradas aleatorias en[-1, 1]; finite-diff(f(x+ε) - f(x-ε)) / (2ε)conε = 1e-4; compara conx.graddey.sum().backward(). Tolerancia1e-4. Salta puntos donde|x| < ε— ReLU es no-diferenciable en 0. -
test_tanh_gradcheck,test_sigmoid_gradcheck: misma forma, sin salto. -
test_softmax_gradcheck: Softmax + sum es diferenciable en todas partes. Usa una entrada de 4 dimensiones. Tolerancia1e-4. -
test_linear_gradcheck: Gradcheck en ambosweightybiaspara unLinear(2, 3)con un batch de entrada(2, 2).
Bloque G — Estabilidad numérica de Softmax¶
-
test_softmax_large_logits_no_nan:Sin el truco de restar el máximo este test falla conx = Tensor(np.array([[100.0, 100.5, 101.0]])) y = Softmax(dim=-1)(x) assert not np.isnan(y.data).any() assert np.allclose(y.data.sum(axis=-1), 1.0)nan. -
test_softmax_sums_to_one: Para una entrada aleatoria(4, 7), cada fila de la salida suma 1 dentro de1e-6.
Bloque H — Round-trip de serialización¶
-
test_linear_state_dict_keys: -
test_linear_load_state_dict_roundtrip: Construye dosLinear(4, 3)con semillas aleatorias distintas; copia el estado de uno al otro; comprueba queweight.dataybias.datadel segundo son iguales elemento a elemento a los del primero. -
test_sequential_of_linear_activation: Construye (vía una lista ad-hoc tipoSequentialdiminuta, o con elSequentialreal si el Bloque I está hecho) una pilaLinear(4, 8) → ReLU → Linear(8, 3). Verifica que tiene 4 parámetros en orden:weight, bias, weight, bias. El forward pass en(2, 4)devuelve(2, 3).
Restricciones¶
- Sin PyTorch en
src/minimodel/. Valores de referencia de PyTorch (si los hay) viven solo en fixtures de tests, y solo para cross-checks — el Lab 02 usa uno de esos cross-checks, no este lab. - Sin nuevas ops en
Tensor. Si aminitorchle faltarelu/tanh/sigmoid/max/exp/sum, arreglaminitorchprimero y escribe la nota de op faltante en tu diario. - Alcance A13 sin cambios. Sin código específico de verbos en este lab; las activaciones y
Linearson agnósticas al dominio. - Sin
nn.Modulede PyTorch colándose. NuestroModuleviene deminimodel.nn.module.
Escollos¶
- Las ops in-place rompen autograd.
x.data -= ...dentro de un forward pass corrompe el tensor guardado y el backward pass devuelve gradientes incorrectos. Produce siempre un nuevoTensordesde ops. - Falta de propagación de
requires_grad. Si envuelves valores intermedios en llamadas frescas aTensor(np.array(...))(p.ej. para el truco de restar el máximo), puedes cortar el grafo de autograd. Usa ops de tensor; no construyas nuevos tensores hoja a mitad de forward. - Softmax sin el truco del máximo.
exp(100.0)hace overflow en FP32. Resta el máximo por fila antes delexp. - Shape del peso de
Linear. PyTorch almacena(out, in)y transpone en forward. Lo igualamos — los checkpoints transfieren a PyTorch en la Fase 18. Linearsin bias registrandoNonecomoParameter. Test:bias=Falsedebe dejar_parameterssin clave"bias".Module.__setattr__ya maneja esto si ponesself.bias = None; verifica.__init__de activación olvidandosuper().__init__(). La baseModulenecesita_parametersy_modulesinicializados, incluso para activaciones sin parámetros. Sinsuper().__init__(),list(ReLU().parameters())revienta.
Condiciones de parada¶
Hecho cuando:
Lineares ≤ 25 líneas incluyendo type hints.- Cada activación es ≤ 6 líneas.
- Todos los tests en Bloques E–H en verde.
mypy --strict src/minimodel/nn/limpio.ruff check src/minimodel/nn/limpio.- Puedes explicar por qué
weighttiene forma(out, in)y no(in, out). - Puedes explicar por qué
Softmaxresta el máximo antes delexp(numérico, no matemático).
Cuándo consultar solutions/¶
Tras pasar todos los tests. solutions/01-linear-and-activations-ref.md (en la apertura de fase) compara tus módulos contra la implementación canónica, con notas sobre inicializaciones alternativas (Xavier vs Kaiming) y el debate bias-vs-no-bias.
Siguiente lab: lab/02-optimizers.md.