23. Redes neuronales multicapa (MLP)

23.1 Introducción

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

23.2 Qué significa “multicapa”

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:

  • Una capa de entrada.
  • Una o varias capas ocultas.
  • Una capa de salida.

La palabra “oculta” no significa misteriosa: simplemente indica que esa capa no es ni la entrada original ni la salida final del modelo.

23.3 Qué significa “Perceptron” en este contexto

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.

23.4 Por qué no alcanza una sola capa lineal

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.

23.5 La importancia de la no linealidad

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.

23.6 Estructura conceptual de un MLP

Una forma sencilla de visualizar un MLP es esta:

entrada - capa oculta 1 - capa oculta 2 - salida

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.

23.7 Qué significa “totalmente conectada”

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.

23.8 Flujo de información en un MLP

Cuando una entrada entra a un MLP, ocurre algo como esto:

  1. La entrada pasa por una primera capa lineal.
  2. Se aplica una activación.
  3. El resultado pasa a una nueva capa lineal.
  4. Se vuelve a aplicar otra activación si corresponde.
  5. Finalmente, se obtiene la salida.

Ese recorrido es la propagación hacia adelante o forward pass.

23.9 Un ejemplo pequeño

Imagina un MLP con arquitectura:

4 - 8 - 4 - 1

Esto podría significar:

  • 4 variables de entrada.
  • 8 neuronas en la primera capa oculta.
  • 4 neuronas en la segunda capa oculta.
  • 1 neurona de salida.

Es una red pequeña, pero ya es claramente multicapa.

23.10 Capas ocultas y representación interna

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.

23.11 Ventaja principal de un MLP

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.

23.12 Limitaciones de un MLP

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.

23.13 MLP en PyTorch

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í:

self.capa1 = nn.Linear(4, 8)
self.capa2 = nn.Linear(8, 4)
self.salida = nn.Linear(4, 1)

Luego, en forward, se decide en qué orden se aplican junto con funciones como ReLU.

23.14 MLP como clase propia

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.

23.15 La función forward en un MLP

En un MLP, la función forward describe el recorrido de la información.

Por ejemplo:

x = self.capa1(x)
x = self.relu(x)
x = self.capa2(x)
x = self.relu(x)
x = self.salida(x)

Ese orden es el corazón de la red.

23.16 MLP para clasificación

En clasificación, la capa de salida del MLP depende del tipo de problema.

  • En clasificación binaria, suele bastar una neurona de salida.
  • En clasificación multiclase, suele haber una neurona por clase.

Luego, la función de pérdida y la interpretación de la salida deben elegirse de acuerdo con el problema.

23.17 MLP para regresión

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.

23.18 Profundidad y capacidad

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

23.19 Relación con temas anteriores

Todo lo que ya estudiamos se conecta directamente con los MLP:

  • Los tensores representan entradas, salidas y parámetros.
  • El entrenamiento usa pérdida, backward y optimizador.
  • La evaluación mide generalización.
  • La regularización puede aplicarse con Dropout o weight decay.
  • BatchNorm también puede incorporarse si hace falta.

En ese sentido, el MLP es un punto de encuentro entre muchas de las ideas vistas hasta ahora.

23.20 Qué errores son comunes al empezar con MLP

Algunos errores frecuentes son:

  • No usar activaciones no lineales entre capas.
  • Elegir una capa de salida incompatible con la tarea.
  • Construir una red demasiado grande para pocos datos.
  • No revisar la forma de entradas y salidas.
  • Confundir arquitectura con hiperparámetros de entrenamiento.

Estos errores son normales al comenzar y forman parte del aprendizaje.

23.21 Qué observaremos en la aplicación final

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á:

  • Generación del dataset.
  • Separación en entrenamiento y validación.
  • Definición del MLP.
  • Entrenamiento.
  • Evaluación.
  • Predicciones sobre nuevos casos.

23.22 Por qué este ejemplo es útil

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.

23.23 Código completo para ejecutar

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.

23.24 Errores comunes al trabajar con MLP

  • Creer que varias capas lineales sin activación ya forman un MLP potente.
  • Usar una salida incorrecta para la tarea.
  • No observar validación mientras se entrena.
  • Construir redes demasiado grandes para datasets pequeños.
  • No revisar si las entradas están bien escaladas o normalizadas.

23.25 Buenas prácticas para estudiantes

Si estás comenzando con MLP, estas recomendaciones suelen ayudar:

  • Empezar con arquitecturas pequeñas.
  • Usar activaciones claras como ReLU.
  • Revisar la forma de entradas y salidas.
  • Comparar entrenamiento y validación.
  • Agregar regularización solo cuando tenga sentido.

23.26 Qué debes recordar de este tema

  • Un MLP es una red neuronal multicapa formada por capas densas y activaciones no lineales.
  • Las capas ocultas construyen representaciones internas útiles.
  • La no linealidad es esencial para que el modelo gane capacidad real.
  • Los MLP son muy útiles para muchos problemas tabulares.
  • La arquitectura, el entrenamiento y la regularización influyen fuertemente en su comportamiento.

23.27 Cierre conceptual

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.