Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

1. La promesa del AutoML

Hacia 2018–2022, una ola de herramientas de AutoML prometió automatizar el tedioso tramo medio del aprendizaje automático: elegir la familia de modelos, ajustar los hiperparámetros, reportar al ganador. auto-sklearn, TPOT, H2O AutoML y pycaret ofrecían una experiencia compare_models() de una sola línea, y ediciones anteriores de este capítulo enseñaban pycaret sobre el mismo conjunto de datos de temperatura usado en la lección 3.7.

Esa ola se retiró. La mayoría de las herramientas académicas de AutoML ya no se mantiene, y pycaret está retirado de este curso. Dos cosas sobrevivieron y vale la pena enseñarlas:

  1. Las bibliotecas de optimización de hiperparámetros. La búsqueda automatizada sobre la configuración del modelo sigue siendo práctica estándar; Optuna es la biblioteca por defecto actual para ello.
  2. Buenos modelos tabulares por defecto. Los árboles con gradient boosting (potenciación de gradiente) — el HistGradientBoostingRegressor de scikit-learn, LightGBM, XGBoost — ganan sobre las tablas de características con tal consistencia que la búsqueda de modelos rara vez es ya el cuello de botella. Elija un árbol potenciado, ajústelo un poco y gaste el tiempo ahorrado en la calidad de los datos y en la evaluación.

Esta lección cubre a los dos sobrevivientes y luego mira lo que reemplazó al AutoML en 2026: los agentes que escriben código, y las destrezas de verificación que le exigen a usted.

🖥️ Diapositivas — Sesión 17 (vie 6 nov)

2. La búsqueda de hiperparámetros que sobrevivió: búsqueda en malla contra Optuna

Reutilizamos exactamente el conjunto de datos de la lección 3.7 — el mismo generador, las mismas características, la misma división —, de modo que los números son comparables entre las dos lecciones. El modelo es HistGradientBoostingRegressor, y la cantidad que optimizamos es el MAE con validación cruzada de 5 pliegues sobre el conjunto de entrenamiento.

import numpy as np
import pandas as pd


def make_daily_temps(start="2012-01-01", end="2019-12-31", seed=42):
    """Synthetic Seattle-like daily maximum temperature record (degrees F).

    Seasonal climatology + a weak warming trend + AR(1) weather noise.
    Generated in-notebook so the lesson does not depend on a remote file.
    """
    rng = np.random.default_rng(seed)
    dates = pd.date_range(start, end, freq="D")
    day_of_year = dates.dayofyear.to_numpy()
    climatology = 62.0 - 15.0 * np.cos(2 * np.pi * (day_of_year - 203) / 365.25)
    trend = 0.05 * np.arange(len(dates)) / 365.25
    noise = np.zeros(len(dates))
    for i in range(1, len(dates)):
        noise[i] = 0.65 * noise[i - 1] + rng.normal(0.0, 3.0)
    df = pd.DataFrame(
        {
            "date": dates,
            "average": np.round(climatology, 1),  # historical average for that calendar day
            "actual": np.round(climatology + trend + noise, 1),
        }
    )
    df["temp_1"] = df["actual"].shift(1)  # yesterday's max
    df["temp_2"] = df["actual"].shift(2)  # two days ago
    df["month"] = df["date"].dt.month
    df["day"] = df["date"].dt.day
    df["doy_sin"] = np.sin(2 * np.pi * day_of_year / 365.25)
    df["doy_cos"] = np.cos(2 * np.pi * day_of_year / 365.25)
    return df.dropna().reset_index(drop=True)


df = make_daily_temps()

features = ["temp_1", "temp_2", "average", "month", "day", "doy_sin", "doy_cos"]
X = df[features]
y = df["actual"]

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
print("Training features:", X_train.shape, " Testing features:", X_test.shape)
Training features: (2190, 7)  Testing features: (730, 7)

Búsqueda en malla

GridSearchCV prueba cada combinación de una retícula fija de valores. Con 3 profundidades, 3 tasas de aprendizaje y 2 límites de nodos hoja, son 18 candidatos, cada uno validado de forma cruzada 5 veces: 90 ajustes.

import time
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GridSearchCV, KFold, cross_val_score

cv = KFold(n_splits=5, shuffle=True, random_state=42)

param_grid = {
    "max_depth": [2, 4, 8],
    "learning_rate": [0.03, 0.1, 0.3],
    "max_leaf_nodes": [15, 31],
}

grid = GridSearchCV(
    HistGradientBoostingRegressor(random_state=42),
    param_grid,
    cv=cv,
    scoring="neg_mean_absolute_error",
)

t0 = time.perf_counter()
grid.fit(X_train, y_train)
grid_time = time.perf_counter() - t0

n_candidates = len(grid.cv_results_["params"])
grid_fits = n_candidates * cv.get_n_splits()
grid_cv_mae = -grid.best_score_
grid_test_mae = mean_absolute_error(y_test, grid.best_estimator_.predict(X_test))

