A lo largo del módulo ha ido apareciendo un mismo síntoma sin nombre: k vecinos con k = 1 acertaba el 100 % en entrenamiento y el 81 % en test (04-04); el árbol sin límite de profundidad, 100 % y 82,5 %; el bosque aleatorio, 0,908 en entrenamiento frente a 0,859 en test (04-05). En todos los casos el modelo aprendía "demasiado bien" los datos que veía y peor los que no. Ese fenómeno se llama sobreajuste, y su opuesto, el modelo demasiado simple que no aprende ni lo que hay, subajuste. Esta última lección del módulo explica ambos con la intuición del compromiso sesgo-varianza, enseña a diagnosticarlos con curvas de aprendizaje y de validación, presenta las técnicas para combatirlos (más datos, menos características, regularización L1/L2, poda de árboles, ensamblados, parada temprana) y, por último, aborda con método la pregunta que hemos ido aplazando desde 04-01: cómo elegir los hiperparámetros (GridSearchCV, RandomizedSearchCV, validación anidada) sin contaminar el test. Es importante porque el sobreajuste es la forma más común de fracasar en producción: un modelo excelente en el portátil de Marta y mediocre con los pedidos de mañana. Cerraremos con el flujo completo que Marta ya domina y con el puente hacia el módulo 5.
Contenido
- Subajuste y sobreajuste: el árbol que memoriza los pedidos
- El compromiso sesgo-varianza
- Diagnóstico: curvas de aprendizaje y curvas de validación
- Técnicas contra el sobreajuste
- Regularización L1 y L2 en código
- Hiperparámetros frente a parámetros: cómo elegirlos
- Búsqueda en rejilla y aleatoria con
GridSearchCVyRandomizedSearchCV - Validación anidada y por qué el test se toca una sola vez
- El flujo completo del módulo, de un vistazo
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Subajuste y sobreajuste: el árbol que memoriza los pedidos
Entrenamos árboles de decisión de profundidad creciente sobre el pipeline de 04-03 y medimos exactitud y AUC en entrenamiento y en test:
from novamarket_ml import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos, crear_preparacion
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score
X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
print("prof. acc_train acc_test auc_train auc_test hojas")
for prof in [1, 2, 3, 4, 6, 8, 12, None]:
p = Pipeline([("prep", crear_preparacion()),
("modelo", DecisionTreeClassifier(max_depth=prof, random_state=42))]).fit(Xtr, ytr)
arbol = p.named_steps["modelo"]
print(f"{str(prof):5s} {p.score(Xtr, ytr):9.3f} {p.score(Xte, yte):8.3f} "
f"{roc_auc_score(ytr, p.predict_proba(Xtr)[:, 1]):9.3f} "
f"{roc_auc_score(yte, p.predict_proba(Xte)[:, 1]):8.3f} {arbol.get_n_leaves():5d}")Salida:
| Profundidad | Exactitud train | Exactitud test | AUC train | AUC test | Hojas |
|---|---|---|---|---|---|
| 1 | 0,846 | 0,827 | 0,611 | 0,570 | 2 |
| 2 | 0,861 | 0,840 | 0,743 | 0,746 | 4 |
| 3 | 0,878 | 0,849 | 0,799 | 0,793 | 8 |
| 4 | 0,886 | 0,855 | 0,830 | 0,792 | 15 |
| 6 | 0,900 | 0,844 | 0,871 | 0,769 | 38 |
| 8 | 0,924 | 0,840 | 0,924 | 0,693 | 89 |
| 12 | 0,965 | 0,811 | 0,984 | 0,634 | 200 |
| Sin límite | 1,000 | 0,788 | 1,000 | 0,648 | 299 |
Tres zonas:
- Profundidad 1-2: subajuste (underfitting). El modelo es demasiado simple para capturar la relación (una sola pregunta no basta, como vimos en 04-01): rinde mal en entrenamiento y en test.
- Profundidad 3-4: el punto dulce. Entrenamiento y test van parejos y el AUC de test es máximo (0,79).
- Profundidad 6 en adelante: sobreajuste (overfitting). El rendimiento en entrenamiento sigue subiendo hasta el 100 % (el árbol sin límite tiene 299 hojas para 2.249 pedidos: muchas hojas contienen un solo pedido y lo "recuerdan"), mientras el de test baja: AUC 0,65, apenas mejor que profundidad 2. El árbol ha aprendido el ruido del sorteo aleatorio del generador (04-01), que en los pedidos nuevos es otro.
La definición práctica: hay sobreajuste cuando la diferencia entre el rendimiento en entrenamiento y en validación crece al aumentar la complejidad del modelo; hay subajuste cuando ambos son bajos. Y una consecuencia que conviene grabar: el rendimiento en entrenamiento no sirve para elegir el modelo; siempre sube con la complejidad.
- El compromiso sesgo-varianza
La explicación clásica descompone el error de un modelo en dos fuentes:
- Sesgo: error por suposiciones demasiado rígidas. Un modelo con mucho sesgo (una recta para una relación curva, un árbol de profundidad 1) se equivoca de forma sistemática, haga lo que haga con los datos. Es el subajuste.
- Varianza: error por sensibilidad excesiva a los datos concretos de entrenamiento. Un modelo con mucha varianza (un árbol sin límite, k = 1) cambia por completo si se entrena con otra muestra, porque sigue al ruido. Es el sobreajuste.
La imagen habitual es la diana:
| Varianza baja | Varianza alta | |
|---|---|---|
| Sesgo bajo | Todos los disparos en el centro: el modelo ideal | Disparos repartidos alrededor del centro: acierta de media pero cada modelo concreto es impredecible (sobreajuste) |
| Sesgo alto | Disparos agrupados pero lejos del centro: se equivoca siempre igual (subajuste) | Disparos dispersos y descentrados: lo peor de ambos |
Al aumentar la complejidad (profundidad, k más pequeño, más características, menos regularización) el sesgo baja y la varianza sube; el error total, suma de ambos más el ruido irreducible, tiene forma de U, y el mínimo es el punto dulce que buscamos. Nunca se elimina el ruido: en nuestros datos la etiqueta se sortea con una probabilidad, así que ni el modelo perfecto acertaría el 100 %; perseguirlo es la vía directa al sobreajuste.
- Diagnóstico: curvas de aprendizaje y curvas de validación
Dos gráficas responden a las dos preguntas del diagnóstico. Ambas se calculan con validación cruzada (04-05), nunca con el test.
3.1 Curva de aprendizaje: ¿ayudarían más datos?
learning_curve entrena el modelo con fracciones crecientes del conjunto de entrenamiento y mide el rendimiento en entrenamiento y en validación para cada tamaño:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve, StratifiedKFold
from sklearn.linear_model import LogisticRegression
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
pipe_log = Pipeline([("prep", crear_preparacion()), ("modelo", LogisticRegression(max_iter=1000))])
tam, auc_tr, auc_va = learning_curve(pipe_log, X, y, cv=cv, scoring="roc_auc",
train_sizes=np.linspace(0.1, 1.0, 6), random_state=42)
print(pd.DataFrame({"n_train": tam, "auc_train": auc_tr.mean(1).round(3), "auc_val": auc_va.mean(1).round(3)}))
plt.plot(tam, auc_tr.mean(1), "o-", label="entrenamiento")
plt.plot(tam, auc_va.mean(1), "o-", label="validación")
plt.xlabel("pedidos de entrenamiento"); plt.ylabel("AUC"); plt.legend(); plt.show()Salida (regresión logística):
n_train auc_train auc_val 0 239 0.921 0.611 1 671 0.842 0.821 2 1103 0.839 0.831 3 1535 0.839 0.835 4 1967 0.831 0.836 5 2399 0.841 0.836
Cómo se lee la gráfica: con 239 pedidos, la curva de entrenamiento está muy alta (0,92) y la de validación muy baja (0,61): con pocos datos, incluso la logística sobreajusta. A partir de unos 1.100 pedidos ambas curvas convergen en torno a 0,83-0,84 y se aplanan: más datos ya no ayudarían a este modelo, y para mejorar habría que cambiar de familia o añadir características. Si repites el cálculo con el árbol sin límite, la curva de entrenamiento se queda en 1,000 para cualquier tamaño y la de validación en 0,65: una brecha grande y persistente, la firma del sobreajuste, en la que más datos sí ayudarían un poco (la brecha se cerraría lentamente) pero lo eficaz es limitar el modelo.
Resumen de lectura: brecha grande entre curvas = varianza (sobreajuste); curvas juntas pero bajas = sesgo (subajuste); curvas juntas y altas = bien.
3.2 Curva de validación: ¿qué valor del hiperparámetro?
validation_curve fija el tamaño de los datos y varía un hiperparámetro:
from sklearn.model_selection import validation_curve
profs = [1, 2, 3, 4, 5, 6, 8, 10, 15, 20]
pipe_arbol = Pipeline([("prep", crear_preparacion()), ("modelo", DecisionTreeClassifier(random_state=42))])
auc_tr, auc_va = validation_curve(pipe_arbol, X, y, param_name="modelo__max_depth",
param_range=profs, cv=cv, scoring="roc_auc")
print(pd.DataFrame({"prof": profs, "auc_train": auc_tr.mean(1).round(3), "auc_val": auc_va.mean(1).round(3)}))
# plt.plot(profs, auc_tr.mean(1), "o-", label="entrenamiento"); plt.plot(profs, auc_va.mean(1), "o-", label="validación")Salida:
prof auc_train auc_val 0 1 0.672 0.657 1 2 0.756 0.745 2 3 0.804 0.789 3 4 0.833 0.793 4 5 0.854 0.789 5 6 0.881 0.775 6 8 0.938 0.722 7 10 0.976 0.680 8 15 0.999 0.667 9 20 1.000 0.656
En la gráfica, la curva de entrenamiento sube monótonamente hacia 1 y la de validación dibuja una U invertida con el máximo en profundidad 4 (0,793); a la izquierda subajuste, a la derecha sobreajuste. Con validación cruzada, la elección (profundidad 4) es fiable, no fruto de una división afortunada. Fíjate en el nombre del parámetro: "modelo__max_depth" con doble guion bajo, la sintaxis para llegar a un hiperparámetro dentro de un Pipeline (paso modelo, parámetro max_depth); la usaremos en las búsquedas.
- Técnicas contra el sobreajuste
| Técnica | Idea | Dónde | Comentario |
|---|---|---|---|
| Más datos | Con más ejemplos, el ruido pesa menos | Cualquier modelo | Solo si la curva de aprendizaje muestra brecha; suele ser lo más caro |
| Menos características | Quitar columnas ruidosas o redundantes (04-03) | Cualquier modelo, sobre todo k-NN y lineales | Selección por importancia, correlación o L1 |
| Regularización L2 (Ridge) | Penalizar la suma de los coeficientes al cuadrado: todos se encogen hacia 0 | Regresión lineal (Ridge, alpha), logística (C = 1/fuerza), SVM (C), redes |
Por defecto en LogisticRegression |
| Regularización L1 (Lasso) | Penalizar la suma de valores absolutos: muchos coeficientes se hacen exactamente 0 | Lasso (alpha), logística con penalty="l1" |
Hace selección de características automática |
| Limitar árboles | max_depth, min_samples_leaf, min_samples_split, poda por coste-complejidad (ccp_alpha) |
Árboles y sus ensamblados | Es lo que hicimos en la sección 1 |
| Ensamblados | Promediar muchos modelos de alta varianza (bagging, bosques) | Árboles sobre todo | Reducen varianza sin subir el sesgo |
| Parada temprana (early stopping) | Detener el entrenamiento iterativo cuando el error de validación deja de bajar | Boosting, redes neuronales | Central en el módulo 5 |
| Validación cruzada | No evita el sobreajuste, pero lo detecta y evita sobreajustar la elección de hiperparámetros a una división | Siempre | Sección 7 |
| Aumento de datos, dropout | Técnicas propias del deep learning | Módulo 5 |
La regularización merece una explicación aparte. Al entrenar una logística se minimiza la pérdida logarítmica (04-01, 04-04). Regularizar es añadir a esa pérdida una penalización por coeficientes grandes: pérdida + λ · Σ coef² (L2) o pérdida + λ · Σ |coef| (L1). Es exactamente la técnica de la penalización que usamos en 03-04 para la asignación de pedidos: convertir una preferencia ("coeficientes moderados") en un término del objetivo. Un coeficiente enorme significa que el modelo se apoya muchísimo en una columna, a menudo para explicar unos pocos ejemplos ruidosos; penalizarlo obliga a explicaciones más "repartidas" y suaves, que generalizan mejor. En scikit-learn el parámetro se llama alpha en Ridge/Lasso (más alto, más regularización) y C en LogisticRegression/SVC (es la inversa: más bajo, más regularización).
- Regularización L1 y L2 en código
Variamos C en la logística del pipeline de 04-03 y observamos los coeficientes:
from sklearn.model_selection import cross_val_score
print("C auc_train auc_val suma|coef| coef≈0")
for C in [0.001, 0.01, 0.1, 1, 10, 100]:
p = Pipeline([("prep", crear_preparacion()), ("modelo", LogisticRegression(C=C, max_iter=1000))])
auc_val = cross_val_score(p, Xtr, ytr, cv=cv, scoring="roc_auc").mean()
p.fit(Xtr, ytr)
coef = p.named_steps["modelo"].coef_[0]
print(f"{C:<7} {roc_auc_score(ytr, p.predict_proba(Xtr)[:, 1]):9.3f} {auc_val:7.3f} "
f"{np.abs(coef).sum():10.2f} {(np.abs(coef) < 0.05).sum():6d}")Salida:
C auc_train auc_val suma|coef| coef≈0 0.001 0.792 0.781 0.71 17 0.01 0.826 0.812 2.63 11 0.1 0.838 0.828 5.03 7 1 0.839 0.829 5.87 6 10 0.839 0.829 9.61 3 100 0.839 0.829 9.65 3
Con C = 0,001 (regularización fortísima) los coeficientes casi desaparecen (suma 0,71; 17 de 21 prácticamente nulos) y el modelo subajusta (AUC 0,78). Entre 0,1 y 100 el rendimiento en validación es el mismo (0,829): en estos datos la logística no sobreajusta apreciablemente, así que la regularización apenas importa, salvo que los coeficientes crecen (5 → 9,6) sin mejorar nada. La L2 aquí es una red de seguridad barata más que una necesidad. Con L1 (LogisticRegression(penalty="l1", solver="liblinear", C=...)) el efecto es distinto: con C = 0,05 solo quedan 7 coeficientes distintos de cero (importe, num_articulos, dias_entrega, importe_por_articulo, cliente_nuevo, categoria_electronica, categoria_hogar) con un AUC de validación de 0,821, casi el del modelo completo: L1 ha seleccionado características automáticamente y ha descartado, entre otras, las tres columnas de codigo_postal_zona, en línea con lo que vimos en 04-03. Para regresión, Ridge(alpha=...) y Lasso(alpha=...) hacen lo mismo con la demanda; el ejercicio 2 lo explora.
- Hiperparámetros frente a parámetros: cómo elegirlos
Recordemos la distinción de 04-01: los parámetros (coeficientes, umbrales de los nodos, centroides) los aprende fit a partir de los datos; los hiperparámetros (max_depth, min_samples_leaf, n_estimators, C, alpha, n_neighbors, n_clusters) los fijamos antes de entrenar y controlan la complejidad o el comportamiento del algoritmo. No hay fórmula para elegirlos: hay que probar valores y medir con validación, que es lo que acabamos de hacer a mano con la curva de validación. Los principios:
- Se elige con validación (validación cruzada sobre el entrenamiento, o un conjunto de validación aparte), nunca con el test.
- Se prueba una rejilla razonable de valores, en escala logarítmica cuando el parámetro es continuo (
Cen 0,001, 0,01, 0,1, 1, 10). - Se prefiere, a igualdad de rendimiento, el modelo más simple (menos profundidad, más regularización).
- Se comprueba que el óptimo no está en el borde de la rejilla; si lo está, se amplía.
- Búsqueda en rejilla y aleatoria con
GridSearchCV y RandomizedSearchCV
GridSearchCV y RandomizedSearchCVGridSearchCV automatiza los principios anteriores: recibe un pipeline, un diccionario de hiperparámetros con los valores a probar, una estrategia de validación cruzada y una métrica; entrena todas las combinaciones, se queda con la mejor y reentrena el pipeline con ella sobre todo el conjunto de entrenamiento. Lo aplicamos al bosque aleatorio, que en 04-05 quedó por detrás de la logística y mostraba signos de sobreajuste:
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
rejilla = {
"modelo__n_estimators": [100, 300],
"modelo__max_depth": [3, 5, 8, None],
"modelo__min_samples_leaf": [1, 5, 20],
} # 2 x 4 x 3 = 24 combinaciones
pipe_rf = Pipeline([("prep", crear_preparacion()), ("modelo", RandomForestClassifier(random_state=42))])
busqueda = GridSearchCV(pipe_rf, rejilla, cv=cv, scoring="roc_auc", n_jobs=-1)
busqueda.fit(Xtr, ytr) # 24 combinaciones x 5 folds = 120 entrenamientos
print("Mejores hiperparámetros:", busqueda.best_params_)
print("AUC de validación cruzada:", round(busqueda.best_score_, 3))
resultados = pd.DataFrame(busqueda.cv_results_)
columnas = ["param_modelo__max_depth", "param_modelo__min_samples_leaf", "param_modelo__n_estimators",
"mean_test_score", "std_test_score", "rank_test_score"]
print(resultados[columnas].sort_values("rank_test_score").head(4).round(3).to_string(index=False))
print(resultados[columnas].sort_values("rank_test_score").tail(2).round(3).to_string(index=False))
# UNA sola vez, al final: el test
print("AUC en test del mejor bosque:", round(roc_auc_score(yte, busqueda.predict_proba(Xte)[:, 1]), 3))Salida:
Mejores hiperparámetros: {'modelo__max_depth': 5, 'modelo__min_samples_leaf': 1, 'modelo__n_estimators': 300}
AUC de validación cruzada: 0.819
param_modelo__max_depth param_modelo__min_samples_leaf param_modelo__n_estimators mean_test_score std_test_score rank_test_score
5 1 300 0.819 0.017 1
5 5 300 0.819 0.017 2
5 1 100 0.819 0.016 3
5 5 100 0.817 0.017 4
None 1 300 0.799 0.027 23
None 1 100 0.797 0.027 24
AUC en test del mejor bosque: 0.838Lectura:
best_params_dice que la mejor combinación es profundidad 5, hoja mínima 1 y 300 árboles;best_score_es su AUC medio en validación cruzada (0,819).cv_results_es una tabla con todas las combinaciones: la media y la desviación de la métrica en los 5 folds y el ranking. Merece siempre una mirada: las cuatro primeras filas están a 0,002 unas de otras (menos que la desviación, 0,017), así que "profundidad 5" es lo que importa y el resto es indiferente; y las peores son los bosques sin límite de profundidad, que sobreajustan (0,797 con una desviación mayor, 0,027).- El bosque con hiperparámetros por defecto (sin límite de profundidad) tenía un AUC de validación de 0,797 y de test de 0,811; el ajustado sube a 0,838 en test. Ajustar los hiperparámetros ha convertido al bosque en un competidor de la logística (0,844), y con la función de coste de 04-05 su coste con umbral 0,2 baja de 2.830 € (por defecto, umbral 0,5) a 1.860 €.
busquedase comporta como un modelo ya entrenado (predict,predict_proba): por dentro ha reentrenado el mejor pipeline con todo el conjunto de entrenamiento. El test se ha usado una sola vez, en la última línea.
Cuando la rejilla es grande (cinco hiperparámetros con seis valores cada uno son 7.776 combinaciones), la búsqueda exhaustiva es inviable: la explosión combinatoria de 03-01. RandomizedSearchCV prueba n_iter combinaciones al azar de distribuciones que le indicamos, y en la práctica encuentra soluciones igual de buenas con una fracción del coste, porque normalmente solo importan uno o dos hiperparámetros:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint
distribuciones = {
"modelo__n_estimators": randint(100, 500), # entero aleatorio entre 100 y 499
"modelo__max_depth": [3, 4, 5, 6, 8, 10, None],
"modelo__min_samples_leaf": randint(1, 40),
"modelo__max_features": ["sqrt", 0.3, 0.5],
}
aleatoria = RandomizedSearchCV(pipe_rf, distribuciones, n_iter=20, cv=cv, scoring="roc_auc",
random_state=42, n_jobs=-1).fit(Xtr, ytr)
print(aleatoria.best_params_, round(aleatoria.best_score_, 3))
# {'modelo__max_depth': 5, 'modelo__max_features': 0.5, 'modelo__min_samples_leaf': 11,
# 'modelo__n_estimators': 187} 0.822 -> AUC en test 0.840Con 20 combinaciones (100 entrenamientos) encuentra profundidad 5 de nuevo y un AUC de validación ligeramente mejor (0,822). Es la misma idea de las metaheurísticas de 03-04: cuando no puedes recorrer todo el espacio, muestrear con inteligencia. Existen métodos más sofisticados (optimización bayesiana, HalvingGridSearchCV, librerías como Optuna) que se mencionan en 07-03.
- Validación anidada y por qué el test se toca una sola vez
Hay una sutileza que conviene entender aunque no siempre se aplique. Cuando GridSearchCV prueba 24 combinaciones y se queda con la mejor, el best_score_ (0,819) es ligeramente optimista: entre 24 candidatos, el ganador lo es en parte por mérito y en parte por suerte en esos folds concretos. Es un pequeño sobreajuste de la selección a los datos de validación. Para estimar el rendimiento sin ese sesgo se usa la validación anidada (nested cross-validation): un bucle externo de validación cruzada, y dentro de cada fold externo, un GridSearchCV completo (bucle interno) que elige los hiperparámetros solo con los datos de entrenamiento de ese fold. En scikit-learn es una línea: cross_val_score(GridSearchCV(...), X, y, cv=cv_externo). Es costoso (5 × 120 entrenamientos en nuestro ejemplo) y en proyectos aplicados a menudo se sustituye por el esquema más simple de la sección anterior, que es correcto siempre que se respete la regla final:
El test se toca una sola vez. Todo lo que hemos hecho en esta lección (curvas, regularización, rejillas) se ha decidido con validación cruzada sobre el conjunto de entrenamiento; el test solo ha aparecido al final para dar la cifra que Marta llevará a Diego (AUC 0,838 del bosque ajustado, 0,844 de la logística). Si hubiéramos elegido los hiperparámetros mirando el test, esa cifra ya no sería una estimación honesta de lo que pasará mañana, sino la mejor de muchas miradas, y el despliegue traería una decepción. Es la misma disciplina que en 04-05 separaba entrenamiento, validación y test.
- El flujo completo del módulo, de un vistazo
Marta ha recorrido, con los pedidos de NovaMarket, todo el flujo que dibujamos en 04-01:
flowchart LR
A[Definir T, E, P<br/>04-01, 04-02] --> B[Datos y calidad<br/>02-03]
B --> C[Preparación en Pipeline<br/>04-03]
C --> D[Elegir algoritmos<br/>04-04]
D --> E[Validación cruzada,<br/>métricas y coste<br/>04-05]
E --> F[Diagnóstico y ajuste<br/>de hiperparámetros<br/>04-06]
F -->|no basta| C
F -->|listo| G[Test una vez,<br/>umbral, despliegue<br/>y monitorización<br/>08-01]
| Paso | Lo que hizo Marta | Resultado |
|---|---|---|
| Definir el problema | Clasificación binaria "devuelto"; línea base: regla de Diego y "siempre no" | T, E, P claros; costes 5 €/30 € |
| Preparar los datos | ensuciar_pedidos → limpieza, ingeniería, ColumnTransformer + Pipeline |
21 características, sin fugas |
| Elegir algoritmos | Logística, k-NN, árbol, bosque, SVM | Logística y bosque como candidatos |
| Evaluar | Matriz de confusión, AUC, F1, coste; validación cruzada estratificada | Logística AUC 0,84; umbral 0,2 (1.610 € frente a 3.690 € de la línea base) |
| Ajustar | Curvas de validación; GridSearchCV del bosque |
Bosque ajustado AUC 0,84; ambos válidos |
| Decidir | Test una sola vez; banda de revisión humana | Modelo listo para 08-01 |
El mismo flujo, con generar_demanda_semanal, TimeSeriesSplit y MAE en lugar de AUC, sirve para la previsión de demanda; y con generar_clientes_ml, el codo y la silueta, para la segmentación. En 09-02 lo practicarás de principio a fin con más ejercicios.
Errores Comunes y Consejos
- Elegir el modelo por su rendimiento en entrenamiento. Siempre sube con la complejidad; solo la validación dice la verdad.
- Interpretar mal la curva de aprendizaje. Curvas juntas y planas: más datos no ayudan, cambia de modelo o de características. Brecha grande: limita el modelo o consigue más datos.
- Confundir la dirección de
Cyalpha.alphaalto = más regularización;Calto = menos regularización. - Rejillas en escala lineal para parámetros multiplicativos.
Cen [1, 2, 3, 4] explora casi nada; usa [0,001, 0,01, 0,1, 1, 10]. - Aceptar un óptimo en el borde de la rejilla. Si lo mejor es el valor más alto probado, amplía la rejilla.
- Mirar el test para decidir. Cada mirada lo contamina. Rejillas y curvas con validación cruzada; test al final, una vez.
- Ajustar decenas de hiperparámetros con pocos datos. El propio ajuste sobreajusta la validación; usa rejillas pequeñas y, si el resultado importa mucho, validación anidada.
- Olvidar el modelo simple. Tras todo el ajuste, el bosque (0,838) empata con la logística sin ajustar (0,844): a igualdad de rendimiento, elige el más simple e interpretable.
Ejercicios
Ejercicio 1. Repite la curva de validación de la sección 3.2 con KNeighborsClassifier variando n_neighbors en [1, 3, 5, 9, 15, 25, 51, 101] (parámetro "modelo__n_neighbors"). Describe la forma de las dos curvas: ¿dónde está el sobreajuste, dónde el subajuste y cuál es el k óptimo? Compara con la sección 3 de 04-04, donde k = 15 parecía el mejor con una sola división.
Ejercicio 2. Sobre la demanda del NovaClean con las características de 04-04 (semana, sen, cos, black_friday) más cinco columnas de ruido puro (rng.normal(size=(len(d), 5)) con np.random.default_rng(0)), entrena LinearRegression, Ridge(alpha=10) y Lasso(alpha=5) con las semanas 1-78 y evalúa el MAE en las 79-104. ¿Cuál trata mejor las columnas de ruido? Mira los coeficientes de Lasso: ¿cuántos son exactamente cero? Escala las características antes (StandardScaler) para que alpha afecte por igual a todas.
Ejercicio 3. Amplía la búsqueda en rejilla del bosque con "modelo__max_features": ["sqrt", 0.5] (48 combinaciones) y cambia la métrica a scoring="f1". ¿Cambian los mejores hiperparámetros? ¿Por qué puede diferir el óptimo según la métrica, y cuál elegirías para NovaMarket sabiendo que la decisión final se toma con umbral y coste (04-05)?
Soluciones
Solución 1. La curva de entrenamiento de k-NN empieza en 1,000 para k = 1 (cada pedido es su propio vecino) y baja al crecer k (0,93 con k = 3, 0,85 con k = 15, 0,81 con k = 101); la de validación arranca muy baja en k = 1 (AUC 0,57: sobreajuste puro), sube con k (0,70 en k = 5, 0,75 en k = 15, 0,77 en k = 25) y se aplana en 0,79 para k = 51 y k = 101. En este rango todavía no aparece la caída por subajuste; haría falta k de varios cientos (promediar casi todo el conjunto) para verla. Con las 21 columnas del pipeline, el óptimo es un k bastante mayor que el 15 que parecía bueno en 04-04 con 4 columnas: cuantas más columnas ruidosas, más vecinos hacen falta para promediar el ruido; y aun así k-NN se queda por debajo de la logística y del bosque, lo que confirma la lectura de 04-05.
Solución 2. Con cinco columnas de ruido y solo 26 semanas de test, los MAE quedan próximos y el orden puede sorprender: la lineal da 16,1, Ridge(alpha=10) 18,2 y Lasso(alpha=5) 17,3 (frente a 16,8 del modelo sin ruido). Lo revelador son los coeficientes: la lineal asigna al ruido coeficientes de hasta ±4 unidades (que en este test concreto, por azar, no estorban), Ridge los encoge pero también encoge la señal (cos pasa de −45 a −39) y Lasso pone exactamente a cero cuatro de los cinco (el quinto queda en −0,1) conservando tendencia, estacionalidad y Black Friday. La diferencia se hace evidente al aumentar el ruido: con 20 columnas de ruido la lineal sube a un MAE de 22 y Ridge a 23, mientras Lasso se mantiene en 17,6 anulando 18 de 20; con 50 columnas, la lineal llega a 26, Ridge a 23 y Lasso sigue en 17,4 con 48 de 50 a cero. Es la selección automática de características de L1. Si subes mucho alpha, Lasso empieza a anular también coeficientes útiles y el MAE se dispara: subajuste por exceso de regularización, la otra rama de la U. Y una lección adicional: con un test de 26 semanas, diferencias de una o dos unidades de MAE son ruido de evaluación; para afirmar algo hay que usar TimeSeriesSplit (04-05).
Solución 3. Con scoring="f1" el óptimo cambia: en nuestras pruebas gana un bosque sin límite de profundidad con min_samples_leaf=5, max_features=0.5 y 100 árboles (F1 medio 0,51), justo el tipo de configuración que con AUC quedaba en la cola. La razón es que el F1 se calcula con el umbral por defecto de 0,5: los bosques poco profundos con hojas grandes producen probabilidades moderadas que rara vez superan 0,5, así que marcan pocos pedidos y su recall (y su F1) es bajo aunque ordenen los pedidos por riesgo mejor (mayor AUC). El AUC mide la calidad de la ordenación, independiente del umbral; el F1 mide un punto concreto de esa ordenación. Como en NovaMarket el umbral se elegirá después con la función de coste y la banda de revisión, lo coherente es ajustar los hiperparámetros con AUC (o con precisión media, si se prefiere centrarse en la clase positiva) y dejar el umbral para el final.
Conclusión
En esta lección hemos puesto nombre al síntoma que arrastrábamos desde 04-04: el sobreajuste (el árbol sin límite que memoriza los 2.249 pedidos con 299 hojas: AUC 1,0 en entrenamiento y 0,65 en test) y su opuesto, el subajuste, explicados con el compromiso sesgo-varianza. Hemos aprendido a diagnosticarlos con las curvas de aprendizaje (¿ayudan más datos?) y de validación (¿qué valor del hiperparámetro?), a combatirlos con más datos, menos características, regularización L2 y L1 (C, alpha; L1 seleccionó 7 de 21 columnas), límites en los árboles, ensamblados y parada temprana, y a elegir los hiperparámetros con GridSearchCV y RandomizedSearchCV leyendo best_params_ y cv_results_, con la validación anidada como refinamiento y el test reservado para una única mirada final. El bosque aleatorio, ajustado, ha alcanzado a la regresión logística, y Marta dispone ahora del flujo completo, de la definición del problema al modelo validado, sobre los tres casos de uso del módulo: devoluciones, demanda y segmentación.
Con esto cerramos el módulo 4. Has pasado de la definición de aprendizaje de Mitchell y el primer fit (04-01) a los paradigmas supervisado, no supervisado y por refuerzo (04-02), la preparación de datos en un Pipeline sin fugas (04-03), los algoritmos clásicos y sus intuiciones (04-04), la evaluación honesta con matriz de confusión, AUC, coste en euros y validación cruzada (04-05) y, en esta lección, el control del sobreajuste y el ajuste de hiperparámetros. En todo el módulo ha reaparecido la idea con la que cerramos el módulo 3: aprender es optimizar. fit minimiza una pérdida sobre los datos de entrenamiento; regularizar es añadir una penalización al objetivo, como en 03-04; y buscar hiperparámetros es una búsqueda, exhaustiva o aleatoria, en un espacio que explota combinatoriamente. En el módulo 5, Redes Neuronales y Deep Learning, llevaremos esas ideas al extremo: modelos con miles o millones de parámetros, en los que el sobreajuste es el enemigo permanente (de ahí la parada temprana, el dropout y el aumento de datos que hemos dejado apuntados) y en los que la optimización se hace, paso a paso, siguiendo el gradiente de la pérdida: el descenso del gradiente y la retropropagación que anticipamos en 03-04 y en la regresión logística de 04-04. Marta y Diego, con el predictor de devoluciones ya validado, se preguntarán en el módulo 5 si una red neuronal puede leer las reseñas de resenas.csv y las fotos de las incidencias, cosas que los algoritmos de este módulo no saben hacer.
Fundamentos de Inteligencia Artificial (IA)
Módulo 1: Introducción a la Inteligencia Artificial
Módulo 2: Principios Básicos de la IA
- Conceptos Fundamentales: Agentes, Entornos y Racionalidad
- Tipos de Inteligencia Artificial
- Los Datos como Materia Prima de la IA
- Ética y Consideraciones en IA
Módulo 3: Algoritmos en IA
- Introducción a los Algoritmos
- Algoritmos de Búsqueda
- Búsqueda con Adversario: Juegos y Minimax
- Algoritmos de Optimización
Módulo 4: Aprendizaje Automático (Machine Learning)
- Conceptos Básicos de Machine Learning
- Tipos de Aprendizaje Automático
- Preparación de Datos y Características
- Algoritmos de Machine Learning
- Evaluación y Validación de Modelos
- Sobreajuste, Regularización y Ajuste de Hiperparámetros
Módulo 5: Redes Neuronales y Deep Learning
- Introducción a las Redes Neuronales
- Arquitectura de Redes Neuronales
- Cómo Aprende una Red: Descenso del Gradiente y Retropropagación
- Deep Learning y sus Aplicaciones
- Transformers, Grandes Modelos de Lenguaje e IA Generativa
Módulo 6: Lógica y Sistemas Expertos
- Lógica en IA
- Sistemas Expertos
- Razonamiento con Incertidumbre: Probabilidad y Redes Bayesianas
- Aplicaciones de Sistemas Expertos
Módulo 7: Herramientas y Lenguajes de Programación en IA
- Lenguajes de Programación para IA
- Python Científico: NumPy, pandas y Matplotlib
- Herramientas y Librerías Populares
- Entornos de Desarrollo
Módulo 8: Proyectos y Casos de Estudio
Módulo 9: Ejercicios y Prácticas
- Ejercicios de Algoritmos
- Prácticas de Machine Learning
- Proyectos de Redes Neuronales
- Proyecto Integrador: de la Idea al Prototipo
