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:
Esa pregunta nos lleva al tema de los hiperparámetros.
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.
Es fundamental no confundir estos dos conceptos:
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.
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.
Algunos de los hiperparámetros más usados son:
learning rate).En la práctica, no siempre se ajustan todos a la vez. Muchas veces se empieza por los más influyentes.
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.
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.
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.
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.
Técnicas como Dropout y weight decay también introducen hiperparámetros.
Aquí, tanto p como weight_decay son decisiones que debemos ajustar.
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.
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.
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:
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.
Otra estrategia es el grid search. Consiste en definir una grilla de valores posibles y probar sistemáticamente todas las combinaciones.
Por ejemplo:
Luego se evalúan todas las combinaciones posibles entre esos valores.
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.
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.
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.
En muchos casos, conviene empezar por los que más impacto suelen tener:
Después, si hace falta, se refinan otros detalles.
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.
Una buena práctica es anotar cada experimento.
Por ejemplo, para cada corrida puedes guardar:
Eso evita repetir pruebas innecesarias y ayuda a comparar con claridad.
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.
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.
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:
Luego compararemos el rendimiento de validación y elegiremos la mejor configuración.
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.
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.
Si estás empezando, estas prácticas suelen ayudarte bastante:
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.