22. Ajuste de hiperparámetros

22.1 Introducción

Hasta ahora hemos construido modelos, los hemos entrenado, evaluado y regularizado. Pero en Deep Learning aparece una pregunta muy importante que todavía no hemos estudiado de manera directa:

¿Cómo elegimos los valores correctos para entrenar un modelo?

Esa pregunta nos lleva al tema de los hiperparámetros.

22.2 Qué es un hiperparámetro

Un hiperparámetro es una configuración del proceso de entrenamiento o de la arquitectura que no se aprende automáticamente a partir de los datos.

Es decir, son valores que el programador o investigador debe decidir antes o durante el entrenamiento.

Ejemplos muy comunes son la tasa de aprendizaje, la cantidad de épocas, el tamaño del batch, la cantidad de neuronas y la intensidad del weight decay.

22.3 Diferencia entre parámetro e hiperparámetro

Es fundamental no confundir estos dos conceptos:

  • Parámetros: son valores que el modelo aprende, como pesos y bias.
  • Hiperparámetros: son valores que configuramos externamente, como lr o el número de capas.

Los parámetros cambian con el entrenamiento. Los hiperparámetros, en cambio, definen cómo será ese entrenamiento o cómo estará construido el modelo.

22.4 Por qué importan tanto

Dos modelos con la misma arquitectura pueden comportarse de manera muy distinta si tienen hiperparámetros diferentes.

Por ejemplo, una tasa de aprendizaje demasiado grande puede volver el entrenamiento inestable, mientras que una demasiado pequeña puede hacerlo lentísimo.

Esto significa que elegir hiperparámetros razonables puede marcar una diferencia enorme en el resultado final.

22.5 Hiperparámetros comunes en Deep Learning

Algunos de los hiperparámetros más usados son:

  • Tasa de aprendizaje (learning rate).
  • Cantidad de épocas.
  • Tamaño del batch.
  • Arquitectura del modelo.
  • Tipo de optimizador.
  • Weight decay.
  • Probabilidad de Dropout.

En la práctica, no siempre se ajustan todos a la vez. Muchas veces se empieza por los más influyentes.

22.6 La tasa de aprendizaje

La tasa de aprendizaje indica el tamaño de los pasos que da el optimizador al actualizar los parámetros.

Si es demasiado grande, el modelo puede oscilar o divergir. Si es demasiado pequeña, puede tardar muchísimo en mejorar.

Por eso suele ser uno de los hiperparámetros más importantes de todos.

22.7 Cantidad de épocas

La cantidad de épocas indica cuántas veces el modelo recorrerá el conjunto de entrenamiento.

Con pocas épocas, puede quedarse corto y no aprender lo suficiente. Con demasiadas, puede sobreajustar.

Por eso este hiperparámetro se relaciona mucho con el underfitting y el overfitting.

22.8 Tamaño del batch

El tamaño del batch indica cuántos ejemplos se procesan juntos antes de actualizar los parámetros.

Un batch pequeño introduce más ruido en el gradiente, pero puede generalizar bien y consumir menos memoria por paso. Un batch grande produce estimaciones más estables, aunque no siempre mejores.

No existe una regla universal: depende del problema, del hardware y del comportamiento del modelo.

22.9 Arquitectura del modelo

La cantidad de capas, de neuronas o el tipo de activación también forma parte del ajuste de hiperparámetros.

Una red demasiado pequeña puede no tener suficiente capacidad. Una demasiado grande puede sobreajustar o volverse difícil de entrenar.

Por eso ajustar hiperparámetros no es solo tocar números del optimizador; también implica pensar la estructura del modelo.

22.10 Hiperparámetros de regularización

Técnicas como Dropout y weight decay también introducen hiperparámetros.

nn.Dropout(p=0.3)
optim.Adam(modelo.parameters(), lr=0.01, weight_decay=0.001)

Aquí, tanto p como weight_decay son decisiones que debemos ajustar.

22.11 Ajustar hiperparámetros no es adivinar

Un error común es pensar que ajustar hiperparámetros consiste en “probar cosas al azar”.

