diff --git a/.lintr b/.lintr index 436e1b52..6fa3c0ff 100644 --- a/.lintr +++ b/.lintr @@ -5,5 +5,6 @@ linters: linters_with_defaults( object_name_linter = object_name_linter(c("snake_case", "CamelCase")), # only allow snake case and camel case object names cyclocomp_linter = NULL, # do not check function complexity commented_code_linter = NULL, # allow code in comments - line_length_linter = line_length_linter(120) + line_length_linter = line_length_linter(120), + indentation_linter(indent = 2L, hanging_indent_style = "never") ) diff --git a/DESCRIPTION b/DESCRIPTION index 0424c1fb..2b949150 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -39,16 +39,16 @@ License: LGPL-3 URL: https://mlr3mbo.mlr-org.com, https://github.com/mlr-org/mlr3mbo BugReports: https://github.com/mlr-org/mlr3mbo/issues Depends: + mlr3tuning (>= 1.1.0), R (>= 3.1.0) Imports: bbotk (>= 1.2.0), checkmate (>= 2.0.0), data.table, lgr (>= 0.3.4), - mlr3 (>= 0.21.0), + mlr3 (>= 0.21.1), mlr3misc (>= 0.11.0), - mlr3tuning (>= 1.0.2), - paradox (>= 1.0.0), + paradox (>= 1.0.1), spacefillr, R6 (>= 2.4.1) Suggests: @@ -62,6 +62,8 @@ Suggests: ranger, rgenoud, rpart, + redux, + rush, stringi, testthat (>= 3.0.0) ByteCompile: no @@ -85,8 +87,12 @@ Collate: 'AcqFunctionPI.R' 'AcqFunctionSD.R' 'AcqFunctionSmsEgo.R' + 'AcqFunctionStochasticCB.R' + 'AcqFunctionStochasticEI.R' 'AcqOptimizer.R' 'aaa.R' + 'OptimizerADBO.R' + 'OptimizerAsyncMbo.R' 'OptimizerMbo.R' 'mlr_result_assigners.R' 'ResultAssigner.R' @@ -95,6 +101,8 @@ Collate: 'Surrogate.R' 'SurrogateLearner.R' 'SurrogateLearnerCollection.R' + 'TunerADBO.R' + 'TunerAsyncMbo.R' 'TunerMbo.R' 'mlr_loop_functions.R' 'bayesopt_ego.R' diff --git a/NAMESPACE b/NAMESPACE index 9b8e3b96..e5b96257 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,7 +16,11 @@ export(AcqFunctionMulti) export(AcqFunctionPI) export(AcqFunctionSD) export(AcqFunctionSmsEgo) +export(AcqFunctionStochasticCB) +export(AcqFunctionStochasticEI) export(AcqOptimizer) +export(OptimizerADBO) +export(OptimizerAsyncMbo) export(OptimizerMbo) export(ResultAssigner) export(ResultAssignerArchive) @@ -24,6 +28,8 @@ export(ResultAssignerSurrogate) export(Surrogate) export(SurrogateLearner) export(SurrogateLearnerCollection) +export(TunerADBO) +export(TunerAsyncMbo) export(TunerMbo) export(acqf) export(acqfs) @@ -58,6 +64,7 @@ importFrom(R6,R6Class) importFrom(stats,dnorm) importFrom(stats,pnorm) importFrom(stats,quantile) +importFrom(stats,rexp) importFrom(stats,runif) importFrom(stats,setNames) importFrom(utils,bibentry) diff --git a/NEWS.md b/NEWS.md index 16fd1198..ace7e6d2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # mlr3mbo (development version) +* refactor: refactored `SurrogateLearner` and `SurrogateLearnerCollection` to allow updating on an asynchronous `Archive` +* feat: added experimental `OptimizerAsyncMbo`, `OptimizerADBO`, `TunerAsyncMbo`, and `TunerADBO` that allow for asynchronous optimization +* feat: added `AcqFunctionStochasticCB` and `AcqFunctionStochasticEI` that are useful for asynchronous optimization +* doc: minor changes to highlight differences between batch and asynchronous objects related to asynchronous support +* refactor: `AcqFunction`s and `AcqOptimizer` gained a `reset()` method. + # mlr3mbo 0.2.6 * refactor: Extract internal tuned values in instance. diff --git a/R/AcqFunction.R b/R/AcqFunction.R index 2dfb2a6f..261bc1f8 100644 --- a/R/AcqFunction.R +++ b/R/AcqFunction.R @@ -73,6 +73,14 @@ AcqFunction = R6Class("AcqFunction", # FIXME: at some point we may want to make this an AB to a private$.update }, + #' @description + #' Reset the acquisition function. + #' + #' Can be implemented by subclasses. + reset = function() { + # FIXME: at some point we may want to make this an AB to a private$.reset + }, + #' @description #' Evaluates multiple input values on the objective function. #' diff --git a/R/AcqFunctionMulti.R b/R/AcqFunctionMulti.R index 5b9543e3..f148e6ba 100644 --- a/R/AcqFunctionMulti.R +++ b/R/AcqFunctionMulti.R @@ -16,7 +16,7 @@ #' If acquisition functions have not been initialized with a surrogate, the surrogate passed during construction or lazy initialization #' will be used for all acquisition functions. #' -#' For optimization, [AcqOptimizer] can be used as for any other [AcqFunction], however, the [bbotk::Optimizer] wrapped within the [AcqOptimizer] +#' For optimization, [AcqOptimizer] can be used as for any other [AcqFunction], however, the [bbotk::OptimizerBatch] wrapped within the [AcqOptimizer] #' must support multi-objective optimization as indicated via the `multi-crit` property. #' #' @family Acquisition Function diff --git a/R/AcqFunctionSmsEgo.R b/R/AcqFunctionSmsEgo.R index 5f30efd1..20c1c503 100644 --- a/R/AcqFunctionSmsEgo.R +++ b/R/AcqFunctionSmsEgo.R @@ -18,6 +18,11 @@ #' In the case of being `NULL`, an epsilon vector is maintained dynamically as #' described in Horn et al. (2015). #' +#' @section Note: +#' * This acquisition function always also returns its current epsilon values in a list column (`acq_epsilon`). +#' These values will be logged into the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatch] of the [AcqOptimizer] and +#' therefore also in the [bbotk::Archive] of the actual [bbotk::OptimInstance] that is to be optimized. +#' #' @references #' * `r format_bib("ponweiser_2008")` #' * `r format_bib("horn_2015")` @@ -78,7 +83,7 @@ AcqFunctionSmsEgo = R6Class("AcqFunctionSmsEgo", #' @field progress (`numeric(1)`)\cr #' Optimization progress (typically, the number of function evaluations left). - #' Note that this requires the [bbotk::OptimInstance] to be terminated via a [bbotk::TerminatorEvals]. + #' Note that this requires the [bbotk::OptimInstanceBatch] to be terminated via a [bbotk::TerminatorEvals]. progress = NULL, #' @description @@ -94,7 +99,7 @@ AcqFunctionSmsEgo = R6Class("AcqFunctionSmsEgo", constants = ps( lambda = p_dbl(lower = 0, default = 1), - epsilon = p_dbl(lower = 0, default = NULL, special_vals = list(NULL)) # for NULL, it will be calculated dynamically + epsilon = p_dbl(lower = 0, default = NULL, special_vals = list(NULL)) # if NULL, it will be calculated dynamically ) constants$values$lambda = lambda constants$values$epsilon = epsilon @@ -140,6 +145,13 @@ AcqFunctionSmsEgo = R6Class("AcqFunctionSmsEgo", } else { self$epsilon = self$constants$values$epsilon } + }, + + #' @description + #' Reset the acquisition function. + #' Resets `epsilon`. + reset = function() { + self$epsilon = NULL } ), @@ -163,7 +175,7 @@ AcqFunctionSmsEgo = R6Class("AcqFunctionSmsEgo", # allocate memory for adding points to front for HV calculation in C front2 = t(rbind(self$ys_front, 0)) sms = .Call("c_sms_indicator", PACKAGE = "mlr3mbo", cbs, self$ys_front, front2, self$epsilon, self$ref_point) # note that the negative indicator is returned from C - data.table(acq_smsego = sms) + data.table(acq_smsego = sms, acq_epsilon = list(self$epsilon)) } ) ) diff --git a/R/AcqFunctionStochasticCB.R b/R/AcqFunctionStochasticCB.R new file mode 100644 index 00000000..b8d66ebf --- /dev/null +++ b/R/AcqFunctionStochasticCB.R @@ -0,0 +1,188 @@ +#' @title Acquisition Function Stochastic Confidence Bound +#' +#' @include AcqFunction.R +#' @name mlr_acqfunctions_stochastic_cb +#' +#' @templateVar id stochastic_cb +#' @template section_dictionary_acqfunctions +#' +#' @description +#' Lower / Upper Confidence Bound with lambda sampling and decay. +#' The initial \eqn{\lambda} is drawn from an uniform distribution between `min_lambda` and `max_lambda` or from an exponential distribution with rate `1 / lambda`. +#' \eqn{\lambda} is updated after each update by the formula `lambda * exp(-rate * (t %% period))`, where `t` is the number of times the acquisition function has been updated. +#' +#' While this acquisition function usually would be used within an asynchronous optimizer, e.g., [OptimizerAsyncMbo], +#' it can in principle also be used in synchronous optimizers, e.g., [OptimizerMbo]. +#' +#' @section Parameters: +#' * `"lambda"` (`numeric(1)`)\cr +#' \eqn{\lambda} value for sampling from the exponential distribution. +#' Defaults to `1.96`. +#' * `"min_lambda"` (`numeric(1)`)\cr +#' Minimum value of \eqn{\lambda}for sampling from the uniform distribution. +#' Defaults to `0.01`. +#' * `"max_lambda"` (`numeric(1)`)\cr +#' Maximum value of \eqn{\lambda} for sampling from the uniform distribution. +#' Defaults to `10`. +#' * `"distribution"` (`character(1)`)\cr +#' Distribution to sample \eqn{\lambda} from. +#' One of `c("uniform", "exponential")`. +#' Defaults to `uniform`. +#' * `"rate"` (`numeric(1)`)\cr +#' Rate of the exponential decay. +#' Defaults to `0` i.e. no decay. +#' * `"period"` (`integer(1)`)\cr +#' Period of the exponential decay. +#' Defaults to `NULL`, i.e., the decay has no period. +#' +#' @section Note: +#' * This acquisition function always also returns its current (`acq_lambda`) and original (`acq_lambda_0`) \eqn{\lambda}. +#' These values will be logged into the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatch] of the [AcqOptimizer] and +#' therefore also in the [bbotk::Archive] of the actual [bbotk::OptimInstance] that is to be optimized. +#' +#' @references +#' * `r format_bib("snoek_2012")` +#' * `r format_bib("egele_2023")` +#' +#' @family Acquisition Function +#' @export +#' @examples +#' if (requireNamespace("mlr3learners") & +#' requireNamespace("DiceKriging") & +#' requireNamespace("rgenoud")) { +#' library(bbotk) +#' library(paradox) +#' library(mlr3learners) +#' library(data.table) +#' +#' fun = function(xs) { +#' list(y = xs$x ^ 2) +#' } +#' domain = ps(x = p_dbl(lower = -10, upper = 10)) +#' codomain = ps(y = p_dbl(tags = "minimize")) +#' objective = ObjectiveRFun$new(fun = fun, domain = domain, codomain = codomain) +#' +#' instance = OptimInstanceBatchSingleCrit$new( +#' objective = objective, +#' terminator = trm("evals", n_evals = 5)) +#' +#' instance$eval_batch(data.table(x = c(-6, -5, 3, 9))) +#' +#' learner = default_gp() +#' +#' surrogate = srlrn(learner, archive = instance$archive) +#' +#' acq_function = acqf("stochastic_cb", surrogate = surrogate, lambda = 3) +#' +#' acq_function$surrogate$update() +#' acq_function$update() +#' acq_function$eval_dt(data.table(x = c(-1, 0, 1))) +#' } +AcqFunctionStochasticCB = R6Class("AcqFunctionStochasticCB", + inherit = AcqFunction, + + public = list( + + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + #' + #' @param surrogate (`NULL` | [SurrogateLearner]). + #' @param lambda (`numeric(1)`). + #' @param min_lambda (`numeric(1)`). + #' @param max_lambda (`numeric(1)`). + #' @param distribution (`character(1)`). + #' @param rate (`numeric(1)`). + #' @param period (`NULL` | `integer(1)`). + initialize = function( + surrogate = NULL, + lambda = 1.96, + min_lambda = 0.01, + max_lambda = 10, + distribution = "uniform", + rate = 0, + period = NULL + ) { + assert_r6(surrogate, "SurrogateLearner", null.ok = TRUE) + private$.lambda = assert_number(lambda, lower = .Machine$double.neg.eps, null.ok = TRUE) + private$.min_lambda = assert_number(min_lambda, lower = .Machine$double.neg.eps, null.ok = TRUE) + private$.max_lambda = assert_number(max_lambda, lower = .Machine$double.neg.eps, null.ok = TRUE) + private$.distribution = assert_choice(distribution, choices = c("uniform", "exponential")) + + if (private$.distribution == "uniform" && (is.null(private$.min_lambda) || is.null(private$.max_lambda))) { + stop('If `distribution` is "uniform", `min_lambda` and `max_lambda` must be set.') + } + + if (private$.distribution == "exponential" && is.null(private$.lambda)) { + stop('If `distribution` is "exponential", `lambda` must be set.') + } + + private$.rate = assert_number(rate, lower = 0) + private$.period = assert_int(period, lower = 1, null.ok = TRUE) + + constants = ps(lambda = p_dbl(lower = 0)) + + super$initialize("acq_cb", + constants = constants, + surrogate = surrogate, + requires_predict_type_se = TRUE, + direction = "same", + label = "Stochastic Lower / Upper Confidence Bound", + man = "mlr3mbo::mlr_acqfunctions_stochastic_cb") + }, + + #' @description + #' Update the acquisition function. + #' Samples and decays lambda. + update = function() { + # sample lambda + if (is.null(self$constants$values$lambda)) { + + if (private$.distribution == "uniform") { + lambda = runif(1, private$.min_lambda, private$.max_lambda) + } else { + lambda = rexp(1, 1 / private$.lambda) + } + + private$.lambda_0 = lambda + self$constants$values$lambda = lambda + } + + # decay lambda + if (private$.rate > 0) { + lambda_0 = private$.lambda_0 + period = private$.period + t = if (is.null(period)) private$.t else private$.t %% period + rate = private$.rate + + self$constants$values$lambda = lambda_0 * exp(-rate * t) + private$.t = t + 1L + } + }, + + #' @description + #' Reset the acquisition function. + #' Resets the private update counter `.t` used within the epsilon decay. + reset = function() { + private$.t = 0L + } + ), + + private = list( + .lambda = NULL, + .min_lambda = NULL, + .max_lambda = NULL, + .distribution = NULL, + .rate = NULL, + .period = NULL, + .lambda_0 = NULL, + .t = 0L, + .fun = function(xdt, lambda) { + p = self$surrogate$predict(xdt) + cb = p$mean - self$surrogate_max_to_min * lambda * p$se + data.table(acq_cb = cb, acq_lambda = lambda, acq_lambda_0 = private$.lambda_0) + } + ) +) + +mlr_acqfunctions$add("stochastic_cb", AcqFunctionStochasticCB) + diff --git a/R/AcqFunctionStochasticEI.R b/R/AcqFunctionStochasticEI.R new file mode 100644 index 00000000..9b3770a3 --- /dev/null +++ b/R/AcqFunctionStochasticEI.R @@ -0,0 +1,157 @@ +#' @title Acquisition Function Stochastic Expected Improvement +#' +#' @include AcqFunction.R +#' @name mlr_acqfunctions_stochastic_ei +#' +#' @templateVar id stochastic_ei +#' @template section_dictionary_acqfunctions +#' +#' @description +#' Expected Improvement with epsilon decay. +#' \eqn{\epsilon} is updated after each update by the formula `epsilon * exp(-rate * (t %% period))` where `t` is the number of times the acquisition function has been updated. +#' +#' While this acquisition function usually would be used within an asynchronous optimizer, e.g., [OptimizerAsyncMbo], +#' it can in principle also be used in synchronous optimizers, e.g., [OptimizerMbo]. +#' +#' @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.1`. +#' * `"rate"` (`numeric(1)`)\cr +# Rate of the exponential decay. +#' Defaults to `0.05`. +#' * `"period"` (`integer(1)`)\cr +#' Period of the exponential decay. +#' Defaults to `NULL`, i.e., the decay has no period. +#' +#' @section Note: +#' * This acquisition function always also returns its current (`acq_epsilon`) and original (`acq_epsilon_0`) \eqn{\epsilon}. +#' These values will be logged into the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatch] of the [AcqOptimizer] and +#' therefore also in the [bbotk::Archive] of the actual [bbotk::OptimInstance] that is to be optimized. +#' +#' @references +#' * `r format_bib("jones_1998")` +#' +#' @family Acquisition Function +#' @export +#' @examples +#' if (requireNamespace("mlr3learners") & +#' requireNamespace("DiceKriging") & +#' requireNamespace("rgenoud")) { +#' library(bbotk) +#' library(paradox) +#' library(mlr3learners) +#' library(data.table) +#' +#' fun = function(xs) { +#' list(y = xs$x ^ 2) +#' } +#' domain = ps(x = p_dbl(lower = -10, upper = 10)) +#' codomain = ps(y = p_dbl(tags = "minimize")) +#' objective = ObjectiveRFun$new(fun = fun, domain = domain, codomain = codomain) +#' +#' instance = OptimInstanceBatchSingleCrit$new( +#' objective = objective, +#' terminator = trm("evals", n_evals = 5)) +#' +#' instance$eval_batch(data.table(x = c(-6, -5, 3, 9))) +#' +#' learner = default_gp() +#' +#' surrogate = srlrn(learner, archive = instance$archive) +#' +#' acq_function = acqf("stochastic_ei", surrogate = surrogate) +#' +#' acq_function$surrogate$update() +#' acq_function$update() +#' acq_function$eval_dt(data.table(x = c(-1, 0, 1))) +#' } +AcqFunctionStochasticEI = R6Class("AcqFunctionStochasticEI", + inherit = AcqFunction, + + public = list( + + #' @field y_best (`numeric(1)`)\cr + #' Best objective function value observed so far. + #' In the case of maximization, this already includes the necessary change of sign. + y_best = NULL, + + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + #' + #' @param surrogate (`NULL` | [SurrogateLearner]). + #' @param epsilon (`numeric(1)`). + #' @param rate (`numeric(1)`). + #' @param period (`NULL` | `integer(1)`). + initialize = function( + surrogate = NULL, + epsilon = 0.1, + rate = 0.05, + period = NULL + ) { + assert_r6(surrogate, "SurrogateLearner", null.ok = TRUE) + private$.epsilon_0 = assert_number(epsilon, lower = 0, finite = TRUE) + private$.rate = assert_number(rate, lower = 0, finite = TRUE) + private$.period = assert_int(period, lower = 1, null.ok = TRUE) + + constants = ps(epsilon = p_dbl(lower = 0, default = 0.1)) + + super$initialize("acq_ei", + constants = constants, + surrogate = surrogate, + requires_predict_type_se = TRUE, + direction = "maximize", + label = "Stochastic Expected Improvement", + man = "mlr3mbo::mlr_acqfunctions_stochastic_ei") + }, + + #' @description + #' Update the acquisition function. + #' Sets `y_best` to the best observed objective function value. + #' Decays epsilon. + update = function() { + self$y_best = min(self$surrogate_max_to_min * self$archive$data[[self$surrogate$cols_y]]) + + # decay epsilon + epsilon_0 = private$.epsilon_0 + period = private$.period + t = if (is.null(period)) private$.t else private$.t %% period + rate = private$.rate + + self$constants$values$epsilon = epsilon_0 * exp(-rate * t) + private$.t = t + 1L + }, + + #' @description + #' Reset the acquisition function. + #' Resets the private update counter `.t` used within the epsilon decay. + reset = function() { + private$.t = 0L + } + ), + + private = list( + .rate = NULL, + .period = NULL, + .epsilon_0 = NULL, + .t = 0L, + .fun = function(xdt, epsilon) { + if (is.null(self$y_best)) { + stop("$y_best is not set. Missed to call $update()?") + } + p = self$surrogate$predict(xdt) + mu = p$mean + se = p$se + 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) + data.table(acq_ei = ei, acq_epsilon = epsilon, acq_epsilon_0 = private$.epsilon_0) + } + ) +) + +mlr_acqfunctions$add("stochastic_ei", AcqFunctionStochasticEI) + diff --git a/R/AcqOptimizer.R b/R/AcqOptimizer.R index ae95d76e..1802ff85 100644 --- a/R/AcqOptimizer.R +++ b/R/AcqOptimizer.R @@ -2,7 +2,7 @@ #' #' @description #' Optimizer for [AcqFunction]s which performs the acquisition function optimization. -#' Wraps an [bbotk::Optimizer] and [bbotk::Terminator]. +#' Wraps an [bbotk::OptimizerBatch] and [bbotk::Terminator]. #' #' @section Parameters: #' \describe{ @@ -10,9 +10,9 @@ #' Number of candidate points to propose. #' Note that this does not affect how the acquisition function itself is calculated (e.g., setting `n_candidates > 1` will not #' result in computing the q- or multi-Expected Improvement) but rather the top `n_candidates` are selected from the -#' [bbotk::Archive] of the acquisition function [bbotk::OptimInstance]. +#' [bbotk::ArchiveBatch] of the acquisition function [bbotk::OptimInstanceBatch]. #' Note that setting `n_candidates > 1` is usually not a sensible idea but it is still supported for experimental reasons. -#' Note that in the case of the acquisition function [bbotk::OptimInstance] being multi-criteria, due to using an [AcqFunctionMulti], +#' Note that in the case of the acquisition function [bbotk::OptimInstanceBatch] being multi-criteria, due to using an [AcqFunctionMulti], #' selection of the best candidates is performed via non-dominated-sorting. #' Default is `1`. #' } @@ -89,7 +89,7 @@ AcqOptimizer = R6Class("AcqOptimizer", public = list( - #' @field optimizer ([bbotk::Optimizer]). + #' @field optimizer ([bbotk::OptimizerBatch]). optimizer = NULL, #' @field terminator ([bbotk::Terminator]). @@ -104,7 +104,7 @@ AcqOptimizer = R6Class("AcqOptimizer", #' @description #' Creates a new instance of this [R6][R6::R6Class] class. #' - #' @param optimizer ([bbotk::Optimizer]). + #' @param optimizer ([bbotk::OptimizerBatch]). #' @param terminator ([bbotk::Terminator]). #' @param acq_function (`NULL` | [AcqFunction]). #' @param callbacks (`NULL` | list of [mlr3misc::Callback]) @@ -150,10 +150,10 @@ AcqOptimizer = R6Class("AcqOptimizer", optimize = function() { is_multi_acq_function = self$acq_function$codomain$length > 1L - logger = lgr::get_logger("bbotk") - old_threshold = logger$threshold - logger$set_threshold(self$param_set$values$logging_level) - on.exit(logger$set_threshold(old_threshold)) + lg = lgr::get_logger("bbotk") + old_threshold = lg$threshold + lg$set_threshold(self$param_set$values$logging_level) + on.exit(lg$set_threshold(old_threshold)) if (is_multi_acq_function) { instance = OptimInstanceBatchMultiCrit$new(objective = self$acq_function, search_space = self$acq_function$domain, terminator = self$terminator, check_values = FALSE, callbacks = self$callbacks) @@ -215,11 +215,18 @@ AcqOptimizer = R6Class("AcqOptimizer", # setcolorder(xdt, c(instance$archive$cols_x, "x_domain", instance$objective$id)) #} xdt[, -c("timestamp", "batch_nr")] # drop timestamp and batch_nr information from the candidates + }, + + #' @description + #' Reset the acquisition function optimizer. + #' + #' Currently not used. + reset = function() { + } ), active = list( - #' @template field_print_id print_id = function(rhs) { if (missing(rhs)) { @@ -240,7 +247,6 @@ AcqOptimizer = R6Class("AcqOptimizer", ), private = list( - .param_set = NULL, deep_clone = function(name, value) { diff --git a/R/OptimizerADBO.R b/R/OptimizerADBO.R new file mode 100644 index 00000000..8565cdf0 --- /dev/null +++ b/R/OptimizerADBO.R @@ -0,0 +1,126 @@ +#' @title Asynchronous Decentralized Bayesian Optimization +#' @name mlr_optimizers_adbo +#' +#' @description +#' `OptimizerADBO` class that implements Asynchronous Decentralized Bayesian Optimization (ADBO). +#' ADBO is a variant of Asynchronous Model Based Optimization (AMBO) that uses [AcqFunctionStochasticCB] with exponential lambda decay. +#' +#' Currently, only single-objective optimization is supported and [OptimizerADBO] is considered an experimental feature and API might be subject to changes. +#' +#' @note +#' The lambda parameter of the confidence bound acquisition function controls the trade-off between exploration and exploitation. +#' A large lambda value leads to more exploration, while a small lambda value leads to more exploitation. +#' The initial lambda value of the acquisition function used on each worker is drawn from an exponential distribution with rate `1 / lambda`. +#' ADBO can use periodic exponential decay to reduce lambda periodically for a given time step `t` with the formula `lambda * exp(-rate * (t %% period))`. +#' The [SurrogateLearner] is configured to use a random forest and the [AcqOptimizer] is a random search with a batch size of 1000 and a budget of 10000 evaluations. +#' +#' @section Parameters: +#' \describe{ +#' \item{`lambda`}{`numeric(1)`\cr +#' Value used for sampling the lambda for each worker from an exponential distribution.} +#' \item{`rate`}{`numeric(1)`\cr +#' Rate of the exponential decay.} +#' \item{`period`}{`integer(1)`\cr +#' Period of the exponential decay.} +#' \item{`initial_design`}{`data.table::data.table()`\cr +#' Initial design of the optimization. +#' If `NULL`, a design of size `design_size` is generated with the specified `design_function`. +#' Default is `NULL`.} +#' \item{`design_size`}{`integer(1)`\cr +#' Size of the initial design if it is to be generated. +#' Default is `100`.} +#' \item{`design_function`}{`character(1)`\cr +#' Sampling function to generate the initial design. +#' Can be `random` [paradox::generate_design_random], `lhs` [paradox::generate_design_lhs], or `sobol` [paradox::generate_design_sobol]. +#' Default is `sobol`.} +#' \item{`n_workers`}{`integer(1)`\cr +#' Number of parallel workers. +#' If `NULL`, all rush workers specified via [rush::rush_plan()] are used. +#' Default is `NULL`.} +#' } +#' +#' @references +#' * `r format_bib("egele_2023")` +#' +#' @export +#' @examples +#' \donttest{ +#' if (requireNamespace("rush") & +#' requireNamespace("mlr3learners") & +#' requireNamespace("DiceKriging") & +#' requireNamespace("rgenoud")) { +#' +#' library(bbotk) +#' library(paradox) +#' library(mlr3learners) +#' +#' fun = function(xs) { +#' list(y = xs$x ^ 2) +#' } +#' domain = ps(x = p_dbl(lower = -10, upper = 10)) +#' codomain = ps(y = p_dbl(tags = "minimize")) +#' objective = ObjectiveRFun$new(fun = fun, domain = domain, codomain = codomain) +#' +#' instance = OptimInstanceAsyncSingleCrit$new( +#' objective = objective, +#' terminator = trm("evals", n_evals = 10)) +#' +#' rush::rush_plan(n_workers=2) +#' +#' optimizer = opt("adbo", design_size = 4, n_workers = 2) +#' +#' optimizer$optimize(instance) +#' } +#' } +OptimizerADBO = R6Class("OptimizerADBO", + inherit = OptimizerAsyncMbo, + + public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + initialize = function() { + param_set = ps( + lambda = p_dbl(lower = 0, default = 1.96), + rate = p_dbl(lower = 0, default = 0.1), + period = p_int(lower = 1L, default = 25L) + ) + + super$initialize( + id = "adbo", + param_set = param_set, + label = "Asynchronous Decentralized Bayesian Optimization", + man = "mlr3mbo::OptimizerADBO") + + self$param_set$set_values( + lambda = 1.96, + rate = 0.1, + period = 25L) + }, + + #' @description + #' Performs the optimization on an [bbotk::OptimInstanceAsyncSingleCrit] until termination. + #' The single evaluations will be written into the [bbotk::ArchiveAsync]. + #' The result will be written into the instance object. + #' + #' @param inst ([bbotk::OptimInstanceAsyncSingleCrit]). + #' @return [data.table::data.table()] + optimize = function(inst) { + self$acq_function = AcqFunctionStochasticCB$new( + lambda = self$param_set$values$lambda, + rate = self$param_set$values$rate, + period = self$param_set$values$period + ) + + self$surrogate = default_surrogate(inst, force_random_forest = TRUE) + + self$acq_optimizer = AcqOptimizer$new( + optimizer = opt("random_search", batch_size = 1000L), + terminator = trm("evals", n_evals = 10000L)) + + super$optimize(inst) + } + ) +) + +#' @include aaa.R +optimizers[["adbo"]] = OptimizerADBO diff --git a/R/OptimizerAsyncMbo.R b/R/OptimizerAsyncMbo.R new file mode 100644 index 00000000..c7b202dc --- /dev/null +++ b/R/OptimizerAsyncMbo.R @@ -0,0 +1,372 @@ +#' @title Asynchronous Model Based Optimization +#' +#' @name mlr_optimizers_async_mbo +#' +#' @description +#' `OptimizerAsyncMbo` class that implements Asynchronous Model Based Optimization (AMBO). +#' AMBO starts multiple sequential MBO runs on different workers. +#' The worker communicate asynchronously through a shared archive relying on the \pkg{rush} package. +#' The optimizer follows a modular layout in which the surrogate model, acquisition function, and acquisition optimizer can be changed. +#' The [SurrogateLearner] will impute missing values due to pending evaluations. +#' A stochastic [AcqFunction], e.g., [AcqFunctionStochasticEI] or [AcqFunctionStochasticCB] is used to create varying versions of the acquisition +#' function on each worker, promoting different exploration-exploitation trade-offs. +#' The [AcqOptimizer] class remains consistent with the one used in synchronous MBO. +#' +#' In contrast to [OptimizerMbo], no [loop_function] can be specified that determines the AMBO flavor as `OptimizerAsyncMbo` simply relies on +#' a surrogate update, acquisition function update and acquisition function optimization step as an internal loop. +#' +#' Currently, only single-objective optimization is supported and `OptimizerAsyncMbo` is considered an experimental feature and API might be subject to changes. +#' +#' Note that in general the [SurrogateLearner] is updated one final time on all available data after the optimization process has terminated. +#' However, in certain scenarios this is not always possible or meaningful. +#' It is therefore recommended to manually inspect the [SurrogateLearner] after optimization if it is to be used, e.g., for visualization purposes to make +#' sure that it has been properly updated on all available data. +#' If this final update of the [SurrogateLearner] could not be performed successfully, a warning will be logged. +#' +#' By specifying a [ResultAssigner], one can alter how the final result is determined after optimization, e.g., +#' simply based on the evaluations logged in the archive [ResultAssignerArchive] or based on the [Surrogate] via [ResultAssignerSurrogate]. +#' +#' @section Archive: +#' The [bbotk::ArchiveAsync] holds the following additional columns that are specific to AMBO algorithms: +#' * `acq_function$id` (`numeric(1)`)\cr +#' The value of the acquisition function. +#' * `".already_evaluated"` (`logical(1))`\cr +#' Whether this point was already evaluated. Depends on the `skip_already_evaluated` parameter of the [AcqOptimizer]. +#' +#' If the [bbotk::ArchiveAsync] does not contain any evaluations prior to optimization, an initial design is needed. +#' If the `initial_design` parameter is specified to be a `data.table`, this data will be used. +#' Otherwise, if it is `NULL`, an initial design of size `design_size` will be generated based on the `generate_design` sampling function. +#' See also the parameters below. +#' +#' @section Parameters: +#' \describe{ +#' \item{`initial_design`}{`data.table::data.table()`\cr +#' Initial design of the optimization. +#' If `NULL`, a design of size `design_size` is generated with the specified `design_function`. +#' Default is `NULL`.} +#' \item{`design_size`}{`integer(1)`\cr +#' Size of the initial design if it is to be generated. +#' Default is `100`.} +#' \item{`design_function`}{`character(1)`\cr +#' Sampling function to generate the initial design. +#' Can be `random` [paradox::generate_design_random], `lhs` [paradox::generate_design_lhs], or `sobol` [paradox::generate_design_sobol]. +#' Default is `sobol`.} +#' \item{`n_workers`}{`integer(1)`\cr +#' Number of parallel workers. +#' If `NULL`, all rush workers specified via [rush::rush_plan()] are used. +#' Default is `NULL`.} +#' } +#' +#' @export +#' @examples +#' \donttest{ +#' if (requireNamespace("rush") & +#' requireNamespace("mlr3learners") & +#' requireNamespace("DiceKriging") & +#' requireNamespace("rgenoud")) { +#' +#' library(bbotk) +#' library(paradox) +#' library(mlr3learners) +#' +#' fun = function(xs) { +#' list(y = xs$x ^ 2) +#' } +#' domain = ps(x = p_dbl(lower = -10, upper = 10)) +#' codomain = ps(y = p_dbl(tags = "minimize")) +#' objective = ObjectiveRFun$new(fun = fun, domain = domain, codomain = codomain) +#' +#' instance = OptimInstanceAsyncSingleCrit$new( +#' objective = objective, +#' terminator = trm("evals", n_evals = 10)) +#' +#' rush::rush_plan(n_workers=2) +#' +#' optimizer = opt("async_mbo", design_size = 4, n_workers = 2) +#' +#' optimizer$optimize(instance) +#' } +#' } +OptimizerAsyncMbo = R6Class("OptimizerAsyncMbo", + inherit = bbotk::OptimizerAsync, + + public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + #' + #' If `surrogate` is `NULL` and the `acq_function$surrogate` field is populated, this [SurrogateLearner] is used. + #' Otherwise, `default_surrogate(instance)` is used. + #' If `acq_function` is `NULL` and the `acq_optimizer$acq_function` field is populated, this [AcqFunction] is used (and therefore its `$surrogate` if populated; see above). + #' Otherwise `default_acqfunction(instance)` is used. + #' If `acq_optimizer` is `NULL`, `default_acqoptimizer(instance)` is used. + #' + #' Even if already initialized, the `surrogate$archive` field will always be overwritten by the [bbotk::ArchiveAsync] of the current [bbotk::OptimInstanceAsyncSingleCrit] to be optimized. + #' + #' For more information on default values for `surrogate`, `acq_function`, `acq_optimizer` and `result_assigner`, see `?mbo_defaults`. + #' + #' @template param_id + #' @template param_surrogate + #' @template param_acq_function + #' @template param_acq_optimizer + #' @template param_result_assigner + #' @template param_label + #' @param param_set ([paradox::ParamSet])\cr + #' Set of control parameters. + #' @template param_man + initialize = function( + id = "async_mbo", + surrogate = NULL, + acq_function = NULL, + acq_optimizer = NULL, + result_assigner = NULL, + param_set = NULL, + label = "Asynchronous Model Based Optimization", + man = "mlr3mbo::OptimizerAsyncMbo" + ) { + + default_param_set = ps( + initial_design = p_uty(), + design_size = p_int(lower = 1, default = 100L), + design_function = p_fct(c("random", "sobol", "lhs"), default = "sobol"), + n_workers = p_int(lower = 1L) + ) + param_set = c(default_param_set, param_set) + + param_set$set_values(design_size = 100L, design_function = "sobol") + + super$initialize("async_mbo", + param_set = param_set, + param_classes = c("ParamLgl", "ParamInt", "ParamDbl", "ParamFct"), # is replaced with dynamic AB after construction + properties = c("dependencies", "single-crit"), # is replaced with dynamic AB after construction + packages = c("mlr3mbo", "rush"), # is replaced with dynamic AB after construction + label = label, + man = man) + + self$surrogate = assert_r6(surrogate, classes = "Surrogate", null.ok = TRUE) + self$acq_function = assert_r6(acq_function, classes = "AcqFunction", null.ok = TRUE) + self$acq_optimizer = assert_r6(acq_optimizer, classes = "AcqOptimizer", null.ok = TRUE) + self$result_assigner = assert_r6(result_assigner, classes = "ResultAssigner", null.ok = TRUE) + }, + + #' @description + #' Print method. + #' + #' @return (`character()`). + print = function() { + catn(format(self), if (is.na(self$label)) "" else paste0(": ", self$label)) + #catn(str_indent("* Parameters:", as_short_string(self$param_set$values))) + catn(str_indent("* Parameter classes:", self$param_classes)) + catn(str_indent("* Properties:", self$properties)) + catn(str_indent("* Packages:", self$packages)) + catn(str_indent("* Surrogate:", if (is.null(self$surrogate)) "-" else self$surrogate$print_id)) + catn(str_indent("* Acquisition Function:", if (is.null(self$acq_function)) "-" else class(self$acq_function)[1L])) + catn(str_indent("* Acquisition Function Optimizer:", if (is.null(self$acq_optimizer)) "-" else self$acq_optimizer$print_id)) + catn(str_indent("* Result Assigner:", if (is.null(self$result_assigner)) "-" else class(self$result_assigner)[1L])) + }, + + #' @description + #' Reset the optimizer. + #' Sets the following fields to `NULL`: + #' `surrogate`, `acq_function`, `acq_optimizer`,`result_assigner` + #' Resets parameter values `design_size` and `design_function` to their defaults. + reset = function() { + private$.surrogate = NULL + private$.acq_function = NULL + private$.acq_optimizer = NULL + private$.result_assigner = NULL + self$param_set$set_values(design_size = 100L, design_function = "sobol") + }, + + #' @description + #' Performs the optimization on an [bbotk::OptimInstanceAsyncSingleCrit] until termination. + #' The single evaluations will be written into the [bbotk::ArchiveAsync]. + #' The result will be written into the instance object. + #' + #' @param inst ([bbotk::OptimInstanceAsyncSingleCrit]). + #' @return [data.table::data.table()] + optimize = function(inst) { + if (is.null(self$acq_function)) { + self$acq_function = self$acq_optimizer$acq_function %??% default_acqfunction(inst) + } + + if (is.null(self$surrogate)) { # acq_function$surrogate has precedence + self$surrogate = self$acq_function$surrogate %??% default_surrogate(inst) + } + + if (is.null(self$acq_optimizer)) { + self$acq_optimizer = default_acqoptimizer(self$acq_function) + } + + if (is.null(self$result_assigner)) { + self$result_assigner = default_result_assigner(inst) + } + + self$surrogate$reset() + self$acq_function$reset() + self$acq_optimizer$reset() + + self$surrogate$archive = inst$archive + self$acq_function$surrogate = self$surrogate + self$acq_optimizer$acq_function = self$acq_function + + # FIXME: if result_assigner is for example ResultAssignerSurrogate the surrogate won't be set automatically + + check_packages_installed(self$packages, msg = sprintf("Package '%%s' required but not installed for Optimizer '%s'", format(self))) + + lg = lgr::get_logger("bbotk") + pv = self$param_set$values + + # initial design + design = if (inst$archive$n_evals) { + lg$debug("Using archive with %s evaluations as initial design", inst$archive$n_evals) + NULL + } else if (is.null(pv$initial_design)) { + # generate initial design + generate_design = switch(pv$design_function, + "random" = generate_design_random, + "sobol" = generate_design_sobol, + "lhs" = generate_design_lhs) + + lg$debug("Generating sobol design with size %s", pv$design_size) + generate_design(inst$search_space, n = pv$design_size)$data + } else { + # use provided initial design + lg$debug("Using provided initial design with size %s", nrow(pv$initial_design)) + pv$initial_design + } + optimize_async_default(inst, self, design, n_workers = pv$n_workers) + } + ), + + active = list( + #' @template field_surrogate + surrogate = function(rhs) { + if (missing(rhs)) { + private$.surrogate + } else { + private$.surrogate = assert_r6(rhs, classes = "SurrogateLearner", null.ok = TRUE) + } + }, + + #' @template field_acq_function + acq_function = function(rhs) { + if (missing(rhs)) { + private$.acq_function + } else { + private$.acq_function = assert_r6(rhs, classes = "AcqFunction", null.ok = TRUE) + } + }, + + #' @template field_acq_optimizer + acq_optimizer = function(rhs) { + if (missing(rhs)) { + private$.acq_optimizer + } else { + private$.acq_optimizer = assert_r6(rhs, classes = "AcqOptimizer", null.ok = TRUE) + } + }, + + #' @template field_result_assigner + result_assigner = function(rhs) { + if (missing(rhs)) { + private$.result_assigner + } else { + private$.result_assigner = assert_r6(rhs, classes = "ResultAssigner", null.ok = TRUE) + } + }, + + #' @template field_param_classes + param_classes = function(rhs) { + if (missing(rhs)) { + param_classes_surrogate = c("logical" = "ParamLgl", "integer" = "ParamInt", "numeric" = "ParamDbl", "factor" = "ParamFct") + if (!is.null(self$surrogate)) { + param_classes_surrogate = param_classes_surrogate[c("logical", "integer", "numeric", "factor") %in% self$surrogate$feature_types] # surrogate has precedence over acq_function$surrogate + } + param_classes_acq_opt = if (!is.null(self$acq_optimizer)) { + self$acq_optimizer$optimizer$param_classes + } else { + c("ParamLgl", "ParamInt", "ParamDbl", "ParamFct") + } + unname(intersect(param_classes_surrogate, param_classes_acq_opt)) + } else { + stop("$param_classes is read-only.") + } + }, + + #' @template field_properties + properties = function(rhs) { + if (missing(rhs)) { + properties_loop_function = "single-crit" + properties_surrogate = "dependencies" + if (!is.null(self$surrogate)) { + if ("missings" %nin% self$surrogate$properties) { + properties_surrogate = character() + } + } + unname(c(properties_surrogate, properties_loop_function)) + } else { + stop("$properties is read-only.") + } + }, + + #' @template field_packages + packages = function(rhs) { + if (missing(rhs)) { + union(c("mlr3mbo", "rush"), c(self$acq_function$packages, self$surrogate$packages, self$acq_optimizer$optimizer$packages, self$result_assigner$packages)) + } else { + stop("$packages is read-only.") + } + } + ), + + private = list( + .surrogate = NULL, + .acq_function = NULL, + .acq_optimizer = NULL, + .result_assigner = NULL, + + .optimize = function(inst) { + lg = lgr::get_logger("bbotk") + lg$debug("Optimizer '%s' evaluates the initial design", self$id) + get_private(inst)$.eval_queue() + + lg$debug("Optimizer '%s' starts the optimization phase", self$id) + + # actual loop + while (!inst$is_terminated) { + # sample + xs = tryCatch({ + self$acq_function$surrogate$update() + self$acq_function$update() + xdt = self$acq_optimizer$optimize() + transpose_list(xdt)[[1L]] + }, mbo_error = function(mbo_error_condition) { + lg$info(paste0(class(mbo_error_condition), collapse = " / ")) + lg$info("Proposing a randomly sampled point") + xdt = generate_design_random(inst$search_space, n = 1L)$data + transpose_list(xdt)[[1L]] + }) + + # eval + get_private(inst)$.eval_point(xs) + } + + on.exit({ + tryCatch( + { + self$surrogate$update() + }, surrogate_update_error = function(error_condition) { + lg$warn("Could not update the surrogate a final time after the optimization process has terminated.") + } + ) + }) + }, + + .assign_result = function(inst) { + self$result_assigner$assign_result(inst) + } + ) +) + +#' @include aaa.R +optimizers[["async_mbo"]] = OptimizerAsyncMbo diff --git a/R/OptimizerMbo.R b/R/OptimizerMbo.R index fa1c7547..745f87a4 100644 --- a/R/OptimizerMbo.R +++ b/R/OptimizerMbo.R @@ -13,7 +13,7 @@ #' #' Detailed descriptions of different MBO flavors are provided in the documentation of the respective [loop_function]. #' -#' Termination is handled via a [bbotk::Terminator] part of the [bbotk::OptimInstance] to be optimized. +#' Termination is handled via a [bbotk::Terminator] part of the [bbotk::OptimInstanceBatch] to be optimized. #' #' Note that in general the [Surrogate] is updated one final time on all available data after the optimization process has terminated. #' However, in certain scenarios this is not always possible or meaningful, e.g., when using [bayesopt_parego()] for multi-objective optimization @@ -22,11 +22,14 @@ #' sure that it has been properly updated on all available data. #' If this final update of the [Surrogate] could not be performed successfully, a warning will be logged. #' +#' By specifying a [ResultAssigner], one can alter how the final result is determined after optimization, e.g., +#' simply based on the evaluations logged in the archive [ResultAssignerArchive] or based on the [Surrogate] via [ResultAssignerSurrogate]. +#' #' @section Archive: -#' The [bbotk::Archive] holds the following additional columns that are specific to MBO algorithms: -#' * `[acq_function$id]` (`numeric(1)`)\cr +#' The [bbotk::ArchiveBatch] holds the following additional columns that are specific to MBO algorithms: +#' * `acq_function$id` (`numeric(1)`)\cr #' The value of the acquisition function. -#' * `.already_evaluated` (`logical(1))`\cr +#' * `".already_evaluated"` (`logical(1))`\cr #' Whether this point was already evaluated. Depends on the `skip_already_evaluated` parameter of the [AcqOptimizer]. #' @export #' @examples @@ -97,13 +100,13 @@ OptimizerMbo = R6Class("OptimizerMbo", #' #' If `surrogate` is `NULL` and the `acq_function$surrogate` field is populated, this [Surrogate] is used. #' Otherwise, `default_surrogate(instance)` is used. - #' If `acq_function` is NULL and the `acq_optimizer$acq_function` field is populated, this [AcqFunction] is used (and therefore its `$surrogate` if populated; see above). + #' If `acq_function` is `NULL` and the `acq_optimizer$acq_function` field is populated, this [AcqFunction] is used (and therefore its `$surrogate` if populated; see above). #' Otherwise `default_acqfunction(instance)` is used. - #' If `acq_optimizer` is NULL, `default_acqoptimizer(instance)` is used. + #' If `acq_optimizer` is `NULL`, `default_acqoptimizer(instance)` is used. #' - #' Even if already initialized, the `surrogate$archive` field will always be overwritten by the [bbotk::Archive] of the current [bbotk::OptimInstance] to be optimized. + #' Even if already initialized, the `surrogate$archive` field will always be overwritten by the [bbotk::ArchiveBatch] of the current [bbotk::OptimInstanceBatch] to be optimized. #' - #' For more information on default values for `loop_function`, `surrogate`, `acq_function` and `acq_optimizer`, see `?mbo_defaults`. + #' For more information on default values for `loop_function`, `surrogate`, `acq_function`, `acq_optimizer` and `result_assigner`, see `?mbo_defaults`. #' #' @template param_loop_function #' @template param_surrogate @@ -146,6 +149,7 @@ OptimizerMbo = R6Class("OptimizerMbo", catn(str_indent("* Surrogate:", if (is.null(self$surrogate)) "-" else self$surrogate$print_id)) catn(str_indent("* Acquisition Function:", if (is.null(self$acq_function)) "-" else class(self$acq_function)[1L])) catn(str_indent("* Acquisition Function Optimizer:", if (is.null(self$acq_optimizer)) "-" else self$acq_optimizer$print_id)) + catn(str_indent("* Result Assigner:", if (is.null(self$result_assigner)) "-" else class(self$result_assigner)[1L])) }, #' @description @@ -159,6 +163,50 @@ OptimizerMbo = R6Class("OptimizerMbo", private$.acq_optimizer = NULL private$.args = NULL private$.result_assigner = NULL + }, + + #' @description + #' Performs the optimization and writes optimization result into [bbotk::OptimInstanceBatch]. + #' The optimization result is returned but the complete optimization path is stored in [bbotk::ArchiveBatch] of [bbotk::OptimInstanceBatch]. + #' + #' @param inst ([bbotk::OptimInstanceBatch]). + #' @return [data.table::data.table]. + optimize = function(inst) { + # FIXME: this needs more checks for edge cases like eips or loop_function bayesopt_parego then default_surrogate should use one learner + + if (is.null(self$loop_function)) { + self$loop_function = default_loop_function(inst) + } + + if (is.null(self$acq_function)) { # acq_optimizer$acq_function has precedence + self$acq_function = self$acq_optimizer$acq_function %??% default_acqfunction(inst) + } + + if (is.null(self$surrogate)) { # acq_function$surrogate has precedence + self$surrogate = self$acq_function$surrogate %??% default_surrogate(inst) + } + + if (is.null(self$acq_optimizer)) { + self$acq_optimizer = default_acqoptimizer(self$acq_function) + } + + if (is.null(self$result_assigner)) { + self$result_assigner = default_result_assigner(inst) + } + + self$surrogate$reset() + self$acq_function$reset() + self$acq_optimizer$reset() + + self$surrogate$archive = inst$archive + self$acq_function$surrogate = self$surrogate + self$acq_optimizer$acq_function = self$acq_function + + # FIXME: if result_assigner is for example ResultAssignerSurrogate the surrogate won't be set automatically + + check_packages_installed(self$packages, msg = sprintf("Package '%%s' required but not installed for Optimizer '%s'", format(self))) + + optimize_batch_default(inst, self) } ), @@ -280,36 +328,6 @@ OptimizerMbo = R6Class("OptimizerMbo", .result_assigner = NULL, .optimize = function(inst) { - # FIXME: this needs more checks for edge cases like eips or loop_function bayesopt_parego then default_surrogate should use one learner - - if (is.null(self$loop_function)) { - self$loop_function = default_loop_function(inst) - } - - if (is.null(self$acq_function)) { # acq_optimizer$acq_function has precedence - self$acq_function = self$acq_optimizer$acq_function %??% default_acqfunction(inst) - } - - if (is.null(self$surrogate)) { # acq_function$surrogate has precedence - self$surrogate = self$acq_function$surrogate %??% default_surrogate(inst) - } - - if (is.null(self$acq_optimizer)) { - self$acq_optimizer = default_acqoptimizer(self$acq_function) - } - - if (is.null(self$result_assigner)) { - self$result_assigner = default_result_assigner(inst) - } - - self$surrogate$archive = inst$archive - self$acq_function$surrogate = self$surrogate - self$acq_optimizer$acq_function = self$acq_function - - # FIXME: if result_assigner is for example ResultAssignerSurrogate the surrogate won't be set automatically - - check_packages_installed(self$packages, msg = sprintf("Package '%%s' required but not installed for Optimizer '%s'", format(self))) - invoke(self$loop_function, instance = inst, surrogate = self$surrogate, acq_function = self$acq_function, acq_optimizer = self$acq_optimizer, .args = self$args) on.exit({ @@ -317,8 +335,8 @@ OptimizerMbo = R6Class("OptimizerMbo", { self$surrogate$update() }, surrogate_update_error = function(error_condition) { - logger = lgr::get_logger("bbotk") - logger$warn("Could not update the surrogate a final time after the optimization process has terminated.") + lg = lgr::get_logger("bbotk") + lg$warn("Could not update the surrogate a final time after the optimization process has terminated.") } ) }) diff --git a/R/ResultAssigner.R b/R/ResultAssigner.R index e6f6997f..bcb664a6 100644 --- a/R/ResultAssigner.R +++ b/R/ResultAssigner.R @@ -28,7 +28,7 @@ ResultAssigner = R6Class("ResultAssigner", #' @description #' Assigns the result, i.e., the final point(s) to the instance. #' - #' @param instance ([bbotk::OptimInstanceBatchSingleCrit] | [bbotk::OptimInstanceBatchMultiCrit])\cr + #' @param instance ([bbotk::OptimInstanceBatchSingleCrit] | [bbotk::OptimInstanceBatchMultiCrit] |[bbotk::OptimInstanceAsyncSingleCrit] | [bbotk::OptimInstanceAsyncMultiCrit])\cr #' The [bbotk::OptimInstance] the final result should be assigned to. assign_result = function(instance) { stop("Abstract.") diff --git a/R/ResultAssignerArchive.R b/R/ResultAssignerArchive.R index 12370377..da70d173 100644 --- a/R/ResultAssignerArchive.R +++ b/R/ResultAssignerArchive.R @@ -26,23 +26,21 @@ ResultAssignerArchive = R6Class("ResultAssignerArchive", #' @description #' Assigns the result, i.e., the final point(s) to the instance. #' - #' @param instance ([bbotk::OptimInstanceBatchSingleCrit] | [bbotk::OptimInstanceBatchMultiCrit])\cr + #' @param instance ([bbotk::OptimInstanceBatchSingleCrit] | [bbotk::OptimInstanceBatchMultiCrit] |[bbotk::OptimInstanceAsyncSingleCrit] | [bbotk::OptimInstanceAsyncMultiCrit])\cr #' The [bbotk::OptimInstance] the final result should be assigned to. assign_result = function(instance) { xydt = instance$archive$best() cols_x = instance$archive$cols_x cols_y = instance$archive$cols_y - xdt = xydt[, cols_x, with = FALSE] extra = xydt[, !c(cols_x, cols_y), with = FALSE] - - if (inherits(instance, "OptimInstanceBatchMultiCrit")) { + if (inherits(instance, c("OptimInstanceBatchMultiCrit", "OptimInstanceAsyncMultiCrit"))) { ydt = xydt[, cols_y, with = FALSE] instance$assign_result(xdt, ydt, extra = extra) } else { y = unlist(xydt[, cols_y, with = FALSE]) - instance$assign_result(xdt, y, extra = extra) + instance$assign_result(xdt = xdt, y = y, extra = extra) } } ), diff --git a/R/ResultAssignerSurrogate.R b/R/ResultAssignerSurrogate.R index 59c3aa95..3f84e382 100644 --- a/R/ResultAssignerSurrogate.R +++ b/R/ResultAssignerSurrogate.R @@ -7,7 +7,7 @@ #' Result assigner that chooses the final point(s) based on a surrogate mean prediction of all evaluated points in the [bbotk::Archive]. #' This is especially useful in the case of noisy objective functions. #' -#' In the case of operating on an [bbotk::OptimInstanceBatchMultiCrit] the [SurrogateLearnerCollection] must use as many learners as there are objective functions. +#' In the case of operating on an [bbotk::OptimInstanceBatchMultiCrit] or [bbotk::OptimInstanceAsyncMultiCrit] the [SurrogateLearnerCollection] must use as many learners as there are objective functions. #' #' @family Result Assigner #' @export @@ -32,15 +32,15 @@ ResultAssignerSurrogate = R6Class("ResultAssignerSurrogate", #' Assigns the result, i.e., the final point(s) to the instance. #' If `$surrogate` is `NULL`, `default_surrogate(instance)` is used and also assigned to `$surrogate`. #' - #' @param instance ([bbotk::OptimInstanceBatchSingleCrit] | [bbotk::OptimInstanceBatchMultiCrit])\cr + #' @param instance ([bbotk::OptimInstanceBatchSingleCrit] | [bbotk::OptimInstanceBatchMultiCrit] |[bbotk::OptimInstanceAsyncSingleCrit] | [bbotk::OptimInstanceAsyncMultiCrit])\cr #' The [bbotk::OptimInstance] the final result should be assigned to. assign_result = function(instance) { if (is.null(self$surrogate)) { self$surrogate = default_surrogate(instance) } - if (inherits(instance, "OptimInstanceBatchSingleCrit")) { + if (inherits(instance, c("OptimInstanceBatchSingleCrit", "OptimInstanceAsyncSingleCrit"))) { assert_r6(self$surrogate, classes = "SurrogateLearner") - } else if (inherits(instance, "OptimInstanceBatchMultiCrit")) { + } else if (inherits(instance, c("OptimInstanceBatchMultiCrit", "OptimInstanceAsyncMultiCrit"))) { assert_r6(self$surrogate, classes = "SurrogateLearnerCollection") if (self$surrogate$n_learner != instance$objective$ydim) { stopf("Surrogate used within the result assigner uses %i learners but the optimization instance has %i objective functions", self$surrogate$n_learner, instance$objective$ydim) @@ -60,16 +60,18 @@ ResultAssignerSurrogate = R6Class("ResultAssignerSurrogate", archive_tmp = archive$clone(deep = TRUE) archive_tmp$data[, self$surrogate$cols_y := means] xydt = archive_tmp$best() - extra = xydt[, !c(archive_tmp$cols_x, archive_tmp$cols_y), with = FALSE] - best = xydt[, archive_tmp$cols_x, with = FALSE] + cols_x = archive_tmp$cols_x + cols_y = archive_tmp$cols_y + best = xydt[, cols_x, with = FALSE] + extra = xydt[, !c(cols_x, cols_y), with = FALSE] # ys are still the ones originally evaluated - best_y = if (inherits(instance, "OptimInstanceBatchSingleCrit")) { - unlist(archive$data[best, on = archive$cols_x][, archive$cols_y, with = FALSE]) - } else if (inherits(instance, "OptimInstanceBatchMultiCrit")) { - archive$data[best, on = archive$cols_x][, archive$cols_y, with = FALSE] + best_y = if (inherits(instance, c("OptimInstanceBatchSingleCrit", "OptimInstanceAsyncSingleCrit"))) { + unlist(archive$data[best, on = cols_x][, cols_y, with = FALSE]) + } else if (inherits(instance, c("OptimInstanceBatchMultiCrit", "OptimInstanceAsyncMultiCrit"))) { + archive$data[best, on = cols_x][, cols_y, with = FALSE] } - instance$assign_result(xdt = best, best_y, extra = extra) + instance$assign_result(xdt = best, y = best_y, extra = extra) } ), diff --git a/R/Surrogate.R b/R/Surrogate.R index a3c6e2c0..00d2752f 100644 --- a/R/Surrogate.R +++ b/R/Surrogate.R @@ -36,25 +36,48 @@ Surrogate = R6Class("Surrogate", #' @description #' Train learner with new data. - #' Subclasses must implement `$private.update()`. + #' Subclasses must implement `private.update()` and `private.update_async()`. #' #' @return `NULL`. update = function() { if (is.null(self$archive)) stop("Archive must be set during construction or manually prior before calling $update().") if (self$param_set$values$catch_errors) { - tryCatch(private$.update(), - error = function(error_condition) { - lg$warn(error_condition$message) - stop(set_class(list(message = error_condition$message, call = NULL), - classes = c("surrogate_update_error", "mbo_error", "error", "condition"))) - } - ) + if (self$archive_is_async) { + tryCatch(private$.update_async(), + error = function(error_condition) { + lg$warn(error_condition$message) + stop(set_class(list(message = error_condition$message, call = NULL), + classes = c("surrogate_update_error", "mbo_error", "error", "condition"))) + } + ) + } else { + tryCatch(private$.update(), + error = function(error_condition) { + lg$warn(error_condition$message) + stop(set_class(list(message = error_condition$message, call = NULL), + classes = c("surrogate_update_error", "mbo_error", "error", "condition"))) + } + ) + } } else { - private$.update() + if (self$archive_is_async) { + private$.update_async() + } else { + private$.update() + } } invisible(NULL) }, + #' @description + #' Reset the surrogate model. + #' Subclasses must implement `private$.reset()`. + #' + #' @return `NULL` + reset = function() { + private$.reset() + }, + #' @description #' Predict mean response and standard error. #' Must be implemented by subclasses. @@ -106,6 +129,15 @@ Surrogate = R6Class("Surrogate", } }, + #' @template field_archive_surrogate_is_async + archive_is_async = function(rhs) { + if (missing(rhs)) { + inherits(private$.archive, "ArchiveAsync") + } else { + stop("$archive_is_async is read-only.") + } + }, + #' @template field_n_learner_surrogate n_learner = function() { stop("Abstract.") @@ -207,6 +239,10 @@ Surrogate = R6Class("Surrogate", stop("Abstract.") }, + .update_async = function() { + stop("Abstract.") + }, + deep_clone = function(name, value) { switch(name, .param_set = value$clone(deep = TRUE), diff --git a/R/SurrogateLearner.R b/R/SurrogateLearner.R index fb5681e4..18abc334 100644 --- a/R/SurrogateLearner.R +++ b/R/SurrogateLearner.R @@ -26,6 +26,11 @@ #' the failed acquisition function optimization (as a result of the failed surrogate) appropriately by, e.g., proposing a randomly sampled point for evaluation? #' Default is `TRUE`. #' } +#' \item{`impute_method`}{`character(1)`\cr +#' Method to impute missing values in the case of updating on an asynchronous [bbotk::ArchiveAsync] with pending evaluations. +#' Can be `"mean"` to use mean imputation or `"random"` to sample values uniformly at random between the empirical minimum and maximum. +#' Default is `"random"`. +#' } #' } #' #' @export @@ -87,9 +92,10 @@ SurrogateLearner = R6Class("SurrogateLearner", assert_insample_perf = p_lgl(), perf_measure = p_uty(custom_check = function(x) check_r6(x, classes = "MeasureRegr")), # FIXME: actually want check_measure perf_threshold = p_dbl(lower = -Inf, upper = Inf), - catch_errors = p_lgl() + catch_errors = p_lgl(), + impute_method = p_fct(c("mean", "random"), default = "random") ) - ps$values = list(assert_insample_perf = FALSE, catch_errors = TRUE) + ps$values = list(assert_insample_perf = FALSE, catch_errors = TRUE, impute_method = "random") ps$add_dep("perf_measure", on = "assert_insample_perf", cond = CondEqual$new(TRUE)) ps$add_dep("perf_threshold", on = "assert_insample_perf", cond = CondEqual$new(TRUE)) @@ -226,6 +232,36 @@ SurrogateLearner = R6Class("SurrogateLearner", } }, + # Train learner with new data. + # Operates on an asynchronous archive and performs imputation as needed. + # Also calculates the insample performance based on the `perf_measure` hyperparameter if `assert_insample_perf = TRUE`. + .update_async = function() { + xydt = self$archive$rush$fetch_tasks_with_state(states = c("queued", "running", "finished"))[, c(self$cols_x, self$cols_y, "state"), with = FALSE] + if (self$param_set$values$impute_method == "mean") { + mean_y = mean(xydt[[self$cols_y]], na.rm = TRUE) + xydt[c("queued", "running"), (self$cols_y) := mean_y, on = "state"] + } else if (self$param_set$values$impute_method == "random") { + min_y = min(xydt[[self$cols_y]], na.rm = TRUE) + max_y = max(xydt[[self$cols_y]], na.rm = TRUE) + xydt[c("queued", "running"), (self$cols_y) := runif(.N, min = min_y, max = max_y), on = "state"] + } + set(xydt, j = "state", value = NULL) + + task = TaskRegr$new(id = "surrogate_task", backend = xydt, target = self$cols_y) + assert_learnable(task, learner = self$learner) + self$learner$train(task) + + if (self$param_set$values$assert_insample_perf) { + measure = assert_measure(self$param_set$values$perf_measure %??% mlr_measures$get("regr.rsq"), task = task, learner = self$learner) + private$.insample_perf = self$learner$predict(task)$score(measure, task = task, learner = self$learner) + self$assert_insample_perf + } + }, + + .reset = function() { + self$learner$reset() + }, + deep_clone = function(name, value) { switch(name, learner = value$clone(deep = TRUE), diff --git a/R/SurrogateLearnerCollection.R b/R/SurrogateLearnerCollection.R index 7ae73662..74aacff4 100644 --- a/R/SurrogateLearnerCollection.R +++ b/R/SurrogateLearnerCollection.R @@ -28,6 +28,11 @@ #' the failed acquisition function optimization (as a result of the failed surrogate) appropriately by, e.g., proposing a randomly sampled point for evaluation? #' Default is `TRUE`. #' } +#' \item{`impute_method`}{`character(1)`\cr +#' Method to impute missing values in the case of updating on an asynchronous [bbotk::ArchiveAsync] with pending evaluations. +#' Can be `"mean"` to use mean imputation or `"random"` to sample values uniformly at random between the empirical minimum and maximum. +#' Default is `"random"`. +#' } #' } #' #' @export @@ -64,6 +69,8 @@ #' #' surrogate$learner #' +#' surrogate$learner[["y1"]]$model +#' #' surrogate$learner[["y2"]]$model #' } SurrogateLearnerCollection = R6Class("SurrogateLearnerCollection", @@ -100,9 +107,10 @@ SurrogateLearnerCollection = R6Class("SurrogateLearnerCollection", assert_insample_perf = p_lgl(), perf_measures = p_uty(custom_check = function(x) check_list(x, types = "MeasureRegr", any.missing = FALSE, len = length(learners))), # FIXME: actually want check_measures perf_thresholds = p_uty(custom_check = function(x) check_double(x, lower = -Inf, upper = Inf, any.missing = FALSE, len = length(learners))), - catch_errors = p_lgl() + catch_errors = p_lgl(), + impute_method = p_fct(c("mean", "random"), default = "random") ) - ps$values = list(assert_insample_perf = FALSE, catch_errors = TRUE) + ps$values = list(assert_insample_perf = FALSE, catch_errors = TRUE, impute_method = "random") ps$add_dep("perf_measures", on = "assert_insample_perf", cond = CondEqual$new(TRUE)) ps$add_dep("perf_thresholds", on = "assert_insample_perf", cond = CondEqual$new(TRUE)) @@ -259,6 +267,66 @@ SurrogateLearnerCollection = R6Class("SurrogateLearnerCollection", } }, + # Train learner with new data. + # Operates on an asynchronous archive and performs imputation as needed. + # Also calculates the insample performance based on the `perf_measures` hyperparameter if `assert_insample_perf = TRUE`. + .update_async = function() { + assert_true((length(self$cols_y) == length(self$learner)) || length(self$cols_y) == 1L) # either as many cols_y as learner or only one + one_to_multiple = length(self$cols_y) == 1L + + xydt = self$archive$rush$fetch_tasks_with_state(states = c("queued", "running", "finished"))[, c(self$cols_x, self$cols_y, "state"), with = FALSE] + if (self$param_set$values$impute_method == "mean") { + walk(self$cols_y, function(col) { + mean_y = mean(xydt[[col]], na.rm = TRUE) + xydt[c("queued", "running"), (col) := mean_y, on = "state"] + }) + } else if (self$param_set$values$impute_method == "random") { + walk(self$cols_y, function(col) { + min_y = min(xydt[[col]], na.rm = TRUE) + max_y = max(xydt[[col]], na.rm = TRUE) + xydt[c("queued", "running"), (col) := runif(.N, min = min_y, max = max_y), on = "state"] + }) + } + set(xydt, j = "state", value = NULL) + + features = setdiff(names(xydt), self$cols_y) + tasks = lapply(self$cols_y, function(col_y) { + # if this turns out to be a bottleneck, we can also operate on a single task here + task = TaskRegr$new(id = paste0("surrogate_task_", col_y), backend = xydt[, c(features, col_y), with = FALSE], target = col_y) + task + }) + if (one_to_multiple) { + tasks = replicate(length(self$learner), tasks[[1L]]) + } + pmap(list(learner = self$learner, task = tasks), .f = function(learner, task) { + assert_learnable(task, learner = learner) + learner$train(task) + invisible(NULL) + }) + + if (one_to_multiple) { + names(self$learner) = rep(self$cols_y, length(self$learner)) + } else { + names(self$learner) = self$cols_y + } + + if (self$param_set$values$assert_insample_perf) { + private$.insample_perf = setNames(pmap_dbl(list(learner = self$learner, task = tasks, perf_measure = self$param_set$values$perf_measures %??% replicate(self$n_learner, mlr_measures$get("regr.rsq"), simplify = FALSE)), + .f = function(learner, task, perf_measure) { + assert_measure(perf_measure, task = task, learner = learner) + learner$predict(task)$score(perf_measure, task = task, learner = learner) + } + ), nm = map_chr(self$param_set$values$perf_measures, "id")) + self$assert_insample_perf + } + }, + + .reset = function() { + for (learner in self$learner) { + learner$reset() + } + }, + deep_clone = function(name, value) { switch(name, learner = map(value, function(x) x$clone(deep = TRUE)), diff --git a/R/TunerADBO.R b/R/TunerADBO.R new file mode 100644 index 00000000..cac3c0fc --- /dev/null +++ b/R/TunerADBO.R @@ -0,0 +1,168 @@ +#' @title TunerAsync using Asynchronous Decentralized Bayesian Optimization +#' @name mlr_tuners_adbo +#' +#' @description +#' `TunerADBO` class that implements Asynchronous Decentralized Bayesian Optimization (ADBO). +#' ADBO is a variant of Asynchronous Model Based Optimization (AMBO) that uses [AcqFunctionStochasticCB] with exponential lambda decay. +#' This is a minimal interface internally passing on to [OptimizerAsyncMbo]. +#' For additional information and documentation see [OptimizerAsyncMbo]. +#' +#' Currently, only single-objective optimization is supported and `TunerADBO` is considered an experimental feature and API might be subject to changes. +#' +#' @section Parameters: +#' \describe{ +#' \item{`initial_design`}{`data.table::data.table()`\cr +#' Initial design of the optimization. +#' If `NULL`, a design of size `design_size` is generated with the specified `design_function`. +#' Default is `NULL`.} +#' \item{`design_size`}{`integer(1)`\cr +#' Size of the initial design if it is to be generated. +#' Default is `100`.} +#' \item{`design_function`}{`character(1)`\cr +#' Sampling function to generate the initial design. +#' Can be `random` [paradox::generate_design_random], `lhs` [paradox::generate_design_lhs], or `sobol` [paradox::generate_design_sobol]. +#' Default is `sobol`.} +#' \item{`n_workers`}{`integer(1)`\cr +#' Number of parallel workers. +#' If `NULL`, all rush workers specified via [rush::rush_plan()] are used. +#' Default is `NULL`.} +#' } +#' +#' @references +#' * `r format_bib("egele_2023")` +#' +#' @export +#' @examples +#' \donttest{ +#' if (requireNamespace("rush") & +#' requireNamespace("mlr3learners") & +#' requireNamespace("DiceKriging") & +#' requireNamespace("rgenoud")) { +#' +#' library(mlr3) +#' library(mlr3tuning) +#' +#' # single-objective +#' task = tsk("wine") +#' learner = lrn("classif.rpart", cp = to_tune(lower = 1e-4, upper = 1, logscale = TRUE)) +#' resampling = rsmp("cv", folds = 3) +#' measure = msr("classif.acc") +#' +#' instance = TuningInstanceAsyncSingleCrit$new( +#' task = task, +#' learner = learner, +#' resampling = resampling, +#' measure = measure, +#' terminator = trm("evals", n_evals = 10)) +#' +#' rush::rush_plan(n_workers=2) +#' +#' tnr("adbo", design_size = 4, n_workers = 2)$optimize(instance) +#' } +#' } +TunerADBO = R6Class("TunerADBO", + inherit = mlr3tuning::TunerAsyncFromOptimizerAsync, + + public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + initialize = function() { + optimizer = OptimizerADBO$new() + + super$initialize(optimizer = optimizer, man = "mlr3mbo::TunerADBO") + }, + + #' @description + #' Print method. + #' + #' @return (`character()`). + print = function() { + catn(format(self), if (is.na(self$label)) "" else paste0(": ", self$label)) + #catn(str_indent("* Parameters:", as_short_string(self$param_set$values))) + catn(str_indent("* Parameter classes:", self$param_classes)) + catn(str_indent("* Properties:", self$properties)) + catn(str_indent("* Packages:", self$packages)) + catn(str_indent("* Surrogate:", if (is.null(self$surrogate)) "-" else self$surrogate$print_id)) + catn(str_indent("* Acquisition Function:", if (is.null(self$acq_function)) "-" else class(self$acq_function)[1L])) + catn(str_indent("* Acquisition Function Optimizer:", if (is.null(self$acq_optimizer)) "-" else self$acq_optimizer$print_id)) + catn(str_indent("* Result Assigner:", if (is.null(self$result_assigner)) "-" else class(self$result_assigner)[1L])) + }, + + #' @description + #' Reset the tuner. + #' Sets the following fields to `NULL`: + #' `surrogate`, `acq_function`, `acq_optimizer`, `result_assigner` + #' Resets parameter values `design_size` and `design_function` to their defaults. + reset = function() { + private$.optimizer$reset() + } + ), + + active = list( + #' @template field_surrogate + surrogate = function(rhs) { + if (missing(rhs)) { + private$.optimizer$surrogate + } else { + private$.optimizer$surrogate = assert_r6(rhs, classes = "Surrogate", null.ok = TRUE) + } + }, + + #' @template field_acq_function + acq_function = function(rhs) { + if (missing(rhs)) { + private$.optimizer$acq_function + } else { + private$.optimizer$acq_function = assert_r6(rhs, classes = "AcqFunction", null.ok = TRUE) + } + }, + + #' @template field_acq_optimizer + acq_optimizer = function(rhs) { + if (missing(rhs)) { + private$.optimizer$acq_optimizer + } else { + private$.optimizer$acq_optimizer = assert_r6(rhs, classes = "AcqOptimizer", null.ok = TRUE) + } + }, + + #' @template field_result_assigner + result_assigner = function(rhs) { + if (missing(rhs)) { + private$.optimizer$result_assigner + } else { + private$.optimizer$result_assigner = assert_r6(rhs, classes = "ResultAssigner", null.ok = TRUE) + } + }, + + #' @template field_param_classes + param_classes = function(rhs) { + if (missing(rhs)) { + private$.optimizer$param_classes + } else { + stop("$param_classes is read-only.") + } + }, + + #' @template field_properties + properties = function(rhs) { + if (missing(rhs)) { + private$.optimizer$properties + } else { + stop("$properties is read-only.") + } + }, + + #' @template field_packages + packages = function(rhs) { + if (missing(rhs)) { + private$.optimizer$packages + } else { + stop("$packages is read-only.") + } + } + ) +) + +#' @include aaa.R +tuners[["adbo"]] = TunerADBO diff --git a/R/TunerAsyncMbo.R b/R/TunerAsyncMbo.R new file mode 100644 index 00000000..3843cbbc --- /dev/null +++ b/R/TunerAsyncMbo.R @@ -0,0 +1,177 @@ +#' @title TunerAsync using Asynchronous Model Based Optimization +#' +#' @include OptimizerAsyncMbo.R +#' @name mlr_tuners_async_mbo +#' +#' @description +#' `TunerAsyncMbo` class that implements Asynchronous Model Based Optimization (AMBO). +#' This is a minimal interface internally passing on to [OptimizerAsyncMbo]. +#' For additional information and documentation see [OptimizerAsyncMbo]. +#' +#' Currently, only single-objective optimization is supported and `TunerAsyncMbo` is considered an experimental feature and API might be subject to changes. +#' +#' @section Parameters: +#' \describe{ +#' \item{`initial_design`}{`data.table::data.table()`\cr +#' Initial design of the optimization. +#' If `NULL`, a design of size `design_size` is generated with the specified `design_function`. +#' Default is `NULL`.} +#' \item{`design_size`}{`integer(1)`\cr +#' Size of the initial design if it is to be generated. +#' Default is `100`.} +#' \item{`design_function`}{`character(1)`\cr +#' Sampling function to generate the initial design. +#' Can be `random` [paradox::generate_design_random], `lhs` [paradox::generate_design_lhs], or `sobol` [paradox::generate_design_sobol]. +#' Default is `sobol`.} +#' \item{`n_workers`}{`integer(1)`\cr +#' Number of parallel workers. +#' If `NULL`, all rush workers specified via [rush::rush_plan()] are used. +#' Default is `NULL`.} +#' } +#' +#' @export +#' @examples +#' \donttest{ +#' if (requireNamespace("rush") & +#' requireNamespace("mlr3learners") & +#' requireNamespace("DiceKriging") & +#' requireNamespace("rgenoud")) { +#' +#' library(mlr3) +#' library(mlr3tuning) +#' +#' # single-objective +#' task = tsk("wine") +#' learner = lrn("classif.rpart", cp = to_tune(lower = 1e-4, upper = 1, logscale = TRUE)) +#' resampling = rsmp("cv", folds = 3) +#' measure = msr("classif.acc") +#' +#' instance = TuningInstanceAsyncSingleCrit$new( +#' task = task, +#' learner = learner, +#' resampling = resampling, +#' measure = measure, +#' terminator = trm("evals", n_evals = 10)) +#' +#' rush::rush_plan(n_workers=2) +#' +#' tnr("async_mbo", design_size = 4, n_workers = 2)$optimize(instance) +#' } +#' } +TunerAsyncMbo = R6Class("TunerAsyncMbo", + inherit = mlr3tuning::TunerAsyncFromOptimizerAsync, + + public = list( + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + #' For more information on default values for `surrogate`, `acq_function`, `acq_optimizer`, and `result_assigner`, see `?mbo_defaults`. + #' + #' Note that all the parameters below are simply passed to the [OptimizerAsyncMbo] and + #' the respective fields are simply (settable) active bindings to the fields of the [OptimizerAsyncMbo]. + #' + #' @template param_surrogate + #' @template param_acq_function + #' @template param_acq_optimizer + #' @template param_result_assigner + #' @param param_set ([paradox::ParamSet])\cr + #' Set of control parameters. + initialize = function(surrogate = NULL, acq_function = NULL, acq_optimizer = NULL, param_set = NULL) { + optimizer = OptimizerAsyncMbo$new(surrogate = surrogate, acq_function = acq_function, acq_optimizer = acq_optimizer, param_set = param_set) + + super$initialize(optimizer = optimizer, man = "mlr3mbo::TunerAsyncMbo") + }, + + #' @description + #' Print method. + #' + #' @return (`character()`). + print = function() { + catn(format(self), if (is.na(self$label)) "" else paste0(": ", self$label)) + #catn(str_indent("* Parameters:", as_short_string(self$param_set$values))) + catn(str_indent("* Parameter classes:", self$param_classes)) + catn(str_indent("* Properties:", self$properties)) + catn(str_indent("* Packages:", self$packages)) + catn(str_indent("* Surrogate:", if (is.null(self$surrogate)) "-" else self$surrogate$print_id)) + catn(str_indent("* Acquisition Function:", if (is.null(self$acq_function)) "-" else class(self$acq_function)[1L])) + catn(str_indent("* Acquisition Function Optimizer:", if (is.null(self$acq_optimizer)) "-" else self$acq_optimizer$print_id)) + catn(str_indent("* Result Assigner:", if (is.null(self$result_assigner)) "-" else class(self$result_assigner)[1L])) + }, + + #' @description + #' Reset the tuner. + #' Sets the following fields to `NULL`: + #' `surrogate`, `acq_function`, `acq_optimizer`, `result_assigner` + #' Resets parameter values `design_size` and `design_function` to their defaults. + reset = function() { + private$.optimizer$reset() + } + ), + + active = list( + #' @template field_surrogate + surrogate = function(rhs) { + if (missing(rhs)) { + private$.optimizer$surrogate + } else { + private$.optimizer$surrogate = assert_r6(rhs, classes = "Surrogate", null.ok = TRUE) + } + }, + + #' @template field_acq_function + acq_function = function(rhs) { + if (missing(rhs)) { + private$.optimizer$acq_function + } else { + private$.optimizer$acq_function = assert_r6(rhs, classes = "AcqFunction", null.ok = TRUE) + } + }, + + #' @template field_acq_optimizer + acq_optimizer = function(rhs) { + if (missing(rhs)) { + private$.optimizer$acq_optimizer + } else { + private$.optimizer$acq_optimizer = assert_r6(rhs, classes = "AcqOptimizer", null.ok = TRUE) + } + }, + + #' @template field_result_assigner + result_assigner = function(rhs) { + if (missing(rhs)) { + private$.optimizer$result_assigner + } else { + private$.optimizer$result_assigner = assert_r6(rhs, classes = "ResultAssigner", null.ok = TRUE) + } + }, + + #' @template field_param_classes + param_classes = function(rhs) { + if (missing(rhs)) { + private$.optimizer$param_classes + } else { + stop("$param_classes is read-only.") + } + }, + + #' @template field_properties + properties = function(rhs) { + if (missing(rhs)) { + private$.optimizer$properties + } else { + stop("$properties is read-only.") + } + }, + + #' @template field_packages + packages = function(rhs) { + if (missing(rhs)) { + private$.optimizer$packages + } else { + stop("$packages is read-only.") + } + } + ) +) + +#' @include aaa.R +tuners[["async_mbo"]] = TunerAsyncMbo diff --git a/R/TunerMbo.R b/R/TunerMbo.R index fe778ea9..25a804a0 100644 --- a/R/TunerMbo.R +++ b/R/TunerMbo.R @@ -1,5 +1,6 @@ #' @title TunerBatch using Model Based Optimization #' +#' @include OptimizerMbo.R #' @name mlr_tuners_mbo #' #' @description @@ -55,7 +56,7 @@ TunerMbo = R6Class("TunerMbo", public = list( #' @description #' Creates a new instance of this [R6][R6::R6Class] class. - #' For more information on default values for `loop_function`, `surrogate`, `acq_function` and `acq_optimizer`, see `?mbo_defaults`. + #' For more information on default values for `loop_function`, `surrogate`, `acq_function`, `acq_optimizer`, and `result_assigner`, see `?mbo_defaults`. #' #' Note that all the parameters below are simply passed to the [OptimizerMbo] and #' the respective fields are simply (settable) active bindings to the fields of the [OptimizerMbo]. @@ -67,7 +68,8 @@ TunerMbo = R6Class("TunerMbo", #' @template param_args #' @template param_result_assigner initialize = function(loop_function = NULL, surrogate = NULL, acq_function = NULL, acq_optimizer = NULL, args = NULL, result_assigner = NULL) { - super$initialize(optimizer = OptimizerMbo$new(loop_function = loop_function, surrogate = surrogate, acq_function = acq_function, acq_optimizer = acq_optimizer, args = args, result_assigner = result_assigner), man = "mlr3mbo::TunerMbo") + optimizer = OptimizerMbo$new(loop_function = loop_function, surrogate = surrogate, acq_function = acq_function, acq_optimizer = acq_optimizer, args = args, result_assigner = result_assigner) + super$initialize(optimizer = optimizer, man = "mlr3mbo::TunerMbo") }, #' @description @@ -84,6 +86,7 @@ TunerMbo = R6Class("TunerMbo", catn(str_indent("* Surrogate:", if (is.null(self$surrogate)) "-" else self$surrogate$print_id)) catn(str_indent("* Acquisition Function:", if (is.null(self$acq_function)) "-" else class(self$acq_function)[1L])) catn(str_indent("* Acquisition Function Optimizer:", if (is.null(self$acq_optimizer)) "-" else self$acq_optimizer$print_id)) + catn(str_indent("* Result Assigner:", if (is.null(self$result_assigner)) "-" else class(self$result_assigner)[1L])) }, #' @description diff --git a/R/bayesopt_ego.R b/R/bayesopt_ego.R index ca3d5736..5dff5359 100644 --- a/R/bayesopt_ego.R +++ b/R/bayesopt_ego.R @@ -14,7 +14,7 @@ #' The [bbotk::OptimInstanceBatchSingleCrit] to be optimized. #' @param init_design_size (`NULL` | `integer(1)`)\cr #' Size of the initial design. -#' If `NULL` and the [bbotk::Archive] contains no evaluations, \code{4 * d} is used with \code{d} being the +#' If `NULL` and the [bbotk::ArchiveBatch] contains no evaluations, \code{4 * d} is used with \code{d} being the #' dimensionality of the search space. #' Points are generated via a Sobol sequence. #' @param surrogate ([Surrogate])\cr @@ -34,7 +34,7 @@ #' @note #' * The `acq_function$surrogate`, even if already populated, will always be overwritten by the `surrogate`. #' * The `acq_optimizer$acq_function`, even if already populated, will always be overwritten by `acq_function`. -#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::Archive] of the [bbotk::OptimInstanceBatchSingleCrit]. +#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatchSingleCrit]. #' #' @return invisible(instance)\cr #' The original instance is modified in-place and returned invisible. diff --git a/R/bayesopt_emo.R b/R/bayesopt_emo.R index c1e96ec8..e6c97baf 100644 --- a/R/bayesopt_emo.R +++ b/R/bayesopt_emo.R @@ -15,7 +15,7 @@ #' The [bbotk::OptimInstanceBatchMultiCrit] to be optimized. #' @param init_design_size (`NULL` | `integer(1)`)\cr #' Size of the initial design. -#' If `NULL` and the [bbotk::Archive] contains no evaluations, \code{4 * d} is used with \code{d} being the +#' If `NULL` and the [bbotk::ArchiveBatch] contains no evaluations, \code{4 * d} is used with \code{d} being the #' dimensionality of the search space. #' Points are generated via a Sobol sequence. #' @param surrogate ([SurrogateLearnerCollection])\cr @@ -34,7 +34,7 @@ #' @note #' * The `acq_function$surrogate`, even if already populated, will always be overwritten by the `surrogate`. #' * The `acq_optimizer$acq_function`, even if already populated, will always be overwritten by `acq_function`. -#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::Archive] of the [bbotk::OptimInstanceBatchMultiCrit]. +#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatchMultiCrit]. #' #' @return invisible(instance)\cr #' The original instance is modified in-place and returned invisible. diff --git a/R/bayesopt_mpcl.R b/R/bayesopt_mpcl.R index 59b4850e..13cc2186 100644 --- a/R/bayesopt_mpcl.R +++ b/R/bayesopt_mpcl.R @@ -16,7 +16,7 @@ #' The [bbotk::OptimInstanceBatchSingleCrit] to be optimized. #' @param init_design_size (`NULL` | `integer(1)`)\cr #' Size of the initial design. -#' If `NULL` and the [bbotk::Archive] contains no evaluations, \code{4 * d} is used with \code{d} being the +#' If `NULL` and the [bbotk::ArchiveBatch] contains no evaluations, \code{4 * d} is used with \code{d} being the #' dimensionality of the search space. #' Points are generated via a Sobol sequence. #' @param surrogate ([Surrogate])\cr @@ -42,7 +42,7 @@ #' @note #' * The `acq_function$surrogate`, even if already populated, will always be overwritten by the `surrogate`. #' * The `acq_optimizer$acq_function`, even if already populated, will always be overwritten by `acq_function`. -#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::Archive] of the [bbotk::OptimInstanceBatchSingleCrit]. +#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatchSingleCrit]. #' * To make use of parallel evaluations in the case of `q > 1, the objective #' function of the [bbotk::OptimInstanceBatchSingleCrit] must be implemented accordingly. #' diff --git a/R/bayesopt_parego.R b/R/bayesopt_parego.R index 744eecb2..c89d9145 100644 --- a/R/bayesopt_parego.R +++ b/R/bayesopt_parego.R @@ -15,7 +15,7 @@ #' The [bbotk::OptimInstanceBatchMultiCrit] to be optimized. #' @param init_design_size (`NULL` | `integer(1)`)\cr #' Size of the initial design. -#' If `NULL` and the [bbotk::Archive] contains no evaluations, \code{4 * d} is used with \code{d} being the +#' If `NULL` and the [bbotk::ArchiveBatch] contains no evaluations, \code{4 * d} is used with \code{d} being the #' dimensionality of the search space. #' Points are generated via a Sobol sequence. #' @param surrogate ([SurrogateLearner])\cr @@ -44,9 +44,9 @@ #' @note #' * The `acq_function$surrogate`, even if already populated, will always be overwritten by the `surrogate`. #' * The `acq_optimizer$acq_function`, even if already populated, will always be overwritten by `acq_function`. -#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::Archive] of the [bbotk::OptimInstanceBatchMultiCrit]. +#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatchMultiCrit]. #' * The scalarizations of the objective function values are stored as the `y_scal` column in the -#' [bbotk::Archive] of the [bbotk::OptimInstanceBatchMultiCrit]. +#' [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatchMultiCrit]. #' * To make use of parallel evaluations in the case of `q > 1, the objective #' function of the [bbotk::OptimInstanceBatchMultiCrit] must be implemented accordingly. #' diff --git a/R/bayesopt_smsego.R b/R/bayesopt_smsego.R index 3f7ee08f..8d53eae9 100644 --- a/R/bayesopt_smsego.R +++ b/R/bayesopt_smsego.R @@ -14,7 +14,7 @@ #' The [bbotk::OptimInstanceBatchMultiCrit] to be optimized. #' @param init_design_size (`NULL` | `integer(1)`)\cr #' Size of the initial design. -#' If `NULL` and the [bbotk::Archive] contains no evaluations, \code{4 * d} is used with \code{d} being the +#' If `NULL` and the [bbotk::ArchiveBatch] contains no evaluations, \code{4 * d} is used with \code{d} being the #' dimensionality of the search space. #' Points are generated via a Sobol sequence. #' @param surrogate ([SurrogateLearnerCollection])\cr @@ -33,7 +33,7 @@ #' @note #' * The `acq_function$surrogate`, even if already populated, will always be overwritten by the `surrogate`. #' * The `acq_optimizer$acq_function`, even if already populated, will always be overwritten by `acq_function`. -#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::Archive] of the [bbotk::OptimInstanceBatchMultiCrit]. +#' * The `surrogate$archive`, even if already populated, will always be overwritten by the [bbotk::ArchiveBatch] of the [bbotk::OptimInstanceBatchMultiCrit]. #' * Due to the iterative computation of the epsilon within the [mlr_acqfunctions_smsego], requires the [bbotk::Terminator] of #' the [bbotk::OptimInstanceBatchMultiCrit] to be a [bbotk::TerminatorEvals]. #' diff --git a/R/bibentries.R b/R/bibentries.R index 68efb918..467d7879 100644 --- a/R/bibentries.R +++ b/R/bibentries.R @@ -111,7 +111,7 @@ bibentries = c( title = "A Multicriteria Generalization of Bayesian Global Optimization", author = "Emmerich, Michael and Yang, Kaifeng and Deutz, Andr{\\'e} and Wang, Hao and Fonseca, Carlos M.", editor = "Pardalos, Panos M. and Zhigljavsky, Anatoly and {\\v{Z}}ilinskas, Julius", - bookTitle = "Advances in Stochastic and Deterministic Global Optimization", + booktitle = "Advances in Stochastic and Deterministic Global Optimization", year = "2016", publisher = "Springer International Publishing", address = "Cham", @@ -125,6 +125,13 @@ bibentries = c( booktitle = "Parallel Problem Solving from Nature -- PPSN XVII", year = "2022", pages = "90--103" + ), + + egele_2023 = bibentry("inproceedings", + title = "Asynchronous Decentralized Bayesian Optimization for Large Scale Hyperparameter Optimization", + author = "Egel{\\'e}, Romain and Guyon, Isabelle and Vishwanath, Venkatram and Balaprakash, Prasanna", + booktitle = "2023 IEEE 19th International Conference on e-Science (e-Science)", + year = "2023", + pages = "1--10" ) ) - diff --git a/R/mbo_defaults.R b/R/mbo_defaults.R index e6bcb7f1..5ec06411 100644 --- a/R/mbo_defaults.R +++ b/R/mbo_defaults.R @@ -31,6 +31,8 @@ default_loop_function = function(instance) { bayesopt_ego } else if (inherits(instance, "OptimInstanceBatchMultiCrit")) { bayesopt_smsego + } else { + stopf("There are no loop functions for %s.", class(instance)[1L]) } } @@ -127,11 +129,8 @@ default_rf = function(noisy = FALSE) { #' In the case of dependencies, the following learner is used as a fallback: #' \code{lrn("regr.featureless")}. #' -#' If the instance is of class [bbotk::OptimInstanceBatchSingleCrit] the learner is wrapped as a -#' [SurrogateLearner]. -#' -#' If the instance is of class [bbotk::OptimInstanceBatchMultiCrit] multiple deep clones of the learner are -#' wrapped as a [SurrogateLearnerCollection]. +#' If `n_learner` is `1`, the learner is wrapped as a [SurrogateLearner]. +#' Otherwise, if `n_learner` is larger than `1`, multiple deep clones of the learner are wrapped as a [SurrogateLearnerCollection]. #' #' @references #' * `r format_bib("ding_2010")` @@ -141,19 +140,21 @@ default_rf = function(noisy = FALSE) { #' @param learner (`NULL` | [mlr3::Learner]). #' If specified, this learner will be used instead of the defaults described above. #' @param n_learner (`NULL` | `integer(1)`). -#' Number of learners to be considered in the construction of the [SurrogateLearner] or [SurrogateLearnerCollection]. +#' Number of learners to be considered in the construction of the [Surrogate]. #' If not specified will be based on the number of objectives as stated by the instance. +#' @param force_random_forest (`logical(1)`). +#' If `TRUE`, a random forest is constructed even if the parameter space is numeric-only. #' @return [Surrogate] #' @family mbo_defaults #' @export -default_surrogate = function(instance, learner = NULL, n_learner = NULL) { - assert_multi_class(instance, c("OptimInstance", "OptimInstanceAsync")) +default_surrogate = function(instance, learner = NULL, n_learner = NULL, force_random_forest = FALSE) { + assert_multi_class(instance, c("OptimInstance", "OptimInstanceBatch", "OptimInstanceAsync")) assert_r6(learner, "Learner", null.ok = TRUE) assert_int(n_learner, lower = 1L, null.ok = TRUE) noisy = "noisy" %in% instance$objective$properties if (is.null(learner)) { - is_mixed_space = !all(instance$search_space$class %in% c("ParamDbl", "ParamInt")) + is_mixed_space = !all(instance$search_space$class %in% c("ParamDbl", "ParamInt")) || force_random_forest has_deps = nrow(instance$search_space$deps) > 0L learner = if (!is_mixed_space) { default_gp(noisy) @@ -190,7 +191,7 @@ default_surrogate = function(instance, learner = NULL, n_learner = NULL) { if (is.null(n_learner)) n_learner = length(instance$archive$cols_y) if (n_learner == 1L) { SurrogateLearner$new(learner) - } else { + } else { learners = replicate(n_learner, learner$clone(deep = TRUE), simplify = FALSE) SurrogateLearnerCollection$new(learners) } @@ -200,10 +201,12 @@ default_surrogate = function(instance, learner = NULL, n_learner = NULL) { #' #' @description #' Chooses a default acquisition function, i.e. the criterion used to propose future points. -#' For single-objective optimization, defaults to [mlr_acqfunctions_ei]. -#' For multi-objective optimization, defaults to [mlr_acqfunctions_smsego]. +#' For synchronous single-objective optimization, defaults to [mlr_acqfunctions_ei]. +#' For synchronous multi-objective optimization, defaults to [mlr_acqfunctions_smsego]. +#' For asynchronous single-objective optimization, defaults to [mlr_acqfunctions_stochastic_cb]. #' #' @param instance ([bbotk::OptimInstance]). +#' An object that inherits from [bbotk::OptimInstance]. #' @return [AcqFunction] #' @family mbo_defaults #' @export @@ -211,8 +214,12 @@ default_acqfunction = function(instance) { assert_r6(instance, classes = "OptimInstance") if (inherits(instance, "OptimInstanceBatchSingleCrit")) { AcqFunctionEI$new() + } else if (inherits(instance, "OptimInstanceAsyncSingleCrit")) { + AcqFunctionStochasticCB$new() } else if (inherits(instance, "OptimInstanceBatchMultiCrit")) { AcqFunctionSmsEgo$new() + } else if (inherits(instance, "OptimInstanceAsyncMultiCrit")) { + stopf("Currently, there is no default acquisition function for %s.", class(instance)[1L]) } } diff --git a/R/sugar.R b/R/sugar.R index 0cc43de3..5dee7717 100644 --- a/R/sugar.R +++ b/R/sugar.R @@ -108,8 +108,8 @@ acqfs = function(.keys, ...) { #' @description #' This function allows to construct an [AcqOptimizer] in the spirit #' of `mlr_sugar` from \CRANpkg{mlr3}. -#' @param optimizer ([bbotk::Optimizer])\cr -#' [bbotk::Optimizer] that is to be used. +#' @param optimizer ([bbotk::OptimizerBatch])\cr +#' [bbotk::OptimizerBatch] that is to be used. #' @param terminator ([bbotk::Terminator])\cr #' [bbotk::Terminator] that is to be used. #' @param acq_function (`NULL` | [AcqFunction])\cr diff --git a/R/zzz.R b/R/zzz.R index 003b52c1..a0e1e22d 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -9,7 +9,7 @@ #' @import lgr #' @import mlr3 #' @import mlr3tuning -#' @importFrom stats setNames runif dnorm pnorm quantile +#' @importFrom stats setNames runif dnorm pnorm quantile rexp #' @useDynLib mlr3mbo c_sms_indicator c_eps_indicator "_PACKAGE" diff --git a/attic/OptimizerADBO.R b/attic/OptimizerADBO.R new file mode 100644 index 00000000..3cdd4754 --- /dev/null +++ b/attic/OptimizerADBO.R @@ -0,0 +1,152 @@ +#' @title Asynchronous Decentralized Bayesian Optimization +#' @name mlr_optimizers_adbo +#' +#' @description +#' Asynchronous Decentralized Bayesian Optimization (ADBO). +#' +#' @note +#' The \eqn{\lambda} parameter of the upper confidence bound acquisition function controls the trade-off between exploration and exploitation. +#' A large \eqn{\lambda} value leads to more exploration, while a small \eqn{\lambda} value leads to more exploitation. +#' ADBO can use periodic exponential decay to reduce \eqn{\lambda} periodically to the exploitation phase. +#' +#' @section Parameters: +#' \describe{ +#' \item{`lambda`}{`numeric(1)`\cr +#' \eqn{\lambda} value used for the confidence bound. +#' Defaults to `1.96`.} +#' \item{`exponential_decay`}{`lgl(1)`\cr +#' Whether to use periodic exponential decay for \eqn{\lambda}.} +#' \item{`rate`}{`numeric(1)`\cr +#' Rate of the exponential decay.} +#' \item{`t`}{`integer(1)`\cr +#' Period of the exponential decay.} +#' \item{`initial_design_size`}{`integer(1)`\cr +#' Size of the initial design.} +#' \item{`initial_design`}{`data.table`\cr +#' Initial design.} +#' \item{`impute_method`}{`character(1)`\cr +#' Imputation method for missing values in the surrogate model.} +#' \item{`n_workers`}{`integer(1)`\cr +#' Number of workers to use. +#' Defaults to the number of workers set by `rush::rush_plan()`} +#' } +#' +#' @export +OptimizerADBO = R6Class("OptimizerADBO", + inherit = OptimizerAsyncMbo, + + public = list( + + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + initialize = function() { + param_set = ps( + lambda = p_dbl(lower = 0, default = 1.96), + exponential_decay = p_lgl(default = TRUE), + rate = p_dbl(lower = 0, default = 0.1), + period = p_int(lower = 1L, default = 25L), + design_size = p_int(lower = 1L), + initial_design = p_uty(), + impute_method = p_fct(c("mean", "random"), default = "random"), + n_workers = p_int(lower = 1L, default = NULL, special_vals = list(NULL)) + ) + + param_set$set_values(lambda = 1.96, exponential_decay = TRUE, rate = 0.1, period = 25L, design_size = 1L, impute_method = "random") + + super$initialize("adbo", + param_set = param_set, + param_classes = c("ParamLgl", "ParamInt", "ParamDbl", "ParamFct"), + properties = c("dependencies", "single-crit"), + packages = "mlr3mbo", + label = "Asynchronous Decentralized Bayesian Optimization", + man = "mlr3mbo::OptimizerADBO") + }, + + + #' @description + #' Performs the optimization on a [OptimInstanceAsyncSingleCrit] or [OptimInstanceAsyncMultiCrit] until termination. + #' The single evaluations will be written into the [ArchiveAsync]. + #' The result will be written into the instance object. + #' + #' @param inst ([OptimInstanceAsyncSingleCrit] | [OptimInstanceAsyncMultiCrit]). + #' + #' @return [data.table::data.table()] + optimize = function(inst) { + pv = self$param_set$values + + # initial design + design = if (is.null(pv$initial_design)) { + + lg$debug("Generating sobol design with size %s", pv$design_size) + generate_design_sobol(inst$search_space, n = pv$design_size)$data + } else { + + lg$debug("Using provided initial design with size %s", nrow(pv$initial_design)) + pv$initial_design + } + + optimize_async_default(inst, self, design, n_workers = pv$n_workers) + } + ), + + private = list( + + .optimize = function(inst) { + pv = self$param_set$values + search_space = inst$search_space + archive = inst$archive + + # sample lambda from exponential distribution + lambda_0 = rexp(1, 1 / pv$lambda) + t = 0 + + surrogate = default_surrogate(inst) + surrogate$param_set$set_values(impute_method = pv$impute_method) + acq_function = acqf("cb", lambda = runif(1, 1 , 3)) + acq_optimizer = acqo(opt("random_search", batch_size = 1000L), terminator = trm("evals", n_evals = 10000L)) + surrogate$archive = inst$archive + acq_function$surrogate = surrogate + acq_optimizer$acq_function = acq_function + + lg$debug("Optimizer '%s' evaluates the initial design", self$id) + evaluate_queue_default(inst) + + lg$debug("Optimizer '%s' starts the tuning phase", self$id) + + # actual loop + while (!inst$is_terminated) { + + # decrease lambda + if (pv$exponential_decay) { + lambda = lambda_0 * exp(-pv$rate * (t %% pv$period)) + t = t + 1 + } else { + lambda = pv$lambda + } + + # sample + acq_function$constants$set_values(lambda = lambda) + acq_function$surrogate$update() + acq_function$update() + xdt = acq_optimizer$optimize() + + # transpose point + xss = transpose_list(xdt) + xs = xss[[1]][inst$archive$cols_x] + lg$trace("Optimizer '%s' draws %s", self$id, as_short_string(xs)) + xs_trafoed = trafo_xs(xs, search_space) + + # eval + key = archive$push_running_point(xs) + ys = inst$objective$eval(xs_trafoed) + + # push result + extra = c(xss[[1]][c("acq_cb", ".already_evaluated")], list(lambda_0 = lambda_0, lambda = lambda)) + archive$push_result(key, ys, x_domain = xs_trafoed, extra = extra) + } + } + ) +) + +#' @include aaa.R +optimizers[["adbo"]] = OptimizerADBO diff --git a/attic/TunerADBO.R b/attic/TunerADBO.R new file mode 100644 index 00000000..6c89074f --- /dev/null +++ b/attic/TunerADBO.R @@ -0,0 +1,48 @@ +#' @title Asynchronous Decentralized Bayesian Optimization +#' @name mlr_tuners_adbo +#' +#' @description +#' Asynchronous Decentralized Bayesian Optimization (ADBO). +#' +#' @note +#' The \eqn{\lambda} parameter of the upper confidence bound acquisition function controls the trade-off between exploration and exploitation. +#' A large \eqn{\lambda} value leads to more exploration, while a small \eqn{\lambda} value leads to more exploitation. +#' ADBO can use periodic exponential decay to reduce \eqn{\lambda} periodically to the exploitation phase. +#' +#' @section Parameters: +#' \describe{ +#' \item{`lambda`}{`numeric(1)`\cr +#' \eqn{\lambda} value used for the confidence bound. +#' Defaults to `1.96`.} +#' \item{`exponential_decay`}{`lgl(1)`\cr +#' Whether to use periodic exponential decay for \eqn{\lambda}.} +#' \item{`rate`}{`numeric(1)`\cr +#' Rate of the exponential decay.} +#' \item{`t`}{`integer(1)`\cr +#' Period of the exponential decay.} +#' \item{`initial_design_size`}{`integer(1)`\cr +#' Size of the initial design.} +#' \item{`initial_design`}{`data.table`\cr +#' Initial design.} +#' } +#' +#' @export +TunerADBO = R6Class("TunerADBO", + inherit = mlr3tuning::TunerAsyncFromOptimizerAsync, + public = list( + + #' @description + #' Creates a new instance of this [R6][R6::R6Class] class. + initialize = function() { + super$initialize( + optimizer = OptimizerADBO$new(), + man = "mlr3tuning::mlr_tuners_adbo" + ) + } + ) +) + +mlr_tuners$add("adbo", TunerADBO) + +#' @include aaa.R +tuners[["adbo"]] = TunerADBO diff --git a/attic/test_OptimizerADBO.R b/attic/test_OptimizerADBO.R new file mode 100644 index 00000000..1c91606e --- /dev/null +++ b/attic/test_OptimizerADBO.R @@ -0,0 +1,15 @@ +# test_that("adbo optimizer works", { +# skip_on_cran() +# skip_if_not_installed("rush") +# flush_redis() + +# rush::rush_plan(n_workers = 2) +# instance = oi_async( +# objective = OBJ_2D, +# search_space = PS_2D, +# terminator = trm("evals", n_evals = 100), +# ) +# optimizer = opt("adbo", design_size = 4) +# optimizer$optimize(instance) +# }) + diff --git a/attic/test_TunerADBO.R b/attic/test_TunerADBO.R new file mode 100644 index 00000000..5d677bff --- /dev/null +++ b/attic/test_TunerADBO.R @@ -0,0 +1,110 @@ +test_that("adbo tuner works", { + skip_on_cran() + skip_if_not_installed("rush") + flush_redis() + + learner = lrn("classif.rpart", + minsplit = to_tune(2, 128), + cp = to_tune(1e-04, 1e-1)) + + rush::rush_plan(n_workers = 4) + instance = ti_async( + task = tsk("pima"), + learner = learner, + resampling = rsmp("cv", folds = 3), + measure = msr("classif.ce"), + terminator = trm("evals", n_evals = 20), + store_benchmark_result = FALSE + ) + + tuner = tnr("adbo", design_size = 4) + tuner$optimize(instance) + + expect_data_table(instance$archive$data, min.rows = 20L) + expect_rush_reset(instance$rush) +}) + +test_that("adbo works with transformation functions", { + skip_on_cran() + skip_if_not_installed("rush") + flush_redis() + + learner = lrn("classif.rpart", + minsplit = to_tune(2, 128, logscale = TRUE), + cp = to_tune(1e-04, 1e-1, logscale = TRUE)) + + rush::rush_plan(n_workers = 2) + instance = ti_async( + task = tsk("pima"), + learner = learner, + resampling = rsmp("cv", folds = 3), + measure = msr("classif.ce"), + terminator = trm("evals", n_evals = 20), + store_benchmark_result = FALSE + ) + + optimizer = tnr("adbo", design_size = 4) + optimizer$optimize(instance) + + expect_data_table(instance$archive$data, min.rows = 20) + expect_rush_reset(instance$rush) +}) + +test_that("search works with dependencies", { + skip_on_cran() + skip_if_not_installed("rush") + flush_redis() + + learner = lrn("classif.rpart", + minsplit = to_tune(p_int(2, 128, depends = keep_model == TRUE)), + cp = to_tune(1e-04, 1e-1), + keep_model = to_tune()) + + rush::rush_plan(n_workers = 2) + instance = ti_async( + task = tsk("pima"), + learner = learner, + resampling = rsmp("cv", folds = 3), + measure = msr("classif.ce"), + terminator = trm("evals", n_evals = 20), + store_benchmark_result = FALSE + ) + + optimizer = tnr("adbo", design_size = 4) + optimizer$optimize(instance) + + expect_data_table(instance$archive$data, min.rows = 20) + expect_rush_reset(instance$rush) +}) + +test_that("adbo works with branching", { + skip_on_cran() + skip_if_not_installed("rush") + skip_if_not_installed("mlr3pipelines") + flush_redis() + library(mlr3pipelines) + + graph_learner = as_learner(ppl("branch", graphs = list(rpart = lrn("classif.rpart", id = "rpart"),debug = lrn("classif.debug", id = "debug")))) + graph_learner$param_set$set_values( + "rpart.cp" = to_tune(p_dbl(1e-04, 1e-1, depends = branch.selection == "rpart")), + "rpart.minsplit" = to_tune(p_int(2, 128, depends = branch.selection == "rpart")), + "debug.x" = to_tune(p_dbl(0, 1, depends = branch.selection == "debug")), + "branch.selection" = to_tune(c("rpart", "debug")) + ) + + rush::rush_plan(n_workers = 2) + instance = ti_async( + task = tsk("pima"), + learner = graph_learner, + resampling = rsmp("cv", folds = 3), + measure = msr("classif.ce"), + terminator = trm("evals", n_evals = 20), + store_benchmark_result = FALSE + ) + + optimizer = tnr("adbo", design_size = 4) + optimizer$optimize(instance) + + expect_data_table(instance$archive$data, min.rows = 20) + expect_rush_reset(instance$rush) +}) diff --git a/man-roxygen/field_archive_surrogate_is_async.R b/man-roxygen/field_archive_surrogate_is_async.R new file mode 100644 index 00000000..f835cec9 --- /dev/null +++ b/man-roxygen/field_archive_surrogate_is_async.R @@ -0,0 +1,2 @@ +#' @field archive_is_async (`bool(1)``)\cr +#' Whether the [bbotk::Archive] is an asynchronous one. diff --git a/man-roxygen/field_properties.R b/man-roxygen/field_properties.R index c9fc652b..299eb381 100644 --- a/man-roxygen/field_properties.R +++ b/man-roxygen/field_properties.R @@ -2,4 +2,4 @@ #' Set of properties of the optimizer. #' Must be a subset of [`bbotk_reflections$optimizer_properties`][bbotk::bbotk_reflections]. #' MBO in principle is very flexible and by default we assume that the optimizer has all properties. -#' When fully initialized, properties are determined based on the `loop_function` and `surrogate`. +#' When fully initialized, properties are determined based on the loop, e.g., the `loop_function`, and `surrogate`. diff --git a/man-roxygen/param_id.R b/man-roxygen/param_id.R new file mode 100644 index 00000000..1f50f0ec --- /dev/null +++ b/man-roxygen/param_id.R @@ -0,0 +1,2 @@ +#' @param id (`character(1)`)\cr +#' Identifier for the new instance. diff --git a/man-roxygen/param_label.R b/man-roxygen/param_label.R new file mode 100644 index 00000000..1a73abff --- /dev/null +++ b/man-roxygen/param_label.R @@ -0,0 +1,3 @@ +#' @param label (`character(1)`)\cr +#' Label for this object. +#' Can be used in tables, plot and text output instead of the ID. diff --git a/man-roxygen/param_man.R b/man-roxygen/param_man.R new file mode 100644 index 00000000..3625469b --- /dev/null +++ b/man-roxygen/param_man.R @@ -0,0 +1,3 @@ +#' @param man (`character(1)`)\cr +#' String in the format `[pkg]::[topic]` pointing to a manual page for this object. +#' The referenced help package can be opened via method `$help()`. diff --git a/man/AcqFunction.Rd b/man/AcqFunction.Rd index 31f16e42..24f48a24 100644 --- a/man/AcqFunction.Rd +++ b/man/AcqFunction.Rd @@ -21,7 +21,9 @@ Other Acquisition Function: \code{\link{mlr_acqfunctions_multi}}, \code{\link{mlr_acqfunctions_pi}}, \code{\link{mlr_acqfunctions_sd}}, -\code{\link{mlr_acqfunctions_smsego}} +\code{\link{mlr_acqfunctions_smsego}}, +\code{\link{mlr_acqfunctions_stochastic_cb}}, +\code{\link{mlr_acqfunctions_stochastic_ei}} } \concept{Acquisition Function} \section{Super class}{ @@ -67,6 +69,7 @@ Set of required packages.} \itemize{ \item \href{#method-AcqFunction-new}{\code{AcqFunction$new()}} \item \href{#method-AcqFunction-update}{\code{AcqFunction$update()}} +\item \href{#method-AcqFunction-reset}{\code{AcqFunction$reset()}} \item \href{#method-AcqFunction-eval_many}{\code{AcqFunction$eval_many()}} \item \href{#method-AcqFunction-eval_dt}{\code{AcqFunction$eval_dt()}} \item \href{#method-AcqFunction-clone}{\code{AcqFunction$clone()}} @@ -145,6 +148,18 @@ Can be implemented by subclasses. \if{html}{\out{
mlr3mbo::Surrogate$format()
mlr3mbo::Surrogate$print()
mlr3mbo::Surrogate$reset()
mlr3mbo::Surrogate$update()
mlr3mbo::Surrogate$format()
mlr3mbo::Surrogate$print()
mlr3mbo::Surrogate$reset()
mlr3mbo::Surrogate$update()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
mlr3mbo::AcqFunction$update()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
mlr3mbo::AcqFunction$update()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
bbotk::Objective$print()
mlr3mbo::AcqFunction$eval_dt()
mlr3mbo::AcqFunction$eval_many()
mlr3mbo::AcqFunction$reset()
mlr3mbo::AcqFunction$update()