Todo este módulo — y buena parte del curso — ha ido dejando una cuenta pendiente: alpha en la regularización, C en la logística y las SVM, K en K-NN, max_depth en los árboles, learning rate y n_estimators en el boosting, capas y dropout en las redes… Hasta ahora los hemos elegido a ojo o con bucles manuales como los de 06-05. Esta lección cierra el módulo automatizando esa búsqueda: aprenderás a distinguir parámetros de hiperparámetros, a usar GridSearchCV y RandomizedSearchCV sobre el pipeline completo del churn de MercaFresh, a entender la búsqueda bayesiana, a evitar la trampa de evaluar con la misma validación que eligió al ganador, y a gestionar tu presupuesto de cómputo con cabeza.

Contenido

  1. Parámetros vs. hiperparámetros
  2. La búsqueda manual y sus límites
  3. GridSearchCV en detalle
  4. Grid search sobre el pipeline completo del churn
  5. RandomizedSearchCV: cuándo es mejor muestrear
  6. Búsqueda bayesiana: buscar con memoria
  7. Validación anidada: no evaluar con el juez que eligió
  8. Buenas prácticas de búsqueda
  9. Cierre del módulo: el arsenal completo

Parámetros vs. hiperparámetros

La distinción es la base de todo:

  • Parámetros: los aprende el algoritmo de los datos durante fit. No los eliges tú.
  • Hiperparámetros: los fijas tú antes de entrenar; configuran cómo aprende el algoritmo.
Modelo (lección) Parámetros (los aprende fit) Hiperparámetros (los eliges tú)
Regresión lineal (04-01) Coeficientes e intercepto — (por eso empezamos por ella)
Ridge/Lasso (07-01) Coeficientes alpha, l1_ratio
Regresión logística (04-02) Coeficientes C, penalty
SVM (04-04) Vectores de soporte y sus pesos C, kernel, gamma
K-NN (04-05) — (memoriza el train) n_neighbors (la K), weights
Árbol (04-03) Las divisiones del árbol max_depth, min_samples_leaf
Random Forest (07-02) Todos los árboles n_estimators, max_features
Gradient boosting (07-03) La secuencia de árboles learning_rate, n_estimators, max_depth
Red neuronal (04-07, 07-04) Pesos y sesgos Nº de capas y neuronas, dropout, learning rate, batch size

La regla mnemotécnica: si aparece en el constructor (Ridge(alpha=...)), es hiperparámetro; si aparece como atributo con guion bajo final tras entrenar (ridge.coef_), es parámetro aprendido. Y el principio metodológico que gobierna la lección: los parámetros se ajustan con el train; los hiperparámetros se eligen con validación — nunca con el test, que sigue guardado bajo llave para el veredicto final (06-01).

La búsqueda manual y sus límites

Ya hemos hecho búsqueda de hiperparámetros tres veces sin ponerle nombre: las curvas de validación sobre max_depth en 06-05, el bucle de alphas en 07-01 y las combinaciones de η/n_estimators en 07-03. El método manual — bucle, CV, apuntar resultados — funciona, pero escala fatal:

  • Explosión combinatoria: con 4 hiperparámetros y 5 valores cada uno son 625 combinaciones; a mano, imposible.
  • Interacciones: el mejor learning_rate depende de n_estimators (lo vimos en 07-03); explorar cada mando por separado se pierde las combinaciones cruzadas.
  • Errores de disciplina: en bucles artesanales es fácil ajustar sin querer con datos de test, o escalar fuera de la CV (la fuga de 06-01).
  • Irreproducibilidad: "probé cosas y me quedé con esta" no es un procedimiento auditable.

sklearn empaqueta la solución en dos herramientas que hacen exactamente lo que hacíamos a mano, pero de forma exhaustiva, paralela y sin fugas.

GridSearchCV en detalle

GridSearchCV recibe un estimador, una rejilla (grid) de valores por hiperparámetro, y una estrategia de CV; entrena y evalúa todas las combinaciones con validación cruzada y se queda con la mejor.

Sus piezas:

  • param_grid: diccionario {hiperparámetro: lista de valores}. El producto cartesiano define las combinaciones.
  • cv: la estrategia de validación cruzada — para el churn, la StratifiedKFold de 06-03.
  • scoring: la métrica que decide (06-02): "f1", "roc_auc", "neg_root_mean_squared_error"… Elegir bien esta métrica es elegir qué significa "mejor"; para el churn desbalanceado de MercaFresh, F1 o AUC, no accuracy.
  • refit=True (por defecto): tras la búsqueda, reentrena automáticamente la mejor combinación sobre todo el train — el objeto resultante ya es un modelo listo para predecir.
  • n_jobs=-1: paraleliza en todos los núcleos; las combinaciones son independientes entre sí.

