294 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			R
		
	
	
	
	
	
			
		
		
	
	
			294 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			R
		
	
	
	
	
	
#' HOPIR subroutine for the LS solution given preprocessed `X`, `Fy` and initial
 | 
						|
#' values `alphas`.
 | 
						|
#'
 | 
						|
#' @keywords internal
 | 
						|
HOPIR.ls <- function(X, Fy, alphas, sample.axis, algorithm, ..., logger) {
 | 
						|
    # Get axis indices (observation modes)
 | 
						|
    modes <- seq_along(dim(X))[-sample.axis]
 | 
						|
    n <- dim(X)[sample.axis]                    # observation count (scalar)
 | 
						|
    p <- dim(X)[-sample.axis]                   # predictor dimensions (vector)
 | 
						|
 | 
						|
    # Least Squares Deltas Estimates given alphas
 | 
						|
    fun.Deltas <- function(alphas) {
 | 
						|
        # Residuals
 | 
						|
        R <- X - mlm(Fy, alphas, modes = modes)
 | 
						|
        # `Delta` moment estimates
 | 
						|
        Deltas <- Map(mcrossprod, list(R), mode = modes)
 | 
						|
        Map(`*`, p / (n * prod(p)), Deltas)
 | 
						|
    }
 | 
						|
 | 
						|
    # wrap logger, provide unified logger interface for all HOPIR subroutines
 | 
						|
    if (is.function(logger)) {
 | 
						|
        callback <- function(iter, alphas) {
 | 
						|
            logger("ls", iter, alphas, fun.Deltas(alphas))
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        callback <- NULL
 | 
						|
    }
 | 
						|
 | 
						|
    if (algorithm == "icu") {
 | 
						|
        # Call (proper parameterized) Iterative Cyclic Updating Optimizer
 | 
						|
        alphas <- ICU(
 | 
						|
            # Optimization Objective (MSE)
 | 
						|
            fun.loss = function(alphas) mean((X - mlm(Fy, alphas, modes))^2),
 | 
						|
            # Updating rule (optimal solution for `alpha_j` given the rest)
 | 
						|
            fun.update = function(alphas, j) {
 | 
						|
                Z <- mlm(Fy, alphas[-j], modes = modes[-j])
 | 
						|
                # least squares solution for `alpha_j | alpha_i, i != j`
 | 
						|
                alphas[[j]] <- t(solve(
 | 
						|
                    mcrossprod(Z, Z, modes[j]), mcrossprod(Z, X, modes[j])
 | 
						|
                ))
 | 
						|
            },
 | 
						|
            # Initial parameter estimates
 | 
						|
            params = alphas,
 | 
						|
            ...,
 | 
						|
            callback = callback)
 | 
						|
    } else {
 | 
						|
        # Call (proper parameterized) Nesterov Accelerated Gradient Descent
 | 
						|
        alphas <- NAGD(
 | 
						|
            # Setup objective function (MSE)
 | 
						|
            fun.loss = function(alphas) mean((X - mlm(Fy, alphas, modes))^2),
 | 
						|
            # Gradient of the objective with respect to the parameter matirces `alpha_j`
 | 
						|
            fun.grad = function(alphas) {
 | 
						|
                # Residuals
 | 
						|
                R <- X - mlm(Fy, alphas, modes)
 | 
						|
                # Gradients for each alpha
 | 
						|
                Map(function(j) {
 | 
						|
                    # MLM of Fy with alpha_k, k in [r] \ j
 | 
						|
                    Fa <- mlm(Fy, alphas[-j], modes[-j])
 | 
						|
                    # Gradient of the loss with respect to alpha_j
 | 
						|
                    (-2 / prod(dim(X))) * mcrossprod(R, Fa, modes[j])
 | 
						|
                }, seq_along(modes))
 | 
						|
            },
 | 
						|
            params = alphas,
 | 
						|
            # Linear Combination of parameters, basically: a * lhs + b * rhs for each
 | 
						|
            # combination of elements in the LHS and RHS lists with scalars a, b.
 | 
						|
            fun.lincomb = function(a, LHS, b, RHS) {
 | 
						|
                Map(function(lhs, rhs) a * lhs + b * rhs, LHS, RHS)
 | 
						|
            },
 | 
						|
            # squared norm of parameters
 | 
						|
            fun.norm2 = function(params) sum(unlist(params, use.names = FALSE)^2),
 | 
						|
            ...,
 | 
						|
            callback = callback)
 | 
						|
    }
 | 
						|
 | 
						|
    # Final estimate includes Deltas
 | 
						|
    list(alphas = alphas, Deltas = fun.Deltas(alphas))
 | 
						|
}
 | 
						|
 | 
						|
#' HPOIR subroutine for the MLE estimation given preprocessed data and initial
 | 
						|
#' alpha, Delta parameters
 | 
						|
#'
 | 
						|
#' @keywords internal
 | 
						|
HOPIR.mle <- function(X, Fy, alphas, Deltas, sample.axis, algorithm, ..., logger) {
 | 
						|
    # Get axis indices (observation modes)
 | 
						|
    modes <- seq_along(dim(X))[-sample.axis]
 | 
						|
    n <- dim(X)[sample.axis]                    # observation count (scalar)
 | 
						|
    p <- dim(X)[-sample.axis]                   # predictor dimensions (vector)
 | 
						|
 | 
						|
    if (algorithm == "icu") {
 | 
						|
        # Call (proper parameterized) Iterative Cyclic Updating Optimizer
 | 
						|
        params <- ICU(
 | 
						|
            # Optimization Objective (negative log-likelihood)
 | 
						|
            fun.loss = function(params) {
 | 
						|
                # residuals
 | 
						|
                R <- X - mlm(Fy, params$alphas, modes)
 | 
						|
                # negative log-likelihood (without additive constant term)
 | 
						|
                0.5 * (
 | 
						|
                    n * prod(p) * sum(log(unlist(Map(det, params$Deltas))) / p) +
 | 
						|
                    sum(mlm(R, Map(solve, params$Deltas), modes) * R)
 | 
						|
                )
 | 
						|
            },
 | 
						|
            # Updating rule, optimal solution for `alpha_j` or `Delta_j` given
 | 
						|
            # all other parameters
 | 
						|
            fun.update = function(params, index) {
 | 
						|
                # residuals
 | 
						|
                R <- X - mlm(Fy, params$alphas, modes)
 | 
						|
                # mode (axis) index
 | 
						|
                j <- index[2]
 | 
						|
                if (index[1] == 1) {
 | 
						|
                    # compute subterms
 | 
						|
                    Delta.invs <- Map(solve, params$Deltas)
 | 
						|
                    Delta.inv.alphas <- Map(`%*%`, Delta.invs, alphas)
 | 
						|
                    XxDi  <- mlm(X,  Delta.invs[-j],       modes[-j])
 | 
						|
                    Fxa   <- mlm(Fy,           alphas[-j], modes[-j])
 | 
						|
                    FxDia <- mlm(Fy, Delta.inv.alphas[-j], modes[-j])
 | 
						|
                    # alpha update
 | 
						|
                    mcrossprod(XxDi, Fxa, modes[j]) %*%
 | 
						|
                        solve(mcrossprod(FxDia, Fxa, modes[j]))
 | 
						|
                } else { # index[1] == 2
 | 
						|
                    # Delta update
 | 
						|
                    (p[j] / (n * prod(p))) * mcrossprod(
 | 
						|
                        mlm(R, Map(solve, params$Deltas[-j]), modes[-j]), R, modes[j])
 | 
						|
                }
 | 
						|
            },
 | 
						|
            # collection of initial alpha and Delta parameters
 | 
						|
            params = list(alphas = alphas, Deltas = Deltas),
 | 
						|
            # parameter "path"-indices, first index {1, 2} toggles between alphas
 | 
						|
            # and Deltas, second index is the mode.
 | 
						|
            # Example: `list(list("a1", "a2", "a3"), list("D1", "D2", "D3"))[[c(2, 1)]] == "D1"`
 | 
						|
            indices = c(Map(c, 1L, seq_along(modes)), Map(c, 2L, seq_along(modes))),
 | 
						|
            ...,
 | 
						|
            callback = if (is.function(logger)) {
 | 
						|
                    function(iter, params) {
 | 
						|
                        logger("mle", iter, params$alphas, params$Deltas)
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    NULL
 | 
						|
                })
 | 
						|
    } else {
 | 
						|
        # Call (proper parameterized) Nesterov Accelerated Gradient Descent
 | 
						|
        # Note that only the `alphas` are subject of Gradient Descent and the
 | 
						|
        # `Deltas` are additional parameters updated given the cueent `alphas`.
 | 
						|
        # Meaning that the gradient is the gradient of the loss with respect to
 | 
						|
        # the `alphas` only.
 | 
						|
        params <- NAGD(
 | 
						|
            # Setup objective function (negative log-likelihood)
 | 
						|
            fun.loss = function(alphas, Deltas) {
 | 
						|
                # residuals
 | 
						|
                R <- X - mlm(Fy, alphas, modes)
 | 
						|
                # negative log-likelihood (without additive constant term)
 | 
						|
                0.5 * (
 | 
						|
                    n * prod(p) * sum(log(unlist(Map(det, Deltas))) / p) +
 | 
						|
                    sum(mlm(R, Map(solve, Deltas), modes) * R)
 | 
						|
                )
 | 
						|
            },
 | 
						|
            # Gradient of the objective with respect to the parameter matirces
 | 
						|
            # `alphas` only, only the first argument, the second are `more.params`.
 | 
						|
            fun.grad = function(alphas, Deltas) {
 | 
						|
                # Residuals
 | 
						|
                R <- X - mlm(Fy, alphas, modes)
 | 
						|
                # Gradients for each alpha
 | 
						|
                Map(function(j) {
 | 
						|
                    # MLM of Fy with alpha_k, k in [r] \ j
 | 
						|
                    Fa <- mlm(Fy, alphas[-j], modes[-j])
 | 
						|
                    # Gradient of the loss with respect to alpha_j
 | 
						|
                    (-2 / prod(dim(X))) * mcrossprod(R, Fa, modes[j])
 | 
						|
                }, seq_along(modes))
 | 
						|
            },
 | 
						|
            # Initial parameters (subject to Gradient Descent)
 | 
						|
            params = alphas,
 | 
						|
            # Initial additional parameters (subject to updating given `params`)
 | 
						|
            more.params = Deltas,
 | 
						|
            # Update `Deltas` given `alphas`
 | 
						|
            fun.more.params = function(alphas, old.Deltas) {
 | 
						|
                # Residuals
 | 
						|
                R <- X - mlm(Fy, alphas, modes)
 | 
						|
                # Solve cross dependent System of Delta equations using the ICU
 | 
						|
                # algorithm (with a small number of iterations, should be enough)
 | 
						|
                ICU(
 | 
						|
                    fun.loss = function(Deltas) {
 | 
						|
                        # negative log-likelihood (without additive constant term)
 | 
						|
                        0.5 * (
 | 
						|
                            n * prod(p) * sum(log(unlist(Map(det, Deltas))) / p) +
 | 
						|
                            sum(mlm(R, Map(solve, Deltas), modes) * R)
 | 
						|
                        )
 | 
						|
                    },
 | 
						|
                    fun.update = function(Deltas, j) {
 | 
						|
                        # Delta update
 | 
						|
                        (p[j] / (n * prod(p))) * mcrossprod(
 | 
						|
                            mlm(R, Map(solve, Deltas[-j]), modes[-j]), R, modes[j])
 | 
						|
                    },
 | 
						|
                    params = old.Deltas,
 | 
						|
                    max.iter = 5L)
 | 
						|
            },
 | 
						|
            # Linear Combination of parameters, basically: a * lhs + b * rhs for each
 | 
						|
            # combination of elements in the LHS and RHS lists with scalars a, b.
 | 
						|
            fun.lincomb = function(a, LHS, b, RHS) {
 | 
						|
                Map(function(lhs, rhs) a * lhs + b * rhs, LHS, RHS)
 | 
						|
            },
 | 
						|
            # squared norm of parameters
 | 
						|
            fun.norm2 = function(params) sum(unlist(params, use.names = FALSE)^2),
 | 
						|
            ...,
 | 
						|
            callback = if (is.function(logger)) {
 | 
						|
                    function(iter, alphas, Deltas) {
 | 
						|
                        logger("mle", iter, alphas, Deltas)
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    NULL
 | 
						|
                })
 | 
						|
        # Remap parameter names
 | 
						|
        names(params) <- c("alphas", "Deltas")
 | 
						|
    }
 | 
						|
 | 
						|
    params
 | 
						|
}
 | 
						|
 | 
						|
#' Higher Order Parametric Inverse Regression
 | 
						|
#'
 | 
						|
#' @export
 | 
						|
HOPIR <- function(X, Fy, sample.axis, method = c("ls", "mle"),
 | 
						|
    algorithm = c("icu", "nagd"), ..., center = TRUE, logger = NULL
 | 
						|
) {
 | 
						|
    # Predair and check input parameters
 | 
						|
    method <- match.arg(method)
 | 
						|
    algorithm <- match.arg(algorithm)
 | 
						|
    # ensure response is tensor valued
 | 
						|
    if (!is.array(Fy)) {
 | 
						|
        # scalar response case (add new axis of size 1)
 | 
						|
        dim(Fy) <- ifelse(seq_along(dim(X)) == sample.axis, dim(X)[sample.axis], 1L)
 | 
						|
    }
 | 
						|
    # Check dimensions and matching of axis (tensor order)
 | 
						|
    stopifnot(exprs = {
 | 
						|
        length(dim(X)) == length(dim(Fy))
 | 
						|
        dim(X)[sample.axis] == dim(Fy)[sample.axis]
 | 
						|
    })
 | 
						|
    # warn about occurence of an axis without reduction
 | 
						|
    if (any(dim(Fy)[-sample.axis] >= dim(X)[-sample.axis])) {
 | 
						|
        warning("Degenerate case 'any(dim(Fy)[-sample.axis] >= dim(X)[-sample.axis])'")
 | 
						|
    }
 | 
						|
 | 
						|
    # Set the default logger
 | 
						|
    if (is.logical(logger) && logger) {
 | 
						|
        start <- Sys.time()
 | 
						|
        logger <- function(method, iter, ...) {
 | 
						|
            if (iter) {
 | 
						|
                cat(sprintf("%4d - %s - Elapsed: %s\n",
 | 
						|
                    iter, method, format(Sys.time() - start)))
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    # Get axis indices (observation modes)
 | 
						|
    modes <- seq_along(dim(X))[-sample.axis]
 | 
						|
    n <- dim(X)[sample.axis]                    # observation count (scalar)
 | 
						|
    p <- dim(X)[-sample.axis]                   # predictor dimensions (vector)
 | 
						|
 | 
						|
    # center data (predictors and responses)
 | 
						|
    if (center) {
 | 
						|
        # Means for X and Fy (a.k.a. sum elements over the sample axis)
 | 
						|
        meanX <- apply(X, modes, mean, simplify = TRUE)
 | 
						|
        meanFy <- apply(Fy, modes, mean, simplify = TRUE)
 | 
						|
        # Center both X and Fy
 | 
						|
        X <- sweep(X, modes, meanX)
 | 
						|
        Fy <- sweep(Fy, modes, meanFy)
 | 
						|
    } else {
 | 
						|
        meanX <- meanFy <- NA
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    ### Step 0: Initial parameter estimates HOPCA
 | 
						|
    alphas <- Map(function(mode, ncol) {
 | 
						|
        La.svd(mcrossprod(X, mode = mode), ncol)$u
 | 
						|
    }, modes, dim(Fy)[modes])
 | 
						|
 | 
						|
    ### Step 1: LS estimate
 | 
						|
    ls <- HOPIR.ls(X, Fy, alphas, sample.axis,
 | 
						|
        algorithm, ..., logger = logger)
 | 
						|
 | 
						|
    ### Step 2: MLE estimate
 | 
						|
    if (method == "mle") {
 | 
						|
        mle <- HOPIR.mle(X, Fy, ls$alphas, ls$Deltas, sample.axis,
 | 
						|
            algorithm, ..., logger = logger)
 | 
						|
 | 
						|
        # add means and LS estimates as attribute
 | 
						|
        structure(
 | 
						|
            c(mle, list(meanX = meanX, meanFy = meanFy)),
 | 
						|
            ls = ls
 | 
						|
        )
 | 
						|
    } else {
 | 
						|
        # add means to LS estimates
 | 
						|
        c(ls, list(meanX = meanX, meanFy = meanFy))
 | 
						|
    }
 | 
						|
}
 |