Tuning with scikit-learn tools

XGBoost · Practical · 12 min

Good hyperparameters often make the difference between an average and an excellent boosted model.

Because XGBClassifier is a scikit-learn estimator, the usual model selection tools work: RandomizedSearchCV, GridSearchCV, HalvingRandomSearchCV, or a loop with an external optimiser such as Optuna.

Search spaces that work well:

  • max_depth: integers 3-10,
  • min_child_weight: log-uniform 1-50,
  • subsample, colsample_bytree: uniform 0.5-1.0,
  • reg_lambda, reg_alpha: log-uniform 1e-3 to 10,
  • gamma: 0-5,
  • learning_rate: fix it at 0.05-0.1 during the search, lower it for the final fit.

Random search with 30-100 candidates usually beats a grid of the same size.

Number of trees: either include n_estimators in the search with a fixed learning rate, or use early stopping inside each fit. Early stopping inside GridSearchCV requires an eval_set, which must not be the CV fold being scored; a clean approach is to tune n_estimators explicitly, or to use xgb.cv per candidate in a custom loop.

XGBoost is already multi-threaded (n_jobs). Running a search with n_jobs=-1 on top oversubscribes the CPU; parallelise at one level only.

If preprocessing is needed (for example a TargetEncoder or ColumnTransformer), put it and the model in a Pipeline and search over xgbclassifier__max_depth and friends, so preprocessing is refitted in every fold.

Example

from scipy.stats import loguniform, randint, uniform
from sklearn.datasets import make_classification
from sklearn.model_selection import RandomizedSearchCV
from xgboost import XGBClassifier

X, y = make_classification(n_samples=2000, n_features=25, n_informative=10,
                           flip_y=0.05, random_state=0)
space = {"max_depth": randint(3, 9), "min_child_weight": loguniform(1, 50),
         "subsample": uniform(0.5, 0.5), "colsample_bytree": uniform(0.5, 0.5),
         "reg_lambda": loguniform(1e-3, 10), "n_estimators": randint(100, 600)}

search = RandomizedSearchCV(XGBClassifier(learning_rate=0.05, n_jobs=4),
                            space, n_iter=25, cv=4, scoring="roc_auc",
                            random_state=0, n_jobs=1)   # parallelise in XGBoost only
search.fit(X, y)
print(round(search.best_score_, 4))                    # 0.932
print({k: round(float(v), 2) for k, v in search.best_params_.items()})
# {'colsample_bytree': 0.96, 'max_depth': 6.0, 'min_child_weight': 1.04,
#  'n_estimators': 358.0, 'reg_lambda': 0.0, 'subsample': 0.86}

Remember

  • Random search over depth, child weight, sampling and regularisation works well
  • Fix the learning rate while searching; lower it for the final model
  • Parallelise either the search or XGBoost, not both

Watch out for

  • Passing the CV validation fold as eval_set inside a search, leaking it into model selection.

Check yourself

  1. You run RandomizedSearchCV(n_jobs=-1) over an XGBClassifier that also uses all cores. What is the likely effect?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: xgboost.readthedocs.io/en/stable/parameter.html