print("Best params:", grid.best_params_)
print(f"Best CV MAE: {grid_cv_mae:.3f} F")
print(f"Fits: {grid_fits} ({n_candidates} candidates x {cv.get_n_splits()} folds), wall time {grid_time:.1f} s")
Best params: {'learning_rate': 0.1, 'max_depth': 2, 'max_leaf_nodes': 15}
Best CV MAE: 2.458 F
Fits: 90 (18 candidates x 5 folds), wall time 78.0 s

Optuna

Optuna muestrea el espacio de hiperparámetros en lugar de recorrer una retícula. Su muestreador TPE construye un modelo de qué regiones puntúan bien y concentra allí los ensayos posteriores. La función objetivo es cualquier cosa que usted pueda calcular: aquí, el mismo MAE de validación cruzada de 5 pliegues.

import optuna

optuna.logging.set_verbosity(optuna.logging.WARNING)


def objective(trial):
    params = {
        "max_depth": trial.suggest_int("max_depth", 2, 10),
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.4, log=True),
        "max_leaf_nodes": trial.suggest_int("max_leaf_nodes", 10, 60),
    }
    model = HistGradientBoostingRegressor(random_state=42, **params)
    scores = -cross_val_score(model, X_train, y_train, cv=cv, scoring="neg_mean_absolute_error")
    return scores.mean()


study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler(seed=42))

t0 = time.perf_counter()
study.optimize(objective, n_trials=30)
optuna_time = time.perf_counter() - t0

optuna_fits = 30 * cv.get_n_splits()
optuna_cv_mae = study.best_value

print("Best params:", study.best_params)
print(f"Best CV MAE: {optuna_cv_mae:.3f} F")
print(f"Fits: {optuna_fits} (30 trials x {cv.get_n_splits()} folds), wall time {optuna_time:.1f} s")
Best params: {'max_depth': 3, 'learning_rate': 0.05958491008634781, 'max_leaf_nodes': 35}
Best CV MAE: 2.438 F
Fits: 150 (30 trials x 5 folds), wall time 145.8 s
# Refit each winner on the full training set, score once on the test set
optuna_model = HistGradientBoostingRegressor(random_state=42, **study.best_params)
optuna_model.fit(X_train, y_train)
optuna_test_mae = mean_absolute_error(y_test, optuna_model.predict(X_test))

comparison = pd.DataFrame(
    {
        "best CV MAE (F)": [grid_cv_mae, optuna_cv_mae],
        "test MAE (F)": [grid_test_mae, optuna_test_mae],
        "wall time (s)": [grid_time, optuna_time],
        "fits": [grid_fits, optuna_fits],
    },
    index=["GridSearchCV", "Optuna (TPE)"],
)
comparison.round(3)
Loading...

La búsqueda en malla gasta su presupuesto en una retícula fija: cada punto se prueba, se haya visto ya mal o no su vecindario. Optuna muestrea el espacio continuo y se adapta a los ensayos pasados, así que puede aterrizar entre los puntos de la retícula y saltarse las regiones muertas. En un problema tan pequeño la diferencia son minutos; en una búsqueda de aprendizaje profundo con ajustes de una hora, son días. En cualquier caso, ambos métodos reportan un score validado de forma cruzada sobre datos de entrenamiento y tocan el conjunto de prueba exactamente una vez, al final.

3. 2026: los agentes escriben el pipeline, usted lo verifica

Hoy la exploración de modelos suele ocurrir de forma conversacional: usted le describe el conjunto de datos a un agente LLM, y este escribe el código de la cadena de modelado — el pipeline: la secuencia ordenada de preprocesamiento, ajuste y evaluación que va de los datos crudos al score —, lo corre y reporta un score. El problema de búsqueda que el AutoML intentó resolver se convirtió en un problema de verificación. El código escrito por máquinas falla de las mismas maneras que el código humano apurado — preprocesamiento con fuga de datos, clases descartadas, scores calculados sobre la división equivocada —, solo que con más fluidez, envuelto en comentarios prolijos y salidas confiadas.

Abajo hay un script de modelado del tipo que un asistente de IA producirá con gusto. Corre, imprime un score fuerte y está mal de tres maneras distintas. Encuéntrelas antes de abrir la solución.

# --- AI-generated modeling script: do NOT trust it yet ---
import mlgeo_synth
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Whole-rock geochemistry: oxide wt%, density, magnetic susceptibility -> rock type
df = mlgeo_synth.geochem_table(n=4000, seed=7)
df = df[df["label"].isin(["granite", "basalt"])]  # remove sparse label noise

feature_cols = ["SIO2", "AL2O3", "FEO", "MGO", "CAO", "NA2O", "K2O", "density_g_cm3", "mag_susc_si"]
X = df[feature_cols].to_numpy()
y = df["label"].to_numpy()

scaler = StandardScaler()  # normalize features
X = scaler.fit_transform(X)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=7)

