diff --git a/NAMESPACE b/NAMESPACE index b645da7..6b20e59 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,6 +17,9 @@ S3method(update,online) export(autoplot) export(batch) export(conline) +export(init_experts_list) +export(make_basis_mats) +export(make_hat_mats) export(make_knots) export(online) export(oracle) diff --git a/NEWS.md b/NEWS.md index 146cf53..467ac64 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,16 @@ +profoc 1.3.0 +============== + +## Improvements + +* New articles explain how to use method on `online()` objects to deploy online learning algorithms in production. +* The `conline` c++ class now exoses `weights` to R. +* A new article on how to interact with the `conline` c++ class was added. +* Various functions are now exported to R to allow for easier interaction with the `conline` c++ class. These functions are: `init_experts_list()`, `make_basis_mats` and `make_hat_mats` +* The code of `online()` was simplified a bit by utilizing the new `init_experts_list()` function. + +## Improvements + profoc 1.2.1 ============== diff --git a/R/misc.R b/R/misc.R index 26345ac..a8b3c6a 100644 --- a/R/misc.R +++ b/R/misc.R @@ -2,6 +2,76 @@ get_seed <- function() { sample.int(.Machine$integer.max, 1) } +#' Create experts list to be used in conline class +#' +#' This function works in conjunction with the conline class. +#' It takes a matrix of experts and a matrix of outcomes and +#' returns a list of experts which fulfills all properties +#' that are needed for passing it to the an instance of conline. +#' @param experts array of predictions with dimension T x D x P x K +#' (Observations x Variables x Quantiles x Experts) or T x D x K or T x P x K. +#' @param y A matrix of outcomes with dimension T x D. +#' @param output_with_names Defaults to FALSE. If TRUE, the function +#' returns a list with +#' the experts list, the names of the variables (dnames) and the +#' names of the experts (enames). +#' @importFrom abind asub adrop +#' @export +init_experts_list <- function(experts, y, output_with_names = FALSE) { + edim <- dim(experts) + if (length(edim) == 3) { + enames <- dimnames(experts)[[3]] + if (is.null(enames)) { + enames <- paste0("E", 1:edim[3]) + } + if (ncol(y) > 1) { # multivariate point + if (is.null(dimnames(experts)[[2]])) { + dnames <- paste0("D", 1:edim[2]) + } else { + dnames <- dimnames(experts)[[2]] + } + experts <- array( + unlist(experts), + dim = c(edim[1], edim[2], 1, edim[3]) + ) + experts <- lapply(seq_len(edim[1]), + asub, + x = experts, + dims = 1, + drop = FALSE + ) + experts <- lapply(experts, adrop, drop = 1) + dim(experts) <- c(edim[1], 1) + } else if (ncol(y) == 1) { # univariate probabilistic + dnames <- "D1" + experts <- lapply(seq_len(edim[1]), + asub, + x = experts, + dims = 1, + drop = FALSE + ) + dim(experts) <- c(edim[1], 1) + } + } else if (length(edim) == 4) { # multivariate probabilistic + if (is.null(dimnames(experts)[[2]])) { + dnames <- paste0("D", 1:edim[2]) + } else { + dnames <- dimnames(experts)[[2]] + } + enames <- dimnames(experts)[[4]] + if (is.null(enames)) { + enames <- paste0("E", 1:edim[4]) + } + experts <- array_to_list(experts) + } + + if (output_with_names) { + return(list(experts = experts, dnames = dnames, enames = enames)) + } else { + return(experts) + } +} + #' @importFrom abind asub adrop array_to_list <- function(x) { x <- lapply(seq_len(dim(x)[1]), diff --git a/R/online.R b/R/online.R index ea53b81..2d9d704 100644 --- a/R/online.R +++ b/R/online.R @@ -176,14 +176,10 @@ online <- function(y, experts, tau, trace = TRUE) { model_instance <- new(conline) model_instance$trace <- trace - model_instance$tau <- tau model_instance$forget_past_performance <- forget_past_performance model_instance$save_past_performance <- save_past_performance model_instance$save_predictions_grid <- save_predictions_grid - - edim <- dim(experts) - if (is.vector(y)) { y <- matrix(y) } @@ -192,68 +188,30 @@ online <- function(y, experts, tau, # preserve names names <- list(y = dimnames(y)) - names$experts <- list(NULL) # Prepare experts - if (length(edim) == 3) { - enames <- dimnames(experts)[[3]] - if (is.null(enames)) { - enames <- paste0("E", 1:edim[3]) - } - if (ncol(y) > 1) { # multivariate point - if (is.null(dimnames(experts)[[2]])) { - dnames <- paste0("D", 1:edim[2]) - } else { - dnames <- dimnames(experts)[[2]] - } - experts <- array( - unlist(experts), - dim = c(edim[1], edim[2], 1, edim[3]) - ) - experts <- lapply(seq_len(edim[1]), - asub, - x = experts, - dims = 1, - drop = FALSE - ) - experts <- lapply(experts, adrop, drop = 1) - dim(experts) <- c(edim[1], 1) - model_instance$experts <- experts - } else if (ncol(y) == 1) { # univariate probabilistic - dnames <- "D1" - experts <- lapply(seq_len(edim[1]), - asub, - x = experts, - dims = 1, - drop = FALSE - ) - dim(experts) <- c(edim[1], 1) - model_instance$experts <- experts - } - } else if (length(edim) == 4) { # multivariate probabilistic - if (is.null(dimnames(experts)[[2]])) { - dnames <- paste0("D", 1:edim[2]) - } else { - dnames <- dimnames(experts)[[2]] - } - enames <- dimnames(experts)[[4]] - if (is.null(enames)) { - enames <- paste0("E", 1:edim[4]) - } - experts <- array_to_list(experts) - model_instance$experts <- experts - } - names$experts[[2]] <- dnames + + e_list <- init_experts_list( + experts = experts, + y = y, + output_with_names = TRUE + ) + model_instance$experts <- e_list$experts + + names$experts <- list(NULL) + names$experts[[2]] <- e_list$dnames names$experts[[3]] <- tau - names$experts[[4]] <- enames + names$experts[[4]] <- e_list$enames + model_instance$tau <- tau + # Define dimensions for convenience - T <- dim(experts)[1] - D <- dim(experts[[1]])[1] - P <- dim(experts[[1]])[2] - K <- dim(experts[[1]])[3] + T <- dim(e_list$experts)[1] + D <- dim(e_list$experts[[1]])[1] + P <- dim(e_list$experts[[1]])[2] + K <- dim(e_list$experts[[1]])[3] - if (nrow(experts) - nrow(y) < 0) { + if (nrow(e_list$experts) - nrow(y) < 0) { stop("Number of provided expert predictions has to match or exceed observations.") } diff --git a/R/splines.R b/R/splines.R index bbc24b8..8a719fd 100644 --- a/R/splines.R +++ b/R/splines.R @@ -37,6 +37,25 @@ make_knots <- function(n, mu = .5, sig = 1, nonc = 0, tailw = 1, deg = 1) { return(knots) } +#' Create a List of Basis Matrices +#' +#' This function creates a list of basis matrices and the corresponding +#' parameters. It is used in `online()` to create the basis matrices +#' for basis smoothing. +#' @param x The predictor variable +#' @param n Number of knots +#' @param mu Beta distribution location parameter +#' @param sigma Beta distribution scale parameter +#' @param nonc Beta distribution noncentrality parameter +#' @param tailw Tailweight +#' @param deg Degree of splines +#' @param periodic Create periodic basis +#' @param idx `make_basis_mats()` will create a grid containing all +#' combinations of the parameters. If idx is set, this grid will +#' be subsetted to the rows specified by idx. +#' @param params Instead of the arguments above, a grid (data.frame +#' or named matrix) of parameters can be passed directly. +#' @export make_basis_mats <- function(x, # Splines basis n = length(x), # (vec of) Number of knots mu = 0.5, # (vec of) Beta dist. mu @@ -106,6 +125,28 @@ make_basis_mats <- function(x, # Splines basis return(out) } +#' Create a List of Hat Matrices +#' +#' This function creates a list of hat matrices and the corresponding +#' parameters. It is used in `online()` to create the hat matrices +#' for penalized smoothing. +#' @param x The predictor variable +#' @param n Number of knots +#' @param mu Beta distribution location parameter +#' @param sigma Beta distribution scale parameter +#' @param nonc Beta distribution noncentrality parameter +#' @param tailw Tailweight +#' @param deg Degree of splines +#' @param ndiff Sets the degree of the differencing matrix for creaing +#' the penalty +#' @param lambda Penalty parameter (higher values lead to higher penalty) +#' @param periodic Create periodic penalty +#' @param idx `make_hat_mats()` will create a grid containing all +#' combinations of the parameters. If idx is set, this grid will +#' be subsetted to the rows specified by idx. +#' @param params Instead of the arguments above, a grid (data.frame +#' or named matrix) of parameters can be passed directly. +#' @export make_hat_mats <- function(x, n = length(x), mu = 0.5, diff --git a/man/init_experts_list.Rd b/man/init_experts_list.Rd new file mode 100644 index 0000000..5aadfe8 --- /dev/null +++ b/man/init_experts_list.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/misc.R +\name{init_experts_list} +\alias{init_experts_list} +\title{Create experts list to be used in conline class} +\usage{ +init_experts_list(experts, y, output_with_names = FALSE) +} +\arguments{ +\item{experts}{array of predictions with dimension T x D x P x K +(Observations x Variables x Quantiles x Experts) or T x D x K or T x P x K.} + +\item{y}{A matrix of outcomes with dimension T x D.} + +\item{output_with_names}{Defaults to FALSE. If TRUE, the function +returns a list with +the experts list, the names of the variables (dnames) and the +names of the experts (enames).} +} +\description{ +This function works in conjunction with the conline class. +It takes a matrix of experts and a matrix of outcomes and +returns a list of experts which fulfills all properties +that are needed for passing it to the an instance of conline. +} diff --git a/man/make_basis_mats.Rd b/man/make_basis_mats.Rd new file mode 100644 index 0000000..431336b --- /dev/null +++ b/man/make_basis_mats.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/splines.R +\name{make_basis_mats} +\alias{make_basis_mats} +\title{Create a List of Basis Matrices} +\usage{ +make_basis_mats( + x, + n = length(x), + mu = 0.5, + sigma = 1, + nonc = 0, + tailw = 1, + deg = 1, + periodic = FALSE, + idx = NULL, + params = NULL +) +} +\arguments{ +\item{x}{The predictor variable} + +\item{n}{Number of knots} + +\item{mu}{Beta distribution location parameter} + +\item{sigma}{Beta distribution scale parameter} + +\item{nonc}{Beta distribution noncentrality parameter} + +\item{tailw}{Tailweight} + +\item{deg}{Degree of splines} + +\item{periodic}{Create periodic basis} + +\item{idx}{\code{make_basis_mats()} will create a grid containing all +combinations of the parameters. If idx is set, this grid will +be subsetted to the rows specified by idx.} + +\item{params}{Instead of the arguments above, a grid (data.frame +or named matrix) of parameters can be passed directly.} +} +\description{ +This function creates a list of basis matrices and the corresponding +parameters. It is used in \code{online()} to create the basis matrices +for basis smoothing. +} diff --git a/man/make_hat_mats.Rd b/man/make_hat_mats.Rd new file mode 100644 index 0000000..5db4b82 --- /dev/null +++ b/man/make_hat_mats.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/splines.R +\name{make_hat_mats} +\alias{make_hat_mats} +\title{Create a List of Hat Matrices} +\usage{ +make_hat_mats( + x, + n = length(x), + mu = 0.5, + sigma = 1, + nonc = 0, + tailw = 1, + deg = 1, + ndiff = 1.5, + lambda = -Inf, + periodic = FALSE, + idx = NULL, + params = NULL +) +} +\arguments{ +\item{x}{The predictor variable} + +\item{n}{Number of knots} + +\item{mu}{Beta distribution location parameter} + +\item{sigma}{Beta distribution scale parameter} + +\item{nonc}{Beta distribution noncentrality parameter} + +\item{tailw}{Tailweight} + +\item{deg}{Degree of splines} + +\item{ndiff}{Sets the degree of the differencing matrix for creaing +the penalty} + +\item{lambda}{Penalty parameter (higher values lead to higher penalty)} + +\item{periodic}{Create periodic penalty} + +\item{idx}{\code{make_hat_mats()} will create a grid containing all +combinations of the parameters. If idx is set, this grid will +be subsetted to the rows specified by idx.} + +\item{params}{Instead of the arguments above, a grid (data.frame +or named matrix) of parameters can be passed directly.} +} +\description{ +This function creates a list of hat matrices and the corresponding +parameters. It is used in \code{online()} to create the hat matrices +for penalized smoothing. +} diff --git a/src/conline_exports.cpp b/src/conline_exports.cpp index d688769..9b134cb 100644 --- a/src/conline_exports.cpp +++ b/src/conline_exports.cpp @@ -27,6 +27,7 @@ RCPP_MODULE(conlineEx) .field("hat_pr", &conline::hat_pr) .field("hat_mv", &conline::hat_mv) .field("w0", &conline::w0) + .field("weights", &conline::weights) .field("R0", &conline::R0) .field("params", &conline::params) .field("params_basis_pr", &conline::params_basis_pr) diff --git a/vignettes/class.Rmd b/vignettes/class.Rmd new file mode 100644 index 0000000..de693cd --- /dev/null +++ b/vignettes/class.Rmd @@ -0,0 +1,156 @@ +--- +title: "Accessing the C++ Interface" +author: Jonathan Berrisch +date: "`r Sys.Date()`" +bibliography: + - ../inst/bib/profoc.bib +output: + rmarkdown::html_vignette: + number_sections: no + toc: no +vignette: > + %\VignetteIndexEntry{Accessing the C++ Interface} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +## Introduction + +All major parts of `online()` are implemented in C++ for speed. Usually, this +comes at the cost of flexibility. However, the profoc package exposes a C++ +class `conline` that allows you to gain fine grained control over objects. +`online()` wrapps this class and provides a convenient interface for the most +common use cases. However, if you need to alter object initialization (i.e. +provide custom basis / hat matrices for smoothing) you can use the C++ class +directly from R. This vignette shows how to do this. + +Note that we will reuse the data from `vignette("profoc")`. + +```{r, include = FALSE} +rm(list = ls()) +knitr::opts_chunk$set( + collapse = TRUE, + # dev = "svg", + warning = FALSE, + message = FALSE, + comment = "#>" +) +Sys.setenv("OMP_THREAD_LIMIT" = 2) + +set.seed(1) +T <- 2^5 # Observations +D <- 1 # Numer of variables +N <- 2 # Experts +P <- 99 # Size of probability grid +probs <- 1:P / (P + 1) + +y <- matrix(rnorm(T)) # Realized observations + +# Experts deviate in mean and standard deviation from true process +experts_mu <- c(-1, 3) +experts_sd <- c(1, 2) + +experts <- array(dim = c(T, P, N)) # Expert predictions + +for (t in 1:T) { + experts[t, , 1] <- qnorm(probs, mean = experts_mu[1], sd = experts_sd[1]) + experts[t, , 2] <- qnorm(probs, mean = experts_mu[2], sd = experts_sd[2]) +} +``` + +## Online learning with `conline` + +First, we need to create a new instance of the c++ class. This can be done by +calling `new(conline)`. + +```{r, include = FALSE} +library(profoc) +model <- new(conline) +``` + +Now we need to pass the data to the class instance. The whole list of accessible field can be printed with `names(model)`. Most of them have defaults. + +```{r} +model$y <- y +tau <- 1:P / (P + 1) +model$tau <- tau +``` + +The experts array is a bit more complicated. C++ expects us to pass a list of arrays. Thereby, the list itself must have dimension `Tx1` and the elements of the list (the arrays) `D x P x K`. For convenience we can use `init_experts_list()` to create such a list from our experts array. + +```{r} +model$experts <- init_experts_list(experts, y) +``` + +Now suppose we want to alter the smoothing behavior across quantiles. We start by creating a new hat matrix. + +```{r} +hat <- make_hat_mats( + x = tau, + mu = 0.2, # Put more knots in the lower tail + periodic = TRUE +) +str(hat) +``` + +We need a list of sparse matrices which `make_hat_mats()` returns. So we can pass that directly to our class. + +```{r} +model$hat_pr <- hat$hat +``` + +The other smoothing matrices have to be filled with defaults. Usually `online()` takes care of this. But we can do it manually as well. + +```{r} +model$basis_mv <- make_basis_mats(x = 1:D / (D + 1))$basis +model$basis_pr <- make_basis_mats(x = 1:P / (P + 1))$basis +model$hat_mv <- make_hat_mats(x = 1:D / (D + 1))$hat +``` + +Now we can specify the parameter grid. We will stick to the defaults here: + +```{r} +parametergrid <- as.matrix( + expand.grid( + forget_regret = 0, + soft_threshold = -Inf, + hard_threshold = -Inf, + fixed_share = 0, + basis_pr_idx = 1, + basis_mv_idx = 1, + hat_pr_idx = 1, + hat_mv_idx = 1, + gamma = 1, + loss_share = 0, + regret_share = 0 + ) +) + +model$params <- parametergrid +``` + +Finally, we can run `model$set_defaults()`. This populates initial states (w0 for weights and R0 for regret). + +```{r} +model$set_defaults() +``` + +Now `model$set_grid_objects()` will create the grid objects (performance, weights, regret etc.) + +```{r} +model$set_grid_objects() +``` + +Finally, we can run `model$learn()` to start the learning process. + +```{r} +model$learn() +``` + +## Accessing the results + +The learning process fills the class objects. Now we can inspect them using the `$` operator, like we would with any other R object. For example, we can access the weights: + +```{r} +model$finalize() +``` \ No newline at end of file diff --git a/vignettes/production.Rmd b/vignettes/production.Rmd index 65028a2..fb85d53 100644 --- a/vignettes/production.Rmd +++ b/vignettes/production.Rmd @@ -1,5 +1,5 @@ --- -title: "Production " +title: "Production" author: Jonathan Berrisch date: "`r Sys.Date()`" bibliography: diff --git a/vignettes/profoc.Rmd b/vignettes/profoc.Rmd index 982fd7c..d049b67 100644 --- a/vignettes/profoc.Rmd +++ b/vignettes/profoc.Rmd @@ -48,6 +48,7 @@ Let's simulate this setting in R before we apply the combination algorithm. ```{r} set.seed(1) T <- 2^5 # Observations +D <- 1 # Numer of variables N <- 2 # Experts P <- 99 # Size of probability grid probs <- 1:P / (P + 1)