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 certa
  • outputs/prompt-loss-debugger.md -- um prompt de diagnóstico para quando sua curva de perda parece errada

Exercícios

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).