Phase 03 - Lesson 05
Funções de Perda
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Sua rede faz uma previsão. A verdade fundamental diz o contrário. Quão errada ela está? Esse número é a perda. Escolha a função de perda errada e seu modelo vai otimizar para a coisa completamente errada.
Tipo: Build Linguagens: Python Pré-requisitos: Lição 03.04 (Funções de Ativação) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Implementar MSE, entropia cruzada binária, entropia cruzada categórica e perda contrastiva (InfoNCE) do zero, com seus gradientes
- Explicar por que o MSE falha na classificação demonstrando o modo de falha "preveja 0.5 para tudo"
- Aplicar suavização de rótulos (label smoothing) à entropia cruzada e descrever como ela previne previsões superconfiantes
- Escolher a função de perda correta para tarefas de regressão, classificação binária, classificação multiclasse e aprendizagem de embeddings
O Problema
Um modelo minimizando MSE em um problema de classificação vai prever, com confiança, 0.5 para tudo. Ele está minimizando a perda. E também é inútil.
A função de perda é a única coisa que seu modelo de fato otimiza. Não a acurácia. Não o F1 score. Nem qualquer métrica que você reporta para o seu gerente. O otimizador pega o gradiente da função de perda e ajusta os pesos para tornar esse número menor. Se a função de perda não captura aquilo com que você se importa, o modelo vai encontrar a maneira matematicamente mais barata de satisfazê-la, e essa maneira quase nunca é o que você queria.
Aqui vai um exemplo concreto. Você tem uma tarefa de classificação binária. Duas classes, divisão 50/50. Você usa MSE como sua perda. O modelo prevê 0.5 para cada entrada. O MSE médio é 0.25, que é o mínimo possível sem de fato aprender nada. O modelo tem zero capacidade discriminativa, mas tecnicamente minimizou sua função de perda. Troque para entropia cruzada e o mesmo modelo é forçado a empurrar as previsões em direção a 0 ou 1, porque -log(0.5) = 0.693 é uma perda terrível, enquanto -log(0.99) = 0.01 recompensa previsões corretas e confiantes. A escolha da função de perda é a diferença entre um modelo que aprende e um modelo que burla a métrica.
E fica pior. Na aprendizagem autossupervisionada, você nem sequer tem rótulos. A perda contrastiva define inteiramente o sinal de aprendizagem: o que conta como similar, o que conta como diferente e com que força o modelo deve afastá-los. Erre a perda contrastiva e seus embeddings colapsam para um único ponto -- toda entrada mapeia para o mesmo vetor. Tecnicamente, perda zero. Completamente inútil.
O Conceito
Erro Quadrático Médio (MSE)
O padrão para regressão. Compute a diferença ao quadrado entre a previsão e o alvo, faça a média sobre todas as amostras.
MSE = (1/n) * sum((y_pred - y_true)^2)
Por que elevar ao quadrado importa: isso penaliza erros grandes de forma quadrática. Um erro de 2 custa 4x mais que um erro de 1. Um erro de 10 custa 100x. Isso torna o MSE sensível a outliers -- uma única previsão absurdamente errada domina a perda.
Números reais: se seu modelo prevê preços de imóveis e erra por US$ 10.000 na maioria das casas, mas erra por US$ 200.000 em uma mansão, o MSE vai tentar agressivamente consertar aquela mansão, potencialmente prejudicando o desempenho nas outras 99 casas.
O gradiente do MSE em relação a uma previsão é:
dMSE/dy_pred = (2/n) * (y_pred - y_true)
Linear no erro. Erros maiores recebem gradientes maiores. Isso é uma vantagem para regressão (erros grandes precisam de correções grandes) e um defeito para classificação (você quer penalizar respostas erradas e confiantes de forma exponencial, não linear).
Perda de Entropia Cruzada
A função de perda para classificação. Enraizada na teoria da informação -- ela mede a divergência entre a distribuição de probabilidade prevista e a distribuição verdadeira.
Entropia Cruzada Binária (BCE):
BCE = -(y * log(p) + (1 - y) * log(1 - p))
Onde y é o rótulo verdadeiro (0 ou 1) e p é a probabilidade prevista.
Por que -log(p) funciona: quando o rótulo verdadeiro é 1 e você prevê p = 0.99, a perda é -log(0.99) = 0.01. Quando você prevê p = 0.01, a perda é -log(0.01) = 4.6. Essa diferença de 460x é o porquê de a entropia cruzada funcionar. Ela pune brutalmente previsões erradas e confiantes enquanto mal penaliza as corretas e confiantes.
O gradiente conta a mesma história:
dBCE/dp = -(y/p) + (1-y)/(1-p)
Quando y = 1 e p está perto de zero, o gradiente é -1/p, que tende a menos infinito. O modelo recebe um sinal enorme para corrigir seu erro. Quando p está perto de 1, o gradiente é minúsculo. Já está correto, nada a consertar.
Entropia Cruzada Categórica:
Para classificação multiclasse com alvos codificados em one-hot.
CCE = -sum(y_i * log(p_i))
Apenas a classe verdadeira contribui para a perda (porque todos os outros y_i são zero). Se houver 10 classes e a classe correta recebe probabilidade 0.1 (chute aleatório), a perda é -log(0.1) = 2.3. Se a classe correta recebe probabilidade 0.9, a perda é -log(0.9) = 0.105. O modelo aprende a concentrar a massa de probabilidade na resposta certa.
Por que o MSE Falha na Classificação
graph TD
subgraph "MSE na Classificação"
P1["Prever 0.5 para a classe 1<br/>MSE = 0.25"]
P2["Prever 0.9 para a classe 1<br/>MSE = 0.01"]
P3["Prever 0.1 para a classe 1<br/>MSE = 0.81"]
end
subgraph "Entropia Cruzada na Classificação"
C1["Prever 0.5 para a classe 1<br/>CE = 0.693"]
C2["Prever 0.9 para a classe 1<br/>CE = 0.105"]
C3["Prever 0.1 para a classe 1<br/>CE = 2.303"]
end
P3 -->|"O gradiente do MSE<br/>achata perto da<br/>saturação"| Slow["Correção lenta"]
C3 -->|"O gradiente da CE<br/>explode perto da<br/>resposta errada"| Fast["Correção rápida"]
Os gradientes do MSE achatam quando as previsões estão perto de 0 ou 1 (devido à saturação do sigmoid). Os gradientes da entropia cruzada compensam isso -- o -log cancela as regiões planas do sigmoid, dando gradientes fortes exatamente onde eles são mais necessários.
Suavização de Rótulos (Label Smoothing)
Rótulos one-hot padrão dizem "isto é 100% classe 3 e 0% todo o resto". Essa é uma afirmação forte. A suavização de rótulos a abranda:
smooth_label = (1 - alpha) * one_hot + alpha / num_classes
Com alpha = 0.1 e 10 classes: em vez de [0, 0, 1, 0, ...], o alvo se torna [0.01, 0.01, 0.91, 0.01, ...]. O modelo mira em 0.91 em vez de 1.0.
Por que isso funciona: um modelo tentando produzir exatamente 1.0 através de um softmax precisa empurrar os logits para o infinito. Isso causa superconfiança, prejudica a generalização e torna o modelo frágil a mudanças de distribuição. A suavização de rótulos limita o alvo a 0.9 (com alpha=0.1), mantendo os logits em uma faixa razoável. O GPT e a maioria dos modelos modernos usam suavização de rótulos ou seu equivalente.
Perda Contrastiva
Sem rótulos. Sem classes. Apenas pares de entradas e a pergunta: estes são similares ou diferentes?
Perda contrastiva no estilo SimCLR (NT-Xent / InfoNCE):
Pegue uma imagem. Crie duas visões aumentadas dela (corte, rotação, alteração de cor). Estas são o "par positivo" -- elas devem ter embeddings similares. Cada outra imagem no lote forma um "par negativo" -- elas devem ter embeddings diferentes.
L = -log(exp(sim(z_i, z_j) / tau) / sum(exp(sim(z_i, z_k) / tau)))
Onde sim() é a similaridade de cosseno, z_i e z_j são o par positivo, a soma é sobre todos os negativos, e tau (temperatura) controla quão acentuada é a distribuição. Temperatura mais baixa = negativos mais difíceis = separação mais agressiva.
Números reais: tamanho de lote 256 significa 255 negativos por par positivo. Temperatura tau = 0.07 (padrão do SimCLR). A perda se parece com um softmax sobre similaridades -- ela quer que a similaridade do par positivo seja a mais alta entre todas as 256 opções.
Perda de Tripla (Triplet Loss):
Recebe três entradas: âncora, positivo (mesma classe), negativo (classe diferente).
L = max(0, d(anchor, positive) - d(anchor, negative) + margin)
A margem (tipicamente 0.2-1.0) impõe uma diferença mínima entre as distâncias positiva e negativa. Se o negativo já está suficientemente longe, a perda é zero -- sem gradiente, sem atualização. Isso torna o treinamento eficiente, mas exige uma mineração cuidadosa de triplas (escolher negativos difíceis que estejam próximos da âncora).
Perda Focal (Focal Loss)
Para conjuntos de dados desbalanceados. A entropia cruzada padrão trata todos os exemplos corretamente classificados de forma igual. A perda focal reduz o peso de exemplos fáceis:
FL = -alpha * (1 - p_t)^gamma * log(p_t)
Onde p_t é a probabilidade prevista da classe verdadeira e gamma controla o foco. Com gamma = 0, isso é a entropia cruzada padrão. Com gamma = 2 (o padrão):
- Exemplo fácil (p_t = 0.9): peso = (0.1)^2 = 0.01. Efetivamente ignorado.
- Exemplo difícil (p_t = 0.1): peso = (0.9)^2 = 0.81. Sinal de gradiente completo.
A perda focal foi introduzida por Lin et al. para detecção de objetos, onde 99% das regiões candidatas são fundo (negativos fáceis). Sem a perda focal, o modelo se afoga em exemplos fáceis de fundo e nunca aprende a detectar objetos. Com ela, o modelo concentra sua capacidade nos casos difíceis e ambíguos que importam.
Árvore de Decisão de Funções de Perda
flowchart TD
Start["Qual é a sua tarefa?"] --> Reg{"Regressão?"}
Start --> Cls{"Classificação?"}
Start --> Emb{"Aprendendo embeddings?"}
Reg -->|"Sim"| Outliers{"Sensível a outliers?"}
Outliers -->|"Sim, penalizar outliers"| MSE["Use MSE"]
Outliers -->|"Não, robusto a outliers"| MAE["Use MAE / Huber"]
Cls -->|"Binária"| BCE["Use CE Binária"]
Cls -->|"Multiclasse"| CCE["Use CE Categórica"]
Cls -->|"Desbalanceada"| FL["Use Perda Focal"]
CCE -->|"Superconfiante?"| LS["Adicione Suavização de Rótulos"]
Emb -->|"Dados em pares"| CL["Use Perda Contrastiva"]
Emb -->|"Triplas disponíveis"| TL["Use Perda de Tripla"]
Emb -->|"Autossupervisionado com lote grande"| NCE["Use InfoNCE"]
Paisagem da Perda
graph LR
subgraph "Formato da Superfície de Perda"
MSE_S["MSE<br/>Parábola suave<br/>Mínimo único<br/>Fácil de otimizar"]
CE_S["Entropia Cruzada<br/>Íngreme perto de respostas erradas<br/>Plana perto de respostas corretas<br/>Gradientes fortes onde necessário"]
CL_S["Contrastiva<br/>Muitos mínimos locais<br/>Depende da composição do lote<br/>A temperatura controla a acentuação"]
end
MSE_S -->|"Melhor para"| Reg2["Regressão"]
CE_S -->|"Melhor para"| Cls2["Classificação"]
CL_S -->|"Melhor para"| Emb2["Aprendizagem de representação"]
Construa
Passo 1: MSE e Seu Gradiente
def mse(predictions, targets):
n = len(predictions)
total = 0.0
for p, t in zip(predictions, targets):
total += (p - t) ** 2
return total / n
def mse_gradient(predictions, targets):
n = len(predictions)
grads = []
for p, t in zip(predictions, targets):
grads.append(2.0 * (p - t) / n)
return grads
Passo 2: Entropia Cruzada Binária
O problema do log(0) é real. Se o modelo prevê exatamente 0 para um exemplo positivo, log(0) = menos infinito. O recorte (clipping) previne isso.
import math
def binary_cross_entropy(predictions, targets, eps=1e-15):
n = len(predictions)
total = 0.0
for p, t in zip(predictions, targets):
p_clipped = max(eps, min(1 - eps, p))
total += -(t * math.log(p_clipped) + (1 - t) * math.log(1 - p_clipped))
return total / n
def bce_gradient(predictions, targets, eps=1e-15):
grads = []
for p, t in zip(predictions, targets):
p_clipped = max(eps, min(1 - eps, p))
grads.append(-(t / p_clipped) + (1 - t) / (1 - p_clipped))
return grads
Passo 3: Entropia Cruzada Categórica com Softmax
O softmax converte logits brutos em probabilidades. Então computamos a entropia cruzada contra alvos one-hot.
def softmax(logits):
max_val = max(logits)
exps = [math.exp(x - max_val) for x in logits]
total = sum(exps)
return [e / total for e in exps]
def categorical_cross_entropy(logits, target_index, eps=1e-15):
probs = softmax(logits)
p = max(eps, probs[target_index])
return -math.log(p)
def cce_gradient(logits, target_index):
probs = softmax(logits)
grads = list(probs)
grads[target_index] -= 1.0
return grads
O gradiente do softmax + entropia cruzada simplifica de forma elegante: é apenas (probabilidade prevista - 1) para a classe verdadeira, e (probabilidade prevista) para todas as outras classes. Essa simplificação elegante não é coincidência -- é por isso que softmax e entropia cruzada andam juntos.
Passo 4: Suavização de Rótulos
def label_smoothed_cce(logits, target_index, num_classes, alpha=0.1, eps=1e-15):
probs = softmax(logits)
loss = 0.0
for i in range(num_classes):
if i == target_index:
smooth_target = 1.0 - alpha + alpha / num_classes
else:
smooth_target = alpha / num_classes
p = max(eps, probs[i])
loss += -smooth_target * math.log(p)
return loss
Passo 5: Perda Contrastiva (InfoNCE Simplificada)
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a < 1e-10 or norm_b < 1e-10:
return 0.0
return dot / (norm_a * norm_b)
def contrastive_loss(anchor, positive, negatives, temperature=0.07):
sim_pos = cosine_similarity(anchor, positive) / temperature
sim_negs = [cosine_similarity(anchor, neg) / temperature for neg in negatives]
max_sim = max(sim_pos, max(sim_negs)) if sim_negs else sim_pos
exp_pos = math.exp(sim_pos - max_sim)
exp_negs = [math.exp(s - max_sim) for s in sim_negs]
total_exp = exp_pos + sum(exp_negs)
return -math.log(max(1e-15, exp_pos / total_exp))
Passo 6: MSE vs Entropia Cruzada na Classificação
Treine a mesma rede da lição 04 (conjunto de dados em círculo) com ambas as funções de perda. Observe a entropia cruzada convergir mais rápido.
import random
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def make_circle_data(n=200, seed=42):
random.seed(seed)
data = []
for _ in range(n):
x = random.uniform(-2, 2)
y = random.uniform(-2, 2)
label = 1.0 if x * x + y * y < 1.5 else 0.0
data.append(([x, y], label))
return data
class LossComparisonNetwork:
def __init__(self, loss_type="bce", hidden_size=8, lr=0.1):
random.seed(0)
self.loss_type = loss_type
self.lr = lr
self.hidden_size = hidden_size
self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
self.b1 = [0.0] * hidden_size
self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
self.b2 = 0.0
def forward(self, x):
self.x = x
self.z1 = []
self.h = []
for i in range(self.hidden_size):
z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
self.z1.append(z)
self.h.append(max(0.0, z))
self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
self.out = sigmoid(self.z2)
return self.out
def backward(self, target):
if self.loss_type == "mse":
d_loss = 2.0 * (self.out - target)
else:
eps = 1e-15
p = max(eps, min(1 - eps, self.out))
d_loss = -(target / p) + (1 - target) / (1 - p)
d_sigmoid = self.out * (1 - self.out)
d_out = d_loss * d_sigmoid
for i in range(self.hidden_size):
d_relu = 1.0 if self.z1[i] > 0 else 0.0
d_h = d_out * self.w2[i] * d_relu
self.w2[i] -= self.lr * d_out * self.h[i]
for j in range(2):
self.w1[i][j] -= self.lr * d_h * self.x[j]
self.b1[i] -= self.lr * d_h
self.b2 -= self.lr * d_out
def compute_loss(self, pred, target):
if self.loss_type == "mse":
return (pred - target) ** 2
else:
eps = 1e-15
p = max(eps, min(1 - eps, pred))
return -(target * math.log(p) + (1 - target) * math.log(1 - p))
def train(self, data, epochs=200):
losses = []
for epoch in range(epochs):
total_loss = 0.0
correct = 0
for x, y in data:
pred = self.forward(x)
self.backward(y)
total_loss += self.compute_loss(pred, y)
if (pred >= 0.5) == (y >= 0.5):
correct += 1
avg_loss = total_loss / len(data)
accuracy = correct / len(data) * 100
losses.append((avg_loss, accuracy))
if epoch % 50 == 0 or epoch == epochs - 1:
print(f" Epoch {epoch:3d}: loss={avg_loss:.4f}, accuracy={accuracy:.1f}%")
return losses
Use
O PyTorch fornece todas as funções de perda padrão com estabilidade numérica embutida:
import torch
import torch.nn as nn
import torch.nn.functional as F
predictions = torch.tensor([0.9, 0.1, 0.7], requires_grad=True)
targets = torch.tensor([1.0, 0.0, 1.0])
mse_loss = F.mse_loss(predictions, targets)
bce_loss = F.binary_cross_entropy(predictions, targets)
logits = torch.randn(4, 10)
labels = torch.tensor([3, 7, 1, 9])
ce_loss = F.cross_entropy(logits, labels)
ce_smooth = F.cross_entropy(logits, labels, label_smoothing=0.1)
Use F.cross_entropy (não F.nll_loss mais softmax manual). Ele combina log-softmax e log-verossimilhança negativa em uma única operação numericamente estável. Aplicar o softmax separadamente e então tirar o log é menos estável -- você perde precisão na subtração de grandes exponenciais.
Para aprendizagem contrastiva, a maioria das equipes usa implementações customizadas ou bibliotecas como lightly ou pytorch-metric-learning. O loop central é sempre o mesmo: computar similaridades par a par, criar o softmax sobre positivos e negativos, retropropagar.
Entregue
Esta lição produz:
outputs/prompt-loss-function-selector.md-- um prompt reutilizável para escolher a função de perda certaoutputs/prompt-loss-debugger.md-- um prompt de diagnóstico para quando sua curva de perda parece errada
Exercícios
Implemente a perda de Huber (perda L1 suave), que é MSE para erros pequenos e MAE para erros grandes. Treine uma rede de regressão prevendo y = sin(x) com MSE vs Huber quando 5% dos alvos de treino têm ruído aleatório adicionado (outliers). Compare o erro final de teste.
Adicione a perda focal ao loop de treinamento de classificação binária. Crie um conjunto de dados desbalanceado (90% classe 0, 10% classe 1). Compare a BCE padrão vs a perda focal (gamma=2) no recall da classe minoritária após 200 épocas.
Implemente a perda de tripla com mineração de negativos semi-difíceis. Gere dados de embedding 2D para 5 classes. Para cada âncora, encontre o negativo mais difícil que ainda esteja mais distante que o positivo (semi-difícil). Compare a convergência com a seleção aleatória de triplas.
Execute a comparação MSE vs entropia cruzada, mas acompanhe as magnitudes dos gradientes em cada camada durante o treinamento. Plote a norma média do gradiente por época. Verifique que a entropia cruzada produz gradientes maiores nas épocas iniciais, quando o modelo está mais incerto.
Implemente a perda de divergência KL e verifique que minimizar KL(verdadeira || prevista) dá os mesmos gradientes que a entropia cruzada quando a distribuição verdadeira é one-hot. Então tente alvos suaves (como na destilação de conhecimento), onde a distribuição "verdadeira" vem da saída softmax de um modelo professor.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Função de perda | "O quão errado o modelo está" | Uma função diferenciável que mapeia previsões e alvos para um escalar que o otimizador minimiza |
| MSE | "Erro quadrático médio" | Média das diferenças ao quadrado entre previsões e alvos; penaliza erros grandes de forma quadrática |
| Entropia cruzada | "A perda de classificação" | Mede a divergência entre a distribuição de probabilidade prevista e a verdadeira usando -log(p) |
| Entropia cruzada binária | "BCE" | Entropia cruzada para duas classes: -(y*log(p) + (1-y)*log(1-p)) |
| Suavização de rótulos | "Abrandar os alvos" | Substituir alvos rígidos 0/1 por valores suaves (ex: 0.1/0.9) para prevenir superconfiança e melhorar a generalização |
| Perda contrastiva | "Aproximar, afastar" | Uma perda que aprende representações tornando pares similares próximos e pares dissimilares distantes no espaço de embedding |
| InfoNCE | "A perda do CLIP/SimCLR" | Entropia cruzada normalizada e escalada por temperatura sobre escores de similaridade; trata a aprendizagem contrastiva como classificação |
| Perda focal | "O conserto para dados desbalanceados" | Entropia cruzada ponderada por (1-p_t)^gamma para reduzir o peso de exemplos fáceis e focar nos difíceis |
| Perda de tripla | "Âncora-positivo-negativo" | Aproxima a âncora mais do positivo do que do negativo por pelo menos uma margem no espaço de embedding |
| Temperatura | "Botão de acentuação" | Um divisor escalar sobre logits/similaridades que controla quão acentuada é a distribuição resultante; menor = mais acentuada |
Leitura Complementar
- Lin et al., "Focal Loss for Dense Object Detection" (2017) -- introduziu a perda focal para lidar com desbalanceamento extremo de classes na detecção de objetos (RetinaNet)
- Chen et al., "A Simple Framework for Contrastive Learning of Visual Representations" (SimCLR, 2020) -- definiu o pipeline moderno de aprendizagem contrastiva com a perda NT-Xent
- Szegedy et al., "Rethinking the Inception Architecture" (2016) -- introduziu a suavização de rótulos como técnica de regularização, hoje padrão na maioria dos grandes modelos
- Hinton et al., "Distilling the Knowledge in a Neural Network" (2015) -- destilação de conhecimento usando alvos suaves e divergência KL, fundamental para a compressão de modelos