Phase 03 - Lesson 05

Funciones de Pérdida

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Tu red hace una predicción. La verdad fundamental dice lo contrario. ¿Qué tan equivocada está? Ese número es la pérdida. Elige la función de pérdida equivocada y tu modelo optimizará para algo completamente errado.

Tipo: Build Lenguajes: Python Prerrequisitos: Lección 03.04 (Funciones de Activación) Tiempo: ~75 minutos

Objetivos de Aprendizaje

  • Implementar MSE, entropía cruzada binaria, entropía cruzada categórica y pérdida contrastiva (InfoNCE) desde cero, con sus gradientes
  • Explicar por qué el MSE falla en la clasificación demostrando el modo de fallo "predecir 0.5 para todo"
  • Aplicar el suavizado de etiquetas (label smoothing) a la entropía cruzada y describir cómo previene predicciones sobreconfiadas
  • Elegir la función de pérdida correcta para tareas de regresión, clasificación binaria, clasificación multiclase y aprendizaje de embeddings

El Problema

Un modelo que minimiza el MSE en un problema de clasificación predecirá, con confianza, 0.5 para todo. Está minimizando la pérdida. Y también es inútil.

La función de pérdida es lo único que tu modelo realmente optimiza. No la exactitud. No el F1 score. Ni cualquier métrica que le reportes a tu gerente. El optimizador toma el gradiente de la función de pérdida y ajusta los pesos para hacer ese número más pequeño. Si la función de pérdida no captura aquello que te importa, el modelo encontrará la manera matemáticamente más barata de satisfacerla, y esa manera casi nunca es lo que querías.

Aquí va un ejemplo concreto. Tienes una tarea de clasificación binaria. Dos clases, división 50/50. Usas MSE como tu pérdida. El modelo predice 0.5 para cada entrada. El MSE promedio es 0.25, que es el mínimo posible sin de hecho aprender nada. El modelo tiene cero capacidad discriminativa, pero técnicamente ha minimizado tu función de pérdida. Cambia a entropía cruzada y el mismo modelo se ve forzado a empujar las predicciones hacia 0 o 1, porque -log(0.5) = 0.693 es una pérdida terrible, mientras que -log(0.99) = 0.01 recompensa las predicciones correctas y confiadas. La elección de la función de pérdida es la diferencia entre un modelo que aprende y un modelo que burla la métrica.

Y se pone peor. En el aprendizaje autosupervisado, ni siquiera tienes etiquetas. La pérdida contrastiva define por completo la señal de aprendizaje: qué cuenta como similar, qué cuenta como diferente y con cuánta fuerza el modelo debe separarlos. Equivoca la pérdida contrastiva y tus embeddings colapsan a un único punto -- toda entrada mapea al mismo vector. Técnicamente, pérdida cero. Completamente inútil.

El Concepto

Error Cuadrático Medio (MSE)

El predeterminado para regresión. Calcula la diferencia al cuadrado entre la predicción y el objetivo, promedia sobre todas las muestras.

MSE = (1/n) * sum((y_pred - y_true)^2)

Por qué importa elevar al cuadrado: penaliza los errores grandes de forma cuadrática. Un error de 2 cuesta 4x más que un error de 1. Un error de 10 cuesta 100x. Esto hace que el MSE sea sensible a los outliers -- una única predicción descabelladamente errónea domina la pérdida.

Números reales: si tu modelo predice precios de viviendas y se equivoca por US$ 10,000 en la mayoría de las casas, pero se equivoca por US$ 200,000 en una mansión, el MSE intentará agresivamente arreglar esa mansión, perjudicando potencialmente el desempeño en las otras 99 casas.

El gradiente del MSE con respecto a una predicción es:

dMSE/dy_pred = (2/n) * (y_pred - y_true)

Lineal en el error. Los errores más grandes reciben gradientes más grandes. Esto es una ventaja para la regresión (los errores grandes necesitan correcciones grandes) y un defecto para la clasificación (quieres penalizar respuestas erradas y confiadas de forma exponencial, no lineal).

Pérdida de Entropía Cruzada

La función de pérdida para clasificación. Arraigada en la teoría de la información -- mide la divergencia entre la distribución de probabilidad predicha y la distribución verdadera.

Entropía Cruzada Binaria (BCE):

BCE = -(y * log(p) + (1 - y) * log(1 - p))

Donde y es la etiqueta verdadera (0 o 1) y p es la probabilidad predicha.

Por qué funciona -log(p): cuando la etiqueta verdadera es 1 y predices p = 0.99, la pérdida es -log(0.99) = 0.01. Cuando predices p = 0.01, la pérdida es -log(0.01) = 4.6. Esa diferencia de 460x es por qué la entropía cruzada funciona. Castiga brutalmente las predicciones erradas y confiadas, mientras apenas penaliza las correctas y confiadas.

