-
Notifications
You must be signed in to change notification settings - Fork 0
/
TensorDecomp.py
281 lines (222 loc) · 10.4 KB
/
TensorDecomp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
from math import inf
import numpy as np
import timeit
from scipy.sparse.linalg import svds
from scipy.linalg import norm
from tensorly.decomposition import parafac
from tensorly.decomposition import tucker
from tensorly.decomposition import non_negative_parafac
from tensorly.decomposition import matrix_product_state
from scipy.linalg import clarkson_woodruff_transform
from sklearn.decomposition import NMF
from scipy.sparse import bsr_matrix
decomp_list = ['svd', 'parafac', 'tucker', 'matrix_product_state', 'NMF', 'clarkson_woodruff_transform', 'non_negative_parafac']
def blk_diag(data):
"""Build a block diagonal sparse matrix from a 3d numpy array
:param data: input array in R^{ngp,d1,d2}
:type data: array
:returns: sparse matrix in R^{ngp*d1, ngp*d2}
Notes
----
for the intended usage here, this function is faster than block_diag 'from scipy.sparse import block_diag'
See Also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.bsr_matrix.html
"""
indptr = np.array(range(0, data.shape[0] + 1))
indices = np.array(range(data.shape[0]))
return bsr_matrix((data, indices, indptr))
class TensorDecomp():
"""
A class to represent a tensor object with decomposition, reconstruction, error methods.
Attributes
----------
tensor : numpy.ndarray
The tensor that is given to the class.
memSize : int
The size of the tensor in the memory before decomposition.
decMemSize : int
The size of the tensor in the memory after decomposition.
decomp_time : float
The time elapsed to decompose the tensor.
decomp_type : str
The __name__ of the provided func argument.
memSaving : float
The relative change of memory requirement of the tensor after decomposition.
Methods
-------
decompose(func, *args, **kwargs):
Decomposes the given tensor with the 'func' decomposition and computes the size in memory after decomposition.
reconstruct(self):
Reconstructs the recons tensor.
error(func, x, y):
Calculates the error between x and y with the given 'func' error handle.
"""
def __init__(self, tensor):
"""Creates the TensorDecomp object with given numpy.ndarray object.
Args:
tensor (numpy.ndarray): The tensor to be decomposed.
"""
self.tensor = tensor
self.decMemSize = 0
self.memSize = tensor.nbytes
def decompose(self, func, *args, **kwargs):
"""
Decomposes the tensor with the func argument decomposition type.
Assigns the objects after decomposition to self.recons.
Computes the decomposition time and assigns to self.decomp_time.
Assigns the func argument as the decomposition type to self.decomp_type.
Args:
----------
func : the decomposition type. e.g.: parafac, NMF etc.
*args : passed arguments.
**kwargs: passed keyword arguments. e.g.: rank, sketch_size etc.
Returns
-------
None
"""
n = 50
if func.__name__ not in decomp_list:
print(f'Error! Given decomposition --> {func.__name__}')
return
elif func.__name__ == 'svd':
self.decomposed = func(self.tensor)
self.decomp_time = timeit.timeit( lambda: func(self.tensor, **kwargs), number = n)/n
self.decomp_type = func.__name__
elif func.__name__ == 'NMF':
try:
self.nmf_obj = NMF(**kwargs)
self.decomposed = []
self.decomposed.append(self.nmf_obj.fit_transform(self.tensor) )
self.decomposed.append(self.nmf_obj.components_)
self.decomp_time = timeit.timeit(lambda: func(self.tensor, **kwargs), number = n)/n
self.decomp_type = func.__name__
except:
raise
elif args:
self.decomposed = func(self.tensor, args[0])
self.decomp_type = func.__name__
# self.decomp_time = timeit.timeit(stmt = 'func(self.tensor, args[0])', setup='from __main__ import func, tensor, args', number = n)/n
else:
self.decomposed = func(self.tensor, **kwargs)
self.decomp_type = func.__name__
# self.decomp_time = timeit.timeit(stmt = 'func(self.tensor, args[0])', globals=globals(), number = n)/n
for array in self.decomposed:
if isinstance(array,(np.ndarray)):
self.decMemSize += array.nbytes
for array in self.decomposed[1]:
if isinstance(array,(np.ndarray)):
self.decMemSize += array.nbytes
# the tensor size change in memory
self.memSaving = (self.memSize - self.decMemSize) / self.memSize
def reconstruct(self):
"""
Reconstructs the recons TensorDecomp object.
Assigns the reconstructed tensor to self.recons attribute.
Args:
----------
None
Returns
-------
None
"""
if self.decomp_type == 'svd':
if self.tensor.ndim < 3:
self.recons = self.decomposed[0][...,:self.tensor.shape[-1]]*self.decomposed[1]@self.decomposed[2]
else:
self.recons = np.matmul(self.decomposed[0][...,:self.tensor.shape[-1]], self.decomposed[1][..., None] * self.decomposed[2])
elif self.decomp_type == 'NMF':
self.recons = self.nmf_obj.inverse_transform(self.decomposed[0])
elif self.decomp_type == 'tucker' :
from tensorly import tucker_tensor as tt
self.recons = tt.tucker_to_tensor(self.decomposed)
elif self.decomp_type == 'parafac' or self.decomp_type == 'non_negative_parafac':
from tensorly import cp_tensor as ct
self.recons = ct.cp_to_tensor(self.decomposed)
elif self.decomp_type == 'matrix_product_state':
from tensorly import tt_tensor as tt
self.recons = tt.tt_to_tensor(self.decomposed)
elif self.decomp_type == 'clarkson_woodruff_transform':
self.recons = self.decomposed
self.decError = (norm(self.tensor-self.recons)) / norm(self.tensor)
def errList(tensor, decompMet, vectorL, MatrixR, MatrixL, normL, rank = None, **kwargs):
"""A function to calculate the norm of the following:
tensor decomposition,
Tensor @ Vector,
Matrix @ Tensor,
Matrix.T @ Tensor @ Matrix operations error,
in this order.
Args:
----------
tensor (TensorDecomp) : TensorDecomp object.
decompMet (Function) : The decomposition method to decompose the TensorDecomp object.
vectorL (numpy.ndarray) : A vector of size tensor.tensor.shape([1])
vectorR (numpy.ndarray) : A vector of size tensor.tensor.shape([-1])
MatrixL (numpy.ndarray) : A matrix of size tensor.tensor.shape([:-1])
MatrixR (numpy.ndarray) : A matrix of size tensor.tensor.shape([-1:-3:-1])
normL (list) : A list of norm functions to calculate the norm of the errors.
rank (int or list) : To decompose with the relatead rank. (optional)
Returns
-------
A list of 7 lists each contains the norm of the errors of the above tensor operations.
"""
if tensor.tensor.ndim != 2 and decompMet in [NMF, clarkson_woodruff_transform] :
return (f"It is not possible to decompose {tensor.tensor.ndim} dimension with the {decompMet.__name__} method!")
if decompMet in [NMF,clarkson_woodruff_transform]:
tensor.decompose(decompMet, **kwargs)
tensor.reconstruct()
else:
tensor.decompose(decompMet, rank)
tensor.reconstruct()
decErr = None
tensVec = [norm(tensor.tensor@vectorR, tensor.recons@vectorR) for norm in normL]
vecTens = [norm([email protected], [email protected]) for norm in normL]
matLTens = [norm([email protected], [email protected]) for norm in normL]
tensMatR = [norm(tensor.tensor@MatrixR, tensor.recons@MatrixR) for norm in normL]
vectTensvec = [norm([email protected]@vectorR, [email protected]@vectorR) for norm in normL]
matLTensMatR = [norm([email protected]@MatrixR, [email protected]@MatrixR) for norm in normL]
if decompMet == clarkson_woodruff_transform:
return [decErr, tensVec, vecTens, tensMatR, matLTens, vectTensvec, matLTensMatR]
else:
decErr = [norm(tensor.tensor, tensor.recons) for norm in normL]
return [decErr, tensVec, vecTens, tensMatR, matLTens, vectTensvec, matLTensMatR]
def tensOpTim(operList):
"""Performs tensor@vectorR, vectorL@tensor, tensor@MatrixR, MatrixL@tensor, vecL@tensor@vecR, matL@tensor@matR
operations and times the each operation returns them in a list in the explained order
Args:
operList : List of tensor operations in lambda form
Returns:
list : List of tensor operation times
"""
timing = []
n = 50
for operation in operList:
opTime = timeit.timeit(operation, number=n)/n
timing.append(opTime)
# opTime = timeit.timeit(lambda:tensor.tensor@vecR, number = n)/n
# timing.append(opTime)
# opTime = timeit.timeit(lambda:[email protected], number = n)/n
# timing.append(opTime)
# opTime = timeit.timeit(lambda:tensor.tensor@matR, number = n)/n
# timing.append(opTime)
# opTime = timeit.timeit(lambda:[email protected], number = n)/n
# timing.append(opTime)
# opTime = timeit.timeit(lambda:[email protected]@vecR, number = n)/n
# timing.append(opTime)
# opTime = timeit.timeit(lambda:[email protected]@matR, number = n)/n
# timing.append(opTime)
return timing
### NOT Implemented ###
# def decTensOpTim(tensor, decompMet, vectorR, vectorL, MatrixR, MatrixL, rank = None, **kwargs):
# timing = []
# if tensor.tensor.ndim != 2 and decompMet in [NMF, clarkson_woodruff_transform] :
# return (f"It is not possible to decompose {tensor.tensor.ndim = } with the {decompMet.__name__} method!")
# if decompMet in [NMF,clarkson_woodruff_transform]:
# tensor.decompose(decompMet, **kwargs)
# else:
# tensor.decompose(decompMet, rank)
# operList = []
# for operation in operList:
# t1 = timer()
# eval(operation)
# t2 = timer()
# timing.append((t2-t1))
# return timing