Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add OptimizerAsyncSuccessiveHalving #116

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/r-cmd-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ jobs:
with:
r-version: ${{ matrix.config.r }}

- uses: supercharge/[email protected]
with:
redis-version: 7

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check

- uses: r-lib/actions/check-r-package@v2
with:
args: 'c("--no-manual")' # "--as-cran" prevents to start external processes
9 changes: 7 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,29 @@ Imports:
mlr3 (>= 0.13.1),
mlr3misc (>= 0.10.0),
paradox (>= 0.9.0),
R6
R6,
uuid
Suggests:
emoa,
mlr3learners (>= 0.5.2),
mlr3pipelines,
rpart,
redux,
rush,
testthat (>= 3.0.0),
xgboost
Config/testthat/edition: 3
Config/testthat/parallel: true
Encoding: UTF-8
NeedsCompilation: no
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
RoxygenNote: 7.3.2
Collate:
'OptimizerAsyncSuccessiveHalving.R'
'aaa.R'
'OptimizerBatchSuccessiveHalving.R'
'OptimizerBatchHyperband.R'
'TunerAsyncSuccessiveHalving.R'
'TunerBatchHyperband.R'
'TunerBatchSuccessiveHalving.R'
'bibentries.R'
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Generated by roxygen2: do not edit by hand

export(OptimizerAsyncSuccessiveHalving)
export(OptimizerBatchHyperband)
export(OptimizerBatchSuccessiveHalving)
export(TunerAsyncSuccessiveHalving)
export(TunerBatchHyperband)
export(TunerBatchSuccessiveHalving)
export(hyperband_budget)
Expand All @@ -17,3 +19,4 @@ import(paradox)
importFrom(R6,R6Class)
importFrom(utils,bibentry)
importFrom(utils,head)
importFrom(uuid,UUIDgenerate)
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# mlr3hyperband (development version)

* feat: Add `OptimizerAsyncSuccessiveHalving` optimizer.

# mlr3hyperband 0.6.0

* compatibility; Work with new bbotk 1.0.0 and mlr3tuning 1.0.0
* compatibility: Work with new bbotk 1.0.0 and mlr3tuning 1.0.0

# mlr3hyperband 0.5.0

Expand Down
190 changes: 190 additions & 0 deletions R/OptimizerAsyncSuccessiveHalving.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#' @title Asynchronous Hyperparameter Optimization with Successive Halving
#'
#' @name mlr_optimizers_async_successive_halving
#' @templateVar id async_successive_halving
#'
#' @description
#' `OptimizerAsyncSuccessiveHalving` class that implements the Asynchronous Successive Halving Algorithm (ASHA).
#' This class implements the asynchronous version of [OptimizerBatchSuccessiveHalving].
#'
#' @template section_dictionary_optimizers
#'
#' @section Parameters:
#' \describe{
#' \item{`eta`}{`numeric(1)`\cr
#' With every stage, the budget is increased by a factor of `eta` and only the best `1 / eta` configurations are promoted to the next stage.
#' Non-integer values are supported, but `eta` is not allowed to be less or equal to 1.}
#' \item{`sampler`}{[paradox::Sampler]\cr
#' Object defining how the samples of the parameter space should be drawn.
#' The default is uniform sampling.}
#' }
#'
#' @section Archive:
#' The [bbotk::Archive] holds the following additional columns that are specific to SHA:
#' * `stage` (`integer(1))`\cr
#' Stage index. Starts counting at 0.
#' * `asha_id` (`character(1))`\cr
#' Unique identifier for each configuration across stages.
#'
#' @template section_custom_sampler
#'
#' @source
#' `r format_bib("li_2020")`
#'
#' @export
OptimizerAsyncSuccessiveHalving = R6Class("OptimizerAsyncSuccessiveHalving",
inherit = OptimizerAsync,

public = list(

#' @description
#' Creates a new instance of this [R6][R6::R6Class] class.
initialize = function() {
param_set = ps(
eta = p_dbl(lower = 1.0001, default = 2),
sampler = p_uty(custom_check = crate({function(x) check_r6(x, "Sampler", null.ok = TRUE)})))

param_set$values = list(eta = 2, sampler = NULL)

super$initialize(
id = "async_successive_halving",
param_set = param_set,
param_classes = c("ParamLgl", "ParamInt", "ParamDbl", "ParamFct"),
properties = c("dependencies", "single-crit", "multi-crit", "async"),
packages = "rush",
label = "Asynchronous Successive Halving",
man = "bbotk::mlr_optimizers_async_successive_halving"
)
},

#' @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) {
pars = self$param_set$values
eta = pars$eta
sampler = pars$sampler
search_space = inst$search_space
budget_id = search_space$ids(tags = "budget")

# check budget
if (length(budget_id) != 1) stopf("Exactly one parameter must be tagged with 'budget'")
assert_choice(search_space$class[[budget_id]], c("ParamInt", "ParamDbl"))

# required for calculation of hypervolume
if (inst$archive$codomain$length > 1) require_namespaces("emoa")

# sampler
search_space_sampler = search_space$clone()$subset(setdiff(search_space$ids(), budget_id))
private$.sampler = if (is.null(sampler)) {
SamplerUnif$new(search_space_sampler)
} else {
assert_set_equal(sampler$param_set$ids(), search_space_sampler$ids())
sampler
}

# function to select the n best configurations
private$.top_n = if (inst$archive$codomain$length == 1) {
function(data, cols_y, n, direction) {
setorderv(data, cols = cols_y, order = direction)
head(data, n)
}
} else {
function(data, cols_y, n, direction) {
points = t(as.matrix(data[, cols_y, with = FALSE]))
ii = nds_selection(points, n, minimize = direction == 1)
data[ii]
}
}

# r_min is the budget of a single configuration in the first stage
# r_max is the maximum budget of a single configuration in the last stage
# the internal budget is rescaled to a minimum budget of 1
# for this, the budget is divided by r_min
# the budget is transformed to the original scale before passing it to the objective function
r_max = search_space$upper[[budget_id]]
private$.r_min = r_min = search_space$lower[[budget_id]]

# maximum budget of a single configuration in the last stage (scaled)
r = r_max / r_min

# number of stages if each configuration in the first stage uses the minimum budget
# and each configuration in the last stage uses no more than maximum budget
private$.s_max = floor(log(r, eta))

optimize_async_default(inst, self)
}
),