El gradiente cuenta la misma historia:

dBCE/dp = -(y/p) + (1-y)/(1-p)

Cuando y = 1 y p está cerca de cero, el gradiente es -1/p, que tiende a menos infinito. El modelo recibe una señal enorme para corregir su error. Cuando p está cerca de 1, el gradiente es minúsculo. Ya está correcto, nada que arreglar.

Entropía Cruzada Categórica:

Para clasificación multiclase con objetivos codificados en one-hot.

CCE = -sum(y_i * log(p_i))

Solo la clase verdadera contribuye a la pérdida (porque todos los demás y_i son cero). Si hay 10 clases y la clase correcta recibe probabilidad 0.1 (adivinanza aleatoria), la pérdida es -log(0.1) = 2.3. Si la clase correcta recibe probabilidad 0.9, la pérdida es -log(0.9) = 0.105. El modelo aprende a concentrar la masa de probabilidad en la respuesta correcta.

Por qué el MSE Falla en la Clasificación

graph TD
    subgraph "MSE en la Clasificación"
        P1["Predecir 0.5 para la clase 1<br/>MSE = 0.25"]
        P2["Predecir 0.9 para la clase 1<br/>MSE = 0.01"]
        P3["Predecir 0.1 para la clase 1<br/>MSE = 0.81"]
    end
    subgraph "Entropía Cruzada en la Clasificación"
        C1["Predecir 0.5 para la clase 1<br/>CE = 0.693"]
        C2["Predecir 0.9 para la clase 1<br/>CE = 0.105"]
        C3["Predecir 0.1 para la clase 1<br/>CE = 2.303"]
    end
    P3 -->|"El gradiente del MSE<br/>se aplana cerca de la<br/>saturación"| Slow["Corrección lenta"]
    C3 -->|"El gradiente de la CE<br/>explota cerca de la<br/>respuesta errada"| Fast["Corrección rápida"]

Los gradientes del MSE se aplanan cuando las predicciones están cerca de 0 o 1 (debido a la saturación del sigmoide). Los gradientes de la entropía cruzada compensan esto -- el -log cancela las regiones planas del sigmoide, dando gradientes fuertes exactamente donde más se necesitan.

Suavizado de Etiquetas (Label Smoothing)

Las etiquetas one-hot estándar dicen "esto es 100% clase 3 y 0% todo lo demás". Esa es una afirmación fuerte. El suavizado de etiquetas la atenúa:

smooth_label = (1 - alpha) * one_hot + alpha / num_classes

Con alpha = 0.1 y 10 clases: en lugar de [0, 0, 1, 0, ...], el objetivo se convierte en [0.01, 0.01, 0.91, 0.01, ...]. El modelo apunta a 0.91 en lugar de 1.0.

Por qué funciona: un modelo que intenta producir exactamente 1.0 a través de un softmax necesita empujar los logits hacia el infinito. Esto causa sobreconfianza, perjudica la generalización y hace al modelo frágil ante cambios de distribución. El suavizado de etiquetas limita el objetivo a 0.9 (con alpha=0.1), manteniendo los logits en un rango razonable. GPT y la mayoría de los modelos modernos usan suavizado de etiquetas o su equivalente.

Pérdida Contrastiva

Sin etiquetas. Sin clases. Solo pares de entradas y la pregunta: ¿son estos similares o diferentes?

Pérdida contrastiva al estilo SimCLR (NT-Xent / InfoNCE):

Toma una imagen. Crea dos vistas aumentadas de ella (recorte, rotación, alteración de color). Estas son el "par positivo" -- deberían tener embeddings similares. Cada otra imagen del lote forma un "par negativo" -- deberían tener embeddings diferentes.

L = -log(exp(sim(z_i, z_j) / tau) / sum(exp(sim(z_i, z_k) / tau)))

Donde sim() es la similitud del coseno, z_i y z_j son el par positivo, la suma es sobre todos los negativos, y tau (temperatura) controla qué tan pronunciada es la distribución. Temperatura más baja = negativos más difíciles = separación más agresiva.

Números reales: un tamaño de lote de 256 significa 255 negativos por par positivo. Temperatura tau = 0.07 (predeterminado de SimCLR). La pérdida se parece a un softmax sobre similitudes -- quiere que la similitud del par positivo sea la más alta entre las 256 opciones.

Pérdida de Tripleta (Triplet Loss):

Toma tres entradas: ancla, positivo (misma clase), negativo (clase diferente).

L = max(0, d(anchor, positive) - d(anchor, negative) + margin)

