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 (
listofnumpy.array): Cores of the TT-decomposition. Stored as a list of numpy arrays. - ttRanks (
listofint): 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 (
tupleorlist): Original shape of the multidimensional array. - reshapedShape (
tupleorlist): Shape of the multidimensional array after reshaping. Note that this attribute is only meaningful before computing a TT-decomposition - indexOrder (
listofint): 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.arrayorlist): Main input to the ttObject. It can either be a multidimensionalnumpy arrayorlist 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 whendatahas typelist. - keepData (
bool, optional): Optional boolean variable to determine if the original array will be kept after compression. Set toFalseby default. - samplesAlongLastDimension (
bool, optional): Boolean variable to ensure if the samples are stacked along the last dimension. Assumed to beTruesince 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
ttcrossmight 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
tupleorlist: 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 iforiginalDatawill be discarded or not while saving. - outputType
str: Type of the output file.ttcfor pickledttObject,txtfor 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 resultingttObjectwill 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,newTensorwill 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,projectedDatawill 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
datashould 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
floatornp.array: Reconstruction error of the data from cores. Returns an array of reconstruction errors for each observation in the batch ifuseExactisFalse.
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
methodis 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 approximatingnewTensorafter incremental updates. If not defined,ttObject.ttEpsilonis used. - tenNorm
float, optional: Norm ofnewTensor
Notes
The following attributes are modified as a result of this function:
ttObject.ttCoresttObject.ttRanksttObject.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 approximatingnewTensorafter incremental updates. If not defined,ttObject.ttEpsilonis 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 innewTensorhave similar norms.
Notes
The following attributes are modified as a result of this function:
ttObject.ttCoresttObject.ttRanksttObject.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,ttNormfunction 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 thecompressedtensor. Set to 0 by default.
Notes
The following attributes are modified as a result of this function:
ttObject.ttCoresttObject.ttRanksttObject.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 ifttRoundingwill 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 thecompressedtensor. Set to 0 by default. Not used ifroundingis set toFalse.
Notes
The following attributes are modified as a result of this function:
ttObject.ttCoresttObject.ttRanksttObject.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