Phase 03 - Lesson 11
Introducao ao PyTorch
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Voce construiu o motor a partir de pistoes e virabrequins. Agora aprenda aquele que todo mundo de fato dirige.
Type: Build Languages: Python Prerequisites: Licao 03.10 (Construa Seu Proprio Mini Framework) Time: ~75 minutos
Objetivos de Aprendizagem
- Construir e treinar redes neurais usando nn.Module, nn.Sequential e o autograd do PyTorch
- Usar tensores do PyTorch, aceleracao por GPU e o laco de treinamento padrao (zero_grad, forward, loss, backward, step)
- Converter os componentes do seu mini framework feito do zero para seus equivalentes em PyTorch
- Medir e comparar a velocidade de treinamento entre seu framework em Python puro e o PyTorch na mesma tarefa
O Problema
Voce tem um mini framework funcional. Camadas lineares, ReLU, dropout, batch norm, Adam, um DataLoader, um laco de treinamento. Ele treina uma rede de 4 camadas em um problema de classificacao de circulos, em Python puro.
Ele tambem e 500x mais lento que o PyTorch no mesmo problema.
Seu mini framework processa uma amostra por vez com lacos aninhados em Python. O PyTorch despacha as mesmas operacoes para kernels otimizados em C++/CUDA que rodam na GPU. Em uma unica NVIDIA A100, o PyTorch treina uma ResNet-50 (25,6M de parametros) na ImageNet (1,28M de imagens) em cerca de 6 horas. Seu framework levaria aproximadamente 3.000 horas na mesma tarefa -- se nao ficasse sem memoria antes.
Velocidade nao e a unica lacuna. Seu framework nao tem suporte a GPU. Nao tem diferenciacao automatica -- voce escreveu backward() a mao para cada modulo. Nao tem serializacao. Nao tem treinamento distribuido. Nao tem precisao mista. Nao tem como depurar o fluxo de gradientes sem instrucoes print.
O PyTorch preenche cada uma dessas lacunas. E faz isso mantendo exatamente o mesmo modelo mental que voce ja construiu: Module, forward(), parameters(), backward(), optimizer.step(). Os conceitos se transferem um a um. A sintaxe e quase identica. A diferenca e que o PyTorch envolve uma decada de engenharia de sistemas por tras da mesma interface que voce projetou do zero.
O Conceito
Por Que o PyTorch Venceu
Em 2015, o TensorFlow exigia que voce definisse um grafo de computacao estatico antes de rodar qualquer coisa. Voce construia o grafo, compilava e entao alimentava dados atraves dele. Depurar significava encarar visualizacoes de grafos. Mudar a arquitetura significava reconstruir o grafo do zero.
O PyTorch foi lancado em 2017 com uma filosofia diferente: execucao eager. Voce escreve Python. Ele roda imediatamente. y = model(x) de fato computa y agora, e nao "adiciona um no a um grafo que computara y depois". Isso fez com que as ferramentas padrao de depuracao do Python funcionassem. print() funcionava. pdb funcionava. if/else no seu forward pass funcionava.
Em 2020, o mercado ja havia se manifestado. A participacao do PyTorch em artigos de pesquisa de ML passou de 7% (2017) para mais de 75% (2022). Meta, Google DeepMind, OpenAI, Anthropic e Hugging Face usam o PyTorch como framework principal. O TensorFlow 2.x adotou execucao eager em resposta -- admissao tacita de que o design do PyTorch estava correto.
A licao: a experiencia do desenvolvedor se acumula. Um framework que e 10% mais lento mas 50% mais rapido de depurar vence sempre.
Tensores
Um tensor e um array multidimensional com tres propriedades criticas: shape, dtype e device.
import torch
x = torch.zeros(3, 4) # shape: (3, 4), dtype: float32, device: cpu
x = torch.randn(2, 3, 224, 224) # batch of 2 RGB images, 224x224
x = torch.tensor([1, 2, 3]) # from a Python list
Shape e a dimensionalidade. Um escalar tem shape (), um vetor e (n,), uma matriz e (m, n), um batch de imagens e (batch, channels, height, width).
Dtype controla precisao e memoria.
| dtype | Bits | Faixa | Caso de uso |
|---|---|---|---|
| float32 | 32 | ~7 digitos decimais | Treinamento padrao |
| float16 | 16 | ~3,3 digitos decimais | Precisao mista |
| bfloat16 | 16 | Mesma faixa que float32, menos precisao | Treinamento de LLM |
| int8 | 8 | -128 a 127 | Inferencia quantizada |
Device determina onde a computacao acontece.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
x = torch.randn(3, 4, device=device)
x = x.to("cuda")
x = x.cpu()
Toda operacao exige que todos os tensores estejam no mesmo device. Este e o erro #1 do PyTorch que iniciantes enfrentam: RuntimeError: Expected all tensors to be on the same device. Corrija movendo tudo para o mesmo device antes da computacao.
Reshape tem tempo constante -- ele muda os metadados, nao os dados.
x = torch.randn(2, 3, 4)
x.view(2, 12) # reshape to (2, 12) -- must be contiguous
x.reshape(6, 4) # reshape to (6, 4) -- works always
x.permute(2, 0, 1) # reorder dimensions
x.unsqueeze(0) # add dimension: (1, 2, 3, 4)
x.squeeze() # remove size-1 dimensions
Autograd
Seu mini framework exigia que voce implementasse backward() para cada modulo. O PyTorch nao. Ele registra cada operacao sobre tensores em um grafo aciclico direcionado (o grafo computacional) e entao percorre esse grafo em sentido reverso para computar gradientes automaticamente.
graph LR
x["x (leaf)"] --> mul["*"]
w["w (leaf, requires_grad)"] --> mul
mul --> add["+"]
b["b (leaf, requires_grad)"] --> add
add --> loss["loss"]
loss --> |".backward()"| add
add --> |"grad"| b
add --> |"grad"| mul
mul --> |"grad"| w
A diferenca fundamental em relacao ao seu framework: o PyTorch usa autodiferenciacao baseada em fita (tape). Cada operacao e anexada a uma "fita" durante o forward pass. Chamar .backward() reproduz a fita em sentido reverso.
x = torch.randn(3, requires_grad=True)
y = x ** 2 + 3 * x
z = y.sum()
z.backward()
print(x.grad) # dz/dx = 2x + 3
Tres regras do autograd:
- Apenas tensores folha (leaf) com
requires_grad=Trueacumulam gradientes - Gradientes acumulam por padrao -- chame
optimizer.zero_grad()antes de cada backward pass torch.no_grad()desativa o rastreamento de gradientes (use durante a avaliacao)
nn.Module
nn.Module e a classe base para todo componente de rede neural no PyTorch. Voce ja construiu essa abstracao na Licao 10. A versao do PyTorch adiciona registro automatico de parametros, descoberta recursiva de modulos, gerenciamento de device e serializacao via state dict.
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.layer1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.layer2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
return x
Quando voce atribui um nn.Module ou nn.Parameter como atributo em __init__, o PyTorch o registra automaticamente. model.parameters() coleta recursivamente cada parametro registrado. E por isso que voce nunca precisa reunir pesos manualmente como fazia no mini framework.
Blocos de construcao fundamentais:
| Modulo | O que faz | Parametros |
|---|---|---|
| nn.Linear(in, out) | Wx + b | in*out + out |
| nn.Conv2d(in_ch, out_ch, k) | Convolucao 2D | in_chout_chk*k + out_ch |
| nn.BatchNorm1d(features) | Normaliza ativacoes | 2 * features |
| nn.Dropout(p) | Zeramento aleatorio | 0 |
| nn.ReLU() | max(0, x) | 0 |
| nn.GELU() | Linear de erro gaussiano | 0 |
| nn.Embedding(vocab, dim) | Tabela de consulta | vocab * dim |
| nn.LayerNorm(dim) | Normalizacao por amostra | 2 * dim |
Funcoes de Perda e Otimizadores
O PyTorch traz versoes prontas para producao de tudo que voce construiu.
Funcoes de perda (de torch.nn):
| Perda | Tarefa | Entrada |
|---|---|---|
| nn.MSELoss() | Regressao | Qualquer shape |
| nn.CrossEntropyLoss() | Classificacao multiclasse | Logits (nao softmax) |
| nn.BCEWithLogitsLoss() | Classificacao binaria | Logits (nao sigmoid) |
| nn.L1Loss() | Regressao (robusta) | Qualquer shape |
| nn.CTCLoss() | Alinhamento de sequencias | Log-probabilidades |
Observacao: CrossEntropyLoss combina LogSoftmax + NLLLoss internamente. Passe logits brutos, nao saidas de softmax. Este e um erro comum que produz gradientes errados silenciosamente.
Otimizadores (de torch.optim):
| Otimizador | Quando usar | LR tipica |
|---|---|---|
| SGD(params, lr, momentum) | CNNs, pipelines bem ajustados | 0,01--0,1 |
| Adam(params, lr) | Ponto de partida padrao | 1e-3 |
| AdamW(params, lr, weight_decay) | Transformers, fine-tuning | 1e-4--1e-3 |
| LBFGS(params) | Pequena escala, segunda ordem | 1.0 |
O Laco de Treinamento
Todo laco de treinamento do PyTorch segue o mesmo padrao de 5 passos. Voce ja conhece isso da Licao 10.
sequenceDiagram
participant D as DataLoader
participant M as Model
participant L as Loss fn
participant O as Optimizer
loop Each Epoch
D->>M: batch = next(dataloader)
M->>L: predictions = model(batch)
L->>L: loss = criterion(predictions, targets)
L->>M: loss.backward()
O->>M: optimizer.step()
O->>O: optimizer.zero_grad()
end
O padrao canonico:
for epoch in range(num_epochs):
model.train()
for inputs, targets in train_loader:
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
Cinco linhas dentro do laco de batch. Cinco linhas que treinaram o GPT-4, o Stable Diffusion e o LLaMA. A arquitetura muda. Os dados mudam. Essas cinco linhas nao.
Dataset e DataLoader
O Dataset do PyTorch e uma classe abstrata com dois metodos: __len__ e __getitem__. O DataLoader o envolve com batching, embaralhamento e carregamento de dados em multiplos processos.
from torch.utils.data import Dataset, DataLoader
class MNISTDataset(Dataset):
def __init__(self, images, labels):
self.images = images
self.labels = labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return self.images[idx], self.labels[idx]
loader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=4)
num_workers=4 cria 4 processos para carregar dados em paralelo enquanto a GPU treina no batch atual. Em cargas limitadas por disco (imagens grandes, audio), isso sozinho pode dobrar a velocidade de treinamento.
Treinamento em GPU
Movendo um modelo para a GPU:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
Isso move recursivamente cada parametro e buffer para a GPU. Depois mova cada batch durante o treinamento:
inputs, targets = inputs.to(device), targets.to(device)
Precisao mista reduz o uso de memoria pela metade e dobra a vazao em GPUs modernas (A100, H100, RTX 4090) ao rodar forward/backward em float16 enquanto mantem os pesos mestres em float32:
from torch.amp import autocast, GradScaler
scaler = GradScaler()
for inputs, targets in loader:
with autocast(device_type="cuda"):
outputs = model(inputs)
loss = criterion(outputs, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
Comparacao: Mini Framework vs PyTorch vs JAX
| Recurso | Mini Framework (L10) | PyTorch | JAX |
|---|---|---|---|
| Autodiff | backward() manual | Autograd baseado em fita | Transformacoes funcionais |
| Execucao | Eager (lacos Python) | Eager (kernels C++) | Tracado + compilado JIT |
| Suporte a GPU | Nao | Sim (CUDA, ROCm, MPS) | Sim (CUDA, TPU) |
| Velocidade (MLP MNIST) | ~300s/epoca | ~0,5s/epoca | ~0,3s/epoca |
| Sistema de modulos | Classe Module customizada | nn.Module | Funcoes sem estado (Flax/Equinox) |
| Depuracao | print() | print(), pdb, breakpoint() | Mais dificil (tracing JIT quebra print) |
| Ecossistema | Nenhum | Hugging Face, Lightning, timm | Flax, Optax, Orbax |
| Curva de aprendizagem | Voce construiu | Moderada | Ingreme (paradigma funcional) |
| Uso em producao | Problemas de brinquedo | Meta, OpenAI, Anthropic, HF | Google DeepMind, Midjourney |
Construa
Uma MLP de 3 camadas treinada na MNIST usando apenas primitivas do PyTorch. Sem wrappers de alto nivel. Sem torchvision.datasets. Baixamos e parseamos os dados brutos nos mesmos.
Passo 1: Carregar a MNIST a Partir de Arquivos Brutos
A MNIST vem em 4 arquivos comprimidos com gzip: imagens de treino (60.000 x 28 x 28), rotulos de treino, imagens de teste (10.000 x 28 x 28), rotulos de teste. Baixamos e parseamos o formato binario.
import torch
import torch.nn as nn
import struct
import gzip
import urllib.request
import os
def download_mnist(path="./mnist_data"):
base_url = "https://storage.googleapis.com/cvdf-datasets/mnist/"
files = [
"train-images-idx3-ubyte.gz",
"train-labels-idx1-ubyte.gz",
"t10k-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz",
]
os.makedirs(path, exist_ok=True)
for f in files:
filepath = os.path.join(path, f)
if not os.path.exists(filepath):
urllib.request.urlretrieve(base_url + f, filepath)
def load_images(filepath):
with gzip.open(filepath, "rb") as f:
magic, num, rows, cols = struct.unpack(">IIII", f.read(16))
data = f.read()
images = torch.frombuffer(bytearray(data), dtype=torch.uint8)
images = images.reshape(num, rows * cols).float() / 255.0
return images
def load_labels(filepath):
with gzip.open(filepath, "rb") as f:
magic, num = struct.unpack(">II", f.read(8))
data = f.read()
labels = torch.frombuffer(bytearray(data), dtype=torch.uint8).long()
return labels
Passo 2: Definir o Modelo
Uma MLP de 3 camadas: 784 -> 256 -> 128 -> 10. Ativacoes ReLU. Dropout para regularizacao. Sem batch norm, para manter a simplicidade.
class MNISTModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 10),
)
def forward(self, x):
return self.net(x)
A camada de saida produz 10 logits brutos (um por digito). Sem softmax -- CrossEntropyLoss cuida disso internamente.
Contagem de parametros: 784256 + 256 + 256128 + 128 + 128*10 + 10 = 235.146. Minusculo para os padroes modernos. O GPT-2 small tem 124M. Isso treina em segundos.
Passo 3: Laco de Treinamento
O padrao canonico forward-loss-backward-step.
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss = 0
correct = 0
total = 0
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
def evaluate(model, loader, criterion, device):
model.eval()
total_loss = 0
correct = 0
total = 0
with torch.no_grad():
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
total_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
Note o torch.no_grad() durante a avaliacao. Isso desativa o autograd, reduzindo o uso de memoria e acelerando a inferencia. Sem ele, o PyTorch constroi um grafo computacional que voce nunca usa.
Passo 4: Conectar Tudo
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
download_mnist()
train_images = load_images("./mnist_data/train-images-idx3-ubyte.gz")
train_labels = load_labels("./mnist_data/train-labels-idx1-ubyte.gz")
test_images = load_images("./mnist_data/t10k-images-idx3-ubyte.gz")
test_labels = load_labels("./mnist_data/t10k-labels-idx1-ubyte.gz")
train_dataset = torch.utils.data.TensorDataset(train_images, train_labels)
test_dataset = torch.utils.data.TensorDataset(test_images, test_labels)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=64, shuffle=True
)
test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=256, shuffle=False
)
model = MNISTModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
num_params = sum(p.numel() for p in model.parameters())
print(f"Device: {device}")
print(f"Parameters: {num_params:,}")
print(f"Train samples: {len(train_dataset):,}")
print(f"Test samples: {len(test_dataset):,}")
print()
for epoch in range(10):
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, device
)
test_loss, test_acc = evaluate(
model, test_loader, criterion, device
)
print(
f"Epoch {epoch+1:2d} | "
f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | "
f"Test Loss: {test_loss:.4f} | Test Acc: {test_acc:.4f}"
)
torch.save(model.state_dict(), "mnist_mlp.pt")
print(f"\nModel saved to mnist_mlp.pt")
print(f"Final test accuracy: {test_acc:.4f}")
Saida esperada apos 10 epocas: ~97,8% de acuracia em teste. Tempo de treinamento em CPU: ~30 segundos. Em GPU: ~5 segundos. No seu mini framework com a mesma arquitetura: ~45 minutos.
Use
Comparacao Rapida: Mini Framework vs PyTorch
| Mini Framework (Licao 10) | PyTorch |
|---|---|
model = Sequential(Linear(784, 256), ReLU(), ...) |
model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), ...) |
pred = model.forward(x) |
pred = model(x) |
optimizer.zero_grad() |
optimizer.zero_grad() |
grad = criterion.backward() depois model.backward(grad) |
loss.backward() |
optimizer.step() |
optimizer.step() |
| Sem GPU | model.to("cuda") |
| backward manual para cada modulo | O autograd cuida de tudo |
A interface e quase identica. A diferenca e tudo o que esta por baixo.
Salvando e Carregando Modelos
torch.save(model.state_dict(), "model.pt")
model = MNISTModel()
model.load_state_dict(torch.load("model.pt", weights_only=True))
model.eval()
Sempre salve o state_dict() (o dicionario de parametros), nao o objeto do modelo. Salvar o objeto do modelo usa pickle, que quebra quando voce refatora o codigo. State dicts sao portateis.
Agendamento de Taxa de Aprendizado
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=10
)
for epoch in range(10):
train_one_epoch(model, train_loader, criterion, optimizer, device)
scheduler.step()
O PyTorch traz mais de 15 schedulers: StepLR, ExponentialLR, CosineAnnealingLR, OneCycleLR, ReduceLROnPlateau. Todos se encaixam na mesma interface de otimizador.
Entregue
Esta licao produz dois artefatos:
outputs/prompt-pytorch-debugger.md-- um prompt para diagnosticar falhas comuns de treinamento em PyTorchoutputs/skill-pytorch-patterns.md-- uma referencia de skill para padroes de treinamento em PyTorch
Exercicios
Adicione batch normalization. Insira
nn.BatchNorm1dapos cada camada linear (antes da ativacao). Compare a acuracia de teste e a velocidade de treinamento com a versao apenas com dropout. O batch norm deve alcancar 98%+ em menos epocas.Implemente um localizador de taxa de aprendizado. Treine por uma epoca com taxa de aprendizado crescente exponencialmente (de 1e-7 ate 1.0). Plote a perda vs LR. A LR otima e logo antes de a perda comecar a subir. Use isso para escolher uma LR melhor para o modelo da MNIST.
Porte para GPU com precisao mista. Adicione
torch.amp.autocasteGradScalerao laco de treinamento. Meca a vazao (amostras/segundo) com e sem precisao mista em GPU. Em uma A100, espere ~2x de aceleracao.Construa um Dataset customizado. Baixe o Fashion-MNIST (mesmo formato que a MNIST, mas com pecas de roupa). Implemente uma classe
FashionMNISTDataset(Dataset)com__getitem__e__len__. Treine a mesma MLP e compare a acuracia. O Fashion-MNIST e mais dificil -- espere ~88% vs ~98%.Substitua o Adam por SGD + momentum. Treine com
SGD(params, lr=0.01, momentum=0.9). Compare as curvas de convergencia. Depois adicione um schedulerCosineAnnealingLRe veja se o SGD alcanca o Adam na epoca 10.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Tensor | "Um array multidimensional" | Um array tipado e ciente de device, com suporte a diferenciacao automatica embutido em cada operacao |
| Autograd | "Backprop automatico" | Um sistema baseado em fita que registra operacoes durante o forward pass e depois as reproduz em sentido reverso para computar gradientes exatos |
| nn.Module | "Uma camada" | A classe base para qualquer bloco de computacao diferenciavel -- registra parametros, suporta aninhamento, gerencia modos de treino/avaliacao |
| state_dict | "Os pesos do modelo" | Um OrderedDict que mapeia nomes de parametros para tensores -- a representacao portatil e serializavel de um modelo treinado |
| .backward() | "Computar gradientes" | Percorrer o grafo computacional em sentido reverso, computando e acumulando gradientes para cada tensor folha com requires_grad=True |
| .to(device) | "Mover para a GPU" | Transferir recursivamente todos os parametros e buffers para o device especificado (CPU, CUDA, MPS) |
| DataLoader | "O pipeline de dados" | Um iterador que faz batching, embaralha e opcionalmente paraleliza o carregamento de dados de um Dataset |
| Precisao mista | "Usar float16" | Treinar com forward/backward em float16 para ganhar velocidade enquanto mantem pesos mestres em float32 para estabilidade numerica |
| Execucao eager | "Rodar agora" | Operacoes executam imediatamente quando chamadas, e nao adiadas para um passo de compilacao posterior -- a escolha de design central que diferencia o PyTorch do TF 1.x |
| zero_grad | "Resetar gradientes" | Zerar todos os gradientes de parametros antes do proximo backward pass, ja que o PyTorch acumula gradientes por padrao |
Leitura Complementar
- Paszke et al., "PyTorch: An Imperative Style, High-Performance Deep Learning Library" (2019) -- o artigo original explicando os tradeoffs de design do PyTorch
- PyTorch Tutorials: "Learning PyTorch with Examples" (https://pytorch.org/tutorials/beginner/pytorch_with_examples.html) -- o caminho oficial dos tensores ao nn.Module
- PyTorch Performance Tuning Guide (https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html) -- precisao mista, workers do DataLoader, memoria fixada (pinned) e outras otimizacoes de producao
- Horace He, "Making Deep Learning Go Brrrr" (https://horace.io/brrr_intro.html) -- por que o treinamento em GPU e rapido, com estrategias de otimizacao especificas do PyTorch