Kruskal Tensors
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.
Kruskal format is a decomposition of a tensor \(\mathcal{X}\) as the sum of the outer products as the columns of matrices. For example, we might write:
\( \mathcal{X} = \sum_{r} a_r \circ b_r \circ c_r \)
where a subscript denotes column index and a circle denotes outer product. In other words, the tensor \(\mathcal{X}\) is built from the columns of the matrices \(A\), \(B\), and \(C\). It’s often helpful to explicitly specify a weight for each outer product, which we do here:
\(
\mathcal{X} = \sum_{r} \lambda_r \, a_r \circ b_r \circ c_r
\)
The ktensor
class stores the components of the tensor \(\mathcal{X}\) and can perform many operations, e.g., ttm
, without explicitly forming the tensor \(\mathcal{X}\).
Kruskal tensor format via ktensor
Kruskal format stores a tensor as a sum of rank-1 outer products. For example, consider a tensor of the following form:
\( \mathcal{X} = a_1 \circ b_1 \circ c_1 + a_2 \circ b_2 \circ c_2 \)
This can be stored in Kruskal form as follows.
import pyttb as ttb
import numpy as np
np.random.seed(0)
A = np.random.rand(4, 2) # First column is a_1, second is a_2.
B = np.random.rand(3, 2) # Likewise for B.
C = np.random.rand(2, 2) # Likewise for C.
X = ttb.ktensor([A, B, C]) # Create the ktensor.
X
ktensor of shape (4, 3, 2) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
Y = ttb.ktensor(
[np.random.rand(4, 1), np.random.rand(2, 1), np.random.rand(3, 1)]
) # Another ktensor.
Y
ktensor of shape (4, 2, 3) with order F
weights=[1.]
factor_matrices[0] =
[[0.77815675]
[0.87001215]
[0.97861834]
[0.79915856]]
factor_matrices[1] =
[[0.46147936]
[0.78052918]]
factor_matrices[2] =
[[0.11827443]
[0.63992102]
[0.14335329]]
For Kruskal format, there can be any number of matrices, but every matrix must have the same number of columns. The number of rows can vary.
Specifying weights in a ktensor
Weights for each rank-1 tensor can be specified by passing in a column vector. For example:
\( \mathcal{X} = \lambda_1 \, a_1 \circ b_1 \circ c_1 + \lambda_2 \, a_2 \circ b_2 \circ c_2 \)
# Upcoming ktensors will be generated with this same initialization.
def generate_sample_ktensor() -> ttb.ktensor:
np.random.seed(0)
A = np.random.rand(4, 2) # Create some data.
B = np.random.rand(3, 2)
C = np.random.rand(2, 2)
weights = np.array([5.0, 0.25])
return ttb.ktensor([A, B, C], weights) # Create the ktensor.
X = generate_sample_ktensor()
X
ktensor of shape (4, 3, 2) with order F
weights=[5. 0.25]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
Creating a one-dimensional ktensor
np.random.seed(0)
Y = ttb.ktensor([np.random.rand(4, 5)]) # A one-dimensional ktensor.
Y
ktensor of shape (4,) with order F
weights=[1. 1. 1. 1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937 0.60276338 0.54488318 0.4236548 ]
[0.64589411 0.43758721 0.891773 0.96366276 0.38344152]
[0.79172504 0.52889492 0.56804456 0.92559664 0.07103606]
[0.0871293 0.0202184 0.83261985 0.77815675 0.87001215]]
Constituent parts of a ktensor
X = generate_sample_ktensor()
X.weights # Weights or multipliers.
array([5. , 0.25])
X.factor_matrices # Cell array of matrices.
[array([[0.5488135 , 0.71518937],
[0.60276338, 0.54488318],
[0.4236548 , 0.64589411],
[0.43758721, 0.891773 ]]),
array([[0.96366276, 0.38344152],
[0.79172504, 0.52889492],
[0.56804456, 0.92559664]]),
array([[0.07103606, 0.0871293 ],
[0.0202184 , 0.83261985]])]
Creating a ktensor
from its constituent parts
X = generate_sample_ktensor()
Y = ttb.ktensor(X.factor_matrices, X.weights) # Recreate X.
Y
ktensor of shape (4, 3, 2) with order F
weights=[5. 0.25]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
Creating an empty ktensor
Z = ttb.ktensor() # Empty ktensor.
Z
ktensor of shape () with order F
weights=[]
factor_matrices=[]
Use full
or to_tensor
to convert a ktensor
to a tensor
X = generate_sample_ktensor()
X.full() # Converts to a tensor.
tensor of shape (4, 3, 2) with order F
data[:, :, 0] =
[[0.19381804 0.16256856 0.12514704]
[0.21086124 0.17577751 0.13259822]
[0.1504007 0.12657497 0.09849813]
[0.15722304 0.13332549 0.10626643]]
data[:, :, 1] =
[[0.11054766 0.12266212 0.16930925]
[0.10221034 0.10823074 0.13959484]
[0.09282405 0.10501592 0.1487711 ]
[0.11380622 0.13320036 0.19694404]]
X.to_tensor() # Same as above.
tensor of shape (4, 3, 2) with order F
data[:, :, 0] =
[[0.19381804 0.16256856 0.12514704]
[0.21086124 0.17577751 0.13259822]
[0.1504007 0.12657497 0.09849813]
[0.15722304 0.13332549 0.10626643]]
data[:, :, 1] =
[[0.11054766 0.12266212 0.16930925]
[0.10221034 0.10823074 0.13959484]
[0.09282405 0.10501592 0.1487711 ]
[0.11380622 0.13320036 0.19694404]]
Use double
to convert a ktensor
to a multidimensional array
X = generate_sample_ktensor()
X.double() # Converts to an array.
array([[[0.19381804, 0.11054766],
[0.16256856, 0.12266212],
[0.12514704, 0.16930925]],
[[0.21086124, 0.10221034],
[0.17577751, 0.10823074],
[0.13259822, 0.13959484]],
[[0.1504007 , 0.09282405],
[0.12657497, 0.10501592],
[0.09849813, 0.1487711 ]],
[[0.15722304, 0.11380622],
[0.13332549, 0.13320036],
[0.10626643, 0.19694404]]])
Use tendiag
or sptendiag
to convert a ktensor
to a ttensor
A ktensor
can be regarded as a ttensor
with a diagonal core.
X = generate_sample_ktensor()
R = len(X.weights) # Number of factors in X.
core = ttb.tendiag(X.weights, ((R,) * X.ndims)) # Create a diagonal core.
Y = ttb.ttensor(core, X.factor_matrices) # Assemble the ttensor
Y
Tensor of shape: (4, 3, 2)
Core is a
tensor of shape (2, 2, 2) with order F
data[:, :, 0] =
[[5. 0.]
[0. 0.]]
data[:, :, 1] =
[[0. 0. ]
[0. 0.25]]
U[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
U[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
U[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
(X.full() - Y.full()).norm() # They are the same.
7.601177430610147e-17
core = ttb.sptendiag(X.weights, ((R,) * X.ndims)) # Sparse diagonal core.
Y = ttb.ttensor(core, X.factor_matrices) # Assemble the ttensor
Y
Tensor of shape: (4, 3, 2)
Core is a
sparse tensor of shape (2, 2, 2) with 2 nonzeros and order F
[0, 0, 0] = 5.0
[1, 1, 1] = 0.25
U[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
U[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
U[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
(X.full() - Y.full()).norm() # They are the same.
7.601177430610147e-17
Use ndims
and shape
for the dimensions of a ktensor
X = generate_sample_ktensor()
X.ndims # Number of dimensions.
3
X.shape # Tuple of the shapes.
(4, 3, 2)
X.shape[1] # Shape of the 2nd mode.
3
Subscripted reference for a ktensor
X = generate_sample_ktensor()
X.weights[1] # Weight of the 2nd factor.
np.float64(0.25)
X.factor_matrices[1] # Extract a matrix.
array([[0.96366276, 0.38344152],
[0.79172504, 0.52889492],
[0.56804456, 0.92559664]])
Subscripted assignment for a ktensor
X = generate_sample_ktensor()
X.weights = np.ones(X.weights.shape) # Insert new multipliers.
X
ktensor of shape (4, 3, 2) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
X.weights[0] = 7 # Change a single element of weights.
X.factor_matrices[2][:, [0]] = np.ones((2, 1)) # Change the matrix for mode 3.
X
ktensor of shape (4, 3, 2) with order F
weights=[7. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[1. 0.0871293 ]
[1. 0.83261985]]
Using negative indexing for the last array index
X = generate_sample_ktensor()
X.factor_matrices[0][-1:, :]
X.shape
(4, 3, 2)
X = generate_sample_ktensor()
X.factor_matrices[0][0][
1 : (np.prod(X.shape) - 1)
].item() # Calculates factor_matrix[0][1]
0.7151893663724195
Adding and subtracting ktensor
s
Adding two ktensors is the same as concatenating the matrices.
np.random.seed(0)
X = ttb.ktensor(
[np.random.rand(4, 2), np.random.rand(2, 2), np.random.rand(3, 2)]
) # Data.
Y = ttb.ktensor(
[np.random.rand(4, 2), np.random.rand(2, 2), np.random.rand(3, 2)]
) # More data.
X, Y
(ktensor of shape (4, 2, 3) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]]
factor_matrices[2] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]],
ktensor of shape (4, 2, 3) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.77815675 0.87001215]
[0.97861834 0.79915856]
[0.46147936 0.78052918]
[0.11827443 0.63992102]]
factor_matrices[1] =
[[0.14335329 0.94466892]
[0.52184832 0.41466194]]
factor_matrices[2] =
[[0.26455561 0.77423369]
[0.45615033 0.56843395]
[0.0187898 0.6176355 ]])
Z = X + Y # Concatenates the factor matrices.
Z
ktensor of shape (4, 2, 3) with order F
weights=[1. 1. 1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937 0.77815675 0.87001215]
[0.60276338 0.54488318 0.97861834 0.79915856]
[0.4236548 0.64589411 0.46147936 0.78052918]
[0.43758721 0.891773 0.11827443 0.63992102]]
factor_matrices[1] =
[[0.96366276 0.38344152 0.14335329 0.94466892]
[0.79172504 0.52889492 0.52184832 0.41466194]]
factor_matrices[2] =
[[0.56804456 0.92559664 0.26455561 0.77423369]
[0.07103606 0.0871293 0.45615033 0.56843395]
[0.0202184 0.83261985 0.0187898 0.6176355 ]]
Z = X - Y # Concatenates as with plus, but changes the weights.
Z
ktensor of shape (4, 2, 3) with order F
weights=[ 1. 1. -1. -1.]
factor_matrices[0] =
[[0.5488135 0.71518937 0.77815675 0.87001215]
[0.60276338 0.54488318 0.97861834 0.79915856]
[0.4236548 0.64589411 0.46147936 0.78052918]
[0.43758721 0.891773 0.11827443 0.63992102]]
factor_matrices[1] =
[[0.96366276 0.38344152 0.14335329 0.94466892]
[0.79172504 0.52889492 0.52184832 0.41466194]]
factor_matrices[2] =
[[0.56804456 0.92559664 0.26455561 0.77423369]
[0.07103606 0.0871293 0.45615033 0.56843395]
[0.0202184 0.83261985 0.0187898 0.6176355 ]]
(Z.full() - (X.full() - Y.full())).norm() # Should be zero.
1.138063217752167e-16
Basic operations with a ktensor
+X # Calls uplus.
ktensor of shape (4, 2, 3) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]]
factor_matrices[2] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
-X # Calls uminus.
ktensor of shape (4, 2, 3) with order F
weights=[-1. -1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]]
factor_matrices[2] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
5 * X # Calls mtimes.
ktensor of shape (4, 2, 3) with order F
weights=[5. 5.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]]
factor_matrices[2] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
Use permute
to reorder the modes of a ktensor
np.random.seed(0)
X = ttb.ktensor(
[np.random.rand(4, 2), np.random.rand(2, 2), np.random.rand(3, 2)]
) # Data.
X.permute(np.array((1, 2, 0))) # Reorders modes of X.
ktensor of shape (2, 3, 4) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]]
factor_matrices[1] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
factor_matrices[2] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
Use arrange
to normalize the factors of a ktensor
The function arrange
normalizes the columns of the factors and then arranges the rank-one pieces in decreasing order of shape.
np.random.seed(0)
X = ttb.ktensor(
[np.random.rand(3, 2), np.random.rand(4, 2), np.random.rand(2, 2)]
) # Unit weights.
X
ktensor of shape (3, 4, 2) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]]
factor_matrices[1] =
[[0.43758721 0.891773 ]
[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
X.arrange() # Normalized and rearranged.
X
ktensor of shape (3, 4, 2) with order F
weights=[1.33623212 0.09761402]
factor_matrices[0] =
[[0.64602825 0.59738279]
[0.49219122 0.65610716]
[0.58343407 0.4611477 ]]
factor_matrices[1] =
[[0.61851988 0.3041712 ]
[0.26594907 0.66985152]
[0.36683329 0.55033591]
[0.64197943 0.3948534 ]]
factor_matrices[2] =
[[0.10407646 0.96180106]
[0.9945693 0.27374937]]
Use fixsigns
for sign indeterminacies in a ktensor
The largest magnitude entry for each factor is changed to be positive provided that we can flip the signs of pairs of vectors in that rank-1 component.
np.random.seed(0)
X = ttb.ktensor(
[np.random.rand(4, 2), np.random.rand(2, 2), np.random.rand(3, 2)]
) # Data.
Y = X
Y.factor_matrices[0][:, 0] = -Y.factor_matrices[0][
:, 0
] # switch the sign on a pair of columns
Y.factor_matrices[1][:, 0] = -Y.factor_matrices[1][:, 0]
Y
ktensor of shape (4, 2, 3) with order F
weights=[1. 1.]
factor_matrices[0] =
[[-0.5488135 0.71518937]
[-0.60276338 0.54488318]
[-0.4236548 0.64589411]
[-0.43758721 0.891773 ]]
factor_matrices[1] =
[[-0.96366276 0.38344152]
[-0.79172504 0.52889492]]
factor_matrices[2] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
Y.fixsigns()
ktensor of shape (4, 2, 3) with order F
weights=[1. 1.]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]]
factor_matrices[2] =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
Use ktensor
to store the ‘skinny’ SVD of a matrix
np.random.seed(0)
A = np.random.rand(4, 3)
A
array([[0.5488135 , 0.71518937, 0.60276338],
[0.54488318, 0.4236548 , 0.64589411],
[0.43758721, 0.891773 , 0.96366276],
[0.38344152, 0.79172504, 0.52889492]])
[U, S, Vh] = np.linalg.svd(A, full_matrices=False) # Compute the SVD.
# Numpy Expects U*S*Vh where pyttb expects U*S*V'
X = ttb.ktensor([U, Vh.transpose()], S) # Store the SVD as a ktensor.
X
ktensor of shape (4, 3) with order F
weights=[2.21425506 0.29626672 0.21874633]
factor_matrices[0] =
[[-0.48665747 -0.08953811 0.54332309]
[-0.41275168 -0.81483266 0.00407301]
[-0.62010377 0.2250203 -0.73183668]
[-0.45636813 0.52668447 0.41133746]]
factor_matrices[1] =
[[-0.42376582 -0.65045995 0.63033672]
[-0.64907895 0.70346017 0.28955189]
[-0.63175869 -0.2864361 -0.72030224]]
print(f"U*S*Vh:\n{U@np.diag(S)@Vh}")
print(
f"\nX.factor_matrices[0]@np.diag(X.weights)@(X.factor_matrices[1].transpose()):\n\
{X.factor_matrices[0]@np.diag(X.weights)@(X.factor_matrices[1].transpose())}"
)
print(f"\nX.full():\n{X.full()}") # Reassemble the original matrix.
U*S*Vh:
[[0.5488135 0.71518937 0.60276338]
[0.54488318 0.4236548 0.64589411]
[0.43758721 0.891773 0.96366276]
[0.38344152 0.79172504 0.52889492]]
X.factor_matrices[0]@np.diag(X.weights)@(X.factor_matrices[1].transpose()):
[[0.5488135 0.71518937 0.60276338]
[0.54488318 0.4236548 0.64589411]
[0.43758721 0.891773 0.96366276]
[0.38344152 0.79172504 0.52889492]]
X.full():
tensor of shape (4, 3) with order F
data[:, :] =
[[0.5488135 0.71518937 0.60276338]
[0.54488318 0.4236548 0.64589411]
[0.43758721 0.891773 0.96366276]
[0.38344152 0.79172504 0.52889492]]
Displaying a ktensor
X = generate_sample_ktensor()
X
ktensor of shape (4, 3, 2) with order F
weights=[5. 0.25]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]
print(X)
ktensor of shape (4, 3, 2) with order F
weights=[5. 0.25]
factor_matrices[0] =
[[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]
[0.43758721 0.891773 ]]
factor_matrices[1] =
[[0.96366276 0.38344152]
[0.79172504 0.52889492]
[0.56804456 0.92559664]]
factor_matrices[2] =
[[0.07103606 0.0871293 ]
[0.0202184 0.83261985]]