El margen (típicamente 0.2-1.0) impone una brecha mínima entre las distancias positiva y negativa. Si el negativo ya está suficientemente lejos, la pérdida es cero -- sin gradiente, sin actualización. Esto hace el entrenamiento eficiente, pero requiere una minería cuidadosa de tripletas (elegir negativos difíciles que estén cerca del ancla).

Pérdida Focal (Focal Loss)

Para conjuntos de datos desbalanceados. La entropía cruzada estándar trata a todos los ejemplos correctamente clasificados por igual. La pérdida focal reduce el peso de los ejemplos fáciles:

FL = -alpha * (1 - p_t)^gamma * log(p_t)

Donde p_t es la probabilidad predicha de la clase verdadera y gamma controla el enfoque. Con gamma = 0, esto es la entropía cruzada estándar. Con gamma = 2 (el predeterminado):

  • Ejemplo fácil (p_t = 0.9): peso = (0.1)^2 = 0.01. Efectivamente ignorado.
  • Ejemplo difícil (p_t = 0.1): peso = (0.9)^2 = 0.81. Señal de gradiente completa.

La pérdida focal fue introducida por Lin et al. para la detección de objetos, donde el 99% de las regiones candidatas son fondo (negativos fáciles). Sin la pérdida focal, el modelo se ahoga en ejemplos fáciles de fondo y nunca aprende a detectar objetos. Con ella, el modelo concentra su capacidad en los casos difíciles y ambiguos que importan.

Árbol de Decisión de Funciones de Pérdida

flowchart TD
    Start["¿Cuál es tu tarea?"] --> Reg{"¿Regresión?"}
    Start --> Cls{"¿Clasificación?"}
    Start --> Emb{"¿Aprendiendo embeddings?"}

    Reg -->|"Sí"| Outliers{"¿Sensible a outliers?"}
    Outliers -->|"Sí, penalizar outliers"| MSE["Usa MSE"]
    Outliers -->|"No, robusto a outliers"| MAE["Usa MAE / Huber"]

    Cls -->|"Binaria"| BCE["Usa CE Binaria"]
    Cls -->|"Multiclase"| CCE["Usa CE Categórica"]
    Cls -->|"Desbalanceada"| FL["Usa Pérdida Focal"]
    CCE -->|"¿Sobreconfiada?"| LS["Agrega Suavizado de Etiquetas"]

    Emb -->|"Datos en pares"| CL["Usa Pérdida Contrastiva"]
    Emb -->|"Tripletas disponibles"| TL["Usa Pérdida de Tripleta"]
    Emb -->|"Autosupervisado con lote grande"| NCE["Usa InfoNCE"]

Paisaje de la Pérdida

graph LR
    subgraph "Forma de la Superficie de Pérdida"
        MSE_S["MSE<br/>Parábola suave<br/>Mínimo único<br/>Fácil de optimizar"]
        CE_S["Entropía Cruzada<br/>Empinada cerca de respuestas erradas<br/>Plana cerca de respuestas correctas<br/>Gradientes fuertes donde se necesitan"]
        CL_S["Contrastiva<br/>Muchos mínimos locales<br/>Depende de la composición del lote<br/>La temperatura controla lo pronunciado"]
    end
    MSE_S -->|"Mejor para"| Reg2["Regresión"]
    CE_S -->|"Mejor para"| Cls2["Clasificación"]
    CL_S -->|"Mejor para"| Emb2["Aprendizaje de representación"]

Constrúyelo

Paso 1: MSE y Su 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

Paso 2: Entropía Cruzada Binaria

El problema del log(0) es real. Si el modelo predice exactamente 0 para un ejemplo positivo, log(0) = menos infinito. El recorte (clipping) previene esto.

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

Paso 3: Entropía Cruzada Categórica con Softmax

El softmax convierte logits crudos en probabilidades. Luego calculamos la entropía cruzada contra los objetivos 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

El gradiente del softmax + entropía cruzada se simplifica de forma elegante: es simplemente (probabilidad predicha - 1) para la clase verdadera, y (probabilidad predicha) para todas las demás clases. Esta simplificación elegante no es coincidencia -- es por eso que el softmax y la entropía cruzada van emparejados.

Paso 4: Suavizado de Etiquetas

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

Paso 5: Pérdida 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))

Paso 6: MSE vs Entropía Cruzada en la Clasificación

Entrena la misma red de la lección 04 (conjunto de datos en círculo) con ambas funciones de pérdida. Observa cómo la entropía cruzada converge más 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

Úsalo

PyTorch proporciona todas las funciones de pérdida estándar con estabilidad numérica incorporada:

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)

Usa F.cross_entropy (no F.nll_loss más softmax manual). Combina log-softmax y log-verosimilitud negativa en una única operación numéricamente estable. Aplicar el softmax por separado y luego tomar el log es menos estable -- pierdes precisión en la resta de grandes exponenciales.

