Introduction to mwTensor

Koki Tsuyuzaki

2026-05-07

Overview

mwTensor is an R package for Multi-Way Component Analysis (MWCA). It provides solvers for decomposing single or multiple matrices and tensors with flexible choices of matrix factorization algorithms (SVD, NMF, ICA, CX, etc.).

Key functions:

Function Purpose
MWCA Decompose a single tensor
CoupledMWCA Decompose multiple coupled tensors simultaneously
checkCoupledMWCA Validate parameters before optimization
initCoupledMWCA Generate reproducible initial values
refineFactor One-step decomposition of a factor (experimental)
MWCAProgram Declarative factorization program representation

1. Single Tensor Decomposition with MWCA

MWCA applies a matrix factorization method to each mode of a single tensor.

library(mwTensor)
#> Warning: no DISPLAY variable so Tk is not available

# Create a 3-way tensor (10 x 12 x 8)
set.seed(123)
X <- array(runif(10 * 12 * 8), dim = c(10, 12, 8))

# Default parameters: SVD on each mode, lower dim = 2
params <- defaultMWCAParams(X)
params@algorithms  # decomposition method per mode
#> [1] "mySVD" "mySVD" "mySVD"
params@dims        # target lower dimension per mode
#> [1] 2 2 2
out <- MWCA(params)

# Factor matrices: one per mode
# Each is dims[i] x dim(X)[i]
lapply(out@factors, dim)
#> [[1]]
#> [1]  2 10
#> 
#> [[2]]
#> [1]  2 12
#> 
#> [[3]]
#> [1] 2 8

# Core tensor
dim(out@core)
#> [1] 2 2 2

# Reconstruction error
out@rec_error
#> [1] 8.776407

Choosing different algorithms per mode

params2 <- new("MWCAParams",
    X = X,
    mask = NULL,
    pseudocount = 1e-10,
    algorithms = c("mySVD", "myNMF", "myICA"),
    dims = c(3L, 4L, 2L),
    transpose = FALSE,
    viz = FALSE,
    figdir = NULL)
out2 <- MWCA(params2)
lapply(out2@factors, dim)
#> [[1]]
#> [1]  3 10
#> 
#> [[2]]
#> [1]  4 12
#> 
#> [[3]]
#> [1] 2 8

Available algorithms: mySVD, myALS_SVD, myNMF, myICA, myCX.

2. Coupled Tensor Decomposition with CoupledMWCA

CoupledMWCA decomposes multiple tensors that share dimensions. Shared dimensions get common factor matrices; unshared dimensions get specific factor matrices.

Toy data

Xs <- toyModel("coupled_CP_Easy")
# X1: 20x30 matrix, X2: 30x30x30 tensor, X3: 30x25 matrix
lapply(Xs, dim)
#> $X1
#> [1] 20 30
#> 
#> $X2
#> [1] 30 30 30
#> 
#> $X3
#> [1] 30 25

The coupling structure:

I2 is shared between X1 and X2. I4 is shared between X2 and X3.

Setting up the model

# common_model maps: block -> (mode_name -> factor_name)
common_model <- list(
    X1 = list(I1 = "A1", I2 = "A2"),
    X2 = list(I2 = "A2", I3 = "A3", I4 = "A4"),
    X3 = list(I4 = "A4", I5 = "A5"))

# A2 appears in both X1 and X2 -> shared factor for dimension I2
# A4 appears in both X2 and X3 -> shared factor for dimension I4

Running with default parameters

params <- defaultCoupledMWCAParams(Xs, common_model)

# Inspect defaults
params@common_algorithms  # "mySVD" for all
#> $A1
#> [1] "mySVD"
#> 
#> $A2
#> [1] "mySVD"
#> 
#> $A3
#> [1] "mySVD"
#> 
#> $A4
#> [1] "mySVD"
#> 
#> $A5
#> [1] "mySVD"
params@common_dims        # 2 for all
#> $A1
#> [1] 2
#> 
#> $A2
#> [1] 2
#> 
#> $A3
#> [1] 2
#> 
#> $A4
#> [1] 2
#> 
#> $A5
#> [1] 2
params@common_coretype    # "Tucker"
#> [1] "Tucker"

