Phase 01 - Lesson 12
Operações com Tensores
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Tensores são a linguagem comum entre dados e deep learning. Toda imagem, toda frase, todo gradiente flui atraves deles.
Tipo: Build Linguagem: Python Pré-requisitos: Fase 1, Lições 01 (Intuição de Álgebra Linear), 02 (Vetores, Matrizes e Operações) Tempo: ~90 minutos
Objetivos de Aprendizagem
- Implementar uma classe de tensor com shape, strides, reshape, transpose e operações elemento a elemento do zero
- Aplicar regras de broadcasting para operar em tensores de formatos diferentes sem copiar dados
- Escrever expressoes einsum para produtos escalares, multiplicações de matrizes, produtos externos e operações em lote
- Rastrear os shapes exatos dos tensores em cada passo dá atencao multi-head
O Problema
Você constrói um transformer. O forward pass parece limpo. Você o executa e recebe: RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768). Você encara os shapes. Você tenta um transpose. Agora ele diz Expected 4D input (got 3D input). Você adiciona um unsqueeze. Outra coisa quebra.
Erros de shape são o bug mais comum em código de deep learning. Eles não são difíceis conceitualmente -- cada operacao tem um contrato de shape -- mas se multiplicam rápido. Um transformer tem dezenas de reshapes, transposes e broadcasts encadeados. Um eixo errado é o erro se propaga em cascata. Pior, alguns erros de shape não lancam erros nenhum. Eles silenciosamente produzem lixo ao fazer broadcasting na dimensão errada ou somar no eixo errado.
Matrizes lidam com relações pareadas entre dois conjuntos de coisas. Dados reais não cabem em duas dimensões. Um lote de 32 imagens RGB a 224x224 é um tensor 4D: (32, 3, 224, 224). A self-attention com 12 heads também e 4D: (batch, heads, seq_len, head_dim). Você precisa de uma estrutura de dados que generalize para qualquer número de dimensões, com operações que se compõem de forma limpa em todas elas. Essa estrutura é o tensor. Domine suas operações e os erros de shape se tornam trivialmente depuraveis.
O Conceito
O que é um tensor
Um tensor é um array multidimensional de números com um tipo de dado uniforme. O número de dimensões é o rank (ou ordem). Cada dimensão é um eixo. O shape é uma tupla listando o tamanho ao longo de cada eixo.
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 = produto de todos os tamanhos. Um shape (2, 3, 4) contem 2 * 3 * 4 = 24 elementos.
Shapes de tensores em deep learning
Diferentes tipos de dados mapeiam para shapes especificos de tensores por convencao.
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
O PyTorch usa NCHW (channels-first). O TensorFlow usa NHWC (channels-last) por padrão. Layouts incompatíveis causam lentidoes silenciosas ou erros.
Como funciona o layout de memória
Um array 2D na memória é uma sequência 1D de bytes. Strides te dizem quantos elementos pular para avancar um passo ao longo de cada eixo.
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
O transpose não move dados. Ele troca os strides, tornando o tensor não-contíguo -- os elementos de uma linha não estão mais adjacentes na memória.
Regras de broadcasting
O broadcasting permite operar em tensores de formatos diferentes sem copiar dados. Alinhe os shapes pela direita. Duas dimensões são compatíveis quando são iguais ou uma delas e 1. Dimensões faltantes são preenchidas com 1s a esquerda.
Tensor A: (8, 1, 6, 1)
Tensor B: (7, 1, 5)
Padded B: (1, 7, 1, 5)
Result: (8, 7, 6, 5)
Einsum: a operacao universal de tensores
A soma de Einstein rotula cada eixo com uma letra. Eixos na entrada mas não na saída são somados. Eixos em ambos são mantidos.
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
Padrões-chave: i,i-> (produto escalar), i,j->ij (produto externo), ii-> (traco), ij->ji (transpose), bij,bjk->bik (matmul em lote), bhtd,bhsd->bhts (scores de atencao).
Construa
O código vive em code/tensors.py. Cada passo referência a implementação la.
Passo 1: Armazenamento de tensores e strides
Um tensor armazena uma lista plana de números mais metadados de shape. Os strides dizem a lógica de indexacao como mapear índices multidimensionais para posições 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 o shape (3, 4), os strides são (4, 1) -- pule 4 elementos para avancar uma linha, pule 1 elemento para avancar uma coluna.
Passo 2: Reshape, squeeze, unsqueeze
O reshape muda o shape sem mudar a ordem dos elementos. O número total de elementos deve permanecer o mesmo. Use -1 em uma dimensão para inferir seu tamanho.
t = Tensor(list(range(12)), shape=(2, 6))
r = t.reshape((3, 4))
r = t.reshape((-1, 3))
O squeeze remove eixos de tamanho 1. O unsqueeze insere um. O unsqueezing é crítico para broadcasting -- um vetor de bias (D,) adicionado a um lote (B, T, D) precisa de unsqueezing para (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)
Passo 3: Transpose e permute
O transpose troca dois eixos. O permute reordena todos os eixos.É assim que você converte entre NCHW e 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))
Após transpose ou permute, o tensor e não-contíguo na memória. No PyTorch, view falha em tensores não-contíguos -- use reshape ou chame .contiguous() primeiro.
Passo 4: Operações elemento a elemento e reducoes
Operações elemento a elemento (add, multiply, subtract) se aplicam independentemente a cada elemento e preservam o shape. Reducoes (sum, mean, max) colapsam um ou mais eixos.
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 em uma CNN: (B, C, H, W).mean(axis=[2, 3]) produz (B, C). Sequence mean pooling em NLP: (B, T, D).mean(axis=1) produz (B, D).
Passo 5: Broadcasting com NumPy
A função demo_broadcasting_numpy() em tensors.py mostra os padrões centrais.
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
Distância pareada via broadcasting: faça reshape de (M, 2) para (M, 1, 2) e de (N, 2) para (1, N, 2), subtraia, eleve ao quadrado, some ao longo do ultimo eixo, tire a raiz quadrada. Resultado: (M, N).
Passo 6: Operações einsum
As funções demo_einsum() e demo_einsum_gallery() percorrem cada padrão comum.
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)
O custo computacional de uma contração é o produto de todos os tamanhos de índice (mantidos e somados). Para bij,bjk->bik com B=32, I=128, J=64, K=128: 32 * 128 * 64 * 128 = 33,554,432 multiplicações-adicoes.
Passo 7: Mecanismo de atencao via einsum
A função demo_attention_einsum() implementa a atencao multi-head de ponta a ponta.
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 passo é uma operacao de tensor: projecao (matmul via einsum), divisão das heads (reshape + transpose), scores de atencao (matmul em lote via einsum), soma ponderada (matmul em lote via einsum), fusao das heads (transpose + reshape), projecao de saída (matmul via einsum).
Use
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)
O PyTorch adiciona autograd, suporte a GPU e kernels BLAS otimizados. A semântica de shape e idêntica. Se você entende a versao scratch, os erros de shape do PyTorch se tornam legíveis.
Cada camada de rede neural como uma operacao 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 |
Entregue
Está lição produz dois prompts reutilizáveis:
outputs/prompt-tensor-shapes.md-- Um prompt sistemático para depurar incompatibilidades de shape de tensores. Inclui tabelas de decisão para cada operacao comum (matmul, broadcast, cat, Linear, Conv2d, BatchNorm, softmax) é uma tabela de consulta de correções.outputs/prompt-tensor-debugger.md-- Um prompt de depuracao passo a passo que você cola em qualquer assistente de IA quando um erro de shape está te bloqueando. Forneca a mensagem de erro e os seus shapes de tensores, receba de volta a correção exata.
Exercícios
Facil -- Round-trip de reshape. Pegue um tensor de shape
(2, 3, 4). Faça reshape para(6, 4), depois para(24,), depois de volta para(2, 3, 4). Verifique se a ordem dos elementos e preservada em cada passo imprimindo os dados planos.Médio -- Implemente broadcasting. Estenda a classe
Tensorcom um métodobroadcast_to(shape)que expande dimensões de tamanho 1 para corresponder a um shape alvo. Depois modifique_elementwise_oppara fazer broadcast automaticamente antes de operar. Teste com shapes(3, 1)e(1, 4)produzindo(3, 4).Difícil -- Construa einsum do zero. Implemente uma função básica
einsum(subscripts, *tensors)que lide com pelo menos: produto escalar (i,i->), multiplicação de matrizes (ij,jk->ik), produto externo (i,j->ij) e transpose (ij->ji). Faça o parse dá string de subscripts, identifique os índices contraidos e itere sobre todas as combinacoes de índices. Compare seus resultados comnp.einsum.Difícil -- Rastreador de shape de atencao. Escreva uma função que receba
batch_size,seq_len,embed_dimenum_headscomo entradas e imprima o shape exato em cada passo dá atencao multi-head: entrada, projecao Q/K/V, divisão das heads, scores de atencao, pesos do softmax, soma ponderada, fusao das heads, projecao de saída. Verifique em relação a saída dedemo_attention_einsum().
Termos-Chave
| Term | What people say | What it actually means |
|---|---|---|
| Tensor | "Uma matriz mas com mais dimensões" | Um array multidimensional com tipo uniforme e shape, strides e operações definidas |
| Rank | "O número de dimensões" | O número de eixos. Uma matriz tem rank 2, não rank igual ao seu posto matricial |
| Shape | "O tamanho do tensor" | Uma tupla listando o tamanho ao longo de cada eixo. (2, 3) significa 2 linhas, 3 colunas |
| Stride | "Como a memória e organizada" | O número de elementos a pular para avancar uma posição ao longo de cada eixo |
| Broadcasting | "Simplesmente funciona quando os shapes diferem" | Um conjunto estrito de regras: alinhe pela direita, as dimensões devem ser iguais ou uma deve ser 1 |
| Contiguous | "O tensor está normal" | Elementos armazenados sequencialmente na memória sem lacunas ou reordenacao em relação ao layout lógico |
| Einsum | "Uma forma chique de escrever matmul" | Uma notacao geral que expressa qualquer contração de tensor, produto externo, traco ou transpose em uma linha |
| View | "Igual a reshape" | Um tensor compartilhando o mesmo buffer de memória mas com metadados de shape/stride diferentes. Falha em dados não-contíguos |
| Contraction | "Somar sobre um índice" | A operacao geral onde um índice compartilhado entre tensores e multiplicado e somado, produzindo um resultado de rank menor |
| NCHW / NHWC | "Formato PyTorch vs TensorFlow" | Convencoes de layout de memória para tensores de imagem. NCHW põe os canais antes das dimensões espaciais, NHWC os põe depois |
Leitura Adicional
- NumPy Broadcasting -- As regras canônicas com exemplos visuais
- PyTorch Tensor Views -- Quando as views funcionam e quando elas copiam
- einops -- Uma biblioteca que torna o reshape de tensores legível é seguro
- The Illustrated Transformer -- Visualiza os shapes de tensores fluindo pela atencao
- Einstein Summation in NumPy -- Documentação completa de einsum com exemplos