Skip to content

English · Español

Lab 00 — Esqueleto de Tensor: la clase, sin ops todavía

Objetivo: crear la clase Tensor en src/minitorch/tensor.py con todas las piezas estructurales (data, grad, _prev, _op, _backward, requires_grad, backward()) cableadas — pero sin operaciones definidas todavía. Confirma que llamar a backward() sobre un Tensor "hoja" es un no-op, que el topo sort gestiona un DAG construido a mano, y que la clase pasa mypy --strict.

Tiempo estimado: 60–90 minutos.

Prerrequisitos: src/minigrad/scalar.py de la fase 7. src/minitorch/BLUEPRINT.md revisado.


Lo que produces

Un nuevo módulo src/minitorch/ con:

  • __init__.py re-exportando Tensor.
  • tensor.py conteniendo el esqueleto de la clase Tensor.
  • Un unit test tests/test_tensor_skeleton.py demostrando 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 es Tensor. La diferencia visible es trivial: cambiamos float por np.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__.py con from .tensor import Tensor.
  • Crea tensor.py con:
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 data a un array de NumPy vía np.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.data es 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 poner self.grad = None (o np.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 con shape==(3,), _prev==(), _op=="", requires_grad==False.
  • test_grad_is_lazy: Un Tensor recién construido tiene grad is None.
  • test_leaf_backward_noop: t = Tensor(5.0); t.backward() no lanza excepción y t.grad == 1.0.
  • test_topo_handles_diamond: Construye a mano un pequeño DAG fijando _prev directamente (sin ops todavía):
    a = 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()
    
    Asegúrate de que el orden topológico visita a a el último (después tanto de b como de c). Usa un closure con efecto secundario en el _backward de 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ía just mypy en CI.

Bloque C — comprobación de cordura frente a Value

  • Mira tu scalar.py de la fase 7. Lado a lado, confirma que los esqueletos de clase son isomorfos: data es el único campo cuyo tipo difiere (float vs np.ndarray), y backward() 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 _backward para ninguna op. El único _backward del esqueleto es el lambda: None por defecto.
  • mypy --strict debe pasar. Usa numpy.typing para los hints de array. Si numpy.typing lanza errores, añade un # type: ignore[name-of-error] con un TODO y sigue adelante — anótalo en las revisiones de PHASE_08_PLAN.md.
  • Sin imports de PyTorch en src/minitorch/. PyTorch es solo para tests.

Escollos

  • grad inicializado siempre a un array de ceros. Desperdicia memoria; reserva arrays (B, V) para tensores que nunca harán backprop. Lazy None + reserva en el primer toque es el patrón correcto.
  • backward() que no asegura raíz escalar. Llamar a backward() sobre un Tensor no escalar sin un grad upstream es un bug que queremos pillar ruidosamente. Assert, no adivines.
  • DFS topo con set() de objetos Tensor. 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:

  1. src/minitorch/tensor.py existe con el esqueleto completo.
  2. tests/test_tensor_skeleton.py tiene 5 tests, todos verdes.
  3. mypy --strict src/minitorch/ está limpio.
  4. ruff check src/minitorch/ está limpio.
  5. Puedes explicar por qué grad es None | FloatArray y no solo FloatArray.

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.