Machine learning projects will commonly require a user to “tune” a model’s hyperparameters to find a good balance between bias and variance. Several tools are available in a data scientist’s toolbox to handle this task, the most blunt of which is a grid search. A grid search gauges the model performance over a pre-defined set of hyperparameters without regard for past performance. As models increase in complexity and training time, grid searches become unwieldly.
Idealy, we would use the information from prior model evaluations to guide us in our future parameter searches. This is precisely the idea behind Bayesian Optimization, in which our prior response distribution is iteratively updated based on our best guess of where the best parameters are. The ParBayesianOptimization
package does exactly this in the following process:
In this example, we will be using the agaricus.train dataset provided in the XGBoost package. Here, we load the packages, data, and create a folds object to be used in the scoring function.
library("xgboost")
library("ParBayesianOptimization")
data(agaricus.train, package = "xgboost")
Folds <- list(Fold1 = as.integer(seq(1,nrow(agaricus.train$data),by = 3))
, Fold2 = as.integer(seq(2,nrow(agaricus.train$data),by = 3))
, Fold3 = as.integer(seq(3,nrow(agaricus.train$data),by = 3)))
Now we need to define the scoring function. This function should, at a minimum, return a list with a Score
element, which is the model evaluation metric we want to maximize. We can also retain other pieces of information created by the scoring function by including them as named elements of the returned list. In this case, we want to retain the optimal number of rounds determined by the xgb.cv
:
scoringFunction <- function(max_depth, min_child_weight, subsample) {
dtrain <- xgb.DMatrix(agaricus.train$data,label = agaricus.train$label)
Pars <- list( booster = "gbtree"
, eta = 0.01
, max_depth = max_depth
, min_child_weight = min_child_weight
, subsample = subsample
, objective = "binary:logistic"
, eval_metric = "auc")
xgbcv <- xgb.cv(params = Pars
, data = dtrain
, nround = 100
, folds = Folds
, prediction = TRUE
, showsd = TRUE
, early_stopping_rounds = 5
, maximize = TRUE
, verbose = 0)
return(list( Score = max(xgbcv$evaluation_log$test_auc_mean)
, nrounds = xgbcv$best_iteration
)
)
}
Some other objects we need to define are the bounds, GP kernel and acquisition function.
bounds
will tell our process its search space.GauPro
function GauPro_kernel_model
and defines the covariance function.bounds <- list( max_depth = c(2L, 10L)
, min_child_weight = c(1, 100)
, subsample = c(0.25, 1))
kern <- "Matern52"
acq <- "ei"
We are now ready to put this all into the BayesianOptimization
function.
ScoreResult <- BayesianOptimization(FUN = scoringFunction
, bounds = bounds
, initPoints = 10
, bulkNew = 1
, nIters = 12
, kern = kern
, acq = acq
, kappa = 2.576
, verbose = 1
, parallel = FALSE)
#>
#> Running initial scoring function 10 times in 1 thread(s).
#>
#> Starting round number 1
#> 1) Fitting Gaussian process...
#> 2) Running local optimum search...
#> 3) Running scoring function 1 times in 1 thread(s)...
#>
#> Starting round number 2
#> 1) Fitting Gaussian process...
#> 2) Running local optimum search...
#> 3) Running scoring function 1 times in 1 thread(s)...
The console informs us that the process initialized by running scoringFunction
10 times. It then fit a Gaussian process to the parameter-score pairs, found the global optimum of the acquisition function, and ran scoringFunction
again. This process continued until we had 12 parameter-score pairs. You can interrogate the ScoreResult
object to see the results:
ScoreResult$ScoreDT
#> Iteration max_depth min_child_weight subsample Elapsed Score nrounds
#> 1: 0 5 78.36285 0.7311405 0.19 0.9779723 1
#> 2: 0 2 77.17162 0.3439467 0.22 0.9716577 5
#> 3: 0 3 72.79564 0.6146692 0.18 0.9779723 1
#> 4: 0 2 55.34643 0.4388264 0.28 0.9791570 7
#> 5: 0 8 65.44863 0.7220941 0.17 0.9779723 1
#> 6: 0 2 90.10825 0.8530226 0.15 0.9779723 1
#> 7: 0 4 58.44727 0.6579348 0.28 0.9796553 5
#> 8: 0 3 27.68439 0.7557071 1.50 0.9987817 75
#> 9: 0 6 74.70969 0.6759357 0.19 0.9779723 1
#> 10: 0 10 74.33917 0.3889225 0.37 0.9733130 10
#> 11: 1 2 1.00000 1.0000000 0.28 0.9871587 8
#> 12: 2 3 21.34722 0.7626571 0.26 0.9918503 5
ScoreResult$BestPars
#> Iteration max_depth min_child_weight subsample Score nrounds elapsedSecs
#> 1: 0 3 27.68439 0.7557071 0.9987817 75 5 secs
#> 2: 1 3 27.68439 0.7557071 0.9987817 75 13 secs
#> 3: 2 3 27.68439 0.7557071 0.9987817 75 24 secs