English · Español
Lab 00 — Esqueleto de Tensor: la clase, sin ops todavía¶
Objetivo: crear la clase
Tensorensrc/minitorch/tensor.pycon todas las piezas estructurales (data,grad,_prev,_op,_backward,requires_grad,backward()) cableadas — pero sin operaciones definidas todavía. Confirma que llamar abackward()sobre unTensor"hoja" es un no-op, que el topo sort gestiona un DAG construido a mano, y que la clase pasamypy --strict.Tiempo estimado: 60–90 minutos.
Prerrequisitos:
src/minigrad/scalar.pyde la fase 7.src/minitorch/BLUEPRINT.mdrevisado.
Lo que produces¶
Un nuevo módulo src/minitorch/ con:
__init__.pyre-exportandoTensor.tensor.pyconteniendo el esqueleto de la claseTensor.- Un unit test
tests/test_tensor_skeleton.pydemostrando el contrato estructural.
Sin operaciones de numpy más allá de la construcción. Sin closures _backward para ninguna op específica. El esqueleto es "cómo se ve Tensor antes de enseñarle matemáticas".
Pista: la fase 7 fue
Value. La fase 8 esTensor. La diferencia visible es trivial: cambiamosfloatpornp.ndarray. La diferencia oculta — broadcasting reverso, ops por familias — la abriremos en los siguientes labs.
TODOs¶
Bloque A — boilerplate del módulo¶
En src/minitorch/:
- Crea
__init__.pyconfrom .tensor import Tensor. - Crea
tensor.pycon:
from __future__ import annotations
from collections.abc import Callable
import numpy as np
import numpy.typing as npt
ArrayLike = npt.ArrayLike
FloatArray = npt.NDArray[np.floating]
class Tensor:
"""Autograd-enabled tensor. See docs/phase-08-tensor-autograd/theory/."""
data: FloatArray
grad: FloatArray | None
_prev: tuple[Tensor, ...]
_op: str
_backward: Callable[[], None]
requires_grad: bool
def __init__(
self,
data: ArrayLike,
*,
requires_grad: bool = False,
_prev: tuple[Tensor, ...] = (),
_op: str = "",
) -> None: ...
def backward(self) -> None: ...
def zero_grad(self) -> None: ...
@property
def shape(self) -> tuple[int, ...]: ...
def __repr__(self) -> str: ...
- Implementa
__init__para: - Convertir
dataa un array de NumPy víanp.asarray(data, dtype=np.float64)(FP64 por defecto para la fase 8; reconsidérese en la fase 26). - Inicializar
grad = None(reservado de forma perezosa, la primera vez que backward lo toque). - Guardar
_prev,_op,_backward = lambda: None. - Guardar
requires_grad. - Implementa
backward()para: - Asegurar que
self.dataes un escalar (self.data.shape == ()); si no, lanzar excepción. (Backward solo tiene sentido desde una raíz escalar en la fase 8.) - Topo-sortear el DAG vía DFS sobre
_prev. - Poner
self.grad = np.array(1.0). - Para cada nodo en orden topológico inverso, llamar a
node._backward(). - Implementa
zero_grad()para ponerself.grad = None(onp.zeros_like(self.data)— escoge y documenta). - Implementa
__repr__para imprimir"Tensor(shape=..., data=..., grad=..., op=...)"truncado para arrays grandes.
Bloque B — unit test estructural¶
En tests/test_tensor_skeleton.py:
-
test_leaf_construction:t = Tensor([1.0, 2.0, 3.0])produce un Tensor conshape==(3,),_prev==(),_op=="",requires_grad==False. -
test_grad_is_lazy: UnTensorrecién construido tienegrad is None. -
test_leaf_backward_noop:t = Tensor(5.0); t.backward()no lanza excepción yt.grad == 1.0. -
test_topo_handles_diamond: Construye a mano un pequeño DAG fijando_prevdirectamente (sin ops todavía):Asegúrate de que el orden topológico visita aa = Tensor(1.0) b = Tensor(2.0, _prev=(a,), _op="dummy") c = Tensor(3.0, _prev=(a,), _op="dummy") d = Tensor(4.0, _prev=(b, c), _op="dummy") d.backward()ael último (después tanto debcomo dec). Usa un closure con efecto secundario en el_backwardde cada nodo para registrar el orden de visita. -
test_repr_does_not_crash:repr(Tensor(np.zeros((100, 100))))devuelve un string y no imprime 10.000 floats. -
test_mypy_strict: no es un método de test en sí; verifica víajust mypyen CI.
Bloque C — comprobación de cordura frente a Value¶
- Mira tu
scalar.pyde la fase 7. Lado a lado, confirma que los esqueletos de clase son isomorfos:dataes el único campo cuyo tipo difiere (floatvsnp.ndarray), ybackward()tiene la misma estructura topo + reverse-traverse. Si encuentras una diferencia estructural no trivial, probablemente ya hayas derivado — reinicia la comparación de diffs hasta que esté limpia.
Restricciones¶
- Sin ops todavía. No definas
__add__,__mul__, etc. Eso es el Lab 01. - Sin
_backwardpara ninguna op. El único_backwarddel esqueleto es ellambda: Nonepor defecto. mypy --strictdebe pasar. Usanumpy.typingpara los hints de array. Sinumpy.typinglanza errores, añade un# type: ignore[name-of-error]con un TODO y sigue adelante — anótalo en las revisiones dePHASE_08_PLAN.md.- Sin imports de PyTorch en
src/minitorch/. PyTorch es solo para tests.
Escollos¶
gradinicializado siempre a un array de ceros. Desperdicia memoria; reserva arrays(B, V)para tensores que nunca harán backprop. LazyNone+ reserva en el primer toque es el patrón correcto.backward()que no asegura raíz escalar. Llamar abackward()sobre un Tensor no escalar sin un grad upstream es un bug que queremos pillar ruidosamente. Assert, no adivines.- DFS topo con
set()de objetosTensor. Los Tensors necesitan ser hasheables por identidad. El__hash__por defecto funciona. No sobreescribas__eq__(np.allclose(t1.data, t2.data)) — rompe la hasheabilidad. - DFS recursivo excediendo el límite de Python. Para ~1000 nodos vas bien. El loop de entrenamiento de la fase 18 puede empujar esto; si es así, cambia a DFS iterativo entonces. Anótalo en BLUEPRINT.
Condiciones de parada¶
Hecho cuando:
src/minitorch/tensor.pyexiste con el esqueleto completo.tests/test_tensor_skeleton.pytiene 5 tests, todos verdes.mypy --strict src/minitorch/está limpio.ruff check src/minitorch/está limpio.- Puedes explicar por qué
gradesNone | FloatArrayy no soloFloatArray.
Cuándo consultar solutions/¶
Después de que los cinco tests pasen. solutions/00-tensor-skeleton-ref.md (al abrir la fase) compara tu clase con la forma de referencia.
Siguiente lab: lab/01-elementwise-ops.md.