Phase 03 - Lesson 13
Depurando Redes Neurais
Sua rede compilou. Ela rodou. Produziu um número. O número está errado e nada quebrou. Bem-vindo ao tipo mais difícil de depuração -- aquele em que não existe mensagem de erro.
Tipo: Prática Linguagens: Python, PyTorch Pré-requisitos: Fase 03 Lições 01-10 (especialmente retropropagação, funções de perda, otimizadores) Tempo: ~90 minutos
Objetivos de Aprendizagem
- Diagnosticar falhas comuns de redes neurais (perda NaN, curva de perda achatada, overfitting, oscilação) usando estratégias sistemáticas de depuração
- Aplicar a técnica de "fazer overfit em um batch" para verificar se a arquitetura do seu modelo e o laço de treinamento estão corretos
- Inspecionar magnitudes de gradiente, distribuições de ativação e normas de pesos para identificar problemas de gradientes que desaparecem/explodem
- Construir um checklist de depuração que cobre problemas no pipeline de dados, arquitetura do modelo, função de perda, otimizador e taxa de aprendizado
O Problema
Software tradicional quebra quando está com defeito. Um ponteiro nulo lança uma exceção. Uma incompatibilidade de tipos falha em tempo de compilação. Um erro de "off-by-one" produz uma saída claramente errada.
Redes neurais não te dão esse luxo.
Uma rede neural com defeito roda até o fim, imprime um valor de perda e produz previsões. A perda pode até diminuir. As previsões podem até parecer plausíveis. Mas o modelo está silenciosamente errado -- aprendendo atalhos, memorizando ruído ou convergindo para um mínimo local inútil. Pesquisadores do Google estimaram que 60-70% do tempo de depuração de ML é gasto em bugs "silenciosos" que não produzem erros, mas degradam a qualidade do modelo.
A diferença entre um modelo que funciona e um que está quebrado costuma ser uma única linha mal colocada: um zero_grad() faltando, uma dimensão transposta, uma taxa de aprendizado errada por um fator de 10x. A canônica "Recipe for Training Neural Networks" (2019) abre com isto: "Os erros mais comuns em redes neurais são bugs que não quebram."
Esta lição te ensina a encontrar esses bugs.
O Conceito
A Mentalidade de Depuração
Esqueça a depuração de imprimir-e-rezar. Depurar redes neurais exige uma abordagem sistemática porque o ciclo de feedback é lento (minutos a horas por execução de treinamento) e os sintomas são ambíguos (uma perda ruim pode significar 20 coisas diferentes).
A regra de ouro: comece simples, adicione complexidade uma peça de cada vez e verifique cada peça de forma independente.
flowchart TD
A["Perda não diminui"] --> B{"Verificar taxa de aprendizado"}
B -->|"Muito alta"| C["Perda oscila ou explode"]
B -->|"Muito baixa"| D["Perda mal se move"]
B -->|"Razoável"| E{"Verificar gradientes"}
E -->|"Todos zero"| F["ReLUs mortas ou gradientes que desaparecem"]
E -->|"NaN/Inf"| G["Gradientes que explodem"]
E -->|"Normal"| H{"Verificar pipeline de dados"}
H -->|"Rótulos embaralhados"| I["Acurácia de chance aleatória"]
H -->|"Bug de pré-processamento"| J["Modelo aprende ruído"]
H -->|"Dados estão OK"| K{"Verificar arquitetura"}
K -->|"Pequena demais"| L["Underfitting"]
K -->|"Profunda demais"| M["Dificuldade de otimização"]
Sintoma 1: Perda Não Diminui
Esta é a reclamação mais comum. O laço de treinamento roda, as épocas passam, e a perda fica achatada ou oscila descontroladamente.
Taxa de aprendizado errada. Muito alta: a perda oscila ou pula para NaN. Muito baixa: a perda diminui tão lentamente que parece achatada. Para o Adam, comece em 1e-3. Para o SGD, comece em 1e-1 ou 1e-2. Sempre teste 3 taxas de aprendizado abrangendo 10x cada (por exemplo, 1e-2, 1e-3, 1e-4) antes de concluir que algo mais está errado.
ReLUs mortas. Se um neurônio ReLU recebe uma entrada negativa grande, ele produz 0 e seu gradiente é 0. Ele nunca mais ativa. Se neurônios suficientes morrem, a rede não consegue aprender. Verifique: imprima a fração de ativações que são exatamente 0 após cada camada ReLU. Se >50% estiverem mortas, troque para LeakyReLU ou reduza a taxa de aprendizado.
Gradientes que desaparecem. Em redes profundas com ativações sigmoid ou tanh, os gradientes encolhem exponencialmente à medida que se propagam para trás. Quando chegam à primeira camada, eles estão em ~0. As primeiras camadas param de aprender. Solução: use ReLU/GELU, adicione conexões residuais ou use normalização em batch.
Gradientes que explodem. O problema oposto -- os gradientes crescem exponencialmente. Comum em RNNs e redes muito profundas. A perda pula para NaN. Solução: recorte de gradiente (torch.nn.utils.clip_grad_norm_), reduza a taxa de aprendizado ou adicione normalização.
Sintoma 2: Perda Diminuindo, Mas o Modelo é Ruim
A perda cai. A acurácia de treinamento atinge 99%. Mas a acurácia de teste é 55%. Ou o modelo produz saídas sem sentido em dados reais.
Overfitting. O modelo memoriza os dados de treinamento em vez de aprender padrões. A diferença entre as perdas de treinamento e validação cresce ao longo do tempo. Solução: mais dados, dropout, decaimento de pesos, parada antecipada, aumento de dados.
Vazamento de dados. Dados de teste vazaram para o treinamento. A acurácia é suspeitamente alta. Causas comuns: embaralhar antes de dividir, pré-processar com estatísticas do conjunto de dados completo, amostras duplicadas entre as divisões. Solução: divida primeiro, pré-processe depois, verifique duplicatas.
Erros de rótulo. 5-10% dos rótulos na maioria dos conjuntos de dados reais estão errados (Northcutt et al., 2021 -- "Pervasive Label Errors in Test Sets"). O modelo aprende o ruído. Solução: use aprendizado confiante (confident learning) para encontrar e corrigir exemplos mal rotulados, ou use truncamento de perda para ignorar amostras de alta perda.
Sintoma 3: NaN ou Inf na Perda
O valor da perda se torna nan ou inf. O treinamento está morto.
Taxa de aprendizado alta demais. As atualizações de gradiente ultrapassam tanto que os pesos explodem. Solução: reduza por 10x.
log(0) ou log(negativo). A perda de entropia cruzada calcula log(p). Se seu modelo produzir exatamente 0 ou uma probabilidade negativa, o log explode. Solução: limite as previsões a [eps, 1-eps] onde eps=1e-7.
Divisão por zero. A normalização em batch divide pelo desvio padrão. Um batch com valores constantes tem std=0. Solução: adicione epsilon ao denominador (o PyTorch faz isso por padrão, mas implementações customizadas podem não fazer).
Overflow numérico. Ativações grandes alimentadas em exp() produzem Inf. O softmax é especialmente propenso. Solução: subtraia o máximo antes de exponenciar (o truque do log-sum-exp).
Técnica 1: Verificação de Gradiente
Compare seus gradientes analíticos (da retropropagação) com os gradientes numéricos (de diferenças finitas). Se eles discordarem, sua passagem para trás (backward pass) tem um bug.
Gradiente numérico para o parâmetro w:
grad_numerical = (loss(w + eps) - loss(w - eps)) / (2 * eps)
Métrica de concordância (diferença relativa):
rel_diff = |grad_analytical - grad_numerical| / max(|grad_analytical|, |grad_numerical|, 1e-8)
Se rel_diff < 1e-5: correto. Se rel_diff > 1e-3: quase certamente um bug.
flowchart LR
A["Parâmetro w"] --> B["w + eps"]
A --> C["w - eps"]
B --> D["Passagem para frente"]
C --> E["Passagem para frente"]
D --> F["loss+"]
E --> G["loss-"]
F --> H["(loss+ - loss-) / 2eps"]
G --> H
H --> I["Comparar com gradiente da retropropagação"]
Técnica 2: Estatísticas de Ativação
Monitore a média e o desvio padrão das ativações após cada camada durante o treinamento. Redes saudáveis mantêm ativações com média próxima de 0 e desvio padrão próximo de 1 (após normalização) ou ao menos limitadas.
| Indicador de saúde | Média | Desvio padrão | Diagnóstico |
|---|---|---|---|
| Saudável | ~0 | ~1 | A rede está aprendendo normalmente |
| Saturada | >>0 ou <<0 | ~0 | Ativações presas em valores extremos |
| Morta | 0 | 0 | Neurônios estão mortos (todos zero) |
| Explodindo | >>10 | >>10 | Ativações crescendo sem limite |
Técnica 3: Visualização do Fluxo de Gradiente
Plote a magnitude média do gradiente para cada camada. Em uma rede saudável, as magnitudes de gradiente devem ser aproximadamente semelhantes entre as camadas. Se as primeiras camadas têm gradientes 1000x menores que as camadas posteriores, você tem gradientes que desaparecem.
graph LR
subgraph "Fluxo de Gradiente Saudável"
L1["Camada 1<br/>grad: 0.05"] --- L2["Camada 2<br/>grad: 0.04"] --- L3["Camada 3<br/>grad: 0.06"] --- L4["Camada 4<br/>grad: 0.05"]
end
graph LR
subgraph "Fluxo de Gradiente que Desaparece"
V1["Camada 1<br/>grad: 0.0001"] --- V2["Camada 2<br/>grad: 0.003"] --- V3["Camada 3<br/>grad: 0.02"] --- V4["Camada 4<br/>grad: 0.08"]
end
Técnica 4: O Teste de Overfit em Um Batch
A técnica de depuração mais importante em deep learning.
Pegue um pequeno batch (8-32 amostras). Treine nele por mais de 100 iterações. A perda deve ir a quase zero e a acurácia de treinamento deve atingir 100%. Se isso não acontecer, seu modelo ou laço de treinamento tem um bug fundamental -- não prossiga para o treinamento completo.
Este teste detecta:
- Funções de perda quebradas
- Passagens para trás (backward passes) quebradas
- Arquitetura pequena demais para representar os dados
- Otimizador não conectado aos parâmetros do modelo
- Dados e rótulos desalinhados
Isso leva 30 segundos para rodar e poupa horas de depuração de execuções completas de treinamento.
Técnica 5: Localizador de Taxa de Aprendizado
Leslie Smith (2017) propôs varrer a taxa de aprendizado de muito pequena (1e-7) a muito grande (10) ao longo de uma época, registrando a perda. Plote a perda versus a taxa de aprendizado. A taxa de aprendizado ótima é aproximadamente 10x menor que a taxa em que a perda começa a diminuir mais rapidamente.
graph TD
subgraph "Gráfico do Localizador de LR"
direction LR
A["1e-7: loss=2.3"] --> B["1e-5: loss=2.3"]
B --> C["1e-3: loss=1.8"]
C --> D["1e-2: loss=0.9 -- mais íngreme"]
D --> E["1e-1: loss=0.5"]
E --> F["1.0: loss=NaN -- alta demais"]
end
Melhor LR neste exemplo: ~1e-3 (uma ordem de magnitude antes do ponto mais íngreme).
Bugs Comuns de PyTorch
Estes são os bugs que desperdiçam mais horas coletivas na comunidade PyTorch:
| Bug | Sintoma | Solução |
|---|---|---|
Esquecer optimizer.zero_grad() |
Gradientes acumulam entre batches, a perda oscila | Adicione optimizer.zero_grad() antes de loss.backward() |
Esquecer model.eval() no momento do teste |
Dropout e batch norm se comportam de forma diferente, a acurácia de teste varia entre execuções | Adicione model.eval() e torch.no_grad() |
| Formatos de tensor errados | Broadcasting silencioso produz resultados errados, sem erro | Imprima os formatos após cada operação durante a depuração |
| Incompatibilidade CPU/GPU | RuntimeError: expected CUDA tensor |
Use .to(device) no modelo E nos dados |
| Não desanexar tensores | O grafo de computação cresce para sempre, OOM | Use .detach() ou with torch.no_grad() |
| Operações in-place quebrando o autograd | RuntimeError: modified by in-place operation |
Substitua x += 1 por x = x + 1 |
| Dados não normalizados | Perda presa no nível de chance aleatória | Normalize as entradas para média=0, std=1 |
| Rótulos com dtype errado | Entropia cruzada espera Long, recebeu Float |
Converta os rótulos: labels.long() |
A Tabela Mestra de Depuração
| Sintoma | Causa provável | Primeira coisa a tentar |
|---|---|---|
| Perda presa em -log(1/num_classes) | Modelo prevendo distribuição uniforme | Verifique o pipeline de dados, confirme se os rótulos correspondem às entradas |
| Perda NaN após alguns passos | Taxa de aprendizado alta demais | Reduza a LR por 10x |
| Perda NaN imediatamente | log(0) ou divisão por zero | Adicione epsilon às operações de log/divisão |
| Perda oscilando descontroladamente | LR alta demais ou batch size pequeno demais | Reduza a LR, aumente o batch size |
| Perda diminuindo e depois estagnando | LR alta demais para a fase de ajuste fino | Adicione um agendamento de LR (decaimento cosseno ou em degraus) |
| Acurácia de treino alta, acurácia de teste baixa | Overfitting | Adicione dropout, decaimento de pesos, mais dados |
| Acurácia de treino = acurácia de teste = chance | Modelo não está aprendendo nada | Execute o teste de overfit em um batch |
| Acurácia de treino = acurácia de teste, mas ambas baixas | Underfitting | Modelo maior, mais camadas, mais features |
| Gradientes todos zero | ReLUs mortas ou grafo de computação desanexado | Troque para LeakyReLU, verifique .requires_grad |
| Sem memória durante o treinamento | Batch grande demais ou grafo não liberado | Reduza o batch size, use torch.no_grad() na avaliação |
Construa
Um kit de diagnóstico que monitora ativações, gradientes e curvas de perda. Você vai deliberadamente quebrar uma rede e usar o kit para diagnosticar cada problema.
Passo 1: A Classe NetworkDebugger
Conecta-se (hooks) a um modelo PyTorch para registrar estatísticas de ativação e gradiente por camada.
import torch
import torch.nn as nn
import math
class NetworkDebugger:
def __init__(self, model):
self.model = model
self.activation_stats = {}
self.gradient_stats = {}
self.loss_history = []
self.lr_losses = []
self.hooks = []
self._register_hooks()
def _register_hooks(self):
for name, module in self.model.named_modules():
if isinstance(module, (nn.Linear, nn.Conv2d, nn.ReLU, nn.LeakyReLU)):
hook = module.register_forward_hook(self._make_activation_hook(name))
self.hooks.append(hook)
hook = module.register_full_backward_hook(self._make_gradient_hook(name))
self.hooks.append(hook)
def _make_activation_hook(self, name):
def hook(module, input, output):
with torch.no_grad():
out = output.detach().float()
self.activation_stats[name] = {
"mean": out.mean().item(),
"std": out.std().item(),
"fraction_zero": (out == 0).float().mean().item(),
"min": out.min().item(),
"max": out.max().item(),
}
return hook
def _make_gradient_hook(self, name):
def hook(module, grad_input, grad_output):
if grad_output[0] is not None:
with torch.no_grad():
grad = grad_output[0].detach().float()
self.gradient_stats[name] = {
"mean": grad.mean().item(),
"std": grad.std().item(),
"abs_mean": grad.abs().mean().item(),
"max": grad.abs().max().item(),
}
return hook
def record_loss(self, loss_value):
self.loss_history.append(loss_value)
def check_loss_health(self):
if len(self.loss_history) < 2:
return "NOT_ENOUGH_DATA"
recent = self.loss_history[-10:]
if any(math.isnan(v) or math.isinf(v) for v in recent):
return "NAN_OR_INF"
if len(self.loss_history) >= 20:
first_half = sum(self.loss_history[:10]) / 10
second_half = sum(self.loss_history[-10:]) / 10
if second_half >= first_half * 0.99:
return "NOT_DECREASING"
if len(recent) >= 5:
diffs = [recent[i+1] - recent[i] for i in range(len(recent)-1)]
if max(diffs) - min(diffs) > 2 * abs(sum(diffs) / len(diffs)):
return "OSCILLATING"
return "HEALTHY"
def check_activations(self):
issues = []
for name, stats in self.activation_stats.items():
if stats["fraction_zero"] > 0.5:
issues.append(f"DEAD_NEURONS: {name} has {stats['fraction_zero']:.0%} zero activations")
if abs(stats["mean"]) > 10:
issues.append(f"EXPLODING_ACTIVATIONS: {name} mean={stats['mean']:.2f}")
if stats["std"] < 1e-6:
issues.append(f"COLLAPSED_ACTIVATIONS: {name} std={stats['std']:.2e}")
return issues if issues else ["HEALTHY"]
def check_gradients(self):
issues = []
grad_magnitudes = []
for name, stats in self.gradient_stats.items():
grad_magnitudes.append((name, stats["abs_mean"]))
if stats["abs_mean"] < 1e-7:
issues.append(f"VANISHING_GRADIENT: {name} abs_mean={stats['abs_mean']:.2e}")
if stats["abs_mean"] > 100:
issues.append(f"EXPLODING_GRADIENT: {name} abs_mean={stats['abs_mean']:.2e}")
if len(grad_magnitudes) >= 2:
first_mag = grad_magnitudes[0][1]
last_mag = grad_magnitudes[-1][1]
if last_mag > 0 and first_mag / last_mag > 100:
issues.append(f"GRADIENT_RATIO: first/last = {first_mag/last_mag:.0f}x (vanishing)")
return issues if issues else ["HEALTHY"]
def print_report(self):
print("\n=== NETWORK DEBUGGER REPORT ===")
print(f"\nLoss health: {self.check_loss_health()}")
if self.loss_history:
print(f" Last 5 losses: {[f'{v:.4f}' for v in self.loss_history[-5:]]}")
print("\nActivation diagnostics:")
for item in self.check_activations():
print(f" {item}")
print("\nGradient diagnostics:")
for item in self.check_gradients():
print(f" {item}")
print("\nPer-layer activation stats:")
for name, stats in self.activation_stats.items():
print(f" {name}: mean={stats['mean']:.4f} std={stats['std']:.4f} zero={stats['fraction_zero']:.1%}")
print("\nPer-layer gradient stats:")
for name, stats in self.gradient_stats.items():
print(f" {name}: abs_mean={stats['abs_mean']:.2e} max={stats['max']:.2e}")
def remove_hooks(self):
for hook in self.hooks:
hook.remove()
self.hooks.clear()
Passo 2: O Teste de Overfit em Um Batch
def overfit_one_batch(model, x_batch, y_batch, criterion, lr=0.01, steps=200):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
model.train()
print("\n=== OVERFIT ONE BATCH TEST ===")
print(f"Batch size: {x_batch.shape[0]}, Steps: {steps}")
for step in range(steps):
optimizer.zero_grad()
output = model(x_batch)
loss = criterion(output, y_batch)
loss.backward()
optimizer.step()
if step % 50 == 0 or step == steps - 1:
with torch.no_grad():
preds = (output > 0).float() if output.shape[-1] == 1 else output.argmax(dim=1)
targets = y_batch if y_batch.dim() == 1 else y_batch.squeeze()
acc = (preds.squeeze() == targets).float().mean().item()
print(f" Step {step:3d} | Loss: {loss.item():.6f} | Accuracy: {acc:.1%}")
final_loss = loss.item()
if final_loss > 0.1:
print(f"\n FAIL: Loss did not converge ({final_loss:.4f}). Model or training loop is broken.")
return False
print(f"\n PASS: Loss converged to {final_loss:.6f}")
return True
Passo 3: Localizador de Taxa de Aprendizado
def find_learning_rate(model, x_data, y_data, criterion, start_lr=1e-7, end_lr=10, steps=100):
import copy
original_state = copy.deepcopy(model.state_dict())
optimizer = torch.optim.SGD(model.parameters(), lr=start_lr)
lr_mult = (end_lr / start_lr) ** (1 / steps)
model.train()
results = []
best_loss = float("inf")
current_lr = start_lr
print("\n=== LEARNING RATE FINDER ===")
for step in range(steps):
optimizer.zero_grad()
output = model(x_data)
loss = criterion(output, y_data)
if math.isnan(loss.item()) or loss.item() > best_loss * 10:
break
best_loss = min(best_loss, loss.item())
results.append((current_lr, loss.item()))
loss.backward()
optimizer.step()
current_lr *= lr_mult
for param_group in optimizer.param_groups:
param_group["lr"] = current_lr
model.load_state_dict(original_state)
if len(results) < 10:
print(" Could not complete LR sweep -- loss diverged too quickly")
return results
min_loss_idx = min(range(len(results)), key=lambda i: results[i][1])
suggested_lr = results[max(0, min_loss_idx - 10)][0]
print(f" Swept {len(results)} steps from {start_lr:.0e} to {results[-1][0]:.0e}")
print(f" Minimum loss {results[min_loss_idx][1]:.4f} at lr={results[min_loss_idx][0]:.2e}")
print(f" Suggested learning rate: {suggested_lr:.2e}")
return results
Passo 4: Verificador de Gradiente
def _flat_to_multi_index(flat_idx, shape):
multi_idx = []
remaining = flat_idx
for dim in reversed(shape):
multi_idx.insert(0, remaining % dim)
remaining //= dim
return tuple(multi_idx)
def gradient_check(model, x, y, criterion, eps=1e-4):
model.train()
x_double = x.double()
y_double = y.double()
model_double = model.double()
print("\n=== GRADIENT CHECK ===")
overall_max_diff = 0
checked = 0
for name, param in model_double.named_parameters():
if not param.requires_grad:
continue
layer_max_diff = 0
model_double.zero_grad()
output = model_double(x_double)
loss = criterion(output, y_double)
loss.backward()
analytical_grad = param.grad.clone()
num_checks = min(5, param.numel())
for i in range(num_checks):
idx = _flat_to_multi_index(i, param.shape)
original = param.data[idx].item()
param.data[idx] = original + eps
with torch.no_grad():
loss_plus = criterion(model_double(x_double), y_double).item()
param.data[idx] = original - eps
with torch.no_grad():
loss_minus = criterion(model_double(x_double), y_double).item()
param.data[idx] = original
numerical = (loss_plus - loss_minus) / (2 * eps)
analytical = analytical_grad[idx].item()
denom = max(abs(numerical), abs(analytical), 1e-8)
rel_diff = abs(numerical - analytical) / denom
layer_max_diff = max(layer_max_diff, rel_diff)
checked += 1
overall_max_diff = max(overall_max_diff, layer_max_diff)
status = "OK" if layer_max_diff < 1e-5 else "MISMATCH"
print(f" {name}: max_rel_diff={layer_max_diff:.2e} [{status}]")
model.float()
print(f"\n Checked {checked} parameters")
if overall_max_diff < 1e-5:
print(" PASS: Gradients match (rel_diff < 1e-5)")
elif overall_max_diff < 1e-3:
print(" WARN: Small differences (1e-5 < rel_diff < 1e-3)")
else:
print(" FAIL: Gradient mismatch detected (rel_diff > 1e-3)")
return overall_max_diff
Passo 5: Redes Deliberadamente Quebradas
Agora aplique o kit a redes quebradas e diagnostique cada uma.
def demo_broken_networks():
torch.manual_seed(42)
x = torch.randn(64, 10)
y = (x[:, 0] > 0).long()
print("\n" + "=" * 60)
print("BUG 1: Learning rate too high (lr=10)")
print("=" * 60)
model1 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger1 = NetworkDebugger(model1)
optimizer1 = torch.optim.SGD(model1.parameters(), lr=10.0)
criterion = nn.CrossEntropyLoss()
for step in range(20):
optimizer1.zero_grad()
out = model1(x)
loss = criterion(out, y)
debugger1.record_loss(loss.item())
loss.backward()
optimizer1.step()
debugger1.print_report()
debugger1.remove_hooks()
print("\n" + "=" * 60)
print("BUG 2: Dead ReLUs from bad initialization")
print("=" * 60)
model2 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 2))
with torch.no_grad():
for m in model2.modules():
if isinstance(m, nn.Linear):
m.weight.fill_(-1.0)
m.bias.fill_(-5.0)
debugger2 = NetworkDebugger(model2)
optimizer2 = torch.optim.Adam(model2.parameters(), lr=1e-3)
for step in range(50):
optimizer2.zero_grad()
out = model2(x)
loss = criterion(out, y)
debugger2.record_loss(loss.item())
loss.backward()
optimizer2.step()
debugger2.print_report()
debugger2.remove_hooks()
print("\n" + "=" * 60)
print("BUG 3: Missing zero_grad (gradients accumulate)")
print("=" * 60)
model3 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger3 = NetworkDebugger(model3)
optimizer3 = torch.optim.SGD(model3.parameters(), lr=0.01)
for step in range(50):
out = model3(x)
loss = criterion(out, y)
debugger3.record_loss(loss.item())
loss.backward()
optimizer3.step()
debugger3.print_report()
debugger3.remove_hooks()
print("\n" + "=" * 60)
print("HEALTHY NETWORK: Correct setup for comparison")
print("=" * 60)
model_good = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger_good = NetworkDebugger(model_good)
optimizer_good = torch.optim.Adam(model_good.parameters(), lr=1e-3)
for step in range(50):
optimizer_good.zero_grad()
out = model_good(x)
loss = criterion(out, y)
debugger_good.record_loss(loss.item())
loss.backward()
optimizer_good.step()
debugger_good.print_report()
debugger_good.remove_hooks()
print("\n" + "=" * 60)
print("OVERFIT-ONE-BATCH TEST (healthy model)")
print("=" * 60)
model_test = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
overfit_one_batch(model_test, x[:8], y[:8], criterion)
print("\n" + "=" * 60)
print("LEARNING RATE FINDER")
print("=" * 60)
model_lr = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
find_learning_rate(model_lr, x, y, criterion)
print("\n" + "=" * 60)
print("GRADIENT CHECK")
print("=" * 60)
model_grad = nn.Sequential(nn.Linear(10, 8), nn.ReLU(), nn.Linear(8, 2))
gradient_check(model_grad, x[:4], y[:4], criterion)
Use
Ferramentas Nativas do PyTorch
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(768, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
with torch.autograd.detect_anomaly():
output = model(input_tensor)
loss = criterion(output, target)
loss.backward()
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}: grad_mean={param.grad.abs().mean():.2e}")
Integração com o Weights & Biases
import wandb
wandb.init(project="debug-training")
for epoch in range(100):
loss = train_one_epoch()
wandb.log({
"loss": loss,
"lr": optimizer.param_groups[0]["lr"],
"grad_norm": torch.nn.utils.clip_grad_norm_(model.parameters(), float("inf")),
})
for name, param in model.named_parameters():
if param.grad is not None:
wandb.log({f"grad/{name}": wandb.Histogram(param.grad.cpu().numpy())})
TensorBoard
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/debug_experiment")
for epoch in range(100):
loss = train_one_epoch()
writer.add_scalar("Loss/train", loss, epoch)
for name, param in model.named_parameters():
writer.add_histogram(f"weights/{name}", param, epoch)
if param.grad is not None:
writer.add_histogram(f"gradients/{name}", param.grad, epoch)
O Checklist de Depuração (Antes do Treinamento Completo)
- Execute o teste de overfit em um batch. Se falhar, pare.
- Imprima o resumo do modelo -- verifique se a contagem de parâmetros é razoável.
- Execute uma única passagem para frente com dados aleatórios -- verifique o formato da saída.
- Treine por 5 épocas -- verifique se a perda diminui.
- Verifique as estatísticas de ativação -- sem camadas mortas, sem explosões.
- Verifique o fluxo de gradiente -- sem gradientes que desaparecem, sem explosões.
- Verifique o pipeline de dados -- imprima 5 amostras aleatórias com rótulos.
Entregue
Esta lição produz:
outputs/prompt-nn-debugger.md-- um prompt para diagnosticar falhas de treinamento de redes neuraisoutputs/skill-debug-checklist.md-- um checklist em árvore de decisão para depurar problemas de treinamento
Padrões-chave de implantação para depuração:
- Adicione hooks de monitoramento aos scripts de treinamento em produção
- Registre estatísticas de ativação e gradiente no W&B ou TensorBoard a cada N passos
- Implemente alertas automáticos para perda NaN, neurônios mortos (>80% zero) ou explosão de gradiente
- Sempre execute o teste de overfit em um batch ao mudar arquiteturas ou pipelines de dados
Exercícios
Adicione um detector de gradiente que explode. Modifique o
NetworkDebuggerpara detectar quando os gradientes excedem um limiar e sugerir automaticamente um valor de recorte de gradiente. Teste-o em uma rede de 20 camadas sem normalização.Construa um ressuscitador de neurônios mortos. Escreva uma função que identifique neurônios ReLU mortos (que sempre produzem 0) e reinicialize seus pesos de entrada com a inicialização de Kaiming. Mostre que isso recupera uma rede onde >70% dos neurônios estão mortos.
Implemente o localizador de taxa de aprendizado com plotagem. Estenda
find_learning_ratepara salvar os resultados como um CSV e escreva um script separado que leia o CSV e exiba a curva de LR versus perda usando matplotlib. Identifique a LR ótima para a ResNet-18 no CIFAR-10.Crie um validador de pipeline de dados. Escreva uma função que verifique: amostras duplicadas entre as divisões de treino/teste, desbalanceamento na distribuição de rótulos (proporção >10:1), normalização das entradas (média próxima de 0, std próximo de 1) e valores NaN/Inf nos dados. Execute-a em um conjunto de dados deliberadamente corrompido.
Depure uma falha real. Pegue o mini-framework da Lição 10, introduza um bug sutil (por exemplo, transpor a matriz de pesos no backward) e use a verificação de gradiente para localizar exatamente qual parâmetro tem gradientes incorretos. Documente o processo de depuração.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Bug silencioso | "Roda mas dá resultados ruins" | Um bug que não produz erro, mas degrada a qualidade do modelo -- o modo de falha dominante em ML |
| ReLU morta | "Os neurônios morreram" | Um neurônio ReLU cuja entrada é sempre negativa, então ele produz 0 e recebe gradiente 0 permanentemente |
| Gradientes que desaparecem | "As primeiras camadas param de aprender" | Os gradientes encolhem exponencialmente através das camadas, deixando os pesos das primeiras camadas efetivamente congelados |
| Gradientes que explodem | "A perda foi para NaN" | Os gradientes crescem exponencialmente através das camadas, causando atualizações de pesos tão grandes que estouram (overflow) |
| Verificação de gradiente | "Verificar se a retropropagação está correta" | Comparar gradientes analíticos da retropropagação com gradientes numéricos de diferenças finitas |
| Overfit em um batch | "O teste de depuração mais importante" | Treinar em um único batch pequeno para verificar se o modelo CONSEGUE aprender -- se não conseguir, algo está fundamentalmente quebrado |
| Localizador de LR | "Varredura para encontrar a taxa de aprendizado certa" | Aumentar exponencialmente a taxa de aprendizado ao longo de uma época e escolher a taxa logo antes de a perda divergir |
| Vazamento de dados | "Dados de teste vazaram para o treinamento" | Quando informação do conjunto de teste contamina o treinamento, produzindo uma acurácia artificialmente alta |
| Estatísticas de ativação | "Monitorar a saúde da camada" | Acompanhar a média, o std e a fração de zeros da saída de cada camada para detectar neurônios mortos, saturados ou que explodem |
| Recorte de gradiente | "Limitar a magnitude do gradiente" | Reduzir a escala dos gradientes quando sua norma excede um limiar, prevenindo atualizações por gradientes que explodem |
Leitura Adicional
- Smith, "Cyclical Learning Rates for Training Neural Networks" (2017) -- o artigo que introduz o teste de faixa de taxa de aprendizado (localizador de LR)
- Northcutt et al., "Pervasive Label Errors in Test Sets Destabilize Machine Learning Benchmarks" (2021) -- demonstra que 3-6% dos rótulos no ImageNet, CIFAR-10 e outros benchmarks importantes estão errados
- Zhang et al., "Understanding Deep Learning Requires Rethinking Generalization" (2017) -- o artigo que mostra que redes neurais podem memorizar rótulos aleatórios, que é o motivo pelo qual o teste de overfit em um batch funciona
- Documentação do PyTorch sobre
torch.autograd.detect_anomalyetorch.autograd.set_detect_anomalypara detecção nativa de NaN/Inf