# machine-learning-clasico-con-python
Machine Learning con scikit-learn y XGBoost
Volver a la portada del cursoAjuste de hiperparámetros con GridSearchCV
Sección 5 · Árboles, boosting con XGBoost y ajuste de hiperparámetros
Ajuste de parámetros
En la sección de ensambles, el bosque aleatorio de PyRitmo usaba 300 árboles y min_samples_leaf=3. ¿De dónde salieron esos números? Probarlos a mano, mirando el test cada vez, es la forma más rápida de engañarse. Esta lección muestra cómo buscarlos de forma ordenada con la validación cruzada estratificada del video anterior, y cómo dejar el test intacto hasta el final. Todo el código está en el notebook validacion-y-ajuste.
Parámetros e hiperparámetros
Un parámetro lo aprende el modelo al llamar fit. La regresión logística del curso aprende 14 coeficientes, uno por columna que sale de pre (9 numéricas y 5 géneros), más el intercepto. En scikit-learn, lo aprendido termina en guion bajo: coef_, intercept_.
Un hiperparámetro lo eliges tú antes de entrenar y se pasa al constructor. Ejemplos que ya usaste:
max_depthymin_samples_leafde los árboles.n_estimatorsde los ensambles (100 por omisión).learning_ratedel gradient boosting (0.1 por omisión).Cde la regresión logística (1.0 por omisión).
Ajustar hiperparámetros es entrenar muchas veces con valores distintos y quedarte con los que mejor generalizan, medidos siempre con validación cruzada sobre train.
Búsqueda en rejilla: GridSearchCV
Defines una lista de valores por hiperparámetro y GridSearchCV prueba todas las combinaciones. Como el modelo vive dentro de un Pipeline, cada nombre lleva el paso, dos guiones bajos y el hiperparámetro: rf__max_depth es el max_depth del paso llamado "rf". Si dudas, pipe.get_params().keys() lista todos los nombres válidos.
pipe = Pipeline([("pre", pre),
("rf", RandomForestClassifier(random_state=42))])
grid = {"rf__n_estimators": [100, 300],
"rf__max_depth": [None, 5, 10],
"rf__min_samples_leaf": [1, 3, 10]}
cv = StratifiedKFold(5, shuffle=True, random_state=42)
gs = GridSearchCV(pipe, grid, cv=cv, scoring="roc_auc", n_jobs=-1)
gs.fit(X_tr, y_tr)
print(gs.best_params_)
print(round(gs.best_score_, 4)) # 0.8474
La rejilla tiene $2 \times 3 \times 3 = 18$ combinaciones. Cada una se evalúa con 5 pliegues: 90 ajustes, más uno final. El costo es el producto de todas las listas, así que crece muy rápido. n_jobs=-1 reparte el trabajo entre todos los núcleos.
La ganadora es max_depth=None, min_samples_leaf=3 y n_estimators=300, con AUC medio de validación cruzada 0.8474. Ese best_score_ es una cifra de selección, no de test.
Leer cv_results_: la meseta
cv_results_ guarda una fila por combinación, con la media y la desviación de los pliegues y su rango. En un DataFrame ordenado por rank_test_score se ve algo que best_params_ esconde: las 12 mejores combinaciones están entre 0.844 y 0.847. Todas las que caen al fondo usan max_depth=5 (entre 0.837 y 0.841).
La lección práctica: la búsqueda sirve sobre todo para evitar zonas malas. Dentro de la meseta, las diferencias son menores que la variación entre pliegues, y elegir la primera o la quinta da casi lo mismo.
Búsqueda aleatoria: RandomizedSearchCV
Cuando hay muchos hiperparámetros, la rejilla se vuelve impagable. RandomizedSearchCV sortea n_iter combinaciones a partir de distribuciones de scipy.stats:
dist = {"gb__learning_rate": loguniform(0.01, 0.3),
"gb__n_estimators": randint(50, 400),
"gb__max_depth": randint(1, 6),
"gb__subsample": [0.6, 0.8, 1.0]}
pipe_gb = Pipeline([("pre", pre),
("gb", GradientBoostingClassifier(random_state=42))])
rs = RandomizedSearchCV(pipe_gb, dist, n_iter=30, cv=cv,
scoring="roc_auc", random_state=42, n_jobs=-1)
rs.fit(X_tr, y_tr)
print(round(rs.best_score_, 4)) # 0.8551
loguniform(0.01, 0.3)sortea la tasa de aprendizaje en escala logarítmica: 0.01 y 0.03 tienen la misma oportunidad que 0.1 y 0.3.randint(50, 400)sortea enteros de 50 a 399; el límite superior no se incluye.- Una lista, como la de
subsample, se sortea con la misma probabilidad para cada valor.
Con 30 combinaciones y 5 pliegues son 150 ajustes. La mejor: learning_rate 0.0463, max_depth 2, n_estimators 138 y subsample 0.8, con AUC de validación cruzada 0.8551, contra 0.849 del gradient boosting con sus valores por omisión. Árboles más bajos y una tasa más lenta: muchos pasos pequeños.
El test, una sola vez
Con refit=True (el valor por omisión), al terminar la búsqueda se reentrena la mejor combinación con todo X_tr. Ahora, y solo ahora, se mira el test:
print(round(rs.score(X_te, y_te), 4)) # 0.8761 (AUC)
pred = rs.predict(X_te)
print(confusion_matrix(y_te, pred)) # [[255 17] [41 62]]
rs.score usa el scoring de la búsqueda, así que devuelve AUC, no exactitud. En test, el modelo ajustado da AUC 0.8761, exactitud 0.845, precision 0.785 y recall 0.602. El AUC de test salió más alto que el de validación cruzada: no es mérito del modelo, son 375 canciones con su propia suerte. Lo honesto es reportarlo tal cual y no volver a ajustar para mejorarlo.
No siempre hay mucho que ganar
Con la regresión logística y una rejilla de C (mientras más pequeño, más regularización), el AUC de validación cruzada va de 0.812 con C=0.001 a 0.8232 con 0.01, 0.8303 con 0.1 y 0.8302 con 1. Desde 0.1 la curva es plana (0.8298 con 100). El valor por omisión ya estaba en la meseta: una búsqueda que confirma el valor por omisión también es información.
Trampas comunes
- Ajustar mirando el test. Si eliges con el test, su cifra deja de ser una estimación honesta.
- Reportar
best_score_como resultado final. Es la mejor de muchas: sale optimista por construcción. - Nombres sin prefijo.
max_depthsinrf__lanzaValueError: Invalid parameter. - Rejillas enormes. 4 valores en 4 hiperparámetros ya son 256 combinaciones y 1280 ajustes; usa búsqueda aleatoria.
- Rangos lineales para tasas. Para
learning_rateoC, usa escala logarítmica.
Empieza con rejillas pequeñas y espaciadas, mira cv_results_ y afina solo alrededor de la meseta.