out <- CoupledMWCA(params)

# Common factor matrices
lapply(out@common_factors, dim)
#> $A1
#> [1]  2 20
#> 
#> $A2
#> [1]  2 30
#> 
#> $A3
#> [1]  2 30
#> 
#> $A4
#> [1]  2 30
#> 
#> $A5
#> [1]  2 25

# Convergence
tail(out@rec_error)
#>       95       96       97       98       99      100 
#> 7913.081 7913.081 7913.081 7913.081 7913.081 7913.081

Customizing algorithms and dimensions

params@common_algorithms <- list(
    A1 = "mySVD", A2 = "myNMF", A3 = "myNMF",
    A4 = "mySVD", A5 = "myCX")
params@common_dims <- list(
    A1 = 3, A2 = 3, A3 = 5, A4 = 4, A5 = 4)
params@common_iteration <- list(
    A1 = 5, A2 = 5, A3 = 5, A4 = 5, A5 = 5)

out2 <- CoupledMWCA(params)
lapply(out2@common_factors, dim)
#> $A1
#> [1]  3 20
#> 
#> $A2
#> [1]  3 30
#> 
#> $A3
#> [1]  5 30
#> 
#> $A4
#> [1]  4 30
#> 
#> $A5
#> [1]  4 25

3. Validating Parameters with checkCoupledMWCA

Before running a potentially expensive optimization, validate the parameter object. Unlike CoupledMWCA which stops on the first error, checkCoupledMWCA collects all errors and warnings.

result <- checkCoupledMWCA(params)
result$ok
#> [1] TRUE
result$summary
#> [1] "Validation passed"
# Introduce multiple errors
bad_params <- params
bad_params@common_algorithms$A1 <- "nonExistentAlgo"
bad_params@common_iteration$A2 <- 1.5  # must be integer
bad_params@common_coretype <- "TUCKER"  # must be "Tucker" or "CP"

result <- checkCoupledMWCA(bad_params)
result$ok
#> [1] FALSE
result$errors
#> [1] "nonExistentAlgo is not defined in .GlovalEnv"                  
#> [2] "params@common_iteration must be specified as an integer vector"
#> [3] "params@common_coretype must be 'Tucker' or 'CP'"
result$summary
#> [1] "Validation failed: 3 error(s), 0 warning(s)"

4. Reproducible Initialization with initCoupledMWCA

initCoupledMWCA generates initial factor matrices without running optimization. This is useful for:

params_ok <- defaultCoupledMWCAParams(Xs, common_model)

# Random initialization with seed
init <- initCoupledMWCA(params_ok, seed = 42L)
init@init_policy
#> [1] "random"
lapply(init@common_factors, dim)
#> $A1
#> [1]  2 20
#> 
#> $A2
#> [1]  2 30
#> 
#> $A3
#> [1]  2 30
#> 
#> $A4
#> [1]  2 30
#> 
#> $A5
#> [1]  2 25

Initialization policies

# SVD-based initialization
init_svd <- initCoupledMWCA(params_ok, seed = 42L, init_policy = "svd")

# Non-negative random (safe for NMF)
init_nn <- initCoupledMWCA(params_ok, seed = 42L, init_policy = "nonneg_random")

# Verify non-negativity
all(init_nn@common_factors$A1 >= 0)
#> [1] TRUE

Using initialized values for optimization

A CoupledMWCAInit object can be passed directly to CoupledMWCA. The pre-computed factors are used as the starting point.

# Initialize, then optimize
init <- initCoupledMWCA(params_ok, seed = 42L, init_policy = "svd")
out <- CoupledMWCA(init)

# Same seed -> same result
out_a <- CoupledMWCA(initCoupledMWCA(params_ok, seed = 123L))
out_b <- CoupledMWCA(initCoupledMWCA(params_ok, seed = 123L))
identical(out_a@common_factors$A1, out_b@common_factors$A1)
#> [1] TRUE

5. Factor Refinement with refineFactor (Experimental)

refineFactor applies a one-step matrix factorization to a factor obtained from a previous decomposition. This is a proof-of-concept for “decomposition of a decomposition.”

Mathematical interpretation

Given a factor matrix A (k x n), the refinement decomposes t(A) (n x k):

