Programação para machine learning
O que significa “Programação para aprendizado de máquina (Programming for ML)”
Trabalho com aprendizado de máquina (machine learning) é uma combinação de engenharia de software (software engineering), computação numérica (numerical computing) e ciência experimental (experimental science). “Programação para aprendizado de máquina” foca nas habilidades práticas necessárias para ir de uma ideia a um modelo funcional:
- Escrever Python correto e legível, que consiga escalar de protótipos à produção
- Usar o ecossistema científico de Python para arranjos (arrays), quadros de dados (dataframes), visualização (visualization) e modelos (models)
- Entender o comportamento numérico (ponto flutuante (floating point), estabilidade (stability), aleatoriedade (randomness)) o suficiente para depurar e otimizar o treinamento (veja Computação Numérica)
- Seguir padrões de fluxo de trabalho que suportem iteração: reprodutibilidade, avaliação, rastreamento de experimentos e empacotamento pronto para implantação
O objetivo não é apenas “fazer o modelo rodar”, mas “torná-lo confiável, depurável e repetível”.
O ecossistema de aprendizado de máquina em Python: um mapa mental
Python domina o aprendizado de máquina principalmente por sua ergonomia (ergonomics) e pela amplitude de bibliotecas maduras. Uma forma útil de organizar o ecossistema é por camadas.
Ferramentas centrais da linguagem que você usará constantemente
- Built-ins e biblioteca padrão:
dataclasses,pathlib,json,pickle,logging,argparse,multiprocessing - Tipagem (typing):
typing/typing_extensionspara manutenibilidade (mypyou pyright podem identificar bugs comuns cedo) - Empacotamento (packaging) e ambientes (environments):
venv+pippara configurações mínimas- Conda/Mamba para stacks científicos complexos (especialmente CUDA)
- Ferramentas modernas de dependências: Poetry, PDM ou
uv(instalações rápidas, arquivos de lock)
Conselho prático: use arquivos de lock (lockfiles) (ou pelo menos versões fixadas) assim que os resultados importarem. Projetos de aprendizado de máquina frequentemente falham em reproduzir resultados porque uma dependência transitiva (transitive dependency) mudou.
A pilha científica (scientific stack)
- NumPy: arranjos N-dimensionais, vetorização, broadcasting, álgebra linear
- SciPy: rotinas científicas (otimização, estatística, matrizes esparsas)
- pandas: manipulação de dados tabulares
- Matplotlib / Seaborn / Plotly: visualização
- pyarrow: memória colunar eficiente, E/S de Parquet; cada vez mais central para grandes conjuntos de dados
Bibliotecas de modelagem e treinamento
- scikit-learn: aprendizado de máquina clássico (modelos lineares, árvores, agrupamento), API consistente de estimadores
- PyTorch: aprendizado profundo (deep learning), execução ansiosa (eager execution) flexível; amplamente usado em pesquisa e indústria
- JAX: transformações componíveis (composable transformations) (
jit,grad,vmap,pmap) e computação numérica de alto desempenho - TensorFlow / Keras: ainda comum em pipelines de produção; Keras oferece uma interface de alto nível
Auxiliares de fluxo de trabalho e MLOps (MLOps)
- Versionamento/validação de dados: Great Expectations, Pandera, DVC
- Rastreamento de experimentos (experiment tracking): MLflow, Weights & Biases
- Serviço de modelos (model serving): FastAPI, BentoML, TorchServe (depende do caso de uso)
- Computação distribuída (distributed compute): Ray, Dask, Spark (conecta com Noções Básicas de Sistemas Distribuídos)
Ambientes de desenvolvimento: notebooks vs. scripts
Cadernos interativos (notebooks) (Jupyter, Colab)
Cadernos interativos são excelentes para:
- Iteração rápida e visualização
- Análise exploratória de dados (exploratory data analysis, EDA)
- Compartilhar “código narrativo” (relatórios de pesquisa)
Mas cadernos interativos facilitam:
- Depender de estado oculto (células executadas fora de ordem)
- Perder reprodutibilidade (ambiente não rastreado, mudanças ad-hoc)
Roteiros (scripts) e pacotes (packages)
Assim que o código estabilizar, migre a lógica central para módulos importáveis (modules):
Um padrão comum:
notebooks/para exploraçãosrc/your_project/para código reutilizávelscripts/para pontos de entrada de interface de linha de comando (command-line interface, CLI) (treinamento, avaliação)
Isso torna testes, reuso e implantação significativamente mais fáceis.
Fundamentos de computação numérica em Python (o que você precisa internalizar)
Treinamento em aprendizado de máquina é fundamentalmente otimização numérica (numerical optimization) sobre vetores/matrizes/tensores (tensors). A maioria dos problemas de desempenho e corretude remete a alguns conceitos.
Arranjos, tensores e vetorização (NumPy)
Um arranjo NumPy é um bloco de memória contíguo (ou com strides) com:
- um
dtype(e.g.,float32) - um
shape(e.g.,(batch, features)) - um padrão de
strideque descreve como percorrer a memória
O maior salto de desempenho vem da vetorização (vectorization): usar operações sobre arranjos que rodam em kernels otimizados em C/Fortran (BLAS/LAPACK), em vez de loops em Python.
import numpy as np
# Bad: Python loop
x = np.random.randn(1_000_000)
y = np.empty_like(x)
for i in range(len(x)):
y[i] = 1 / (1 + np.exp(-x[i]))
# Good: vectorized
y2 = 1 / (1 + np.exp(-x))
A vetorização importa porque loops em Python têm alto overhead; chamadas vetorizadas do NumPy empurram o trabalho para código nativo otimizado.
Broadcasting e shapes
Broadcasting permite que operações se apliquem a shapes compatíveis sem “tiling” explícito.
X = np.random.randn(100, 10) # 100 samples, 10 features
w = np.random.randn(10) # weights
b = 0.5 # bias scalar
y = X @ w + b # (100,) + scalar -> (100,)
Bugs comuns:
- misturar acidentalmente
(n,)vs(n,1) - transpor o eixo errado
- assumir broadcasting quando os shapes são incompatíveis
Uma abordagem disciplinada:
- imprimir ou validar com
assertos shapes nas fronteiras - adotar convenções (
(batch, features)em todo lugar) - usar ferramentas de tensores com nomes quando possível (e.g.,
einopsem aprendizado profundo)
Ponto flutuante, dtypes e estabilidade
Aprendizado de máquina é numericamente sensível. Dois pontos-chave:
- float32 é padrão para treinamento; float16/bfloat16 melhora a velocidade, mas pode desestabilizar o treinamento sem cuidado.
- Muitos “NaNs misteriosos” vêm de overflow/underflow em exponenciais, logs ou divisões.
Exemplo: sigmoide estável (stable sigmoid) e log-sum-exp (log-sum-exp) estável são necessidades comuns (detalhes em Computação Numérica).
import numpy as np
def sigmoid(x):
# Numerically safer sigmoid
# Works better for large |x| than 1/(1+exp(-x))
out = np.empty_like(x)
pos = x >= 0
neg = ~pos
out[pos] = 1 / (1 + np.exp(-x[pos]))
expx = np.exp(x[neg])
out[neg] = expx / (1 + expx)
return out
Aleatoriedade e reprodutibilidade
Aprendizado de máquina depende de aleatoriedade (inicialização, embaralhamento, aumento de dados). Reprodutibilidade exige controlar sementes (seeds) e entender que nem todas as operações na GPU são determinísticas.
No NumPy:
import numpy as np
rng = np.random.default_rng(seed=42)
X = rng.normal(size=(100, 10))
No PyTorch (simplificado):
import torch
torch.manual_seed(42)
# CUDA determinism can require extra flags and may reduce performance
Boas práticas de reprodutibilidade:
- registrar sementes, versões de bibliotecas, versão do dataset e configuração de treinamento
- separar “aleatoriedade do experimento” (seed) de “identidade dos dados” (snapshot do dataset)
Um exemplo numérico concreto: regressão linear com descida do gradiente (NumPy)
Mesmo que você use bibliotecas de alto nível, ajuda entender a mecânica por trás do treinamento. Regressão linear ilustra a numérica central do aprendizado de máquina: multiplicação de matrizes, cálculo de perda, gradientes (gradients).
Queremos minimizar o erro quadrático médio (mean squared error):
[ L(w, b) = \frac{1}{N}\sum_{i=1}^N (x_i^\top w + b - y_i)^2 ]
import numpy as np
rng = np.random.default_rng(0)
N, D = 200, 3
X = rng.normal(size=(N, D))
true_w = np.array([2.0, -1.0, 0.5])
true_b = -0.2
y = X @ true_w + true_b + 0.1 * rng.normal(size=N)
w = rng.normal(size=D)
b = 0.0
lr = 0.1
for step in range(200):
y_pred = X @ w + b # (N,)
err = y_pred - y # (N,)
loss = np.mean(err**2)
# Gradients (vectorized)
grad_w = (2 / N) * (X.T @ err) # (D,)
grad_b = (2 / N) * np.sum(err) # scalar
w -= lr * grad_w
b -= lr * grad_b
print("learned w:", w, "b:", b)
Este exemplo espelha o que frameworks de aprendizado profundo fazem — só que com diferenciação automática (automatic differentiation) em vez de fórmulas manuais de gradiente (veja Retropropagação e Descida do Gradiente).
Padrões de manipulação e pré-processamento de dados
Formatos de dados e E/S
Formatos comuns:
- CSV: ubíquo, lento, tipagem fraca
- Parquet: colunar, comprimido, rápido; bom padrão para dados tabulares em escala de analytics
- NumPy
.npy/.npz: simples, rápido para arranjos - HDF5: legado, mas ainda usado em contextos científicos
Para grandes datasets, prefira Parquet + pyarrow e faça streaming em lotes em vez de carregar tudo na memória.
pandas para aprendizado de máquina tabular (e quando ir além)
pandas é ideal para:
- joins, agrupamentos, criação de atributos (features)
- tratamento de valores ausentes
- análise exploratória
Mas para dados muito grandes:
- considere Polars (mais rápido, execução lazy) ou Spark/Dask/Ray dependendo da escala
- converta para arranjos NumPy para modelagem a fim de reduzir overhead
Divisão treino/validação/teste e vazamento
Um fluxo de trabalho canônico:
- Defina seu alvo de previsão e a unidade de amostragem (linha, usuário, sessão).
- Divida em treino/validação/teste antes de ajustar transformações que possam vazar informação do alvo.
- Ajuste o pré-processamento apenas no treino e então aplique em validação/teste.
Séries temporais e datasets por usuário exigem cuidado especial:
- divisões por tempo para evitar vazamento do “futuro”
- divisões por grupo para evitar compartilhar informação do mesmo usuário/dispositivo
Exemplo: pipeline de pré-processamento com scikit-learn
Pipelines (pipelines) do scikit-learn ajudam a evitar vazamento ao ajustar o pré-processamento dentro do pipeline.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
df = pd.DataFrame({
"age": [25, 40, 32, 19, 55],
"country": ["US", "FR", "US", "JP", "FR"],
"clicked": [0, 1, 0, 0, 1],
})
X = df[["age", "country"]]
y = df["clicked"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.4, random_state=0, stratify=y
)
preprocess = ColumnTransformer(
transformers=[
("num", StandardScaler(), ["age"]),
("cat", OneHotEncoder(handle_unknown="ignore"), ["country"]),
]
)
model = Pipeline(steps=[
("prep", preprocess),
("clf", LogisticRegression(max_iter=1000))
])
model.fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1]
print("AUC:", roc_auc_score(y_test, proba))
Ideia-chave: fit/transform fica com o escopo correto, o que é fácil de errar com código de pré-processamento ad-hoc.
APIs de modelagem e padrões comuns de bibliotecas
A interface de estimadores do scikit-learn
scikit-learn padroniza:
fit(X, y)predict(X)oupredict_proba(X)- transformadores com
fit_transform
Isso facilita:
- trocar modelos
- fazer validação cruzada
- fazer busca em grade de hiperparâmetros
Se você está construindo sistemas clássicos de aprendizado de máquina, aprender profundamente a API de estimadores tem alto retorno.
Frameworks de aprendizado profundo: tensores + autodiferenciação
Aprendizado profundo se baseia em diferenciação automática (automatic differentiation): você define um cálculo, e a biblioteca computa gradientes para treinamento (veja Retropropagação).
Conceitualmente:
- você constrói um grafo de computação (computation graph) (explicitamente ou implicitamente)
- calcula uma perda escalar
- calcula os gradientes da perda em relação aos parâmetros
- atualiza os parâmetros com um otimizador (optimizer)
PyTorch e JAX têm filosofias um pouco diferentes:
- PyTorch: execução ansiosa, grafos dinâmicos, muito depurável
- JAX: estilo funcional (functional style) com transformações (
grad,jit), excelente para desempenho e pesquisa
Exemplo: um loop de treinamento (training loop) mínimo em PyTorch
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
# Fake regression data
torch.manual_seed(0)
X = torch.randn(512, 10)
true_w = torch.randn(10, 1)
y = X @ true_w + 0.1 * torch.randn(512, 1)
ds = TensorDataset(X, y)
dl = DataLoader(ds, batch_size=64, shuffle=True)
model = nn.Sequential(
nn.Linear(10, 32),
nn.ReLU(),
nn.Linear(32, 1),
)
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
loss_fn = nn.MSELoss()
for epoch in range(10):
model.train()
total = 0.0
for xb, yb in dl:
pred = model(xb)
loss = loss_fn(pred, yb)
opt.zero_grad()
loss.backward()
opt.step()
total += loss.item() * xb.size(0)
print(f"epoch {epoch} mse={total/len(ds):.4f}")
Lições de fluxo de trabalho:
- Sempre separe os modos
train()vseval()para camadas como dropout (dropout) / normalização em lote (batchnorm) - Mantenha o loop explícito até você entendê-lo; treinadores de nível mais alto vêm depois
Modelos profundos como os de Redes Neurais ou Arquitetura Transformer são construídos a partir da mesma estrutura de loop.
Padrões comuns de fluxo de trabalho para projetos de aprendizado de máquina
Um layout prático de projeto
Uma estrutura simples e escalável:
src/project_name/data.py(carregamento, esquemas)features.py(transformações)models.pytrain.pyeval.py
configs/(configs YAML/JSON)scripts/(wrappers de CLI)tests/notebooks/
Isso suporta:
- modularidade
- testabilidade
- colaboração mais fácil
Gerenciamento de configuração
Hardcodar hiperparâmetros (hyperparameters) leva a experimentos irreprodutíveis. Opções comuns:
argparse(flags simples de CLI)- configs YAML/JSON + dataclasses estruturadas
- Hydra (composição poderosa; usada em muitos codebases de pesquisa)
Padrão mínimo viável de configuração:
from dataclasses import dataclass
@dataclass(frozen=True)
class TrainConfig:
lr: float = 1e-3
batch_size: int = 64
epochs: int = 10
seed: int = 0
Registre a configuração junto com métricas e checkpoints.
Logging, métricas e rastreamento de experimentos
No mínimo, capture:
- métricas de treino/validação por época/passo
- taxa de aprendizado
- informações de execução (tipo de GPU/CPU, versões de bibliotecas)
- caminho do checkpoint do modelo e hash da revisão do código (git commit)
Ferramentas:
- módulo
logging(baseline) - TensorBoard (comum em aprendizado profundo)
- MLflow/W&B (dashboards centrais, armazenamento de artefatos)
Checkpoints e serialização
Em geral, você precisa de:
- pesos do modelo
- estado do otimizador (para retomar o treinamento)
- configuração e artefatos de pré-processamento
Em PyTorch:
torch.save({...})para checkpoints- evite fazer pickling (pickling) de objetos inteiros se puder; salve
state_dicts
Em scikit-learn:
joblib.dump(model, path)é comum, mas trate como dependente do ambiente (fixe versões)
Testes de código de aprendizado de máquina (sim, até código de pesquisa)
Nem todo componente de aprendizado de máquina é testável como funções puras, mas muitos são:
- parsing de dados: “o esquema está correto, sem colunas ausentes”
- transformações de atributos: “shape/dtype de saída é o esperado”
- passo de treinamento: “a perda diminui em um dataset sintético pequeno”
Use pytest e fixtures determinísticos pequenos. Isso também incentiva fronteiras limpas — uma ideia que se sobrepõe a Algoritmos e Estruturas de Dados em termos de componentes limpos e componíveis.
Desempenho e escalabilidade: o que mais importa no começo
Primeiro otimize a forma algorítmica, depois as micro-otimizações
Antes de ajustar código de baixo nível, verifique:
- Você está vetorizando (NumPy) ou fazendo processamento em lotes (batching) corretamente (PyTorch)?
- Você está convertendo repetidamente entre pandas ↔ NumPy ↔ tensores do torch?
- Você está fazendo pré-processamento caro dentro do loop de treinamento?
Essa mentalidade se alinha com pensamento básico de complexidade (veja Complexidade).
Ferramentas de profiling
- Tempo de CPU:
cProfile,py-spy - Por linha:
line_profiler - Memória:
memory_profiler,tracemalloc - PyTorch:
torch.profiler
Faça profiling com tamanhos de lote representativos e caminhos de dados realistas.
Paralelismo e carregamento de dados
Gargalo típico: a GPU fica esperando dados.
Abordagens:
- pré-computar e colocar em cache atributos caros
- usar workers do DataLoader (
num_workers>0) e memória pinned (PyTorch) - armazenar datasets em formatos otimizados para streaming (Parquet, WebDataset, TFRecord)
Ao escalar além de uma máquina, os problemas rapidamente viram problemas de sistemas distribuídos: particionamento (sharding), rede, tolerância a falhas e consistência (veja Noções Básicas de Sistemas Distribuídos).
Armadilhas comuns (e como evitá-las)
- Vazamento de dados (data leakage): ajustar scalers/encoders em todos os dados; usar informação futura em séries temporais.
- Correção: pipelines, estratégia cuidadosa de split, disciplina explícita de “ajustar apenas no treino”.
- Incompatibilidades silenciosas de dtype/dispositivo:
- float64 entrando sem querer (mais lento, inconsistente)
- tensor na CPU misturado com tensor na GPU (erro em runtime)
- Correção: validar dtypes, padronizar conversões nas fronteiras.
- Confusão de shapes:
- Correção: imprimir shapes cedo; adicionar checagens em runtime; preferir convenções claras.
- Não determinismo (non-determinism):
- Correção: definir seed para tudo, registrar versões, aceitar que kernels de GPU ainda podem variar.
- Overfitting por iteração:
- Correção: manter um conjunto de teste realmente intocado; tratar o conjunto de validação como parte do loop de treinamento.
Resumo: hábitos centrais que valem a pena
- Pense em arranjos/tensores e prefira computação vetorizada e em lotes
- Entenda ponto flutuante e estabilidade numérica ao menos em nível prático (Computação Numérica)
- Use pipelines (especialmente no scikit-learn) para evitar vazamento e impor fronteiras corretas de fit/transform
- Trate aprendizado de máquina como software: código modular, configs, logging e testes
- Faça profiling de gargalos antes de “otimizar” e só escale para fora depois que o fluxo em uma máquina estiver sólido
Se você quiser, também posso adicionar uma seção curta de “toolkit mínimo recomendado” (ambiente + pacotes + layout de repositório template) adaptada para fluxos de trabalho de aprendizado de máquina clássico, aprendizado profundo ou focados em LLM.