Para el aprendizaje contrastivo, la mayoría de los equipos usan implementaciones personalizadas o bibliotecas como lightly o pytorch-metric-learning. El bucle central es siempre el mismo: calcular similitudes por pares, crear el softmax sobre positivos y negativos, retropropagar.

Entrégalo

Esta lección produce:

  • outputs/prompt-loss-function-selector.md -- un prompt reutilizable para elegir la función de pérdida correcta
  • outputs/prompt-loss-debugger.md -- un prompt de diagnóstico para cuando tu curva de pérdida se ve mal

Ejercicios

  1. Implementa la pérdida de Huber (pérdida L1 suave), que es MSE para errores pequeños y MAE para errores grandes. Entrena una red de regresión que prediga y = sin(x) con MSE vs Huber cuando el 5% de los objetivos de entrenamiento tienen ruido aleatorio agregado (outliers). Compara el error final de prueba.

  2. Agrega la pérdida focal al bucle de entrenamiento de clasificación binaria. Crea un conjunto de datos desbalanceado (90% clase 0, 10% clase 1). Compara la BCE estándar vs la pérdida focal (gamma=2) en el recall de la clase minoritaria después de 200 épocas.

  3. Implementa la pérdida de tripleta con minería de negativos semidifíciles. Genera datos de embedding 2D para 5 clases. Para cada ancla, encuentra el negativo más difícil que aún esté más lejos que el positivo (semidifícil). Compara la convergencia con la selección aleatoria de tripletas.

  4. Ejecuta la comparación MSE vs entropía cruzada, pero registra las magnitudes de los gradientes en cada capa durante el entrenamiento. Grafica la norma promedio del gradiente por época. Verifica que la entropía cruzada produce gradientes más grandes en las épocas iniciales, cuando el modelo está más incierto.

  5. Implementa la pérdida de divergencia KL y verifica que minimizar KL(verdadera || predicha) da los mismos gradientes que la entropía cruzada cuando la distribución verdadera es one-hot. Luego prueba objetivos suaves (como en la destilación de conocimiento), donde la distribución "verdadera" proviene de la salida softmax de un modelo profesor.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
Función de pérdida "Qué tan equivocado está el modelo" Una función diferenciable que mapea predicciones y objetivos a un escalar que el optimizador minimiza
MSE "Error cuadrático medio" Media de las diferencias al cuadrado entre predicciones y objetivos; penaliza los errores grandes de forma cuadrática
Entropía cruzada "La pérdida de clasificación" Mide la divergencia entre la distribución de probabilidad predicha y la verdadera usando -log(p)
Entropía cruzada binaria "BCE" Entropía cruzada para dos clases: -(y*log(p) + (1-y)*log(1-p))
Suavizado de etiquetas "Atenuar los objetivos" Reemplazar objetivos rígidos 0/1 por valores suaves (ej: 0.1/0.9) para prevenir la sobreconfianza y mejorar la generalización
Pérdida contrastiva "Acercar, alejar" Una pérdida que aprende representaciones acercando los pares similares y alejando los pares disímiles en el espacio de embedding
InfoNCE "La pérdida de CLIP/SimCLR" Entropía cruzada normalizada y escalada por temperatura sobre puntuaciones de similitud; trata el aprendizaje contrastivo como clasificación
Pérdida focal "El arreglo para datos desbalanceados" Entropía cruzada ponderada por (1-p_t)^gamma para reducir el peso de los ejemplos fáciles y enfocarse en los difíciles
Pérdida de tripleta "Ancla-positivo-negativo" Acerca el ancla más al positivo que al negativo por al menos un margen en el espacio de embedding
Temperatura "Perilla de agudeza" Un divisor escalar sobre logits/similitudes que controla qué tan pronunciada es la distribución resultante; menor = más pronunciada

Lectura Adicional

  • Lin et al., "Focal Loss for Dense Object Detection" (2017) -- introdujo la pérdida focal para manejar el desbalanceo extremo de clases en la detección de objetos (RetinaNet)
  • Chen et al., "A Simple Framework for Contrastive Learning of Visual Representations" (SimCLR, 2020) -- definió el pipeline moderno de aprendizaje contrastivo con la pérdida NT-Xent
  • Szegedy et al., "Rethinking the Inception Architecture" (2016) -- introdujo el suavizado de etiquetas como técnica de regularización, hoy estándar en la mayoría de los modelos grandes
  • Hinton et al., "Distilling the Knowledge in a Neural Network" (2015) -- destilación de conocimiento usando objetivos suaves y divergencia KL, fundamental para la compresión de modelos
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).