t(A) ~ U * V

where U is (n x dim) and V is (dim x k). This gives:

set.seed(42)
X <- matrix(runif(20 * 30), nrow = 20, ncol = 30)
params_mat <- defaultMWCAParams(X)
params_mat@dims <- c(5L, 5L)
fit <- MWCA(params_mat)

# Factor 1 is 5 x 20. Refine it down to dim=2.
ref <- refineFactor(fit, 1L, algorithm = "mySVD", dim = 2L)
dim(ref@sub_factors)  # 2 x 20
#> [1]  2 20
dim(ref@coef)         # 5 x 2
#> [1] 5 2

# Approximation quality
max(abs(ref@source_factor - ref@coef %*% ref@sub_factors))
#> [1] 0.5069393

Refinement on CoupledMWCA results

params_c <- defaultCoupledMWCAParams(Xs, common_model)
params_c@common_dims <- list(A1=3, A2=3, A3=3, A4=3, A5=3)
fit_c <- CoupledMWCA(params_c)

# Refine common factor A2 (3 x 30) with NMF
ref_A2 <- refineFactor(fit_c, "A2", algorithm = "myNMF", dim = 2L)
dim(ref_A2@sub_factors)  # 2 x 30
#> [1]  2 30
dim(ref_A2@coef)         # 3 x 2
#> [1] 3 2

6. Factorization Programs with MWCAProgram (Experimental)

MWCAProgram provides a declarative way to describe factorization problems, including one-step factor refinements. This is an internal representation layer designed for future recursive decomposition support.

Defining a program

prog <- MWCAProgram(
    blocks = list(
        X1 = MWCAProgramBlock(
            modes = c("I1", "I2"),
            factor_map = c(I1 = "A1", I2 = "A2")),
        X2 = MWCAProgramBlock(
            modes = c("I2", "I3", "I4"),
            factor_map = c(I2 = "A2", I3 = "A3", I4 = "A4")),
        X3 = MWCAProgramBlock(
            modes = c("I4", "I5"),
            factor_map = c(I4 = "A4", I5 = "A5"))),
    factors = list(
        A1 = MWCAProgramFactor(mode = "I1", dim = 3),
        A2 = MWCAProgramFactor(mode = "I2", dim = 3),
        A3 = MWCAProgramFactor(mode = "I3", dim = 5, algorithm = "myNMF"),
        A4 = MWCAProgramFactor(mode = "I4", dim = 4),
        A5 = MWCAProgramFactor(mode = "I5", dim = 4)))
print(prog)
#> MWCAProgram
#>   Blocks:      3
#>   Factors:     5
#>   Refinements: 0
#>   Block details:
#>     X1 [common]: modes=I1,I2 -> factors={A1, A2}
#>     X2 [common]: modes=I2,I3,I4 -> factors={A2, A3, A4}
#>     X3 [common]: modes=I4,I5 -> factors={A4, A5}
#>   Factor details:
#>     A1 [common]: mode=I1, dim=3, status=decomposed, algo=mySVD
#>     A2 [common]: mode=I2, dim=3, status=decomposed, algo=mySVD
#>     A3 [common]: mode=I3, dim=5, status=decomposed, algo=myNMF
#>     A4 [common]: mode=I4, dim=4, status=decomposed, algo=mySVD
#>     A5 [common]: mode=I5, dim=4, status=decomposed, algo=mySVD

Validation

v <- validateMWCAProgram(prog)
v$ok
#> [1] TRUE
v$summary
#> [1] "Program is valid"

Compiling to solver parameters

compiled <- compileMWCAProgram(prog, Xs)
is(compiled, "CoupledMWCAParams")
#> [1] TRUE
compiled@common_dims
#> $A1
#> [1] 3
#> 
#> $A2
#> [1] 3
#> 
#> $A3
#> [1] 5
#> 
#> $A4
#> [1] 4
#> 
#> $A5
#> [1] 4
compiled@common_algorithms$A3  # "myNMF" as specified
#> [1] "myNMF"

Programs with refinement

prog_ref <- MWCAProgram(
    blocks = prog$blocks,
    factors = prog$factors,
    refinements = list(
        R1 = MWCAProgramRefinement(
            source_factor = "A2",
            algorithm = "mySVD",
            dim = 2)))