Y sus resultados:

  • best_params_: la combinación ganadora.
  • best_score_: su puntuación media de CV.
  • cv_results_: la tabla completa de la búsqueda (todas las combinaciones, medias, desviaciones, tiempos) — conviértela en DataFrame y estúdiala: vale más que el ganador solo.
  • best_estimator_: el modelo reentrenado (si refit=True).

Grid search sobre el pipeline completo del churn

La regla de oro de 06-03 sigue vigente: lo que se busca es el pipeline completo, no el modelo suelto, para que el preprocesamiento se reajuste dentro de cada pliegue sin fugas. Los hiperparámetros de un paso del pipeline se nombran con la notación paso__parametro (doble guion bajo) que ya conocimos entonces — y que permite incluso tratar decisiones de preprocesamiento como hiperparámetros más.

import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold, GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

# Dataset de churn de MercaFresh (features RFM, como en 07-02/07-03)
rng = np.random.default_rng(42)
n = 1000
recencia = rng.gamma(2, 15, n)
frecuencia = rng.poisson(5, n) + 1
monetario = rng.gamma(3, 40, n)
antiguedad = rng.uniform(1, 60, n)
incidencias = rng.poisson(0.5, n)
logits = 0.05 * recencia - 0.4 * frecuencia + 0.6 * incidencias - 0.01 * antiguedad - 0.5
y = (rng.random(n) < 1 / (1 + np.exp(-logits))).astype(int)
X = pd.DataFrame({"recencia": recencia, "frecuencia": frecuencia,
                  "monetario": monetario, "antiguedad": antiguedad,
                  "incidencias": incidencias})

# Test apartado ANTES de cualquier búsqueda (06-01)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)

pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression(max_iter=5000)),
])

param_grid = {
    "clf__C": np.logspace(-3, 2, 6),          # 0.001 ... 100, escala log (07-01)
    "clf__penalty": ["l1", "l2"],             # Lasso o Ridge sobre la logística
    "clf__solver": ["liblinear"],             # solver compatible con ambas
}

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
busqueda = GridSearchCV(pipe, param_grid, cv=cv, scoring="f1",
                        n_jobs=-1, refit=True)
busqueda.fit(X_train, y_train)                # 6 x 2 = 12 combinaciones x 5 pliegues = 60 fits

print("Mejor combinación:", busqueda.best_params_)
print(f"Mejor F1 (CV): {busqueda.best_score_:.3f}")

# La tabla completa, para entender el paisaje y no solo el pico
resultados = pd.DataFrame(busqueda.cv_results_)
print(resultados[["param_clf__C", "param_clf__penalty",
                  "mean_test_score", "std_test_score"]]
      .sort_values("mean_test_score", ascending=False).head())

# Veredicto final: el test intacto, una sola vez
from sklearn.metrics import f1_score
print(f"F1 en test: {f1_score(y_test, busqueda.predict(X_test)):.3f}")

Puntos que merecen relectura:

  • El coste real es combinaciones × pliegues entrenamientos (aquí 60): el grid crece multiplicativamente y la CV lo multiplica otra vez. Presupuesta antes de lanzar.
  • busqueda se usa después como un modelo normal (predict, predict_proba): gracias a refit, dentro lleva el mejor pipeline reentrenado con todo el train.
  • Mira siempre std_test_score: un "ganador" por 0.002 de media con desviaciones de 0.03 es un empate técnico; en ese caso elige la combinación más simple/regularizada, no la primera de la tabla.
  • El F1 de test suele quedar algo por debajo de best_score_ — y esa diferencia tiene nombre y sección propia más abajo.

RandomizedSearchCV: cuándo es mejor muestrear

El grid exhaustivo muere por combinatoria: con el gradient boosting de 07-03 querríamos explorar learning_rate, max_iter, max_depth, min_samples_leaf, subsample… 5 valores de cada = 3.125 combinaciones × 5 pliegues = 15.625 entrenamientos.

