Alternating least squares for Tucker model

Copyright 2022 National Technology & Engineering Solutions of Sandia,
LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the
U.S. Government retains certain rights in this software.

The function pyttb.tucker_als() computes the best rank-\((R_1,R_2,\ldots,R_n)\) approximation of tensor \(\mathcal{X}\), according to the specified dimensions in the vector \((R_1,R_2,\ldots,R_n)\). The form of \(\mathcal{X}\) can be a tensor, sptensor, ktensor, or ttensor. The result is a ttensor.

The method is originally from Tucker (1966) and later revisited in De Lathauwer et al. (2000).

  • L. R. Tucker, Some mathematical notes on three-mode factor analysis, Psychometrika, 31:279-311, 1966, http://dx.doi.org/10.1007/BF02289464

  • L. De Lathauwer, B. De Moor, J. Vandewalle, On the best rank-1 and rank-(R_1, R_2, R_N) approximation of higher-order tensors, SIAM J. Matrix Analysis and Applications, 21:1324-1342, 2000, http://doi.org/10.1137/S0895479898346995

Note: Oftentimes it’s better to use pyttb.hosvd() instead.

import os
import sys
import pyttb as ttb
import numpy as np

Create a data tensor of shape (5, 4, 3)

np.random.seed(0)  # Set seed for reproducibility
X = ttb.sptenrand(
    [5, 4, 3], nonzeros=10
)  # Create a tensor with 10 nonzeros using the 'nonzeros' param.
X
sparse tensor of shape (5, 4, 3) with 10 nonzeros and order F
[0, 0, 2] = 0.26455561210462697
[0, 2, 0] = 0.7742336894342167
[1, 3, 1] = 0.45615033221654855
[2, 1, 1] = 0.5684339488686485
[2, 2, 1] = 0.018789800436355142
[2, 3, 0] = 0.6176354970758771
[2, 3, 2] = 0.6120957227224214
[3, 1, 2] = 0.6169339968747569
[3, 3, 2] = 0.9437480785146242
[4, 2, 1] = 0.6818202991034834

Create an approximation with all ranks equal to 2

T = ttb.tucker_als(X, 2)  # best rank(2,2,2) approximation
T
Tucker Alternating Least-Squares:

 Iter 0: fit = 3.447071e-01 fitdelta = 3.4e-01
 Iter 1: fit = 3.633748e-01 fitdelta = 1.9e-02
 Iter 2: fit = 3.666282e-01 fitdelta = 3.3e-03
 Iter 3: fit = 3.689686e-01 fitdelta = 2.3e-03
 Iter 4: fit = 3.706438e-01 fitdelta = 1.7e-03
 Iter 5: fit = 3.717753e-01 fitdelta = 1.1e-03
 Iter 6: fit = 3.725083e-01 fitdelta = 7.3e-04
 Iter 7: fit = 3.729702e-01 fitdelta = 4.6e-04
 Iter 8: fit = 3.732561e-01 fitdelta = 2.9e-04
 Iter 9: fit = 3.734310e-01 fitdelta = 1.7e-04
 Iter 10: fit = 3.735372e-01 fitdelta = 1.1e-04
 Iter 11: fit = 3.736015e-01 fitdelta = 6.4e-05