result <- executeMWCAProgram(prog_ref, Xs)
is(result$fit, "CoupledMWCAResult")
#> [1] TRUE
length(result$refinements)  # 1
#> [1] 1
dim(result$refinements$R1@sub_factors)
#> [1]  2 30

Factor status: decomposed, fixed, frozen

prog_status <- MWCAProgram(
    blocks = list(
        X1 = MWCAProgramBlock(
            modes = c("I1", "I2"),
            factor_map = c(I1 = "A1", I2 = "A2")),
        X2 = MWCAProgramBlock(
            modes = c("I2", "I3"),
            factor_map = c(I2 = "A2", I3 = "A3"))),
    factors = list(
        A1 = MWCAProgramFactor(mode = "I1", dim = 3, status = "decomposed"),
        A2 = MWCAProgramFactor(mode = "I2", dim = 3, status = "fixed"),
        A3 = MWCAProgramFactor(mode = "I3", dim = 3, status = "frozen")))
# decomposed: updated by solver
# fixed:      initialized but not updated (fix=TRUE)
# frozen:     identity-like, not decomposed (decomp=FALSE)
validateMWCAProgram(prog_status)$ok
#> [1] TRUE

Session Info

sessionInfo()
#> R version 4.4.3 (2025-02-28)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Rocky Linux 9.5 (Blue Onyx)
#> 
#> Matrix products: default
#> BLAS:   /opt/R/4.4.3/lib64/R/lib/libRblas.so 
#> LAPACK: /opt/R/4.4.3/lib64/R/lib/libRlapack.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=C              
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Asia/Tokyo
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] mwTensor_1.2.2
#> 
#> loaded via a namespace (and not attached):
#>  [1] dotCall64_1.2       gtable_0.3.6        ellipse_0.5.0      
#>  [4] spam_2.11-3         xfun_0.57           bslib_0.10.0       
#>  [7] ggplot2_4.0.3       tagcloud_0.7.0      ggrepel_0.9.8      
#> [10] lattice_0.22-6      vctrs_0.7.3         tools_4.4.3        
#> [13] generics_0.1.4      parallel_4.4.3      tibble_3.3.1       
#> [16] rARPACK_0.11-0      pkgconfig_2.0.3     Matrix_1.7-5       
#> [19] RColorBrewer_1.1-3  S7_0.2.2            mixOmics_6.30.0    
#> [22] groupICA_0.1.1      lifecycle_1.0.5     compiler_4.4.3     
#> [25] farver_2.1.2        stringr_1.6.0       fields_17.3        
#> [28] iTensor_1.0.2       codetools_0.2-20    misc3d_0.9-2       
#> [31] einsum_0.1.2        htmltools_0.5.9     maps_3.4.3         
#> [34] sass_0.4.10         yaml_2.3.12         pillar_1.11.1      
#> [37] jquerylib_0.1.4     tidyr_1.3.2         MASS_7.3-65        
#> [40] BiocParallel_1.40.2 cachem_1.1.0        nlme_3.1-167       
#> [43] RSpectra_0.16-2     nnTensor_1.3.0      tidyselect_1.2.1   
#> [46] digest_0.6.39       stringi_1.8.7       dplyr_1.2.1        
#> [49] reshape2_1.4.4      purrr_1.2.2         splines_4.4.3      
#> [52] fastmap_1.2.0       grid_4.4.3          cli_3.6.6          
#> [55] rTensor_1.4.9       ccTensor_1.0.3      magrittr_2.0.5     
#> [58] corpcor_1.6.10      scales_1.4.0        plot3D_1.4.2       
#> [61] rmarkdown_2.31      matrixStats_1.4.1   igraph_2.1.4       
#> [64] otel_0.2.0          gridExtra_2.3       evaluate_1.0.5     
#> [67] knitr_1.51          tcltk_4.4.3         viridisLite_0.4.3  
#> [70] mgcv_1.9-1          jointDiag_0.4       rlang_1.2.0        
#> [73] Rcpp_1.1.1-1.1      glue_1.8.1          geigen_2.3         
#> [76] jsonlite_2.0.0      R6_2.6.1            plyr_1.8.9