En realidad, aunque la experimentación es importante, conviene hacerlo de manera organizada, registrando resultados y entendiendo qué se está cambiando.

La idea es reducir la improvisación y convertir el proceso en algo más sistemático.

22.12 El papel del conjunto de validación

Para ajustar hiperparámetros no debemos guiarnos por el rendimiento de entrenamiento, sino por el de validación.

La razón es sencilla: queremos elegir configuraciones que generalicen bien, no configuraciones que simplemente memoricen el conjunto de entrenamiento.

Por eso el ajuste de hiperparámetros y la validación están íntimamente conectados.

22.13 Qué no debe hacerse con el conjunto de prueba

El conjunto de prueba no debería usarse para tomar decisiones continuas sobre hiperparámetros.

Si lo usamos una y otra vez para elegir configuraciones, deja de ser una evaluación imparcial.

La lógica correcta suele ser esta:

  • Entrenamiento para ajustar parámetros.
  • Validación para ajustar hiperparámetros.
  • Prueba para la evaluación final.

22.14 Una estrategia manual simple

Una forma muy común de empezar es cambiar un hiperparámetro a la vez.

Por ejemplo, probar varias tasas de aprendizaje manteniendo el resto fijo, observar cuál funciona mejor y luego pasar al siguiente hiperparámetro.

Este enfoque no es perfecto, pero es muy didáctico y bastante práctico para problemas pequeños.

22.15 Grid search

Otra estrategia es el grid search. Consiste en definir una grilla de valores posibles y probar sistemáticamente todas las combinaciones.

Por ejemplo:

learning_rate = [0.1, 0.01, 0.001]
batch_size = [16, 32, 64]

Luego se evalúan todas las combinaciones posibles entre esos valores.

22.16 Ventajas y límites del grid search

El grid search tiene la ventaja de ser claro y sistemático.

Sin embargo, puede volverse costoso rápidamente si el número de hiperparámetros o de valores posibles crece demasiado.

Por eso funciona mejor en problemas pequeños o cuando queremos experimentar con pocos candidatos bien elegidos.

22.17 Random search

Una alternativa muy usada es random search. En lugar de probar todas las combinaciones posibles, se eligen combinaciones al azar dentro de ciertos rangos.

Esto puede ser sorprendentemente efectivo, sobre todo cuando hay muchos hiperparámetros y no todos tienen la misma importancia.

La idea es explorar de manera más amplia sin necesidad de revisar exhaustivamente toda la grilla.

22.18 Métodos más avanzados

En proyectos más avanzados también existen técnicas como optimización bayesiana, Hyperband o enfoques automáticos más sofisticados.

Sin embargo, para un estudiante que está empezando, lo esencial es comprender bien el enfoque manual, la validación y las comparaciones organizadas.

Con esas bases, luego será mucho más fácil entender herramientas más complejas.

22.19 Qué hiperparámetros conviene ajustar primero

En muchos casos, conviene empezar por los que más impacto suelen tener:

  • Tasa de aprendizaje.
  • Arquitectura básica.
  • Regularización.
  • Cantidad de épocas.

Después, si hace falta, se refinan otros detalles.

22.20 El peligro de cambiar demasiadas cosas a la vez

Si cambias tasa de aprendizaje, arquitectura, dropout y optimizador simultáneamente, será difícil saber qué produjo la mejora o el empeoramiento.

Por eso, al aprender, conviene avanzar con cierto orden.

Esto no significa que siempre debas cambiar exactamente una sola variable, pero sí que debes mantener trazabilidad sobre los experimentos.

22.21 Registrar resultados

Una buena práctica es anotar cada experimento.

Por ejemplo, para cada corrida puedes guardar:

  • Los hiperparámetros usados.
  • La pérdida de validación final.
  • La accuracy de validación final.
  • Observaciones relevantes.

Eso evita repetir pruebas innecesarias y ayuda a comparar con claridad.

22.22 Qué significa “mejor hiperparámetro”

No siempre existe un único mejor valor en sentido absoluto.