(Tensor of shape: (5, 4, 3)
 	Core is a
 		tensor of shape (2, 2, 2) with order F
 		data[:, :, 0] =
 		[[ 1.27279000e+00 -1.67070163e-03]
 		 [ 1.01465015e-04  2.79556305e-01]]
 		data[:, :, 1] =
 		[[-0.15850994  0.00097856]
 		 [ 0.00311484  0.72168356]]
 	U[0] = 
 		[[ 5.54196607e-04  9.98323007e-01]
 		 [ 3.62027807e-02 -5.10572842e-06]
 		 [ 5.43516219e-01  1.94246971e-03]
 		 [ 8.38617413e-01 -1.92351491e-03]
 		 [ 7.34446743e-05  5.78247452e-02]]
 	U[1] = 
 		[[-5.54213670e-05 -5.63307471e-04]
 		 [ 4.16133877e-01 -1.66331412e-03]
 		 [ 2.36213646e-03  9.99996772e-01]
 		 [ 9.09300288e-01 -1.83657557e-03]]
 	U[2] = 
 		[[ 0.35603206  0.93309453]
 		 [ 0.11412179  0.01048844]
 		 [ 0.92747905 -0.35947823]],
 [None,
  array([[0.3595079 , 0.43703195],
         [0.6976312 , 0.06022547],
         [0.66676672, 0.67063787],
         [0.21038256, 0.1289263 ]]),
  array([[0.31542835, 0.36371077],
         [0.57019677, 0.43860151],
         [0.98837384, 0.10204481]])],
 {'params': (0.0001, 1000, 1, array([0, 1, 2])),
  'iters': 11,
  'normresidual': np.float64(1.2038024607325746),
  'fit': np.float64(0.3736014755013256)})

Create an approximation with specific ranks of [2, 2, 1]

T = ttb.tucker_als(X, [2, 2, 1])  # best rank(2,2,1) approximation
T
Tucker Alternating Least-Squares:

 Iter 0: fit = 9.473514e-02 fitdelta = 9.5e-02
 Iter 1: fit = 2.296442e-01 fitdelta = 1.3e-01
 Iter 2: fit = 2.670080e-01 fitdelta = 3.7e-02
 Iter 3: fit = 2.755574e-01 fitdelta = 8.5e-03
 Iter 4: fit = 2.770953e-01 fitdelta = 1.5e-03
 Iter 5: fit = 2.773711e-01 fitdelta = 2.8e-04
 Iter 6: fit = 2.774238e-01 fitdelta = 5.3e-05
(Tensor of shape: (5, 4, 3)
 	Core is a
 		tensor of shape (2, 2, 1) with order F
 		data[:, :, 0] =
 		[[ 1.27097122e+00 -2.09211686e-05]
 		 [-3.74534404e-05  3.86751630e-01]]
 	U[0] = 
 		[[ 3.03244294e-04  9.82053530e-01]
 		 [ 4.42778953e-02  1.49740989e-04]
 		 [ 6.01599455e-01  6.18051935e-03]
 		 [ 7.97569723e-01 -5.06514409e-03]
 		 [ 9.11526161e-05  1.88432980e-01]]
 	U[1] = 
 		[[ 5.74845811e-05  6.08196916e-01]
 		 [ 3.89880068e-01 -6.10988633e-03]
 		 [ 1.26736143e-03  7.93761336e-01]
 		 [ 9.20864769e-01  1.45643367e-03]]
 	U[2] = 
 		[[0.3786518 ]
 		 [0.13177771]
 		 [0.91610996]],
 [None,
  array([[0.20887676, 0.16130952],
         [0.65310833, 0.2532916 ],
         [0.46631077, 0.24442559],
         [0.15896958, 0.11037514]]),
  array([[0.65632959],
         [0.13818295],
         [0.19658236]])],
 {'params': (0.0001, 1000, 1, array([0, 1, 2])),
  'iters': 6,
  'normresidual': np.float64(1.3886352427479627),
  'fit': np.float64(0.27742375057545454)})

Use a different ordering of the dimensions

T = ttb.tucker_als(X, 2, dimorder=[2, 1, 0])
T
Tucker Alternating Least-Squares:

 Iter 0: fit = 3.024840e-01 fitdelta = 3.0e-01
 Iter 1: fit = 3.647849e-01 fitdelta = 6.2e-02
 Iter 2: fit = 3.679934e-01 fitdelta = 3.2e-03
 Iter 3: fit = 3.699743e-01 fitdelta = 2.0e-03
 Iter 4: fit = 3.713304e-01 fitdelta = 1.4e-03
 Iter 5: fit = 3.722231e-01 fitdelta = 8.9e-04
 Iter 6: fit = 3.727917e-01 fitdelta = 5.7e-04
 Iter 7: fit = 3.731461e-01 fitdelta = 3.5e-04
 Iter 8: fit = 3.733639e-01 fitdelta = 2.2e-04
 Iter 9: fit = 3.734966e-01 fitdelta = 1.3e-04
 Iter 10: fit = 3.735769e-01 fitdelta = 8.0e-05
