Skip to content

Commit

Permalink
* The Optimizer object has a new method optimize which can be use…
Browse files Browse the repository at this point in the history
…d for minimization and maximization by setting the argument `direction`.

* In method `Objective$initialize()`, renamed argument `objective` -> `f`.

* In method `Objective$initialize()`, if `target = NULL` (the new default), the first argument from `f` is taken as target argument.
  • Loading branch information
loelschlaeger committed May 19, 2024
1 parent 782267e commit 3ff2d96
Show file tree
Hide file tree
Showing 18 changed files with 196 additions and 51 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: optimizeR
Title: Unified Framework for Numerical Optimizers
Version: 1.0.5
Version: 1.1.0
Authors@R: c(
person("Lennart", "Oelschl\u00e4ger",
email = "oelschlaeger.lennart@gmail.com",
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# optimizerR 1.1.0

* The `Optimizer` object has a new method `optimize` which can be used for minimization and maximization by setting the argument `direction`.

* In method `Objective$initialize()`, renamed argument `objective` -> `f`.

* In method `Objective$initialize()`, if `target = NULL` (the new default), the first argument from `f` is taken as target argument.

# optimizeR 1.0.5

* Removed `install_optimizer_packages()`.
Expand Down
File renamed without changes.
70 changes: 48 additions & 22 deletions R/objective.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,43 @@
#' The \code{Objective} object specifies the framework for an objective function
#' for numerical optimization.
#'
#' @param objective
#' @param f
#' A \code{function} to be optimized that
#' 1. has at least one argument that receives a \code{numeric} \code{vector}
#' 2. and returns a single \code{numeric} value.
#'
#' @param target
#' A \code{character}, the argument names of \code{objective} that get
#' optimized. These arguments must receive a \code{numeric} \code{vector}.
#' A \code{character}, the argument name(s) of \code{f} that get optimized.
#'
#' All target arguments must receive a \code{numeric} \code{vector}.
#'
#' Can be \code{NULL} (default), then it is the first argument of \code{f}.
#'
#' @param npar
#' A \code{integer} of the same length as \code{target}, defining the length
#' of the respective \code{numeric} \code{vector} argument.
#'
#' @param ...
#' Optionally additional arguments to \code{objective} that are fixed during
#' Optionally additional arguments to \code{f} that are fixed during
#' the optimization.
#'
#' @param overwrite
#' Either \code{TRUE} (default) to allow overwriting, or \code{FALSE} if not.
#'
#' @param verbose
#' Either \code{TRUE} (default) to print status messages, or \code{FALSE}
#' to hide those.
#'
#' @param argument_name
#' A \code{character}, a name of an argument for \code{objective}.
#' A \code{character}, a name of an argument for \code{f}.
#'
#' @param .at
#' A \code{numeric} of length \code{sum(self$npar)}, the values for the target
#' arguments written in a single vector.
#'
#' @param .negate
#' Either \code{TRUE} to negate the \code{numeric} return value of
#' \code{objective}, or \code{FALSE} (default) else.
#' \code{f}, or \code{FALSE} (default) else.
#'
#' @return
#' An \code{Objective} object.
Expand All @@ -38,16 +49,18 @@
#'
#' @examples
#' ### define log-likelihood function of Gaussian mixture model
#' llk <- function(mu, sd, lambda, data){
#' llk <- function(mu, sd, lambda, data) {
#' sd <- exp(sd)
#' lambda <- plogis(lambda)
#' sum(log(lambda * dnorm(data, mu[1], sd[1]) + (1 - lambda) * dnorm(data, mu[2], sd[2])))
#' cluster_1 <- lambda * dnorm(data, mu[1], sd[1])
#' cluster_2 <- (1 - lambda) * dnorm(data, mu[2], sd[2])
#' sum(log(cluster_1 + cluster_2))
#' }
#'
#' ### the log-likelihood function is supposed to be optimized over the first
#' ### three arguments, the 'data' argument is constant
#' objective <- Objective$new(
#' objective = llk, target = c("mu", "sd", "lambda"), npar = c(2, 2, 1),
#' f = llk, target = c("mu", "sd", "lambda"), npar = c(2, 2, 1),
#' data = faithful$eruptions
#' )
#'
Expand All @@ -65,20 +78,28 @@ Objective <- R6::R6Class(
#' Creates a new \code{Objective} object.
#' @return
#' A new \code{Objective} object.
initialize = function(objective, target, npar, ...) {
initialize = function(f, target = NULL, npar, ...) {

### input checks
checkmate::assert_function(f)
if (is.null(target)) {
target <- oeli::function_arguments(f, with_ellipsis = FALSE)[1]
}
checkmate::assert_character(target, any.missing = FALSE, min.len = 1)
checkmate::assert_function(objective, args = target)
checkmate::assert_function(f, args = target)
checkmate::assert_integerish(
npar, lower = 1, any.missing = FALSE, len = length(target)
)
arguments <- list(...)
arguments <- c(
arguments,
oeli::function_defaults(objective, names(arguments))
oeli::function_defaults(f, names(arguments))
)

### define objective
do.call(self$set_argument, c(arguments, list(verbose = FALSE)))
self$objective_name <- oeli::variable_name(objective)
private$.objective <- objective
self$objective_name <- oeli::variable_name(f)
private$.f <- f
private$.target <- target
private$.npar <- npar
},
Expand Down Expand Up @@ -181,7 +202,7 @@ Objective <- R6::R6Class(
tryCatch(
{
suppressWarnings(
value <- do.call(what = private$.objective, args = args),
value <- do.call(what = private$.f, args = args),
classes = if (self$hide_warnings) "warning" else ""
)
if (.negate) -value else value
Expand All @@ -207,7 +228,7 @@ Objective <- R6::R6Class(
print = function() {
cli::cat_bullet(c(
paste("Function:", private$.objective_name),
paste("Definition:", oeli::function_body(private$.objective, nchar = 40)),
paste("Definition:", oeli::function_body(private$.f, nchar = 40)),
paste("Targets (length):", paste(paste0(private$.target, " (", private$.npar, ")"), collapse = ", ")),
paste("Fixed arguments specified:", paste(names(private$.arguments), collapse = ", "))
))
Expand Down Expand Up @@ -299,7 +320,7 @@ Objective <- R6::R6Class(

private = list(

.objective = NULL,
.f = NULL,
.objective_name = character(),
.target = character(),
.npar = integer(),
Expand Down Expand Up @@ -333,26 +354,31 @@ Objective <- R6::R6Class(
checkmate::assert_string(argument_name)
if (!argument_name %in% names(private$.arguments)) {
cli::cli_abort(
"Function argument {.var {argument_name}} is required but not specified,
please call {.var $set_argument({.val {argument_name}} = ...)} first.",
"Function argument {.var {argument_name}} is required but not
specified, please call
{.var $set_argument({.val {argument_name}} = ...)} first.",
call = NULL
)
}
if (verbose) {
cli::cli_alert_success("Required argument {.val {argument_name}} is specified.")
cli::cli_alert_success(
"Required argument {.val {argument_name}} is specified."
)
}
},

### helper function that checks if all required arguments are specified
.check_arguments_complete = function(verbose = self$verbose) {
arguments_required <- oeli::function_arguments(
private$.objective, with_default = FALSE, with_ellipsis = FALSE
private$.f, with_default = FALSE, with_ellipsis = FALSE
)
for (argument_name in setdiff(arguments_required, private$.target)) {
private$.check_argument_specified(argument_name, verbose = FALSE)
}
if (verbose) {
cli::cli_alert_success("All required fixed arguments are specified.")
cli::cli_alert_success(
"All required fixed arguments are specified."
)
}
}

Expand Down
37 changes: 36 additions & 1 deletion R/optimizer.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
#'
#' Alternatively, it can also be a \code{\link{Objective}} object for more
#' flexibility.
#'
#' @param initial
#' A \code{numeric} vector with starting parameter values for the optimization.
#'
#' @param ...
#' Optionally additional arguments to be passed to the optimizer algorithm.
#' Without specifications, default values are used.
#'
#' @param direction
#' Either \code{"min"} for minimization or \code{"max"} for maximization.
#'
Expand Down Expand Up @@ -332,6 +335,38 @@ Optimizer <- R6::R6Class(
)
},

#' @description
#' Performing minimization or maximization.
#' @return
#' A named \code{list}, containing at least these five elements:
#' \describe{
#' \item{\code{value}}{A \code{numeric}, the maximum function value.}
#' \item{\code{parameter}}{A \code{numeric} vector, the parameter vector
#' where the maximum is obtained.}
#' \item{\code{seconds}}{A \code{numeric}, the optimization time in seconds.}
#' \item{\code{initial}}{A \code{numeric}, the initial parameter values.}
#' \item{\code{error}}{Either \code{TRUE} if an error occurred, or \code{FALSE}, else.}
#' }
#' Appended are additional output elements of the optimizer.
#'
#' If an error occurred, then the error message is also appended as element
#' \code{error_message}.
#'
#' If the time limit was exceeded, this also counts as an error. In addition,
#' the flag \code{time_out = TRUE} is appended.
#' @examples
#' objective <- function(x) -x^4 + 3*x - 5
#' optimizer <- Optimizer$new("stats::nlm")
#' optimizer$optimize(objective = objective, initial = 2, direction = "min")
#' optimizer$optimize(objective = objective, initial = 2, direction = "max")

optimize = function(objective, initial, direction = "min", ...) {
private$.optimize(
objective = objective, initial = initial,
additional_arguments = list(...), direction = direction
)
},

#' @description
#' Prints the optimizer label.
#' @return
Expand Down Expand Up @@ -397,7 +432,7 @@ Optimizer <- R6::R6Class(
.build_objective = function(objective, initial) {
if (!checkmate::test_r6(objective, "Objective")) {
objective <- Objective$new(
objective = objective,
f = objective,
target = names(formals(objective))[1],
npar = length(initial)
)
Expand Down
2 changes: 1 addition & 1 deletion README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Both artifacts are not allowed by `stats::nlm` and most of other available optim

```{r, define objective}
objective <- Objective$new(
objective = f, # f is our objective function
f = f, # f is our objective function
target = c("x", "y"), # x and y are the target arguments
npar = c(1, 1), # the target arguments have both a length of 1
"a" = 20,
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ define an objective object which we later can pass to the optimizers:

``` r
objective <- Objective$new(
objective = f, # f is our objective function
f = f, # f is our objective function
target = c("x", "y"), # x and y are the target arguments
npar = c(1, 1), # the target arguments have both a length of 1
"a" = 20,
Expand Down Expand Up @@ -185,7 +185,7 @@ nlm$maximize(objective = objective, initial = c(3, 3))
#> [1] 1.974451 1.974451
#>
#> $seconds
#> [1] 0.01279807
#> [1] 0.01002908
#>
#> $initial
#> [1] 3 3
Expand All @@ -194,7 +194,7 @@ nlm$maximize(objective = objective, initial = c(3, 3))
#> [1] FALSE
#>
#> $gradient
#> [1] 5.577962e-08 5.577962e-08
#> [1] 5.757896e-08 5.757896e-08
#>
#> $code
#> [1] 1
Expand All @@ -212,7 +212,7 @@ nelder_mead$maximize(objective = objective, initial = c(3, 3))
#> [1] 0 0
#>
#> $seconds
#> [1] 0.004384041
#> [1] 0.005402327
#>
#> $initial
#> [1] 3 3
Expand Down Expand Up @@ -286,7 +286,8 @@ GitHub](https://github.com/loelschlaeger/optimizeR/issues/new/choose).

## References

<div id="refs" class="references csl-bib-body hanging-indent">
<div id="refs" class="references csl-bib-body hanging-indent"
entry-spacing="0">

<div id="ref-optimx" class="csl-entry">

Expand Down
Loading

0 comments on commit 3ff2d96

Please sign in to comment.