Módulo 5 · Redes neuronales
MLP, activaciones y pérdidas
En esta páginaEl problema
El problema
Tienes cuatro flores con dos medidas del pétalo en centímetros y su especie codificada como entero:
| muestra | largo_cm | ancho_cm | especie |
|---|---|---|---|
| M-1 | 1.4 | 0.2 | 0 |
| M-2 | 4.7 | 1.4 | 1 |
| M-3 | 6.0 | 2.5 | 2 |
| M-4 | 4.5 | 1.5 | 1 |
Una red recién creada recibe las dos medidas y devuelve tres números por flor. Para M-1 devuelve -0.207, -0.034 y 0.159. Dos son negativos y la suma no es 1, así que no son probabilidades. Antes de ajustar los pesos de la red necesitas una pérdida: un solo número que compare esos tres valores con la especie 0.
Capas y módulos
nn.Linear(entradas, salidas) es una capa que calcula, para cada salida, una combinación lineal de las entradas más un sesgo. Guarda sus pesos en weight y sus sesgos en bias. Ambos son parámetros: tensores que la capa crea con valores aleatorios.
Pregunta. Con nn.Linear(2, 3) y las cuatro flores, escribe la forma de capa.weight, la de capa.bias y la de salida. Decide también si capa.weight tiene requires_grad en True.
import torch
from torch import nn
X = torch.tensor([[1.4, 0.2], [4.7, 1.4], [6.0, 2.5], [4.5, 1.5]])
y = torch.tensor([0, 1, 2, 1])
torch.manual_seed(0)
capa = nn.Linear(2, 3)
salida = capa(X)
print(capa.weight.shape)
print(capa.bias.shape)
print(salida.shape)
print(capa.weight.requires_grad)
La salida esperada está oculta.
La capa aplica la misma cuenta a cada fila de X.
Pregunta. ¿Qué forma tiene X @ capa.weight.T? ¿Coincide capa(X) con X @ capa.weight.T + capa.bias?
Ver resultado
La primera frase da la forma del producto de las cuatro flores por los pesos transpuestos, sin sesgo. La segunda compara, valor por valor, la salida de capa con esa cuenta más el sesgo: True significa que las 12 salidas coinciden.
X @ capa.weight.T tiene forma (4, 3).
torch.allclose(capa(X), X @ capa.weight.T + capa.bias) da True.
X de forma (4, 2) por weight.T de forma (2, 3) da (4, 3). El sesgo de forma (3,) se suma a cada fila por broadcasting. capa(X) es exactamente esa cuenta.nn.Sequential encadena módulos: la salida de uno es la entrada del siguiente. nn.ReLU() es un módulo sin parámetros que se coloca entre dos capas lineales. Capas lineales alternadas con activaciones forman un perceptrón multicapa (MLP). Las capas intermedias se llaman ocultas.
Pregunta. ¿Cuántos parámetros tiene red en total? Cuenta pesos y sesgos de las dos capas lineales. Escribe también la forma de red(X).
torch.manual_seed(0)
red = nn.Sequential(nn.Linear(2, 8), nn.ReLU(), nn.Linear(8, 3))
print(red)
print(sum(p.numel() for p in red.parameters()))
print(red(X).shape)
La salida esperada está oculta.
Cuando el cálculo no es una cadena simple, se escribe una clase que hereda de nn.Module. En __init__ se crean las capas como atributos, y así el módulo registra sus parámetros. En forward se escribe el cálculo. modelo(X) llama a forward; no se llama a forward directamente.
Pregunta. Clasificador crea las mismas capas en el mismo orden que red, con la misma semilla. ¿Serán iguales las salidas de modelo y red?
class Clasificador(nn.Module):
def __init__(self, entradas, ocultas, clases):
super().__init__()
self.oculta = nn.Linear(entradas, ocultas)
self.activacion = nn.ReLU()
self.final = nn.Linear(ocultas, clases)
def forward(self, x):
return self.final(self.activacion(self.oculta(x)))
torch.manual_seed(0)
modelo = Clasificador(2, 8, 3)
print([nombre for nombre, _ in modelo.named_parameters()])
print(torch.allclose(modelo(X), red(X)))
La salida esperada está oculta.
Funciones de activación
Una activación se aplica a cada valor por separado y no cambia la forma. Las tres más comunes son ReLU, max(0, z); la sigmoide, 1 / (1 + e^(−z)); y tanh, la tangente hiperbólica. Cada una tiene su módulo: nn.ReLU(), nn.Sigmoid() y nn.Tanh(). Su derivada decide cuánto gradiente pasa hacia las capas anteriores en backpropagation.
Pregunta. Para z = −3, −1, 0, 1, 3, decide qué función tiene derivada cero en los negativos, cuál da valores entre 0 y 1, y cuál tiene la derivada más grande en z = 0.
Ver resultado
Cada fila da, para una función, su valor o su derivada en los cinco puntos, redondeados a tres decimales. Las columnas función y cantidad dicen qué se mide; las cinco columnas z = … dan el valor en cada punto. Las derivadas se obtienen con autograd. Por ejemplo, la fila «sigmoide, derivada» dice que la pendiente de la sigmoide vale 0.25 en z = 0 y 0.045 en z = 3.
| función | cantidad | z = −3 | z = −1 | z = 0 | z = 1 | z = 3 |
|---|---|---|---|---|---|---|
| relu | valor | 0 | 0 | 0 | 1 | 3 |
| relu | derivada | 0 | 0 | 0 | 1 | 1 |
| sigmoide | valor | 0.047 | 0.269 | 0.5 | 0.731 | 0.953 |
| sigmoide | derivada | 0.045 | 0.197 | 0.25 | 0.197 | 0.045 |
| tanh | valor | −0.995 | −0.762 | 0 | 0.762 | 0.995 |
| tanh | derivada | 0.01 | 0.42 | 1 | 0.42 | 0.01 |
tanh da valores entre −1 y 1, centrados en 0, con derivada 1 en z = 0. En z = ±3, la sigmoide y tanh casi no cambian: están saturadas y su derivada es cercana a 0.sin_activacion tiene las mismas capas lineales que red, pero sin ReLU entre ellas. Indexar un nn.Sequential devuelve sus capas en orden: sin_activacion[0] es la primera capa lineal, y W1 y b1 son sus pesos y sesgos; W2 y b2 son los de la segunda.
sin_activacion = nn.Sequential(nn.Linear(2, 8), nn.Linear(8, 3))
W1, b1 = sin_activacion[0].weight, sin_activacion[0].bias
W2, b2 = sin_activacion[1].weight, sin_activacion[1].bias
Pregunta. Se combinan los pesos de sin_activacion en una matriz W = W2 @ W1 y un sesgo b = W2 @ b1 + b2. ¿Qué forma tiene W? ¿X @ W.T + b reproduce la salida de sin_activacion?
Ver resultado
La primera frase da las formas de la matriz y el sesgo combinados. La segunda compara la salida de sin_activacion en las cuatro flores con la de esa única capa: True significa que los 12 valores coinciden con tolerancia 1e-6.
W tiene forma (3, 2) y b tiene forma (3,).
torch.allclose(sin_activacion(X), X @ W.T + b, atol=1e-6) da True.
nn.Linear(2, 3). La red sin activación es una sola capa lineal con más parámetros: la capa oculta de 8 no añade fronteras curvas. Un problema como XOR, que ninguna recta separa, necesita la activación entre capas.Logits y probabilidades
La última capa de modelo no lleva activación. Sus salidas se llaman logits: puntajes reales sin límite, uno por clase. torch.softmax(logits, dim=1) los convierte en probabilidades: aplica la exponencial a cada logit y divide entre la suma de su fila.
Pregunta. ¿Qué clase elige el modelo para cada flor si toma el logit más grande? ¿Cambia la elección si toma la probabilidad más grande?
with torch.no_grad():
logits = modelo(X)
probabilidades = torch.softmax(logits, dim=1)
print(logits.round(decimals=3))
print(probabilidades.round(decimals=3))
print(probabilidades.sum(dim=1).round(decimals=3))
print(logits.argmax(dim=1), probabilidades.argmax(dim=1))
La salida esperada está oculta.
Con dos clases basta un logit por muestra. La sigmoide lo convierte en la probabilidad de la clase positiva.
Pregunta. ¿Qué probabilidad corresponde a un logit 0? ¿Coincide sigmoid(2) con la segunda probabilidad de softmax([0, 2])?
Ver resultado
La primera fila aplica la sigmoide a tres logits por separado. La segunda aplica softmax a dos logits de una misma muestra. La columna «probabilidades» da los resultados en el mismo orden que los logits. Por ejemplo, en la primera fila el logit −2 da 0.119 y el logit 2 da 0.881.
| cálculo | probabilidades |
|---|---|
torch.sigmoid de [−2, 0, 2] | 0.119, 0.5, 0.881 |
torch.softmax de [0, 2] | 0.119, 0.881 |
0 da 0.5. Un logit positivo favorece la clase positiva y uno negativo, la otra. sigmoid(2) y la segunda probabilidad de softmax([0, 2]) valen 0.881: la sigmoide es softmax con dos clases cuando el logit de la clase negativa es 0.Funciones de pérdida
nn.CrossEntropyLoss recibe logits de forma (N, C) y etiquetas enteras de forma (N,), con valores de 0 a C − 1. Internamente aplica log_softmax. Para cada muestra toma menos el logaritmo de la probabilidad de su clase correcta y devuelve el promedio.
Pregunta. Las probabilidades de la clase correcta están entre 0.27 y 0.45. ¿Cerca de qué número quedará la pérdida? Escribe también el dtype y la forma de y.
criterio = nn.CrossEntropyLoss()
logits = modelo(X)
perdida = criterio(logits, y)
perdida_a_mano = -torch.log_softmax(logits, dim=1)[torch.arange(4), y].mean()
print(y.dtype, y.shape)
print(round(perdida.item(), 3), round(perdida_a_mano.item(), 3))
print(perdida.requires_grad)
La salida esperada está oculta.
La pérdida ya aplica log_softmax. Los logits seguros corresponden a una red que acierta en las cuatro flores con un margen de 5.
Pregunta. ¿Qué pérdida dan los logits seguros? ¿Qué pérdida da el mismo acierto si antes se aplica softmax?
seguros = torch.tensor(
[[5.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 5.0], [0.0, 5.0, 0.0]]
)
print(round(criterio(seguros, y).item(), 3))
print(round(criterio(torch.softmax(seguros, dim=1), y).item(), 3))
La salida esperada está oculta.
Para una pregunta de sí o no, como «¿la flor es de especie 2?», la red devuelve un logit por muestra, con forma (N, 1). nn.BCEWithLogitsLoss aplica la sigmoide internamente y compara con objetivos decimales 0.0 o 1.0 de la misma forma. Con reduction="none" devuelve la pérdida de cada muestra sin promediar.
Pregunta. Escribe el objetivo de cada flor y su forma. ¿Qué flor aporta más a la pérdida?
logits_binarios = torch.tensor([[-2.0], [0.5], [3.0], [-1.0]])
objetivo = (y == 2).float().unsqueeze(1)
p = torch.sigmoid(logits_binarios)
bce_a_mano = -(objetivo * torch.log(p) + (1 - objetivo) * torch.log(1 - p)).mean()
por_muestra = nn.BCEWithLogitsLoss(reduction="none")(logits_binarios, objetivo)
print(objetivo.squeeze(1), objetivo.shape)
print(por_muestra.squeeze(1).round(decimals=3))
print(round(nn.BCEWithLogitsLoss()(logits_binarios, objetivo).item(), 3))
print(round(bce_a_mano.item(), 3))
La salida esperada está oculta.
Para predecir un número, como el ancho a partir del largo, la red devuelve un valor por muestra. nn.MSELoss promedia los cuadrados de las diferencias. X[:, 1:2] selecciona la columna del ancho y conserva los dos ejes.
Pregunta. Calcula a mano la pérdida con estas cuatro predicciones y los anchos de la tabla.
prediccion = torch.tensor([[0.5], [1.5], [2.0], [1.5]])
ancho = X[:, 1:2]
print(ancho.shape)
print(round(nn.MSELoss()(prediccion, ancho).item(), 4))
La salida esperada está oculta.
Errores de forma
La primera capa de red es nn.Linear(2, 8). Multiplica el último eje de la entrada por weight.T, de forma (2, 8).
Pregunta. red espera dos características. ¿Qué pasa si recibe tres? ¿Qué forma de salida da una sola flor escrita como vector (2,)?
try:
red(torch.ones(4, 3))
except RuntimeError as error:
print(error)
una_flor = torch.tensor([1.4, 0.2])
print(red(una_flor).shape)
print(red(una_flor.unsqueeze(0)).shape)
La salida esperada está oculta.
CrossEntropyLoss también revisa las etiquetas.
Pregunta. ¿Qué pasa si las etiquetas llegan como decimales? ¿Y como columna de forma (4, 1)?
for etiquetas in [y.float(), y.unsqueeze(1)]:
try:
criterio(logits, etiquetas)
except RuntimeError as error:
print(etiquetas.dtype, tuple(etiquetas.shape), "->", error)
La salida esperada está oculta.
X[:, 1] selecciona la misma columna que X[:, 1:2], pero con forma (4,) en lugar de (4, 1).
Pregunta. Si X[:, 1] se usa como objetivo con prediccion de forma (4, 1), ¿qué forma tiene la resta? ¿La pérdida sigue siendo 0.0875?
import warnings
with warnings.catch_warnings(record=True) as avisos:
warnings.simplefilter("always")
perdida_mal = nn.MSELoss()(prediccion, X[:, 1])
print((prediccion - X[:, 1]).shape)
print(round(perdida_mal.item(), 4))
print(avisos[0].message)
La salida esperada está oculta.
Práctica
Traza guiada
Antes de procesar, escribe la forma de traza[0].weight, el número total de parámetros, las formas de oculta y salida_traza, y el intervalo donde caen los valores de oculta.
torch.manual_seed(0)
traza = nn.Sequential(nn.Linear(5, 16), nn.Tanh(), nn.Linear(16, 4))
entrada = torch.randn(10, 5)
oculta = traza[1](traza[0](entrada))
salida_traza = traza(entrada)
Comprueba con print. Si el número de parámetros no coincide, revisa que contaste un sesgo por salida en cada capa lineal.
Clasificador binario
Cambia Clasificador para que responda «¿la flor es de especie 2?» con un solo logit por flor. Antes de procesar, predice la forma de la salida y la del objetivo (y == 2).float().unsqueeze(1). Calcula nn.BCEWithLogitsLoss y comprueba con assert que coincide con la fórmula de «Funciones de pérdida» con tolerancia 1e-6. Si aparece Target size ... must be the same as input size, compara las dos formas.
Regresor propio
Genera 20 flores sintéticas con np.random.default_rng(0): largo uniforme entre 1 y 7, y ancho igual a 0.4 · largo − 0.4 más ruido normal con desviación 0.1. Escribe una subclase de nn.Module que:
- reciba el largo como tensor
float32de forma(20, 1) - use una capa oculta de 4 unidades con
nn.Tanh() - devuelva un valor por flor
- calcule
nn.MSELosscon un objetivo de forma(20, 1)y compruebe ambas formas conassert - repita la pérdida con el objetivo de forma
(20,)y registre el aviso y el valor distinto
Reto opcional
Escribe logits de forma (4, 3) que acierten en las cuatro flores y den una CrossEntropyLoss menor que 0.01. Antes de procesar, predice qué logit de cada fila debe ser el mayor y por cuánto. Después aplica softmax antes de la pérdida y calcula a mano el valor al que se acerca cuando la diferencia entre logits crece sin límite.
Errores que revisar
- Si la pérdida de clasificación se estanca muy por encima de 0 aunque la red acierte (con tres clases, cerca de 0.55), revisa si aplicaste
softmaxantes deCrossEntropyLoss. La pérdida espera logits. - Si aparece
mat1 and mat2 shapes cannot be multiplied (4x3 and 2x8), el número de características de la entrada (3) no coincide conin_featuresde la primera capa (2). - Si
CrossEntropyLosslanzaexpected scalar type Long but found Floatomulti-target not supported, convierte las etiquetas con.long()y dales forma(N,)con.squeeze(1). - Si
MSELossavisaUsing a target size ... that is different to the input size, la pérdida comparó todas las parejas por broadcasting. Da al objetivo la misma forma que la predicción. - Si
BCEWithLogitsLosslanzaTarget size (torch.Size([4])) must be the same as input size (torch.Size([4, 1])), añade el eje con.unsqueeze(1)y convierte el objetivo afloat.
Cierre
Explica qué forma, qué tipo y qué significado tienen la salida de modelo y las etiquetas y cuando se pasan a CrossEntropyLoss, y por qué esa salida no pasa antes por softmax.
Lista de comprobación
- ¿Qué forma tiene
weightennn.Linear(entradas, salidas)y cuántos parámetros aporta la capa? - ¿Por qué dos capas lineales sin activación equivalen a una sola?
- ¿Qué cambia y qué se conserva al pasar de logits a probabilidades en una misma fila?
- ¿Qué forma y qué
dtypeespera como objetivo cada una deCrossEntropyLoss,BCEWithLogitsLossyMSELoss? - ¿Qué error de forma produce un número equivocado sin lanzar excepción?
Documentación oficial
- Build the Neural Network: subclases de
nn.Module,nn.Linear,nn.ReLU,nn.Sequentialynn.Softmax. - torch.nn: índice de capas, activaciones y funciones de pérdida.
- CrossEntropyLoss: formas de entrada y objetivo, etiquetas enteras y
reduction. - BCEWithLogitsLoss: pérdida binaria que recibe logits.
- MSELoss: error cuadrático medio para regresión.