(Tensor of shape: (5, 4, 3)
 	Core is a
 		tensor of shape (2, 2, 2) with order F
 		data[:, :, 0] =
 		[[ 1.27270552e+00 -1.98145584e-03]
 		 [ 1.79435530e-04  2.80015614e-01]]
 		data[:, :, 1] =
 		[[-0.15918278  0.00117904]
 		 [ 0.00329255  0.72142572]]
 	U[0] = 
 		[[ 5.47518964e-04  9.98369702e-01]
 		 [ 3.62028131e-02 -6.01571747e-06]
 		 [ 5.43516192e-01  1.91824360e-03]
 		 [ 8.38617434e-01 -1.89973928e-03]
 		 [ 7.27790758e-05  5.70144694e-02]]
 	U[1] = 
 		[[-6.97808323e-05  6.22841697e-07]
 		 [ 4.16133442e-01 -1.93714805e-03]
 		 [ 2.62134849e-03  9.99996131e-01]
 		 [ 9.09299776e-01 -1.99629018e-03]]
 	U[2] = 
 		[[ 0.35642397  0.9320929 ]
 		 [ 0.11587932  0.02443826]
 		 [ 0.92711053 -0.36139396]],
 [array([[0.60484552, 0.73926358],
         [0.03918779, 0.28280696],
         [0.12019656, 0.2961402 ],
         [0.11872772, 0.31798318],
         [0.41426299, 0.0641475 ]]),
  array([[0.36872517, 0.82099323],
         [0.09710128, 0.83794491],
         [0.09609841, 0.97645947],
         [0.4686512 , 0.97676109]]),
  None],
 {'params': (0.0001, 1000, 1, array([2, 1, 0])),
  'iters': 10,
  'normresidual': np.float64(1.2038496593897914),
  'fit': np.float64(0.37357691568341256)})

Use the "nvecs" initialization method

This initialization is more expensive but generally works very well.

T = ttb.tucker_als(X, 2, dimorder=[0, 1, 2], init="nvecs")
T
 Computing 2 leading e-vector for factor 1.

 Computing 2 leading e-vector for factor 2.


Tucker Alternating Least-Squares:

 Iter 0: fit = 3.431977e-01 fitdelta = 3.4e-01
 Iter 1: fit = 3.452953e-01 fitdelta = 2.1e-03
 Iter 2: fit = 3.454463e-01 fitdelta = 1.5e-04
 Iter 3: fit = 3.454585e-01 fitdelta = 1.2e-05
/home/docs/checkouts/readthedocs.org/user_builds/pyttb/envs/392/lib/python3.9/site-packages/pyttb/tensor.py:2024: ComplexWarning: Casting complex values to real discards the imaginary part
  self.data[actualIdx] = value
(Tensor of shape: (5, 4, 3)
 	Core is a
 		tensor of shape (2, 2, 2) with order F
 		data[:, :, 0] =
 		[[ 1.28134569 -0.00423492]
 		 [-0.01678659  0.10347465]]
 		data[:, :, 1] =
 		[[-0.05410898  0.03494158]
 		 [ 0.01336928  0.67364048]]
 	U[0] = 
 		[[-2.25372685e-04 -2.31002840e-03]
 		 [ 3.60504385e-02 -8.44144689e-03]
 		 [ 5.42954195e-01  4.24222481e-02]
 		 [ 8.38872743e-01 -4.36495907e-02]
 		 [ 1.39132432e-02  9.98107445e-01]]
 	U[1] = 
 		[[-3.81204622e-05  2.66340731e-06]
 		 [ 4.16865412e-01  4.75019433e-02]
 		 [ 6.11161335e-03  9.98464506e-01]
 		 [ 9.08947675e-01 -2.84990509e-02]]
 	U[2] = 
 		[[ 0.23592519 -0.03977318]
 		 [ 0.15389222  0.98808283]
 		 [ 0.95950846 -0.14869568]],
 [None,
  array([[ 4.38558455e-16+0.j, -5.96188401e-16+0.j],
         [ 3.85357475e-01+0.j,  1.35152399e-02-0.j],
         [ 3.98308926e-03+0.j,  9.99859058e-01-0.j],
         [ 9.22758772e-01+0.j, -9.96005336e-03+0.j]]),
  array([[-0.        , -0.        ],
         [-0.38920902,  0.92114947],
         [ 0.92114947,  0.38920902]])],
 {'params': (0.0001, 1000, 1, array([0, 1, 2])),
  'iters': 3,
  'normresidual': np.float64(1.2578871401165317),
  'fit': np.float64(0.34545851644517134)})

