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

Vignettes/58 #67

Merged
merged 25 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
63fb5fb
Updated and improved vignettes. New structure is 1-introduction, 2-DL…
moralapablo Dec 11, 2023
ce89b49
Removed keras callback globalVariable as it is no longer needed
moralapablo Dec 11, 2023
da0f90c
Updated vignettes. Renamed title in 3 to Multiclass classification an…
moralapablo Dec 12, 2023
1587819
Rebuilt vignettes removing number from headings and using luz::as_dat…
moralapablo Dec 12, 2023
05db34d
Changed eval_poly arguments order, improved documentation and modifie…
moralapablo Dec 19, 2023
0abe656
Added missing mention to the intercept being represented by 0 in docu…
moralapablo Dec 19, 2023
d747aee
Changed eval_poly argument name from x to newdata, to match predict.n…
moralapablo Dec 20, 2023
ac756ef
Updated predict.nn2poly and nn2poly docs, added examples, details and…
moralapablo Dec 20, 2023
8185fdd
Modified predict.nn2poly to work with multiple layers polynomials fro…
moralapablo Dec 21, 2023
d699057
Fixed typo
moralapablo Dec 21, 2023
9bf59b5
Added mising parameter description
moralapablo Dec 22, 2023
a7dd163
Added mising parameter description
moralapablo Dec 22, 2023
3d6fe1e
Merge branch 'vignettes/58' of https://github.com/IBiDat/nn2poly into…
moralapablo Dec 22, 2023
083aee9
Skip torch related tests in mac OS due to GitHub Actions problem.
moralapablo Jan 8, 2024
db478ac
Updated `add_constraints` documentation
moralapablo Jan 8, 2024
0f569b7
Updated documentation in `add_constraints` and `luz_model_sequential`
moralapablo Jan 8, 2024
106817b
Changed nn2poly output when keep_layers=TRUE to match predict notatio…
moralapablo Jan 9, 2024
c3d6545
Removed `eval_poly()` from exported functions but left it documented …
moralapablo Jan 9, 2024
18a0863
Changed output from `nn2poly`, `predict` and `eval_poly` to represent…
moralapablo Jan 9, 2024
7622c81
Regenerated vignettes with lastest changes
moralapablo Jan 9, 2024
8a295ae
fix NOTE about figure dir
Enchufa2 Jan 11, 2024
6affee2
clean up python detritus
Enchufa2 Jan 11, 2024
ff47882
Added missing examples for `add_constraints()` and `luz_model_sequent…
moralapablo Jan 15, 2024
93383e6
Siwtch to native pipe operator in examples.
moralapablo Jan 15, 2024
269aee7
Simplified constraints examples to avoid training, added requireNames…
moralapablo Jan 15, 2024
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
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ S3method(plot_taylor_and_activation_potentials,default)
S3method(plot_taylor_and_activation_potentials,list)
S3method(predict,nn2poly)
export(add_constraints)
export(eval_poly)
export(fit)
export(luz_model_sequential)
export(nn2poly)
Expand Down
1 change: 1 addition & 0 deletions R/callback.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ build_callback.luz_module_generator <- function(object,
luz_callback()
}

# Keras callback is built in Python due to performance problems if built in R
build_callback.keras.engine.training.Model <- function(object,
type = c("l1_norm", "l2_norm")) {
keras_callback <- py_load_class("KerasCallback")
Expand Down
57 changes: 55 additions & 2 deletions R/constraints.R
Original file line number Diff line number Diff line change
@@ -1,14 +1,67 @@
#' Add constraints to a neural network
#'
#' This function sets up a neural network object with the constraints required
#' by the \code{\link{nn2poly}} algorithm.
#' by the \code{\link{nn2poly}} algorithm. Currently supported neural network
#' frameworks are \code{keras/tensorflow} and \code{luz/torch}.
#'
#' @param object A neural network object.
#' @param object A neural network object in sequential form from one of the
#' supported frameworks.
#' @param type Constraint type. Currently, `l1_norm` and `l2_norm` are supported.
#' @param ... Additional arguments (unused).
#'
#' @details
#' Constraints are added to the model object using callbacks in their specific
#' framework. These callbacks are used during training when calling fit on the
#' model. Specifically we are using callbacks that are applied at the end of
#' each train batch.
#'
#' Models in \code{luz/torch} need to use the \code{\link{luz_model_sequential}}
#' helper in order to have a sequential model in the appropriate form.
#'
#' @return A `nn2poly` neural network object.
#'
#' @seealso [luz_model_sequential()]
#'
#' @examples
#' \dontrun{
#' if (requireNamespace("keras", quietly=TRUE)) {
#' # ---- Example with a keras/tensorflow network ----
#' # Build a small nn:
#' nn <- keras::keras_model_sequential()
#' nn <- keras::layer_dense(nn, units = 10, activation = "tanh", input_shape = 2)
#' nn <- keras::layer_dense(nn, units = 1, activation = "linear")
#'
#' # Add constraints
#' nn_constrained <- add_constraints(nn, constraint_type = "l1_norm")
#'
#' # Check that class of the constrained nn is "nn2poly"
#' class(nn_constrained)[1]
#' }
#' }
#'
#' if (requireNamespace("luz", quietly=TRUE)) {
#' # ---- Example with a luz/torch network ----
#'
#' # Build a small nn
#' nn <- luz_model_sequential(
#' torch::nn_linear(2,10),
#' torch::nn_tanh(),
#' torch::nn_linear(10,1)
#' )
#'
#' # With luz/torch we need to setup the nn before adding the constraints
#' nn <- luz::setup(module = nn,
#' loss = torch::nn_mse_loss(),
#' optimizer = torch::optim_adam,
#' )
#'
#' # Add constraints
#' nn <- add_constraints(nn)
#'
#' # Check that class of the constrained nn is "nn2poly"
#' class(nn)[1]
#' }
#'
#' @export
add_constraints <- function(object, type = c("l1_norm", "l2_norm"), ...) {
UseMethod("add_constraints")
Expand Down
123 changes: 72 additions & 51 deletions R/eval_poly.R
Original file line number Diff line number Diff line change
@@ -1,97 +1,118 @@
#' Evaluates one or several polynomials over one or more data points in the
#' desired variables.
#' Polynomial evaluation
#'
#' @param x Input data as matrix, vector or dataframe.
#' The number of columns should be the number of variables in the polynomial
#' (the dimension p). Response variable to be predicted should not be included.
#' Evaluates one or several polynomials on the given data.
#'
#' Note that this function is unstable and subject to change. Therefore it is
#' not exported but this documentations is left available so users can use it if
#' needed to simulate data by using \code{nn2poly:::eval_poly()}
#'
#' @param poly List containing 2 items: \code{labels} and \code{values}.
#' - \code{labels} List of integer vectors with same length (or number of rows)
#' - \code{labels}: List of integer vectors with same length (or number of cols)
#' as \code{values}, where each integer vector denotes the combination of
#' variables associated to the coefficient value stored at the same position in
#' \code{values}. Note that the variables are numbered from 1 to p.
#' - \code{values} Matrix (or also a vector if single polynomial), where each
#' row represents a polynomial, with same number of columns as the length of
#' \code{labels}, containing at each column the value of the coefficient
#' given by the equivalent label in that same position.
#' \code{values}. That is, the monomials in the polynomial. Note that the
#' variables are numbered from 1 to p, with the intercept is represented by 0.
#' - \code{values}: Matrix (can also be a vector if single polynomial), where
#' each column represents a polynomial, with same number of rows as the length
#' of \code{labels}, containing at each row the value of the coefficient
#' of the monomial given by the equivalent label in that same position.
#'
#' Example: If \code{labels} contains the integer vector c(1,1,3) at position
#' 5, then the value stored in \code{values} at position 5 is the coefficient
#' 5, then the value stored in \code{values} at row 5 is the coefficient
#' associated with the term x_1^2*x_3.
#'
#' @return Vector containing the evaluation of a single polynomial or
#' matrix containing the evaluation of the polynomials. Each row
#' corresponds to each polynomial used and each column to each observation,
#' meaning that each row vector corresponds to the results of evaluating all the
#' given data for each polynomial.
#' @param newdata Input data as matrix, vector or dataframe.
#' Number of columns (or elements in vector) should be the number of variables
#' in the polynomial (dimension p). Response variable to be predicted should
#' not be included.
#'
#' @return Returns a matrix containing the evaluation of the polynomials.
#' Each column corresponds to each polynomial used and each row to each
#' observation, meaning that each column vector corresponds to the results of
#' evaluating all the given data for each polynomial.
#'
#' @seealso \code{eval_poly()} is also used in [predict.nn2poly()].
#'
#' @examples
#' # Single polynomial evaluation
#' # Create the polynomial 1 + (-1)·x_1 + 1·x_2 + 0.5·(x_1)^2 as a list
#' poly <- list()
#' poly$values <- c(1,-1,1,0.5)
#' poly$labels <- list(c(0),c(1),c(2),c(1,1))
#' # Create two observations, (x_1,x_2) = (1,2) and (x_1,x_2) = (3,1)
#' x <- rbind(c(1,2), c(3,1))
#' newdata <- rbind(c(1,2), c(3,1))
#' # Evaluate the polynomial on both observations
#' eval_poly(x,poly)
#' nn2poly:::eval_poly(poly = poly,newdata = newdata)
#'
#' @export
eval_poly <- function(x, poly) {
#' # Multiple polynomial evaluation, with same terms but different coefficients
#' # Create the polynomial 1 + (-1)·x_1 + 1·x_2 + 0.5·(x_1)^2 as a list
#' poly <- list()
#' coeff_matrix <- cbind(c(1,-1,1,0.5),
#' c(2,-3,0,1.3))
#' poly$values <- coeff_matrix
#' poly$labels <- list(c(0),c(1),c(2),c(1,1))
#' # Create two observations, (x_1,x_2) = (1,2) and (x_1,x_2) = (3,1)
#' new_data <- rbind(c(1,2), c(3,1))
#' # Evaluate the polynomial on both observations
#' nn2poly:::eval_poly(poly = poly, newdata = newdata)
eval_poly <- function(poly, newdata) {

# Remove names and transform into matrix (variables as columns)
x <- unname(as.matrix(x))
newdata <- unname(as.matrix(newdata))

# If x is a single vector, transpose to have it as row vector:
if(ncol(x)==1){
x = t(x)
# If newdata is a single vector, transpose to have it as row vector:
if(ncol(newdata)==1){
newdata = t(newdata)
}

# If values is a single vector, transform into matrix
if (!is.matrix(poly$values)){
poly$values <- t(as.matrix(poly$values))
poly$values <- as.matrix(poly$values)
}

# Detect if the polynomial has intercept or not, needed in later steps
bool_intercept <- FALSE
if (c(0) %in% poly$labels) bool_intercept <- TRUE


# If there is intercept and it is not the first element, reorder the
# polynomial labels and values
if (c(0) %in% poly$labels){
if (bool_intercept){
intercept_position <- which(sapply(poly$labels, function(x) c(0) %in% x))
if (intercept_position != 1){

# Divide again in single observation or matrix form:


# Store the value
intercept_value <- poly$values[,intercept_position]
intercept_value <- poly$values[intercept_position,]

# Remove label and value
poly$labels <- poly$labels[-intercept_position]
poly$values <- poly$values[,-intercept_position, drop = FALSE]
poly$values <- poly$values[-intercept_position,, drop = FALSE]

# Add label and value back at start of list
poly$labels <- append(poly$labels, c(0), after=0)
poly$values <- unname(cbind(intercept_value, poly$values))
poly$values <- unname(rbind(intercept_value, poly$values))
}
}



# Initialize matrix which will contain results for each desired polynomial,
# with rows equal to the rows of `poly$values`, that is, the number of
# polynomials and columns equal to the number of observations evaluated.
n_polynomials <- nrow(poly$values)
response <- matrix(0, nrow = n_polynomials, ncol = nrow(x))
# with columns equal to the columns of `poly$values`, that is, the number of
# polynomials and rows equal to the number of observations evaluated.
n_polynomials <- ncol(poly$values)
response <- matrix(0, nrow = nrow(newdata), ncol = n_polynomials)
for (j in 1:n_polynomials){

# Select the desired polynomial values (row of poly$values)
values_j <- poly$values[j,]
values_j <- poly$values[,j]

# Intercept (label = 0) should always be the first element of labels at this
# point of the function (labels reordered previously)
if (poly$labels[[1]] == c(0)){
response_j <- rep(values_j[1], nrow(x))
if (bool_intercept){
response_j <- rep(values_j[1], nrow(newdata))
start_loop <- 2
} else {
response_j <- rep(0, nrow(x))
response_j <- rep(0, nrow(newdata))
start_loop <- 1
}

Expand All @@ -103,16 +124,16 @@ eval_poly <- function(x, poly) {
# Need to differentiate between 1 single label or more to use rowProds
if(length(label_i) == 1){
# When single variable, it is included in 1:p, that are also the
# number of columns in x
var_prod <- x[,label_i]
# number of columns in newdata
var_prod <- newdata[,label_i]
} else {
# Special case if x is a single observation.
# Selecting the vars in x returns a column instead of row in this case
if(nrow(x)==1){
var_prod <- matrixStats::colProds(as.matrix(x[,label_i]))
# Special case if newdata is a single observation.
# Selecting the vars in newdata returns a column instead of row in this case
if(nrow(newdata)==1){
var_prod <- matrixStats::colProds(as.matrix(newdata[,label_i]))
} else {
# Obtain the product of each variable as many times as label_i indicates
var_prod <- matrixStats::rowProds(x[,label_i])
var_prod <- matrixStats::rowProds(newdata[,label_i])
}

}
Expand All @@ -122,11 +143,11 @@ eval_poly <- function(x, poly) {
# with their associated coefficient value
response_j <- response_j + values_j[i] * var_prod
}
response[j,] <- response_j
response[,j] <- response_j
}

# Check if it is a single polynomial:
if (dim(response)[1]==1){
# Check if it is a single polynomial and transform to vector:
if (dim(response)[2]==1){
response <- as.vector(response)
}

Expand Down
35 changes: 34 additions & 1 deletion R/helpers.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
#' Luz Model composed of a linear stack of layers
#' Build a \code{luz} model composed of a linear stack of layers
#'
#' Helper function to build \code{luz} models as a sequential model, by feeding
#' it a stack of \code{luz} layers.
#'
#' @param ... Sequence of modules to be added.
#'
#' @return A `nn_sequential` module.
#'
#' @details This step is needed so we can get the activation functions and
#' layers and neurons architecture easily with \code{nn2poly:::get_parameters()}.
#' Furthermore, this step is also needed to be able to impose the needed
#' constraints when using the \code{luz/torch} framework.
#'
#' @seealso [add_constraints()]
#'
#' @examples
#' if (requireNamespace("luz", quietly=TRUE)) {
#' # Create a NN using luz/torch as a sequential model
#' # with 3 fully connected linear layers,
#' # the first one with input = 5 variables,
#' # 100 neurons and tanh activation function, the second
#' # one with 50 neurons and softplus activation function
#' # and the last one with 1 linear output.
#' nn <- luz_model_sequential(
#' torch::nn_linear(5,100),
#' torch::nn_tanh(),
#' torch::nn_linear(100,50),
#' torch::nn_softplus(),
#' torch::nn_linear(50,1)
#' )
#'
#' nn
#'
#' # Check that the nn is of class nn_squential
#' class(nn)
#' }
#'
#'
#' @export
luz_model_sequential <- function(...) {
if (!requireNamespace("torch", quietly = TRUE))
Expand Down
Loading