Curso / PyTorch / Tensores y datos
● piloto de formato

PyTorch · Fundamentos

Tensores y datos: el vocabulario que ya usás sin pensarlo

Después de HF y fast.ai, ya construiste redes, entrenaste con LoRA, corriste inferencia con vLLM — todo sobre PyTorch, sin mirarlo de frente. Esta página fija el vocabulario oficial de la pieza más básica: el tensor y cómo entra la data al modelo.

1. El tensor: un ndarray que sabe usar GPU y derivarse

La documentación oficial es directa: un tensor es "una estructura de datos especializada, muy similar a arrays y matrices" — básicamente un ndarray de NumPy con dos poderes extra: puede vivir en GPU (o cualquier accelerator: CUDA, MPS, XPU) y está optimizado para diferenciación automática. Se puede crear de varias formas equivalentes:

x_data = torch.tensor([[1, 2],[3, 4]])       # directo desde datos
x_np = torch.from_numpy(np_array)              # desde un array de NumPy
x_ones = torch.ones_like(x_data)               # conserva shape/dtype de otro tensor
rand_tensor = torch.rand((2,3))              # shape explícito

Tres atributos definen a cualquier tensor: .shape, .dtype y .device (dónde vive físicamente — CPU por defecto). Mover un tensor a GPU es explícito y nunca automático:

if torch.accelerator.is_available():
    tensor = tensor.to(torch.accelerator.current_accelerator())

La API cubre más de 1200 operaciones — indexado estilo NumPy, torch.cat para concatenar, @/matmul para producto matricial, */mul para producto elemento a elemento. Vale la pena memorizar un solo hecho: tensores en CPU y arrays de NumPy pueden compartir la misma memoria subyacentet.numpy() y torch.from_numpy(n) no copian datos, así que modificar uno modifica el otro. Es la razón por la que convertir entre ambos formatos es prácticamente gratis, y también una fuente clásica de bugs sutiles cuando no se espera ese acoplamiento.

2. Operaciones in-place: el detalle que rompe autograd

Las operaciones que terminan en guion bajo (x.add_(5), x.copy_(y), x.t_()) modifican el tensor en el lugar en vez de crear uno nuevo — ahorran memoria, pero la documentación oficial es explícita: su uso está desaconsejado, porque pueden causar una pérdida inmediata del historial necesario para calcular derivadas. Si un tensor participa en el grafo de autograd y se sobreescribe in-place, PyTorch puede no tener forma de reconstruir el valor que necesitaba para el backward pass — el error clásico es RuntimeError: a leaf Variable that requires grad is being used in an in-place operation, o peor, un gradiente silenciosamente incorrecto.

3. Dataset: tres métodos, cualquier fuente de datos

torch.utils.data.Dataset es deliberadamente minimalista: cualquier clase que implemente __init__, __len__ y __getitem__ ya es un Dataset válido, sin heredar nada más complejo.

class CustomImageDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None):
        self.img_labels = pd.read_csv(annotations_file)
        self.img_dir = img_dir
        self.transform = transform

    def __len__(self): return len(self.img_labels)

    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        image = decode_image(img_path)
        label = self.img_labels.iloc[idx, 1]
        if self.transform: image = self.transform(image)
        return image, label

El diseño es intencional: desacoplar el código de datos del código de entrenamiento. Un Dataset no sabe nada de batches, GPUs, ni epochs — solo sabe cuántos ítems tiene y cómo devolver el ítem i. Esta simplicidad es la razón por la que casi cualquier fuente de datos (un CSV, una carpeta de imágenes, un dataset del Hub de HF, un stream remoto) puede envolverse en la misma interfaz de tres métodos.

4. DataLoader: batching, shuffling y paralelismo gratis

DataLoader envuelve un Dataset y resuelve todo lo que un training loop necesita y un Dataset deliberadamente no sabe hacer: agrupar en mini-batches, barajar en cada epoch, y usar multiprocessing para que la carga de datos no bloquee la GPU esperando el próximo batch.

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)

train_features, train_labels = next(iter(train_dataloader))
# train_features.shape → [64, 1, 28, 28] — el batch ya armado

Iterar el DataLoader da tuplas (features, labels) ya agrupadas en el tamaño de batch pedido — el mismo patrón exacto que ya usaste, sin necesariamente haberlo escrito a mano, en Trainer de HF y en el Learner de fastai/miniai: ambos son, por dentro, un DataLoader de PyTorch con una capa de conveniencia encima.

🔧 Es literalmente la misma pieza que ya viste tres veces

El TextDataLoaders/ImageDataLoaders de fastai (Parte 1 y 2), los DataLoaders de miniai y los Dataset/DataLoader de Hugging Face para tokenizar y batchear texto en el Capítulo 5 de HF son, en su base, exactamente esta misma pareja de clases de PyTorch — cada framework agrega su propia capa de conveniencia arriba (integración con datasets del Hub, transforms automáticos, collation específica), pero el contrato de fondo (__getitem__ + batching + shuffle) es idéntico en los tres.

5. Resumen

  1. Un tensor es un ndarray con dos poderes extra: puede vivir en un accelerator y soporta diferenciación automática.
  2. Tensores en CPU y arrays de NumPy comparten memoria — convertir entre ambos es gratis, pero acopla los dos objetos.
  3. Las operaciones in-place (sufijo _) ahorran memoria pero pueden romper el historial que necesita autograd — evitarlas quirúrgicamente, no por default.
  4. Dataset son solo tres métodos (__init__, __len__, __getitem__); DataLoader agrega batching, shuffle y paralelismo encima, sin que el Dataset sepa nada de eso.
  5. fastai, miniai y HF Transformers construyen sus propias abstracciones de datos directamente sobre estas dos clases — no las reemplazan, las envuelven.