RandomizedSearchCV cambia la estrategia: en lugar de probarlo todo, muestrea n_iter combinaciones al azar de distribuciones que tú defines. Ventajas decisivas:

  • Presupuesto fijo: tú decides cuántos entrenamientos pagas (n_iter=50), sea cual sea el tamaño del espacio.
  • Distribuciones continuas en lugar de listas: loguniform(0.001, 0.3) para el learning rate explora todo el rango, no 5 puntos sueltos.
  • Mejor cobertura de lo que importa: en la práctica solo unos pocos hiperparámetros son influyentes; el muestreo aleatorio prueba muchos valores distintos de cada hiperparámetro, mientras el grid desperdicia presupuesto repitiendo los mismos valores del hiperparámetro influyente para cada valor del irrelevante.
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import HistGradientBoostingClassifier
from scipy.stats import loguniform, randint

pipe_gb = Pipeline([("clf", HistGradientBoostingClassifier(random_state=42))])

param_dist = {
    "clf__learning_rate": loguniform(0.005, 0.3),   # continuo, escala log
    "clf__max_iter": randint(100, 1000),
    "clf__max_depth": randint(2, 8),
    "clf__min_samples_leaf": randint(10, 60),
}

azar = RandomizedSearchCV(pipe_gb, param_dist, n_iter=50, cv=cv,
                          scoring="f1", n_jobs=-1, random_state=42)
azar.fit(X_train, y_train)                          # 50 x 5 = 250 fits, decidido por ti
print(azar.best_params_, f"F1-CV: {azar.best_score_:.3f}")

Regla práctica: grid para espacios pequeños y discretos (2-3 hiperparámetros con pocos valores razonables, como el ejemplo de la logística); randomized para todo lo demás, y especialmente para boosting y redes.

Búsqueda bayesiana: buscar con memoria

Grid y randomized comparten una ingenuidad: cada combinación se elige sin mirar los resultados anteriores. La búsqueda bayesiana (u optimización bayesiana) es la evolución natural: construye un modelo probabilístico de la función "hiperparámetros → puntuación" — sí, un modelo de ML para optimizar modelos de ML — y lo usa para decidir qué combinación probar a continuación, equilibrando explotar las zonas que pintan bien y explorar las zonas inciertas. Es la misma lógica de actualización de creencias con evidencia del teorema de Bayes (02-05), aplicada a la búsqueda.

Con presupuestos ajustados (decenas de evaluaciones de un modelo caro), suele encontrar mejores combinaciones que el azar puro. La biblioteca de referencia es Optuna (también scikit-optimize); su API se integra bien con sklearn y añade extras como descartar a mitad de entrenamiento las combinaciones que van mal (pruning). No la desarrollamos aquí: conceptualmente ya tienes lo esencial, y su sintaxis se aprende en una tarde cuando la necesites.

Estrategia Cómo elige Cuándo usarla
Manual (06-05) Tu intuición Exploración inicial, aprendizaje
GridSearchCV Todas las combinaciones Espacios pequeños y discretos
RandomizedSearchCV Muestreo aleatorio con presupuesto Espacios grandes; opción por defecto
Bayesiana (Optuna) Modelo que aprende de los intentos previos Modelos caros de entrenar, presupuesto escaso

Validación anidada: no evaluar con el juez que eligió

Una sutileza importante que ya asomó en el ejemplo del grid: best_score_ es una estimación optimista del rendimiento real. ¿Por qué? Porque probaste muchas combinaciones sobre la misma CV y te quedaste con la que mejor puntuó en esa CV concreta: parte de su ventaja es mérito y parte es suerte con esos pliegues. Es el mismo sesgo de selección que en 06-01 nos llevó a separar validación y test — reapareciendo un nivel más arriba.

La solución limpia es la validación anidada (nested CV): un bucle externo de CV para estimar rendimiento, y dentro de cada pliegue externo, un bucle interno completo de búsqueda que elige los hiperparámetros. Así, los datos que puntúan nunca participaron en la elección:

from sklearn.model_selection import cross_val_score

# La búsqueda entera (bucle interno) se trata como un estimador más
# y se evalúa con una CV externa que ella nunca ve
f1_anidado = cross_val_score(busqueda, X_train, y_train, cv=5, scoring="f1")
print(f"F1 honesto (anidado): {f1_anidado.mean():.3f} ± {f1_anidado.std():.3f}")