private = list(
.r_min = NULL,
.s_max = NULL,
.top_n = NULL,
.sampler = NULL,

.optimize = function(inst) {
archive = inst$archive
r_min = private$.r_min
s_max = private$.s_max
eta = self$param_set$values$eta
budget_id = inst$search_space$ids(tags = "budget")
direction = inst$archive$codomain$maximization_to_minimization

while (!inst$is_terminated) {
# sample new point xs
xdt = private$.sampler$sample(1)$data
xs = transpose_list(xdt)[[1]]

# add unique id across stages, stage number, and budget
asha_id = UUIDgenerate()
xs = c(xs, list(asha_id = asha_id, stage = 1))
xs[[budget_id]] = private$.r_min

# evaluate
get_private(inst)$.eval_point(xs)

# s_max is 0 if r_min == r_max
if (s_max > 0) {
# iterate stages
for (s in seq(s_max)) {
# fetch finished points of current stage
data_stage = archive$finished_data[list(s), , on = "stage"]

# how many configurations can be promoted to the next stage
# at least one configuration must be promotable
n_promotable = max(floor(nrow(data_stage) / eta), 1)

lg$debug("%i promotable configurations in stage %i", n_promotable, s)

# get the n best configurations of the current stage
candidates = private$.top_n(data_stage, archive$cols_y, n_promotable, direction)

# if xs is not among the best configurations of the current stage draw a new random configuration
if (asha_id %nin% candidates$asha_id) {
lg$debug("Configuration %s is not promotable to stage %i", asha_id, s + 1)
break
}

lg$debug("Configuration %s is promotable to stage %i", asha_id, s + 1)

# increase budget of xs
rs = r_min * eta^s
if (inst$search_space$class[[budget_id]] == "ParamInt") rs = round(rs)
xs[[budget_id]] = rs
xs$stage = s + 1

# evaluate
get_private(inst)$.eval_point(xs)
}
}
}
}
)
)

mlr_optimizers$add("async_successive_halving", OptimizerAsyncSuccessiveHalving)
34 changes: 34 additions & 0 deletions R/TunerAsyncSuccessiveHalving.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#' @title Asynchronous Hyperparameter Tuning with Successive Halving
#'
#' @include OptimizerAsyncSuccessiveHalving.R
#' @name mlr_tuners_async_successive_halving
#' @templateVar id async_successive_halving
#'
#' @inherit mlr_optimizers_async_successive_halving description
#' @template section_dictionary_tuners
#' @inheritSection mlr_optimizers_async_successive_halving Parameters
#' @inheritSection mlr_optimizers_async_successive_halving Archive
#' @template section_subsample_budget
#' @template section_custom_sampler
#'
#' @source
#' `r format_bib("li_2020")`
#'
#' @export
TunerAsyncSuccessiveHalving = R6Class("TunerAsyncSuccessiveHalving",
inherit = TunerAsyncFromOptimizerAsync,
public = list(

#' @description
#' Creates a new instance of this [R6][R6::R6Class] class.
initialize = function() {
super$initialize(
optimizer = OptimizerAsyncSuccessiveHalving$new(),
man = "mlr3hyperband::mlr_tuners_async_successive_halving"
)
}
)
)

#' @include aaa.R
tuners[["async_successive_halving"]] = TunerAsyncSuccessiveHalving
15 changes: 14 additions & 1 deletion R/bibentries.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,18 @@ bibentries = c(
series = "Proceedings of Machine Learning Research",
address = "Cadiz, Spain",
publisher = "PMLR",
url = "http://proceedings.mlr.press/v51/jamieson16.html")
url = "http://proceedings.mlr.press/v51/jamieson16.html"),

li_2020 = bibentry("InProceedings",
key = "li_2020",
title = "A System for Massively Parallel Hyperparameter Tuning",
author = "Liam Li and Kevin Jamieson and Afshin Rostamizadeh and Ekaterina Gonina and Jonathan Ben-tzur and Moritz Hardt and Benjamin Recht and Ameet Talwalkar",
booktitle = "Proceedings of Machine Learning and Systems",
editor = "I. Dhillon and D. Papailiopoulos and V. Sze",
pages = "230--246",
volume = "2",
year = "2020",
url = "https://proceedings.mlsys.org/paper_files/paper/2020/hash/a06f20b349c6cf09a6b171c71b88bbfc-Abstract.html")
)


2 changes: 1 addition & 1 deletion R/helper.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ hyperband_n_configs = function(r_min, r_max, eta) {
#' @template param_r_max
#' @template param_eta
#' @template param_integer_budget
#'
#'
#' @return `integer(1)`
#' @export
hyperband_budget = function(r_min, r_max, eta, integer_budget = FALSE) {
Expand Down
1 change: 1 addition & 0 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#' @import bbotk
#' @importFrom R6 R6Class
#' @importFrom utils head
#' @importFrom uuid UUIDgenerate
"_PACKAGE"

register_bbotk = function() {
Expand Down
Loading
Loading