diff --git a/R/objective.R b/R/objective.R index af4cacd..7a7674c 100644 --- a/R/objective.R +++ b/R/objective.R @@ -4,7 +4,7 @@ #' The \code{Objective} object specifies the framework for an objective function #' for numerical optimization. #' -#' @param f +#' @param f \[`function`\]\cr #' A \code{function} to be optimized. #' #' It is expected that \code{f} has at least one \code{numeric} argument. @@ -13,38 +13,36 @@ #' structure \code{numeric(1)}, i.e. a single \code{numeric} value (although #' this can be altered via the \code{output_template} field). #' -#' @param target -#' A \code{character}, the argument name(s) of \code{f} that get optimized. +#' @param target \[`character()`\]\cr +#' 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 npar \[`integer()`\]\cr +#' The length of each target arguments, i.e., the length(s) of the +#' \code{numeric} \code{vector} argument(s) specified by \code{target}. #' #' @param ... #' 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 overwrite \[`logical(1)`\]\cr +#' Allow overwriting? #' -#' @param verbose -#' Either \code{TRUE} (default) to print status messages, or \code{FALSE} -#' to hide those. +#' @param verbose \[`logical(1)`\]\cr +#' Print status messages? #' -#' @param argument_name -#' A \code{character}, a name of an argument for \code{f}. +#' @param argument_name \[`character(1)`\]\cr +#' 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 .at \[`numeric()`\]\cr +#' The values for the target argument(s), written in a single vector (hence must +#' be of length \code{sum(self$npar)}). #' -#' @param .negate -#' Either \code{TRUE} to negate the \code{numeric} return value of -#' \code{f}, or \code{FALSE} (default) else. +#' @param .negate \[`logical(1)`\]\cr +#' Negate the \code{numeric} return value of \code{f}? #' #' @return #' An \code{Objective} object. @@ -80,8 +78,7 @@ Objective <- R6::R6Class( #' @description #' Creates a new \code{Objective} object. - #' @return - #' A new \code{Objective} object. + initialize = function(f, target = NULL, npar, ...) { ### input checks @@ -110,8 +107,7 @@ Objective <- R6::R6Class( #' @description #' Set a fixed function argument. - #' @return - #' Invisibly the \code{Objective} object. + set_argument = function(..., overwrite = TRUE, verbose = self$verbose) { checkmate::assert_flag(overwrite) checkmate::assert_flag(verbose) @@ -143,8 +139,7 @@ Objective <- R6::R6Class( #' @description #' Get a fixed function argument. - #' @return - #' The argument value. + get_argument = function(argument_name, verbose = self$verbose) { private$.check_argument_specified(argument_name, verbose = verbose) checkmate::assert_flag(verbose) @@ -156,8 +151,7 @@ Objective <- R6::R6Class( #' @description #' Remove a fixed function argument. - #' @return - #' Invisibly the \code{Objective} object. + remove_argument = function(argument_name, verbose = self$verbose) { private$.check_argument_specified(argument_name, verbose = verbose) checkmate::assert_flag(verbose) @@ -170,8 +164,7 @@ Objective <- R6::R6Class( #' @description #' Validate an \code{Objective} object. - #' @return - #' Invisibly the \code{Objective} object. + validate = function(.at) { if (missing(.at)) { cli::cli_abort( @@ -199,14 +192,15 @@ Objective <- R6::R6Class( #' @description #' Evaluate the objective function. - #' @return - #' The objective value. + evaluate = function(.at, .negate = FALSE, ...) { private$.check_target(.at, verbose = FALSE) checkmate::assert_flag(.negate) splits <- c(0, cumsum(private$.npar)) .at <- structure( - lapply(seq_along(splits)[-1], function(i) .at[(splits[i - 1] + 1):(splits[i])]), + lapply(seq_along(splits)[-1], function(i) { + .at[(splits[i - 1] + 1):(splits[i])] + }), names = private$.target ) setTimeLimit(cpu = self$seconds, elapsed = self$seconds, transient = TRUE) @@ -238,14 +232,25 @@ Objective <- R6::R6Class( #' @description #' Print details of the \code{Objective} object. - #' @return - #' Invisibly the \code{Objective} object. + print = function() { cli::cat_bullet(c( - paste("Function:", private$.objective_name), - 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 = ", ")) + paste( + "Function:", private$.objective_name + ), + 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 = ", ") + ) )) invisible(self) } @@ -254,8 +259,9 @@ Objective <- R6::R6Class( active = list( - #' @field objective_name - #' A \code{character}, a label for the objective function. + #' @field objective_name \[`character(1)`\]\cr + #' The label for the objective function. + objective_name = function(value) { if (missing(value)) { return(private$.objective_name) @@ -265,8 +271,8 @@ Objective <- R6::R6Class( } }, - #' @field fixed_arguments - #' A \code{character}, the names of the fixed arguments (if any). + #' @field fixed_arguments \[`character()`\]\cr + #' The name(s) of the fixed argument(s) (if any). fixed_arguments = function(value) { if (missing(value)) { names(private$.arguments) @@ -278,13 +284,14 @@ Objective <- R6::R6Class( } }, - #' @field seconds - #' A \code{numeric}, a time limit in seconds. Computations are interrupted + #' @field seconds \[`numeric(1)`\]\cr + #' A time limit in seconds. Computations are interrupted #' prematurely if \code{seconds} is exceeded. #' #' No time limit if \code{seconds = Inf} (the default). #' #' Note the limitations documented in \code{\link[base]{setTimeLimit}}. + seconds = function(value) { if (missing(value)) { return(private$.seconds) @@ -294,9 +301,9 @@ Objective <- R6::R6Class( } }, - #' @field hide_warnings - #' Either \code{TRUE} to hide warnings when evaluating the objective function, - #' or \code{FALSE} (default) if not. + #' @field hide_warnings \[`logical(1)`\]\cr + #' Hide warnings when evaluating the objective function? + hide_warnings = function(value) { if (missing(value)) { return(private$.hide_warnings) @@ -306,9 +313,9 @@ Objective <- R6::R6Class( } }, - #' @field verbose - #' Either \code{TRUE} (default) to print status messages, or \code{FALSE} - #' to hide those. + #' @field verbose \[`logical(1)`\]\cr + #' Print status messages? + verbose = function(value) { if (missing(value)) { return(private$.verbose) @@ -318,8 +325,9 @@ Objective <- R6::R6Class( } }, - #' @field npar - #' An \code{integer} vector, defining the length of each target argument. + #' @field npar \[`integer()`\]\cr + #' The length of each target argument. + npar = function(value) { if (missing(value)) { structure(private$.npar, names = private$.target) @@ -331,9 +339,10 @@ Objective <- R6::R6Class( } }, - #' @field output_template + #' @field output_template \[`any`\]\cr #' A template of the expected output value, used for the \code{validate} #' method. + output_template = function(value) { if (missing(value)) { private$.output_template @@ -370,14 +379,17 @@ Objective <- R6::R6Class( } if (verbose) { cli::cli_alert_success( - "The value{?s} for the {length(private$.npar)} target argument{?s} {?is/are} correctly specified." + "The value{?s} for the {length(private$.npar)} target argument{?s} + {?is/are} correctly specified." ) } invisible(TRUE) }, ### helper function that checks if a function argument is specified - .check_argument_specified = function(argument_name, verbose = self$verbose) { + .check_argument_specified = function( + argument_name, verbose = self$verbose + ) { checkmate::assert_string(argument_name) if (!argument_name %in% names(private$.arguments)) { cli::cli_abort( diff --git a/man/Objective.Rd b/man/Objective.Rd index 8a911ad..9893d5d 100644 --- a/man/Objective.Rd +++ b/man/Objective.Rd @@ -34,26 +34,31 @@ objective$evaluate(1:5) \section{Active bindings}{ \if{html}{\out{
}} \describe{ -\item{\code{objective_name}}{A \code{character}, a label for the objective function.} +\item{\code{objective_name}}{[\code{character(1)}]\cr +The label for the objective function.} -\item{\code{fixed_arguments}}{A \code{character}, the names of the fixed arguments (if any).} +\item{\code{fixed_arguments}}{[\code{character()}]\cr +The name(s) of the fixed argument(s) (if any).} -\item{\code{seconds}}{A \code{numeric}, a time limit in seconds. Computations are interrupted +\item{\code{seconds}}{[\code{numeric(1)}]\cr +A time limit in seconds. Computations are interrupted prematurely if \code{seconds} is exceeded. No time limit if \code{seconds = Inf} (the default). Note the limitations documented in \code{\link[base]{setTimeLimit}}.} -\item{\code{hide_warnings}}{Either \code{TRUE} to hide warnings when evaluating the objective function, -or \code{FALSE} (default) if not.} +\item{\code{hide_warnings}}{[\code{logical(1)}]\cr +Hide warnings when evaluating the objective function?} -\item{\code{verbose}}{Either \code{TRUE} (default) to print status messages, or \code{FALSE} -to hide those.} +\item{\code{verbose}}{[\code{logical(1)}]\cr +Print status messages?} -\item{\code{npar}}{An \code{integer} vector, defining the length of each target argument.} +\item{\code{npar}}{[\code{integer()}]\cr +The length of each target argument.} -\item{\code{output_template}}{A template of the expected output value, used for the \code{validate} +\item{\code{output_template}}{[\code{any}]\cr +A template of the expected output value, used for the \code{validate} method.} } \if{html}{\out{
}} @@ -83,7 +88,8 @@ Creates a new \code{Objective} object. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{f}}{A \code{function} to be optimized. +\item{\code{f}}{[\code{function}]\cr +A \code{function} to be optimized. It is expected that \code{f} has at least one \code{numeric} argument. @@ -91,23 +97,22 @@ Further, it is expected that the return value of \code{f} is of the structure \code{numeric(1)}, i.e. a single \code{numeric} value (although this can be altered via the \code{output_template} field).} -\item{\code{target}}{A \code{character}, the argument name(s) of \code{f} that get optimized. +\item{\code{target}}{[\code{character()}]\cr +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}.} -\item{\code{npar}}{A \code{integer} of the same length as \code{target}, defining the length -of the respective \code{numeric} \code{vector} argument.} +\item{\code{npar}}{[\code{integer()}]\cr +The length of each target arguments, i.e., the length(s) of the +\code{numeric} \code{vector} argument(s) specified by \code{target}.} \item{\code{...}}{Optionally additional arguments to \code{f} that are fixed during the optimization.} } \if{html}{\out{
}} } -\subsection{Returns}{ -A new \code{Objective} object. -} } \if{html}{\out{
}} \if{html}{\out{}} @@ -124,16 +129,14 @@ Set a fixed function argument. \item{\code{...}}{Optionally additional arguments to \code{f} that are fixed during the optimization.} -\item{\code{overwrite}}{Either \code{TRUE} (default) to allow overwriting, or \code{FALSE} if not.} +\item{\code{overwrite}}{[\code{logical(1)}]\cr +Allow overwriting?} -\item{\code{verbose}}{Either \code{TRUE} (default) to print status messages, or \code{FALSE} -to hide those.} +\item{\code{verbose}}{[\code{logical(1)}]\cr +Print status messages?} } \if{html}{\out{}} } -\subsection{Returns}{ -Invisibly the \code{Objective} object. -} } \if{html}{\out{
}} \if{html}{\out{}} @@ -147,16 +150,14 @@ Get a fixed function argument. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{argument_name}}{A \code{character}, a name of an argument for \code{f}.} +\item{\code{argument_name}}{[\code{character(1)}]\cr +A name of an argument for \code{f}.} -\item{\code{verbose}}{Either \code{TRUE} (default) to print status messages, or \code{FALSE} -to hide those.} +\item{\code{verbose}}{[\code{logical(1)}]\cr +Print status messages?} } \if{html}{\out{
}} } -\subsection{Returns}{ -The argument value. -} } \if{html}{\out{
}} \if{html}{\out{}} @@ -170,16 +171,14 @@ Remove a fixed function argument. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{argument_name}}{A \code{character}, a name of an argument for \code{f}.} +\item{\code{argument_name}}{[\code{character(1)}]\cr +A name of an argument for \code{f}.} -\item{\code{verbose}}{Either \code{TRUE} (default) to print status messages, or \code{FALSE} -to hide those.} +\item{\code{verbose}}{[\code{logical(1)}]\cr +Print status messages?} } \if{html}{\out{
}} } -\subsection{Returns}{ -Invisibly the \code{Objective} object. -} } \if{html}{\out{
}} \if{html}{\out{}} @@ -193,14 +192,12 @@ Validate an \code{Objective} object. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{.at}}{A \code{numeric} of length \code{sum(self$npar)}, the values for the target -arguments written in a single vector.} +\item{\code{.at}}{[\code{numeric()}]\cr +The values for the target argument(s), written in a single vector (hence must +be of length \code{sum(self$npar)}).} } \if{html}{\out{
}} } -\subsection{Returns}{ -Invisibly the \code{Objective} object. -} } \if{html}{\out{
}} \if{html}{\out{}} @@ -214,20 +211,18 @@ Evaluate the objective function. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{.at}}{A \code{numeric} of length \code{sum(self$npar)}, the values for the target -arguments written in a single vector.} +\item{\code{.at}}{[\code{numeric()}]\cr +The values for the target argument(s), written in a single vector (hence must +be of length \code{sum(self$npar)}).} -\item{\code{.negate}}{Either \code{TRUE} to negate the \code{numeric} return value of -\code{f}, or \code{FALSE} (default) else.} +\item{\code{.negate}}{[\code{logical(1)}]\cr +Negate the \code{numeric} return value of \code{f}?} \item{\code{...}}{Optionally additional arguments to \code{f} that are fixed during the optimization.} } \if{html}{\out{
}} } -\subsection{Returns}{ -The objective value. -} } \if{html}{\out{
}} \if{html}{\out{}} @@ -238,9 +233,6 @@ Print details of the \code{Objective} object. \if{html}{\out{
}}\preformatted{Objective$print()}\if{html}{\out{
}} } -\subsection{Returns}{ -Invisibly the \code{Objective} object. -} } \if{html}{\out{
}} \if{html}{\out{}} diff --git a/vignettes/optimizeR_package.Rmd b/vignettes/optimizeR_package.Rmd index defceeb..508f242 100644 --- a/vignettes/optimizeR_package.Rmd +++ b/vignettes/optimizeR_package.Rmd @@ -17,13 +17,35 @@ knitr::opts_chunk$set( devtools::load_all() # TODO ``` -# Numerical optimization +# Introduction + +The `{optimizeR}` package offers an object-oriented solution for numerical optimization in R, addressing the inconsistency in how optimization algorithms are implemented across different R packages. By providing a unified API, `{optimizeR}` streamlines interactions with various optimizers, which traditionally differ in their argument names, function call structures, and output formats. For instance, the `{stats}` package functions `nlm()` and `optim()` have distinct argument names for the objective function and initial parameter vector, with these elements positioned differently in the function call, and their output values, such as the estimated minimum and parameter vector, labeled inconsistently. + +Beyond unifying optimizers, the object-oriented framework of `{optimizeR}` offers additional convenience over directly using individual algorithms. With `{optimizeR}`, any optimizer can be employed for both minimization and maximization tasks, functions can be optimized over multiple arguments, and computation time can be measured or limited for prolonged tasks. + +The `{optimizeR}` package itself does not introduce new optimizers but instead provides a flexible framework to standardize the usage of existing ones and accommodate future implementations. The key design principle of `{optimizeR}` is flexibility -- ensuring compatibility with nearly any optimization algorithm in R -- while maintaining ease of use. + +This vignette introduces the three main objects in `{optimizeR}`: the `Objective` object for defining objective functions, the `Optimizer` object for specifying numerical optimizers, and the `ParameterSpaces` object for transforming parameters across different spaces. # Objective function +An objective function defines the goal of an optimization problem, representing the mathematical expression that an algorithm seeks to minimize or maximize. In numerical optimization, the algorithm adjusts input variables (parameters) to find the optimal solution, yielding either the minimum (for minimization) or maximum (for maximization) value of the function. + +The `Objective` object encapsulates an objective function and is instantiated as follows: + +```{r, define Objective demo, eval = FALSE} +objective_function <- Objective$new(f, target, npar, ...) +``` + +Here, `f` represents the function to be optimized, `target` specifies the `numeric` `vector` arguments over which the function `f` will be optimized (the so-called target arguments), `npar` defines the length of the target arguments, and `...` allows for additional arguments to be passed to `f` that are fixed during the optimization process. + +Once the `Objective` object is created, you can evaluate the objective function at a given point using `objective_function$evaluate(at)`. Fixed arguments can be accessed with `$get_argument()`, modified using `$set_argument()`, or removed via `$remove_argument()`. The `$validate()` method ensures that the object is properly configured. + +The following two examples illustrate how to create and use the `Objective` object. + ## Example 1: Himmelblau's function -The following is an implementation of the [Himmelblau's function](https://en.wikipedia.org/wiki/Himmelblau%27s_function) $$f(x, y) = (x^2 + y - 11)^2 + (x + y^2 - 7)^2:$$ +Here is an implementation of the [Himmelblau's function](https://en.wikipedia.org/wiki/Himmelblau%27s_function) $$f(x, y) = (x^2 + y - 11)^2 + (x + y^2 - 7)^2:$$ ```{r, himmelblau} himmelblau <- function(x) (x[1]^2 + x[2] - 11)^2 + (x[1] + x[2]^2 - 7)^2 @@ -61,18 +83,9 @@ ggplot(grid, aes(x = Var1, y = Var2, z = z)) + ) ``` -For the Himmelblau's function, it is straightforward to define the analytical gradient as follows: - -```{r, himmelblau gradient} -gradient <- function(x) { - c( - 4 * x[1] * (x[1]^2 + x[2] - 11) + 2 * (x[1] + x[2]^2 - 7), - 2 * (x[1]^2 + x[2] - 11) + 4 * x[2] * (x[1] + x[2]^2 - 7) - ) -} -``` +The Himmelblau's function is two-dimensional, but only has a single vector input `x` of length 2. The following creates the `Objective` object: -## Example 2: Log-likelihood function of two-class Gaussian mixture model +## Example 2: Log-likelihood function of two-class Gaussian mixture Consider fitting a two-class Gaussian mixture model via maximizing the model's log-likelihood function @@ -102,6 +115,21 @@ normal_mixture_llk <- function(theta, data) { # Optimizer objects +## Example 1 (cont.) + +For the Himmelblau's function, it is straightforward to define the analytical gradient as follows: + +```{r, himmelblau gradient} +gradient <- function(x) { + c( + 4 * x[1] * (x[1]^2 + x[2] - 11) + 2 * (x[1] + x[2]^2 - 7), + 2 * (x[1]^2 + x[2] - 11) + 4 * x[2] * (x[1] + x[2]^2 - 7) + ) +} +``` + +## Example 2 (cont.) + ## Optimizer dictionary ## Custom optimizers