DaMAT.ttObject
This is a python class for tensors in TT-format. Through this object you can compute TT-decomposition of multidimensional arrays in one shot using TTSVD algorithm or incrementally using TT-ICE algorithm.
Furthermore, this object allows exporting and importing TT-cores using native
format (.ttc
) and intermediary formats (.txt
).
1# -*- coding: utf-8 -*- 2""" 3This is a python class for tensors in TT-format. 4Through this object you can compute TT-decomposition of multidimensional arrays in 5one shot using [TTSVD algorithm](https://epubs.siam.org/doi/epdf/10.1137/090752286) 6or incrementally using [TT-ICE algorithm](https://arxiv.org/abs/2211.12487). 7 8Furthermore, this object allows exporting and importing TT-cores using native 9format (`.ttc`) and intermediary formats (`.txt`). 10""" 11 12import warnings 13import time 14 15import numpy as np 16 17from logging import warning 18from .utils import deltaSVD, ttsvd, coreContraction, mode_n_unfolding 19from pickle import dump, load 20 21 22class ttObject: 23 """ 24 Python object for tensors in Tensor-Train format. 25 26 This object computes the TT-decomposition of multidimensional arrays using `TTSVD`_, 27 `TT-ICE`_, and `TT-ICE*`_. 28 It furthermore contains an inferior method, ITTD, for benchmarking purposes. 29 30 This object handles various operations in TT-format such as dot product and norm. 31 Also provides supporting operations for uncompressed multidimensional arrays such as 32 reshaping and transpose. 33 Once the tensor train approximation for a multidimensional array is computed, you can 34 compute projection of appropriate arrays onto the TT-cores, reconstruct projected 35 or compressed tensors and compute projection/compression accuracy. 36 37 You can also save/load tensors as `.ttc` or `.txt` files. 38 39 Attributes 40 ---------- 41 inputType: type 42 Type of the input data. This determines how the object is initialized. 43 originalData: 44 Original multidimensional input array. Generally will not be stored 45 after computing 46 an initial set of TT-cores. 47 method: :obj:`str` 48 Method of computing the initial set of TT-cores. Currently only accepts 49 `'ttsvd'` as input 50 ttEpsilon: :obj:`float` 51 Desired relative error upper bound. 52 ttCores: :obj:`list` of :obj:`numpy.array` 53 Cores of the TT-decomposition. Stored as a list of numpy arrays. 54 ttRanks: :obj:`list` of :obj:`int` 55 TT-ranks of the decomposition. Stored as a list of integers. 56 nCores: :obj:`int` 57 Number of TT-cores in the decomposition. 58 nElements: :obj:`int` 59 Number of entries present in the current `ttObject`. 60 originalShape: :obj:`tuple` or :obj:`list` 61 Original shape of the multidimensional array. 62 reshapedShape: :obj:`tuple` or :obj:`list` 63 Shape of the multidimensional array after reshaping. Note that 64 this attribute is only meaningful before computing a TT-decomposition 65 indexOrder: :obj:`list` of :obj:`int` 66 Keeps track of original indices in case of transposing the input array. 67 .. _TTSVD: 68 https://epubs.siam.org/doi/epdf/10.1137/090752286 69 _TT-ICE: 70 https://arxiv.org/abs/2211.12487 71 _TT-ICE*: 72 https://arxiv.org/abs/2211.12487 73 74 """ 75 76 def __init__( 77 self, 78 data, 79 epsilon: float = None, 80 keepData: bool = False, 81 samplesAlongLastDimension: bool = True, 82 method: str = "ttsvd", 83 ) -> None: 84 """ 85 Initializes the ttObject. 86 87 Parameters 88 ---------- 89 data: :obj:`numpy.array` or :obj:`list` 90 Main input to the ttObject. It can either be a multidimensional `numpy array` 91 or `list of numpy arrays`. 92 If list of numpy arrays are presented as input, the object will interpret it 93 as the TT-cores of an existing decomposition. 94 epsilon: :obj:`float`, optional 95 The relative error upper bound desired for approximation. Optional for cases 96 when `data` has type `list`. 97 keepData: :obj:`bool`, optional 98 Optional boolean variable to determine if the original array will be kept 99 after compression. Set to `False` by default. 100 samplesAlongLastDimension: :obj:`bool`, optional 101 Boolean variable to ensure if the samples are stacked along the last 102 dimension. Assumed to be `True` since it is one of the assumptions in TT-ICE. 103 method: :obj:`str`, optional 104 Determines the computation method for tensor train decomposition of 105 the multidimensional array presented as `data`. Set to `'ttsvd'` by default. 106 107 Currently the package only has support for ttsvd, additional support such as 108 `ttcross` might be included in the future. 109 110 """ 111 # self.ttRanks=ranks 112 self.ttCores = None 113 self.nCores = None 114 self.nElements = None 115 self.inputType = type(data) 116 self.method = method 117 self.keepOriginal = keepData 118 self.originalData = data 119 self.samplesAlongLastDimension = samplesAlongLastDimension 120 if self.inputType is np.memmap: 121 self.inputType = np.ndarray 122 123 if self.inputType == np.ndarray: 124 self.ttEpsilon = epsilon 125 self.originalShape = list(data.shape) 126 self.reshapedShape = self.originalShape 127 self.indexOrder = [idx for idx in range(len(self.originalShape))] 128 elif self.inputType == list: 129 self.nCores = len(data) 130 self.ttCores = data 131 self.reshapedShape = [core.shape[1] for core in self.ttCores] 132 else: 133 raise TypeError("Unknown input type!") 134 135 @property 136 def coreOccupancy(self) -> None: 137 """ 138 :obj:`list` of :obj:`float`: A metric showing the *relative rank* of each 139 TT-core. This metric is used for a heuristic enhancement tool in `TT-ICE*` 140 algorithm 141 """ 142 try: 143 return [ 144 core.shape[-1] / np.prod(core.shape[:-1]) for core in self.ttCores[:-1] 145 ] 146 except ValueError: 147 warnings.warn( 148 "No TT cores exist, maybe forgot calling object.ttDecomp?", Warning 149 ) 150 return None 151 152 @property 153 def compressionRatio(self) -> float: 154 """ 155 :obj:`float`: A metric showing how much compression with respect to the 156 original multidimensional array is achieved. 157 """ 158 originalNumEl = 1 159 compressedNumEl = 0 160 for core in self.ttCores: 161 originalNumEl *= core.shape[1] 162 compressedNumEl += np.prod(core.shape) 163 return originalNumEl / compressedNumEl 164 165 def changeShape(self, newShape: tuple or list) -> None: 166 """ 167 Function to change shape of input tensors and keeping track of the reshaping. 168 Reshapes `originalData` and saves the final shape in `reshapedShape` 169 170 Note 171 ---- 172 A simple `numpy.reshape` would be sufficient for this purpose but in order to keep 173 track of the shape changes the `reshapedShape` attribute also needs to be updated 174 accordingly. 175 176 Parameters 177 ---------- 178 newShape:obj:`tuple` or `list` 179 New shape of the tensor 180 181 Raises 182 ------ 183 warning 184 If an attempt is done to modify the shape after computing a TT-decomposition. 185 This is important since it will cause incompatibilities in other functions 186 regarding the shape of the uncompressed tensor. 187 188 """ 189 if self.ttCores is not None: 190 warning( 191 "Warning! You are reshaping the original data after computing a\ 192 TT-decomposition! We will proceed without reshaping self.originalData!!" 193 ) 194 return None 195 self.reshapedShape = newShape 196 self.originalData = np.reshape(self.originalData, self.reshapedShape) 197 self.reshapedShape = list(self.originalData.shape) 198 # Changed reshapedShape to a list for the trick in ttICEstar 199 if self.samplesAlongLastDimension: 200 self.singleDataShape = self.reshapedShape[:-1] 201 # This line assumes we keep the last index as the samples index and don't 202 # interfere with its shape 203 204 def computeTranspose(self, newOrder: list) -> None: 205 """ 206 Transposes the axes of `originalData`. 207 208 Similar to `changeShape`, a simple 209 `numpy.transpose` would be sufficient for this purpose but in order to 210 keep track of the transposition order `indexOrder` attribute also needs 211 to be updated accordingly. 212 213 Parameters 214 ---------- 215 newOrder:obj:`list` 216 New order of the axes. 217 218 Raises 219 ------ 220 ValueError 221 When the number of transposition axes are not equal to the number of 222 dimensions of `originalData`. 223 """ 224 assert self.inputType == np.ndarray and self.ttCores is None 225 if len(newOrder) != len(self.indexOrder): 226 raise ValueError( 227 "size of newOrder and self.indexOrder does not match. \ 228 Maybe you forgot reshaping?" 229 ) 230 self.indexOrder = [self.indexOrder[idx] for idx in newOrder] 231 self.originalData = self.originalData.transpose(newOrder) 232 self.reshapedShape = list(self.originalData.shape) 233 234 def indexMap(self) -> None: 235 """ 236 Function to map the original indices to the reshaped indices. 237 Currently not implemented. 238 """ 239 self.A = 2 240 241 def primeReshaping(self) -> None: 242 """ 243 Function to reshape the first d dimensions to prime factors. 244 Currently not implemented 245 """ 246 self.A = 2 247 248 def saveData( 249 self, fileName: str, directory="./", justCores=True, outputType="ttc" 250 ) -> None: 251 """ 252 Writes the computed TT-cores to an external file. 253 254 Parameters 255 ---------- 256 fileName:obj:`str` 257 258 directory:obj:`str` 259 Location to save files with respect to the present working directory. 260 justCores:obj:`bool` 261 Boolean variable to determine if `originalData` will be discarded 262 or not while saving. 263 outputType:obj:`str` 264 Type of the output file. `ttc` for pickled `ttObject`, `txt` for 265 individual text files for each TT-core. 266 """ 267 if justCores: 268 if outputType == "ttc": 269 with open(directory + fileName + ".ttc", "wb") as saveFile: 270 temp = ttObject(self.ttCores) 271 for attribute in vars(self): 272 if attribute != "originalData": 273 setattr(temp, attribute, eval(f"self.{attribute}")) 274 dump(temp, saveFile) 275 elif outputType == "txt": 276 for coreIdx, core in enumerate(self.ttCores): 277 np.savetxt( 278 directory + f"{fileName}_{coreIdx}.txt", 279 core.reshape(-1, core.shape[-1]), 280 header=f"{core.shape[0]} {core.shape[1]} {core.shape[2]}", 281 delimiter=" ", 282 ) 283 elif outputType == "npy": 284 for coreIdx, core in enumerate(self.ttCores): 285 np.save( 286 directory + f"{fileName}_{coreIdx}.npy", 287 core, 288 ) 289 else: 290 raise ValueError(f"Output type {outputType} is not supported!") 291 else: 292 if outputType == "txt": 293 raise ValueError( 294 ".txt type outputs are only supported for justCores=True!!" 295 ) 296 if self.method == "ttsvd": 297 with open(directory + fileName + ".ttc", "wb") as saveFile: 298 dump(self, saveFile) 299 else: 300 raise ValueError("Unknown Method!") 301 302 @staticmethod 303 def loadData(fileName: str, numCores=None) -> "ttObject": 304 """ 305 Loads data from a `.ttc`, `.npy`, or `.txt` file 306 307 308 Static method to load TT-cores into a ttObject object. 309 Note 310 ---- 311 If data is stored in {coreFile}_{coreIdx}.txt format, 312 the input fileName should just be coreFile.txt 313 314 Parameters 315 ---------- 316 fileName:obj:`str` 317 Name of the file that will be loaded. 318 numCores:obj:`int` 319 Number of cores that the resulting `ttObject` will have 320 (Only required when input data format is `.txt`) 321 """ 322 fileExt = fileName.split(".")[-1] 323 if fileExt == "ttc": 324 with open(fileName, "rb") as f: 325 dataSetObject = load(f) 326 return dataSetObject 327 elif fileExt == "txt": 328 if numCores is None: 329 raise ValueError("Number of cores are not defined!!") 330 fileBody = fileName.split(".")[0] 331 coreList = [] 332 for coreIdx in range(numCores): 333 with open(f"{fileBody}_{coreIdx}.{fileExt}"): 334 coreShape = f.readline()[2:-1] 335 coreShape = [int(item) for item in coreShape.split(" ")] 336 coreList.append( 337 np.loadtxt(f"{fileBody}_{coreIdx}.{fileExt}").reshape[coreShape] 338 ) 339 return coreList 340 elif fileExt == "npy": 341 if numCores is None: 342 raise ValueError("Number of cores are not defined!!") 343 fileBody = fileName.split(".")[0] 344 coreList = [] 345 for coreIdx in range(numCores): 346 coreList.append( 347 np.load(f"{fileBody}_{coreIdx}.{fileExt}") 348 ) 349 return ttObject(coreList) 350 else: 351 raise ValueError(f"{fileExt} files are not supported!") 352 353 @staticmethod 354 def ttDot(tt1, tt2) -> float(): 355 """ 356 Computes the dot product of two tensors in TT format. 357 358 Parameters 359 ---------- 360 tt1:obj:`ttObject` 361 Tensor in TT-format 362 tt2:obj:`ttObject` 363 Tensor in TT-format 364 365 Returns 366 ------- 367 prod:obj:`float` 368 Dot product of `tt1` and `tt2`. 369 370 Raises 371 ------ 372 AttributeError 373 When either one of the input parameters are not `ttObject`s. 374 375 """ 376 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 377 if isinstance(tt1, list): 378 tt1 = ttObject(tt1) 379 if isinstance(tt2, list): 380 tt2 = ttObject(tt2) 381 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 382 raise AttributeError("One of the passed objects is not in TT-format!") 383 prod = np.kron(tt1.ttCores[0][:, 0, :], tt2.ttCores[0][:, 0, :]) 384 for i1 in range(1, tt1.ttCores[0].shape[1]): 385 prod += np.kron(tt1.ttCores[0][:, i1, :], tt2.ttCores[0][:, i1, :]) 386 for coreIdx in range(1, len(tt1.ttCores)): 387 p = [] 388 for ik in range(tt1.ttCores[coreIdx].shape[1]): 389 p.append( 390 prod 391 @ ( 392 np.kron( 393 tt1.ttCores[coreIdx][:, ik, :], tt2.ttCores[coreIdx][:, ik, :] 394 ) 395 ) 396 ) 397 prod = np.sum(p, axis=0) 398 return prod.item() 399 400 @staticmethod 401 def ttNorm(tt1) -> float: 402 """ 403 Computes norm of a tensor in TT-format. 404 405 Parameters 406 ---------- 407 tt1:obj:`ttObject` 408 Tensor in TT-format 409 410 Returns 411 ------- 412 norm:obj:`float` 413 Norm of `tt1`. 414 415 Raises 416 ------ 417 AttributeError 418 When the input parameter is not a `ttObject`. 419 """ 420 if not isinstance(tt1, ttObject): 421 if isinstance(tt1, list): 422 tt1 = ttObject(tt1) 423 else: 424 raise AttributeError("Passed object is not in TT-format!") 425 try: 426 norm = ttObject.ttDot(tt1, tt1) 427 norm = np.sqrt(norm) 428 except MemoryError: 429 norm = np.linalg.norm(tt1.ttCores[-1]) 430 return norm 431 432 def projectTensor(self, newData: np.array, upTo=None) -> np.array: 433 """ 434 Projects tensors onto basis spanned by TT-cores. 435 436 Given a tensor with appropriate dimensions, this function leverages 437 the fact that TT-cores obtained through `TTSVD`_ and `TT-ICE`_ are 438 column-orthonormal in the mode-2 unfolding. 439 440 Note 441 ---- 442 This function will not yield correct results if the TT-cores are 443 not comprised of orthogonal vectors. 444 445 Parameters 446 ---------- 447 newData:obj:`np.aray` 448 Tensor to be projected 449 upTo:obj:`int`, optional 450 Index that the projection will be terminated. If an integer is 451 passed as this parameter, `newTensor` will be projected up to 452 (not including) the core that has index `upTo`. Assumes 1-based 453 indexing. 454 455 .. _TTSVD: 456 https://epubs.siam.org/doi/epdf/10.1137/090752286 457 _TT-ICE: 458 https://arxiv.org/abs/2211.12487 459 _TT-ICE*: 460 https://arxiv.org/abs/2211.12487 461 """ 462 for coreIdx, core in enumerate(self.ttCores): 463 if (coreIdx == len(self.ttCores) - 1) or coreIdx == upTo: 464 break 465 newData = ( 466 core.reshape(np.prod(core.shape[:2]), -1).transpose() 467 ) @ newData.reshape( 468 self.ttRanks[coreIdx] * self.ttCores[coreIdx].shape[1], -1 469 ) 470 return newData 471 472 def reconstruct(self, projectedData, upTo=None): 473 """ 474 Reconstructs tensors using TT-cores. 475 476 Assumes that `projectedData` is a slice from the last 477 TT-core. 478 While reconstructing any projected tensor from `projectTensor`, 479 this function leverages the fact that TT-cores obtained through 480 `TTSVD`_ and `TT-ICE`_ are column-orthonormal in the mode-2 481 unfolding. 482 483 Note 484 ---- 485 This function might not yield correct results for projected tensors 486 if the TT-cores are not comprised of orthogonal vectors. 487 488 Parameters 489 ---------- 490 projectedData:obj:`np.aray` 491 A tensor slice (or alternatively an array) 492 upTo:obj:`int`, optional 493 Index that the reconstruction will be terminated. If an integer is 494 passed as this parameter, `projectedData` will be projected up to 495 (not including) the core that has index `upTo`. Assumes 1-based 496 indexing. 497 .. _TTSVD: 498 https://epubs.siam.org/doi/epdf/10.1137/090752286 499 _TT-ICE: 500 https://arxiv.org/abs/2211.12487 501 502 """ 503 if upTo is None: 504 upTo = len(self.ttCores) - 1 # Write the core index in 1-indexed form!! 505 for core in self.ttCores[:upTo][::-1]: 506 projectedData = np.tensordot(core, projectedData, axes=(-1, 0)) 507 return projectedData 508 509 def updateRanks(self) -> None: 510 """ 511 Updates the ranks of the `ttObject` after incremental updates. 512 """ 513 self.ttRanks = [1] 514 for core in self.ttCores: 515 self.ttRanks.append(core.shape[-1]) 516 return None 517 518 def computeRelError(self, newTensor: np.array, useExact=True) -> np.array: 519 """ 520 Computes relative error by projecting data onto TT-cores. 521 522 This function computes the error induced by projecting `data` onto the TT-cores 523 of `ttObject`. The last index of `newTensor` is assumed to be the number of 524 individual observations. 525 526 Note 527 ---- 528 - In order to compute the projection onto the TT-cores, the dimensions of `data` 529 should match that of `ttObject`. 530 - If a single observation will be passed as `newTensor`, an additional 531 index/dimension should be introduced either through reshaping or [:,None] 532 533 Parameters 534 ---------- 535 newTensor:obj:`np.array` 536 Tensor for which the projection error is computed 537 useExact:`bool` 538 Boolean flag determining whether a projection error will be 539 returned for the entire batch or each observation in the batch. 540 541 Returns 542 ------- 543 relError:obj:`np.array` 544 Array of relative errors 545 """ 546 elementwiseNorm = np.linalg.norm(newTensor, axis=0) 547 for _ in range(len(newTensor.shape) - 2): 548 elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0) 549 projectedData = self.projectTensor(newTensor) 550 reconstructedData = self.reconstruct(projectedData).reshape(newTensor.shape) 551 difference = newTensor - reconstructedData 552 differenceNorm = np.linalg.norm(difference, axis=0) 553 for _ in range(len(difference.shape) - 2): 554 differenceNorm = np.linalg.norm(differenceNorm, axis=0) 555 556 if useExact: 557 relError = np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm) 558 else: 559 relError = differenceNorm / elementwiseNorm 560 return relError 561 562 def computeRecError(self, data: np.array, start=None, finish=None, useExact=True) -> None: 563 """ 564 Function to compute relative error by reconstructing data from slices 565 of TT-cores. 566 567 Parameters 568 ---------- 569 data:obj:`np.array` 570 Tensor for which the reconstruction error is computed 571 start:`int` 572 Index for the start of the batch in the compressed last core 573 finish:`int` 574 Index for the end of the batch in the compressed last core 575 useExact:`bool` 576 Boolean flag determining whether a reconstruction error will be 577 returned for the entire batch or each observation in the batch. 578 579 Returns 580 ------- 581 recError:obj:`float` or `np.array` 582 Reconstruction error of the data from cores. Returns an array 583 of reconstruction errors for each observation in the batch if 584 `useExact` is `False`. 585 """ 586 if start is None: 587 start = self.ttCores[-1].shape[1]-1 588 if finish is None: 589 finish = start+1 590 rec=self.reconstruct(self.ttCores[-1][:,start:finish,:]).reshape(data.shape) 591 elementwiseNorm=np.linalg.norm(data,axis=0) 592 for idx in range(len(data.shape)-2): 593 elementwiseNorm=np.linalg.norm(elementwiseNorm,axis=0) 594 difference=data-rec 595 differenceNorm=np.linalg.norm(difference,axis=0) 596 for idx in range(len(difference.shape)-2): 597 differenceNorm=np.linalg.norm(differenceNorm,axis=0) 598 599 if useExact: 600 recError=np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm) 601 else: 602 recError=differenceNorm/elementwiseNorm 603 604 return recError 605 606 def ttDecomp(self, norm=None, dtype=np.float32) -> "ttObject.ttCores": 607 """ 608 Computes TT-decomposition of a multidimensional array using `TTSVD`_ algorithm. 609 610 Currently only supports `ttsvd` as method. In the future additional formats may 611 be covered. 612 613 Parameters 614 ---------- 615 norm:obj:`float`, optional 616 Norm of the tensor to be compressed 617 dtype:obj:`type`, optional 618 Desired data type for the compression. Intended to allow lower precision 619 if needed. 620 621 Raises 622 ------ 623 ValueError 624 When `method` is not one of the admissible methods. 625 626 627 The following attributes are modified as a result of this function: 628 ------- 629 - `ttObject.ttCores` 630 - `ttObject.ttRanks` 631 - `ttObject.compressionRatio` 632 633 .. _TTSVD: 634 https://epubs.siam.org/doi/epdf/10.1137/090752286 635 """ 636 if norm is None: 637 norm = np.linalg.norm(self.originalData) 638 if self.method == "ttsvd": 639 startTime = time.time() 640 self.ttRanks, self.ttCores = ttsvd( 641 self.originalData, norm, self.ttEpsilon, dtype=dtype 642 ) 643 self.compressionTime = time.time() - startTime 644 self.nCores = len(self.ttCores) 645 self.nElements = 0 646 for cores in self.ttCores: 647 self.nElements += np.prod(cores.shape) 648 if not self.keepOriginal: 649 self.originalData = None 650 return None 651 else: 652 raise ValueError("Method unknown. Please select a valid method!") 653 654 def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None: 655 """ 656 `TT-ICE`_ algorithmn without any heuristic upgrades. 657 658 Given a set of TT-cores, this function provides incremental updates 659 to the TT-cores to approximate `newTensor` within a relative error 660 defined in `epsilon` 661 662 Note 663 ---- 664 This algorithm/function relies on the fact that TT-cores are columnwise 665 orthonormal in the mode-2 unfolding. 666 667 Parameters 668 ---------- 669 newTensor:obj:`np.array` 670 New/streamed tensor that will be used to expand the orthonormal bases defined 671 in TT-cores 672 epsilon:obj:`float`, optional 673 Relative error upper bound for approximating `newTensor` after incremental 674 updates. If not defined, `ttObject.ttEpsilon` is used. 675 tenNorm:obj:`float`, optional 676 Norm of `newTensor` 677 678 Notes 679 ------- 680 **The following attributes are modified as a result of this function:** 681 - `ttObject.ttCores` 682 - `ttObject.ttRanks` 683 - `ttObject.compressionRatio` 684 .. _TT-ICE: 685 https://arxiv.org/abs/2211.12487 686 """ 687 if tenNorm is None: 688 tenNorm = np.linalg.norm(newTensor) 689 if epsilon is None: 690 epsilon = self.ttEpsilon 691 newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 692 newTensorSize = len(newTensor.shape) - 1 693 newTensor = newTensor.reshape(self.reshapedShape[0], -1) 694 Ui = self.ttCores[0].reshape(self.reshapedShape[0], -1) 695 Ri = newTensor - Ui @ (Ui.T @ newTensor) 696 for coreIdx in range(0, len(self.ttCores) - 2): 697 URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon) 698 self.ttCores[coreIdx] = np.hstack((Ui, URi)).reshape( 699 self.ttCores[coreIdx].shape[0], self.reshapedShape[coreIdx], -1 700 ) 701 self.ttCores[coreIdx + 1] = np.concatenate( 702 ( 703 self.ttCores[coreIdx + 1], 704 np.zeros( 705 ( 706 URi.shape[-1], 707 self.reshapedShape[coreIdx + 1], 708 self.ttRanks[coreIdx + 2], 709 ) 710 ), 711 ), 712 axis=0, 713 ) 714 Ui = self.ttCores[coreIdx].reshape( 715 self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1 716 ) 717 newTensor = (Ui.T @ newTensor).reshape( 718 np.prod(self.ttCores[coreIdx + 1].shape[:-1]), -1 719 ) 720 Ui = self.ttCores[coreIdx + 1].reshape( 721 self.ttCores[coreIdx].shape[-1] * self.reshapedShape[coreIdx + 1], -1 722 ) 723 Ri = newTensor - Ui @ (Ui.T @ newTensor) 724 coreIdx = len(self.ttCores) - 2 725 URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon) 726 self.ttCores[coreIdx] = np.hstack((Ui, URi)) 727 self.ttCores[coreIdx + 1] = np.concatenate( 728 ( 729 self.ttCores[coreIdx + 1], 730 np.zeros( 731 ( 732 URi.shape[-1], 733 self.ttCores[coreIdx + 1].shape[1], 734 self.ttRanks[coreIdx + 2], 735 ) 736 ), 737 ), 738 axis=0, 739 ) 740 newTensor = self.ttCores[coreIdx].T @ newTensor 741 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 742 self.ttCores[coreIdx - 1].shape[-1], self.reshapedShape[coreIdx], -1 743 ) 744 coreIdx += 1 745 Ui = self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0], -1) 746 self.ttCores[coreIdx] = np.hstack((Ui, newTensor)).reshape( 747 self.ttCores[coreIdx].shape[0], -1, 1 748 ) 749 self.updateRanks() 750 return None 751 752 def ttICEstar( 753 self, 754 newTensor: np.array, 755 epsilon: float = None, 756 tenNorm: float = None, 757 elementwiseNorm: np.array = None, 758 elementwiseEpsilon: np.array = None, 759 heuristicsToUse: list = ["skip", "subselect", "occupancy"], 760 occupancyThreshold: float = 0.8, 761 simpleEpsilonUpdate: bool = False, 762 surrogateThreshold=True, 763 sampleEpsilon=False, 764 samplePercent=0.1 765 ) -> None: 766 """ 767 `TT-ICE*`_ algorithmn with heuristic performance upgrades. 768 769 Given a set of TT-cores, this function provides incremental updates 770 to the TT-cores to approximate `newTensor` within a relative error 771 defined in `epsilon`. 772 773 Note 774 ---- 775 This algorithm/function relies on the fact that TT-cores are columnwise 776 orthonormal in the mode-2 unfolding. 777 778 Parameters 779 ---------- 780 newTensor:obj:`np.array` 781 New/streamed tensor that will be used to expand the orthonormal bases defined 782 in TT-cores 783 epsilon:obj:`float`, optional 784 Relative error upper bound for approximating `newTensor` after incremental 785 updates. If not defined, `ttObject.ttEpsilon` is used. 786 tenNorm:obj:`float`, optional 787 Norm of `newTensor`. 788 elementwiseNorm:obj:`np.array`, optional 789 Individual norms of the observations in `newTensor`. 790 elementwiseEpsilon:obj:`np.array`, optional 791 Individual relative projection errors of the observations in `newTensor`. 792 heuristicsToUse:obj:`list`, optional 793 List of heuristics to use while updating TT-cores. Currently only accepts 794 `'skip'`, `'subselect'`, and `'occupancy'`. 795 occupancyThreshold:obj:`float`, optional 796 Threshold determining whether to skip updating a single core or not. Not used 797 if `'occupancy'` is not in `heuristicsToUse` 798 simpleEpsilonUpdate:obj:`bool`, optional 799 Uses the simple epsilon update equation. *Warning*: this relies on the 800 assumption that all observations in `newTensor` have similar norms. 801 802 Notes 803 ------- 804 **The following attributes are modified as a result of this function:** 805 - `ttObject.ttCores` 806 - `ttObject.ttRanks` 807 - `ttObject.compressionRatio` 808 .. _TT-ICE*: 809 https://arxiv.org/abs/2211.12487 810 """ 811 if epsilon is None: 812 epsilon = self.ttEpsilon 813 if ("subselect" in heuristicsToUse) and (newTensor.shape[-1] == 1): 814 warning( 815 "The streamed tensor has only 1 observation in it. \ 816 Subselect heuristic will not be useful!!" 817 ) 818 newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 819 updEpsilon = epsilon 820 newTensorSize = len(newTensor.shape) - 1 821 822 if elementwiseEpsilon is None: 823 elementwiseEpsilon = self.computeRelError(newTensor) 824 if "skip" in heuristicsToUse: 825 if surrogateThreshold: 826 if sampleEpsilon: 827 sampleSize=int(newTensor.shape[-1]*samplePercent) 828 skipCriterion=np.mean(np.random.choice(elementwiseEpsilon,size=sampleSize,replace=False)) 829 else: 830 skipCriterion=np.mean(elementwiseEpsilon) 831 if skipCriterion<= epsilon: 832 newTensor = self.projectTensor(newTensor) 833 self.ttCores[-1] = np.hstack( 834 (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor) 835 ).reshape(self.ttRanks[-2], -1, 1) 836 return None 837 else: 838 errorNorm=elementwiseEpsilon*elementwiseNorm 839 errorNorm=np.sqrt(np.sum(errorNorm**2))/np.linalg.norm(elementwiseNorm) 840 if errorNorm <= epsilon: 841 newTensor = self.projectTensor(newTensor) 842 self.ttCores[-1] = np.hstack( 843 (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor) 844 ).reshape(self.ttRanks[-2], -1, 1) 845 return None 846 if tenNorm is None and elementwiseNorm is None: 847 tenNorm = np.linalg.norm(newTensor) 848 elif tenNorm is None: 849 tenNorm = np.linalg.norm(elementwiseNorm) 850 851 select = [True] * newTensor.shape[-1] 852 discard = [False] * newTensor.shape[-1] 853 if "subselect" in heuristicsToUse: 854 select = elementwiseEpsilon > epsilon 855 discard = elementwiseEpsilon <= epsilon 856 if simpleEpsilonUpdate: 857 updEpsilon = ( 858 epsilon * newTensor.shape[-1] 859 - np.mean(elementwiseEpsilon[discard]) * discard.sum() 860 ) / (select.sum()) 861 else: 862 if elementwiseNorm is None: 863 elementwiseNorm = np.linalg.norm(newTensor, axis=0) 864 for _ in range(len(self.ttCores) - 1): 865 elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0) 866 allowedError = (self.ttEpsilon * np.linalg.norm(elementwiseNorm)) ** 2 867 discardedError = np.sum( 868 (elementwiseEpsilon[discard] * elementwiseNorm[discard]) ** 2 869 ) 870 updEpsilon = np.sqrt( 871 (allowedError - discardedError) 872 / (np.linalg.norm(elementwiseNorm[select]) ** 2) 873 ) 874 self.reshapedShape[-1] = np.array( 875 select 876 ).sum() # a little trick for ease of coding 877 878 indexString = "[" 879 for _ in range(len(self.reshapedShape)): 880 # this heuristic assumes that the last dimension is for observations 881 indexString += ":," 882 selectString = indexString + "select]" 883 selected = eval("newTensor" + selectString) 884 885 selected = selected.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 886 for coreIdx in range(0, len(self.ttCores) - 1): 887 selected = selected.reshape(np.prod(self.ttCores[coreIdx].shape[:-1]), -1) 888 if ("occupancy" in heuristicsToUse) and ( 889 self.coreOccupancy[coreIdx] >= occupancyThreshold 890 ): 891 pass 892 else: 893 Ui = self.ttCores[coreIdx].reshape( 894 self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1 895 ) 896 Ri = selected - Ui @ (Ui.T @ selected) 897 if (elementwiseNorm is None) or ("subselect" not in heuristicsToUse): 898 URi, _, _ = deltaSVD( 899 Ri, np.linalg.norm(selected), newTensorSize, updEpsilon 900 ) 901 else: 902 URi, _, _ = deltaSVD( 903 Ri, 904 np.linalg.norm(elementwiseNorm[select]), 905 newTensorSize, 906 updEpsilon, 907 ) 908 self.ttCores[coreIdx] = np.hstack((Ui, URi)) 909 self.ttCores[coreIdx + 1] = np.concatenate( 910 ( 911 self.ttCores[coreIdx + 1], 912 np.zeros( 913 ( 914 URi.shape[-1], 915 self.ttCores[coreIdx + 1].shape[1], 916 self.ttRanks[coreIdx + 2], 917 ) 918 ), 919 ), 920 axis=0, 921 ) 922 923 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 924 np.prod(self.ttCores[coreIdx].shape[:-1]), -1 925 ) 926 # #project onto existing core and reshape for next core 927 selected = (self.ttCores[coreIdx].T @ selected).reshape( 928 self.ttCores[coreIdx + 1].shape[0] * self.reshapedShape[coreIdx + 1], -1 929 ) 930 # fold back the previous core 931 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 932 -1, self.reshapedShape[coreIdx], self.ttCores[coreIdx].shape[-1] 933 ) 934 # self.ttCores[coreIdx]=self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0],self.reshapedShape[coreIdx],-1) 935 # #fold back the previous core 936 self.updateRanks() 937 coreIdx += 1 938 # coreIdx=len(self.ttCores), i.e working on the last core 939 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 940 self.ttCores[coreIdx].shape[0], -1 941 ) 942 self.ttCores[coreIdx] = np.hstack( 943 (self.ttCores[coreIdx], self.projectTensor(newTensor)) 944 ).reshape(self.ttCores[coreIdx].shape[0], -1, 1) 945 return None 946 947 def ttRound(self, norm, epsilon=0) -> None: 948 """ 949 Reorthogonalizes and recompresses TT-cores as per `Oseledets 2011`_. 950 951 This is the TT-rounding algorithm described in `TTSVD`_ paper. This 952 function first reorthogonalizes the cores from last to first using 953 QR decomposition and then recompresses the reorthogonalized TT-cores 954 from first to last using `TTSVD`_ algorithm. 955 956 Parameters 957 ---------- 958 norm:obj:`float` 959 Norm of the TT-approximation. If there is no information on the 960 uncompressed tensor, `ttNorm` function can be used. 961 epsilon:obj:`float`, optional 962 Relative error upper bound for the recompression step. Note that 963 this relative error upper bound is not with respect to the original 964 tensor but to the `compressed` tensor. Set to 0 by default. 965 966 Notes 967 ----- 968 **The following attributes are modified as a result of this function:** 969 - `ttObject.ttCores` 970 - `ttObject.ttRanks` 971 - `ttObject.compressionRatio` 972 973 .. _Oseledets 2011: 974 https://epubs.siam.org/doi/epdf/10.1137/090752286 975 TTSVD: 976 https://epubs.siam.org/doi/epdf/10.1137/090752286 977 """ 978 d = [core.shape[1] for core in self.ttCores] 979 for coreIdx in np.arange(len(self.ttCores))[::-1]: 980 currCore = self.ttCores[coreIdx] 981 # Compute mode 1 unfolding of the tt-core 982 currCore = currCore.reshape(currCore.shape[0], -1) 983 # Using the transpose results in a row orthogonal Q matrix !!! 984 q, r = np.linalg.qr(currCore.T) 985 q, r = q.T, r.T 986 self.ttCores[coreIdx] = q 987 # Contract previous tt-core and R matrix 988 self.ttCores[coreIdx - 1] = np.tensordot( 989 self.ttCores[coreIdx - 1], r, axes=(-1, 0) 990 ) 991 if coreIdx == 1: 992 break 993 # pass #TODO: write the case for the last core here. 994 # Compression of the orthogonalized representation # 995 ranks = [1] 996 for coreIdx in range(len(self.ttCores) - 1): 997 core = self.ttCores[coreIdx] 998 core = core.reshape(np.prod(core.shape[:-1]), -1) 999 self.ttCores[coreIdx], sigma, v = deltaSVD( 1000 core, norm, len(self.ttCores), epsilon 1001 ) 1002 ranks.append(self.ttCores[coreIdx].shape[-1]) 1003 # fold matrices into tt-cores 1004 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 1005 ranks[coreIdx], d[coreIdx], -1 1006 ) 1007 self.ttCores[coreIdx + 1] = ( 1008 (np.diag(sigma) @ v) @ self.ttCores[coreIdx + 1] 1009 ).reshape(ranks[-1] * d[coreIdx + 1], -1) 1010 self.ttCores[-1] = self.ttCores[-1].reshape(-1, d[-1], 1) 1011 self.ttRanks = [1] 1012 for core in self.ttCores: 1013 self.ttRanks.append(core.shape[-1]) 1014 return None 1015 1016 @staticmethod 1017 def ittd(tt1, tt2, rounding=True, epsilon=0) -> "ttObject": 1018 """ 1019 Alternative incremental tensor decomposition algorithm used to benchmark. 1020 1021 Note 1022 ---- 1023 This method is empirically shown to be inferior to `TT-ICE`_ and 1024 `TT-ICE*`_. 1025 1026 Parameters 1027 ---------- 1028 tt1:obj:`ttObject` 1029 Tensor in TT-format. 1030 tt2:obj:`ttObject` 1031 Tensor in TT-format. 1032 rounding:obj:`bool`, optional 1033 Determines if `ttRounding` will be done to the incremented TT-core. 1034 Note that for unbounded streams, rounding is essential. Otherwise 1035 TT-ranks will grow unboundedly. 1036 epsilon:obj:`float`, optional 1037 Relative error upper bound for the recompression step. Note that 1038 this relative error upper bound is not with respect to the original 1039 tensor but to the `compressed` tensor. Set to 0 by default. Not used 1040 if `rounding` is set to `False`. 1041 1042 Notes 1043 ----- 1044 **The following attributes are modified as a result of this function:** 1045 - `ttObject.ttCores` 1046 - `ttObject.ttRanks` 1047 - `ttObject.compressionRatio` 1048 1049 .. _TT-ICE: 1050 https://arxiv.org/abs/2211.12487 1051 _TT-ICE*: 1052 https://arxiv.org/abs/2211.12487 1053 """ 1054 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 1055 if isinstance(tt1, list): 1056 tt1 = ttObject(tt1) 1057 if isinstance(tt2, list): 1058 tt2 = ttObject(tt2) 1059 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 1060 raise AttributeError("One of the passed objects is not in TT-format!") 1061 tt1.ttCores[-1] = tt1.ttCores[-1].squeeze(-1) 1062 tt2.ttCores[-1] = tt2.ttCores[-1].squeeze(-1) 1063 rank1, numObs1 = tt1.ttCores[-1].shape 1064 rank2, numObs2 = tt2.ttCores[-1].shape 1065 tt1.ttCores[-1] = np.concatenate( 1066 (tt1.ttCores[-1], np.zeros((rank1, numObs2))), axis=1 1067 ) 1068 tt2.ttCores[-1] = np.concatenate( 1069 (np.zeros((rank2, numObs1)), tt2.ttCores[-1]), axis=1 1070 ) 1071 # Addition in TT-format # 1072 for coreIdx in range(len(tt1.ttCores)): 1073 if coreIdx == 0: 1074 tt1.ttCores[coreIdx] = np.concatenate( 1075 (tt1.ttCores[coreIdx].squeeze(0), tt2.ttCores[coreIdx].squeeze(0)), 1076 axis=1, 1077 )[None, :, :] 1078 elif coreIdx == len(tt1.ttCores) - 1: 1079 tt1.ttCores[coreIdx] = np.concatenate( 1080 (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0 1081 )[:, :, None] 1082 else: 1083 s11, s12, s13 = tt1.ttCores[coreIdx].shape 1084 s21, s22, s23 = tt2.ttCores[coreIdx].shape 1085 tt1.ttCores[coreIdx] = np.concatenate( 1086 (tt1.ttCores[coreIdx], np.zeros((s11, s12, s23))), axis=2 1087 ) 1088 tt2.ttCores[coreIdx] = np.concatenate( 1089 (np.zeros((s21, s22, s13)), tt2.ttCores[coreIdx]), axis=2 1090 ) 1091 tt1.ttCores[coreIdx] = np.concatenate( 1092 (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0 1093 ) 1094 tt1.ttRanks = [1] 1095 for core in tt1.ttCores: 1096 tt1.ttRanks.append(core.shape[-1]) 1097 if rounding: 1098 tenNorm = ttObject.ttNorm(tt1) 1099 tt1.ttRound(tenNorm, epsilon=epsilon) 1100 return tt1 1101 1102 @staticmethod 1103 def ttfoa6d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None): 1104 nDims=len(data.shape) 1105 dims=list(data.shape) 1106 coreSwap=False 1107 if forgettingFactor is None: 1108 forgettingFactor=0.7 1109 1110 if len(ttRanks) != nDims-1: 1111 raise ValueError("Number of dimensions do not match with number of TT-ranks") 1112 1113 if ttCores is None: 1114 # Initialize TT-cores randomly if no core is given. 1115 ttCores=[] 1116 ttCores.append(np.random.randn(1,dims[0],ttRanks[0])) 1117 for dimIdx in range(1,nDims-1): 1118 ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx])) 1119 ttCores.append(np.empty((ttRanks[-1],1))) 1120 coreSwap=True 1121 # else: 1122 ttCores_old=ttCores.copy() 1123 if S is None: 1124 # Initialize S matrices as identity matrices if no S is given from previous step. 1125 S=[] 1126 S.append(100*np.eye(ttRanks[0])) 1127 for dimIdx in range(1,nDims-1): 1128 S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx])) 1129 1130 # Update the last core G6 1131 1132 Ht_1=coreContraction(ttCores_old[:-1]) 1133 # Ht_1=Ht_1.reshape(-1,ttRanks[-1]) 1134 Ht_1=mode_n_unfolding(Ht_1,5).T #this might be 5 1135 g6=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F') 1136 if coreSwap: 1137 ttCores[-1]=g6 1138 else: 1139 ttCores[-1]=np.hstack((ttCores[-1],g6)) 1140 recData=(Ht_1@g6).reshape(dims,order='F') 1141 delta=data-recData 1142 1143 # Update G1 1144 deltaunfold=mode_n_unfolding(delta,0) 1145 Bk=coreContraction(ttCores_old[1:-1]+[g6]) 1146 Wk=mode_n_unfolding(Bk,0) 1147 S[0]=forgettingFactor*S[0]+Wk@Wk.T 1148 # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0] 1149 Vk=np.linalg.pinv(S[0])@Wk 1150 ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T 1151 1152 # Update G2 1153 deltaunfold=mode_n_unfolding(delta,1) 1154 Ak=mode_n_unfolding(ttCores_old[0],2)#.T 1155 Bk=coreContraction(ttCores_old[2:-1]+[g6]) 1156 Bk=mode_n_unfolding(Bk,0) 1157 Wk=np.kron(Bk,Ak) 1158 S[1]=forgettingFactor*S[1]+Wk@Wk.T 1159 # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0] 1160 Vk=np.linalg.pinv(S[1])@Wk 1161 ttCores[1]=mode_n_unfolding(ttCores_old[1],1) 1162 ttCores[1]=ttCores[1]+deltaunfold@Vk.T 1163 ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2) 1164 1165 # Update G3 1166 deltaunfold=mode_n_unfolding(delta,2) 1167 Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2) 1168 Bk=coreContraction(ttCores_old[3:-1]+[g6]) #insert a mode 0 unfolding here if necessary 1169 Bk=mode_n_unfolding(Bk,0) 1170 Wk=np.kron(Bk,Ak) 1171 S[2]=forgettingFactor*S[2]+Wk@Wk.T 1172 # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0] 1173 Vk=np.linalg.pinv(S[2])@Wk 1174 ttCores[2]=mode_n_unfolding(ttCores_old[2],1) 1175 ttCores[2]=ttCores[2]+deltaunfold@Vk.T 1176 ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2) 1177 1178 # Update G4 1179 deltaunfold=mode_n_unfolding(delta,3) 1180 Ak=mode_n_unfolding(coreContraction(ttCores_old[:3]),3) 1181 Bk=coreContraction(ttCores_old[4:-1]+[g6]) #insert a mode 0 unfolding here if necessary 1182 Wk=np.kron(Bk,Ak) 1183 S[3]=forgettingFactor=S[3]+Wk@Wk.T 1184 # Vk=np.linalg.lstsq(S[3],Wk,rcond=None)[0] 1185 Vk=np.linalg.pinv(S[3])@Wk 1186 ttCores[3]=mode_n_unfolding(ttCores_old[3],1) 1187 ttCores[3]=ttCores[3]+deltaunfold@Vk.T 1188 ttCores[3]=ttCores[3].reshape(dims[3],ttRanks[2],ttRanks[3],order='F').transpose(1,0,2) 1189 1190 # Update G5 1191 deltaunfold=mode_n_unfolding(delta,4) 1192 Ak=mode_n_unfolding(coreContraction(ttCores_old[:4]),4) 1193 # Bk=coreContraction(g6) #insert a mode 0 unfolding here if necessary 1194 Wk=np.kron(g6,Ak) 1195 S[4]=forgettingFactor=S[4]+Wk@Wk.T 1196 # Vk=np.linalg.lstsq(S[4],Wk,rcond=None)[0] 1197 Vk=np.linalg.pinv(S[4])@Wk 1198 ttCores[4]=mode_n_unfolding(ttCores_old[4],1) 1199 ttCores[4]=ttCores[4]+deltaunfold@Vk.T 1200 ttCores[4]=ttCores[4].reshape(dims[4],ttRanks[3],ttRanks[4],order='F').transpose(1,0,2) 1201 1202 return S,ttCores 1203 1204 @staticmethod 1205 def ttfoa4d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None): 1206 nDims=len(data.shape) 1207 dims=list(data.shape) 1208 coreSwap=False 1209 if forgettingFactor is None: 1210 forgettingFactor=0.7 1211 1212 if len(ttRanks) != nDims-1: 1213 raise ValueError("Number of dimensions do not match with number of TT-ranks") 1214 1215 if ttCores is None: 1216 # Initialize TT-cores randomly if no core is given. 1217 ttCores=[] 1218 ttCores.append(np.random.randn(1,dims[0],ttRanks[0])) 1219 for dimIdx in range(1,nDims-1): 1220 ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx])) 1221 ttCores.append(np.empty((ttRanks[-1],1))) 1222 coreSwap=True 1223 # else: 1224 ttCores_old=ttCores.copy() 1225 if S is None: 1226 # Initialize S matrices as identity matrices if no S is given from previous step. 1227 S=[] 1228 S.append(100*np.eye(ttRanks[0])) 1229 for dimIdx in range(1,nDims-1): 1230 S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx])) 1231 1232 Ht_1=coreContraction(ttCores_old[:-1]) 1233 # Ht_1=Ht_1.reshape(-1,ttRanks[-1]) 1234 Ht_1=mode_n_unfolding(Ht_1,3).T #this might be 4 1235 g4=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F') 1236 if coreSwap: 1237 ttCores[-1]=g4 1238 else: 1239 ttCores[-1]=np.hstack((ttCores[-1],g4)) 1240 recData=(Ht_1@g4).reshape(dims,order='F') 1241 delta=data-recData 1242 1243 # Update G1 1244 deltaunfold=mode_n_unfolding(delta,0) 1245 # Bk=coreContraction(ttCores_old[1:-1]+[g4]) 1246 Bk=np.tensordot(np.tensordot(ttCores_old[1],ttCores_old[2],axes=[-1,0]),g4,axes=[-1,0]) 1247 Wk=mode_n_unfolding(Bk,0) 1248 S[0]=forgettingFactor*S[0]+Wk@Wk.T 1249 # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0] 1250 try: 1251 Vk=np.linalg.solve(S[0],Wk) 1252 except np.linalg.LinAlgError: 1253 print('Singular S[0] matrix, using Moore-Penrose inverse') 1254 Vk=np.linalg.pinv(S[0])@Wk 1255 ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T 1256 1257 # Update G2 1258 deltaunfold=mode_n_unfolding(delta,1) 1259 Ak=mode_n_unfolding(ttCores_old[0],2)#.T 1260 Bk=coreContraction(ttCores_old[2:-1]+[g4]) 1261 Bk=mode_n_unfolding(Bk,0) 1262 Wk=np.kron(Bk,Ak) 1263 S[1]=forgettingFactor*S[1]+Wk@Wk.T 1264 # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0] 1265 try: 1266 Vk=np.linalg.solve(S[1],Wk) 1267 except np.linalg.LinAlgError: 1268 print('Singular S[1] matrix, using Moore-Penrose inverse') 1269 Vk=np.linalg.pinv(S[1])@Wk 1270 ttCores[1]=mode_n_unfolding(ttCores_old[1],1) 1271 ttCores[1]=ttCores[1]+deltaunfold@Vk.T 1272 ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2) 1273 1274 # Update G3 1275 deltaunfold=mode_n_unfolding(delta,2) 1276 Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2) 1277 # Bk=coreContraction(ttCores_old[3:-1]+[g4]) #insert a mode 0 unfolding here if necessary 1278 # Bk=mode_n_unfolding(Bk,0) 1279 Wk=np.kron(g4,Ak) 1280 S[2]=forgettingFactor*S[2]+Wk@Wk.T 1281 # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0] 1282 try: 1283 Vk=np.linalg.solve(S[2],Wk) 1284 except np.linalg.LinAlgError: 1285 print('Singular S[2] matrix, using Moore-Penrose inverse') 1286 Vk=np.linalg.pinv(S[2])@Wk 1287 ttCores[2]=mode_n_unfolding(ttCores_old[2],1) 1288 ttCores[2]=ttCores[2]+deltaunfold@Vk.T 1289 ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2) 1290 1291 return S,ttCores
23class ttObject: 24 """ 25 Python object for tensors in Tensor-Train format. 26 27 This object computes the TT-decomposition of multidimensional arrays using `TTSVD`_, 28 `TT-ICE`_, and `TT-ICE*`_. 29 It furthermore contains an inferior method, ITTD, for benchmarking purposes. 30 31 This object handles various operations in TT-format such as dot product and norm. 32 Also provides supporting operations for uncompressed multidimensional arrays such as 33 reshaping and transpose. 34 Once the tensor train approximation for a multidimensional array is computed, you can 35 compute projection of appropriate arrays onto the TT-cores, reconstruct projected 36 or compressed tensors and compute projection/compression accuracy. 37 38 You can also save/load tensors as `.ttc` or `.txt` files. 39 40 Attributes 41 ---------- 42 inputType: type 43 Type of the input data. This determines how the object is initialized. 44 originalData: 45 Original multidimensional input array. Generally will not be stored 46 after computing 47 an initial set of TT-cores. 48 method: :obj:`str` 49 Method of computing the initial set of TT-cores. Currently only accepts 50 `'ttsvd'` as input 51 ttEpsilon: :obj:`float` 52 Desired relative error upper bound. 53 ttCores: :obj:`list` of :obj:`numpy.array` 54 Cores of the TT-decomposition. Stored as a list of numpy arrays. 55 ttRanks: :obj:`list` of :obj:`int` 56 TT-ranks of the decomposition. Stored as a list of integers. 57 nCores: :obj:`int` 58 Number of TT-cores in the decomposition. 59 nElements: :obj:`int` 60 Number of entries present in the current `ttObject`. 61 originalShape: :obj:`tuple` or :obj:`list` 62 Original shape of the multidimensional array. 63 reshapedShape: :obj:`tuple` or :obj:`list` 64 Shape of the multidimensional array after reshaping. Note that 65 this attribute is only meaningful before computing a TT-decomposition 66 indexOrder: :obj:`list` of :obj:`int` 67 Keeps track of original indices in case of transposing the input array. 68 .. _TTSVD: 69 https://epubs.siam.org/doi/epdf/10.1137/090752286 70 _TT-ICE: 71 https://arxiv.org/abs/2211.12487 72 _TT-ICE*: 73 https://arxiv.org/abs/2211.12487 74 75 """ 76 77 def __init__( 78 self, 79 data, 80 epsilon: float = None, 81 keepData: bool = False, 82 samplesAlongLastDimension: bool = True, 83 method: str = "ttsvd", 84 ) -> None: 85 """ 86 Initializes the ttObject. 87 88 Parameters 89 ---------- 90 data: :obj:`numpy.array` or :obj:`list` 91 Main input to the ttObject. It can either be a multidimensional `numpy array` 92 or `list of numpy arrays`. 93 If list of numpy arrays are presented as input, the object will interpret it 94 as the TT-cores of an existing decomposition. 95 epsilon: :obj:`float`, optional 96 The relative error upper bound desired for approximation. Optional for cases 97 when `data` has type `list`. 98 keepData: :obj:`bool`, optional 99 Optional boolean variable to determine if the original array will be kept 100 after compression. Set to `False` by default. 101 samplesAlongLastDimension: :obj:`bool`, optional 102 Boolean variable to ensure if the samples are stacked along the last 103 dimension. Assumed to be `True` since it is one of the assumptions in TT-ICE. 104 method: :obj:`str`, optional 105 Determines the computation method for tensor train decomposition of 106 the multidimensional array presented as `data`. Set to `'ttsvd'` by default. 107 108 Currently the package only has support for ttsvd, additional support such as 109 `ttcross` might be included in the future. 110 111 """ 112 # self.ttRanks=ranks 113 self.ttCores = None 114 self.nCores = None 115 self.nElements = None 116 self.inputType = type(data) 117 self.method = method 118 self.keepOriginal = keepData 119 self.originalData = data 120 self.samplesAlongLastDimension = samplesAlongLastDimension 121 if self.inputType is np.memmap: 122 self.inputType = np.ndarray 123 124 if self.inputType == np.ndarray: 125 self.ttEpsilon = epsilon 126 self.originalShape = list(data.shape) 127 self.reshapedShape = self.originalShape 128 self.indexOrder = [idx for idx in range(len(self.originalShape))] 129 elif self.inputType == list: 130 self.nCores = len(data) 131 self.ttCores = data 132 self.reshapedShape = [core.shape[1] for core in self.ttCores] 133 else: 134 raise TypeError("Unknown input type!") 135 136 @property 137 def coreOccupancy(self) -> None: 138 """ 139 :obj:`list` of :obj:`float`: A metric showing the *relative rank* of each 140 TT-core. This metric is used for a heuristic enhancement tool in `TT-ICE*` 141 algorithm 142 """ 143 try: 144 return [ 145 core.shape[-1] / np.prod(core.shape[:-1]) for core in self.ttCores[:-1] 146 ] 147 except ValueError: 148 warnings.warn( 149 "No TT cores exist, maybe forgot calling object.ttDecomp?", Warning 150 ) 151 return None 152 153 @property 154 def compressionRatio(self) -> float: 155 """ 156 :obj:`float`: A metric showing how much compression with respect to the 157 original multidimensional array is achieved. 158 """ 159 originalNumEl = 1 160 compressedNumEl = 0 161 for core in self.ttCores: 162 originalNumEl *= core.shape[1] 163 compressedNumEl += np.prod(core.shape) 164 return originalNumEl / compressedNumEl 165 166 def changeShape(self, newShape: tuple or list) -> None: 167 """ 168 Function to change shape of input tensors and keeping track of the reshaping. 169 Reshapes `originalData` and saves the final shape in `reshapedShape` 170 171 Note 172 ---- 173 A simple `numpy.reshape` would be sufficient for this purpose but in order to keep 174 track of the shape changes the `reshapedShape` attribute also needs to be updated 175 accordingly. 176 177 Parameters 178 ---------- 179 newShape:obj:`tuple` or `list` 180 New shape of the tensor 181 182 Raises 183 ------ 184 warning 185 If an attempt is done to modify the shape after computing a TT-decomposition. 186 This is important since it will cause incompatibilities in other functions 187 regarding the shape of the uncompressed tensor. 188 189 """ 190 if self.ttCores is not None: 191 warning( 192 "Warning! You are reshaping the original data after computing a\ 193 TT-decomposition! We will proceed without reshaping self.originalData!!" 194 ) 195 return None 196 self.reshapedShape = newShape 197 self.originalData = np.reshape(self.originalData, self.reshapedShape) 198 self.reshapedShape = list(self.originalData.shape) 199 # Changed reshapedShape to a list for the trick in ttICEstar 200 if self.samplesAlongLastDimension: 201 self.singleDataShape = self.reshapedShape[:-1] 202 # This line assumes we keep the last index as the samples index and don't 203 # interfere with its shape 204 205 def computeTranspose(self, newOrder: list) -> None: 206 """ 207 Transposes the axes of `originalData`. 208 209 Similar to `changeShape`, a simple 210 `numpy.transpose` would be sufficient for this purpose but in order to 211 keep track of the transposition order `indexOrder` attribute also needs 212 to be updated accordingly. 213 214 Parameters 215 ---------- 216 newOrder:obj:`list` 217 New order of the axes. 218 219 Raises 220 ------ 221 ValueError 222 When the number of transposition axes are not equal to the number of 223 dimensions of `originalData`. 224 """ 225 assert self.inputType == np.ndarray and self.ttCores is None 226 if len(newOrder) != len(self.indexOrder): 227 raise ValueError( 228 "size of newOrder and self.indexOrder does not match. \ 229 Maybe you forgot reshaping?" 230 ) 231 self.indexOrder = [self.indexOrder[idx] for idx in newOrder] 232 self.originalData = self.originalData.transpose(newOrder) 233 self.reshapedShape = list(self.originalData.shape) 234 235 def indexMap(self) -> None: 236 """ 237 Function to map the original indices to the reshaped indices. 238 Currently not implemented. 239 """ 240 self.A = 2 241 242 def primeReshaping(self) -> None: 243 """ 244 Function to reshape the first d dimensions to prime factors. 245 Currently not implemented 246 """ 247 self.A = 2 248 249 def saveData( 250 self, fileName: str, directory="./", justCores=True, outputType="ttc" 251 ) -> None: 252 """ 253 Writes the computed TT-cores to an external file. 254 255 Parameters 256 ---------- 257 fileName:obj:`str` 258 259 directory:obj:`str` 260 Location to save files with respect to the present working directory. 261 justCores:obj:`bool` 262 Boolean variable to determine if `originalData` will be discarded 263 or not while saving. 264 outputType:obj:`str` 265 Type of the output file. `ttc` for pickled `ttObject`, `txt` for 266 individual text files for each TT-core. 267 """ 268 if justCores: 269 if outputType == "ttc": 270 with open(directory + fileName + ".ttc", "wb") as saveFile: 271 temp = ttObject(self.ttCores) 272 for attribute in vars(self): 273 if attribute != "originalData": 274 setattr(temp, attribute, eval(f"self.{attribute}")) 275 dump(temp, saveFile) 276 elif outputType == "txt": 277 for coreIdx, core in enumerate(self.ttCores): 278 np.savetxt( 279 directory + f"{fileName}_{coreIdx}.txt", 280 core.reshape(-1, core.shape[-1]), 281 header=f"{core.shape[0]} {core.shape[1]} {core.shape[2]}", 282 delimiter=" ", 283 ) 284 elif outputType == "npy": 285 for coreIdx, core in enumerate(self.ttCores): 286 np.save( 287 directory + f"{fileName}_{coreIdx}.npy", 288 core, 289 ) 290 else: 291 raise ValueError(f"Output type {outputType} is not supported!") 292 else: 293 if outputType == "txt": 294 raise ValueError( 295 ".txt type outputs are only supported for justCores=True!!" 296 ) 297 if self.method == "ttsvd": 298 with open(directory + fileName + ".ttc", "wb") as saveFile: 299 dump(self, saveFile) 300 else: 301 raise ValueError("Unknown Method!") 302 303 @staticmethod 304 def loadData(fileName: str, numCores=None) -> "ttObject": 305 """ 306 Loads data from a `.ttc`, `.npy`, or `.txt` file 307 308 309 Static method to load TT-cores into a ttObject object. 310 Note 311 ---- 312 If data is stored in {coreFile}_{coreIdx}.txt format, 313 the input fileName should just be coreFile.txt 314 315 Parameters 316 ---------- 317 fileName:obj:`str` 318 Name of the file that will be loaded. 319 numCores:obj:`int` 320 Number of cores that the resulting `ttObject` will have 321 (Only required when input data format is `.txt`) 322 """ 323 fileExt = fileName.split(".")[-1] 324 if fileExt == "ttc": 325 with open(fileName, "rb") as f: 326 dataSetObject = load(f) 327 return dataSetObject 328 elif fileExt == "txt": 329 if numCores is None: 330 raise ValueError("Number of cores are not defined!!") 331 fileBody = fileName.split(".")[0] 332 coreList = [] 333 for coreIdx in range(numCores): 334 with open(f"{fileBody}_{coreIdx}.{fileExt}"): 335 coreShape = f.readline()[2:-1] 336 coreShape = [int(item) for item in coreShape.split(" ")] 337 coreList.append( 338 np.loadtxt(f"{fileBody}_{coreIdx}.{fileExt}").reshape[coreShape] 339 ) 340 return coreList 341 elif fileExt == "npy": 342 if numCores is None: 343 raise ValueError("Number of cores are not defined!!") 344 fileBody = fileName.split(".")[0] 345 coreList = [] 346 for coreIdx in range(numCores): 347 coreList.append( 348 np.load(f"{fileBody}_{coreIdx}.{fileExt}") 349 ) 350 return ttObject(coreList) 351 else: 352 raise ValueError(f"{fileExt} files are not supported!") 353 354 @staticmethod 355 def ttDot(tt1, tt2) -> float(): 356 """ 357 Computes the dot product of two tensors in TT format. 358 359 Parameters 360 ---------- 361 tt1:obj:`ttObject` 362 Tensor in TT-format 363 tt2:obj:`ttObject` 364 Tensor in TT-format 365 366 Returns 367 ------- 368 prod:obj:`float` 369 Dot product of `tt1` and `tt2`. 370 371 Raises 372 ------ 373 AttributeError 374 When either one of the input parameters are not `ttObject`s. 375 376 """ 377 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 378 if isinstance(tt1, list): 379 tt1 = ttObject(tt1) 380 if isinstance(tt2, list): 381 tt2 = ttObject(tt2) 382 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 383 raise AttributeError("One of the passed objects is not in TT-format!") 384 prod = np.kron(tt1.ttCores[0][:, 0, :], tt2.ttCores[0][:, 0, :]) 385 for i1 in range(1, tt1.ttCores[0].shape[1]): 386 prod += np.kron(tt1.ttCores[0][:, i1, :], tt2.ttCores[0][:, i1, :]) 387 for coreIdx in range(1, len(tt1.ttCores)): 388 p = [] 389 for ik in range(tt1.ttCores[coreIdx].shape[1]): 390 p.append( 391 prod 392 @ ( 393 np.kron( 394 tt1.ttCores[coreIdx][:, ik, :], tt2.ttCores[coreIdx][:, ik, :] 395 ) 396 ) 397 ) 398 prod = np.sum(p, axis=0) 399 return prod.item() 400 401 @staticmethod 402 def ttNorm(tt1) -> float: 403 """ 404 Computes norm of a tensor in TT-format. 405 406 Parameters 407 ---------- 408 tt1:obj:`ttObject` 409 Tensor in TT-format 410 411 Returns 412 ------- 413 norm:obj:`float` 414 Norm of `tt1`. 415 416 Raises 417 ------ 418 AttributeError 419 When the input parameter is not a `ttObject`. 420 """ 421 if not isinstance(tt1, ttObject): 422 if isinstance(tt1, list): 423 tt1 = ttObject(tt1) 424 else: 425 raise AttributeError("Passed object is not in TT-format!") 426 try: 427 norm = ttObject.ttDot(tt1, tt1) 428 norm = np.sqrt(norm) 429 except MemoryError: 430 norm = np.linalg.norm(tt1.ttCores[-1]) 431 return norm 432 433 def projectTensor(self, newData: np.array, upTo=None) -> np.array: 434 """ 435 Projects tensors onto basis spanned by TT-cores. 436 437 Given a tensor with appropriate dimensions, this function leverages 438 the fact that TT-cores obtained through `TTSVD`_ and `TT-ICE`_ are 439 column-orthonormal in the mode-2 unfolding. 440 441 Note 442 ---- 443 This function will not yield correct results if the TT-cores are 444 not comprised of orthogonal vectors. 445 446 Parameters 447 ---------- 448 newData:obj:`np.aray` 449 Tensor to be projected 450 upTo:obj:`int`, optional 451 Index that the projection will be terminated. If an integer is 452 passed as this parameter, `newTensor` will be projected up to 453 (not including) the core that has index `upTo`. Assumes 1-based 454 indexing. 455 456 .. _TTSVD: 457 https://epubs.siam.org/doi/epdf/10.1137/090752286 458 _TT-ICE: 459 https://arxiv.org/abs/2211.12487 460 _TT-ICE*: 461 https://arxiv.org/abs/2211.12487 462 """ 463 for coreIdx, core in enumerate(self.ttCores): 464 if (coreIdx == len(self.ttCores) - 1) or coreIdx == upTo: 465 break 466 newData = ( 467 core.reshape(np.prod(core.shape[:2]), -1).transpose() 468 ) @ newData.reshape( 469 self.ttRanks[coreIdx] * self.ttCores[coreIdx].shape[1], -1 470 ) 471 return newData 472 473 def reconstruct(self, projectedData, upTo=None): 474 """ 475 Reconstructs tensors using TT-cores. 476 477 Assumes that `projectedData` is a slice from the last 478 TT-core. 479 While reconstructing any projected tensor from `projectTensor`, 480 this function leverages the fact that TT-cores obtained through 481 `TTSVD`_ and `TT-ICE`_ are column-orthonormal in the mode-2 482 unfolding. 483 484 Note 485 ---- 486 This function might not yield correct results for projected tensors 487 if the TT-cores are not comprised of orthogonal vectors. 488 489 Parameters 490 ---------- 491 projectedData:obj:`np.aray` 492 A tensor slice (or alternatively an array) 493 upTo:obj:`int`, optional 494 Index that the reconstruction will be terminated. If an integer is 495 passed as this parameter, `projectedData` will be projected up to 496 (not including) the core that has index `upTo`. Assumes 1-based 497 indexing. 498 .. _TTSVD: 499 https://epubs.siam.org/doi/epdf/10.1137/090752286 500 _TT-ICE: 501 https://arxiv.org/abs/2211.12487 502 503 """ 504 if upTo is None: 505 upTo = len(self.ttCores) - 1 # Write the core index in 1-indexed form!! 506 for core in self.ttCores[:upTo][::-1]: 507 projectedData = np.tensordot(core, projectedData, axes=(-1, 0)) 508 return projectedData 509 510 def updateRanks(self) -> None: 511 """ 512 Updates the ranks of the `ttObject` after incremental updates. 513 """ 514 self.ttRanks = [1] 515 for core in self.ttCores: 516 self.ttRanks.append(core.shape[-1]) 517 return None 518 519 def computeRelError(self, newTensor: np.array, useExact=True) -> np.array: 520 """ 521 Computes relative error by projecting data onto TT-cores. 522 523 This function computes the error induced by projecting `data` onto the TT-cores 524 of `ttObject`. The last index of `newTensor` is assumed to be the number of 525 individual observations. 526 527 Note 528 ---- 529 - In order to compute the projection onto the TT-cores, the dimensions of `data` 530 should match that of `ttObject`. 531 - If a single observation will be passed as `newTensor`, an additional 532 index/dimension should be introduced either through reshaping or [:,None] 533 534 Parameters 535 ---------- 536 newTensor:obj:`np.array` 537 Tensor for which the projection error is computed 538 useExact:`bool` 539 Boolean flag determining whether a projection error will be 540 returned for the entire batch or each observation in the batch. 541 542 Returns 543 ------- 544 relError:obj:`np.array` 545 Array of relative errors 546 """ 547 elementwiseNorm = np.linalg.norm(newTensor, axis=0) 548 for _ in range(len(newTensor.shape) - 2): 549 elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0) 550 projectedData = self.projectTensor(newTensor) 551 reconstructedData = self.reconstruct(projectedData).reshape(newTensor.shape) 552 difference = newTensor - reconstructedData 553 differenceNorm = np.linalg.norm(difference, axis=0) 554 for _ in range(len(difference.shape) - 2): 555 differenceNorm = np.linalg.norm(differenceNorm, axis=0) 556 557 if useExact: 558 relError = np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm) 559 else: 560 relError = differenceNorm / elementwiseNorm 561 return relError 562 563 def computeRecError(self, data: np.array, start=None, finish=None, useExact=True) -> None: 564 """ 565 Function to compute relative error by reconstructing data from slices 566 of TT-cores. 567 568 Parameters 569 ---------- 570 data:obj:`np.array` 571 Tensor for which the reconstruction error is computed 572 start:`int` 573 Index for the start of the batch in the compressed last core 574 finish:`int` 575 Index for the end of the batch in the compressed last core 576 useExact:`bool` 577 Boolean flag determining whether a reconstruction error will be 578 returned for the entire batch or each observation in the batch. 579 580 Returns 581 ------- 582 recError:obj:`float` or `np.array` 583 Reconstruction error of the data from cores. Returns an array 584 of reconstruction errors for each observation in the batch if 585 `useExact` is `False`. 586 """ 587 if start is None: 588 start = self.ttCores[-1].shape[1]-1 589 if finish is None: 590 finish = start+1 591 rec=self.reconstruct(self.ttCores[-1][:,start:finish,:]).reshape(data.shape) 592 elementwiseNorm=np.linalg.norm(data,axis=0) 593 for idx in range(len(data.shape)-2): 594 elementwiseNorm=np.linalg.norm(elementwiseNorm,axis=0) 595 difference=data-rec 596 differenceNorm=np.linalg.norm(difference,axis=0) 597 for idx in range(len(difference.shape)-2): 598 differenceNorm=np.linalg.norm(differenceNorm,axis=0) 599 600 if useExact: 601 recError=np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm) 602 else: 603 recError=differenceNorm/elementwiseNorm 604 605 return recError 606 607 def ttDecomp(self, norm=None, dtype=np.float32) -> "ttObject.ttCores": 608 """ 609 Computes TT-decomposition of a multidimensional array using `TTSVD`_ algorithm. 610 611 Currently only supports `ttsvd` as method. In the future additional formats may 612 be covered. 613 614 Parameters 615 ---------- 616 norm:obj:`float`, optional 617 Norm of the tensor to be compressed 618 dtype:obj:`type`, optional 619 Desired data type for the compression. Intended to allow lower precision 620 if needed. 621 622 Raises 623 ------ 624 ValueError 625 When `method` is not one of the admissible methods. 626 627 628 The following attributes are modified as a result of this function: 629 ------- 630 - `ttObject.ttCores` 631 - `ttObject.ttRanks` 632 - `ttObject.compressionRatio` 633 634 .. _TTSVD: 635 https://epubs.siam.org/doi/epdf/10.1137/090752286 636 """ 637 if norm is None: 638 norm = np.linalg.norm(self.originalData) 639 if self.method == "ttsvd": 640 startTime = time.time() 641 self.ttRanks, self.ttCores = ttsvd( 642 self.originalData, norm, self.ttEpsilon, dtype=dtype 643 ) 644 self.compressionTime = time.time() - startTime 645 self.nCores = len(self.ttCores) 646 self.nElements = 0 647 for cores in self.ttCores: 648 self.nElements += np.prod(cores.shape) 649 if not self.keepOriginal: 650 self.originalData = None 651 return None 652 else: 653 raise ValueError("Method unknown. Please select a valid method!") 654 655 def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None: 656 """ 657 `TT-ICE`_ algorithmn without any heuristic upgrades. 658 659 Given a set of TT-cores, this function provides incremental updates 660 to the TT-cores to approximate `newTensor` within a relative error 661 defined in `epsilon` 662 663 Note 664 ---- 665 This algorithm/function relies on the fact that TT-cores are columnwise 666 orthonormal in the mode-2 unfolding. 667 668 Parameters 669 ---------- 670 newTensor:obj:`np.array` 671 New/streamed tensor that will be used to expand the orthonormal bases defined 672 in TT-cores 673 epsilon:obj:`float`, optional 674 Relative error upper bound for approximating `newTensor` after incremental 675 updates. If not defined, `ttObject.ttEpsilon` is used. 676 tenNorm:obj:`float`, optional 677 Norm of `newTensor` 678 679 Notes 680 ------- 681 **The following attributes are modified as a result of this function:** 682 - `ttObject.ttCores` 683 - `ttObject.ttRanks` 684 - `ttObject.compressionRatio` 685 .. _TT-ICE: 686 https://arxiv.org/abs/2211.12487 687 """ 688 if tenNorm is None: 689 tenNorm = np.linalg.norm(newTensor) 690 if epsilon is None: 691 epsilon = self.ttEpsilon 692 newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 693 newTensorSize = len(newTensor.shape) - 1 694 newTensor = newTensor.reshape(self.reshapedShape[0], -1) 695 Ui = self.ttCores[0].reshape(self.reshapedShape[0], -1) 696 Ri = newTensor - Ui @ (Ui.T @ newTensor) 697 for coreIdx in range(0, len(self.ttCores) - 2): 698 URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon) 699 self.ttCores[coreIdx] = np.hstack((Ui, URi)).reshape( 700 self.ttCores[coreIdx].shape[0], self.reshapedShape[coreIdx], -1 701 ) 702 self.ttCores[coreIdx + 1] = np.concatenate( 703 ( 704 self.ttCores[coreIdx + 1], 705 np.zeros( 706 ( 707 URi.shape[-1], 708 self.reshapedShape[coreIdx + 1], 709 self.ttRanks[coreIdx + 2], 710 ) 711 ), 712 ), 713 axis=0, 714 ) 715 Ui = self.ttCores[coreIdx].reshape( 716 self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1 717 ) 718 newTensor = (Ui.T @ newTensor).reshape( 719 np.prod(self.ttCores[coreIdx + 1].shape[:-1]), -1 720 ) 721 Ui = self.ttCores[coreIdx + 1].reshape( 722 self.ttCores[coreIdx].shape[-1] * self.reshapedShape[coreIdx + 1], -1 723 ) 724 Ri = newTensor - Ui @ (Ui.T @ newTensor) 725 coreIdx = len(self.ttCores) - 2 726 URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon) 727 self.ttCores[coreIdx] = np.hstack((Ui, URi)) 728 self.ttCores[coreIdx + 1] = np.concatenate( 729 ( 730 self.ttCores[coreIdx + 1], 731 np.zeros( 732 ( 733 URi.shape[-1], 734 self.ttCores[coreIdx + 1].shape[1], 735 self.ttRanks[coreIdx + 2], 736 ) 737 ), 738 ), 739 axis=0, 740 ) 741 newTensor = self.ttCores[coreIdx].T @ newTensor 742 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 743 self.ttCores[coreIdx - 1].shape[-1], self.reshapedShape[coreIdx], -1 744 ) 745 coreIdx += 1 746 Ui = self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0], -1) 747 self.ttCores[coreIdx] = np.hstack((Ui, newTensor)).reshape( 748 self.ttCores[coreIdx].shape[0], -1, 1 749 ) 750 self.updateRanks() 751 return None 752 753 def ttICEstar( 754 self, 755 newTensor: np.array, 756 epsilon: float = None, 757 tenNorm: float = None, 758 elementwiseNorm: np.array = None, 759 elementwiseEpsilon: np.array = None, 760 heuristicsToUse: list = ["skip", "subselect", "occupancy"], 761 occupancyThreshold: float = 0.8, 762 simpleEpsilonUpdate: bool = False, 763 surrogateThreshold=True, 764 sampleEpsilon=False, 765 samplePercent=0.1 766 ) -> None: 767 """ 768 `TT-ICE*`_ algorithmn with heuristic performance upgrades. 769 770 Given a set of TT-cores, this function provides incremental updates 771 to the TT-cores to approximate `newTensor` within a relative error 772 defined in `epsilon`. 773 774 Note 775 ---- 776 This algorithm/function relies on the fact that TT-cores are columnwise 777 orthonormal in the mode-2 unfolding. 778 779 Parameters 780 ---------- 781 newTensor:obj:`np.array` 782 New/streamed tensor that will be used to expand the orthonormal bases defined 783 in TT-cores 784 epsilon:obj:`float`, optional 785 Relative error upper bound for approximating `newTensor` after incremental 786 updates. If not defined, `ttObject.ttEpsilon` is used. 787 tenNorm:obj:`float`, optional 788 Norm of `newTensor`. 789 elementwiseNorm:obj:`np.array`, optional 790 Individual norms of the observations in `newTensor`. 791 elementwiseEpsilon:obj:`np.array`, optional 792 Individual relative projection errors of the observations in `newTensor`. 793 heuristicsToUse:obj:`list`, optional 794 List of heuristics to use while updating TT-cores. Currently only accepts 795 `'skip'`, `'subselect'`, and `'occupancy'`. 796 occupancyThreshold:obj:`float`, optional 797 Threshold determining whether to skip updating a single core or not. Not used 798 if `'occupancy'` is not in `heuristicsToUse` 799 simpleEpsilonUpdate:obj:`bool`, optional 800 Uses the simple epsilon update equation. *Warning*: this relies on the 801 assumption that all observations in `newTensor` have similar norms. 802 803 Notes 804 ------- 805 **The following attributes are modified as a result of this function:** 806 - `ttObject.ttCores` 807 - `ttObject.ttRanks` 808 - `ttObject.compressionRatio` 809 .. _TT-ICE*: 810 https://arxiv.org/abs/2211.12487 811 """ 812 if epsilon is None: 813 epsilon = self.ttEpsilon 814 if ("subselect" in heuristicsToUse) and (newTensor.shape[-1] == 1): 815 warning( 816 "The streamed tensor has only 1 observation in it. \ 817 Subselect heuristic will not be useful!!" 818 ) 819 newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 820 updEpsilon = epsilon 821 newTensorSize = len(newTensor.shape) - 1 822 823 if elementwiseEpsilon is None: 824 elementwiseEpsilon = self.computeRelError(newTensor) 825 if "skip" in heuristicsToUse: 826 if surrogateThreshold: 827 if sampleEpsilon: 828 sampleSize=int(newTensor.shape[-1]*samplePercent) 829 skipCriterion=np.mean(np.random.choice(elementwiseEpsilon,size=sampleSize,replace=False)) 830 else: 831 skipCriterion=np.mean(elementwiseEpsilon) 832 if skipCriterion<= epsilon: 833 newTensor = self.projectTensor(newTensor) 834 self.ttCores[-1] = np.hstack( 835 (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor) 836 ).reshape(self.ttRanks[-2], -1, 1) 837 return None 838 else: 839 errorNorm=elementwiseEpsilon*elementwiseNorm 840 errorNorm=np.sqrt(np.sum(errorNorm**2))/np.linalg.norm(elementwiseNorm) 841 if errorNorm <= epsilon: 842 newTensor = self.projectTensor(newTensor) 843 self.ttCores[-1] = np.hstack( 844 (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor) 845 ).reshape(self.ttRanks[-2], -1, 1) 846 return None 847 if tenNorm is None and elementwiseNorm is None: 848 tenNorm = np.linalg.norm(newTensor) 849 elif tenNorm is None: 850 tenNorm = np.linalg.norm(elementwiseNorm) 851 852 select = [True] * newTensor.shape[-1] 853 discard = [False] * newTensor.shape[-1] 854 if "subselect" in heuristicsToUse: 855 select = elementwiseEpsilon > epsilon 856 discard = elementwiseEpsilon <= epsilon 857 if simpleEpsilonUpdate: 858 updEpsilon = ( 859 epsilon * newTensor.shape[-1] 860 - np.mean(elementwiseEpsilon[discard]) * discard.sum() 861 ) / (select.sum()) 862 else: 863 if elementwiseNorm is None: 864 elementwiseNorm = np.linalg.norm(newTensor, axis=0) 865 for _ in range(len(self.ttCores) - 1): 866 elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0) 867 allowedError = (self.ttEpsilon * np.linalg.norm(elementwiseNorm)) ** 2 868 discardedError = np.sum( 869 (elementwiseEpsilon[discard] * elementwiseNorm[discard]) ** 2 870 ) 871 updEpsilon = np.sqrt( 872 (allowedError - discardedError) 873 / (np.linalg.norm(elementwiseNorm[select]) ** 2) 874 ) 875 self.reshapedShape[-1] = np.array( 876 select 877 ).sum() # a little trick for ease of coding 878 879 indexString = "[" 880 for _ in range(len(self.reshapedShape)): 881 # this heuristic assumes that the last dimension is for observations 882 indexString += ":," 883 selectString = indexString + "select]" 884 selected = eval("newTensor" + selectString) 885 886 selected = selected.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 887 for coreIdx in range(0, len(self.ttCores) - 1): 888 selected = selected.reshape(np.prod(self.ttCores[coreIdx].shape[:-1]), -1) 889 if ("occupancy" in heuristicsToUse) and ( 890 self.coreOccupancy[coreIdx] >= occupancyThreshold 891 ): 892 pass 893 else: 894 Ui = self.ttCores[coreIdx].reshape( 895 self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1 896 ) 897 Ri = selected - Ui @ (Ui.T @ selected) 898 if (elementwiseNorm is None) or ("subselect" not in heuristicsToUse): 899 URi, _, _ = deltaSVD( 900 Ri, np.linalg.norm(selected), newTensorSize, updEpsilon 901 ) 902 else: 903 URi, _, _ = deltaSVD( 904 Ri, 905 np.linalg.norm(elementwiseNorm[select]), 906 newTensorSize, 907 updEpsilon, 908 ) 909 self.ttCores[coreIdx] = np.hstack((Ui, URi)) 910 self.ttCores[coreIdx + 1] = np.concatenate( 911 ( 912 self.ttCores[coreIdx + 1], 913 np.zeros( 914 ( 915 URi.shape[-1], 916 self.ttCores[coreIdx + 1].shape[1], 917 self.ttRanks[coreIdx + 2], 918 ) 919 ), 920 ), 921 axis=0, 922 ) 923 924 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 925 np.prod(self.ttCores[coreIdx].shape[:-1]), -1 926 ) 927 # #project onto existing core and reshape for next core 928 selected = (self.ttCores[coreIdx].T @ selected).reshape( 929 self.ttCores[coreIdx + 1].shape[0] * self.reshapedShape[coreIdx + 1], -1 930 ) 931 # fold back the previous core 932 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 933 -1, self.reshapedShape[coreIdx], self.ttCores[coreIdx].shape[-1] 934 ) 935 # self.ttCores[coreIdx]=self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0],self.reshapedShape[coreIdx],-1) 936 # #fold back the previous core 937 self.updateRanks() 938 coreIdx += 1 939 # coreIdx=len(self.ttCores), i.e working on the last core 940 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 941 self.ttCores[coreIdx].shape[0], -1 942 ) 943 self.ttCores[coreIdx] = np.hstack( 944 (self.ttCores[coreIdx], self.projectTensor(newTensor)) 945 ).reshape(self.ttCores[coreIdx].shape[0], -1, 1) 946 return None 947 948 def ttRound(self, norm, epsilon=0) -> None: 949 """ 950 Reorthogonalizes and recompresses TT-cores as per `Oseledets 2011`_. 951 952 This is the TT-rounding algorithm described in `TTSVD`_ paper. This 953 function first reorthogonalizes the cores from last to first using 954 QR decomposition and then recompresses the reorthogonalized TT-cores 955 from first to last using `TTSVD`_ algorithm. 956 957 Parameters 958 ---------- 959 norm:obj:`float` 960 Norm of the TT-approximation. If there is no information on the 961 uncompressed tensor, `ttNorm` function can be used. 962 epsilon:obj:`float`, optional 963 Relative error upper bound for the recompression step. Note that 964 this relative error upper bound is not with respect to the original 965 tensor but to the `compressed` tensor. Set to 0 by default. 966 967 Notes 968 ----- 969 **The following attributes are modified as a result of this function:** 970 - `ttObject.ttCores` 971 - `ttObject.ttRanks` 972 - `ttObject.compressionRatio` 973 974 .. _Oseledets 2011: 975 https://epubs.siam.org/doi/epdf/10.1137/090752286 976 TTSVD: 977 https://epubs.siam.org/doi/epdf/10.1137/090752286 978 """ 979 d = [core.shape[1] for core in self.ttCores] 980 for coreIdx in np.arange(len(self.ttCores))[::-1]: 981 currCore = self.ttCores[coreIdx] 982 # Compute mode 1 unfolding of the tt-core 983 currCore = currCore.reshape(currCore.shape[0], -1) 984 # Using the transpose results in a row orthogonal Q matrix !!! 985 q, r = np.linalg.qr(currCore.T) 986 q, r = q.T, r.T 987 self.ttCores[coreIdx] = q 988 # Contract previous tt-core and R matrix 989 self.ttCores[coreIdx - 1] = np.tensordot( 990 self.ttCores[coreIdx - 1], r, axes=(-1, 0) 991 ) 992 if coreIdx == 1: 993 break 994 # pass #TODO: write the case for the last core here. 995 # Compression of the orthogonalized representation # 996 ranks = [1] 997 for coreIdx in range(len(self.ttCores) - 1): 998 core = self.ttCores[coreIdx] 999 core = core.reshape(np.prod(core.shape[:-1]), -1) 1000 self.ttCores[coreIdx], sigma, v = deltaSVD( 1001 core, norm, len(self.ttCores), epsilon 1002 ) 1003 ranks.append(self.ttCores[coreIdx].shape[-1]) 1004 # fold matrices into tt-cores 1005 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 1006 ranks[coreIdx], d[coreIdx], -1 1007 ) 1008 self.ttCores[coreIdx + 1] = ( 1009 (np.diag(sigma) @ v) @ self.ttCores[coreIdx + 1] 1010 ).reshape(ranks[-1] * d[coreIdx + 1], -1) 1011 self.ttCores[-1] = self.ttCores[-1].reshape(-1, d[-1], 1) 1012 self.ttRanks = [1] 1013 for core in self.ttCores: 1014 self.ttRanks.append(core.shape[-1]) 1015 return None 1016 1017 @staticmethod 1018 def ittd(tt1, tt2, rounding=True, epsilon=0) -> "ttObject": 1019 """ 1020 Alternative incremental tensor decomposition algorithm used to benchmark. 1021 1022 Note 1023 ---- 1024 This method is empirically shown to be inferior to `TT-ICE`_ and 1025 `TT-ICE*`_. 1026 1027 Parameters 1028 ---------- 1029 tt1:obj:`ttObject` 1030 Tensor in TT-format. 1031 tt2:obj:`ttObject` 1032 Tensor in TT-format. 1033 rounding:obj:`bool`, optional 1034 Determines if `ttRounding` will be done to the incremented TT-core. 1035 Note that for unbounded streams, rounding is essential. Otherwise 1036 TT-ranks will grow unboundedly. 1037 epsilon:obj:`float`, optional 1038 Relative error upper bound for the recompression step. Note that 1039 this relative error upper bound is not with respect to the original 1040 tensor but to the `compressed` tensor. Set to 0 by default. Not used 1041 if `rounding` is set to `False`. 1042 1043 Notes 1044 ----- 1045 **The following attributes are modified as a result of this function:** 1046 - `ttObject.ttCores` 1047 - `ttObject.ttRanks` 1048 - `ttObject.compressionRatio` 1049 1050 .. _TT-ICE: 1051 https://arxiv.org/abs/2211.12487 1052 _TT-ICE*: 1053 https://arxiv.org/abs/2211.12487 1054 """ 1055 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 1056 if isinstance(tt1, list): 1057 tt1 = ttObject(tt1) 1058 if isinstance(tt2, list): 1059 tt2 = ttObject(tt2) 1060 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 1061 raise AttributeError("One of the passed objects is not in TT-format!") 1062 tt1.ttCores[-1] = tt1.ttCores[-1].squeeze(-1) 1063 tt2.ttCores[-1] = tt2.ttCores[-1].squeeze(-1) 1064 rank1, numObs1 = tt1.ttCores[-1].shape 1065 rank2, numObs2 = tt2.ttCores[-1].shape 1066 tt1.ttCores[-1] = np.concatenate( 1067 (tt1.ttCores[-1], np.zeros((rank1, numObs2))), axis=1 1068 ) 1069 tt2.ttCores[-1] = np.concatenate( 1070 (np.zeros((rank2, numObs1)), tt2.ttCores[-1]), axis=1 1071 ) 1072 # Addition in TT-format # 1073 for coreIdx in range(len(tt1.ttCores)): 1074 if coreIdx == 0: 1075 tt1.ttCores[coreIdx] = np.concatenate( 1076 (tt1.ttCores[coreIdx].squeeze(0), tt2.ttCores[coreIdx].squeeze(0)), 1077 axis=1, 1078 )[None, :, :] 1079 elif coreIdx == len(tt1.ttCores) - 1: 1080 tt1.ttCores[coreIdx] = np.concatenate( 1081 (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0 1082 )[:, :, None] 1083 else: 1084 s11, s12, s13 = tt1.ttCores[coreIdx].shape 1085 s21, s22, s23 = tt2.ttCores[coreIdx].shape 1086 tt1.ttCores[coreIdx] = np.concatenate( 1087 (tt1.ttCores[coreIdx], np.zeros((s11, s12, s23))), axis=2 1088 ) 1089 tt2.ttCores[coreIdx] = np.concatenate( 1090 (np.zeros((s21, s22, s13)), tt2.ttCores[coreIdx]), axis=2 1091 ) 1092 tt1.ttCores[coreIdx] = np.concatenate( 1093 (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0 1094 ) 1095 tt1.ttRanks = [1] 1096 for core in tt1.ttCores: 1097 tt1.ttRanks.append(core.shape[-1]) 1098 if rounding: 1099 tenNorm = ttObject.ttNorm(tt1) 1100 tt1.ttRound(tenNorm, epsilon=epsilon) 1101 return tt1 1102 1103 @staticmethod 1104 def ttfoa6d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None): 1105 nDims=len(data.shape) 1106 dims=list(data.shape) 1107 coreSwap=False 1108 if forgettingFactor is None: 1109 forgettingFactor=0.7 1110 1111 if len(ttRanks) != nDims-1: 1112 raise ValueError("Number of dimensions do not match with number of TT-ranks") 1113 1114 if ttCores is None: 1115 # Initialize TT-cores randomly if no core is given. 1116 ttCores=[] 1117 ttCores.append(np.random.randn(1,dims[0],ttRanks[0])) 1118 for dimIdx in range(1,nDims-1): 1119 ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx])) 1120 ttCores.append(np.empty((ttRanks[-1],1))) 1121 coreSwap=True 1122 # else: 1123 ttCores_old=ttCores.copy() 1124 if S is None: 1125 # Initialize S matrices as identity matrices if no S is given from previous step. 1126 S=[] 1127 S.append(100*np.eye(ttRanks[0])) 1128 for dimIdx in range(1,nDims-1): 1129 S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx])) 1130 1131 # Update the last core G6 1132 1133 Ht_1=coreContraction(ttCores_old[:-1]) 1134 # Ht_1=Ht_1.reshape(-1,ttRanks[-1]) 1135 Ht_1=mode_n_unfolding(Ht_1,5).T #this might be 5 1136 g6=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F') 1137 if coreSwap: 1138 ttCores[-1]=g6 1139 else: 1140 ttCores[-1]=np.hstack((ttCores[-1],g6)) 1141 recData=(Ht_1@g6).reshape(dims,order='F') 1142 delta=data-recData 1143 1144 # Update G1 1145 deltaunfold=mode_n_unfolding(delta,0) 1146 Bk=coreContraction(ttCores_old[1:-1]+[g6]) 1147 Wk=mode_n_unfolding(Bk,0) 1148 S[0]=forgettingFactor*S[0]+Wk@Wk.T 1149 # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0] 1150 Vk=np.linalg.pinv(S[0])@Wk 1151 ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T 1152 1153 # Update G2 1154 deltaunfold=mode_n_unfolding(delta,1) 1155 Ak=mode_n_unfolding(ttCores_old[0],2)#.T 1156 Bk=coreContraction(ttCores_old[2:-1]+[g6]) 1157 Bk=mode_n_unfolding(Bk,0) 1158 Wk=np.kron(Bk,Ak) 1159 S[1]=forgettingFactor*S[1]+Wk@Wk.T 1160 # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0] 1161 Vk=np.linalg.pinv(S[1])@Wk 1162 ttCores[1]=mode_n_unfolding(ttCores_old[1],1) 1163 ttCores[1]=ttCores[1]+deltaunfold@Vk.T 1164 ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2) 1165 1166 # Update G3 1167 deltaunfold=mode_n_unfolding(delta,2) 1168 Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2) 1169 Bk=coreContraction(ttCores_old[3:-1]+[g6]) #insert a mode 0 unfolding here if necessary 1170 Bk=mode_n_unfolding(Bk,0) 1171 Wk=np.kron(Bk,Ak) 1172 S[2]=forgettingFactor*S[2]+Wk@Wk.T 1173 # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0] 1174 Vk=np.linalg.pinv(S[2])@Wk 1175 ttCores[2]=mode_n_unfolding(ttCores_old[2],1) 1176 ttCores[2]=ttCores[2]+deltaunfold@Vk.T 1177 ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2) 1178 1179 # Update G4 1180 deltaunfold=mode_n_unfolding(delta,3) 1181 Ak=mode_n_unfolding(coreContraction(ttCores_old[:3]),3) 1182 Bk=coreContraction(ttCores_old[4:-1]+[g6]) #insert a mode 0 unfolding here if necessary 1183 Wk=np.kron(Bk,Ak) 1184 S[3]=forgettingFactor=S[3]+Wk@Wk.T 1185 # Vk=np.linalg.lstsq(S[3],Wk,rcond=None)[0] 1186 Vk=np.linalg.pinv(S[3])@Wk 1187 ttCores[3]=mode_n_unfolding(ttCores_old[3],1) 1188 ttCores[3]=ttCores[3]+deltaunfold@Vk.T 1189 ttCores[3]=ttCores[3].reshape(dims[3],ttRanks[2],ttRanks[3],order='F').transpose(1,0,2) 1190 1191 # Update G5 1192 deltaunfold=mode_n_unfolding(delta,4) 1193 Ak=mode_n_unfolding(coreContraction(ttCores_old[:4]),4) 1194 # Bk=coreContraction(g6) #insert a mode 0 unfolding here if necessary 1195 Wk=np.kron(g6,Ak) 1196 S[4]=forgettingFactor=S[4]+Wk@Wk.T 1197 # Vk=np.linalg.lstsq(S[4],Wk,rcond=None)[0] 1198 Vk=np.linalg.pinv(S[4])@Wk 1199 ttCores[4]=mode_n_unfolding(ttCores_old[4],1) 1200 ttCores[4]=ttCores[4]+deltaunfold@Vk.T 1201 ttCores[4]=ttCores[4].reshape(dims[4],ttRanks[3],ttRanks[4],order='F').transpose(1,0,2) 1202 1203 return S,ttCores 1204 1205 @staticmethod 1206 def ttfoa4d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None): 1207 nDims=len(data.shape) 1208 dims=list(data.shape) 1209 coreSwap=False 1210 if forgettingFactor is None: 1211 forgettingFactor=0.7 1212 1213 if len(ttRanks) != nDims-1: 1214 raise ValueError("Number of dimensions do not match with number of TT-ranks") 1215 1216 if ttCores is None: 1217 # Initialize TT-cores randomly if no core is given. 1218 ttCores=[] 1219 ttCores.append(np.random.randn(1,dims[0],ttRanks[0])) 1220 for dimIdx in range(1,nDims-1): 1221 ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx])) 1222 ttCores.append(np.empty((ttRanks[-1],1))) 1223 coreSwap=True 1224 # else: 1225 ttCores_old=ttCores.copy() 1226 if S is None: 1227 # Initialize S matrices as identity matrices if no S is given from previous step. 1228 S=[] 1229 S.append(100*np.eye(ttRanks[0])) 1230 for dimIdx in range(1,nDims-1): 1231 S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx])) 1232 1233 Ht_1=coreContraction(ttCores_old[:-1]) 1234 # Ht_1=Ht_1.reshape(-1,ttRanks[-1]) 1235 Ht_1=mode_n_unfolding(Ht_1,3).T #this might be 4 1236 g4=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F') 1237 if coreSwap: 1238 ttCores[-1]=g4 1239 else: 1240 ttCores[-1]=np.hstack((ttCores[-1],g4)) 1241 recData=(Ht_1@g4).reshape(dims,order='F') 1242 delta=data-recData 1243 1244 # Update G1 1245 deltaunfold=mode_n_unfolding(delta,0) 1246 # Bk=coreContraction(ttCores_old[1:-1]+[g4]) 1247 Bk=np.tensordot(np.tensordot(ttCores_old[1],ttCores_old[2],axes=[-1,0]),g4,axes=[-1,0]) 1248 Wk=mode_n_unfolding(Bk,0) 1249 S[0]=forgettingFactor*S[0]+Wk@Wk.T 1250 # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0] 1251 try: 1252 Vk=np.linalg.solve(S[0],Wk) 1253 except np.linalg.LinAlgError: 1254 print('Singular S[0] matrix, using Moore-Penrose inverse') 1255 Vk=np.linalg.pinv(S[0])@Wk 1256 ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T 1257 1258 # Update G2 1259 deltaunfold=mode_n_unfolding(delta,1) 1260 Ak=mode_n_unfolding(ttCores_old[0],2)#.T 1261 Bk=coreContraction(ttCores_old[2:-1]+[g4]) 1262 Bk=mode_n_unfolding(Bk,0) 1263 Wk=np.kron(Bk,Ak) 1264 S[1]=forgettingFactor*S[1]+Wk@Wk.T 1265 # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0] 1266 try: 1267 Vk=np.linalg.solve(S[1],Wk) 1268 except np.linalg.LinAlgError: 1269 print('Singular S[1] matrix, using Moore-Penrose inverse') 1270 Vk=np.linalg.pinv(S[1])@Wk 1271 ttCores[1]=mode_n_unfolding(ttCores_old[1],1) 1272 ttCores[1]=ttCores[1]+deltaunfold@Vk.T 1273 ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2) 1274 1275 # Update G3 1276 deltaunfold=mode_n_unfolding(delta,2) 1277 Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2) 1278 # Bk=coreContraction(ttCores_old[3:-1]+[g4]) #insert a mode 0 unfolding here if necessary 1279 # Bk=mode_n_unfolding(Bk,0) 1280 Wk=np.kron(g4,Ak) 1281 S[2]=forgettingFactor*S[2]+Wk@Wk.T 1282 # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0] 1283 try: 1284 Vk=np.linalg.solve(S[2],Wk) 1285 except np.linalg.LinAlgError: 1286 print('Singular S[2] matrix, using Moore-Penrose inverse') 1287 Vk=np.linalg.pinv(S[2])@Wk 1288 ttCores[2]=mode_n_unfolding(ttCores_old[2],1) 1289 ttCores[2]=ttCores[2]+deltaunfold@Vk.T 1290 ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2) 1291 1292 return S,ttCores
Python object for tensors in Tensor-Train format.
This object computes the TT-decomposition of multidimensional arrays using TTSVD, TT-ICE, and TT-ICE*. It furthermore contains an inferior method, ITTD, for benchmarking purposes.
This object handles various operations in TT-format such as dot product and norm. Also provides supporting operations for uncompressed multidimensional arrays such as reshaping and transpose. Once the tensor train approximation for a multidimensional array is computed, you can compute projection of appropriate arrays onto the TT-cores, reconstruct projected or compressed tensors and compute projection/compression accuracy.
You can also save/load tensors as .ttc
or .txt
files.
Attributes
- inputType (type): Type of the input data. This determines how the object is initialized.
- originalData:: Original multidimensional input array. Generally will not be stored after computing an initial set of TT-cores.
- method (
str
): Method of computing the initial set of TT-cores. Currently only accepts'ttsvd'
as input - ttEpsilon (
float
): Desired relative error upper bound. - ttCores (
list
ofnumpy.array
): Cores of the TT-decomposition. Stored as a list of numpy arrays. - ttRanks (
list
ofint
): TT-ranks of the decomposition. Stored as a list of integers. - nCores (
int
): Number of TT-cores in the decomposition. - nElements (
int
): Number of entries present in the currentttObject
. - originalShape (
tuple
orlist
): Original shape of the multidimensional array. - reshapedShape (
tuple
orlist
): Shape of the multidimensional array after reshaping. Note that this attribute is only meaningful before computing a TT-decomposition - indexOrder (
list
ofint
): Keeps track of original indices in case of transposing the input array.
77 def __init__( 78 self, 79 data, 80 epsilon: float = None, 81 keepData: bool = False, 82 samplesAlongLastDimension: bool = True, 83 method: str = "ttsvd", 84 ) -> None: 85 """ 86 Initializes the ttObject. 87 88 Parameters 89 ---------- 90 data: :obj:`numpy.array` or :obj:`list` 91 Main input to the ttObject. It can either be a multidimensional `numpy array` 92 or `list of numpy arrays`. 93 If list of numpy arrays are presented as input, the object will interpret it 94 as the TT-cores of an existing decomposition. 95 epsilon: :obj:`float`, optional 96 The relative error upper bound desired for approximation. Optional for cases 97 when `data` has type `list`. 98 keepData: :obj:`bool`, optional 99 Optional boolean variable to determine if the original array will be kept 100 after compression. Set to `False` by default. 101 samplesAlongLastDimension: :obj:`bool`, optional 102 Boolean variable to ensure if the samples are stacked along the last 103 dimension. Assumed to be `True` since it is one of the assumptions in TT-ICE. 104 method: :obj:`str`, optional 105 Determines the computation method for tensor train decomposition of 106 the multidimensional array presented as `data`. Set to `'ttsvd'` by default. 107 108 Currently the package only has support for ttsvd, additional support such as 109 `ttcross` might be included in the future. 110 111 """ 112 # self.ttRanks=ranks 113 self.ttCores = None 114 self.nCores = None 115 self.nElements = None 116 self.inputType = type(data) 117 self.method = method 118 self.keepOriginal = keepData 119 self.originalData = data 120 self.samplesAlongLastDimension = samplesAlongLastDimension 121 if self.inputType is np.memmap: 122 self.inputType = np.ndarray 123 124 if self.inputType == np.ndarray: 125 self.ttEpsilon = epsilon 126 self.originalShape = list(data.shape) 127 self.reshapedShape = self.originalShape 128 self.indexOrder = [idx for idx in range(len(self.originalShape))] 129 elif self.inputType == list: 130 self.nCores = len(data) 131 self.ttCores = data 132 self.reshapedShape = [core.shape[1] for core in self.ttCores] 133 else: 134 raise TypeError("Unknown input type!")
Initializes the ttObject.
Parameters
- data (
numpy.array
orlist
): Main input to the ttObject. It can either be a multidimensionalnumpy array
orlist of numpy arrays
. If list of numpy arrays are presented as input, the object will interpret it as the TT-cores of an existing decomposition. - epsilon (
float
, optional): The relative error upper bound desired for approximation. Optional for cases whendata
has typelist
. - keepData (
bool
, optional): Optional boolean variable to determine if the original array will be kept after compression. Set toFalse
by default. - samplesAlongLastDimension (
bool
, optional): Boolean variable to ensure if the samples are stacked along the last dimension. Assumed to beTrue
since it is one of the assumptions in TT-ICE. method (
str
, optional): Determines the computation method for tensor train decomposition of the multidimensional array presented asdata
. Set to'ttsvd'
by default.Currently the package only has support for ttsvd, additional support such as
ttcross
might be included in the future.
136 @property 137 def coreOccupancy(self) -> None: 138 """ 139 :obj:`list` of :obj:`float`: A metric showing the *relative rank* of each 140 TT-core. This metric is used for a heuristic enhancement tool in `TT-ICE*` 141 algorithm 142 """ 143 try: 144 return [ 145 core.shape[-1] / np.prod(core.shape[:-1]) for core in self.ttCores[:-1] 146 ] 147 except ValueError: 148 warnings.warn( 149 "No TT cores exist, maybe forgot calling object.ttDecomp?", Warning 150 ) 151 return None
list
of float
: A metric showing the relative rank of each
TT-core. This metric is used for a heuristic enhancement tool in TT-ICE*
algorithm
153 @property 154 def compressionRatio(self) -> float: 155 """ 156 :obj:`float`: A metric showing how much compression with respect to the 157 original multidimensional array is achieved. 158 """ 159 originalNumEl = 1 160 compressedNumEl = 0 161 for core in self.ttCores: 162 originalNumEl *= core.shape[1] 163 compressedNumEl += np.prod(core.shape) 164 return originalNumEl / compressedNumEl
float
: A metric showing how much compression with respect to the
original multidimensional array is achieved.
166 def changeShape(self, newShape: tuple or list) -> None: 167 """ 168 Function to change shape of input tensors and keeping track of the reshaping. 169 Reshapes `originalData` and saves the final shape in `reshapedShape` 170 171 Note 172 ---- 173 A simple `numpy.reshape` would be sufficient for this purpose but in order to keep 174 track of the shape changes the `reshapedShape` attribute also needs to be updated 175 accordingly. 176 177 Parameters 178 ---------- 179 newShape:obj:`tuple` or `list` 180 New shape of the tensor 181 182 Raises 183 ------ 184 warning 185 If an attempt is done to modify the shape after computing a TT-decomposition. 186 This is important since it will cause incompatibilities in other functions 187 regarding the shape of the uncompressed tensor. 188 189 """ 190 if self.ttCores is not None: 191 warning( 192 "Warning! You are reshaping the original data after computing a\ 193 TT-decomposition! We will proceed without reshaping self.originalData!!" 194 ) 195 return None 196 self.reshapedShape = newShape 197 self.originalData = np.reshape(self.originalData, self.reshapedShape) 198 self.reshapedShape = list(self.originalData.shape) 199 # Changed reshapedShape to a list for the trick in ttICEstar 200 if self.samplesAlongLastDimension: 201 self.singleDataShape = self.reshapedShape[:-1] 202 # This line assumes we keep the last index as the samples index and don't 203 # interfere with its shape
Function to change shape of input tensors and keeping track of the reshaping.
Reshapes originalData
and saves the final shape in reshapedShape
Note
A simple numpy.reshape
would be sufficient for this purpose but in order to keep
track of the shape changes the reshapedShape
attribute also needs to be updated
accordingly.
Parameters
- newShape
tuple
orlist
: New shape of the tensor
Raises
- warning: If an attempt is done to modify the shape after computing a TT-decomposition. This is important since it will cause incompatibilities in other functions regarding the shape of the uncompressed tensor.
205 def computeTranspose(self, newOrder: list) -> None: 206 """ 207 Transposes the axes of `originalData`. 208 209 Similar to `changeShape`, a simple 210 `numpy.transpose` would be sufficient for this purpose but in order to 211 keep track of the transposition order `indexOrder` attribute also needs 212 to be updated accordingly. 213 214 Parameters 215 ---------- 216 newOrder:obj:`list` 217 New order of the axes. 218 219 Raises 220 ------ 221 ValueError 222 When the number of transposition axes are not equal to the number of 223 dimensions of `originalData`. 224 """ 225 assert self.inputType == np.ndarray and self.ttCores is None 226 if len(newOrder) != len(self.indexOrder): 227 raise ValueError( 228 "size of newOrder and self.indexOrder does not match. \ 229 Maybe you forgot reshaping?" 230 ) 231 self.indexOrder = [self.indexOrder[idx] for idx in newOrder] 232 self.originalData = self.originalData.transpose(newOrder) 233 self.reshapedShape = list(self.originalData.shape)
Transposes the axes of originalData
.
Similar to changeShape
, a simple
numpy.transpose
would be sufficient for this purpose but in order to
keep track of the transposition order indexOrder
attribute also needs
to be updated accordingly.
Parameters
- newOrder
list
: New order of the axes.
Raises
- ValueError: When the number of transposition axes are not equal to the number of
dimensions of
originalData
.
235 def indexMap(self) -> None: 236 """ 237 Function to map the original indices to the reshaped indices. 238 Currently not implemented. 239 """ 240 self.A = 2
Function to map the original indices to the reshaped indices. Currently not implemented.
242 def primeReshaping(self) -> None: 243 """ 244 Function to reshape the first d dimensions to prime factors. 245 Currently not implemented 246 """ 247 self.A = 2
Function to reshape the first d dimensions to prime factors. Currently not implemented
249 def saveData( 250 self, fileName: str, directory="./", justCores=True, outputType="ttc" 251 ) -> None: 252 """ 253 Writes the computed TT-cores to an external file. 254 255 Parameters 256 ---------- 257 fileName:obj:`str` 258 259 directory:obj:`str` 260 Location to save files with respect to the present working directory. 261 justCores:obj:`bool` 262 Boolean variable to determine if `originalData` will be discarded 263 or not while saving. 264 outputType:obj:`str` 265 Type of the output file. `ttc` for pickled `ttObject`, `txt` for 266 individual text files for each TT-core. 267 """ 268 if justCores: 269 if outputType == "ttc": 270 with open(directory + fileName + ".ttc", "wb") as saveFile: 271 temp = ttObject(self.ttCores) 272 for attribute in vars(self): 273 if attribute != "originalData": 274 setattr(temp, attribute, eval(f"self.{attribute}")) 275 dump(temp, saveFile) 276 elif outputType == "txt": 277 for coreIdx, core in enumerate(self.ttCores): 278 np.savetxt( 279 directory + f"{fileName}_{coreIdx}.txt", 280 core.reshape(-1, core.shape[-1]), 281 header=f"{core.shape[0]} {core.shape[1]} {core.shape[2]}", 282 delimiter=" ", 283 ) 284 elif outputType == "npy": 285 for coreIdx, core in enumerate(self.ttCores): 286 np.save( 287 directory + f"{fileName}_{coreIdx}.npy", 288 core, 289 ) 290 else: 291 raise ValueError(f"Output type {outputType} is not supported!") 292 else: 293 if outputType == "txt": 294 raise ValueError( 295 ".txt type outputs are only supported for justCores=True!!" 296 ) 297 if self.method == "ttsvd": 298 with open(directory + fileName + ".ttc", "wb") as saveFile: 299 dump(self, saveFile) 300 else: 301 raise ValueError("Unknown Method!")
Writes the computed TT-cores to an external file.
Parameters
- fileName
str
- directory
str
: Location to save files with respect to the present working directory. - justCores
bool
: Boolean variable to determine iforiginalData
will be discarded or not while saving. - outputType
str
: Type of the output file.ttc
for pickledttObject
,txt
for individual text files for each TT-core.
303 @staticmethod 304 def loadData(fileName: str, numCores=None) -> "ttObject": 305 """ 306 Loads data from a `.ttc`, `.npy`, or `.txt` file 307 308 309 Static method to load TT-cores into a ttObject object. 310 Note 311 ---- 312 If data is stored in {coreFile}_{coreIdx}.txt format, 313 the input fileName should just be coreFile.txt 314 315 Parameters 316 ---------- 317 fileName:obj:`str` 318 Name of the file that will be loaded. 319 numCores:obj:`int` 320 Number of cores that the resulting `ttObject` will have 321 (Only required when input data format is `.txt`) 322 """ 323 fileExt = fileName.split(".")[-1] 324 if fileExt == "ttc": 325 with open(fileName, "rb") as f: 326 dataSetObject = load(f) 327 return dataSetObject 328 elif fileExt == "txt": 329 if numCores is None: 330 raise ValueError("Number of cores are not defined!!") 331 fileBody = fileName.split(".")[0] 332 coreList = [] 333 for coreIdx in range(numCores): 334 with open(f"{fileBody}_{coreIdx}.{fileExt}"): 335 coreShape = f.readline()[2:-1] 336 coreShape = [int(item) for item in coreShape.split(" ")] 337 coreList.append( 338 np.loadtxt(f"{fileBody}_{coreIdx}.{fileExt}").reshape[coreShape] 339 ) 340 return coreList 341 elif fileExt == "npy": 342 if numCores is None: 343 raise ValueError("Number of cores are not defined!!") 344 fileBody = fileName.split(".")[0] 345 coreList = [] 346 for coreIdx in range(numCores): 347 coreList.append( 348 np.load(f"{fileBody}_{coreIdx}.{fileExt}") 349 ) 350 return ttObject(coreList) 351 else: 352 raise ValueError(f"{fileExt} files are not supported!")
Loads data from a .ttc
, .npy
, or .txt
file
Static method to load TT-cores into a ttObject object.
Note
If data is stored in {coreFile}_{coreIdx}.txt format, the input fileName should just be coreFile.txt
Parameters
- fileName
str
: Name of the file that will be loaded. - numCores
int
: Number of cores that the resultingttObject
will have (Only required when input data format is.txt
)
354 @staticmethod 355 def ttDot(tt1, tt2) -> float(): 356 """ 357 Computes the dot product of two tensors in TT format. 358 359 Parameters 360 ---------- 361 tt1:obj:`ttObject` 362 Tensor in TT-format 363 tt2:obj:`ttObject` 364 Tensor in TT-format 365 366 Returns 367 ------- 368 prod:obj:`float` 369 Dot product of `tt1` and `tt2`. 370 371 Raises 372 ------ 373 AttributeError 374 When either one of the input parameters are not `ttObject`s. 375 376 """ 377 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 378 if isinstance(tt1, list): 379 tt1 = ttObject(tt1) 380 if isinstance(tt2, list): 381 tt2 = ttObject(tt2) 382 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 383 raise AttributeError("One of the passed objects is not in TT-format!") 384 prod = np.kron(tt1.ttCores[0][:, 0, :], tt2.ttCores[0][:, 0, :]) 385 for i1 in range(1, tt1.ttCores[0].shape[1]): 386 prod += np.kron(tt1.ttCores[0][:, i1, :], tt2.ttCores[0][:, i1, :]) 387 for coreIdx in range(1, len(tt1.ttCores)): 388 p = [] 389 for ik in range(tt1.ttCores[coreIdx].shape[1]): 390 p.append( 391 prod 392 @ ( 393 np.kron( 394 tt1.ttCores[coreIdx][:, ik, :], tt2.ttCores[coreIdx][:, ik, :] 395 ) 396 ) 397 ) 398 prod = np.sum(p, axis=0) 399 return prod.item()
401 @staticmethod 402 def ttNorm(tt1) -> float: 403 """ 404 Computes norm of a tensor in TT-format. 405 406 Parameters 407 ---------- 408 tt1:obj:`ttObject` 409 Tensor in TT-format 410 411 Returns 412 ------- 413 norm:obj:`float` 414 Norm of `tt1`. 415 416 Raises 417 ------ 418 AttributeError 419 When the input parameter is not a `ttObject`. 420 """ 421 if not isinstance(tt1, ttObject): 422 if isinstance(tt1, list): 423 tt1 = ttObject(tt1) 424 else: 425 raise AttributeError("Passed object is not in TT-format!") 426 try: 427 norm = ttObject.ttDot(tt1, tt1) 428 norm = np.sqrt(norm) 429 except MemoryError: 430 norm = np.linalg.norm(tt1.ttCores[-1]) 431 return norm
433 def projectTensor(self, newData: np.array, upTo=None) -> np.array: 434 """ 435 Projects tensors onto basis spanned by TT-cores. 436 437 Given a tensor with appropriate dimensions, this function leverages 438 the fact that TT-cores obtained through `TTSVD`_ and `TT-ICE`_ are 439 column-orthonormal in the mode-2 unfolding. 440 441 Note 442 ---- 443 This function will not yield correct results if the TT-cores are 444 not comprised of orthogonal vectors. 445 446 Parameters 447 ---------- 448 newData:obj:`np.aray` 449 Tensor to be projected 450 upTo:obj:`int`, optional 451 Index that the projection will be terminated. If an integer is 452 passed as this parameter, `newTensor` will be projected up to 453 (not including) the core that has index `upTo`. Assumes 1-based 454 indexing. 455 456 .. _TTSVD: 457 https://epubs.siam.org/doi/epdf/10.1137/090752286 458 _TT-ICE: 459 https://arxiv.org/abs/2211.12487 460 _TT-ICE*: 461 https://arxiv.org/abs/2211.12487 462 """ 463 for coreIdx, core in enumerate(self.ttCores): 464 if (coreIdx == len(self.ttCores) - 1) or coreIdx == upTo: 465 break 466 newData = ( 467 core.reshape(np.prod(core.shape[:2]), -1).transpose() 468 ) @ newData.reshape( 469 self.ttRanks[coreIdx] * self.ttCores[coreIdx].shape[1], -1 470 ) 471 return newData
Projects tensors onto basis spanned by TT-cores.
Given a tensor with appropriate dimensions, this function leverages the fact that TT-cores obtained through TTSVD and TT-ICE are column-orthonormal in the mode-2 unfolding.
Note
This function will not yield correct results if the TT-cores are not comprised of orthogonal vectors.
Parameters
- newData
np.aray
: Tensor to be projected - upTo
int
, optional: Index that the projection will be terminated. If an integer is passed as this parameter,newTensor
will be projected up to (not including) the core that has indexupTo
. Assumes 1-based indexing.
473 def reconstruct(self, projectedData, upTo=None): 474 """ 475 Reconstructs tensors using TT-cores. 476 477 Assumes that `projectedData` is a slice from the last 478 TT-core. 479 While reconstructing any projected tensor from `projectTensor`, 480 this function leverages the fact that TT-cores obtained through 481 `TTSVD`_ and `TT-ICE`_ are column-orthonormal in the mode-2 482 unfolding. 483 484 Note 485 ---- 486 This function might not yield correct results for projected tensors 487 if the TT-cores are not comprised of orthogonal vectors. 488 489 Parameters 490 ---------- 491 projectedData:obj:`np.aray` 492 A tensor slice (or alternatively an array) 493 upTo:obj:`int`, optional 494 Index that the reconstruction will be terminated. If an integer is 495 passed as this parameter, `projectedData` will be projected up to 496 (not including) the core that has index `upTo`. Assumes 1-based 497 indexing. 498 .. _TTSVD: 499 https://epubs.siam.org/doi/epdf/10.1137/090752286 500 _TT-ICE: 501 https://arxiv.org/abs/2211.12487 502 503 """ 504 if upTo is None: 505 upTo = len(self.ttCores) - 1 # Write the core index in 1-indexed form!! 506 for core in self.ttCores[:upTo][::-1]: 507 projectedData = np.tensordot(core, projectedData, axes=(-1, 0)) 508 return projectedData
Reconstructs tensors using TT-cores.
Assumes that projectedData
is a slice from the last
TT-core.
While reconstructing any projected tensor from projectTensor
,
this function leverages the fact that TT-cores obtained through
TTSVD and TT-ICE are column-orthonormal in the mode-2
unfolding.
Note
This function might not yield correct results for projected tensors if the TT-cores are not comprised of orthogonal vectors.
Parameters
- projectedData
np.aray
: A tensor slice (or alternatively an array) - upTo
int
, optional: Index that the reconstruction will be terminated. If an integer is passed as this parameter,projectedData
will be projected up to (not including) the core that has indexupTo
. Assumes 1-based indexing.
510 def updateRanks(self) -> None: 511 """ 512 Updates the ranks of the `ttObject` after incremental updates. 513 """ 514 self.ttRanks = [1] 515 for core in self.ttCores: 516 self.ttRanks.append(core.shape[-1]) 517 return None
Updates the ranks of the ttObject
after incremental updates.
519 def computeRelError(self, newTensor: np.array, useExact=True) -> np.array: 520 """ 521 Computes relative error by projecting data onto TT-cores. 522 523 This function computes the error induced by projecting `data` onto the TT-cores 524 of `ttObject`. The last index of `newTensor` is assumed to be the number of 525 individual observations. 526 527 Note 528 ---- 529 - In order to compute the projection onto the TT-cores, the dimensions of `data` 530 should match that of `ttObject`. 531 - If a single observation will be passed as `newTensor`, an additional 532 index/dimension should be introduced either through reshaping or [:,None] 533 534 Parameters 535 ---------- 536 newTensor:obj:`np.array` 537 Tensor for which the projection error is computed 538 useExact:`bool` 539 Boolean flag determining whether a projection error will be 540 returned for the entire batch or each observation in the batch. 541 542 Returns 543 ------- 544 relError:obj:`np.array` 545 Array of relative errors 546 """ 547 elementwiseNorm = np.linalg.norm(newTensor, axis=0) 548 for _ in range(len(newTensor.shape) - 2): 549 elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0) 550 projectedData = self.projectTensor(newTensor) 551 reconstructedData = self.reconstruct(projectedData).reshape(newTensor.shape) 552 difference = newTensor - reconstructedData 553 differenceNorm = np.linalg.norm(difference, axis=0) 554 for _ in range(len(difference.shape) - 2): 555 differenceNorm = np.linalg.norm(differenceNorm, axis=0) 556 557 if useExact: 558 relError = np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm) 559 else: 560 relError = differenceNorm / elementwiseNorm 561 return relError
Computes relative error by projecting data onto TT-cores.
This function computes the error induced by projecting data
onto the TT-cores
of ttObject
. The last index of newTensor
is assumed to be the number of
individual observations.
Note
- In order to compute the projection onto the TT-cores, the dimensions of
data
should match that ofttObject
. - If a single observation will be passed as
newTensor
, an additional index/dimension should be introduced either through reshaping or [:,None]
Parameters
- newTensor
np.array
: Tensor for which the projection error is computed - useExact (
bool
): Boolean flag determining whether a projection error will be returned for the entire batch or each observation in the batch.
Returns
- relError
np.array
: Array of relative errors
563 def computeRecError(self, data: np.array, start=None, finish=None, useExact=True) -> None: 564 """ 565 Function to compute relative error by reconstructing data from slices 566 of TT-cores. 567 568 Parameters 569 ---------- 570 data:obj:`np.array` 571 Tensor for which the reconstruction error is computed 572 start:`int` 573 Index for the start of the batch in the compressed last core 574 finish:`int` 575 Index for the end of the batch in the compressed last core 576 useExact:`bool` 577 Boolean flag determining whether a reconstruction error will be 578 returned for the entire batch or each observation in the batch. 579 580 Returns 581 ------- 582 recError:obj:`float` or `np.array` 583 Reconstruction error of the data from cores. Returns an array 584 of reconstruction errors for each observation in the batch if 585 `useExact` is `False`. 586 """ 587 if start is None: 588 start = self.ttCores[-1].shape[1]-1 589 if finish is None: 590 finish = start+1 591 rec=self.reconstruct(self.ttCores[-1][:,start:finish,:]).reshape(data.shape) 592 elementwiseNorm=np.linalg.norm(data,axis=0) 593 for idx in range(len(data.shape)-2): 594 elementwiseNorm=np.linalg.norm(elementwiseNorm,axis=0) 595 difference=data-rec 596 differenceNorm=np.linalg.norm(difference,axis=0) 597 for idx in range(len(difference.shape)-2): 598 differenceNorm=np.linalg.norm(differenceNorm,axis=0) 599 600 if useExact: 601 recError=np.linalg.norm(differenceNorm)/np.linalg.norm(elementwiseNorm) 602 else: 603 recError=differenceNorm/elementwiseNorm 604 605 return recError
Function to compute relative error by reconstructing data from slices of TT-cores.
Parameters
- data
np.array
: Tensor for which the reconstruction error is computed - start (
int
): Index for the start of the batch in the compressed last core - finish (
int
): Index for the end of the batch in the compressed last core - useExact (
bool
): Boolean flag determining whether a reconstruction error will be returned for the entire batch or each observation in the batch.
Returns
- recError
float
ornp.array
: Reconstruction error of the data from cores. Returns an array of reconstruction errors for each observation in the batch ifuseExact
isFalse
.
607 def ttDecomp(self, norm=None, dtype=np.float32) -> "ttObject.ttCores": 608 """ 609 Computes TT-decomposition of a multidimensional array using `TTSVD`_ algorithm. 610 611 Currently only supports `ttsvd` as method. In the future additional formats may 612 be covered. 613 614 Parameters 615 ---------- 616 norm:obj:`float`, optional 617 Norm of the tensor to be compressed 618 dtype:obj:`type`, optional 619 Desired data type for the compression. Intended to allow lower precision 620 if needed. 621 622 Raises 623 ------ 624 ValueError 625 When `method` is not one of the admissible methods. 626 627 628 The following attributes are modified as a result of this function: 629 ------- 630 - `ttObject.ttCores` 631 - `ttObject.ttRanks` 632 - `ttObject.compressionRatio` 633 634 .. _TTSVD: 635 https://epubs.siam.org/doi/epdf/10.1137/090752286 636 """ 637 if norm is None: 638 norm = np.linalg.norm(self.originalData) 639 if self.method == "ttsvd": 640 startTime = time.time() 641 self.ttRanks, self.ttCores = ttsvd( 642 self.originalData, norm, self.ttEpsilon, dtype=dtype 643 ) 644 self.compressionTime = time.time() - startTime 645 self.nCores = len(self.ttCores) 646 self.nElements = 0 647 for cores in self.ttCores: 648 self.nElements += np.prod(cores.shape) 649 if not self.keepOriginal: 650 self.originalData = None 651 return None 652 else: 653 raise ValueError("Method unknown. Please select a valid method!")
Computes TT-decomposition of a multidimensional array using TTSVD algorithm.
Currently only supports ttsvd
as method. In the future additional formats may
be covered.
Parameters
- norm
float
, optional: Norm of the tensor to be compressed - dtype
type
, optional: Desired data type for the compression. Intended to allow lower precision if needed.
Raises
- ValueError: When
method
is not one of the admissible methods. - The following attributes are modified as a result of this function:
- -------
- -
ttObject.ttCores
- -
ttObject.ttRanks
- -
ttObject.compressionRatio
655 def ttICE(self, newTensor, epsilon=None, tenNorm=None) -> None: 656 """ 657 `TT-ICE`_ algorithmn without any heuristic upgrades. 658 659 Given a set of TT-cores, this function provides incremental updates 660 to the TT-cores to approximate `newTensor` within a relative error 661 defined in `epsilon` 662 663 Note 664 ---- 665 This algorithm/function relies on the fact that TT-cores are columnwise 666 orthonormal in the mode-2 unfolding. 667 668 Parameters 669 ---------- 670 newTensor:obj:`np.array` 671 New/streamed tensor that will be used to expand the orthonormal bases defined 672 in TT-cores 673 epsilon:obj:`float`, optional 674 Relative error upper bound for approximating `newTensor` after incremental 675 updates. If not defined, `ttObject.ttEpsilon` is used. 676 tenNorm:obj:`float`, optional 677 Norm of `newTensor` 678 679 Notes 680 ------- 681 **The following attributes are modified as a result of this function:** 682 - `ttObject.ttCores` 683 - `ttObject.ttRanks` 684 - `ttObject.compressionRatio` 685 .. _TT-ICE: 686 https://arxiv.org/abs/2211.12487 687 """ 688 if tenNorm is None: 689 tenNorm = np.linalg.norm(newTensor) 690 if epsilon is None: 691 epsilon = self.ttEpsilon 692 newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 693 newTensorSize = len(newTensor.shape) - 1 694 newTensor = newTensor.reshape(self.reshapedShape[0], -1) 695 Ui = self.ttCores[0].reshape(self.reshapedShape[0], -1) 696 Ri = newTensor - Ui @ (Ui.T @ newTensor) 697 for coreIdx in range(0, len(self.ttCores) - 2): 698 URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon) 699 self.ttCores[coreIdx] = np.hstack((Ui, URi)).reshape( 700 self.ttCores[coreIdx].shape[0], self.reshapedShape[coreIdx], -1 701 ) 702 self.ttCores[coreIdx + 1] = np.concatenate( 703 ( 704 self.ttCores[coreIdx + 1], 705 np.zeros( 706 ( 707 URi.shape[-1], 708 self.reshapedShape[coreIdx + 1], 709 self.ttRanks[coreIdx + 2], 710 ) 711 ), 712 ), 713 axis=0, 714 ) 715 Ui = self.ttCores[coreIdx].reshape( 716 self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1 717 ) 718 newTensor = (Ui.T @ newTensor).reshape( 719 np.prod(self.ttCores[coreIdx + 1].shape[:-1]), -1 720 ) 721 Ui = self.ttCores[coreIdx + 1].reshape( 722 self.ttCores[coreIdx].shape[-1] * self.reshapedShape[coreIdx + 1], -1 723 ) 724 Ri = newTensor - Ui @ (Ui.T @ newTensor) 725 coreIdx = len(self.ttCores) - 2 726 URi, _, _ = deltaSVD(Ri, tenNorm, newTensorSize, epsilon) 727 self.ttCores[coreIdx] = np.hstack((Ui, URi)) 728 self.ttCores[coreIdx + 1] = np.concatenate( 729 ( 730 self.ttCores[coreIdx + 1], 731 np.zeros( 732 ( 733 URi.shape[-1], 734 self.ttCores[coreIdx + 1].shape[1], 735 self.ttRanks[coreIdx + 2], 736 ) 737 ), 738 ), 739 axis=0, 740 ) 741 newTensor = self.ttCores[coreIdx].T @ newTensor 742 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 743 self.ttCores[coreIdx - 1].shape[-1], self.reshapedShape[coreIdx], -1 744 ) 745 coreIdx += 1 746 Ui = self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0], -1) 747 self.ttCores[coreIdx] = np.hstack((Ui, newTensor)).reshape( 748 self.ttCores[coreIdx].shape[0], -1, 1 749 ) 750 self.updateRanks() 751 return None
TT-ICE algorithmn without any heuristic upgrades.
Given a set of TT-cores, this function provides incremental updates
to the TT-cores to approximate newTensor
within a relative error
defined in epsilon
Note
This algorithm/function relies on the fact that TT-cores are columnwise orthonormal in the mode-2 unfolding.
Parameters
- newTensor
np.array
: New/streamed tensor that will be used to expand the orthonormal bases defined in TT-cores - epsilon
float
, optional: Relative error upper bound for approximatingnewTensor
after incremental updates. If not defined,ttObject.ttEpsilon
is used. - tenNorm
float
, optional: Norm ofnewTensor
Notes
The following attributes are modified as a result of this function:
ttObject.ttCores
ttObject.ttRanks
ttObject.compressionRatio
753 def ttICEstar( 754 self, 755 newTensor: np.array, 756 epsilon: float = None, 757 tenNorm: float = None, 758 elementwiseNorm: np.array = None, 759 elementwiseEpsilon: np.array = None, 760 heuristicsToUse: list = ["skip", "subselect", "occupancy"], 761 occupancyThreshold: float = 0.8, 762 simpleEpsilonUpdate: bool = False, 763 surrogateThreshold=True, 764 sampleEpsilon=False, 765 samplePercent=0.1 766 ) -> None: 767 """ 768 `TT-ICE*`_ algorithmn with heuristic performance upgrades. 769 770 Given a set of TT-cores, this function provides incremental updates 771 to the TT-cores to approximate `newTensor` within a relative error 772 defined in `epsilon`. 773 774 Note 775 ---- 776 This algorithm/function relies on the fact that TT-cores are columnwise 777 orthonormal in the mode-2 unfolding. 778 779 Parameters 780 ---------- 781 newTensor:obj:`np.array` 782 New/streamed tensor that will be used to expand the orthonormal bases defined 783 in TT-cores 784 epsilon:obj:`float`, optional 785 Relative error upper bound for approximating `newTensor` after incremental 786 updates. If not defined, `ttObject.ttEpsilon` is used. 787 tenNorm:obj:`float`, optional 788 Norm of `newTensor`. 789 elementwiseNorm:obj:`np.array`, optional 790 Individual norms of the observations in `newTensor`. 791 elementwiseEpsilon:obj:`np.array`, optional 792 Individual relative projection errors of the observations in `newTensor`. 793 heuristicsToUse:obj:`list`, optional 794 List of heuristics to use while updating TT-cores. Currently only accepts 795 `'skip'`, `'subselect'`, and `'occupancy'`. 796 occupancyThreshold:obj:`float`, optional 797 Threshold determining whether to skip updating a single core or not. Not used 798 if `'occupancy'` is not in `heuristicsToUse` 799 simpleEpsilonUpdate:obj:`bool`, optional 800 Uses the simple epsilon update equation. *Warning*: this relies on the 801 assumption that all observations in `newTensor` have similar norms. 802 803 Notes 804 ------- 805 **The following attributes are modified as a result of this function:** 806 - `ttObject.ttCores` 807 - `ttObject.ttRanks` 808 - `ttObject.compressionRatio` 809 .. _TT-ICE*: 810 https://arxiv.org/abs/2211.12487 811 """ 812 if epsilon is None: 813 epsilon = self.ttEpsilon 814 if ("subselect" in heuristicsToUse) and (newTensor.shape[-1] == 1): 815 warning( 816 "The streamed tensor has only 1 observation in it. \ 817 Subselect heuristic will not be useful!!" 818 ) 819 newTensor = newTensor.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 820 updEpsilon = epsilon 821 newTensorSize = len(newTensor.shape) - 1 822 823 if elementwiseEpsilon is None: 824 elementwiseEpsilon = self.computeRelError(newTensor) 825 if "skip" in heuristicsToUse: 826 if surrogateThreshold: 827 if sampleEpsilon: 828 sampleSize=int(newTensor.shape[-1]*samplePercent) 829 skipCriterion=np.mean(np.random.choice(elementwiseEpsilon,size=sampleSize,replace=False)) 830 else: 831 skipCriterion=np.mean(elementwiseEpsilon) 832 if skipCriterion<= epsilon: 833 newTensor = self.projectTensor(newTensor) 834 self.ttCores[-1] = np.hstack( 835 (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor) 836 ).reshape(self.ttRanks[-2], -1, 1) 837 return None 838 else: 839 errorNorm=elementwiseEpsilon*elementwiseNorm 840 errorNorm=np.sqrt(np.sum(errorNorm**2))/np.linalg.norm(elementwiseNorm) 841 if errorNorm <= epsilon: 842 newTensor = self.projectTensor(newTensor) 843 self.ttCores[-1] = np.hstack( 844 (self.ttCores[-1].reshape(self.ttRanks[-2], -1), newTensor) 845 ).reshape(self.ttRanks[-2], -1, 1) 846 return None 847 if tenNorm is None and elementwiseNorm is None: 848 tenNorm = np.linalg.norm(newTensor) 849 elif tenNorm is None: 850 tenNorm = np.linalg.norm(elementwiseNorm) 851 852 select = [True] * newTensor.shape[-1] 853 discard = [False] * newTensor.shape[-1] 854 if "subselect" in heuristicsToUse: 855 select = elementwiseEpsilon > epsilon 856 discard = elementwiseEpsilon <= epsilon 857 if simpleEpsilonUpdate: 858 updEpsilon = ( 859 epsilon * newTensor.shape[-1] 860 - np.mean(elementwiseEpsilon[discard]) * discard.sum() 861 ) / (select.sum()) 862 else: 863 if elementwiseNorm is None: 864 elementwiseNorm = np.linalg.norm(newTensor, axis=0) 865 for _ in range(len(self.ttCores) - 1): 866 elementwiseNorm = np.linalg.norm(elementwiseNorm, axis=0) 867 allowedError = (self.ttEpsilon * np.linalg.norm(elementwiseNorm)) ** 2 868 discardedError = np.sum( 869 (elementwiseEpsilon[discard] * elementwiseNorm[discard]) ** 2 870 ) 871 updEpsilon = np.sqrt( 872 (allowedError - discardedError) 873 / (np.linalg.norm(elementwiseNorm[select]) ** 2) 874 ) 875 self.reshapedShape[-1] = np.array( 876 select 877 ).sum() # a little trick for ease of coding 878 879 indexString = "[" 880 for _ in range(len(self.reshapedShape)): 881 # this heuristic assumes that the last dimension is for observations 882 indexString += ":," 883 selectString = indexString + "select]" 884 selected = eval("newTensor" + selectString) 885 886 selected = selected.reshape(list(self.reshapedShape[:-1]) + [-1])[None, :] 887 for coreIdx in range(0, len(self.ttCores) - 1): 888 selected = selected.reshape(np.prod(self.ttCores[coreIdx].shape[:-1]), -1) 889 if ("occupancy" in heuristicsToUse) and ( 890 self.coreOccupancy[coreIdx] >= occupancyThreshold 891 ): 892 pass 893 else: 894 Ui = self.ttCores[coreIdx].reshape( 895 self.ttCores[coreIdx].shape[0] * self.reshapedShape[coreIdx], -1 896 ) 897 Ri = selected - Ui @ (Ui.T @ selected) 898 if (elementwiseNorm is None) or ("subselect" not in heuristicsToUse): 899 URi, _, _ = deltaSVD( 900 Ri, np.linalg.norm(selected), newTensorSize, updEpsilon 901 ) 902 else: 903 URi, _, _ = deltaSVD( 904 Ri, 905 np.linalg.norm(elementwiseNorm[select]), 906 newTensorSize, 907 updEpsilon, 908 ) 909 self.ttCores[coreIdx] = np.hstack((Ui, URi)) 910 self.ttCores[coreIdx + 1] = np.concatenate( 911 ( 912 self.ttCores[coreIdx + 1], 913 np.zeros( 914 ( 915 URi.shape[-1], 916 self.ttCores[coreIdx + 1].shape[1], 917 self.ttRanks[coreIdx + 2], 918 ) 919 ), 920 ), 921 axis=0, 922 ) 923 924 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 925 np.prod(self.ttCores[coreIdx].shape[:-1]), -1 926 ) 927 # #project onto existing core and reshape for next core 928 selected = (self.ttCores[coreIdx].T @ selected).reshape( 929 self.ttCores[coreIdx + 1].shape[0] * self.reshapedShape[coreIdx + 1], -1 930 ) 931 # fold back the previous core 932 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 933 -1, self.reshapedShape[coreIdx], self.ttCores[coreIdx].shape[-1] 934 ) 935 # self.ttCores[coreIdx]=self.ttCores[coreIdx].reshape(self.ttCores[coreIdx].shape[0],self.reshapedShape[coreIdx],-1) 936 # #fold back the previous core 937 self.updateRanks() 938 coreIdx += 1 939 # coreIdx=len(self.ttCores), i.e working on the last core 940 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 941 self.ttCores[coreIdx].shape[0], -1 942 ) 943 self.ttCores[coreIdx] = np.hstack( 944 (self.ttCores[coreIdx], self.projectTensor(newTensor)) 945 ).reshape(self.ttCores[coreIdx].shape[0], -1, 1) 946 return None
TT-ICE* algorithmn with heuristic performance upgrades.
Given a set of TT-cores, this function provides incremental updates
to the TT-cores to approximate newTensor
within a relative error
defined in epsilon
.
Note
This algorithm/function relies on the fact that TT-cores are columnwise orthonormal in the mode-2 unfolding.
Parameters
- newTensor
np.array
: New/streamed tensor that will be used to expand the orthonormal bases defined in TT-cores - epsilon
float
, optional: Relative error upper bound for approximatingnewTensor
after incremental updates. If not defined,ttObject.ttEpsilon
is used. - tenNorm
float
, optional: Norm ofnewTensor
. - elementwiseNorm
np.array
, optional: Individual norms of the observations innewTensor
. - elementwiseEpsilon
np.array
, optional: Individual relative projection errors of the observations innewTensor
. - heuristicsToUse
list
, optional: List of heuristics to use while updating TT-cores. Currently only accepts'skip'
,'subselect'
, and'occupancy'
. - occupancyThreshold
float
, optional: Threshold determining whether to skip updating a single core or not. Not used if'occupancy'
is not inheuristicsToUse
- simpleEpsilonUpdate
bool
, optional: Uses the simple epsilon update equation. Warning: this relies on the assumption that all observations innewTensor
have similar norms.
Notes
The following attributes are modified as a result of this function:
ttObject.ttCores
ttObject.ttRanks
ttObject.compressionRatio
948 def ttRound(self, norm, epsilon=0) -> None: 949 """ 950 Reorthogonalizes and recompresses TT-cores as per `Oseledets 2011`_. 951 952 This is the TT-rounding algorithm described in `TTSVD`_ paper. This 953 function first reorthogonalizes the cores from last to first using 954 QR decomposition and then recompresses the reorthogonalized TT-cores 955 from first to last using `TTSVD`_ algorithm. 956 957 Parameters 958 ---------- 959 norm:obj:`float` 960 Norm of the TT-approximation. If there is no information on the 961 uncompressed tensor, `ttNorm` function can be used. 962 epsilon:obj:`float`, optional 963 Relative error upper bound for the recompression step. Note that 964 this relative error upper bound is not with respect to the original 965 tensor but to the `compressed` tensor. Set to 0 by default. 966 967 Notes 968 ----- 969 **The following attributes are modified as a result of this function:** 970 - `ttObject.ttCores` 971 - `ttObject.ttRanks` 972 - `ttObject.compressionRatio` 973 974 .. _Oseledets 2011: 975 https://epubs.siam.org/doi/epdf/10.1137/090752286 976 TTSVD: 977 https://epubs.siam.org/doi/epdf/10.1137/090752286 978 """ 979 d = [core.shape[1] for core in self.ttCores] 980 for coreIdx in np.arange(len(self.ttCores))[::-1]: 981 currCore = self.ttCores[coreIdx] 982 # Compute mode 1 unfolding of the tt-core 983 currCore = currCore.reshape(currCore.shape[0], -1) 984 # Using the transpose results in a row orthogonal Q matrix !!! 985 q, r = np.linalg.qr(currCore.T) 986 q, r = q.T, r.T 987 self.ttCores[coreIdx] = q 988 # Contract previous tt-core and R matrix 989 self.ttCores[coreIdx - 1] = np.tensordot( 990 self.ttCores[coreIdx - 1], r, axes=(-1, 0) 991 ) 992 if coreIdx == 1: 993 break 994 # pass #TODO: write the case for the last core here. 995 # Compression of the orthogonalized representation # 996 ranks = [1] 997 for coreIdx in range(len(self.ttCores) - 1): 998 core = self.ttCores[coreIdx] 999 core = core.reshape(np.prod(core.shape[:-1]), -1) 1000 self.ttCores[coreIdx], sigma, v = deltaSVD( 1001 core, norm, len(self.ttCores), epsilon 1002 ) 1003 ranks.append(self.ttCores[coreIdx].shape[-1]) 1004 # fold matrices into tt-cores 1005 self.ttCores[coreIdx] = self.ttCores[coreIdx].reshape( 1006 ranks[coreIdx], d[coreIdx], -1 1007 ) 1008 self.ttCores[coreIdx + 1] = ( 1009 (np.diag(sigma) @ v) @ self.ttCores[coreIdx + 1] 1010 ).reshape(ranks[-1] * d[coreIdx + 1], -1) 1011 self.ttCores[-1] = self.ttCores[-1].reshape(-1, d[-1], 1) 1012 self.ttRanks = [1] 1013 for core in self.ttCores: 1014 self.ttRanks.append(core.shape[-1]) 1015 return None
Reorthogonalizes and recompresses TT-cores as per Oseledets 2011.
This is the TT-rounding algorithm described in TTSVD
_ paper. This
function first reorthogonalizes the cores from last to first using
QR decomposition and then recompresses the reorthogonalized TT-cores
from first to last using TTSVD
_ algorithm.
Parameters
- norm
float
: Norm of the TT-approximation. If there is no information on the uncompressed tensor,ttNorm
function can be used. - epsilon
float
, optional: Relative error upper bound for the recompression step. Note that this relative error upper bound is not with respect to the original tensor but to thecompressed
tensor. Set to 0 by default.
Notes
The following attributes are modified as a result of this function:
ttObject.ttCores
ttObject.ttRanks
ttObject.compressionRatio
TTSVD:
https://epubs.siam.org/doi/epdf/10.1137/090752286
1017 @staticmethod 1018 def ittd(tt1, tt2, rounding=True, epsilon=0) -> "ttObject": 1019 """ 1020 Alternative incremental tensor decomposition algorithm used to benchmark. 1021 1022 Note 1023 ---- 1024 This method is empirically shown to be inferior to `TT-ICE`_ and 1025 `TT-ICE*`_. 1026 1027 Parameters 1028 ---------- 1029 tt1:obj:`ttObject` 1030 Tensor in TT-format. 1031 tt2:obj:`ttObject` 1032 Tensor in TT-format. 1033 rounding:obj:`bool`, optional 1034 Determines if `ttRounding` will be done to the incremented TT-core. 1035 Note that for unbounded streams, rounding is essential. Otherwise 1036 TT-ranks will grow unboundedly. 1037 epsilon:obj:`float`, optional 1038 Relative error upper bound for the recompression step. Note that 1039 this relative error upper bound is not with respect to the original 1040 tensor but to the `compressed` tensor. Set to 0 by default. Not used 1041 if `rounding` is set to `False`. 1042 1043 Notes 1044 ----- 1045 **The following attributes are modified as a result of this function:** 1046 - `ttObject.ttCores` 1047 - `ttObject.ttRanks` 1048 - `ttObject.compressionRatio` 1049 1050 .. _TT-ICE: 1051 https://arxiv.org/abs/2211.12487 1052 _TT-ICE*: 1053 https://arxiv.org/abs/2211.12487 1054 """ 1055 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 1056 if isinstance(tt1, list): 1057 tt1 = ttObject(tt1) 1058 if isinstance(tt2, list): 1059 tt2 = ttObject(tt2) 1060 if not isinstance(tt1, ttObject) or not isinstance(tt2, ttObject): 1061 raise AttributeError("One of the passed objects is not in TT-format!") 1062 tt1.ttCores[-1] = tt1.ttCores[-1].squeeze(-1) 1063 tt2.ttCores[-1] = tt2.ttCores[-1].squeeze(-1) 1064 rank1, numObs1 = tt1.ttCores[-1].shape 1065 rank2, numObs2 = tt2.ttCores[-1].shape 1066 tt1.ttCores[-1] = np.concatenate( 1067 (tt1.ttCores[-1], np.zeros((rank1, numObs2))), axis=1 1068 ) 1069 tt2.ttCores[-1] = np.concatenate( 1070 (np.zeros((rank2, numObs1)), tt2.ttCores[-1]), axis=1 1071 ) 1072 # Addition in TT-format # 1073 for coreIdx in range(len(tt1.ttCores)): 1074 if coreIdx == 0: 1075 tt1.ttCores[coreIdx] = np.concatenate( 1076 (tt1.ttCores[coreIdx].squeeze(0), tt2.ttCores[coreIdx].squeeze(0)), 1077 axis=1, 1078 )[None, :, :] 1079 elif coreIdx == len(tt1.ttCores) - 1: 1080 tt1.ttCores[coreIdx] = np.concatenate( 1081 (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0 1082 )[:, :, None] 1083 else: 1084 s11, s12, s13 = tt1.ttCores[coreIdx].shape 1085 s21, s22, s23 = tt2.ttCores[coreIdx].shape 1086 tt1.ttCores[coreIdx] = np.concatenate( 1087 (tt1.ttCores[coreIdx], np.zeros((s11, s12, s23))), axis=2 1088 ) 1089 tt2.ttCores[coreIdx] = np.concatenate( 1090 (np.zeros((s21, s22, s13)), tt2.ttCores[coreIdx]), axis=2 1091 ) 1092 tt1.ttCores[coreIdx] = np.concatenate( 1093 (tt1.ttCores[coreIdx], tt2.ttCores[coreIdx]), axis=0 1094 ) 1095 tt1.ttRanks = [1] 1096 for core in tt1.ttCores: 1097 tt1.ttRanks.append(core.shape[-1]) 1098 if rounding: 1099 tenNorm = ttObject.ttNorm(tt1) 1100 tt1.ttRound(tenNorm, epsilon=epsilon) 1101 return tt1
Alternative incremental tensor decomposition algorithm used to benchmark.
Note
This method is empirically shown to be inferior to TT-ICE and TT-ICE*.
Parameters
- tt1
ttObject
: Tensor in TT-format. - tt2
ttObject
: Tensor in TT-format. - rounding
bool
, optional: Determines ifttRounding
will be done to the incremented TT-core. Note that for unbounded streams, rounding is essential. Otherwise TT-ranks will grow unboundedly. - epsilon
float
, optional: Relative error upper bound for the recompression step. Note that this relative error upper bound is not with respect to the original tensor but to thecompressed
tensor. Set to 0 by default. Not used ifrounding
is set toFalse
.
Notes
The following attributes are modified as a result of this function:
ttObject.ttCores
ttObject.ttRanks
ttObject.compressionRatio
1103 @staticmethod 1104 def ttfoa6d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None): 1105 nDims=len(data.shape) 1106 dims=list(data.shape) 1107 coreSwap=False 1108 if forgettingFactor is None: 1109 forgettingFactor=0.7 1110 1111 if len(ttRanks) != nDims-1: 1112 raise ValueError("Number of dimensions do not match with number of TT-ranks") 1113 1114 if ttCores is None: 1115 # Initialize TT-cores randomly if no core is given. 1116 ttCores=[] 1117 ttCores.append(np.random.randn(1,dims[0],ttRanks[0])) 1118 for dimIdx in range(1,nDims-1): 1119 ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx])) 1120 ttCores.append(np.empty((ttRanks[-1],1))) 1121 coreSwap=True 1122 # else: 1123 ttCores_old=ttCores.copy() 1124 if S is None: 1125 # Initialize S matrices as identity matrices if no S is given from previous step. 1126 S=[] 1127 S.append(100*np.eye(ttRanks[0])) 1128 for dimIdx in range(1,nDims-1): 1129 S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx])) 1130 1131 # Update the last core G6 1132 1133 Ht_1=coreContraction(ttCores_old[:-1]) 1134 # Ht_1=Ht_1.reshape(-1,ttRanks[-1]) 1135 Ht_1=mode_n_unfolding(Ht_1,5).T #this might be 5 1136 g6=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F') 1137 if coreSwap: 1138 ttCores[-1]=g6 1139 else: 1140 ttCores[-1]=np.hstack((ttCores[-1],g6)) 1141 recData=(Ht_1@g6).reshape(dims,order='F') 1142 delta=data-recData 1143 1144 # Update G1 1145 deltaunfold=mode_n_unfolding(delta,0) 1146 Bk=coreContraction(ttCores_old[1:-1]+[g6]) 1147 Wk=mode_n_unfolding(Bk,0) 1148 S[0]=forgettingFactor*S[0]+Wk@Wk.T 1149 # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0] 1150 Vk=np.linalg.pinv(S[0])@Wk 1151 ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T 1152 1153 # Update G2 1154 deltaunfold=mode_n_unfolding(delta,1) 1155 Ak=mode_n_unfolding(ttCores_old[0],2)#.T 1156 Bk=coreContraction(ttCores_old[2:-1]+[g6]) 1157 Bk=mode_n_unfolding(Bk,0) 1158 Wk=np.kron(Bk,Ak) 1159 S[1]=forgettingFactor*S[1]+Wk@Wk.T 1160 # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0] 1161 Vk=np.linalg.pinv(S[1])@Wk 1162 ttCores[1]=mode_n_unfolding(ttCores_old[1],1) 1163 ttCores[1]=ttCores[1]+deltaunfold@Vk.T 1164 ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2) 1165 1166 # Update G3 1167 deltaunfold=mode_n_unfolding(delta,2) 1168 Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2) 1169 Bk=coreContraction(ttCores_old[3:-1]+[g6]) #insert a mode 0 unfolding here if necessary 1170 Bk=mode_n_unfolding(Bk,0) 1171 Wk=np.kron(Bk,Ak) 1172 S[2]=forgettingFactor*S[2]+Wk@Wk.T 1173 # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0] 1174 Vk=np.linalg.pinv(S[2])@Wk 1175 ttCores[2]=mode_n_unfolding(ttCores_old[2],1) 1176 ttCores[2]=ttCores[2]+deltaunfold@Vk.T 1177 ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2) 1178 1179 # Update G4 1180 deltaunfold=mode_n_unfolding(delta,3) 1181 Ak=mode_n_unfolding(coreContraction(ttCores_old[:3]),3) 1182 Bk=coreContraction(ttCores_old[4:-1]+[g6]) #insert a mode 0 unfolding here if necessary 1183 Wk=np.kron(Bk,Ak) 1184 S[3]=forgettingFactor=S[3]+Wk@Wk.T 1185 # Vk=np.linalg.lstsq(S[3],Wk,rcond=None)[0] 1186 Vk=np.linalg.pinv(S[3])@Wk 1187 ttCores[3]=mode_n_unfolding(ttCores_old[3],1) 1188 ttCores[3]=ttCores[3]+deltaunfold@Vk.T 1189 ttCores[3]=ttCores[3].reshape(dims[3],ttRanks[2],ttRanks[3],order='F').transpose(1,0,2) 1190 1191 # Update G5 1192 deltaunfold=mode_n_unfolding(delta,4) 1193 Ak=mode_n_unfolding(coreContraction(ttCores_old[:4]),4) 1194 # Bk=coreContraction(g6) #insert a mode 0 unfolding here if necessary 1195 Wk=np.kron(g6,Ak) 1196 S[4]=forgettingFactor=S[4]+Wk@Wk.T 1197 # Vk=np.linalg.lstsq(S[4],Wk,rcond=None)[0] 1198 Vk=np.linalg.pinv(S[4])@Wk 1199 ttCores[4]=mode_n_unfolding(ttCores_old[4],1) 1200 ttCores[4]=ttCores[4]+deltaunfold@Vk.T 1201 ttCores[4]=ttCores[4].reshape(dims[4],ttRanks[3],ttRanks[4],order='F').transpose(1,0,2) 1202 1203 return S,ttCores
1205 @staticmethod 1206 def ttfoa4d(data,ttRanks,ttCores=None,S=None,forgettingFactor=None): 1207 nDims=len(data.shape) 1208 dims=list(data.shape) 1209 coreSwap=False 1210 if forgettingFactor is None: 1211 forgettingFactor=0.7 1212 1213 if len(ttRanks) != nDims-1: 1214 raise ValueError("Number of dimensions do not match with number of TT-ranks") 1215 1216 if ttCores is None: 1217 # Initialize TT-cores randomly if no core is given. 1218 ttCores=[] 1219 ttCores.append(np.random.randn(1,dims[0],ttRanks[0])) 1220 for dimIdx in range(1,nDims-1): 1221 ttCores.append(np.random.randn(ttRanks[dimIdx-1],dims[dimIdx],ttRanks[dimIdx])) 1222 ttCores.append(np.empty((ttRanks[-1],1))) 1223 coreSwap=True 1224 # else: 1225 ttCores_old=ttCores.copy() 1226 if S is None: 1227 # Initialize S matrices as identity matrices if no S is given from previous step. 1228 S=[] 1229 S.append(100*np.eye(ttRanks[0])) 1230 for dimIdx in range(1,nDims-1): 1231 S.append(100*np.eye(ttRanks[dimIdx-1]*ttRanks[dimIdx])) 1232 1233 Ht_1=coreContraction(ttCores_old[:-1]) 1234 # Ht_1=Ht_1.reshape(-1,ttRanks[-1]) 1235 Ht_1=mode_n_unfolding(Ht_1,3).T #this might be 4 1236 g4=np.linalg.lstsq(Ht_1,data.reshape(-1,order='F'),rcond=None)[0].reshape(-1,1,order='F') 1237 if coreSwap: 1238 ttCores[-1]=g4 1239 else: 1240 ttCores[-1]=np.hstack((ttCores[-1],g4)) 1241 recData=(Ht_1@g4).reshape(dims,order='F') 1242 delta=data-recData 1243 1244 # Update G1 1245 deltaunfold=mode_n_unfolding(delta,0) 1246 # Bk=coreContraction(ttCores_old[1:-1]+[g4]) 1247 Bk=np.tensordot(np.tensordot(ttCores_old[1],ttCores_old[2],axes=[-1,0]),g4,axes=[-1,0]) 1248 Wk=mode_n_unfolding(Bk,0) 1249 S[0]=forgettingFactor*S[0]+Wk@Wk.T 1250 # Vk=np.linalg.lstsq(S[0],Wk,rcond=None)[0] 1251 try: 1252 Vk=np.linalg.solve(S[0],Wk) 1253 except np.linalg.LinAlgError: 1254 print('Singular S[0] matrix, using Moore-Penrose inverse') 1255 Vk=np.linalg.pinv(S[0])@Wk 1256 ttCores[0]=ttCores_old[0]+deltaunfold@Vk.T 1257 1258 # Update G2 1259 deltaunfold=mode_n_unfolding(delta,1) 1260 Ak=mode_n_unfolding(ttCores_old[0],2)#.T 1261 Bk=coreContraction(ttCores_old[2:-1]+[g4]) 1262 Bk=mode_n_unfolding(Bk,0) 1263 Wk=np.kron(Bk,Ak) 1264 S[1]=forgettingFactor*S[1]+Wk@Wk.T 1265 # Vk=np.linalg.lstsq(S[1],Wk,rcond=None)[0] 1266 try: 1267 Vk=np.linalg.solve(S[1],Wk) 1268 except np.linalg.LinAlgError: 1269 print('Singular S[1] matrix, using Moore-Penrose inverse') 1270 Vk=np.linalg.pinv(S[1])@Wk 1271 ttCores[1]=mode_n_unfolding(ttCores_old[1],1) 1272 ttCores[1]=ttCores[1]+deltaunfold@Vk.T 1273 ttCores[1]=ttCores[1].reshape(dims[1],ttRanks[0],ttRanks[1],order='F').transpose(1,0,2) 1274 1275 # Update G3 1276 deltaunfold=mode_n_unfolding(delta,2) 1277 Ak=mode_n_unfolding(coreContraction(ttCores_old[:2]),2) 1278 # Bk=coreContraction(ttCores_old[3:-1]+[g4]) #insert a mode 0 unfolding here if necessary 1279 # Bk=mode_n_unfolding(Bk,0) 1280 Wk=np.kron(g4,Ak) 1281 S[2]=forgettingFactor*S[2]+Wk@Wk.T 1282 # Vk=np.linalg.lstsq(S[2],Wk,rcond=None)[0] 1283 try: 1284 Vk=np.linalg.solve(S[2],Wk) 1285 except np.linalg.LinAlgError: 1286 print('Singular S[2] matrix, using Moore-Penrose inverse') 1287 Vk=np.linalg.pinv(S[2])@Wk 1288 ttCores[2]=mode_n_unfolding(ttCores_old[2],1) 1289 ttCores[2]=ttCores[2]+deltaunfold@Vk.T 1290 ttCores[2]=ttCores[2].reshape(dims[2],ttRanks[1],ttRanks[2],order='F').transpose(1,0,2) 1291 1292 return S,ttCores