2022-03-22 15:26:24 +00:00
|
|
|
#' (Slightly altered) old implementation
|
|
|
|
#'
|
|
|
|
#' @export
|
2022-04-29 16:37:25 +00:00
|
|
|
kpir.base <- function(X, Fy, shape = c(dim(X)[-1], dim(Fy[-1])),
|
2022-03-22 15:26:24 +00:00
|
|
|
method = c("mle", "ls"),
|
|
|
|
eps1 = 1e-10, eps2 = 1e-10, max.iter = 500L,
|
|
|
|
logger = NULL
|
|
|
|
) {
|
|
|
|
|
2022-04-29 16:37:25 +00:00
|
|
|
# Check if X and Fy have same number of observations
|
|
|
|
stopifnot(nrow(X) == NROW(Fy))
|
|
|
|
n <- nrow(X) # Number of observations
|
|
|
|
|
|
|
|
# Get and check predictor dimensions
|
|
|
|
if (length(dim(X)) == 2L) {
|
|
|
|
stopifnot(!missing(shape))
|
|
|
|
stopifnot(ncol(X) == prod(shape[1:2]))
|
|
|
|
p <- as.integer(shape[1]) # Predictor "height"
|
|
|
|
q <- as.integer(shape[2]) # Predictor "width"
|
|
|
|
} else if (length(dim(X)) == 3L) {
|
|
|
|
p <- dim(X)[2]
|
|
|
|
q <- dim(X)[3]
|
|
|
|
dim(X) <- c(n, p * q)
|
|
|
|
} else {
|
|
|
|
stop("'X' must be a matrix or 3-tensor")
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get and check response dimensions
|
|
|
|
if (!is.array(Fy)) {
|
|
|
|
Fy <- as.array(Fy)
|
|
|
|
}
|
|
|
|
if (length(dim(Fy)) == 1L) {
|
|
|
|
k <- r <- 1L
|
|
|
|
dim(Fy) <- c(n, 1L)
|
|
|
|
} else if (length(dim(Fy)) == 2L) {
|
|
|
|
stopifnot(!missing(shape))
|
|
|
|
stopifnot(ncol(Fy) == prod(shape[3:4]))
|
|
|
|
k <- as.integer(shape[3]) # Response functional "height"
|
|
|
|
r <- as.integer(shape[4]) # Response functional "width"
|
|
|
|
} else if (length(dim(Fy)) == 3L) {
|
|
|
|
k <- dim(Fy)[2]
|
|
|
|
r <- dim(Fy)[3]
|
|
|
|
dim(Fy) <- c(n, k * r)
|
|
|
|
} else {
|
|
|
|
stop("'Fy' must be a vector, matrix or 3-tensor")
|
|
|
|
}
|
|
|
|
|
2022-03-22 15:26:24 +00:00
|
|
|
log.likelihood <- function(par, X, Fy, Delta.inv, da, db) {
|
|
|
|
alpha <- matrix(par[1:prod(da)], da[1L])
|
|
|
|
beta <- matrix(par[(prod(da) + 1):length(par)], db[1L])
|
|
|
|
error <- X - tcrossprod(Fy, kronecker(alpha, beta))
|
|
|
|
sum(error * (error %*% Delta.inv))
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate method using unexact matching.
|
|
|
|
method <- match.arg(method)
|
|
|
|
|
|
|
|
### Step 1: (Approx) Least Squares solution for `X = Fy B' + epsilon`
|
|
|
|
cpFy <- crossprod(Fy)
|
|
|
|
if (n <= k * r || qr(cpFy)$rank < k * r) {
|
|
|
|
# In case of under-determined system replace the inverse in the normal
|
|
|
|
# equation by the Moore-Penrose Pseudo Inverse
|
|
|
|
B <- t(matpow(cpFy, -1) %*% crossprod(Fy, X))
|
|
|
|
} else {
|
|
|
|
# Compute OLS estimate by the Normal Equation
|
|
|
|
B <- t(solve(cpFy, crossprod(Fy, X)))
|
|
|
|
}
|
|
|
|
|
|
|
|
# Estimate alpha, beta as nearest kronecker approximation.
|
2022-04-29 16:37:25 +00:00
|
|
|
c(alpha, beta) %<-% approx.kronecker(B, c(q, r), c(p, k))
|
2022-03-22 15:26:24 +00:00
|
|
|
|
|
|
|
if (method == "ls") {
|
|
|
|
# Estimate Delta.
|
|
|
|
B <- kronecker(alpha, beta)
|
|
|
|
rank <- if (ncol(Fy) == 1) 1L else qr(Fy)$rank
|
|
|
|
resid <- X - tcrossprod(Fy, B)
|
|
|
|
Delta <- crossprod(resid) / (nrow(X) - rank)
|
|
|
|
|
|
|
|
} else { # mle
|
|
|
|
B <- kronecker(alpha, beta)
|
|
|
|
|
|
|
|
# Compute residuals
|
|
|
|
resid <- X - tcrossprod(Fy, B)
|
|
|
|
|
|
|
|
# Estimate initial Delta.
|
|
|
|
Delta <- crossprod(resid) / nrow(X)
|
|
|
|
|
|
|
|
# call logger with initial starting value
|
|
|
|
if (is.function(logger)) {
|
|
|
|
# Transformed Residuals (using `matpow` as robust inversion algo,
|
|
|
|
# uses Moore-Penrose Pseudo Inverse in case of singular `Delta`)
|
|
|
|
resid.trans <- resid %*% matpow(Delta, -1)
|
|
|
|
loss <- 0.5 * (nrow(X) * log(det(Delta)) + sum(resid.trans * resid))
|
|
|
|
logger(0L, loss, alpha, beta, Delta, NA)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (iter in 1:max.iter) {
|
|
|
|
# Optimize log-likelihood for alpha, beta with fixed Delta.
|
|
|
|
opt <- optim(c(alpha, beta), log.likelihood, gr = NULL,
|
2022-04-29 16:37:25 +00:00
|
|
|
X, Fy, matpow(Delta, -1), c(q, r), c(p, k))
|
2022-03-22 15:26:24 +00:00
|
|
|
# Store previous alpha, beta and Delta (for break consition).
|
|
|
|
Delta.last <- Delta
|
|
|
|
B.last <- B
|
|
|
|
# Extract optimized alpha, beta.
|
2022-04-29 16:37:25 +00:00
|
|
|
alpha <- matrix(opt$par[1:(q * r)], q, r)
|
|
|
|
beta <- matrix(opt$par[(q * r + 1):length(opt$par)], p, k)
|
2022-03-22 15:26:24 +00:00
|
|
|
# Calc new Delta with likelihood optimized alpha, beta.
|
|
|
|
B <- kronecker(alpha, beta)
|
|
|
|
resid <- X - tcrossprod(Fy, B)
|
|
|
|
Delta <- crossprod(resid) / nrow(X)
|
|
|
|
|
|
|
|
# call logger before break condition check
|
|
|
|
if (is.function(logger)) {
|
|
|
|
# Transformed Residuals (using `matpow` as robust inversion algo,
|
|
|
|
# uses Moore-Penrose Pseudo Inverse in case of singular `Delta`)
|
|
|
|
resid.trans <- resid %*% matpow(Delta, -1)
|
|
|
|
loss <- 0.5 * (nrow(X) * log(det(Delta)) + sum(resid.trans * resid))
|
|
|
|
logger(iter, loss, alpha, beta, Delta, NA)
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check break condition 1.
|
2022-04-29 16:37:25 +00:00
|
|
|
if (norm(Delta - Delta.last, "F") < eps1 * norm(Delta, "F")) {
|
2022-03-22 15:26:24 +00:00
|
|
|
# Check break condition 2.
|
2022-04-29 16:37:25 +00:00
|
|
|
if (norm(B - B.last, "F") < eps2 * norm(B, "F")) {
|
2022-03-22 15:26:24 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# calc final loss
|
|
|
|
resid.trans <- resid %*% matpow(Delta, -1)
|
|
|
|
loss <- 0.5 * (nrow(X) * log(det(Delta)) + sum(resid.trans * resid))
|
|
|
|
|
|
|
|
list(loss = loss, alpha = alpha, beta = beta, Delta = Delta)
|
|
|
|
}
|