El coste se multiplica (5 × 60 = 300 fits en nuestro ejemplo), así que en la práctica se reserva para cuando la cifra de rendimiento debe ser rigurosa — comparar algoritmos para un informe, o estimar qué rendirá el modelo antes de comprometerse con negocio. Para el día a día basta el esquema de esta lección: buscar con CV sobre el train y dar el veredicto final una única vez sobre el test intacto.

Buenas prácticas de búsqueda

  1. De grueso a fino. Primera pasada con rangos amplios y pocos puntos (o n_iter moderado); mira cv_results_, localiza la zona buena y lanza una segunda pasada refinada alrededor. Dos búsquedas baratas rinden más que una carísima.
  2. Escala logarítmica para los multiplicativos. alpha, C, learning_rate, gamma actúan por órdenes de magnitud: usa np.logspace / loguniform. Para los estructurales (max_depth, n_neighbors), escala lineal.
  3. Presupuesta antes de lanzar. Calcula combinaciones × pliegues × tiempo_por_fit y decide si te lo puedes permitir; si no, reduce el grid, baja pliegues (5 → 3) o pasa a randomized. Mide primero un fit suelto con %timeit o time.perf_counter.
  4. Busca menos donde importa menos. No todos los hiperparámetros merecen rejilla: en Random Forest, n_estimators se fija alto y ya (07-02); en boosting, el early stopping elige n_estimators por ti (07-03) — quítalos del espacio de búsqueda.
  5. Fija random_state en todo (CV, modelos, búsqueda) para que la búsqueda sea reproducible y las comparaciones justas.
  6. Guarda cv_results_. El paisaje completo dice cosas que el ganador calla: qué hiperparámetros influyen de verdad, dónde el modelo es robusto y dónde frágil.

Cierre del módulo: el arsenal completo

Este módulo respondió a la pregunta con la que lo abrimos — "¿puedo hacerlo aún mejor?" — con cinco técnicas complementarias:

Técnica (lección) Qué mejora Cuándo aplicarla
Regularización L1/L2/EN (07-01) Doma la varianza de los modelos lineales; L1 además selecciona features Modelos lineales con muchas features o correlacionadas; siempre con escalado
Ensembles: bagging/RF, voting, stacking (07-02) Cancela errores descorrelacionados; RF como baseline fuerte universal Casi siempre en tabular: empieza aquí
Gradient boosting (07-03) Techo de rendimiento en datos tabulares Cuando cada punto de métrica vale dinero y puedes pagar el ajuste
Deep learning (07-04) Aprende representaciones jerárquicas Imágenes, audio, texto; tabular solo con datos masivos
Optimización de hiperparámetros (07-05) Exprime cualquiera de las anteriores de forma sistemática y sin fugas Siempre, con presupuesto proporcional a lo que hay en juego

Y el flujo que las une para un problema tabular como el churn de MercaFresh: baseline Dummy (06-02) → modelo simple interpretable → Random Forest → gradient boosting con early stopping → búsqueda de hiperparámetros del finalista → veredicto único sobre el test.

Errores Comunes y Consejos

  • Buscar hiperparámetros usando el test. El error capital. Si el test influye en cualquier decisión — hiperparámetros, features, umbral — deja de medir generalización. Test intacto hasta el final, una sola evaluación.
  • Pasar el modelo suelto en vez del pipeline. Si escalas antes y por fuera de la búsqueda, cada pliegue de la CV ve estadísticas del resto: la fuga de 06-01 en versión sutil. Busca siempre sobre el Pipeline con la notación paso__parametro.
  • Rejillas lineales para hiperparámetros multiplicativos. C en [1, 2, 3, 4, 5] explora una esquina minúscula del rango útil; np.logspace(-3, 2, 6) explora cinco órdenes de magnitud con el mismo coste.
  • Tomar best_score_ como el rendimiento esperado. Es optimista por sesgo de selección; el número honesto sale del test final o de la validación anidada.
  • Coronar ganadores por diferencias dentro del ruido. Compara mean_test_score junto a std_test_score; en empate técnico, gana el modelo más simple.
  • Consejo: trata la búsqueda como un experimento científico: hipótesis (rangos razonados, no arbitrarios), procedimiento reproducible (semillas fijadas), resultados archivados (cv_results_ a CSV). Tu yo de dentro de tres meses — o el auditor del modelo — te lo agradecerá.

