diff --git a/DESCRIPTION b/DESCRIPTION index df043bf3..ffc831b7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: mlr3mbo Title: Flexible Bayesian Optimization -Version: 0.2.3.9000 +Version: 0.2.4.9000 Authors@R: c( person("Lennart", "Schneider", , "lennart.sch@web.de", role = c("cre", "aut"), comment = c(ORCID = "0000-0003-4152-5308")), @@ -66,13 +66,14 @@ Suggests: rpart, stringi, testthat (>= 3.0.0) +Remotes: mlr-org/bbotk ByteCompile: no Encoding: UTF-8 Config/testthat/edition: 3 Config/testthat/parallel: false NeedsCompilation: yes Roxygen: list(markdown = TRUE, r6 = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 Collate: 'mlr_acqfunctions.R' 'AcqFunction.R' diff --git a/NEWS.md b/NEWS.md index 57766688..cb1beef6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # mlr3mbo (development version) +# mlr3mbo 0.2.4 + +* fix: Improve runtime of `AcqOptimizer` by setting `check_values = FALSE`. + # mlr3mbo 0.2.3 * compatibility: Work with new bbotk and mlr3tuning version 1.0.0. diff --git a/R/AcqFunctionCB.R b/R/AcqFunctionCB.R index 14a980c3..81328e05 100644 --- a/R/AcqFunctionCB.R +++ b/R/AcqFunctionCB.R @@ -76,8 +76,8 @@ AcqFunctionCB = R6Class("AcqFunctionCB", constants = list(...) lambda = constants$lambda p = self$surrogate$predict(xdt) - res = p$mean - self$surrogate_max_to_min * lambda * p$se - data.table(acq_cb = res) + cb = p$mean - self$surrogate_max_to_min * lambda * p$se + data.table(acq_cb = cb) } ) ) diff --git a/R/AcqFunctionEI.R b/R/AcqFunctionEI.R index 1782b972..f03ec39b 100644 --- a/R/AcqFunctionEI.R +++ b/R/AcqFunctionEI.R @@ -9,6 +9,13 @@ #' @description #' Expected Improvement. #' +#' @section Parameters: +#' * `"epsilon"` (`numeric(1)`)\cr +#' \eqn{\epsilon} value used to determine the amount of exploration. +#' Higher values result in the importance of improvements predicted by the posterior mean +#' decreasing relative to the importance of potential improvements in regions of high predictive uncertainty. +#' Defaults to `0` (standard Expected Improvement). +#' #' @references #' * `r format_bib("jones_1998")` #' @@ -60,9 +67,15 @@ AcqFunctionEI = R6Class("AcqFunctionEI", #' Creates a new instance of this [R6][R6::R6Class] class. #' #' @param surrogate (`NULL` | [SurrogateLearner]). - initialize = function(surrogate = NULL) { + #' @param epsilon (`numeric(1)`). + initialize = function(surrogate = NULL, epsilon = 0) { assert_r6(surrogate, "SurrogateLearner", null.ok = TRUE) - super$initialize("acq_ei", surrogate = surrogate, requires_predict_type_se = TRUE, direction = "maximize", label = "Expected Improvement", man = "mlr3mbo::mlr_acqfunctions_ei") + assert_number(epsilon, lower = 0, finite = TRUE) + + constants = ps(epsilon = p_dbl(lower = 0, default = 0)) + constants$values$epsilon = epsilon + + super$initialize("acq_ei", constants = constants, surrogate = surrogate, requires_predict_type_se = TRUE, direction = "maximize", label = "Expected Improvement", man = "mlr3mbo::mlr_acqfunctions_ei") }, #' @description @@ -73,14 +86,16 @@ AcqFunctionEI = R6Class("AcqFunctionEI", ), private = list( - .fun = function(xdt) { + .fun = function(xdt, ...) { if (is.null(self$y_best)) { stop("$y_best is not set. Missed to call $update()?") } + constants = list(...) + epsilon = constants$epsilon p = self$surrogate$predict(xdt) mu = p$mean se = p$se - d = self$y_best - self$surrogate_max_to_min * mu + d = (self$y_best - self$surrogate_max_to_min * mu) - epsilon d_norm = d / se ei = d * pnorm(d_norm) + se * dnorm(d_norm) ei = ifelse(se < 1e-20, 0, ei) diff --git a/R/AcqOptimizer.R b/R/AcqOptimizer.R index 68d6cd84..f9f0f5d6 100644 --- a/R/AcqOptimizer.R +++ b/R/AcqOptimizer.R @@ -93,16 +93,21 @@ AcqOptimizer = R6Class("AcqOptimizer", #' @field acq_function ([AcqFunction]). acq_function = NULL, + #' @field callbacks (`NULL` | list of [mlr3misc::Callback]). + callbacks = NULL, + #' @description #' Creates a new instance of this [R6][R6::R6Class] class. #' #' @param optimizer ([bbotk::Optimizer]). #' @param terminator ([bbotk::Terminator]). #' @param acq_function (`NULL` | [AcqFunction]). - initialize = function(optimizer, terminator, acq_function = NULL) { + #' @param callbacks (`NULL` | list of [mlr3misc::Callback]) + initialize = function(optimizer, terminator, acq_function = NULL, callbacks = NULL) { self$optimizer = assert_r6(optimizer, "Optimizer") self$terminator = assert_r6(terminator, "Terminator") self$acq_function = assert_r6(acq_function, "AcqFunction", null.ok = TRUE) + self$callbacks = assert_callbacks(as_callbacks(callbacks)) ps = ps( n_candidates = p_int(lower = 1, default = 1L), logging_level = p_fct(levels = c("fatal", "error", "warn", "info", "debug", "trace"), default = "warn"), @@ -146,7 +151,7 @@ AcqOptimizer = R6Class("AcqOptimizer", logger$set_threshold(self$param_set$values$logging_level) on.exit(logger$set_threshold(old_threshold)) - instance = OptimInstanceBatchSingleCrit$new(objective = self$acq_function, search_space = self$acq_function$domain, terminator = self$terminator, check_values = FALSE) + instance = OptimInstanceBatchSingleCrit$new(objective = self$acq_function, search_space = self$acq_function$domain, terminator = self$terminator, check_values = FALSE, callbacks = self$callbacks) # warmstart if (self$param_set$values$warmstart) { diff --git a/R/sugar.R b/R/sugar.R index ac9279c6..69c6a8b0 100644 --- a/R/sugar.R +++ b/R/sugar.R @@ -20,7 +20,7 @@ #' @param cols_y (`NULL` | `character()`)\cr #' Column id(s) in the [bbotk::Archive] that should be used as a target. #' If a list of [mlr3::LearnerRegr] is provided as the `learner` argument and `cols_y` is -#' specified as well, as many column names as learners must be provided. +#' specified as well, as many column names as learners must be provided. #' Can also be `NULL` in which case this is automatically inferred based on the archive. #' @param ... (named `list()`)\cr #' Named arguments passed to the constructor, to be set as parameters in the @@ -90,6 +90,8 @@ acqf = function(.key, ...) { #' @param acq_function (`NULL` | [AcqFunction])\cr #' [AcqFunction] that is to be used. #' Can also be `NULL`. +#' @param callbacks (`NULL` | list of [mlr3misc::Callback]) +#' Callbacks used during acquisition function optimization. #' @param ... (named `list()`)\cr #' Named arguments passed to the constructor, to be set as parameters in the #' [paradox::ParamSet]. @@ -101,9 +103,9 @@ acqf = function(.key, ...) { #' library(bbotk) #' acqo(opt("random_search"), trm("evals"), catch_errors = FALSE) #' @export -acqo = function(optimizer, terminator, acq_function = NULL, ...) { +acqo = function(optimizer, terminator, acq_function = NULL, callbacks = NULL, ...) { dots = list(...) - acqopt = AcqOptimizer$new(optimizer = optimizer, terminator = terminator, acq_function = acq_function) + acqopt = AcqOptimizer$new(optimizer = optimizer, terminator = terminator, acq_function = acq_function, callbacks = callbacks) acqopt$param_set$values = insert_named(acqopt$param_set$values, dots) acqopt } diff --git a/man/AcqOptimizer.Rd b/man/AcqOptimizer.Rd index f0f8d9b3..b4c143b1 100644 --- a/man/AcqOptimizer.Rd +++ b/man/AcqOptimizer.Rd @@ -94,6 +94,8 @@ if (requireNamespace("mlr3learners") & \item{\code{terminator}}{(\link[bbotk:Terminator]{bbotk::Terminator}).} \item{\code{acq_function}}{(\link{AcqFunction}).} + +\item{\code{callbacks}}{(\code{NULL} | list of \link[mlr3misc:Callback]{mlr3misc::Callback}).} } \if{html}{\out{}} } @@ -124,7 +126,7 @@ Set of hyperparameters.} \subsection{Method \code{new()}}{ Creates a new instance of this \link[R6:R6Class]{R6} class. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{AcqOptimizer$new(optimizer, terminator, acq_function = NULL)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{AcqOptimizer$new(optimizer, terminator, acq_function = NULL, callbacks = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -135,6 +137,8 @@ Creates a new instance of this \link[R6:R6Class]{R6} class. \item{\code{terminator}}{(\link[bbotk:Terminator]{bbotk::Terminator}).} \item{\code{acq_function}}{(\code{NULL} | \link{AcqFunction}).} + +\item{\code{callbacks}}{(\code{NULL} | list of \link[mlr3misc:Callback]{mlr3misc::Callback})} } \if{html}{\out{}} } diff --git a/man/acqo.Rd b/man/acqo.Rd index 9455152e..43114a0c 100644 --- a/man/acqo.Rd +++ b/man/acqo.Rd @@ -4,7 +4,7 @@ \alias{acqo} \title{Syntactic Sugar Acquisition Function Optimizer Construction} \usage{ -acqo(optimizer, terminator, acq_function = NULL, ...) +acqo(optimizer, terminator, acq_function = NULL, callbacks = NULL, ...) } \arguments{ \item{optimizer}{(\link[bbotk:Optimizer]{bbotk::Optimizer})\cr @@ -17,6 +17,9 @@ acqo(optimizer, terminator, acq_function = NULL, ...) \link{AcqFunction} that is to be used. Can also be \code{NULL}.} +\item{callbacks}{(\code{NULL} | list of \link[mlr3misc:Callback]{mlr3misc::Callback}) +Callbacks used during acquisition function optimization.} + \item{...}{(named \code{list()})\cr Named arguments passed to the constructor, to be set as parameters in the \link[paradox:ParamSet]{paradox::ParamSet}.} diff --git a/man/mlr_acqfunctions_ei.Rd b/man/mlr_acqfunctions_ei.Rd index 700e72ab..4357d844 100644 --- a/man/mlr_acqfunctions_ei.Rd +++ b/man/mlr_acqfunctions_ei.Rd @@ -17,6 +17,17 @@ acqf("ei") }\if{html}{\out{}} } +\section{Parameters}{ + +\itemize{ +\item \code{"epsilon"} (\code{numeric(1)})\cr +\eqn{\epsilon} value used to determine the amount of exploration. +Higher values result in the importance of improvements predicted by the posterior mean +decreasing relative to the importance of potential improvements in regions of high predictive uncertainty. +Defaults to \code{0} (standard Expected Improvement). +} +} + \examples{ if (requireNamespace("mlr3learners") & requireNamespace("DiceKriging") & @@ -110,13 +121,15 @@ In the case of maximization, this already includes the necessary change of sign. \subsection{Method \code{new()}}{ Creates a new instance of this \link[R6:R6Class]{R6} class. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{AcqFunctionEI$new(surrogate = NULL)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{AcqFunctionEI$new(surrogate = NULL, epsilon = 0)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{surrogate}}{(\code{NULL} | \link{SurrogateLearner}).} + +\item{\code{epsilon}}{(\code{numeric(1)}).} } \if{html}{\out{
}} } diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 55a84b5d..b72e56b5 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -16,8 +16,8 @@ toc: navbar: structure: - left: [reference, news, book] - right: [github, mattermost, stackoverflow, rss, lightswitch] + left: [reference, intro, news, book] + right: [search, github, mattermost, stackoverflow, rss, lightswitch] components: home: ~ reference: diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index b3ea492d..aba5c6e6 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -39,12 +39,18 @@ PS_1D_MIXED_DEPS = PS_1D_MIXED$clone(deep = TRUE) PS_1D_MIXED_DEPS$add_dep("x2", on = "x4", cond = CondEqual$new(TRUE)) FUN_1D_MIXED = function(xs) { + if (is.null(xs$x2)) { + xs$x2 = "a" + } list(y = (xs$x1 - switch(xs$x2, "a" = 0, "b" = 1, "c" = 2)) %% xs$x3 + (if (xs$x4) xs$x1 else pi)) } OBJ_1D_MIXED = ObjectiveRFun$new(fun = FUN_1D_MIXED, domain = PS_1D_MIXED, properties = "single-crit") OBJ_1D_MIXED_DEPS = ObjectiveRFun$new(fun = FUN_1D_MIXED, domain = PS_1D_MIXED_DEPS, properties = "single-crit") FUN_1D_2_MIXED = function(xs) { + if (is.null(xs$x2)) { + xs$x2 = "a" + } list(y1 = (xs$x1 - switch(xs$x2, "a" = 0, "b" = 1, "c" = 2)) %% xs$x3 + (if (xs$x4) xs$x1 else pi), y2 = xs$x1) } OBJ_1D_2_MIXED = ObjectiveRFun$new(fun = FUN_1D_2_MIXED, domain = PS_1D_MIXED, codomain = FUN_1D_2_CODOMAIN, properties = "multi-crit") diff --git a/tests/testthat/test_AcqFunctionCB.R b/tests/testthat/test_AcqFunctionCB.R index 1749b3d2..eecaa100 100644 --- a/tests/testthat/test_AcqFunctionCB.R +++ b/tests/testthat/test_AcqFunctionCB.R @@ -12,6 +12,9 @@ test_that("AcqFunctionCB works", { expect_learner(acqf$surrogate$learner) expect_true(acqf$requires_predict_type_se) + expect_r6(acqf$constants, "ParamSet") + expect_equal(acqf$constants$ids(), "lambda") + design = MAKE_DESIGN(inst) inst$eval_batch(design) diff --git a/tests/testthat/test_AcqFunctionEHVIGH.R b/tests/testthat/test_AcqFunctionEHVIGH.R index 801fd841..58273cca 100644 --- a/tests/testthat/test_AcqFunctionEHVIGH.R +++ b/tests/testthat/test_AcqFunctionEHVIGH.R @@ -15,6 +15,9 @@ test_that("AcqFunctionEHVIGH works", { expect_true(acqf$requires_predict_type_se) expect_setequal(acqf$packages, c("emoa", "fastGHQuad")) + expect_r6(acqf$constants, "ParamSet") + expect_equal(acqf$constants$ids(), c("k", "r")) + design = MAKE_DESIGN(inst) inst$eval_batch(design) diff --git a/tests/testthat/test_AcqFunctionEI.R b/tests/testthat/test_AcqFunctionEI.R index 59eae9ef..dd8be11a 100644 --- a/tests/testthat/test_AcqFunctionEI.R +++ b/tests/testthat/test_AcqFunctionEI.R @@ -13,6 +13,9 @@ test_that("AcqFunctionEI works", { expect_learner(acqf$surrogate$learner) expect_true(acqf$requires_predict_type_se) + expect_r6(acqf$constants, "ParamSet") + expect_equal(acqf$constants$ids(), "epsilon") + design = MAKE_DESIGN(inst) inst$eval_batch(design) diff --git a/tests/testthat/test_AcqFunctionSmsEgo.R b/tests/testthat/test_AcqFunctionSmsEgo.R index 020a946f..b6cdece9 100644 --- a/tests/testthat/test_AcqFunctionSmsEgo.R +++ b/tests/testthat/test_AcqFunctionSmsEgo.R @@ -12,6 +12,9 @@ test_that("AcqFunctionSmsEgo works", { expect_list(acqf$surrogate$learner, types = "Learner") expect_true(acqf$requires_predict_type_se) + expect_r6(acqf$constants, "ParamSet") + expect_equal(acqf$constants$ids(), c("lambda", "epsilon")) + design = MAKE_DESIGN(inst) inst$eval_batch(design) diff --git a/tests/testthat/test_AcqOptimizer.R b/tests/testthat/test_AcqOptimizer.R index 213963fe..69e2ca2f 100644 --- a/tests/testthat/test_AcqOptimizer.R +++ b/tests/testthat/test_AcqOptimizer.R @@ -120,3 +120,32 @@ test_that("AcqOptimizer deep clone", { expect_true(address(acqopt1$terminator) != address(acqopt2$terminator)) }) +test_that("AcqOptimizer callbacks", { + domain = ps(x = p_dbl(lower = 10, upper = 20, trafo = function(x) x - 15)) + objective = ObjectiveRFunDt$new( + fun = function(xdt) data.table(y = xdt$x ^ 2), + domain = domain, + codomain = ps(y = p_dbl(tags = "minimize")), + check_values = FALSE + ) + instance = MAKE_INST(objective = objective, search_space = domain, terminator = trm("evals", n_evals = 5L)) + design = MAKE_DESIGN(instance) + instance$eval_batch(design) + callback = callback_batch("mlr3mbo.acqopt_time", + on_optimization_begin = function(callback, context) { + callback$state$begin = Sys.time() + }, + on_optimization_end = function(callback, context) { + callback$state$end = Sys.time() + attr(callback$state$outer_instance, "acq_opt_runtime") = as.numeric(callback$state$end - callback$state$begin) + } + ) + callback$state$outer_instance = instance + acqfun = AcqFunctionEI$new(SurrogateLearner$new(REGR_FEATURELESS, archive = instance$archive)) + acqopt = AcqOptimizer$new(opt("random_search", batch_size = 10L), trm("evals", n_evals = 10L), acq_function = acqfun, callbacks = callback) + acqfun$surrogate$update() + acqfun$update() + res = acqopt$optimize() + expect_number(attr(instance, "acq_opt_runtime")) +}) +