Muchas veces buscamos una configuración que logre un buen equilibrio entre rendimiento, estabilidad, costo computacional y capacidad de generalización.

Por eso, el “mejor” hiperparámetro suele depender del criterio que estamos optimizando.

22.23 Ajustar hiperparámetros también requiere criterio

Si una configuración mejora apenas 0.1% pero tarda el triple, quizá no sea una mejora realmente útil.

Si otra configuración es muy inestable entre corridas, quizá tampoco convenga confiar demasiado en ella.

Esto muestra que el ajuste de hiperparámetros no es solo una tarea numérica, sino también una tarea de interpretación.

22.24 Qué haremos en la aplicación final

En la aplicación final vamos a automatizar una búsqueda pequeña y didáctica.

Tomaremos un problema de clasificación binaria y probaremos varias combinaciones de hiperparámetros:

  • Diferentes tasas de aprendizaje.
  • Diferentes intensidades de Dropout.
  • Diferentes cantidades de neuronas ocultas.

Luego compararemos el rendimiento de validación y elegiremos la mejor configuración.

22.25 Por qué este ejemplo es útil

El ejemplo no intenta cubrir todas las técnicas posibles de ajuste, sino mostrar de forma clara la lógica general del proceso.

El estudiante podrá ver que el ajuste de hiperparámetros no es magia: consiste en definir candidatos, entrenar, medir validación y comparar.

Eso ya constituye una base muy valiosa para problemas más serios.

22.26 Código completo para ejecutar

El siguiente script genera un problema de clasificación binaria relacionado con rendimiento académico y luego prueba varias combinaciones de hiperparámetros. Al final informa cuál fue la mejor configuración según la pérdida de validación.

import itertools
import torch
import torch.nn as nn
import torch.optim as optim

# Fijamos semilla para repetir el experimento.
torch.manual_seed(22)

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)
    distraccion = torch.rand(n, 1)

    # Armamos las entradas.
    X = torch.cat([
        horas_estudio / 10.0,
        asistencia,
        tareas,
        parcial_1 / 10.0,
        parcial_2 / 10.0,
        distraccion
    ], dim=1)

    # Regla oculta con algo de ruido.
    puntaje = (
        3.2 * (horas_estudio / 10.0) +
        2.8 * asistencia +
        2.5 * tareas +
        2.0 * (parcial_1 / 10.0) +
        2.4 * (parcial_2 / 10.0) -
        2.0 * distraccion +
        1.0 * (tareas * asistencia)
    )
    ruido = 0.7 * torch.randn(n, 1)
    y = ((puntaje + ruido) > 6.0).float()
    return X, y

# Separamos entrenamiento y validacion.
X_train, y_train = generar_datos(140)
X_val, y_val = generar_datos(400)

class RedClasificacion(nn.Module):
    def __init__(self, hidden_dim, dropout_p):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(6, hidden_dim),
            nn.ReLU(),
            nn.Dropout(p=dropout_p),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Dropout(p=dropout_p),
            nn.Linear(hidden_dim, 1)
        )

    def forward(self, x):
        return self.net(x)

def accuracy_desde_logits(logits, y_real):
    probs = torch.sigmoid(logits)
    pred = (probs >= 0.5).float()
    return (pred == y_real).float().mean().item()

def entrenar_y_evaluar(hidden_dim, dropout_p, lr, epochs=180):
    modelo = RedClasificacion(hidden_dim=hidden_dim, dropout_p=dropout_p)
    criterio = nn.BCEWithLogitsLoss()
    optimizador = optim.Adam(modelo.parameters(), lr=lr)

    for _ in range(epochs):
        modelo.train()
        logits_train = modelo(X_train)
        loss_train = criterio(logits_train, y_train)

        optimizador.zero_grad()
        loss_train.backward()
        optimizador.step()

    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)

    return {
        "hidden_dim": hidden_dim,
        "dropout_p": dropout_p,
        "lr": lr,
        "train_loss": train_loss,
        "val_loss": val_loss,
        "train_acc": train_acc,
        "val_acc": val_acc,
    }