Ejercicios

  1. Grid sobre el K-NN del churn. Construye un pipeline StandardScaler + KNeighborsClassifier y busca con GridSearchCV (scoring="f1", la CV estratificada de la lección) sobre clf__n_neighbors en [3, 5, 9, 15, 25, 41] y clf__weights en ["uniform", "distance"]. Reporta best_params_, best_score_ y el F1 en test. ¿Cuántos entrenamientos ha ejecutado la búsqueda?
  2. Grid vs. randomized con el mismo presupuesto. Sobre el HistGradientBoostingClassifier del ejemplo, compara: (a) un GridSearchCV con 3 valores de learning_rate × 3 de max_depth (9 combinaciones) y (b) un RandomizedSearchCV con n_iter=9 sobre las distribuciones continuas del ejemplo. Mismo cv, mismo scoring. Repite (b) con tres random_state distintos. ¿Quién gana y qué estabilidad observas?
  3. El tamaño del optimismo. Calcula para la búsqueda de la logística del ejemplo: (a) best_score_, (b) el F1 de validación anidada con cross_val_score(busqueda, ...), y (c) el F1 en test. Ordena las tres cifras y explica cada diferencia con los conceptos de la lección.

Soluciones

  1. Estructura: Pipeline([("scaler", StandardScaler()), ("clf", KNeighborsClassifier())]) y el grid con la notación clf__. Con estos datos, el ganador suele rondar n_neighbors entre 15 y 41 con weights="distance" (el churn tiene ruido y las vecindades amplias promedian mejor), con F1-CV cercano al de la logística. Entrenamientos: 6 × 2 = 12 combinaciones × 5 pliegues = 60 fits (más 1 final del refit). El F1 de test debería quedar en el entorno del best_score_, ligeramente por debajo.
  2. Con solo 2 hiperparámetros bien acotados, el grid es competitivo y a veces gana; el randomized queda muy cerca y, según la semilla, lo supera — sus 9 puntos cubren valores de learning rate que el grid ni prueba. Entre semillas, el best_score_ del randomized oscila (típicamente en la 2ª-3ª décima decimal): con n_iter tan bajo, el azar de qué combinaciones salen pesa. Moraleja doble: en espacios pequeños el grid es defendible; en cuanto el espacio crece, la cobertura continua del randomized gana — y con más n_iter, su varianza entre semillas se encoge.
  3. El orden esperado es best_score_ ≥ F1 anidado ≈ F1 test. (a) > (b): best_score_ lleva el sesgo de selección — la combinación ganadora lo es en parte por suerte con esos pliegues concretos, y la CV externa del anidado, que no participó en la elección, lo descuenta. (b) ≈ (c): ambos son estimaciones honestas sobre datos no usados en ninguna decisión; difieren solo por ruido de muestreo (el test es una única partición; el anidado promedia cinco). Si en tu ejecución (c) sale por encima de (a), también es lección: con datasets de este tamaño, el ruido entre particiones puede superar a los sesgos que medimos.

Conclusión

Has cerrado el círculo que abrió el módulo 6: los parámetros los aprende fit, los hiperparámetros los elige una búsqueda disciplinada — GridSearchCV para espacios pequeños, RandomizedSearchCV con distribuciones y presupuesto para los grandes, búsqueda bayesiana cuando cada entrenamiento cuesta caro —, siempre sobre el pipeline completo, siempre con CV sobre el train, con la validación anidada como árbitro cuando la cifra debe ser irreprochable y el test intacto para el veredicto final. Con esto, el módulo 7 queda completo: regularización para contener, ensembles y boosting para combinar, redes profundas para los datos no estructurados y optimización sistemática para exprimirlo todo. El equipo de MercaFresh tiene por fin su mejor modelo de churn, afinado y evaluado con honestidad; pero un modelo que vive en un notebook no retiene a ningún cliente. El mejor modelo del mundo no vale nada si no llega a producción, y a eso dedicamos el módulo 8: frameworks, despliegue, monitoreo y las consideraciones éticas de poner machine learning delante de personas reales.

Curso de Machine Learning

Módulo 1: Introducción al Machine Learning

Módulo 2: Fundamentos de Estadística y Probabilidad

Módulo 3: Preprocesamiento de Datos

Módulo 4: Algoritmos de Machine Learning Supervisado

Módulo 5: Algoritmos de Machine Learning No Supervisado

Módulo 6: Evaluación y Validación de Modelos

Módulo 7: Técnicas Avanzadas y Optimización

Módulo 8: Implementación y Despliegue de Modelos

Módulo 9: Proyectos Prácticos

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados