Phase 01 - Lesson 12
Operaciones con Tensores
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Los tensores son el lenguaje común entre los datos y el deep learning. Cada imagen, cada oración, cada gradiente fluye a través de ellos.
Tipo: Build Lenguaje: Python Requisitos previos: Fase 1, Lecciones 01 (Intuición de Álgebra Lineal), 02 (Vectores, Matrices y Operaciones) Tiempo: ~90 minutos
Objetivos de Aprendizaje
- Implementar una clase de tensor con shape, strides, reshape, transpose y operaciones elemento a elemento desde cero
- Aplicar reglas de broadcasting para operar en tensores de distintas formas sin copiar datos
- Escribir expresiones einsum para productos punto, multiplicaciones de matrices, productos externos y operaciones por lotes
- Rastrear las formas exactas de los tensores en cada paso de la atención multi-head
El Problema
Construyes un transformer. El forward pass se ve limpio. Lo ejecutas y obtienes: RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768). Miras fijamente las formas. Pruebas un transpose. Ahora dice Expected 4D input (got 3D input). Agregas un unsqueeze. Algo más se rompe.
Los errores de forma son el bug más común en código de deep learning. No son difíciles conceptualmente -- cada operación tiene un contrato de forma -- pero se multiplican rápido. Un transformer tiene docenas de reshapes, transposes y broadcasts encadenados. Un eje equivocado y el error se propaga en cascada. Peor aún, algunos errores de forma no lanzan errores en absoluto. Silenciosamente producen basura al hacer broadcasting sobre la dimensión equivocada o al sumar sobre el eje equivocado.
Las matrices manejan relaciones por pares entre dos conjuntos de cosas. Los datos reales no caben en dos dimensiones. Un lote de 32 imágenes RGB a 224x224 es un tensor 4D: (32, 3, 224, 224). La self-attention con 12 heads también es 4D: (batch, heads, seq_len, head_dim). Necesitas una estructura de datos que generalice a cualquier número de dimensiones, con operaciones que se compongan limpiamente en todas ellas. Esa estructura es el tensor. Domina sus operaciones y los errores de forma se vuelven trivialmente depurables.
El Concepto
Que es un tensor
Un tensor es un arreglo multidimensional de números con un tipo de dato uniforme. El número de dimensiones es el rank (u orden). Cada dimensión es un eje. El shape es una tupla que lista el tamaño a lo largo de cada eje.
graph LR
S["Scalar<br/>rank 0<br/>shape: ()"] --> V["Vector<br/>rank 1<br/>shape: (3,)"]
V --> M["Matrix<br/>rank 2<br/>shape: (2,3)"]
M --> T3["3D Tensor<br/>rank 3<br/>shape: (2,2,2)"]
T3 --> T4["4D Tensor<br/>rank 4<br/>shape: (B,C,H,W)"]
Total de elementos = producto de todos los tamaños. Un shape (2, 3, 4) contiene 2 * 3 * 4 = 24 elementos.
Shapes de tensores en deep learning
Distintos tipos de datos se mapean a shapes especificos de tensores por convención.
graph TD
subgraph Vision
V1["(B, C, H, W)<br/>32, 3, 224, 224"]
end
subgraph NLP
N1["(B, T, D)<br/>16, 128, 768"]
end
subgraph Attention
A1["(B, H, T, D)<br/>16, 12, 128, 64"]
end
subgraph Weights
W1["Linear: (out, in)<br/>Conv2D: (out_c, in_c, kH, kW)<br/>Embedding: (vocab, dim)"]
end
PyTorch usa NCHW (channels-first). TensorFlow usa NHWC (channels-last) por defecto. Los layouts incompatibles causan ralentizaciones silenciosas o errores.
Como funciona el layout de memoria
Un arreglo 2D en memoria es una secuencia 1D de bytes. Los strides te dicen cuantos elementos saltar para avanzar un paso a lo largo de cada eje.
graph LR
subgraph "Row-major (C order)"
R["a b c d e f<br/>strides: (3, 1)"]
end
subgraph "Column-major (F order)"
C["a d b e c f<br/>strides: (1, 2)"]
end
El transpose no mueve datos. Intercambia los strides, haciendo que el tensor sea no contiguo -- los elementos de una fila ya no están adyacentes en memoria.
Reglas de broadcasting
El broadcasting te permite operar en tensores de distintas formas sin copiar datos. Alinea las formas desde la derecha. Dos dimensiones son compatibles cuando son iguales o una de ellas es 1. Las dimensiones faltantes se rellenan con 1s a la izquierda.
Tensor A: (8, 1, 6, 1)
Tensor B: (7, 1, 5)
Padded B: (1, 7, 1, 5)
Result: (8, 7, 6, 5)
Einsum: la operación universal de tensores
La suma de Einstein etiqueta cada eje con una letra. Los ejes en la entrada pero no en la salida se suman. Los ejes en ambos se conservan.
graph LR
subgraph "matmul: ik,kj -> ij"
A["A(I,K)"] --> |"sum over k"| C["C(I,J)"]
B["B(K,J)"] --> |"sum over k"| C
end
Patrones clave: i,i-> (producto punto), i,j->ij (producto externo), ii-> (traza), ij->ji (transpose), bij,bjk->bik (matmul por lotes), bhtd,bhsd->bhts (scores de atención).
Construye
El código vive en code/tensors.py. Cada paso hace referencia a la implementación alli.
Paso 1: Almacenamiento de tensores y strides
Un tensor almacena una lista plana de números más metadatos de shape. Los strides le dicen a la lógica de indexación como mapear índices multidimensionales a posiciones planas.
class Tensor:
def __init__(self, data, shape=None):
if isinstance(data, (list, tuple)):
self._data, self._shape = self._flatten_nested(data)
elif isinstance(data, np.ndarray):
self._data = data.flatten().tolist()
self._shape = tuple(data.shape)
else:
self._data = [data]
self._shape = ()
if shape is not None:
total = reduce(lambda a, b: a * b, shape, 1)
if total != len(self._data):
raise ValueError(
f"Cannot reshape {len(self._data)} elements into shape {shape}"
)
self._shape = tuple(shape)
self._strides = self._compute_strides(self._shape)
@staticmethod
def _compute_strides(shape):
if len(shape) == 0:
return ()
strides = [1] * len(shape)
for i in range(len(shape) - 2, -1, -1):
strides[i] = strides[i + 1] * shape[i + 1]
return tuple(strides)
Para el shape (3, 4), los strides son (4, 1) -- salta 4 elementos para avanzar una fila, salta 1 elemento para avanzar una columna.
Paso 2: Reshape, squeeze, unsqueeze
El reshape cambia la forma sin cambiar el orden de los elementos. El número total de elementos debe permanecer igual. Usa -1 en una dimensión para inferir su tamaño.
t = Tensor(list(range(12)), shape=(2, 6))
r = t.reshape((3, 4))
r = t.reshape((-1, 3))
El squeeze elimina ejes de tamaño 1. El unsqueeze inserta uno. El unsqueezing es crítico para el broadcasting -- un vector de bias (D,) agregado a un lote (B, T, D) necesita unsqueezing a (1, 1, D).
t = Tensor(list(range(6)), shape=(1, 3, 1, 2))
s = t.squeeze()
v = Tensor([1, 2, 3])
u = v.unsqueeze(0)
Paso 3: Transpose y permute
El transpose intercambia dos ejes. El permute reordena todos los ejes. Así es como conviertes entre NCHW y NHWC.
mat = Tensor(list(range(6)), shape=(2, 3))
tr = mat.transpose(0, 1)
t4d = Tensor(list(range(24)), shape=(1, 2, 3, 4))
perm = t4d.permute((0, 2, 3, 1))
Después de un transpose o permute, el tensor es no contiguo en memoria. En PyTorch, view falla en tensores no contiguos -- usa reshape o llama a .contiguous() primero.
Paso 4: Operaciones elemento a elemento y reducciones
Las operaciones elemento a elemento (add, multiply, subtract) se aplican independientemente a cada elemento y preservan la forma. Las reducciones (sum, mean, max) colapsan uno o más ejes.
a = Tensor([[1, 2], [3, 4]])
b = Tensor([[10, 20], [30, 40]])
c = a + b
d = a * 2
s = a.sum(axis=0)
Global average pooling en una CNN: (B, C, H, W).mean(axis=[2, 3]) produce (B, C). Sequence mean pooling en NLP: (B, T, D).mean(axis=1) produce (B, D).
Paso 5: Broadcasting con NumPy
La función demo_broadcasting_numpy() en tensors.py muestra los patrones centrales.
activations = np.random.randn(4, 3)
bias = np.array([0.1, 0.2, 0.3])
result = activations + bias
images = np.random.randn(2, 3, 4, 4)
scale = np.array([0.5, 1.0, 1.5]).reshape(1, 3, 1, 1)
result = images * scale
a = np.array([1, 2, 3]).reshape(-1, 1)
b = np.array([10, 20, 30, 40]).reshape(1, -1)
outer = a * b
Distancia por pares via broadcasting: haz reshape de (M, 2) a (M, 1, 2) y de (N, 2) a (1, N, 2), resta, eleva al cuadrado, suma a lo largo del último eje, toma la raíz cuadrada. Resultado: (M, N).
Paso 6: Operaciones einsum
Las funciones demo_einsum() y demo_einsum_gallery() recorren cada patron común.
a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
dot = np.einsum("i,i->", a, b)
A = np.array([[1, 2], [3, 4], [5, 6]], dtype=float)
B = np.array([[7, 8, 9], [10, 11, 12]], dtype=float)
matmul = np.einsum("ik,kj->ij", A, B)
batch_A = np.random.randn(4, 3, 5)
batch_B = np.random.randn(4, 5, 2)
batch_mm = np.einsum("bij,bjk->bik", batch_A, batch_B)
El costo computacional de una contracción es el producto de todos los tamaños de índice (conservados y sumados). Para bij,bjk->bik con B=32, I=128, J=64, K=128: 32 * 128 * 64 * 128 = 33,554,432 multiplicaciones-sumas.
Paso 7: Mecanismo de atención via einsum
La función demo_attention_einsum() implementa la atención multi-head de extremo a extremo.
B, H, T, D = 2, 4, 8, 16
E = H * D
X = np.random.randn(B, T, E)
W_q = np.random.randn(E, E) * 0.02
Q = np.einsum("bte,ek->btk", X, W_q)
Q = Q.reshape(B, T, H, D).transpose(0, 2, 1, 3)
scores = np.einsum("bhtd,bhsd->bhts", Q, K) / np.sqrt(D)
weights = softmax(scores, axis=-1)
attn_output = np.einsum("bhts,bhsd->bhtd", weights, V)
concat = attn_output.transpose(0, 2, 1, 3).reshape(B, T, E)
output = np.einsum("bte,ek->btk", concat, W_o)
Cada paso es una operación de tensor: proyección (matmul via einsum), división de las heads (reshape + transpose), scores de atención (matmul por lotes via einsum), suma ponderada (matmul por lotes via einsum), fusión de las heads (transpose + reshape), proyección de salida (matmul via einsum).
Usalo
Scratch vs NumPy
| Operation | Scratch (Tensor class) | NumPy |
|---|---|---|
| Create | Tensor([[1,2],[3,4]]) |
np.array([[1,2],[3,4]]) |
| Reshape | t.reshape((3,4)) |
a.reshape(3,4) |
| Transpose | t.transpose(0,1) |
a.T or a.transpose(0,1) |
| Squeeze | t.squeeze(0) |
np.squeeze(a, 0) |
| Sum | t.sum(axis=0) |
a.sum(axis=0) |
| Einsum | N/A | np.einsum("ij,jk->ik", a, b) |
Scratch vs PyTorch
import torch
t = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
t.shape
t.stride()
t.is_contiguous()
t.reshape(3, 2)
t.unsqueeze(0)
t.transpose(0, 1)
t.transpose(0, 1).contiguous()
torch.einsum("ik,kj->ij", A, B)
PyTorch agrega autograd, soporte para GPU y kernels BLAS optimizados. La semántica de shape es idéntica. Si entiendes la versión scratch, los errores de forma de PyTorch se vuelven legibles.
Cada capa de red neuronal como una operación de tensor
| Operation | Tensor Form | Einsum |
|---|---|---|
| Linear layer | Y = X @ W.T + b |
"bd,od->bo" + bias |
| Attention QKV | Q = X @ W_q |
"btd,dh->bth" |
| Attention scores | Q @ K.T / sqrt(d) |
"bhtd,bhsd->bhts" |
| Attention output | softmax(scores) @ V |
"bhts,bhsd->bhtd" |
| Batch norm | (X - mu) / sigma * gamma |
element-wise + broadcast |
| Softmax | exp(x) / sum(exp(x)) |
element-wise + reduction |
Entregalo
Esta lección produce dos prompts reutilizables:
outputs/prompt-tensor-shapes.md-- Un prompt sistemático para depurar discrepancias de forma de tensores. Incluye tablas de decisión para cada operación común (matmul, broadcast, cat, Linear, Conv2d, BatchNorm, softmax) y una tabla de consulta de correcciones.outputs/prompt-tensor-debugger.md-- Un prompt de depuración paso a paso que pegas en cualquier asistente de IA cuando un error de forma te está bloqueando. Dale el mensaje de error y tus formas de tensores, recibe de vuelta la corrección exacta.
Ejercicios
Fácil -- Round-trip de reshape. Toma un tensor de shape
(2, 3, 4). Haz reshape a(6, 4), luego a(24,), luego de vuelta a(2, 3, 4). Verifica que el orden de los elementos se preserve en cada paso imprimiendo los datos planos.Medio -- Implementa broadcasting. Extiende la clase
Tensorcon un métodobroadcast_to(shape)que expanda dimensiones de tamaño 1 para coincidir con un shape objetivo. Luego modifica_elementwise_oppara hacer broadcast automáticamente antes de operar. Prueba con shapes(3, 1)y(1, 4)produciendo(3, 4).Difícil -- Construye einsum desde cero. Implementa una función básica
einsum(subscripts, *tensors)que maneje al menos: producto punto (i,i->), multiplicación de matrices (ij,jk->ik), producto externo (i,j->ij) y transpose (ij->ji). Haz el parse del string de subscripts, identifica los índices contraídos e itera sobre todas las combinaciones de índices. Compara tus resultados connp.einsum.Difícil -- Rastreador de forma de atención. Escribe una función que reciba
batch_size,seq_len,embed_dimynum_headscomo entradas e imprima la forma exacta en cada paso de la atención multi-head: entrada, proyección Q/K/V, división de las heads, scores de atención, pesos del softmax, suma ponderada, fusión de las heads, proyección de salida. Verifica contra la salida dedemo_attention_einsum().
Términos Clave
| Term | What people say | What it actually means |
|---|---|---|
| Tensor | "Una matriz pero con más dimensiones" | Un arreglo multidimensional con tipo uniforme y shape, strides y operaciones definidos |
| Rank | "El número de dimensiones" | El número de ejes. Una matriz tiene rank 2, no rank igual a su rango matricial |
| Shape | "El tamaño del tensor" | Una tupla que lista el tamaño a lo largo de cada eje. (2, 3) significa 2 filas, 3 columnas |
| Stride | "Como está dispuesta la memoria" | El número de elementos a saltar para avanzar una posición a lo largo de cada eje |
| Broadcasting | "Simplemente funciona cuando las formas difieren" | Un conjunto estricto de reglas: alinea desde la derecha, las dimensiones deben ser iguales o una debe ser 1 |
| Contiguous | "El tensor esta normal" | Elementos almacenados secuencialmente en memoria sin huecos ni reordenamiento respecto al layout lógico |
| Einsum | "Una forma elegante de escribir matmul" | Una notación general que expresa cualquier contracción de tensor, producto externo, traza o transpose en una línea |
| View | "Igual que reshape" | Un tensor que comparte el mismo buffer de memoria pero con metadatos de shape/stride distintos. Falla en datos no contiguos |
| Contraction | "Sumar sobre un índice" | La operación general donde un índice compartido entre tensores se multiplica y suma, produciendo un resultado de rank menor |
| NCHW / NHWC | "Formato PyTorch vs TensorFlow" | Convenciones de layout de memoria para tensores de imagen. NCHW pone los canales antes de las dimensiones espaciales, NHWC los pone después |
Lectura Adicional
- NumPy Broadcasting -- Las reglas canónicas con ejemplos visuales
- PyTorch Tensor Views -- Cuando funcionan las views y cuando copian
- einops -- Una biblioteca que hace el reshape de tensores legible y seguro
- The Illustrated Transformer -- Visualiza los shapes de tensores fluyendo por la atención
- Einstein Summation in NumPy -- Documentación completa de einsum con ejemplos