models = {
    "logistic_regression": LogisticRegression(max_iter=2000),
    "random_forest": RandomForestClassifier(n_estimators=200, random_state=7),
    "hist_gradient_boosting": HistGradientBoostingClassifier(random_state=7),
}

best_name, best_score = None, -1.0
for name, clf in models.items():
    clf.fit(X_train, y_train)
    score = clf.score(X_train, y_train)  # evaluate each candidate
    print(f"{name}: accuracy = {score:.3f}")
    if score > best_score:
        best_name, best_score = name, score

print(f"\nbest model: {best_name}, accuracy = {best_score:.3f}")
logistic_regression: accuracy = 1.000
random_forest: accuracy = 1.000
hist_gradient_boosting: accuracy = 1.000

best model: logistic_regression, accuracy = 1.000

El pipeline corregido

Las correcciones: conservar las tres clases; dividir primero, con estratificación para que la minoría de andesita aparezca en ambos subconjuntos; poner el escalador dentro de un Pipeline para que se ajuste solo sobre los pliegues de entrenamiento; reportar un modelo de referencia de clase mayoritaria antes de cualquier modelo; seleccionar entre los candidatos por el F1 macro validado de forma cruzada sobre el conjunto de entrenamiento (el F1 macro pondera por igual a la clase minoritaria); y tocar el conjunto de prueba una vez, al final, con scores por clase.

from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, classification_report, f1_score
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

df = mlgeo_synth.geochem_table(n=4000, seed=7)  # all three classes kept
print(df["label"].value_counts(), "\n")

X = df[feature_cols]
y = df["label"]

# Split FIRST, stratified so class proportions match in train and test
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=7, stratify=y
)

# Baseline before any model
dummy = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
print(f"Majority-class baseline: accuracy = {dummy.score(X_test, y_test):.3f}, "
      f"macro-F1 = {f1_score(y_test, dummy.predict(X_test), average='macro'):.3f}\n")

candidates = {
    "logistic_regression": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
    "random_forest": make_pipeline(StandardScaler(), RandomForestClassifier(n_estimators=200, random_state=7)),
    "hist_gradient_boosting": make_pipeline(StandardScaler(), HistGradientBoostingClassifier(random_state=7)),
}

best_name, best_cv = None, -1.0
for name, pipe in candidates.items():
    scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring="f1_macro")
    print(f"{name}: CV macro-F1 = {scores.mean():.3f} +/- {scores.std():.3f}")
    if scores.mean() > best_cv:
        best_name, best_cv = name, scores.mean()

# One look at the test set, for the selected model only
final = candidates[best_name].fit(X_train, y_train)
y_pred = final.predict(X_test)
print(f"\nSelected model: {best_name}")
print(f"Test accuracy = {accuracy_score(y_test, y_pred):.3f}, "
      f"test macro-F1 = {f1_score(y_test, y_pred, average='macro'):.3f}\n")
print(classification_report(y_test, y_pred))
label
granite     2205
basalt      1397
andesite     398
Name: count, dtype: int64 

Majority-class baseline: accuracy = 0.551, macro-F1 = 0.237

logistic_regression: CV macro-F1 = 0.999 +/- 0.002
random_forest: CV macro-F1 = 0.999 +/- 0.003
hist_gradient_boosting: CV macro-F1 = 0.999 +/- 0.001

Selected model: hist_gradient_boosting
Test accuracy = 0.999, test macro-F1 = 0.998

              precision    recall  f1-score   support

    andesite       1.00      0.99      0.99       100
      basalt       1.00      1.00      1.00       349
     granite       1.00      1.00      1.00       551

    accuracy                           1.00      1000
   macro avg       1.00      1.00      1.00      1000
weighted avg       1.00      1.00      1.00      1000

El pipeline corregido también reporta un score alto — estos tipos de roca están genuinamente bien separados en el espacio de los óxidos —, pero el número hace ahora una afirmación distinta. Cubre las tres clases, incluida la andesita que el script de la IA nunca podría predecir; se mide sobre datos apartados en lugar de datos memorizados; y se sostiene contra un modelo de referencia mayoritario de 0.55. Los mismos dígitos, otro significado. En un conjunto de datos más difícil los dos flujos de trabajo divergen: el score de entrenamiento del script de la IA se queda cerca de 1.0 pase lo que pase, mientras que el número honesto baja para decírselo.

4. Qué conservar

La automatización se mudó. En 2020 vivía en los algoritmos de búsqueda — el AutoML iterando sobre modelos e hiperparámetros. En 2026 vive en los agentes que escriben código y producen el pipeline completo a pedido. La verificación no se mudó. Un modelo de referencia trivial, una división sin fuga de datos, la validación cruzada dentro del conjunto de entrenamiento y una sola mirada a un conjunto de prueba apartado atrapan los errores escritos por máquinas exactamente igual que atrapan los humanos. Automatice la búsqueda; nunca la verificación.