Até aqui, os melhores modelos para dados tabulares foram os ensembles de árvores. Mas quando os dados são imagens, texto, áudio ou sequências, a engenharia manual de features atinge um teto — e é aí que o deep learning domina. Uma rede neural profunda aprende as próprias representações a partir dos dados brutos, dispensando boa parte da feature engineering manual. Esta aula constrói a base: o que é um neurônio, como redes aprendem por backpropagation, e como treiná-las de forma estável sem cair nas muitas armadilhas do caminho.
Entender a arquitetura de um MLP (perceptron multicamadas), o mecanismo de backpropagation e descida de gradiente, as funções de ativação e perda, e — sobretudo — as técnicas práticas que fazem o treino convergir: normalização, inicialização, otimizadores, regularização e early stopping.
Do neurônio à rede
Um neurônio artificial calcula uma combinação linear das entradas mais um viés, e aplica uma ativação não-linear:
saída = ativação(w·x + b)
Empilhando neurônios em camadas e camadas em profundidade, obtemos um MLP. A não-linearidade é essencial: sem ela, empilhar camadas lineares resultaria em... outra função linear. É a ativação que dá à rede o poder de aproximar funções complexas (teorema da aproximação universal).
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, n_in, n_hidden, n_out):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_in, n_hidden),
nn.ReLU(),
nn.BatchNorm1d(n_hidden),
nn.Dropout(0.3),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(n_hidden, n_out), # logits (sem softmax aqui)
)
def forward(self, x):
return self.net(x)
modelo = MLP(20, 64, 2)
print(modelo)
print("Parâmetros:", sum(p.numel() for p in modelo.parameters()))
Funções de ativação
- ReLU (
max(0, x)): padrão atual. Simples, rápida, evita o desaparecimento de gradiente das saturações. Problema: "dying ReLU" (neurônios que morrem em zero). - Leaky ReLU / GELU / SiLU: variantes que resolvem o dying ReLU; GELU é padrão em Transformers.
- Sigmoid / Tanh: saturam nas pontas (gradiente ~0), causando vanishing gradient em redes profundas. Use sigmoid apenas na saída de classificação binária.
- Softmax: na saída de classificação multiclasse, converte logits em probabilidades.
Função de perda e o que a rede minimiza
- Classificação: cross-entropy (entropia cruzada). No PyTorch,
nn.CrossEntropyLossrecebe logits e aplica softmax internamente — não aplique softmax antes. - Regressão: MSE (erro quadrático) ou MAE/Huber (robusta a outliers).
criterio = nn.CrossEntropyLoss() # espera logits + rótulos inteiros
# criterio = nn.MSELoss() # para regressão
Backpropagation e descida de gradiente
O treino é um loop: a rede faz uma predição (forward), calcula a perda, propaga o gradiente da perda de volta pelos pesos (backward, via regra da cadeia) e ajusta cada peso na direção que reduz a perda. O PyTorch calcula os gradientes automaticamente (autograd).
from torch.utils.data import TensorDataset, DataLoader
import torch
# Dados sintéticos
X = torch.randn(3000, 20)
y = (X[:, :5].sum(dim=1) > 0).long()
loader = DataLoader(TensorDataset(X, y), batch_size=64, shuffle=True)
modelo = MLP(20, 64, 2)
otimizador = torch.optim.Adam(modelo.parameters(), lr=1e-3, weight_decay=1e-4)
criterio = nn.CrossEntropyLoss()
for epoca in range(10):
modelo.train()
perda_total = 0.0
for xb, yb in loader:
otimizador.zero_grad() # zera gradientes acumulados
logits = modelo(xb) # forward
perda = criterio(logits, yb) # perda
perda.backward() # backward (calcula gradientes)
otimizador.step() # atualiza pesos
perda_total += perda.item()
print(f"Época {epoca+1}: perda={perda_total/len(loader):.4f}")
O zero_grad() é obrigatório — o PyTorch acumula gradientes por padrão; esquecê-lo é um bug clássico.
Otimizadores
- SGD: o clássico; com momentum acelera e escapa de mínimos rasos.
- Adam: taxa de aprendizado adaptativa por parâmetro; o padrão de fato, converge rápido e é robusto.
- AdamW: Adam com weight decay corretamente desacoplado; preferível quando se usa regularização L2.
A taxa de aprendizado é o hiperparâmetro mais importante. Alta demais diverge; baixa demais fica presa. Use schedulers (cosine annealing, redução no platô) e, idealmente, um learning rate finder.
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(otimizador, patience=3)
# ...após cada época: scheduler.step(perda_validacao)
Normalização e inicialização — por que o treino "não aprende"
Dois motivos frequentes de um treino que não converge:
- Dados não normalizados: redes treinam mal com features em escalas muito diferentes. Sempre padronize as entradas (z-score).
- Inicialização ruim: pesos mal inicializados causam gradientes que explodem ou desaparecem. Use inicialização de He (para ReLU) ou Xavier/Glorot (para tanh) — o PyTorch já faz razoavelmente por padrão.
Batch Normalization normaliza as ativações internas por mini-batch, estabilizando e acelerando o treino. Layer Normalization é a variante usada em Transformers. Elas reduzem a sensibilidade à inicialização e permitem taxas de aprendizado maiores.
Regularização — o combate ao overfitting
Redes têm milhões de parâmetros e overfittam com facilidade. Arsenal:
- Dropout: desliga aleatoriamente uma fração dos neurônios no treino, forçando redundância. Desativa-se automaticamente na avaliação (
modelo.eval()). - Weight decay (L2): penaliza pesos grandes (parâmetro
weight_decaydo otimizador). - Early stopping: para quando a perda de validação para de melhorar — talvez o regularizador mais eficaz.
- Data augmentation: aumenta artificialmente os dados, o que é decisivo em visão computacional.
modelo.eval() # desativa dropout e usa estatísticas fixas do BatchNorm
with torch.no_grad():
logits = modelo(X_val)
pred = logits.argmax(dim=1)
Esquecer modelo.eval() na avaliação (deixando dropout/BN em modo treino) é outro bug clássico que corrompe as métricas.
O loop de treino completo com validação e early stopping
import copy
def treinar(modelo, tr_loader, val_loader, epocas=100, paciencia=10):
otim = torch.optim.AdamW(modelo.parameters(), lr=1e-3, weight_decay=1e-4)
criterio = nn.CrossEntropyLoss()
melhor_perda, melhor_estado, espera = float("inf"), None, 0
for ep in range(epocas):
# --- treino ---
modelo.train()
for xb, yb in tr_loader:
otim.zero_grad()
criterio(modelo(xb), yb).backward()
otim.step()
# --- validação ---
modelo.eval()
perda_val = 0.0
with torch.no_grad():
for xb, yb in val_loader:
perda_val += criterio(modelo(xb), yb).item()
perda_val /= len(val_loader)
# --- early stopping ---
if perda_val < melhor_perda:
melhor_perda, melhor_estado, espera = perda_val, copy.deepcopy(modelo.state_dict()), 0
else:
espera += 1
if espera >= paciencia:
print(f"Early stopping na época {ep+1}")
break
modelo.load_state_dict(melhor_estado) # restaura o melhor modelo
return modelo
Este esqueleto — treino/validação por época, monitoramento da perda de validação, restauração do melhor estado — é o padrão que você reaproveitará em qualquer projeto de deep learning.
Deep learning vale a pena para dados tabulares?
Nem sempre. Para tabelas de tamanho pequeno/médio, gradient boosting (XGBoost/LightGBM) frequentemente supera redes, treina mais rápido e exige menos ajuste. Redes brilham quando: os dados são de alta dimensão e estruturados (imagens, texto, áudio, sequências); há muitos dados; ou você precisa de aprendizado de representação e transfer learning. Escolha a ferramenta pelo problema, não pela moda.
Um MLP empilha neurônios, cada um uma combinação linear seguida de ativação não linear, e é a não linearidade que dá o poder de aproximação — sem ela, o empilhamento colapsa numa única transformação linear. A ReLU, e variantes como a GELU, é a ativação padrão; sigmoid e tanh saturam e provocam vanishing gradient em profundidade. As perdas usuais são cross-entropy na classificação, recebendo logits, e MSE ou Huber na regressão. O treino é backpropagation com descida de gradiente, e dois detalhes cobram caro quando esquecidos: o zero_grad() antes do backward e o modelo.eval() na avaliação. Adam e AdamW são os otimizadores padrão, com a taxa de aprendizado no papel de hiperparâmetro central e os schedulers ajudando a conduzi-la. Para o treino ficar estável, normalize as entradas, use inicialização adequada — He ou Xavier — e BatchNorm ou LayerNorm. Contra o overfitting, dropout, weight decay, early stopping e aumentação de dados. E uma régua honesta sobre quando usar: em tabelas pequenas e médias o boosting costuma vencer; as redes brilham em dados de alta dimensão e quando há transfer learning disponível.
Exercícios
-
Por que uma rede composta apenas por camadas lineares (sem funções de ativação não-lineares) não é mais poderosa que uma única camada linear?
✓ Resposta:A composição de transformações lineares é, ela mesma, uma transformação linear. Se cada camada calcula
Wx + bsem não-linearidade, empilhar duas camadas dáW₂(W₁x + b₁) + b₂ = (W₂W₁)x + (W₂b₁ + b₂), que é equivalente a uma única camada linear com pesosW₂W₁e viés combinado. Ou seja, profundidade não adiciona capacidade expressiva sem não-linearidade — a rede só consegue representar funções lineares, independentemente de quantas camadas tenha. As funções de ativação não-lineares (ReLU, etc.) quebram essa colapsabilidade e permitem que a rede aproxime funções arbitrariamente complexas (teorema da aproximação universal). -
O que faz
optimizer.zero_grad()e por que esquecê-lo causa um bug?✓ Resposta:O PyTorch acumula (soma) os gradientes no atributo
.gradde cada parâmetro a cada chamada debackward(), em vez de sobrescrevê-los.zero_grad()zera esses gradientes acumulados antes de calcular os do batch atual. Se você esquecê-lo, os gradientes de mini-batches sucessivos se somam indevidamente, de modo que cada atualização usa uma mistura dos gradientes de vários batches. Isso corrompe a descida de gradiente, geralmente fazendo o treino divergir ou se comportar de forma errática. (O acúmulo é intencional e útil em casos específicos, como simular batches grandes, mas no loop padrão deve-se zerar a cada passo.) -
Por que é importante chamar
modelo.eval()antes de avaliar, e o que muda no comportamento da rede?✓ Resposta:modelo.eval()coloca a rede em modo de avaliação, o que altera camadas cujo comportamento difere entre treino e inferência — principalmente Dropout e BatchNorm. Em modo treino, o Dropout desliga neurônios aleatoriamente e o BatchNorm usa as estatísticas (média/variância) do mini-batch atual. Em modo avaliação, o Dropout é desativado (usa todos os neurônios) e o BatchNorm usa as estatísticas acumuladas durante o treino, não as do batch. Se você avaliar sem chamareval(), as métricas ficam ruidosas e enviesadas (dropout aleatório nas predições, BN dependente do batch de teste), corrompendo a avaliação. Lembre também de voltar amodelo.train()antes de continuar treinando. -
Explique o problema do vanishing gradient e como ReLU, normalização e inicialização adequada ajudam a mitigá-lo.
✓ Resposta:No vanishing gradient, os gradientes propagados de volta pelas camadas encolhem exponencialmente, de modo que as camadas iniciais recebem gradientes minúsculos e quase não aprendem. Ocorre principalmente com ativações saturantes (sigmoid, tanh), cujas derivadas são próximas de zero nas extremidades, multiplicadas ao longo de muitas camadas. ReLU ajuda porque sua derivada é 1 na região positiva (não satura para valores grandes), preservando a magnitude do gradiente. Inicialização adequada (He para ReLU, Xavier para tanh) mantém a variância dos sinais e gradientes estável entre camadas, evitando que encolham ou explodam. Normalização (BatchNorm/LayerNorm) mantém as ativações em uma faixa bem-comportada a cada camada, estabilizando a escala dos gradientes e permitindo treinar redes profundas com taxas de aprendizado maiores.
-
Você tem um dataset tabular com 5.000 linhas e 30 features. Vale a pena usar uma rede neural profunda? Justifique.
✓ Resposta:Provavelmente não como primeira escolha. Para dados tabulares de tamanho pequeno/médio (milhares de linhas), modelos de gradient boosting (XGBoost, LightGBM, CatBoost) tipicamente superam redes neurais, treinam muito mais rápido, exigem menos ajuste de hiperparâmetros e lidam naturalmente com features heterogêneas e faltantes. Redes profundas costumam precisar de muitos dados para brilhar e de cuidadosa normalização/regularização, oferecendo pouca vantagem em tabelas desse porte. A recomendação é começar com boosting como baseline forte; considerar redes apenas se houver estrutura especial (muitas categóricas de altíssima cardinalidade favorecendo embeddings, necessidade de transfer learning, ou integração com dados não estruturados) ou se o volume de dados crescer bastante.
Fontes e leituras recomendadas
- PyTorch — Deep Learning with PyTorch: A 60 Minute Blitz
- Dive into Deep Learning (livro aberto)
- Goodfellow, Bengio, Courville — Deep Learning (livro): deeplearningbook.org
- Kingma & Ba (2015) — Adam: A Method for Stochastic Optimization
- Ioffe & Szegedy (2015) — Batch Normalization
- Srivastava et al. (2014) — Dropout