Hasta este punto del curso ya hemos trabajado con tensores, entrenamiento, evaluación, regularización, Dropout, Batch Normalization e incluso ajuste de hiperparámetros.
Ahora es momento de estudiar una arquitectura muy importante y muy clásica dentro del Deep Learning: la red neuronal multicapa, también conocida como MLP por sus siglas en inglés: Multi-Layer Perceptron.
Comprender bien qué es un MLP resulta fundamental, porque muchas ideas de redes más complejas parten precisamente de aquí.
Cuando decimos que una red es multicapa, queremos decir que no está formada por una sola transformación lineal simple, sino por varias capas encadenadas.
En una visión muy general, un MLP suele tener:
La palabra “oculta” no significa misteriosa: simplemente indica que esa capa no es ni la entrada original ni la salida final del modelo.
El nombre viene de la idea histórica del perceptrón como unidad básica de decisión.
Hoy, cuando hablamos de MLP, no pensamos tanto en un único perceptrón aislado, sino en una red compuesta por muchas neuronas organizadas en capas totalmente conectadas.
En la práctica, un MLP es una secuencia de capas lineales y funciones de activación que transforman la información paso a paso.
Si usamos solo una capa lineal, el modelo queda limitado a representar relaciones lineales.
Eso puede ser suficiente para algunos problemas muy simples, pero rápidamente se vuelve insuficiente cuando las relaciones entre entrada y salida son más complejas.
El gran salto aparece cuando agregamos capas ocultas y funciones de activación no lineales. Allí el modelo gana mucha más capacidad de representación.
Este punto es central: si encadenáramos varias capas lineales sin activaciones no lineales entre ellas, el resultado global seguiría siendo equivalente a una sola transformación lineal.
Por eso, un MLP necesita funciones como ReLU, Sigmoid o Tanh entre capas.
La no linealidad es la que permite que el modelo aprenda relaciones ricas y no simples rectas o planos.
Una forma sencilla de visualizar un MLP es esta:
Cada capa toma una representación de los datos y la transforma en otra representación.
Podemos pensar que las capas ocultas van construyendo descripciones internas cada vez más útiles para la tarea.
En un MLP clásico, las capas suelen ser totalmente conectadas o fully connected.
Eso significa que cada neurona de una capa recibe información de todas las neuronas de la capa anterior.
En PyTorch, este tipo de capa suele implementarse con nn.Linear.
Cuando una entrada entra a un MLP, ocurre algo como esto:
Ese recorrido es la propagación hacia adelante o forward pass.
Imagina un MLP con arquitectura:
Esto podría significar:
Es una red pequeña, pero ya es claramente multicapa.
Una idea importante es que las capas ocultas no suelen interpretarse una por una de forma tan directa como las entradas o la salida.
Su papel principal es construir representaciones internas útiles para el problema.
En otras palabras, las capas ocultas “reexpresan” la información de manera que la tarea final resulte más fácil para la red.
La gran ventaja del MLP es su flexibilidad para modelar relaciones no lineales.
Puede usarse en clasificación, regresión y muchas tareas tabulares donde las relaciones entre variables no son triviales.
Por eso sigue siendo una arquitectura muy importante, especialmente como punto de partida conceptual.
Aunque los MLP son muy útiles, no son la mejor herramienta para todos los problemas.
Por ejemplo, para imágenes suelen ser más adecuadas las redes convolucionales, y para secuencias largas suelen aparecer otras arquitecturas más especializadas.
Sin embargo, para datos tabulares y para fines didácticos, el MLP es una herramienta excelente.
En PyTorch, un MLP se puede construir definiendo varias capas nn.Linear y conectándolas mediante activaciones.
Una idea básica podría verse así:
Luego, en forward, se decide en qué orden se aplican junto con funciones como ReLU.
Aunque PyTorch permite usar nn.Sequential, para aprender conviene mucho definir el modelo como una clase que hereda de nn.Module.
Eso hace más claro qué capas tiene el modelo y cómo fluye la información.
Además, cuando la arquitectura crece o requiere más control, definir una clase propia se vuelve la opción más natural.
En un MLP, la función forward describe el recorrido de la información.
Por ejemplo:
Ese orden es el corazón de la red.
En clasificación, la capa de salida del MLP depende del tipo de problema.
Luego, la función de pérdida y la interpretación de la salida deben elegirse de acuerdo con el problema.
En regresión, la salida suele ser un valor continuo, por lo que muchas veces se usa una sola neurona final sin activación especial.
Esto muestra que la misma arquitectura general de MLP puede adaptarse a distintos tipos de tareas.
Lo que cambia es la forma de la salida, la pérdida y la interpretación del resultado.
Cuantas más capas ocultas y más neuronas tenga un MLP, mayor será su capacidad para representar relaciones complejas.
Pero ese aumento de capacidad también trae costos: más parámetros, más riesgo de overfitting y más dificultad de entrenamiento.
Por eso no conviene pensar que “más grande” siempre significa “mejor”.
Todo lo que ya estudiamos se conecta directamente con los MLP:
En ese sentido, el MLP es un punto de encuentro entre muchas de las ideas vistas hasta ahora.
Algunos errores frecuentes son:
Estos errores son normales al comenzar y forman parte del aprendizaje.
En la aplicación final construiremos un MLP para un problema de clasificación binaria sobre datos tabulares.
La idea será predecir si un estudiante aprobará una evaluación final a partir de varias variables académicas.
El ejemplo incluirá:
El ejemplo no será excesivamente complejo, pero sí lo bastante rico como para mostrar el papel real de un MLP.
De ese modo, el estudiante podrá ver una arquitectura multicapa funcionando sobre una tarea concreta, sin perderse en detalles innecesarios.
El objetivo es fijar la idea de que un MLP es una red de capas densas con activaciones intermedias que aprende representaciones útiles para resolver una tarea.
El siguiente script construye y entrena un MLP para predecir si un estudiante aprobará una evaluación final a partir de variables académicas y de comportamiento.
import torch
import torch.nn as nn
import torch.optim as optim
# Fijamos la semilla para repetir el experimento.
torch.manual_seed(23)
def generar_datos(n):
# Variables del estudiante.
horas_estudio = 10 * torch.rand(n, 1)
asistencia = 0.4 + 0.6 * torch.rand(n, 1)
tareas = torch.rand(n, 1)
parcial_1 = 10 * torch.rand(n, 1)
parcial_2 = 10 * torch.rand(n, 1)
participacion = torch.rand(n, 1)
distraccion = torch.rand(n, 1)
# Armamos las entradas normalizando algunas variables.
X = torch.cat([
horas_estudio / 10.0,
asistencia,
tareas,
parcial_1 / 10.0,
parcial_2 / 10.0,
participacion,
distraccion
], dim=1)
# Regla oculta del problema.
puntaje = (
3.0 * (horas_estudio / 10.0) +
2.6 * asistencia +
2.5 * tareas +
2.2 * (parcial_1 / 10.0) +
2.7 * (parcial_2 / 10.0) +
1.4 * participacion -
2.1 * distraccion +
1.2 * (tareas * asistencia)
)
ruido = 0.8 * torch.randn(n, 1)
y = ((puntaje + ruido) > 6.4).float()
return X, y
# Separamos entrenamiento y validacion.
X_train, y_train = generar_datos(180)
X_val, y_val = generar_datos(450)
class MLPClasificacion(nn.Module):
def __init__(self):
super().__init__()
self.capa1 = nn.Linear(7, 32)
self.capa2 = nn.Linear(32, 16)
self.salida = nn.Linear(16, 1)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(p=0.20)
def forward(self, x):
# Primera transformacion oculta.
x = self.capa1(x)
x = self.relu(x)
x = self.dropout(x)
# Segunda transformacion oculta.
x = self.capa2(x)
x = self.relu(x)
# Capa final de salida.
x = self.salida(x)
return x
def accuracy_desde_logits(logits, y_real):
probs = torch.sigmoid(logits)
pred = (probs >= 0.5).float()
return (pred == y_real).float().mean().item()
modelo = MLPClasificacion()
criterio = nn.BCEWithLogitsLoss()
optimizador = optim.Adam(modelo.parameters(), lr=0.01, weight_decay=0.001)
for epoca in range(250):
modelo.train()
logits_train = modelo(X_train)
loss_train = criterio(logits_train, y_train)
optimizador.zero_grad()
loss_train.backward()
optimizador.step()
if (epoca + 1) % 50 == 0:
modelo.eval()
with torch.no_grad():
logits_train_eval = modelo(X_train)
logits_val = modelo(X_val)
train_loss = criterio(logits_train_eval, y_train).item()
val_loss = criterio(logits_val, y_val).item()
train_acc = accuracy_desde_logits(logits_train_eval, y_train)
val_acc = accuracy_desde_logits(logits_val, y_val)
print(f"Epoca {epoca+1:3d} | train loss={train_loss:.4f} | val loss={val_loss:.4f} | train acc={train_acc:.3f} | val acc={val_acc:.3f}")
modelo.eval()
with torch.no_grad():
logits_train = modelo(X_train)
logits_val = modelo(X_val)
train_loss = criterio(logits_train, y_train).item()
val_loss = criterio(logits_val, y_val).item()
train_acc = accuracy_desde_logits(logits_train, y_train)
val_acc = accuracy_desde_logits(logits_val, y_val)
print()
print("RESUMEN FINAL")
print(f"train loss={train_loss:.4f} | val loss={val_loss:.4f}")
print(f"train acc={train_acc:.3f} | val acc={val_acc:.3f}")
# Probamos el MLP con estudiantes nuevos.
ejemplos_nuevos = torch.tensor([
[0.90, 0.95, 0.85, 0.80, 0.88, 0.75, 0.10],
[0.25, 0.55, 0.20, 0.35, 0.40, 0.25, 0.85],
[0.65, 0.85, 0.70, 0.60, 0.72, 0.60, 0.20]
], dtype=torch.float32)
with torch.no_grad():
probs = torch.sigmoid(modelo(ejemplos_nuevos))
preds = (probs >= 0.5).float()
print()
print("Probabilidades para estudiantes nuevos:")
print(probs)
print("Predicciones finales:")
print(preds)
Este ejemplo permite ver a un MLP completo en acción: múltiples capas lineales, activaciones no lineales, Dropout, entrenamiento, evaluación y predicciones finales. Es una forma concreta de conectar la teoría con la práctica.
Si estás comenzando con MLP, estas recomendaciones suelen ayudar:
ReLU.Las redes neuronales multicapa son una pieza central para entender el Deep Learning. Aunque luego aparezcan arquitecturas más especializadas, muchas de las ideas fundamentales se entienden primero aquí.
Dominar el MLP significa comprender cómo una red transforma información capa a capa hasta llegar a una predicción útil.