Lección 4 de 10 · La red · 15 min
Pérdidas
Error cuadrático y entropía cruzada: el número que la red quiere bajar
Notebook del episodio
El código de esta lección, para correrlo y modificarlo en Google Colab, gratis y con tu cuenta. Para guardar tus cambios: Archivo → Guardar una copia en Drive.
Deep learning a secas. Episodio cuatro: pérdidas. Hasta ahora los pesos los elegimos nosotros. Para que una red los encuentre sola, primero necesita saber cuánto se equivoca, con un único número. Ese número es la pérdida, y hoy vemos las dos que se usan en casi todo: el error cuadrático y la entropía cruzada.
Qué es una pérdida
- Un solo número: cuánto se equivocó la red
- Cero = perfecto; más grande = peor
- Tiene que tener pendiente: así se sabe hacia dónde mover
- Se elige según la tarea: un número o una clase
Cuatro cosas antes de las fórmulas. Una pérdida resume todos los errores de un batch en un solo número. Cero significa que acertó todo; cuanto más grande, peor. Y tiene que ser una curva suave, con pendiente, porque entrenar va a ser mover cada peso en la dirección en que la pérdida baja. El escalón del episodio uno no servía por eso. La pérdida se elige según lo que la red tiene que predecir: un número cualquiera, como una temperatura, o una clase, como qué dígito es.
Parte 1 · Predecir un número: error cuadrático
Cuando la red predice un número, la pérdida más natural mide la distancia al valor real.
El error cuadrático medio
Cuatro predicciones de la red, y sombrero. Y los cuatro valores reales, y. El error de cada ejemplo es la resta. Lo elevamos al cuadrado, promediamos, y comparamos con la función de PyTorch. Y probamos también sin el cuarto ejemplo. Errores: menos cero coma cinco, cero coma cinco, cero y cuatro. Al cuadrado: un cuarto, un cuarto, cero y dieciséis. El promedio es cuatro coma uno dos cinco, igual que PyTorch. Esto es el MSE, mean squared error, error cuadrático medio. Y sin el cuarto ejemplo, la pérdida baja a cero coma uno siete: un solo error grande se lleva casi toda la pérdida.
y_hat = torch.tensor([2.5, 0.0, 2.0, 7.0]) # predicciones
y = torch.tensor([3.0, -0.5, 2.0, 3.0]) # valores reales
e = y_hat - y # error de cada ejemplo
print("errores: ", e.tolist())
print("al cuadrado:", (e ** 2).tolist())
print("MSE a mano:", round((e ** 2).mean().item(), 4))
print("F.mse_loss:", round(F.mse_loss(y_hat, y).item(), 4))
print("sin el 4°: ", round((e[:3] ** 2).mean().item(), 4))
errores: [-0.5, 0.5, 0.0, 4.0]
al cuadrado: [0.25, 0.25, 0.0, 16.0]
MSE a mano: 4.125
F.mse_loss: 4.125
sin el 4°: 0.1667
MSE, dibujado
error con signo de cada ejemplo
al cuadrado: todo positivo; después, el promedio
la pendiente crece con el error
Paso por paso. e sub i es el error del ejemplo i: predicción menos real. Tiene signo. En el dibujo, si los sumáramos así, el menos cero coma cinco y el cero coma cinco se cancelarían, y parecería que esos dos acertaron. Por eso se elevan al cuadrado: todo queda positivo. Después, se suman y se dividen por N, la cantidad de ejemplos. En el dibujo, la barra del cuarto ejemplo, dieciséis, aplasta a las demás. Y la pendiente del cuadrado: d sobre d e de e al cuadrado es dos e. En el dibujo, mirá cómo, al moverse el punto, la tangente se empina a medida que se aleja del cero: errores grandes empujan fuerte, errores chicos empujan suave.
Un error grande domina
La misma idea en barras. El cuarto ejemplo, con error cuatro, aporta dieciséis; los otros tres juntos, medio. Esto es una virtud y un defecto. Virtud: los errores grandes se corrigen primero. Defecto: si ese dato está mal cargado, un valor atípico, la red va a torcerse para complacerlo. Por eso, a veces, en vez del cuadrado se usa el valor absoluto, que crece más despacio.
- ej. 1: 0,25
- ej. 2: 0,25
- ej. 3: 0
- ej. 4: 16
Parte 2 · Predecir sí o no: entropía cruzada binaria
Cuando la respuesta es una clase, la red no debería dar un número cualquiera, sino una probabilidad.
De logit a probabilidad a pérdida
z: logit, el número crudo que sale de la red
castiga la probabilidad que le dio a lo correcto
las dos en una fórmula: y elige cuál cuenta
Tres pasos. La red saca un número crudo, z, que se llama logit. La sigmoide lo convierte en p, la probabilidad de que la respuesta sea uno. En el dibujo, logit dos da cero coma ochenta y ocho; logit menos tres, cero coma cero cinco. La pérdida mira la probabilidad que la red le dio a la respuesta correcta y le toma menos el logaritmo. El log de un número entre cero y uno es negativo; el menos lo da vuelta. En el dibujo, el logaritmo de p: en uno vale cero, y se hunde cuando p se acerca a cero. Mirá la pendiente: cerca de cero es enorme. Las dos fórmulas en una: si y vale uno, el segundo término se anula, y al revés. Es la BCE, binary cross-entropy, entropía cruzada binaria. En el dibujo, las cuatro pérdidas del snippet y su promedio.
BCE a mano y en PyTorch
Cuatro logits. Las respuestas correctas: uno, cero, cero, uno. La sigmoide da las probabilidades. La fórmula, término a término. Y la versión de PyTorch, que recibe los logits directamente, sin sigmoide: la aplica adentro, de forma más estable. El primer ejemplo era uno y la red dio cero coma ochenta y ocho: pérdida cero coma trece. El cuarto también era uno, pero la red dio cero coma cero cinco: pérdida tres coma cero cinco. El promedio, uno coma uno dos, coincide con PyTorch.
z = torch.tensor([2.0, -1.0, 0.5, -3.0]) # logits: lo que sale de la red
y = torch.tensor([1.0, 0.0, 0.0, 1.0]) # respuesta correcta: 0 o 1
p = torch.sigmoid(z) # probabilidad de "1"
L = -(y * torch.log(p) + (1 - y) * torch.log(1 - p))
print("p: ", p.numpy().round(2))
print("pérdidas:", L.numpy().round(2))
print("BCE a mano:", round(L.mean().item(), 4))
print("PyTorch: ", round(F.binary_cross_entropy_with_logits(z, y).item(), 4))
p: [0.88 0.27 0.62 0.05]
pérdidas: [0.13 0.31 0.97 3.05]
BCE a mano: 1.1157
PyTorch: 1.1157
¿Por qué no MSE para probabilidades?
Podríamos usar el error cuadrático contra cero o uno. ¿Por qué no? Supongamos que la respuesta correcta es uno, y probamos cuatro probabilidades. El cuadrático. Y menos el logaritmo. Con cero coma nueve, los dos castigan poco. Pero mirá abajo: si la red da cero coma cero uno, segura y equivocada, el cuadrático da cero coma noventa y ocho, casi lo mismo que con cero coma uno. Nunca pasa de uno. Menos el logaritmo da cuatro coma seis, y sigue creciendo sin límite. La entropía cruzada castiga muy caro equivocarse con confianza.
# la respuesta correcta es 1; ¿cuánto cuesta cada probabilidad?
for p in [0.9, 0.5, 0.1, 0.01]:
mse = (1 - p) ** 2
ce = -np.log(p)
print(f"p = {p:<4} MSE = {mse:.2f} -log p = {ce:.2f}")
p = 0.9 MSE = 0.01 -log p = 0.11
p = 0.5 MSE = 0.25 -log p = 0.69
p = 0.1 MSE = 0.81 -log p = 2.30
p = 0.01 MSE = 0.98 -log p = 4.61
El castigo, comparado
Los mismos números, fila por fila. Bien: los dos castigan poco. Duda: un medio. Menos el logaritmo da cero coma seis nueve, un número que vas a ver mucho. Mal. Y mal y seguro: acá está la diferencia. Hay otra razón más técnica: con sigmoide más cuadrático, la pendiente se aplana justo cuando la red está segura y equivocada, y aprende lento. Con entropía cruzada, no. La cuenta la hacemos en el episodio cinco.
| p del correcto | MSE (1 − p)² | −log p | lectura |
|---|---|---|---|
| 0,9 | 0,01 | 0,11 | bien |
| 0,5 | 0,25 | 0,69 | duda |
| 0,1 | 0,81 | 2,30 | mal |
| 0,01 | 0,98 | 4,61 | mal y seguro |
Parte 3 · Muchas clases: softmax y entropía cruzada
Para elegir entre diez dígitos hace falta una probabilidad por clase, y que sumen uno.
Softmax y entropía cruzada
softmax: exponencial y normalizar
menos el log de la probabilidad de la clase correcta
lo derivamos en el episodio 5
La softmax generaliza la sigmoide a muchas clases. e a cada logit da siempre positivo; dividir por la suma hace que todo sume uno. En el dibujo, los logits dos, uno y cero coma uno se vuelven cero coma sesenta y seis, cero coma veinticuatro y cero coma diez. La pérdida: menos el logaritmo de la probabilidad de la clase correcta. En el dibujo, el ejemplo cero acertó con cero coma sesenta y seis: pérdida cero coma cuatro dos. El ejemplo uno le dio cero coma cero nueve a la correcta: dos coma cuatro dos. Adelanto: la pendiente respecto de cada logit es la probabilidad, menos uno en la clase correcta. La derivamos en el episodio cinco. Es la misma pérdida de Alineación, episodio dos, con doscientas cincuenta y seis mil clases.
Entropía cruzada, a mano y en PyTorch
Un batch de dos ejemplos, cada uno con tres logits. La clase correcta de cada uno: cero y dos. La softmax, fila por fila. De cada fila tomamos la probabilidad de su clase correcta, con indexado de PyTorch, y le aplicamos menos log. Y F punto cross entropy, que recibe logits, no probabilidades. Las probabilidades, las pérdidas cero coma cuatro dos y dos coma cuatro dos, y el promedio, uno coma cuatro dos, igual en los dos lados. Error clásico: pasarle a cross entropy la salida de una softmax. Aplica otra softmax encima, y la pérdida queda mal sin dar error.
z = torch.tensor([[2.0, 1.0, 0.1], # ejemplo 0: logits de 3 clases
[0.5, 2.5, 0.3]]) # ejemplo 1
y = torch.tensor([0, 2]) # clase correcta de cada uno
p = torch.exp(z) / torch.exp(z).sum(dim=1, keepdim=True) # softmax
print("p ej. 0:", p[0].numpy().round(2))
print("p ej. 1:", p[1].numpy().round(2))
L = -torch.log(p[torch.arange(2), y]) # -log p de la clase correcta
print("pérdidas:", L.numpy().round(3))
print("a mano: ", round(L.mean().item(), 4))
print("F.cross_entropy:", round(F.cross_entropy(z, y).item(), 4))
p ej. 0: [0.66 0.24 0.1 ]
p ej. 1: [0.11 0.8 0.09]
pérdidas: [0.417 2.42 ]
a mano: 1.4185
F.cross_entropy: 1.4185
La trampa numérica
Una trampa que PyTorch resuelve por vos. Logits grandes, de mil. La softmax tal cual. El arreglo: restarle a todos el máximo antes de exponenciar. Y lo que se llama log sum exp. La versión ingenua da nan, not a number: e a la mil no entra en un número de punto flotante, da infinito, e infinito sobre infinito no está definido. La estable da cero coma seis seis, cero coma dos cuatro, cero coma cero nueve: lo mismo que con logits dos, uno y cero, porque solo importan las diferencias.
z = np.array([1000.0, 999.0, 998.0]) # logits grandes
ingenua = np.exp(z) / np.exp(z).sum() # e^1000 no entra en un float
print("ingenua:", ingenua)
m = z.max() # restar el máximo
estable = np.exp(z - m) / np.exp(z - m).sum()
print("estable:", estable.round(3))
print("logsumexp:", round(m + np.log(np.exp(z - m).sum()), 3))
ingenua: [nan nan nan]
estable: [0.665 0.245 0.09 ]
logsumexp: 1000.408
Por qué restar el máximo no cambia nada
e^{−m} sale de factor común y se cancela
logsumexp: el mayor exponente queda en 0
La cuenta que justifica el truco. Restar m en el exponente es multiplicar por e a la menos m, arriba y abajo. Como es el mismo factor en el numerador y en cada término de la suma, sale de factor común y se cancela. En el dibujo, los logits del snippet ya restados: cero, menos uno, menos dos. Y las probabilidades no cambian. Lo mismo con el logaritmo de la suma: sacamos m afuera. Ahora el exponente más grande es cero, y e a la cero es uno: nunca se desborda. En el dibujo, las tres exponenciales suman uno coma cinco, y el resultado es mil coma cuatro, como imprimió el snippet.
Guo, Pleiss, Sun & Weinberger, 2017 — On Calibration of Modern Neural Networks · arXiv:1706.04599
softmax(z / T) · «We discover that modern neural networks, unlike those from a decade ago, are poorly calibrated.»
Una consecuencia de entrenar con entropía cruzada. Guo y coautores mostraron que las redes modernas están mal calibradas: cuando dicen noventa por ciento, aciertan bastante menos. Calibrada significa que la confianza coincide con la tasa de aciertos. Y lo que mejor funcionó fue lo más simple: dividir todos los logits por un único número, la temperatura T, ajustado después de entrenar. Un solo parámetro.
Parte 4 · Elegir la pérdida
Cada tarea tiene su combinación de salida y pérdida. Y PyTorch espera algo concreto en cada caso.
Tarea → salida → pérdida
La tabla para tener a mano. Si la red predice un número, la última capa no lleva activación y la pérdida es el error cuadrático. Si es sí o no, la red saca un solo logit, y la pérdida es la entropía cruzada binaria, con la sigmoide adentro. Si es una entre K clases, K logits, y la entropía cruzada con la softmax adentro. Fijate en la columna de PyTorch: las dos de clasificación reciben logits. La activación final está escondida en la pérdida.
| tarea | salida de la red | pérdida | en PyTorch |
|---|---|---|---|
| un número | lineal, sin activación | MSE | nn.MSELoss |
| sí o no | 1 logit | BCE | nn.BCEWithLogitsLoss |
| una de K clases | K logits | entropía cruzada | nn.CrossEntropyLoss |
La pérdida de una red que no sabe nada
Un chequeo que conviene hacer siempre antes de entrenar. Doscientos cincuenta y seis dígitos escritos a mano, con píxeles escalados entre cero y uno. La red del episodio dos, con pesos al azar. La entropía cruzada de sus logits. Y la comparamos con la de adivinar: si das un décimo a cada dígito, la pérdida es menos el log de un décimo. La red sin entrenar da dos coma tres tres. Adivinar, dos coma tres cero. Coinciden: una red recién creada no sabe nada, y su pérdida tiene que estar cerca de log de diez. Si arrancás muy lejos de ese número, algo está mal antes de empezar.
d = load_digits()
X = torch.tensor(d.data[:256], dtype=torch.float32) / 16 # píxeles 0..1
y = torch.tensor(d.target[:256]) # dígito 0..9
red = nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10))
logits = red(X) # (256, 10), sin softmax
L = F.cross_entropy(logits, y) # softmax + -log, adentro
print("sin entrenar: ", round(L.item(), 3))
print("azar, -log(1/10): ", round(math.log(10), 3))
sin entrenar: 2.331
azar, -log(1/10): 2.303
Resumiendo
- Pérdida: un número con pendiente que mide el error
- MSE: promedio de ; los errores grandes dominan
- BCE: de la probabilidad del correcto, con sigmoide
- Entropía cruzada: softmax y ; recibe logits
- Restar el máximo: misma softmax, sin desbordes
- Próximo: backpropagation, la pendiente de la pérdida respecto de cada peso
Papers del episodio
¿Qué tal esta lección?
Sé el primero en calificarla
¿Terminaste la lección?
Márcala para sumar +50 XP y mantener tu racha.
Tutor IA
Contexto: Pérdidas
Conectando con el tutor…