# Definimos una grilla pequena de hiperparametros.
learning_rates = [0.1, 0.01, 0.001]
dropouts = [0.0, 0.2, 0.4]
hidden_dims = [16, 32, 64]

resultados = []

for hidden_dim, dropout_p, lr in itertools.product(hidden_dims, dropouts, learning_rates):
    resultado = entrenar_y_evaluar(hidden_dim=hidden_dim, dropout_p=dropout_p, lr=lr)
    resultados.append(resultado)
    print(
        f"hidden={hidden_dim:2d} | dropout={dropout_p:.1f} | lr={lr:.3f} | "
        f"train_loss={resultado['train_loss']:.4f} | val_loss={resultado['val_loss']:.4f} | "
        f"train_acc={resultado['train_acc']:.3f} | val_acc={resultado['val_acc']:.3f}"
    )

# Elegimos la mejor configuracion segun perdida de validacion.
mejor = min(resultados, key=lambda x: x["val_loss"])

print()
print("MEJOR CONFIGURACION ENCONTRADA")
print(mejor)

# Reentrenamos un modelo con la mejor configuracion para hacer predicciones nuevas.
modelo_final = RedClasificacion(hidden_dim=mejor["hidden_dim"], dropout_p=mejor["dropout_p"])
criterio = nn.BCEWithLogitsLoss()
optimizador = optim.Adam(modelo_final.parameters(), lr=mejor["lr"])

for _ in range(180):
    modelo_final.train()
    logits_train = modelo_final(X_train)
    loss_train = criterio(logits_train, y_train)
    optimizador.zero_grad()
    loss_train.backward()
    optimizador.step()

modelo_final.eval()
ejemplos_nuevos = torch.tensor([
    [0.90, 0.95, 0.90, 0.80, 0.85, 0.10],
    [0.25, 0.60, 0.30, 0.40, 0.35, 0.80],
    [0.70, 0.88, 0.75, 0.70, 0.78, 0.25]
], dtype=torch.float32)

with torch.no_grad():
    probs = torch.sigmoid(modelo_final(ejemplos_nuevos))
    preds = (probs >= 0.5).float()
    print()
    print("Predicciones con el modelo final:")
    print(probs)
    print(preds)

Este ejemplo muestra algo muy importante: el ajuste de hiperparámetros no consiste en elegir una configuración “porque suena bien”, sino en comparar alternativas con un criterio concreto de validación.

22.27 Errores comunes al ajustar hiperparámetros

  • Guiarse por el entrenamiento en lugar de la validación.
  • Cambiar demasiadas cosas al mismo tiempo y perder trazabilidad.
  • Usar el conjunto de prueba para ajustar decisiones intermedias.
  • No registrar resultados de cada experimento.
  • Concluir demasiado rápido con pocas pruebas mal organizadas.

22.28 Buenas prácticas para estudiantes

Si estás empezando, estas prácticas suelen ayudarte bastante:

  • Comenzar con una grilla pequeña y razonable.
  • Ajustar primero los hiperparámetros con mayor impacto.
  • Observar entrenamiento y validación al mismo tiempo.
  • Registrar qué cambiaste y qué resultado obtuviste.
  • No olvidar que el mejor valor depende del problema.

22.29 Qué debes recordar de este tema

  • Los hiperparámetros no se aprenden automáticamente; se eligen.
  • La tasa de aprendizaje, el batch size y la arquitectura son ejemplos típicos.
  • El ajuste de hiperparámetros debe hacerse mirando la validación.
  • Grid search y random search son estrategias comunes.
  • No conviene usar el conjunto de prueba para decidir configuraciones intermedias.
  • Ajustar hiperparámetros requiere organización, registro y criterio.

22.30 Cierre conceptual

El ajuste de hiperparámetros es una parte inevitable del trabajo en Deep Learning. No basta con definir una red y entrenarla una vez: casi siempre hace falta comparar configuraciones y observar con cuidado qué cambia.

Dominar este tema significa dejar de ver el entrenamiento como un proceso rígido y empezar a entenderlo como una búsqueda guiada por evidencia.