Specify the initial guess manually

U0 = [np.random.rand(5, 2), np.random.rand(4, 2), np.random.rand(3, 2)]
T = ttb.tucker_als(X, 2, dimorder=[0, 1, 2], init=U0)
T
Tucker Alternating Least-Squares:

 Iter 0: fit = 2.537626e-01 fitdelta = 2.5e-01
 Iter 1: fit = 3.610897e-01 fitdelta = 1.1e-01
 Iter 2: fit = 3.663016e-01 fitdelta = 5.2e-03
 Iter 3: fit = 3.689843e-01 fitdelta = 2.7e-03
 Iter 4: fit = 3.707288e-01 fitdelta = 1.7e-03
 Iter 5: fit = 3.718567e-01 fitdelta = 1.1e-03
 Iter 6: fit = 3.725713e-01 fitdelta = 7.1e-04
 Iter 7: fit = 3.730148e-01 fitdelta = 4.4e-04
 Iter 8: fit = 3.732861e-01 fitdelta = 2.7e-04
 Iter 9: fit = 3.734505e-01 fitdelta = 1.6e-04
 Iter 10: fit = 3.735496e-01 fitdelta = 9.9e-05
(Tensor of shape: (5, 4, 3)
 	Core is a
 		tensor of shape (2, 2, 2) with order F
 		data[:, :, 0] =
 		[[1.27358153e+00 5.78083962e-04]
 		 [9.26654182e-04 2.66531368e-01]]
 		data[:, :, 1] =
 		[[-0.15202347 -0.00191261]
 		 [-0.00177027  0.72643245]]
 	U[0] = 
 		[[-8.37927959e-04  9.97421314e-01]
 		 [ 3.62038232e-02  2.62791661e-04]
 		 [ 5.43519078e-01 -2.66961465e-03]
 		 [ 8.38615271e-01  2.72772405e-03]
 		 [ 1.43322311e-04 -7.16664969e-02]]
 	U[1] = 
 		[[ 6.72753212e-05  9.52629611e-04]
 		 [ 4.16135369e-01  1.39137514e-03]
 		 [ 1.55068156e-04  9.99998252e-01]
 		 [ 9.09302659e-01 -8.07357563e-04]]
 	U[2] = 
 		[[ 0.35119109  0.93419866]
 		 [ 0.10031332 -0.10417526]
 		 [ 0.93091464 -0.3412043 ]],
 [array([[0.69247212, 0.56660145],
         [0.26538949, 0.52324805],
         [0.09394051, 0.5759465 ],
         [0.9292962 , 0.31856895],
         [0.66741038, 0.13179786]]),
  array([[0.7163272 , 0.28940609],
         [0.18319136, 0.58651293],
         [0.02010755, 0.82894003],
         [0.00469548, 0.67781654]]),
  array([[0.27000797, 0.73519402],
         [0.96218855, 0.24875314],
         [0.57615733, 0.59204193]])],
 {'params': (0.0001, 1000, 1, array([0, 1, 2])),
  'iters': 10,
  'normresidual': np.float64(1.2039021505574306),
  'fit': np.float64(0.37354960190807895)})