├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── conf.py ├── index.rst ├── modules.rst ├── sktensor.rst └── usage.rst ├── examples └── cp_sensory_bread_data.py ├── setup.cfg ├── setup.py └── sktensor ├── __init__.py ├── core.py ├── cp.py ├── dedicom.py ├── dtensor.py ├── indscal.py ├── ktensor.py ├── pyutils.py ├── rescal.py ├── setup.py ├── sptensor.py ├── tests ├── __init__.py ├── sptensor_fixture.py ├── sptensor_rand_fixture.py ├── test_base.py ├── test_dtensor.py ├── test_ktensor.py ├── test_pyutils.py ├── test_sptensor.py ├── test_tucker_hooi.py ├── test_utils.py └── ttm_fixture.py ├── tucker.py ├── utils.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | build 4 | dist 5 | scikit_tensor.egg-info 6 | docs/build 7 | docs/source/api 8 | .cache 9 | .eggs/ 10 | venv/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | python: 4 | - "2.7_with_system_site_packages" 5 | - 3.4 6 | before_install: 7 | - sudo apt-get update -qq 8 | - if [[ $TRAVIS_PYTHON_VERSION == *"2."* ]]; then sudo apt-get install python-numpy python-scipy; fi 9 | - if [[ $TRAVIS_PYTHON_VERSION == *"3."* ]]; then sudo apt-get install python3-numpy python3-scipy; fi 10 | - sudo apt-get install -qq libatlas-dev libatlas-base-dev liblapack-dev gfortran 11 | install: 12 | - pip install -U pip wheel 13 | - pip install numpy 14 | - pip install scipy nose pytest sphinx numpydoc sphinx-rtd-theme 15 | - pip install -e . 16 | script: 17 | - py.test 18 | - python setup.py build_sphinx 19 | - python setup.py egg_info -b.dev sdist --formats gztar 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | recursive-include docs * 3 | recursive-include examples * 4 | recursive-include sktensor *.c *.h *.pyx *.pxd 5 | recursive-include sktensor/datasets *.csv *.csv.gz *.rst *.jpg *.txt 6 | include LICENSE 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scikit-tensor 2 | ![Travis CI](https://travis-ci.org/mnick/scikit-tensor.svg?branch=master) 3 | 4 | scikit-tensor is a Python module for multilinear algebra and tensor 5 | factorizations. Currently, scikit-tensor supports basic tensor operations 6 | such as folding/unfolding, tensor-matrix and tensor-vector products as 7 | well as the following tensor factorizations: 8 | 9 | * Canonical / Parafac Decomposition 10 | * Tucker Decomposition 11 | * RESCAL 12 | * DEDICOM 13 | * INDSCAL 14 | 15 | Moreover, all operations support dense and tensors. 16 | 17 | #### Dependencies 18 | The required dependencies to build the software are `Numpy >= 1.3`, `SciPy >= 0.7`. 19 | 20 | #### Usage 21 | Example script to decompose sensory bread data (available from http://www.models.life.ku.dk/datasets) using CP-ALS 22 | 23 | ```python 24 | import logging 25 | from scipy.io.matlab import loadmat 26 | from sktensor import dtensor, cp_als 27 | 28 | # Set logging to DEBUG to see CP-ALS information 29 | logging.basicConfig(level=logging.DEBUG) 30 | 31 | # Load Matlab data and convert it to dense tensor format 32 | mat = loadmat('../data/sensory-bread/brod.mat') 33 | T = dtensor(mat['X']) 34 | 35 | # Decompose tensor using CP-ALS 36 | P, fit, itr, exectimes = cp_als(T, 3, init='random') 37 | ``` 38 | 39 | #### Install 40 | This package uses distutils, which is the default way of installing python modules. The use of virtual environments is recommended. 41 | 42 | pip install scikit-tensor 43 | 44 | To install in development mode 45 | 46 | git clone git@github.com:mnick/scikit-tensor.git 47 | pip install -e scikit-tensor/ 48 | 49 | #### Contributing & Development 50 | scikit-tensor is still an extremely young project, and I'm happy for any contributions (patches, code, bugfixes, *documentation*, whatever) to get it to a stable and useful point. Feel free to get in touch with me via email (mnick at AT mit DOT edu) or directly via github. 51 | 52 | Development is synchronized via git. To clone this repository, run 53 | 54 | git clone git://github.com/mnick/scikit-tensor.git 55 | 56 | #### Authors 57 | Maximilian Nickel: [Web](http://web.mit.edu/~mnick/www), [Email](mailto://mnick AT mit DOT edu), [Twitter](http://twitter.com/mnick) 58 | 59 | #### License 60 | scikit-tensor is licensed under the [GPLv3](http://www.gnu.org/licenses/gpl-3.0.txt) 61 | 62 | #### Related Projects 63 | * [Matlab Tensor Toolbox](http://www.sandia.gov/~tgkolda/TensorToolbox/index-2.5.html): 64 | A Matlab toolbox for tensor factorizations and tensor operations freely available for research and evaluation. 65 | * [Matlab Tensorlab](http://www.tensorlab.net/) 66 | A Matlab toolbox for tensor factorizations, complex optimization, and tensor optimization freely available for 67 | non-commercial academic research. 68 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # scikit-tensor documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Apr 20 14:28:17 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import sphinx_rtd_theme 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | # sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.pngmath', 36 | 'sphinx.ext.napoleon', 37 | 'sphinx.ext.autosummary', 38 | 'numpydoc' 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix of source filenames. 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | # source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'scikit-tensor' 55 | copyright = '2016, Maximilian Nickel' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = '0.1' 63 | # The full version, including alpha/beta/rc tags. 64 | release = '0.1' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # language = None 69 | 70 | # There are two options for replacing |today|: either, you set today to some 71 | # non-false value, then it is used: 72 | # today = '' 73 | # Else, today_fmt is used as the format for a strftime call. 74 | # today_fmt = '%B %d, %Y' 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | exclude_patterns = ['_build', '**tests**', '**setup**', '**extern**', 79 | '**data**'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | # default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | # add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | # add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | # show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | # modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | # keep_warnings = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | html_theme = "sphinx_rtd_theme" 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | # html_theme_options = {} 116 | 117 | # Add any paths that contain custom themes here, relative to this directory. 118 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 119 | 120 | # The name for this set of Sphinx documents. If None, it defaults to 121 | # " v documentation". 122 | # html_title = None 123 | 124 | # A shorter title for the navigation bar. Default is the same as html_title. 125 | # html_short_title = None 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | # html_logo = None 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | # html_favicon = None 135 | 136 | # Add any paths that contain custom static files (such as style sheets) here, 137 | # relative to this directory. They are copied after the builtin static files, 138 | # so a file named "default.css" will overwrite the builtin "default.css". 139 | html_static_path = ['_static'] 140 | 141 | # Add any extra paths that contain custom files (such as robots.txt or 142 | # .htaccess) here, relative to this directory. These files are copied 143 | # directly to the root of the documentation. 144 | # html_extra_path = [] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | # html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | # html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | # html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | # html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | # html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | # html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | # html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | # html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | # html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | # html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | # html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | # html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = 'dspydoc' 189 | 190 | 191 | # -- Options for LaTeX output --------------------------------------------- 192 | 193 | pngmath_latex_preamble = ( 194 | '\\usepackage{amsmath}\n' 195 | '\\usepackage{amssymb}\n' 196 | '\\newcommand{\\unfold}[2]{{#1}_{(#2)}}\n' 197 | '\\newcommand{\\ten}[1]{\\mathcal{#1}}\n' 198 | '\\newcommand{\\kr}{\\otimes}' 199 | ) 200 | 201 | latex_elements = { 202 | # The paper size ('letterpaper' or 'a4paper'). 203 | # 'papersize': 'letterpaper', 204 | 205 | # The font size ('10pt', '11pt' or '12pt'). 206 | # 'pointsize': '10pt', 207 | 208 | # Additional stuff for the LaTeX preamble. 209 | 'preamble': pngmath_latex_preamble, 210 | } 211 | 212 | # Grouping the document tree into LaTeX files. List of tuples 213 | # (source start file, target name, title, 214 | # author, documentclass [howto, manual, or own class]). 215 | latex_documents = [ 216 | ('index', 'index.tex', 'scikit-tensor Documentation', 217 | 'Maximilian Nickel', 'manual'), 218 | ] 219 | 220 | # The name of an image file (relative to this directory) to place at the top of 221 | # the title page. 222 | # latex_logo = None 223 | 224 | # For "manual" documents, if this is true, then toplevel headings are parts, 225 | # not chapters. 226 | # latex_use_parts = False 227 | 228 | # If true, show page references after internal links. 229 | # latex_show_pagerefs = False 230 | 231 | # If true, show URL addresses after external links. 232 | # latex_show_urls = False 233 | 234 | # Documents to append as an appendix to all manuals. 235 | # latex_appendices = [] 236 | 237 | # If false, no module index is generated. 238 | # latex_domain_indices = True 239 | 240 | 241 | # -- Options for manual page output --------------------------------------- 242 | 243 | # One entry per manual page. List of tuples 244 | # (source start file, name, description, authors, manual section). 245 | man_pages = [ 246 | ('index', 'scikit-tensor', 'scikit-tensor Documentation', 247 | ['Maximilian Nickel'], 1) 248 | ] 249 | 250 | # If true, show URL addresses after external links. 251 | # man_show_urls = False 252 | 253 | 254 | # -- Options for Texinfo output ------------------------------------------- 255 | 256 | # Grouping the document tree into Texinfo files. List of tuples 257 | # (source start file, target name, title, author, 258 | # dir menu entry, description, category) 259 | texinfo_documents = [ 260 | ('index', 'scikit-tensor', 'scikit-tensor Documentation', 261 | 'Maximilian Nickel', 'scikit-tensor', 262 | 'One line description of project.', 263 | 'Miscellaneous'), 264 | ] 265 | 266 | # Documents to append as an appendix to all manuals. 267 | # texinfo_appendices = [] 268 | 269 | # If false, no module index is generated. 270 | # texinfo_domain_indices = True 271 | 272 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 273 | # texinfo_show_urls = 'footnote' 274 | 275 | # If true, do not generate a @detailmenu in the "Top" node's menu. 276 | # texinfo_no_detailmenu = False 277 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Index 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | self 8 | usage 9 | modules 10 | 11 | scikit-tensor is a Python module for multilinear algebra and tensor factorizations. Currently, scikit-tensor supports basic tensor operations such as folding/unfolding, tensor-matrix and tensor-vector products as well as the following tensor factorizations: 12 | 13 | - Canonical / Parafac Decomposition 14 | - Tucker Decomposition 15 | - RESCAL 16 | - DEDICOM 17 | - INDSCAL 18 | 19 | Moreover, all operations support dense and tensors. 20 | 21 | Installation 22 | ============ 23 | 24 | .. code:: sh 25 | 26 | pip install scikit-tensor 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | .. toctree:: 5 | 6 | sktensor 7 | -------------------------------------------------------------------------------- /docs/sktensor.rst: -------------------------------------------------------------------------------- 1 | sktensor package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | sktensor.core module 8 | -------------------- 9 | 10 | .. automodule:: sktensor.core 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | sktensor.cp module 16 | ------------------ 17 | 18 | .. automodule:: sktensor.cp 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | sktensor.dedicom module 24 | ----------------------- 25 | 26 | .. automodule:: sktensor.dedicom 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | sktensor.dtensor module 32 | ----------------------- 33 | 34 | .. automodule:: sktensor.dtensor 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | sktensor.indscal module 40 | ----------------------- 41 | 42 | .. automodule:: sktensor.indscal 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | sktensor.ktensor module 48 | ----------------------- 49 | 50 | .. automodule:: sktensor.ktensor 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | sktensor.pyutils module 56 | ----------------------- 57 | 58 | .. automodule:: sktensor.pyutils 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | sktensor.rescal module 64 | ---------------------- 65 | 66 | .. automodule:: sktensor.rescal 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | sktensor.setup module 72 | --------------------- 73 | 74 | .. automodule:: sktensor.setup 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | sktensor.sptensor module 80 | ------------------------ 81 | 82 | .. automodule:: sktensor.sptensor 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | sktensor.tucker module 88 | ---------------------- 89 | 90 | .. automodule:: sktensor.tucker 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | sktensor.utils module 96 | --------------------- 97 | 98 | .. automodule:: sktensor.utils 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | sktensor.version module 104 | ----------------------- 105 | 106 | .. automodule:: sktensor.version 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | 112 | Module contents 113 | --------------- 114 | 115 | .. automodule:: sktensor 116 | :members: 117 | :undoc-members: 118 | :show-inheritance: 119 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Example script to decompose sensory bread data (available from http://www.models.life.ku.dk/datasets) using CP-ALS 5 | 6 | .. code:: python 7 | 8 | import logging 9 | from scipy.io.matlab import loadmat 10 | from sktensor import dtensor, cp_als 11 | 12 | # Set logging to DEBUG to see CP-ALS information 13 | logging.basicConfig(level=logging.DEBUG) 14 | 15 | # Load Matlab data and convert it to dense tensor format 16 | mat = loadmat('../data/sensory-bread/brod.mat') 17 | T = dtensor(mat['X']) 18 | 19 | # Decompose tensor using CP-ALS 20 | P, fit, itr, exectimes = cp_als(T, 3, init='random') 21 | -------------------------------------------------------------------------------- /examples/cp_sensory_bread_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | from scipy.io.matlab import loadmat 5 | from sktensor import dtensor, cp_als 6 | 7 | # Set logging to DEBUG to see CP-ALS information 8 | logging.basicConfig(level=logging.DEBUG) 9 | 10 | # Load Matlab data and convert it to dense tensor format 11 | mat = loadmat('../data/sensory-bread/brod.mat') 12 | T = dtensor(mat['X']) 13 | 14 | # Decompose tensor using CP-ALS 15 | P, fit, itr, exectimes = cp_als(T, 3, init='random') 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | descr = """Python module for multilinear algebra and tensor factorizations""" 3 | 4 | import os 5 | import sys 6 | 7 | DISTNAME = 'scikit-tensor' 8 | DESCRIPTION = descr 9 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f: 10 | LONG_DESCRIPTION = f.read() 11 | MAINTAINER = 'Maximilian Nickel', 12 | MAINTAINER_EMAIL = 'mnick@mit.edu', 13 | URL = 'http://github.com/mnick/scikit-tensor' 14 | LICENSE = 'GPLv3' 15 | DOWNLOAD_URL = URL 16 | PACKAGE_NAME = 'sktensor' 17 | EXTRA_INFO = dict( 18 | classifiers=[ 19 | "Development Status :: 3 - Alpha", 20 | 'Intended Audience :: Developers', 21 | 'Intended Audience :: Science/Research', 22 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 23 | 'Topic :: Scientific/Engineering', 24 | 'Topic :: Software Development', 25 | 'Operating System :: Microsoft :: Windows', 26 | 'Operating System :: POSIX', 27 | 'Operating System :: Unix', 28 | 'Operating System :: MacOS', 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.6', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.3', 34 | ] 35 | ) 36 | 37 | try: 38 | import setuptools # If you want to enable 'python setup.py develop' 39 | EXTRA_INFO.update(dict( 40 | zip_safe=False, # the package can run out of an .egg file 41 | include_package_data=True, 42 | )) 43 | except: 44 | print('setuptools module not found.') 45 | print("Install setuptools if you want to enable 'python setup.py develop'.") 46 | 47 | 48 | def configuration(parent_package='', top_path=None, package_name=DISTNAME): 49 | if os.path.exists('MANIFEST'): 50 | os.remove('MANIFEST') 51 | 52 | from numpy.distutils.misc_util import Configuration 53 | config = Configuration(None, parent_package, top_path) 54 | 55 | # Avoid non-useful msg: "Ignoring attempt to set 'name' (from ... " 56 | config.set_options( 57 | ignore_setup_xxx_py=True, 58 | assume_default_configuration=True, 59 | delegate_options_to_subpackages=True, 60 | quiet=True 61 | ) 62 | 63 | config.add_subpackage(PACKAGE_NAME) 64 | return config 65 | 66 | 67 | def get_version(): 68 | """Obtain the version number""" 69 | import imp 70 | mod = imp.load_source('version', os.path.join(PACKAGE_NAME, 'version.py')) 71 | return mod.__version__ 72 | 73 | 74 | def setup_package(): 75 | # Call the setup function 76 | metadata = dict( 77 | name=DISTNAME, 78 | maintainer=MAINTAINER, 79 | maintainer_email=MAINTAINER_EMAIL, 80 | description=DESCRIPTION, 81 | license=LICENSE, 82 | url=URL, 83 | download_url=DOWNLOAD_URL, 84 | long_description=LONG_DESCRIPTION, 85 | version=get_version(), 86 | install_requires=[ 87 | 'numpy', 88 | 'scipy', 89 | 'nose' 90 | ], 91 | #test_suite="nose.collector", 92 | **EXTRA_INFO 93 | ) 94 | 95 | if (len(sys.argv) >= 2 96 | and ('--help' in sys.argv[1:] or sys.argv[1] 97 | in ('--help-commands', 'egg_info', '--version', 'clean'))): 98 | 99 | # For these actions, NumPy is not required. 100 | # 101 | # They are required to succeed without Numpy for example when 102 | # pip is used to install Scikit when Numpy is not yet present in 103 | # the system. 104 | try: 105 | from setuptools import setup 106 | except ImportError: 107 | from distutils.core import setup 108 | 109 | metadata['version'] = get_version() 110 | else: 111 | metadata['configuration'] = configuration 112 | from numpy.distutils.core import setup 113 | 114 | 115 | setup(**metadata) 116 | 117 | if __name__ == "__main__": 118 | setup_package() 119 | -------------------------------------------------------------------------------- /sktensor/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | 3 | from .utils import * 4 | from .core import * 5 | 6 | # data types 7 | from .sptensor import sptensor, unfolded_sptensor 8 | from .dtensor import dtensor, unfolded_dtensor 9 | from .ktensor import ktensor 10 | 11 | # import algorithms 12 | from .cp import als as cp_als 13 | from .tucker import hooi as tucker_hooi 14 | from .tucker import hooi as tucker_hosvd 15 | -------------------------------------------------------------------------------- /sktensor/core.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Maximilian Nickel 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import numpy as np 17 | from numpy import array, dot, zeros, ones, arange, kron 18 | from numpy import setdiff1d 19 | from scipy.linalg import eigh 20 | from scipy.sparse import issparse as issparse_mat 21 | from scipy.sparse import csr_matrix 22 | from scipy.sparse.linalg import eigsh 23 | from abc import ABCMeta, abstractmethod 24 | from .pyutils import is_sequence, func_attr 25 | #from coremod import khatrirao 26 | 27 | import sys 28 | import types 29 | 30 | module_funs = [] 31 | 32 | 33 | def modulefunction(func): 34 | module_funs.append(func_attr(func, 'name')) 35 | 36 | 37 | class tensor_mixin(object): 38 | """ 39 | Base tensor class from which all tensor classes are subclasses. 40 | Can not be instaniated 41 | 42 | See also 43 | -------- 44 | sktensor.dtensor : Subclass for *dense* tensors. 45 | sktensor.sptensor : Subclass for *sparse* tensors. 46 | """ 47 | 48 | __metaclass__ = ABCMeta 49 | 50 | def ttm(self, V, mode=None, transp=False, without=False): 51 | """ 52 | Tensor times matrix product 53 | 54 | Parameters 55 | ---------- 56 | V : M x N array_like or list of M_i x N_i array_likes 57 | Matrix or list of matrices for which the tensor times matrix 58 | products should be performed 59 | mode : int or list of int's, optional 60 | Modes along which the tensor times matrix products should be 61 | performed 62 | transp: boolean, optional 63 | If True, tensor times matrix products are computed with 64 | transpositions of matrices 65 | without: boolean, optional 66 | It True, tensor times matrix products are performed along all 67 | modes **except** the modes specified via parameter ``mode`` 68 | 69 | 70 | Examples 71 | -------- 72 | Create dense tensor 73 | 74 | >>> T = zeros((3, 4, 2)) 75 | >>> T[:, :, 0] = [[ 1, 4, 7, 10], [ 2, 5, 8, 11], [3, 6, 9, 12]] 76 | >>> T[:, :, 1] = [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]] 77 | >>> T = dtensor(T) 78 | 79 | Create matrix 80 | 81 | >>> V = array([[1, 3, 5], [2, 4, 6]]) 82 | 83 | Multiply tensor with matrix along mode 0 84 | 85 | >>> Y = T.ttm(V, 0) 86 | >>> Y[:, :, 0] 87 | array([[ 22., 49., 76., 103.], 88 | [ 28., 64., 100., 136.]]) 89 | >>> Y[:, :, 1] 90 | array([[ 130., 157., 184., 211.], 91 | [ 172., 208., 244., 280.]]) 92 | 93 | """ 94 | if mode is None: 95 | mode = range(self.ndim) 96 | if isinstance(V, np.ndarray): 97 | Y = self._ttm_compute(V, mode, transp) 98 | elif is_sequence(V): 99 | dims, vidx = check_multiplication_dims(mode, self.ndim, len(V), vidx=True, without=without) 100 | Y = self._ttm_compute(V[vidx[0]], dims[0], transp) 101 | for i in xrange(1, len(dims)): 102 | Y = Y._ttm_compute(V[vidx[i]], dims[i], transp) 103 | return Y 104 | 105 | def ttv(self, v, modes=[], without=False): 106 | """ 107 | Tensor times vector product 108 | 109 | Parameters 110 | ---------- 111 | v : 1-d array or tuple of 1-d arrays 112 | Vector to be multiplied with tensor. 113 | modes : array_like of integers, optional 114 | Modes in which the vectors should be multiplied. 115 | without : boolean, optional 116 | If True, vectors are multiplied in all modes **except** the 117 | modes specified in ``modes``. 118 | 119 | """ 120 | if not isinstance(v, tuple): 121 | v = (v, ) 122 | dims, vidx = check_multiplication_dims(modes, self.ndim, len(v), vidx=True, without=without) 123 | for i in range(len(dims)): 124 | if not len(v[vidx[i]]) == self.shape[dims[i]]: 125 | raise ValueError('Multiplicant is wrong size') 126 | remdims = np.setdiff1d(range(self.ndim), dims) 127 | return self._ttv_compute(v, dims, vidx, remdims) 128 | 129 | #@abstractmethod 130 | #def ttt(self, other, modes=None): 131 | # pass 132 | 133 | @abstractmethod 134 | def _ttm_compute(self, V, mode, transp): 135 | pass 136 | 137 | @abstractmethod 138 | def _ttv_compute(self, v, dims, vidx, remdims): 139 | pass 140 | 141 | @abstractmethod 142 | def unfold(self, rdims, cdims=None, transp=False): 143 | pass 144 | 145 | @abstractmethod 146 | def uttkrp(self, U, mode): 147 | """ 148 | Unfolded tensor times Khatri-Rao product: 149 | :math:`M = \\unfold{X}{3} (U_1 \kr \cdots \kr U_N)` 150 | 151 | Computes the _matrix_ product of the unfolding 152 | of a tensor and the Khatri-Rao product of multiple matrices. 153 | Efficient computations are perfomed by the respective 154 | tensor implementations. 155 | 156 | Parameters 157 | ---------- 158 | U : list of array-likes 159 | Matrices for which the Khatri-Rao product is computed and 160 | which are multiplied with the tensor in mode ``mode``. 161 | mode: int 162 | Mode in which the Khatri-Rao product of ``U`` is multiplied 163 | with the tensor. 164 | 165 | Returns 166 | ------- 167 | M : np.ndarray 168 | Matrix which is the result of the matrix product of the unfolding of 169 | the tensor and the Khatri-Rao product of ``U`` 170 | 171 | See also 172 | -------- 173 | For efficient computations of unfolded tensor times Khatri-Rao products 174 | for specialiized tensors see also 175 | dtensor.uttkrp, sptensor.uttkrp, ktensor.uttkrp, ttensor.uttkrp 176 | 177 | References 178 | ---------- 179 | .. [1] B.W. Bader, T.G. Kolda 180 | Efficient Matlab Computations With Sparse and Factored Tensors 181 | SIAM J. Sci. Comput, Vol 30, No. 1, pp. 205--231, 2007 182 | """ 183 | pass 184 | 185 | @abstractmethod 186 | def transpose(self, axes=None): 187 | """ 188 | Compute transpose of tensors. 189 | 190 | Parameters 191 | ---------- 192 | axes : array_like of ints, optional 193 | Permute the axes according to the values given. 194 | 195 | Returns 196 | ------- 197 | d : tensor_mixin 198 | tensor with axes permuted. 199 | 200 | See also 201 | -------- 202 | dtensor.transpose, sptensor.transpose 203 | """ 204 | pass 205 | 206 | 207 | def istensor(X): 208 | return isinstance(X, tensor_mixin) 209 | 210 | 211 | # dynamically create module level functions 212 | conv_funcs = [ 213 | 'norm', 214 | 'transpose', 215 | 'ttm', 216 | 'ttv', 217 | 'unfold', 218 | ] 219 | 220 | for fname in conv_funcs: 221 | def call_on_me(obj, *args, **kwargs): 222 | if not istensor(obj): 223 | raise ValueError('%s() object must be tensor (%s)' % (fname, type(obj))) 224 | func = getattr(obj, fname) 225 | return func(*args, **kwargs) 226 | 227 | nfunc = types.FunctionType( 228 | func_attr(call_on_me, 'code'), 229 | { 230 | 'getattr': getattr, 231 | 'fname': fname, 232 | 'istensor': istensor, 233 | 'ValueError': ValueError, 234 | 'type': type 235 | }, 236 | name=fname, 237 | argdefs=func_attr(call_on_me, 'defaults'), 238 | closure=func_attr(call_on_me, 'closure') 239 | ) 240 | setattr(sys.modules[__name__], fname, nfunc) 241 | 242 | 243 | def check_multiplication_dims(dims, N, M, vidx=False, without=False): 244 | dims = array(dims, ndmin=1) 245 | if len(dims) == 0: 246 | dims = arange(N) 247 | if without: 248 | dims = setdiff1d(range(N), dims) 249 | if not np.in1d(dims, arange(N)).all(): 250 | raise ValueError('Invalid dimensions') 251 | P = len(dims) 252 | sidx = np.argsort(dims) 253 | sdims = dims[sidx] 254 | if vidx: 255 | if M > N: 256 | raise ValueError('More multiplicants than dimensions') 257 | if M != N and M != P: 258 | raise ValueError('Invalid number of multiplicants') 259 | if P == M: 260 | vidx = sidx 261 | else: 262 | vidx = sdims 263 | return sdims, vidx 264 | else: 265 | return sdims 266 | 267 | 268 | def innerprod(X, Y): 269 | """ 270 | Inner prodcut with a Tensor 271 | """ 272 | return dot(X.flatten(), Y.flatten()) 273 | 274 | 275 | def nvecs(X, n, rank, do_flipsign=True, dtype=np.float): 276 | """ 277 | Eigendecomposition of mode-n unfolding of a tensor 278 | """ 279 | Xn = X.unfold(n) 280 | if issparse_mat(Xn): 281 | Xn = csr_matrix(Xn, dtype=dtype) 282 | Y = Xn.dot(Xn.T) 283 | _, U = eigsh(Y, rank, which='LM') 284 | else: 285 | Y = Xn.dot(Xn.T) 286 | N = Y.shape[0] 287 | _, U = eigh(Y, eigvals=(N - rank, N - 1)) 288 | #_, U = eigsh(Y, rank, which='LM') 289 | # reverse order of eigenvectors such that eigenvalues are decreasing 290 | U = array(U[:, ::-1]) 291 | # flip sign 292 | if do_flipsign: 293 | U = flipsign(U) 294 | return U 295 | 296 | 297 | def flipsign(U): 298 | """ 299 | Flip sign of factor matrices such that largest magnitude 300 | element will be positive 301 | """ 302 | midx = abs(U).argmax(axis=0) 303 | for i in range(U.shape[1]): 304 | if U[midx[i], i] < 0: 305 | U[:, i] = -U[:, i] 306 | return U 307 | 308 | 309 | def center(X, n): 310 | Xn = unfold(X, n) 311 | N = Xn.shape[0] 312 | m = Xn.sum(axis=0) / N 313 | m = kron(m, ones((N, 1))) 314 | Xn = Xn - m 315 | return fold(Xn, n) 316 | 317 | 318 | def center_matrix(X): 319 | m = X.mean(axis=0) 320 | return X - m 321 | 322 | 323 | def scale(X, n): 324 | Xn = unfold(X, n) 325 | m = np.float_(np.sqrt((Xn ** 2).sum(axis=1))) 326 | m[m == 0] = 1 327 | for i in range(Xn.shape[0]): 328 | Xn[i, :] = Xn[i] / m[i] 329 | return fold(Xn, n, X.shape) 330 | 331 | 332 | # TODO more efficient cython implementation 333 | def khatrirao(A, reverse=False): 334 | """ 335 | Compute the columnwise Khatri-Rao product. 336 | 337 | Parameters 338 | ---------- 339 | A : tuple of ndarrays 340 | Matrices for which the columnwise Khatri-Rao product should be computed 341 | 342 | reverse : boolean 343 | Compute Khatri-Rao product in reverse order 344 | 345 | Examples 346 | -------- 347 | >>> A = np.random.randn(5, 2) 348 | >>> B = np.random.randn(4, 2) 349 | >>> C = khatrirao((A, B)) 350 | >>> C.shape 351 | (20, 2) 352 | >>> (C[:, 0] == np.kron(A[:, 0], B[:, 0])).all() 353 | true 354 | >>> (C[:, 1] == np.kron(A[:, 1], B[:, 1])).all() 355 | true 356 | """ 357 | 358 | if not isinstance(A, tuple): 359 | raise ValueError('A must be a tuple of array likes') 360 | N = A[0].shape[1] 361 | M = 1 362 | for i in range(len(A)): 363 | if A[i].ndim != 2: 364 | raise ValueError('A must be a tuple of matrices (A[%d].ndim = %d)' % (i, A[i].ndim)) 365 | elif N != A[i].shape[1]: 366 | raise ValueError('All matrices must have same number of columns') 367 | M *= A[i].shape[0] 368 | matorder = arange(len(A)) 369 | if reverse: 370 | matorder = matorder[::-1] 371 | # preallocate 372 | P = np.zeros((M, N), dtype=A[0].dtype) 373 | for n in range(N): 374 | ab = A[matorder[0]][:, n] 375 | for j in range(1, len(matorder)): 376 | ab = np.kron(ab, A[matorder[j]][:, n]) 377 | P[:, n] = ab 378 | return P 379 | 380 | 381 | def teneye(dim, order): 382 | """ 383 | Create tensor with superdiagonal all one, rest zeros 384 | """ 385 | I = zeros(dim ** order) 386 | for f in range(dim): 387 | idd = f 388 | for i in range(1, order): 389 | idd = idd + dim ** (i - 1) * (f - 1) 390 | I[idd] = 1 391 | return I.reshape(ones(order) * dim) 392 | 393 | 394 | def tvecmat(m, n): 395 | d = m * n 396 | i2 = arange(d).reshape(m, n).T.flatten() 397 | Tmn = zeros((d, d)) 398 | Tmn[arange(d), i2] = 1 399 | return Tmn 400 | 401 | #i = arange(d); 402 | #rI = m * (i-1)-(m*n-1) * floor((i-1)/n) 403 | #print rI 404 | #I1s = s2i((d,d), rI, arange(d)) 405 | #print I1s 406 | #Tmn[I1s] = 1 407 | #return Tmn.reshape((d,d)).T 408 | 409 | # vim: set et: 410 | -------------------------------------------------------------------------------- /sktensor/cp.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (C) 2013 Maximilian Nickel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """ 17 | This module holds diffent algorithms to compute the CP decomposition, i.e. 18 | algorithms where 19 | 20 | .. math:: \\ten{X} \\approx \sum_{r=1}^{rank} \\vec{u}_r^{(1)} \outer \cdots \outer \\vec{u}_r^{(N)} 21 | 22 | """ 23 | import logging 24 | import time 25 | import numpy as np 26 | from numpy import array, dot, ones, sqrt 27 | from scipy.linalg import pinv 28 | from numpy.random import rand 29 | from .core import nvecs, norm 30 | from .ktensor import ktensor 31 | 32 | _log = logging.getLogger('CP') 33 | _DEF_MAXITER = 500 34 | _DEF_INIT = 'nvecs' 35 | _DEF_CONV = 1e-5 36 | _DEF_FIT_METHOD = 'full' 37 | _DEF_TYPE = np.float 38 | 39 | __all__ = [ 40 | 'als', 41 | 'opt', 42 | 'wopt' 43 | ] 44 | 45 | 46 | def als(X, rank, **kwargs): 47 | """ 48 | Alternating least-sqaures algorithm to compute the CP decomposition. 49 | 50 | Parameters 51 | ---------- 52 | X : tensor_mixin 53 | The tensor to be decomposed. 54 | rank : int 55 | Tensor rank of the decomposition. 56 | init : {'random', 'nvecs'}, optional 57 | The initialization method to use. 58 | - random : Factor matrices are initialized randomly. 59 | - nvecs : Factor matrices are initialzed via HOSVD. 60 | (default 'nvecs') 61 | max_iter : int, optional 62 | Maximium number of iterations of the ALS algorithm. 63 | (default 500) 64 | fit_method : {'full', None} 65 | The method to compute the fit of the factorization 66 | - 'full' : Compute least-squares fit of the dense approximation of. 67 | X and X. 68 | - None : Do not compute the fit of the factorization, but iterate 69 | until ``max_iter`` (Useful for large-scale tensors). 70 | (default 'full') 71 | conv : float 72 | Convergence tolerance on difference of fit between iterations 73 | (default 1e-5) 74 | 75 | Returns 76 | ------- 77 | P : ktensor 78 | Rank ``rank`` factorization of X. ``P.U[i]`` corresponds to the factor 79 | matrix for the i-th mode. ``P.lambda[i]`` corresponds to the weight 80 | of the i-th mode. 81 | fit : float 82 | Fit of the factorization compared to ``X`` 83 | itr : int 84 | Number of iterations that were needed until convergence 85 | exectimes : ndarray of floats 86 | Time needed for each single iteration 87 | 88 | Examples 89 | -------- 90 | Create random dense tensor 91 | 92 | >>> from sktensor import dtensor, ktensor 93 | >>> U = [np.random.rand(i,3) for i in (20, 10, 14)] 94 | >>> T = dtensor(ktensor(U).toarray()) 95 | 96 | Compute rank-3 CP decomposition of ``T`` with ALS 97 | 98 | >>> P, fit, itr, _ = als(T, 3) 99 | 100 | Result is a decomposed tensor stored as a Kruskal operator 101 | 102 | >>> type(P) 103 | 104 | 105 | Factorization should be close to original data 106 | 107 | >>> np.allclose(T, P.totensor()) 108 | True 109 | 110 | References 111 | ---------- 112 | .. [1] Kolda, T. G. & Bader, B. W. 113 | Tensor Decompositions and Applications. 114 | SIAM Rev. 51, 455–500 (2009). 115 | .. [2] Harshman, R. A. 116 | Foundations of the PARAFAC procedure: models and conditions for an 'explanatory' multimodal factor analysis. 117 | UCLA Working Papers in Phonetics 16, (1970). 118 | .. [3] Carroll, J. D., Chang, J. J. 119 | Analysis of individual differences in multidimensional scaling via an N-way generalization of 'Eckart-Young' decomposition. 120 | Psychometrika 35, 283–319 (1970). 121 | """ 122 | 123 | # init options 124 | ainit = kwargs.pop('init', _DEF_INIT) 125 | maxiter = kwargs.pop('max_iter', _DEF_MAXITER) 126 | fit_method = kwargs.pop('fit_method', _DEF_FIT_METHOD) 127 | conv = kwargs.pop('conv', _DEF_CONV) 128 | dtype = kwargs.pop('dtype', _DEF_TYPE) 129 | if not len(kwargs) == 0: 130 | raise ValueError('Unknown keywords (%s)' % (kwargs.keys())) 131 | 132 | N = X.ndim 133 | normX = norm(X) 134 | 135 | U = _init(ainit, X, N, rank, dtype) 136 | fit = 0 137 | exectimes = [] 138 | for itr in range(maxiter): 139 | tic = time.clock() 140 | fitold = fit 141 | 142 | for n in range(N): 143 | Unew = X.uttkrp(U, n) 144 | Y = ones((rank, rank), dtype=dtype) 145 | for i in (list(range(n)) + list(range(n + 1, N))): 146 | Y = Y * dot(U[i].T, U[i]) 147 | Unew = Unew.dot(pinv(Y)) 148 | # Normalize 149 | if itr == 0: 150 | lmbda = sqrt((Unew ** 2).sum(axis=0)) 151 | else: 152 | lmbda = Unew.max(axis=0) 153 | lmbda[lmbda < 1] = 1 154 | U[n] = Unew / lmbda 155 | 156 | P = ktensor(U, lmbda) 157 | if fit_method == 'full': 158 | normresidual = normX ** 2 + P.norm() ** 2 - 2 * P.innerprod(X) 159 | fit = 1 - (normresidual / normX ** 2) 160 | else: 161 | fit = itr 162 | fitchange = abs(fitold - fit) 163 | exectimes.append(time.clock() - tic) 164 | _log.debug( 165 | '[%3d] fit: %.5f | delta: %7.1e | secs: %.5f' % 166 | (itr, fit, fitchange, exectimes[-1]) 167 | ) 168 | if itr > 0 and fitchange < conv: 169 | break 170 | 171 | return P, fit, itr, array(exectimes) 172 | 173 | 174 | def opt(X, rank, **kwargs): 175 | ainit = kwargs.pop('init', _DEF_INIT) 176 | maxiter = kwargs.pop('maxIter', _DEF_MAXITER) 177 | conv = kwargs.pop('conv', _DEF_CONV) 178 | dtype = kwargs.pop('dtype', _DEF_TYPE) 179 | if not len(kwargs) == 0: 180 | raise ValueError('Unknown keywords (%s)' % (kwargs.keys())) 181 | 182 | N = X.ndim 183 | U = _init(ainit, X, N, rank, dtype) 184 | 185 | 186 | def wopt(X, rank, **kwargs): 187 | raise NotImplementedError() 188 | 189 | 190 | def _init(init, X, N, rank, dtype): 191 | """ 192 | Initialization for CP models 193 | """ 194 | Uinit = [None for _ in range(N)] 195 | if isinstance(init, list): 196 | Uinit = init 197 | elif init == 'random': 198 | for n in range(1, N): 199 | Uinit[n] = array(rand(X.shape[n], rank), dtype=dtype) 200 | elif init == 'nvecs': 201 | for n in range(1, N): 202 | Uinit[n] = array(nvecs(X, n, rank), dtype=dtype) 203 | else: 204 | raise 'Unknown option (init=%s)' % str(init) 205 | return Uinit 206 | 207 | # vim: set et: 208 | -------------------------------------------------------------------------------- /sktensor/dedicom.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Maximilian Nickel 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import logging 17 | import time 18 | import numpy as np 19 | from numpy import dot, ones, zeros, diag, kron, outer, array, prod, eye 20 | from numpy.linalg import norm, solve, eigvals 21 | from numpy.random import rand 22 | from scipy.linalg import qr 23 | from scipy.sparse.linalg import eigsh 24 | from scipy.optimize import fmin_l_bfgs_b, fmin_ncg, fmin_tnc 25 | from scipy.sparse import issparse 26 | 27 | _DEF_MAXITER = 500 28 | _DEF_INIT = 'nvecs' 29 | _DEF_PROJ = True 30 | _DEF_CONV = 1e-5 31 | _DEF_NNE = -1 32 | _DEF_OPTFUNC = 'lbfgs' 33 | 34 | _log = logging.getLogger('DEDICOM') 35 | np.seterr(invalid='raise') 36 | 37 | 38 | def asalsan(X, rank, **kwargs): 39 | """ 40 | ASALSAN algorithm to compute the three-way DEDICOM decomposition 41 | of a tensor 42 | 43 | See 44 | --- 45 | .. [1] Brett W. Bader, Richard A. Harshman, Tamara G. Kolda 46 | "Temporal analysis of semantic graphs using ASALSAN" 47 | 7th International Conference on Data Mining, 2007 48 | 49 | .. [2] Brett W. Bader, Richard A. Harshman, Tamara G. Kolda 50 | "Temporal analysis of Social Networks using Three-way DEDICOM" 51 | Technical Report, 2006 52 | """ 53 | # init options 54 | ainit = kwargs.pop('init', _DEF_INIT) 55 | proj = kwargs.pop('proj', _DEF_PROJ) 56 | maxIter = kwargs.pop('maxIter', _DEF_MAXITER) 57 | conv = kwargs.pop('conv', _DEF_CONV) 58 | nne = kwargs.pop('nne', _DEF_NNE) 59 | optfunc = kwargs.pop('optfunc', _DEF_OPTFUNC) 60 | if not len(kwargs) == 0: 61 | raise BaseException('Unknown keywords (%s)' % (kwargs.keys())) 62 | 63 | # init starting points 64 | D = ones((len(X), rank)) 65 | sz = X[0].shape 66 | n = sz[0] 67 | R = rand(rank, rank) 68 | if ainit == 'random': 69 | A = rand(n, rank) 70 | elif ainit == 'nvecs': 71 | S = zeros((n, n)) 72 | T = zeros((n, n)) 73 | for i in range(len(X)): 74 | T = X[i] 75 | S = S + T + T.T 76 | evals, A = eigsh(S, rank) 77 | if nne > 0: 78 | A[A < 0] = 0 79 | if proj: 80 | Q, A2 = qr(A) 81 | X2 = __projectSlices(X, Q) 82 | R = __updateR(X2, A2, D, R, nne) 83 | else: 84 | R = __updateR(X, A, D, R, nne) 85 | elif isinstance(ainit, np.ndarray): 86 | A = ainit 87 | else: 88 | raise 'Unknown init option ("%s")' % ainit 89 | 90 | # perform decomposition 91 | if issparse(X[0]): 92 | normX = [norm(M.data) ** 2 for M in X] 93 | Xflat = [M.tolil().reshape((1, prod(M.shape))).tocsr() for M in X] 94 | else: 95 | normX = [norm(M) ** 2 for M in X] 96 | Xflat = [M.flatten() for M in X] 97 | M = zeros((n, n)) 98 | normXSum = sum(normX) 99 | #normX = norm(X)**2 100 | fit = fitold = f = fitchange = 0 101 | exectimes = [] 102 | for iters in xrange(maxIter): 103 | tic = time.clock() 104 | fitold = fit 105 | A = __updateA(X, A, D, R, nne) 106 | if proj: 107 | Q, A2 = qr(A) 108 | X2 = __projectSlices(X, Q) 109 | R = __updateR(X2, A2, D, R, nne) 110 | D, f = __updateD(X2, A2, D, R, nne, optfunc) 111 | else: 112 | R = __updateR(X, A, D, R, nne) 113 | D, f = __updateD(X, A, D, R, nne, optfunc) 114 | 115 | # compute fit 116 | f = 0 117 | for i in xrange(len(X)): 118 | AD = dot(A, diag(D[i, :])) 119 | M = dot(dot(AD, R), AD.T) 120 | f += normX[i] + norm(M) ** 2 - 2 * Xflat[i].dot(M.flatten()) 121 | f *= 0.5 122 | fit = 1 - (f / normXSum) 123 | fitchange = abs(fitold - fit) 124 | 125 | exectimes.append(time.clock() - tic) 126 | 127 | # print iter info when debugging is enabled 128 | _log.debug('[%3d] fit: %.5f | delta: %7.1e | secs: %.5f' % ( 129 | iters, fit, fitchange, exectimes[-1] 130 | )) 131 | 132 | if iters > 1 and fitchange < conv: 133 | break 134 | return A, R, D, fit, iters, array(exectimes) 135 | 136 | 137 | def __updateA(X, A, D, R, nne): 138 | rank = A.shape[1] 139 | F = zeros((X[0].shape[0], rank)) 140 | E = zeros((rank, rank)) 141 | 142 | AtA = dot(A.T, A) 143 | for i in range(len(X)): 144 | Dk = diag(D[i, :]) 145 | DRD = dot(Dk, dot(R, Dk)) 146 | DRtD = DRD.T 147 | F += X[i].dot(dot(A, DRtD)) + X[i].T.dot(dot(A, DRD)) 148 | E += dot(DRD, dot(AtA, DRtD)) + dot(DRtD, dot(AtA, DRD)) 149 | if nne > 0: 150 | E = dot(A, E) + nne 151 | A = A * (F / E) 152 | else: 153 | A = solve(E.T, F.T).T 154 | return A 155 | 156 | 157 | def __updateR(X, A, D, R, nne): 158 | r = A.shape[1] ** 2 159 | T = zeros((r, r)) 160 | t = zeros(r) 161 | for i in range(len(X)): 162 | AD = dot(A, diag(D[i, :])) 163 | ADt = AD.T 164 | tmp = dot(ADt, AD) 165 | T = T + kron(tmp, tmp) 166 | tmp = dot(ADt, X[i].dot(AD)) 167 | t = t + tmp.flatten() 168 | r = A.shape[1] 169 | if nne > 0: 170 | Rflat = R.flatten() 171 | T = dot(T, Rflat) + nne 172 | R = (Rflat * t / T).reshape(r, r) 173 | else: 174 | # TODO check if this is correct 175 | R = solve(T, t).reshape(r, r) 176 | #R = (pinv(T + eye(r ** 2)).dot(t)).reshape(r, r) 177 | return R 178 | 179 | 180 | def __updateD(X, A, D, R, nne, optfunc): 181 | f = 0 182 | for i in range(len(X)): 183 | d = D[i, :] 184 | u = Updater(X[i], A, R) 185 | if nne > 0: 186 | bounds = len(d) * [(0, None)] 187 | res = fmin_l_bfgs_b( 188 | u.updateD_F, d, u.updateD_G, factr=1e12, bounds=bounds 189 | ) 190 | else: 191 | if optfunc == 'lbfgs': 192 | res = fmin_l_bfgs_b(u.updateD_F, d, u.updateD_G, factr=1e12) 193 | D[i, :] = res[0] 194 | f += res[1] 195 | elif optfunc == 'ncg': 196 | res = fmin_ncg( 197 | u.updateD_F, d, u.updateD_G, fhess=u.updateD_H, 198 | full_output=True, disp=False 199 | ) 200 | # TODO: check return value of ncg and update D, f 201 | raise NotImplementedError() 202 | elif optfunc == 'tnc': 203 | res = fmin_tnc(u.updateD_F, d, u.updateD_G, disp=False) 204 | # TODO: check return value of tnc and update D, f 205 | raise NotImplementedError() 206 | return D, f 207 | 208 | 209 | class Updater: 210 | def __init__(self, Z, A, R): 211 | self.Z = Z 212 | self.A = A 213 | self.R = R 214 | self.x = None 215 | 216 | def precompute(self, x, cache=True): 217 | if not cache or self.x is None or (x != self.x).any(): 218 | self.AD = dot(self.A, diag(x)) 219 | self.ADt = self.AD.T 220 | self.E = self.Z - dot(self.AD, dot(self.R, self.ADt)) 221 | 222 | def updateD_F(self, x): 223 | self.precompute(x) 224 | return norm(self.E, 'fro') ** 2 225 | 226 | def updateD_G(self, x): 227 | """ 228 | Compute Gradient for update of D 229 | 230 | See [2] for derivation of Gradient 231 | """ 232 | self.precompute(x) 233 | g = zeros(len(x)) 234 | Ai = zeros(self.A.shape[0]) 235 | for i in range(len(g)): 236 | Ai = self.A[:, i] 237 | g[i] = (self.E * (dot(self.AD, outer(self.R[:, i], Ai)) + 238 | dot(outer(Ai, self.R[i, :]), self.ADt))).sum() 239 | return -2 * g 240 | 241 | def updateD_H(self, x): 242 | """ 243 | Compute Hessian for update of D 244 | 245 | See [2] for derivation of Hessian 246 | """ 247 | self.precompute(x) 248 | H = zeros((len(x), len(x))) 249 | Ai = zeros(self.A.shape[0]) 250 | Aj = zeros(Ai.shape) 251 | for i in range(len(x)): 252 | Ai = self.A[:, i] 253 | ti = dot(self.AD, outer(self.R[:, i], Ai)) + dot(outer(Ai, self.R[i, :]), self.ADt) 254 | 255 | for j in range(i, len(x)): 256 | Aj = self.A[:, j] 257 | tj = outer(Ai, Aj) 258 | H[i, j] = ( 259 | self.E * (self.R[i, j] * tj + self.R[j, i] * tj.T) - 260 | ti * ( 261 | dot(self.AD, outer(self.R[:, j], Aj)) + 262 | dot(outer(Aj, self.R[j, :]), self.ADt) 263 | ) 264 | ).sum() 265 | H[j, i] = H[i, j] 266 | H *= -2 267 | e = eigvals(H).min() 268 | H = H + (eye(H.shape[0]) * e) 269 | return H 270 | 271 | 272 | def __projectSlices(X, Q): 273 | X2 = [] 274 | for i in range(len(X)): 275 | X2.append(Q.T.dot(X[i].dot(Q))) 276 | return X2 277 | -------------------------------------------------------------------------------- /sktensor/dtensor.py: -------------------------------------------------------------------------------- 1 | # sktensor.dtensor - base class for dense tensors 2 | # Copyright (C) 2013 Maximilian Nickel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import numpy as np 18 | from numpy import array, prod, argsort 19 | from .core import tensor_mixin, khatrirao 20 | from .pyutils import inherit_docstring_from, from_to_without 21 | 22 | 23 | __all__ = [ 24 | 'dtensor', 25 | 'unfolded_dtensor', 26 | ] 27 | 28 | 29 | class dtensor(tensor_mixin, np.ndarray): 30 | """ 31 | Class to store **dense** tensors 32 | 33 | Parameters 34 | ---------- 35 | input_array : np.ndarray 36 | Multidimenional numpy array which holds the entries of the tensor 37 | 38 | Examples 39 | -------- 40 | Create dense tensor from numpy array 41 | 42 | >>> T = np.zeros((3, 4, 2)) 43 | >>> T[:, :, 0] = [[ 1, 4, 7, 10], [ 2, 5, 8, 11], [3, 6, 9, 12]] 44 | >>> T[:, :, 1] = [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]] 45 | >>> T = dtensor(T) 46 | """ 47 | 48 | def __new__(cls, input_array): 49 | obj = np.asarray(input_array).view(cls) 50 | return obj 51 | 52 | def __array_wrap__(self, out_arr, context=None): 53 | return np.ndarray.__array_wrap__(self, out_arr, context) 54 | 55 | def __eq__(self, other): 56 | return np.equal(self, other) 57 | 58 | def _ttm_compute(self, V, mode, transp): 59 | sz = array(self.shape) 60 | r1, r2 = from_to_without(0, self.ndim, mode, separate=True) 61 | #r1 = list(range(0, mode)) 62 | #r2 = list(range(mode + 1, self.ndim)) 63 | order = [mode] + r1 + r2 64 | newT = self.transpose(axes=order) 65 | newT = newT.reshape(sz[mode], prod(sz[r1 + list(range(mode + 1, len(sz)))])) 66 | if transp: 67 | newT = V.T.dot(newT) 68 | p = V.shape[1] 69 | else: 70 | newT = V.dot(newT) 71 | p = V.shape[0] 72 | newsz = [p] + list(sz[:mode]) + list(sz[mode + 1:]) 73 | newT = newT.reshape(newsz) 74 | # transpose + argsort(order) equals ipermute 75 | newT = newT.transpose(argsort(order)) 76 | return dtensor(newT) 77 | 78 | def _ttv_compute(self, v, dims, vidx, remdims): 79 | """ 80 | Tensor times vector product 81 | 82 | Parameter 83 | --------- 84 | """ 85 | if not isinstance(v, tuple): 86 | raise ValueError('v must be a tuple of vectors') 87 | ndim = self.ndim 88 | order = list(remdims) + list(dims) 89 | if ndim > 1: 90 | T = self.transpose(order) 91 | sz = array(self.shape)[order] 92 | for i in np.arange(len(dims), 0, -1): 93 | T = T.reshape((sz[:ndim - 1].prod(), sz[ndim - 1])) 94 | T = T.dot(v[vidx[i - 1]]) 95 | ndim -= 1 96 | if ndim > 0: 97 | T = T.reshape(sz[:ndim]) 98 | return T 99 | 100 | def ttt(self, other, modes=None): 101 | pass 102 | 103 | def unfold(self, mode): 104 | """ 105 | Unfolds a dense tensor in mode n. 106 | 107 | Parameters 108 | ---------- 109 | mode : int 110 | Mode in which tensor is unfolded 111 | 112 | Returns 113 | ------- 114 | unfolded_dtensor : unfolded_dtensor object 115 | Tensor unfolded along mode 116 | 117 | Examples 118 | -------- 119 | Create dense tensor from numpy array 120 | 121 | >>> T = np.zeros((3, 4, 2)) 122 | >>> T[:, :, 0] = [[ 1, 4, 7, 10], [ 2, 5, 8, 11], [3, 6, 9, 12]] 123 | >>> T[:, :, 1] = [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]] 124 | >>> T = dtensor(T) 125 | 126 | Unfolding of dense tensors 127 | 128 | >>> T.unfold(0) 129 | array([[ 1., 4., 7., 10., 13., 16., 19., 22.], 130 | [ 2., 5., 8., 11., 14., 17., 20., 23.], 131 | [ 3., 6., 9., 12., 15., 18., 21., 24.]]) 132 | >>> T.unfold(1) 133 | array([[ 1., 2., 3., 13., 14., 15.], 134 | [ 4., 5., 6., 16., 17., 18.], 135 | [ 7., 8., 9., 19., 20., 21.], 136 | [ 10., 11., 12., 22., 23., 24.]]) 137 | >>> T.unfold(2) 138 | array([[ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 139 | 12.], 140 | [ 13., 14., 15., 16., 17., 18., 19., 20., 21., 22., 23., 141 | 24.]]) 142 | """ 143 | 144 | sz = array(self.shape) 145 | N = len(sz) 146 | order = ([mode], from_to_without(N - 1, -1, mode, step=-1, skip=-1)) 147 | newsz = (sz[order[0]][0], prod(sz[order[1]])) 148 | arr = self.transpose(axes=(order[0] + order[1])) 149 | arr = arr.reshape(newsz) 150 | return unfolded_dtensor(arr, mode, self.shape) 151 | 152 | def norm(self): 153 | """ 154 | Computes the Frobenius norm for dense tensors 155 | :math:`norm(X) = \sqrt{\sum_{i_1,\ldots,i_N} x_{i_1,\ldots,i_N}^2}` 156 | 157 | References 158 | ---------- 159 | [Kolda and Bader, 2009; p.457] 160 | """ 161 | return np.linalg.norm(self) 162 | 163 | @inherit_docstring_from(tensor_mixin) 164 | def uttkrp(self, U, n): 165 | order = list(range(n)) + list(range(n + 1, self.ndim)) 166 | Z = khatrirao(tuple(U[i] for i in order), reverse=True) 167 | return self.unfold(n).dot(Z) 168 | 169 | @inherit_docstring_from(tensor_mixin) 170 | def transpose(self, axes=None): 171 | return dtensor(np.transpose(array(self), axes=axes)) 172 | 173 | 174 | class unfolded_dtensor(np.ndarray): 175 | 176 | def __new__(cls, input_array, mode, ten_shape): 177 | obj = np.asarray(input_array).view(cls) 178 | obj.ten_shape = ten_shape 179 | obj.mode = mode 180 | return obj 181 | 182 | def __array_finalize__(self, obj): 183 | if obj is None: 184 | return 185 | self.ten_shape = getattr(obj, 'ten_shape', None) 186 | self.mode = getattr(obj, 'mode', None) 187 | 188 | def fold(self): 189 | shape = array(self.ten_shape) 190 | N = len(shape) 191 | order = ([self.mode], from_to_without(0, N, self.mode, reverse=True)) 192 | arr = self.reshape(tuple(shape[order[0]],) + tuple(shape[order[1]])) 193 | arr = np.transpose(arr, argsort(order[0] + order[1])) 194 | return dtensor(arr) 195 | -------------------------------------------------------------------------------- /sktensor/indscal.py: -------------------------------------------------------------------------------- 1 | from numpy import zeros, dot, diag 2 | from numpy.random import rand 3 | from scipy.linalg import svd, norm, orth 4 | from scipy.sparse.linalg import eigsh 5 | import time 6 | import logging 7 | 8 | _log = logging.getLogger('INDSCAL') 9 | 10 | _DEF_MAXITER = 50 11 | _DEF_INIT = 'random' 12 | _DEF_CONV = 1e-7 13 | 14 | 15 | def orth_als(X, ncomp, **kwargs): 16 | 17 | ainit = kwargs.pop('init', _DEF_INIT) 18 | maxiter = kwargs.pop('max_iter', _DEF_MAXITER) 19 | conv = kwargs.pop('conv', _DEF_CONV) 20 | if not len(kwargs) == 0: 21 | raise ValueError('Unknown keywords (%s)' % (kwargs.keys())) 22 | 23 | K = len(X) 24 | normX = sum([norm(Xk)**2 for Xk in X]) 25 | 26 | A = init(X, ainit, ncomp) 27 | fit = 0 28 | exectimes = [] 29 | for itr in range(maxiter): 30 | tic = time.time() 31 | fitold = fit 32 | D = _updateD(X, A) 33 | A = _updateA(X, A, D) 34 | 35 | fit = sum([norm(X[k] - dot(A, dot(diag(D[k, :]), A.T)))**2 for k in range(K)]) 36 | fit = 1 - fit / normX 37 | fitchange = abs(fitold - fit) 38 | 39 | exectimes.append(time.time() - tic) 40 | _log.info('[%3d] fit: %0.5f | delta: %7.1e | secs: %.5f' % ( 41 | itr, fit, fitchange, exectimes[-1] 42 | )) 43 | if itr > 0 and fitchange < conv: 44 | break 45 | return A, D 46 | 47 | 48 | def _updateA(X, A, D): 49 | G = zeros(A.shape) 50 | for k in range(len(X)): 51 | G = G + dot(X[k], dot(A, diag(D[k, :]))) 52 | U, _, Vt = svd(G, full_matrices=0) 53 | A = dot(U, Vt) 54 | return A 55 | 56 | 57 | def _updateD(X, A): 58 | K, R = len(X), A.shape[1] 59 | D = zeros((K, R)) 60 | for k in range(K): 61 | D[k, :] = diag(dot(A.T, dot(X[k], A))) 62 | D[D < 0] = 0 63 | return D 64 | 65 | 66 | def init(X, init, ncomp): 67 | N, K = X[0].shape[0], len(X) 68 | if init == 'random': 69 | A = orth(rand(N, ncomp)) 70 | elif init == 'nvecs': 71 | S = zeros(N, N) 72 | for k in range(K): 73 | S = S + X[k] + X[k].T 74 | _, A = eigsh(S, ncomp) 75 | return A 76 | -------------------------------------------------------------------------------- /sktensor/ktensor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Maximilian Nickel 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import numpy as np 17 | from numpy import dot, ones, array, outer, zeros, prod, sum 18 | from sktensor.core import khatrirao, tensor_mixin 19 | from sktensor.dtensor import dtensor 20 | 21 | __all__ = [ 22 | 'ktensor', 23 | 'vectorized_ktensor', 24 | ] 25 | 26 | 27 | class ktensor(object): 28 | """ 29 | Tensor stored in decomposed form as a Kruskal operator. 30 | 31 | Intended Usage 32 | The Kruskal operator is particularly useful to store 33 | the results of a CP decomposition. 34 | 35 | Parameters 36 | ---------- 37 | U : list of ndarrays 38 | Factor matrices from which the tensor representation 39 | is created. All factor matrices ``U[i]`` must have the 40 | same number of columns, but can have different 41 | number of rows. 42 | lmbda : array_like of floats, optional 43 | Weights for each dimension of the Kruskal operator. 44 | ``len(lambda)`` must be equal to ``U[i].shape[1]`` 45 | 46 | See also 47 | -------- 48 | sktensor.dtensor : Dense tensors 49 | sktensor.sptensor : Sparse tensors 50 | sktensor.ttensor : Tensors stored in form of the Tucker operator 51 | 52 | References 53 | ---------- 54 | .. [1] B.W. Bader, T.G. Kolda 55 | Efficient Matlab Computations With Sparse and Factored Tensors 56 | SIAM J. Sci. Comput, Vol 30, No. 1, pp. 205--231, 2007 57 | """ 58 | 59 | def __init__(self, U, lmbda=None): 60 | self.U = U 61 | self.shape = tuple(Ui.shape[0] for Ui in U) 62 | self.ndim = len(self.shape) 63 | self.rank = U[0].shape[1] 64 | self.lmbda = lmbda 65 | if not all(array([Ui.shape[1] for Ui in U]) == self.rank): 66 | raise ValueError('Dimension mismatch of factor matrices') 67 | if lmbda is None: 68 | self.lmbda = ones(self.rank) 69 | 70 | def __eq__(self, other): 71 | if isinstance(other, ktensor): 72 | # avoid costly elementwise comparison for obvious cases 73 | if self.ndim != other.ndim or self.shape != other.shape: 74 | return False 75 | # do elementwise comparison 76 | return all( 77 | [(self.U[i] == other.U[i]).all() for i in range(self.ndim)] + 78 | [(self.lmbda == other.lmbda).all()] 79 | ) 80 | else: 81 | # TODO implement __eq__ for tensor_mixins and ndarrays 82 | raise NotImplementedError() 83 | 84 | def uttkrp(self, U, mode): 85 | 86 | """ 87 | Unfolded tensor times Khatri-Rao product for Kruskal tensors 88 | 89 | Parameters 90 | ---------- 91 | X : tensor_mixin 92 | Tensor whose unfolding should be multiplied. 93 | U : list of array_like 94 | Matrices whose Khatri-Rao product should be multiplied. 95 | mode : int 96 | Mode in which X should be unfolded. 97 | 98 | See also 99 | -------- 100 | sktensor.sptensor.uttkrp : Efficient computation of uttkrp for sparse tensors 101 | ttensor.uttkrp : Efficient computation of uttkrp for Tucker operators 102 | """ 103 | N = self.ndim 104 | if mode == 1: 105 | R = U[1].shape[1] 106 | else: 107 | R = U[0].shape[1] 108 | W = np.tile(self.lmbda, 1, R) 109 | for i in range(mode) + range(mode + 1, N): 110 | W = W * dot(self.U[i].T, U[i]) 111 | return dot(self.U[mode], W) 112 | 113 | def norm(self): 114 | """ 115 | Efficient computation of the Frobenius norm for ktensors 116 | 117 | Returns 118 | ------- 119 | norm : float 120 | Frobenius norm of the ktensor 121 | """ 122 | N = len(self.shape) 123 | coef = outer(self.lmbda, self.lmbda) 124 | for i in range(N): 125 | coef = coef * dot(self.U[i].T, self.U[i]) 126 | return np.sqrt(coef.sum()) 127 | 128 | def innerprod(self, X): 129 | """ 130 | Efficient computation of the inner product of a ktensor with another tensor 131 | 132 | Parameters 133 | ---------- 134 | X : tensor_mixin 135 | Tensor to compute the inner product with. 136 | 137 | Returns 138 | ------- 139 | p : float 140 | Inner product between ktensor and X. 141 | """ 142 | N = len(self.shape) 143 | R = len(self.lmbda) 144 | res = 0 145 | for r in range(R): 146 | vecs = [] 147 | for n in range(N): 148 | vecs.append(self.U[n][:, r]) 149 | res += self.lmbda[r] * X.ttv(tuple(vecs)) 150 | return res 151 | 152 | def toarray(self): 153 | """ 154 | Converts a ktensor into a dense multidimensional ndarray 155 | 156 | Returns 157 | ------- 158 | arr : np.ndarray 159 | Fully computed multidimensional array whose shape matches 160 | the original ktensor. 161 | """ 162 | A = dot(self.lmbda, khatrirao(tuple(self.U)).T) 163 | return A.reshape(self.shape) 164 | 165 | def totensor(self): 166 | """ 167 | Converts a ktensor into a dense tensor 168 | 169 | Returns 170 | ------- 171 | arr : dtensor 172 | Fully computed multidimensional array whose shape matches 173 | the original ktensor. 174 | """ 175 | return dtensor(self.toarray()) 176 | 177 | def tovec(self): 178 | v = zeros(sum([s * self.rank for s in self.shape])) 179 | offset = 0 180 | for M in self.U: 181 | noff = offset + prod(M.shape) 182 | v[offset:noff] = M.flatten() 183 | offset = noff 184 | return vectorized_ktensor(v, self.shape, self.lmbda) 185 | 186 | 187 | class vectorized_ktensor(object): 188 | 189 | def __init__(self, v, shape, lmbda): 190 | self.v = v 191 | self.shape = shape 192 | self.lmbda = lmbda 193 | 194 | def toktensor(self): 195 | order = len(self.shape) 196 | rank = len(self.v) / sum(self.shape) 197 | U = [None for _ in range(order)] 198 | offset = 0 199 | for i in range(order): 200 | noff = offset + self.shape[i] * rank 201 | U[i] = self.v[offset:noff].reshape((self.shape[i], rank)) 202 | offset = noff 203 | return ktensor(U, self.lmbda) 204 | 205 | # vim: set et: 206 | -------------------------------------------------------------------------------- /sktensor/pyutils.py: -------------------------------------------------------------------------------- 1 | def inherit_docstring_from(cls): 2 | def docstring_inheriting_decorator(fn): 3 | fn.__doc__ = getattr(cls, fn.__name__).__doc__ 4 | return fn 5 | return docstring_inheriting_decorator 6 | 7 | 8 | def is_sequence(obj): 9 | """ 10 | Helper function to determine sequences 11 | across Python 2.x and 3.x 12 | """ 13 | try: 14 | from collections import Sequence 15 | except ImportError: 16 | from operator import isSequenceType 17 | return isSequenceType(obj) 18 | else: 19 | return isinstance(obj, Sequence) 20 | 21 | 22 | def is_number(obj): 23 | """ 24 | Helper function to determine numbers 25 | across Python 2.x and 3.x 26 | """ 27 | try: 28 | from numbers import Number 29 | except ImportError: 30 | from operator import isNumberType 31 | return isNumberType(obj) 32 | else: 33 | return isinstance(obj, Number) 34 | 35 | 36 | def func_attr(f, attr): 37 | """ 38 | Helper function to get the attribute of a function 39 | like, name, code, defaults across Python 2.x and 3.x 40 | """ 41 | if hasattr(f, 'func_%s' % attr): 42 | return getattr(f, 'func_%s' % attr) 43 | elif hasattr(f, '__%s__' % attr): 44 | return getattr(f, '__%s__' % attr) 45 | else: 46 | raise ValueError('Object %s has no attr' % (str(f), attr)) 47 | 48 | 49 | def from_to_without(frm, to, without, step=1, skip=1, reverse=False, separate=False): 50 | """ 51 | Helper function to create ranges with missing entries 52 | """ 53 | if reverse: 54 | frm, to = (to - 1), (frm - 1) 55 | step *= -1 56 | skip *= -1 57 | a = list(range(frm, without, step)) 58 | b = list(range(without + skip, to, step)) 59 | if separate: 60 | return a, b 61 | else: 62 | return a + b 63 | -------------------------------------------------------------------------------- /sktensor/rescal.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # rescal.py - python script to compute the RESCAL tensor factorization 3 | # Copyright (C) 2013 Maximilian Nickel 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import logging 19 | import time 20 | import numpy as np 21 | from numpy import dot, zeros, array, eye, kron, prod 22 | from numpy.linalg import norm, solve, inv, svd 23 | from scipy.sparse import csr_matrix, issparse 24 | from scipy.sparse.linalg import eigsh 25 | from numpy.random import rand 26 | 27 | __version__ = "0.5" 28 | __all__ = ['als'] 29 | 30 | _DEF_MAXITER = 100 31 | _DEF_INIT = 'nvecs' 32 | _DEF_CONV = 1e-4 33 | _DEF_LMBDA = 0 34 | _DEF_ATTR = [] 35 | _DEF_NO_FIT = 1e9 36 | _DEF_FIT_METHOD = None 37 | 38 | _log = logging.getLogger('RESCAL') 39 | 40 | 41 | def als(X, rank, **kwargs): 42 | """ 43 | RESCAL-ALS algorithm to compute the RESCAL tensor factorization. 44 | 45 | 46 | Parameters 47 | ---------- 48 | X : list 49 | List of frontal slices X_k of the tensor X. 50 | The shape of each X_k is ('N', 'N'). 51 | X_k's are expected to be instances of scipy.sparse.csr_matrix 52 | rank : int 53 | Rank of the factorization 54 | lmbdaA : float, optional 55 | Regularization parameter for A factor matrix. 0 by default 56 | lmbdaR : float, optional 57 | Regularization parameter for R_k factor matrices. 0 by default 58 | lmbdaV : float, optional 59 | Regularization parameter for V_l factor matrices. 0 by default 60 | attr : list, optional 61 | List of sparse ('N', 'L_l') attribute matrices. 'L_l' may be different 62 | for each attribute 63 | init : string, optional 64 | Initialization method of the factor matrices. 'nvecs' (default) 65 | initializes A based on the eigenvectors of X. 'random' initializes 66 | the factor matrices randomly. 67 | compute_fit : boolean, optional 68 | If true, compute the fit of the factorization compared to X. 69 | For large sparse tensors this should be turned of. None by default. 70 | maxIter : int, optional 71 | Maximium number of iterations of the ALS algorithm. 500 by default. 72 | conv : float, optional 73 | Stop when residual of factorization is less than conv. 1e-5 by default 74 | 75 | Returns 76 | ------- 77 | A : ndarray 78 | array of shape ('N', 'rank') corresponding to the factor matrix A 79 | R : list 80 | list of 'M' arrays of shape ('rank', 'rank') corresponding to the 81 | factor matrices R_k 82 | fval : float 83 | function value of the factorization 84 | itr : int 85 | number of iterations until convergence 86 | exectimes : ndarray 87 | execution times to compute the updates in each iteration 88 | 89 | Examples 90 | -------- 91 | >>> X1 = csr_matrix(([1,1,1], ([2,1,3], [0,2,3])), shape=(4, 4)) 92 | >>> X2 = csr_matrix(([1,1,1,1], ([0,2,3,3], [0,1,2,3])), shape=(4, 4)) 93 | >>> A, R, fval, iter, exectimes = rescal([X1, X2], 2) 94 | 95 | See 96 | --- 97 | For a full description of the algorithm see: 98 | .. [1] Maximilian Nickel, Volker Tresp, Hans-Peter-Kriegel, 99 | "A Three-Way Model for Collective Learning on Multi-Relational Data", 100 | ICML 2011, Bellevue, WA, USA 101 | 102 | .. [2] Maximilian Nickel, Volker Tresp, Hans-Peter-Kriegel, 103 | "Factorizing YAGO: Scalable Machine Learning for Linked Data" 104 | WWW 2012, Lyon, France 105 | """ 106 | 107 | # ------------ init options ---------------------------------------------- 108 | ainit = kwargs.pop('init', _DEF_INIT) 109 | maxIter = kwargs.pop('maxIter', _DEF_MAXITER) 110 | conv = kwargs.pop('conv', _DEF_CONV) 111 | lmbdaA = kwargs.pop('lambda_A', _DEF_LMBDA) 112 | lmbdaR = kwargs.pop('lambda_R', _DEF_LMBDA) 113 | lmbdaV = kwargs.pop('lambda_V', _DEF_LMBDA) 114 | func_compute_fval = kwargs.pop('compute_fval', _DEF_FIT_METHOD) 115 | orthogonalize = kwargs.pop('orthogonalize', False) 116 | P = kwargs.pop('attr', _DEF_ATTR) 117 | dtype = kwargs.pop('dtype', np.float) 118 | 119 | # ------------- check input ---------------------------------------------- 120 | if not len(kwargs) == 0: 121 | raise ValueError('Unknown keywords (%s)' % (kwargs.keys())) 122 | 123 | # check frontal slices have same size and are matrices 124 | sz = X[0].shape 125 | for i in range(len(X)): 126 | if X[i].ndim != 2: 127 | raise ValueError('Frontal slices of X must be matrices') 128 | if X[i].shape != sz: 129 | raise ValueError('Frontal slices of X must be all of same shape') 130 | #if not issparse(X[i]): 131 | #raise ValueError('X[%d] is not a sparse matrix' % i) 132 | 133 | if func_compute_fval is None: 134 | if orthogonalize: 135 | func_compute_fval = _compute_fval_orth 136 | elif prod(X[0].shape) * len(X) > _DEF_NO_FIT: 137 | _log.warn('For large tensors automatic computation of fit is disabled by default\nTo compute the fit, call rescal.als with "compute_fit=True"\nPlease note that this might cause memory and runtime problems') 138 | func_compute_fval = None 139 | else: 140 | func_compute_fval = _compute_fval 141 | 142 | n = sz[0] 143 | k = len(X) 144 | 145 | _log.debug( 146 | '[Config] rank: %d | maxIter: %d | conv: %7.1e | lmbda: %7.1e' % 147 | (rank, maxIter, conv, lmbdaA) 148 | ) 149 | _log.debug('[Config] dtype: %s / %s' % (dtype, X[0].dtype)) 150 | 151 | # ------- convert X and P to CSR ------------------------------------------ 152 | for i in range(k): 153 | if issparse(X[i]): 154 | X[i] = X[i].tocsr() 155 | X[i].sort_indices() 156 | for i in range(len(P)): 157 | if issparse(P[i]): 158 | P[i] = P[i].tocoo().tocsr() 159 | P[i].sort_indices() 160 | 161 | # ---------- initialize A ------------------------------------------------ 162 | _log.debug('Initializing A') 163 | if ainit == 'random': 164 | A = array(rand(n, rank), dtype=dtype) 165 | elif ainit == 'nvecs': 166 | S = csr_matrix((n, n), dtype=dtype) 167 | for i in range(k): 168 | S = S + X[i] 169 | S = S + X[i].T 170 | _, A = eigsh(csr_matrix(S, dtype=dtype, shape=(n, n)), rank) 171 | A = array(A, dtype=dtype) 172 | else: 173 | raise ValueError('Unknown init option ("%s")' % ainit) 174 | 175 | # ------- initialize R and Z --------------------------------------------- 176 | R = _updateR(X, A, lmbdaR) 177 | Z = _updateZ(A, P, lmbdaV) 178 | 179 | # precompute norms of X 180 | normX = [sum(M.data ** 2) for M in X] 181 | 182 | # ------ compute factorization ------------------------------------------ 183 | fit = fitchange = fitold = f = 0 184 | exectimes = [] 185 | for itr in range(maxIter): 186 | tic = time.time() 187 | fitold = fit 188 | A = _updateA(X, A, R, P, Z, lmbdaA, orthogonalize) 189 | R = _updateR(X, A, lmbdaR) 190 | Z = _updateZ(A, P, lmbdaV) 191 | 192 | # compute fit value 193 | if func_compute_fval is not None: 194 | fit = func_compute_fval(X, A, R, P, Z, lmbdaA, lmbdaR, lmbdaV, normX) 195 | else: 196 | fit = np.Inf 197 | 198 | fitchange = abs(fitold - fit) 199 | 200 | toc = time.time() 201 | exectimes.append(toc - tic) 202 | 203 | _log.debug('[%3d] fval: %0.5f | delta: %7.1e | secs: %.5f' % ( 204 | itr, fit, fitchange, exectimes[-1] 205 | )) 206 | if itr > 0 and fitchange < conv: 207 | break 208 | return A, R, f, itr + 1, array(exectimes) 209 | 210 | 211 | # ------------------ Update A ------------------------------------------------ 212 | def _updateA(X, A, R, P, Z, lmbdaA, orthogonalize): 213 | """Update step for A""" 214 | n, rank = A.shape 215 | F = zeros((n, rank), dtype=A.dtype) 216 | E = zeros((rank, rank), dtype=A.dtype) 217 | 218 | AtA = dot(A.T, A) 219 | 220 | for i in range(len(X)): 221 | F += X[i].dot(dot(A, R[i].T)) + X[i].T.dot(dot(A, R[i])) 222 | E += dot(R[i], dot(AtA, R[i].T)) + dot(R[i].T, dot(AtA, R[i])) 223 | 224 | # regularization 225 | I = lmbdaA * eye(rank, dtype=A.dtype) 226 | 227 | # attributes 228 | for i in range(len(Z)): 229 | F += P[i].dot(Z[i].T) 230 | E += dot(Z[i], Z[i].T) 231 | 232 | # finally compute update for A 233 | A = solve(I + E.T, F.T).T 234 | return orth(A) if orthogonalize else A 235 | 236 | 237 | # ------------------ Update R ------------------------------------------------ 238 | def _updateR(X, A, lmbdaR): 239 | rank = A.shape[1] 240 | U, S, Vt = svd(A, full_matrices=False) 241 | Shat = kron(S, S) 242 | Shat = (Shat / (Shat ** 2 + lmbdaR)).reshape(rank, rank) 243 | R = [] 244 | for i in range(len(X)): 245 | Rn = Shat * dot(U.T, X[i].dot(U)) 246 | Rn = dot(Vt.T, dot(Rn, Vt)) 247 | R.append(Rn) 248 | return R 249 | 250 | 251 | # ------------------ Update Z ------------------------------------------------ 252 | def _updateZ(A, P, lmbdaZ): 253 | Z = [] 254 | if len(P) == 0: 255 | return Z 256 | #_log.debug('Updating Z (Norm EQ, %d)' % len(P)) 257 | pinvAt = inv(dot(A.T, A) + lmbdaZ * eye(A.shape[1], dtype=A.dtype)) 258 | pinvAt = dot(pinvAt, A.T).T 259 | for i in range(len(P)): 260 | if issparse(P[i]): 261 | Zn = P[i].tocoo().T.tocsr().dot(pinvAt).T 262 | else: 263 | Zn = dot(pinvAt.T, P[i]) 264 | Z.append(Zn) 265 | return Z 266 | 267 | 268 | def _compute_fval(X, A, R, P, Z, lmbdaA, lmbdaR, lmbdaZ, normX): 269 | """Compute fit for full slices""" 270 | f = lmbdaA * norm(A) ** 2 271 | for i in range(len(X)): 272 | ARAt = dot(A, dot(R[i], A.T)) 273 | f += (norm(X[i] - ARAt) ** 2) / normX[i] + lmbdaR * norm(R[i]) ** 2 274 | return f 275 | 276 | 277 | def _compute_fval_orth(X, A, R, P, Z, lmbdaA, lmbdaR, lmbdaZ, normX): 278 | f = lmbdaA * norm(A) ** 2 279 | for i in range(len(X)): 280 | f += (normX[i] - norm(R[i]) ** 2) / normX[i] + lmbdaR * norm(R[i]) ** 2 281 | return f 282 | 283 | 284 | def sptensor_to_list(X): 285 | from scipy.sparse import lil_matrix 286 | if X.ndim != 3: 287 | raise ValueError('Only third-order tensors are supported (ndim=%d)' % X.ndim) 288 | if X.shape[0] != X.shape[1]: 289 | raise ValueError('First and second mode must be of identical length') 290 | N = X.shape[0] 291 | K = X.shape[2] 292 | res = [lil_matrix((N, N)) for _ in range(K)] 293 | for n in range(X.nnz()): 294 | res[X.subs[2][n]][X.subs[0][n], X.subs[1][n]] = X.vals[n] 295 | return res 296 | 297 | def orth(A): 298 | [U, _, Vt] = svd(A, full_matrices=0) 299 | return dot(U, Vt) 300 | -------------------------------------------------------------------------------- /sktensor/setup.py: -------------------------------------------------------------------------------- 1 | def configuration(parent_package='', top_path=None): 2 | from numpy.distutils.misc_util import Configuration 3 | config = Configuration('sktensor', parent_package, top_path) 4 | 5 | config.add_subpackage('tests') 6 | 7 | return config 8 | -------------------------------------------------------------------------------- /sktensor/sptensor.py: -------------------------------------------------------------------------------- 1 | # sktensor.sptensor - base module for sparse tensors 2 | # Copyright (C) 2013 Maximilian Nickel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import numpy as np 18 | from numpy import zeros, ones, array, arange, copy, ravel_multi_index, unravel_index 19 | from numpy import setdiff1d, hstack, hsplit, vsplit, sort, prod, lexsort, unique, bincount 20 | from scipy.sparse import coo_matrix 21 | from scipy.sparse import issparse as issparse_mat 22 | from sktensor.core import tensor_mixin 23 | from sktensor.utils import accum 24 | from sktensor.dtensor import unfolded_dtensor 25 | from sktensor.pyutils import inherit_docstring_from, from_to_without 26 | 27 | 28 | __all__ = [ 29 | 'concatenate', 30 | 'fromarray', 31 | 'sptensor', 32 | 'unfolded_sptensor', 33 | ] 34 | 35 | 36 | class sptensor(tensor_mixin): 37 | """ 38 | A sparse tensor. 39 | 40 | Data is stored in COOrdinate format. 41 | 42 | Sparse tensors can be instantiated via 43 | 44 | Parameters 45 | ---------- 46 | subs : n-tuple of array-likes 47 | Subscripts of the nonzero entries in the tensor. 48 | Length of tuple n must be equal to dimension of tensor. 49 | vals : array-like 50 | Values of the nonzero entries in the tensor. 51 | shape : n-tuple, optional 52 | Shape of the sparse tensor. 53 | Length of tuple n must be equal to dimension of tensor. 54 | dtype : dtype, optional 55 | Type of the entries in the tensor 56 | accumfun : function pointer 57 | Function to be accumulate duplicate entries 58 | 59 | Examples 60 | -------- 61 | >>> S = sptensor(([0,1,2], [3,2,0], [2,2,2]), [1,1,1], shape=(10, 20, 5), dtype=np.float) 62 | >>> S.shape 63 | (10, 20, 5) 64 | >>> S.dtype 65 | 66 | """ 67 | 68 | def __init__(self, subs, vals, shape=None, dtype=None, accumfun=None, issorted=False): 69 | if not isinstance(subs, tuple): 70 | raise ValueError('Subscripts must be a tuple of array-likes') 71 | if len(subs[0]) != len(vals): 72 | raise ValueError('Subscripts and values must be of equal length') 73 | if dtype is None: 74 | dtype = array(vals).dtype 75 | for i in range(len(subs)): 76 | if array(subs[i]).dtype.kind != 'i': 77 | raise ValueError('Subscripts must be integers') 78 | 79 | vals = array(vals, dtype=dtype) 80 | if accumfun is not None: 81 | vals, subs = accum( 82 | subs, vals, 83 | issorted=False, with_subs=True, func=accumfun 84 | ) 85 | self.subs = subs 86 | self.vals = vals 87 | self.dtype = dtype 88 | self.issorted = issorted 89 | self.accumfun = accumfun 90 | 91 | if shape is None: 92 | self.shape = tuple(array(subs).max(axis=1).flatten() + 1) 93 | else: 94 | self.shape = tuple(int(d) for d in shape) 95 | self.ndim = len(subs) 96 | 97 | def __eq__(self, other): 98 | if isinstance(other, sptensor): 99 | self._sort() 100 | other._sort() 101 | return (self.vals == other.vals).all() and (array(self.subs) == array(other.subs)).all() 102 | elif isinstance(other, np.ndarray): 103 | return (self.toarray() == other).all() 104 | else: 105 | raise NotImplementedError('Unsupported object class for sptensor.__eq__ (%s)' % type(other)) 106 | 107 | def __getitem__(self, idx): 108 | # TODO check performance 109 | if len(idx) != self.ndim: 110 | raise ValueError('subscripts must be complete') 111 | sidx = ones(len(self.vals)) 112 | for i in range(self.ndim): 113 | sidx = np.logical_and(self.subs[i] == idx[i], sidx) 114 | vals = self.vals[sidx] 115 | if len(vals) == 0: 116 | vals = 0 117 | elif len(vals) > 1: 118 | if self.accumfun is None: 119 | raise ValueError('Duplicate entries without specified accumulation function') 120 | vals = self.accumfun(vals) 121 | return vals 122 | 123 | def __sub__(self, other): 124 | if isinstance(other, np.ndarray): 125 | res = -other 126 | res[self.subs] += self.vals 127 | else: 128 | raise NotImplementedError() 129 | return res 130 | 131 | def _sort(self): 132 | # TODO check performance 133 | subs = array(self.subs) 134 | sidx = lexsort(subs) 135 | self.subs = tuple(z.flatten()[sidx] for z in vsplit(subs, len(self.shape))) 136 | self.vals = self.vals[sidx] 137 | self.issorted = True 138 | 139 | def _ttm_compute(self, V, mode, transp): 140 | Z = self.unfold(mode, transp=True).tocsr() 141 | if transp: 142 | V = V.T 143 | Z = Z.dot(V.T) 144 | shape = copy(self.shape) 145 | shape[mode] = V.shape[0] 146 | if issparse_mat(Z): 147 | newT = unfolded_sptensor((Z.data, (Z.row, Z.col)), [mode], None, shape=shape).fold() 148 | else: 149 | newT = unfolded_dtensor(Z.T, mode, shape).fold() 150 | 151 | return newT 152 | 153 | def _ttv_compute(self, v, dims, vidx, remdims): 154 | nvals = self.vals 155 | nsubs = self.subs 156 | for i in range(len(dims)): 157 | idx = nsubs[dims[i]] 158 | w = v[vidx[i]] 159 | nvals = nvals * w[idx] 160 | 161 | # Case 1: all dimensions used -> return sum 162 | if len(remdims) == 0: 163 | return nvals.sum() 164 | 165 | nsubs = tuple(self.subs[i] for i in remdims) 166 | nshp = tuple(self.shape[i] for i in remdims) 167 | 168 | # Case 2: result is a vector 169 | if len(remdims) == 1: 170 | usubs = unique(nsubs[0]) 171 | bins = usubs.searchsorted(nsubs[0]) 172 | c = bincount(bins, weights=nvals) 173 | (nz,) = c.nonzero() 174 | return sptensor((usubs[nz],), c[nz], nshp) 175 | 176 | # Case 3: result is an array 177 | return sptensor(nsubs, nvals, shape=nshp, accumfun=np.sum) 178 | 179 | def _ttm_me_compute(self, V, edims, sdims, transp): 180 | """ 181 | Assume Y = T x_i V_i for i = 1...n can fit into memory 182 | """ 183 | shapeY = np.copy(self.shape) 184 | 185 | # Determine size of Y 186 | for n in np.union1d(edims, sdims): 187 | shapeY[n] = V[n].shape[1] if transp else V[n].shape[0] 188 | 189 | # Allocate Y (final result) and v (vectors for elementwise computations) 190 | Y = zeros(shapeY) 191 | shapeY = array(shapeY) 192 | v = [None for _ in range(len(edims))] 193 | 194 | for i in range(np.prod(shapeY[edims])): 195 | rsubs = unravel_index(shapeY[edims], i) 196 | 197 | def unfold(self, rdims, cdims=None, transp=False): 198 | if isinstance(rdims, type(1)): 199 | rdims = [rdims] 200 | if transp: 201 | cdims = rdims 202 | rdims = setdiff1d(range(self.ndim), cdims)[::-1] 203 | elif cdims is None: 204 | cdims = setdiff1d(range(self.ndim), rdims)[::-1] 205 | if not (arange(self.ndim) == sort(hstack((rdims, cdims)))).all(): 206 | raise ValueError( 207 | 'Incorrect specification of dimensions (rdims: %s, cdims: %s)' 208 | % (str(rdims), str(cdims)) 209 | ) 210 | M = prod([self.shape[r] for r in rdims]) 211 | N = prod([self.shape[c] for c in cdims]) 212 | ridx = _build_idx(self.subs, self.vals, rdims, self.shape) 213 | cidx = _build_idx(self.subs, self.vals, cdims, self.shape) 214 | return unfolded_sptensor((self.vals, (ridx, cidx)), (M, N), rdims, cdims, self.shape) 215 | 216 | @inherit_docstring_from(tensor_mixin) 217 | def uttkrp(self, U, mode): 218 | R = U[1].shape[1] if mode == 0 else U[0].shape[1] 219 | #dims = list(range(0, mode)) + list(range(mode + 1, self.ndim)) 220 | dims = from_to_without(0, self.ndim, mode) 221 | V = zeros((self.shape[mode], R)) 222 | for r in range(R): 223 | Z = tuple(U[n][:, r] for n in dims) 224 | TZ = self.ttv(Z, mode, without=True) 225 | if isinstance(TZ, sptensor): 226 | V[TZ.subs, r] = TZ.vals 227 | else: 228 | V[:, r] = self.ttv(Z, mode, without=True) 229 | return V 230 | 231 | @inherit_docstring_from(tensor_mixin) 232 | def transpose(self, axes=None): 233 | """ 234 | Compute transpose of sparse tensors. 235 | 236 | Parameters 237 | ---------- 238 | axes : array_like of ints, optional 239 | Permute the axes according to the values given. 240 | 241 | Returns 242 | ------- 243 | d : dtensor 244 | dtensor with axes permuted. 245 | """ 246 | if axes is None: 247 | raise NotImplementedError( 248 | 'Sparse tensor transposition without axes argument is not supported' 249 | ) 250 | nsubs = tuple([self.subs[idx] for idx in axes]) 251 | nshape = [self.shape[idx] for idx in axes] 252 | return sptensor(nsubs, self.vals, nshape) 253 | 254 | def concatenate(self, tpl, axis=None): 255 | """ 256 | Concatenates sparse tensors. 257 | 258 | Parameters 259 | ---------- 260 | tpl : tuple of sparse tensors 261 | Tensors to be concatenated. 262 | axis : int, optional 263 | Axis along which concatenation should take place 264 | """ 265 | if axis is None: 266 | raise NotImplementedError( 267 | 'Sparse tensor concatenation without axis argument is not supported' 268 | ) 269 | T = self 270 | for i in range(1, len(tpl)): 271 | T = _single_concatenate(T, tpl[i], axis=axis) 272 | return T 273 | 274 | def norm(self): 275 | """ 276 | Frobenius norm for tensors 277 | 278 | References 279 | ---------- 280 | [Kolda and Bader, 2009; p.457] 281 | """ 282 | return np.linalg.norm(self.vals) 283 | 284 | def toarray(self): 285 | A = zeros(self.shape) 286 | A.put(ravel_multi_index(self.subs, tuple(self.shape)), self.vals) 287 | return A 288 | 289 | 290 | class unfolded_sptensor(coo_matrix): 291 | """ 292 | An unfolded sparse tensor. 293 | 294 | Data is stored in form of a sparse COO matrix. 295 | Unfolded_sptensor objects additionall hold information about the 296 | original tensor, such that re-folding the tensor into its original 297 | shape can be done easily. 298 | 299 | Unfolded_sptensor objects can be instantiated via 300 | 301 | Parameters 302 | ---------- 303 | tpl : (data, (i, j)) tuple 304 | Construct sparse matrix from three arrays: 305 | 1. ``data[:]`` the entries of the matrix, in any order 306 | 2. ``i[:]`` the row indices of the matrix entries 307 | 3. ``j[:]`` the column indices of the matrix entries 308 | where ``A[i[k], j[k]] = data[k]``. 309 | shape : tuple of integers 310 | Shape of the unfolded tensor. 311 | rdims : array_like of integers 312 | Modes of the original tensor that are mapped onto rows. 313 | cdims : array_like of integers 314 | Modes of the original tensor that are mapped onto columns. 315 | ten_shape : tuple of integers 316 | Shape of the original tensor. 317 | dtype : np.dtype, optional 318 | Data type of the unfolded tensor. 319 | copy : boolean, optional 320 | If true, data and subscripts are copied. 321 | 322 | Returns 323 | ------- 324 | M : unfolded_sptensor 325 | Sparse matrix in COO format where ``rdims`` are mapped to rows and 326 | ``cdims`` are mapped to columns of the matrix. 327 | """ 328 | 329 | def __init__(self, tpl, shape, rdims, cdims, ten_shape, dtype=None, copy=False): 330 | self.ten_shape = array(ten_shape) 331 | if isinstance(rdims, int): 332 | rdims = [rdims] 333 | if cdims is None: 334 | cdims = setdiff1d(range(len(self.ten_shape)), rdims)[::-1] 335 | self.rdims = rdims 336 | self.cdims = cdims 337 | super(unfolded_sptensor, self).__init__(tpl, shape=shape, dtype=dtype, copy=copy) 338 | 339 | def fold(self): 340 | """ 341 | Recreate original tensor by folding unfolded_sptensor according toc 342 | ``ten_shape``. 343 | 344 | Returns 345 | ------- 346 | T : sptensor 347 | Sparse tensor that is created by refolding according to ``ten_shape``. 348 | """ 349 | nsubs = zeros((len(self.data), len(self.ten_shape)), dtype=np.int) 350 | if len(self.rdims) > 0: 351 | nidx = unravel_index(self.row, self.ten_shape[self.rdims]) 352 | for i in range(len(self.rdims)): 353 | nsubs[:, self.rdims[i]] = nidx[i] 354 | if len(self.cdims) > 0: 355 | nidx = unravel_index(self.col, self.ten_shape[self.cdims]) 356 | for i in range(len(self.cdims)): 357 | nsubs[:, self.cdims[i]] = nidx[i] 358 | nsubs = [z.flatten() for z in hsplit(nsubs, len(self.ten_shape))] 359 | return sptensor(tuple(nsubs), self.data, self.ten_shape) 360 | 361 | 362 | def fromarray(A): 363 | """Create a sptensor from a dense numpy array""" 364 | subs = np.nonzero(A) 365 | vals = A[subs] 366 | return sptensor(subs, vals, shape=A.shape, dtype=A.dtype) 367 | 368 | 369 | def _single_concatenate(ten, other, axis): 370 | tshape = ten.shape 371 | oshape = other.shape 372 | if len(tshape) != len(oshape): 373 | raise ValueError("len(tshape) != len(oshape") 374 | oaxes = setdiff1d(range(len(tshape)), [axis]) 375 | for i in oaxes: 376 | if tshape[i] != oshape[i]: 377 | raise ValueError("Dimensions must match") 378 | nsubs = [None for _ in range(len(tshape))] 379 | for i in oaxes: 380 | nsubs[i] = np.concatenate((ten.subs[i], other.subs[i])) 381 | nsubs[axis] = np.concatenate(( 382 | ten.subs[axis], other.subs[axis] + tshape[axis] 383 | )) 384 | nvals = np.concatenate((ten.vals, other.vals)) 385 | nshape = np.copy(tshape) 386 | nshape[axis] = tshape[axis] + oshape[axis] 387 | return sptensor(nsubs, nvals, nshape) 388 | 389 | 390 | def _build_idx(subs, vals, dims, tshape): 391 | shape = array([tshape[d] for d in dims], ndmin=1) 392 | dims = array(dims, ndmin=1) 393 | if len(shape) == 0: 394 | idx = ones(len(vals), dtype=vals.dtype) 395 | elif len(subs) == 0: 396 | idx = array(tuple()) 397 | else: 398 | idx = ravel_multi_index(tuple(subs[i] for i in dims), shape) 399 | return idx 400 | -------------------------------------------------------------------------------- /sktensor/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnick/scikit-tensor/fe517e9661a08164b8d30d2dddf7c96aeeabcf36/sktensor/tests/__init__.py -------------------------------------------------------------------------------- /sktensor/tests/sptensor_fixture.py: -------------------------------------------------------------------------------- 1 | from numpy import array 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def subs(): 7 | return ( 8 | array([0, 1, 0, 5, 7, 8]), 9 | array([2, 0, 4, 5, 3, 9]), 10 | array([0, 1, 2, 2, 1, 0]) 11 | ) 12 | 13 | 14 | @pytest.fixture 15 | def vals(): 16 | return array([1, 2, 3, 4, 5, 6.1]) 17 | 18 | 19 | @pytest.fixture 20 | def shape(): 21 | return (10, 12, 3) 22 | -------------------------------------------------------------------------------- /sktensor/tests/sptensor_rand_fixture.py: -------------------------------------------------------------------------------- 1 | from numpy.random import randint, seed 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def sptensor_seed(): 7 | return seed(5) 8 | 9 | 10 | @pytest.fixture 11 | def sz(): 12 | return 100 13 | 14 | 15 | @pytest.fixture 16 | def vals(sptensor_seed, sz): 17 | return randint(0, 100, sz) 18 | 19 | 20 | @pytest.fixture 21 | def shape(): 22 | return (25, 11, 18, 7, 2) 23 | 24 | 25 | @pytest.fixture 26 | def subs(sptensor_seed, shape, sz): 27 | return tuple(randint(0, shape[i], sz) for i in range(len(shape))) 28 | -------------------------------------------------------------------------------- /sktensor/tests/test_base.py: -------------------------------------------------------------------------------- 1 | from numpy import array 2 | from numpy.random import randn 3 | from sktensor.core import * 4 | from sktensor import dtensor, sptensor, ktensor 5 | from .ttm_fixture import T, U, Y 6 | from .sptensor_fixture import shape, vals, subs 7 | 8 | 9 | def test_check_multiplication_dims(): 10 | ndims = 3 11 | M = 2 12 | assert ([1, 2] == check_multiplication_dims(0, ndims, M, without=True)).all() 13 | assert ([0, 2] == check_multiplication_dims(1, ndims, M, without=True)).all() 14 | assert ([0, 1] == check_multiplication_dims(2, ndims, M, without=True)).all() 15 | 16 | 17 | def test_khatrirao(): 18 | A = array([ 19 | [1, 2, 3], 20 | [4, 5, 6], 21 | [7, 8, 9] 22 | ]) 23 | B = array([ 24 | [1, 4, 7], 25 | [2, 5, 8], 26 | [3, 6, 9] 27 | ]) 28 | C = array([ 29 | [1, 8, 21], 30 | [2, 10, 24], 31 | [3, 12, 27], 32 | [4, 20, 42], 33 | [8, 25, 48], 34 | [12, 30, 54], 35 | [7, 32, 63], 36 | [14, 40, 72], 37 | [21, 48, 81] 38 | ]) 39 | 40 | D = khatrirao((A, B)) 41 | assert C.shape == D.shape 42 | assert (C == D).all() 43 | 44 | 45 | def test_dense_fold(T): 46 | X = dtensor(T) 47 | I, J, K = T.shape 48 | X1 = X[:, :, 0] 49 | X2 = X[:, :, 1] 50 | 51 | U = X.unfold(0) 52 | assert (3, 8) == U.shape 53 | for j in range(J): 54 | assert (U[:, j] == X1[:, j]).all() 55 | assert (U[:, j + J] == X2[:, j]).all() 56 | 57 | U = X.unfold(1) 58 | assert (4, 6) == U.shape 59 | for i in range(I): 60 | assert (U[:, i] == X1[i, :]).all() 61 | assert (U[:, i + I] == X2[i, :]).all() 62 | 63 | U = X.unfold(2) 64 | assert (2, 12) == U.shape 65 | for k in range(U.shape[1]): 66 | assert (U[:, k] == array([X1.flatten('F')[k], X2.flatten('F')[k]])).all() 67 | 68 | 69 | def test_dtensor_fold_unfold(): 70 | sz = (10, 35, 3, 12) 71 | X = dtensor(randn(*sz)) 72 | for i in range(4): 73 | U = X.unfold(i).fold() 74 | assert (X == U).all() 75 | 76 | 77 | def test_dtensor_ttm(T, U, Y): 78 | X = dtensor(T) 79 | Y2 = X.ttm(U, 0) 80 | assert (2, 4, 2) == Y2.shape 81 | assert (Y == Y2).all() 82 | 83 | 84 | def test_spttv(subs, vals, shape): 85 | #subs = ( 86 | # array([0, 1, 0, 5, 7, 8]), 87 | # array([2, 0, 4, 5, 3, 9]), 88 | # array([0, 1, 2, 2, 1, 0]) 89 | #) 90 | #vals = array([1, 1, 1, 1, 1, 1]) 91 | S = sptensor(subs, vals, shape=shape) 92 | K = ktensor([randn(shape[0], 2), randn(shape[1], 2), randn(shape[2], 2)]) 93 | K.innerprod(S) 94 | -------------------------------------------------------------------------------- /sktensor/tests/test_dtensor.py: -------------------------------------------------------------------------------- 1 | from numpy import array 2 | from numpy.random import randn 3 | from sktensor.dtensor import dtensor 4 | from .ttm_fixture import T, U, Y 5 | 6 | 7 | def test_new(): 8 | sz = (10, 23, 5) 9 | A = randn(*sz) 10 | T = dtensor(A) 11 | assert A.ndim == T.ndim 12 | assert A.shape == T.shape 13 | assert (A == T).all() 14 | assert (T == A).all() 15 | 16 | 17 | def test_dense_fold(T): 18 | X = dtensor(T) 19 | I, J, K = T.shape 20 | X1 = X[:, :, 0] 21 | X2 = X[:, :, 1] 22 | 23 | U = X.unfold(0) 24 | assert (3, 8) == U.shape 25 | for j in range(J): 26 | assert (U[:, j] == X1[:, j]).all() 27 | assert (U[:, j + J] == X2[:, j]).all() 28 | 29 | U = X.unfold(1) 30 | assert (4, 6) == U.shape 31 | for i in range(I): 32 | assert (U[:, i] == X1[i, :]).all() 33 | assert (U[:, i + I] == X2[i, :]).all() 34 | 35 | U = X.unfold(2) 36 | assert (2, 12) == U.shape 37 | for k in range(U.shape[1]): 38 | assert (U[:, k] == array([X1.flatten('F')[k], X2.flatten('F')[k]])).all() 39 | 40 | 41 | def test_dtensor_fold_unfold(): 42 | sz = (10, 35, 3, 12) 43 | X = dtensor(randn(*sz)) 44 | for i in range(4): 45 | U = X.unfold(i).fold() 46 | assert (X == U).all() 47 | 48 | 49 | def test_dtensor_ttm(T, Y, U): 50 | X = dtensor(T) 51 | Y2 = X.ttm(U, 0) 52 | assert (2, 4, 2) == Y2.shape 53 | assert (Y == Y2).all() 54 | -------------------------------------------------------------------------------- /sktensor/tests/test_ktensor.py: -------------------------------------------------------------------------------- 1 | from numpy.random import randn 2 | from sktensor import ktensor 3 | 4 | 5 | def test_vectorization(): 6 | rank = 5 7 | shape = (5, 27, 3, 13) 8 | U = [randn(s, rank) for s in shape] 9 | K = ktensor(U) 10 | v = K.tovec() 11 | K2 = v.toktensor() 12 | 13 | assert sum([s * rank for s in shape]) == len(v.v) 14 | assert K == K2 15 | -------------------------------------------------------------------------------- /sktensor/tests/test_pyutils.py: -------------------------------------------------------------------------------- 1 | from sktensor.pyutils import * 2 | 3 | 4 | def test_from_to_without(): 5 | frm, to, without = 2, 88, 47 6 | lst = list(range(frm, without)) + list(range(without + 1, to)) 7 | assert lst == from_to_without(frm, to, without) 8 | 9 | rlst = list(range(to - 1, without, -1)) + list(range(without - 1, frm - 1,-1)) 10 | assert rlst == from_to_without(frm, to, without, reverse=True) 11 | assert lst[::-1] == from_to_without(frm, to, without, reverse=True) 12 | -------------------------------------------------------------------------------- /sktensor/tests/test_sptensor.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from numpy import ones, zeros, array, setdiff1d, allclose 4 | from numpy.random import randint 5 | from sktensor.dtensor import dtensor 6 | from sktensor.sptensor import sptensor, fromarray 7 | from .ttm_fixture import T, U, Y 8 | from .sptensor_rand_fixture import subs, vals, shape, sptensor_seed, sz 9 | 10 | 11 | def setup_diagonal(): 12 | """ 13 | Setup data for a 20x20x20 diagonal tensor 14 | """ 15 | n = 20 16 | shape = (n, n, n) 17 | subs = [np.arange(0, shape[i]) for i in range(len(shape))] 18 | vals = ones(n) 19 | return tuple(subs), vals, shape 20 | 21 | 22 | def test_init(subs, vals, shape): 23 | """ 24 | Creation of new sptensor objects 25 | """ 26 | T = sptensor(subs, vals, shape) 27 | assert len(shape) == T.ndim 28 | assert (array(shape) == T.shape).all() 29 | 30 | T = sptensor(subs, vals) 31 | tshape = array(subs).max(axis=1) + 1 32 | assert len(subs) == len(T.shape) 33 | assert (tshape == array(T.shape)).all() 34 | 35 | 36 | def test_init_diagonal(): 37 | subs, vals, shape = setup_diagonal() 38 | T = sptensor(subs, vals, shape) 39 | assert len(shape) == T.ndim 40 | assert (array(shape) == T.shape).all() 41 | 42 | T = sptensor(subs, vals) 43 | assert len(subs) == len(T.shape) 44 | assert (shape == array(T.shape)).all() 45 | 46 | 47 | def test_non2Dsubs(): 48 | with pytest.raises(ValueError): 49 | sptensor(randint(0, 10, 18).reshape(3, 3, 2), ones(10)) 50 | 51 | 52 | def test_nonEqualLength(subs): 53 | with pytest.raises(ValueError): 54 | sptensor(subs, ones(len(subs) + 1)) 55 | 56 | 57 | def test_unfold(T, subs, vals, shape): 58 | Td = dtensor(zeros(shape, dtype=np.float32)) 59 | Td[subs] = vals 60 | 61 | for i in range(len(shape)): 62 | rdims = [i] 63 | cdims = setdiff1d(range(len(shape)), rdims)[::-1] 64 | Md = Td.unfold(i) 65 | 66 | T = sptensor(subs, vals, shape, accumfun=lambda l: l[-1]) 67 | 68 | Ms = T.unfold(rdims, cdims) 69 | assert Md.shape == Ms.shape 70 | assert (allclose(Md, Ms.toarray())) 71 | 72 | Ms = T.unfold(rdims) 73 | assert Md.shape == Ms.shape 74 | assert (allclose(Md, Ms.toarray())) 75 | 76 | Md = Md.T 77 | Ms = T.unfold(rdims, cdims, transp=True) 78 | assert Md.shape == Ms.shape 79 | assert (allclose(Md, Ms.toarray())) 80 | 81 | 82 | def test_fold(subs, vals, shape): 83 | T = sptensor(subs, vals, shape) 84 | for i in range(len(shape)): 85 | X = T.unfold([i]).fold() 86 | assert shape == tuple(T.shape) 87 | assert len(shape) == len(T.subs) 88 | assert len(subs) == len(T.subs) 89 | assert X == T 90 | for j in range(len(subs)): 91 | subs[j].sort() 92 | T.subs[j].sort() 93 | assert (subs[j] == T.subs[j]).all() 94 | 95 | 96 | def test_ttm(T, Y, U): 97 | S = sptensor(T.nonzero(), T.flatten(), T.shape) 98 | Y2 = S.ttm(U, 0) 99 | assert (2, 4, 2) == Y2.shape 100 | assert (Y == Y2).all() 101 | 102 | 103 | def test_ttv_sparse_result(): 104 | # Test case by Andre Panisson to check return type of sptensor.ttv 105 | subs = ( 106 | array([0, 1, 0, 5, 7, 8]), 107 | array([2, 0, 4, 5, 3, 9]), 108 | array([0, 1, 2, 2, 1, 0]) 109 | ) 110 | vals = array([1, 1, 1, 1, 1, 1]) 111 | S = sptensor(subs, vals, shape=[10, 10, 3]) 112 | 113 | sttv = S.ttv((zeros(10), zeros(10)), modes=[0, 1]) 114 | assert type(sttv) == sptensor 115 | # sparse tensor should return only nonzero vals 116 | assert (allclose(np.array([]), sttv.vals)) 117 | assert (allclose(np.array([]), sttv.subs)) 118 | assert sttv.shape == (3,) 119 | 120 | 121 | def test_ttv(T): 122 | result = array([ 123 | [70, 190], 124 | [80, 200], 125 | [90, 210] 126 | ]) 127 | 128 | X = fromarray(T) 129 | v = array([1, 2, 3, 4]) 130 | Xv = X.ttv(v, 1) 131 | 132 | assert (3, 2) == Xv.shape 133 | assert (Xv == result).all() 134 | 135 | 136 | def test_sttm_me(T, U): 137 | S = sptensor(T.nonzero(), T.flatten(), T.shape) 138 | S._ttm_me_compute(U, [1], [0], False) 139 | 140 | 141 | def test_sp_uttkrp(subs, vals, shape): 142 | # Test case by Andre Panisson, sparse ttv 143 | # see issue #3 144 | S = sptensor(subs, vals, shape) 145 | U = [] 146 | for shp in shape: 147 | U.append(np.zeros((shp, 5))) 148 | SU = S.uttkrp(U, mode=0) 149 | assert SU.shape == (25, 5) 150 | 151 | 152 | def test_getitem(): 153 | subs = ( 154 | array([0, 1, 0, 5, 7, 8]), 155 | array([2, 0, 4, 5, 3, 9]), 156 | array([0, 1, 2, 2, 1, 0]) 157 | ) 158 | vals = array([1, 2, 3, 4, 5, 6]) 159 | S = sptensor(subs, vals, shape=[10, 10, 3]) 160 | assert 0 == S[1, 1, 1] 161 | assert 0 == S[1, 2, 3] 162 | assert 1 == S[0, 2, 0] 163 | assert 2 == S[1, 0, 1] 164 | assert 3 == S[0, 4, 2] 165 | assert 4 == S[5, 5, 2] 166 | assert 5 == S[7, 3, 1] 167 | assert 6 == S[8, 9, 0] 168 | 169 | 170 | def test_add(): 171 | subs = ( 172 | array([0, 1, 0]), 173 | array([2, 0, 2]), 174 | array([0, 1, 2]) 175 | ) 176 | vals = array([1, 2, 3]) 177 | S = sptensor(subs, vals, shape=[3, 3, 3]) 178 | D = np.arange(27).reshape(3, 3, 3) 179 | T = S - D 180 | for i in range(3): 181 | for j in range(3): 182 | for k in range(3): 183 | assert S[i, j, k] - D[i, j, k] == T[i, j, k] 184 | -------------------------------------------------------------------------------- /sktensor/tests/test_tucker_hooi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import logging 3 | from numpy import allclose 4 | from numpy.random import randn 5 | from scipy.sparse import rand as sprand 6 | from sktensor import tucker 7 | from sktensor.core import ttm 8 | from sktensor.dtensor import dtensor, unfolded_dtensor 9 | from sktensor.sptensor import unfolded_sptensor 10 | #from sktensor.rotation import orthomax 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | 15 | def normalize(X): 16 | return X / X.sum(axis=0) 17 | 18 | 19 | def disabled_test_factorization(): 20 | I, J, K, rank = 10, 20, 75, 5 21 | A = orthomax(randn(I, rank)) 22 | B = orthomax(randn(J, rank)) 23 | C = orthomax(randn(K, rank)) 24 | 25 | core_real = dtensor(randn(rank, rank, rank)) 26 | T = core_real.ttm([A, B, C]) 27 | core, U = tucker.hooi(T, rank) 28 | 29 | assert allclose(T, ttm(core, U)) 30 | assert allclose(A, orthomax(U[0])) 31 | assert allclose(B, orthomax(U[1])) 32 | assert allclose(C, orthomax(U[2])) 33 | assert allclose(core_real, core) 34 | 35 | 36 | def disabled_test_factorization_sparse(): 37 | I, J, K, rank = 10, 20, 75, 5 38 | Tmat = sprand(I, J * K, 0.1).tocoo() 39 | T = unfolded_sptensor((Tmat.data, (Tmat.row, Tmat.col)), None, 0, [], (I, J, K)).fold() 40 | core, U = tucker.hooi(T, rank, maxIter=20) 41 | 42 | Tmat = Tmat.toarray() 43 | T = unfolded_dtensor(Tmat, 0, (I, J, K)).fold() 44 | core2, U2 = tucker.hooi(T, rank, maxIter=20) 45 | 46 | assert allclose(core2, core) 47 | for i in range(len(U)): 48 | assert allclose(U2[i], U[i]) 49 | -------------------------------------------------------------------------------- /sktensor/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from ..utils import accum 2 | from numpy import array, allclose 3 | 4 | 5 | def test_accum(): 6 | subs1 = array([0, 1, 1, 2, 2, 2]) 7 | subs2 = array([0, 1, 1, 1, 2, 2]) 8 | vals = array([1, 2, 3, 4, 5, 6]) 9 | nvals, nsubs = accum((subs1, subs2), vals, with_subs=True) 10 | assert allclose(nvals, array([1, 5, 4, 11])) 11 | assert allclose(nsubs[0], array([0, 1, 2, 2])) 12 | assert allclose(nsubs[1], array([0, 1, 1, 2])) 13 | 14 | subs1 = array([0, 0, 1]) 15 | subs2 = array([0, 0, 1]) 16 | vals = array([1, 2, 3]) 17 | nvals, nsubs = accum((subs1, subs2), vals, with_subs=True) 18 | assert allclose(nvals, array([3, 3])) 19 | assert allclose(nsubs[0], array([0, 1])) 20 | assert allclose(nsubs[1], array([0, 1])) 21 | -------------------------------------------------------------------------------- /sktensor/tests/ttm_fixture.py: -------------------------------------------------------------------------------- 1 | from numpy import array, zeros 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def T(): 7 | T = zeros((3, 4, 2)) 8 | T[:, :, 0] = array([[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]]) 9 | T[:, :, 1] = array([[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]]) 10 | return T 11 | 12 | 13 | @pytest.fixture 14 | def Y(): 15 | Y = zeros((2, 4, 2)) 16 | Y[:, :, 0] = array([[22, 49, 76, 103], [28, 64, 100, 136]]) 17 | Y[:, :, 1] = array([[130, 157, 184, 211], [172, 208, 244, 280]]) 18 | return Y 19 | 20 | 21 | @pytest.fixture 22 | def U(): 23 | return array([[1, 3, 5], [2, 4, 6]]) 24 | -------------------------------------------------------------------------------- /sktensor/tucker.py: -------------------------------------------------------------------------------- 1 | # sktensor.tucker - Algorithms to compute Tucker decompositions 2 | # Copyright (C) 2013 Maximilian Nickel 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | import time 19 | import numpy as np 20 | from numpy import array, ones, sqrt 21 | from numpy.random import rand 22 | from .pyutils import is_number 23 | from .core import ttm, nvecs, norm 24 | 25 | __all__ = [ 26 | 'hooi', 27 | 'hosvd', 28 | ] 29 | 30 | _log = logging.getLogger('TUCKER') 31 | __DEF_MAXITER = 500 32 | __DEF_INIT = 'nvecs' 33 | __DEF_CONV = 1e-7 34 | 35 | 36 | def hooi(X, rank, **kwargs): 37 | """ 38 | Compute Tucker decomposition of a tensor using Higher-Order Orthogonal 39 | Iterations. 40 | 41 | Parameters 42 | ---------- 43 | X : tensor_mixin 44 | The tensor to be decomposed 45 | rank : array_like 46 | The rank of the decomposition for each mode of the tensor. 47 | The length of ``rank`` must match the number of modes of ``X``. 48 | init : {'random', 'nvecs'}, optional 49 | The initialization method to use. 50 | - random : Factor matrices are initialized randomly. 51 | - nvecs : Factor matrices are initialzed via HOSVD. 52 | default : 'nvecs' 53 | 54 | Examples 55 | -------- 56 | Create dense tensor 57 | 58 | >>> T = np.zeros((3, 4, 2)) 59 | >>> T[:, :, 0] = [[ 1, 4, 7, 10], [ 2, 5, 8, 11], [3, 6, 9, 12]] 60 | >>> T[:, :, 1] = [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]] 61 | >>> T = dtensor(T) 62 | 63 | Compute Tucker decomposition of ``T`` with n-rank [2, 3, 1] via higher-order 64 | orthogonal iterations 65 | 66 | >>> Y = hooi(T, [2, 3, 1], init='nvecs') 67 | 68 | Shape of the core tensor matches n-rank of the decomposition. 69 | 70 | >>> Y['core'].shape 71 | (2, 3, 1) 72 | >>> Y['U'][1].shape 73 | (3, 2) 74 | 75 | References 76 | ---------- 77 | .. [1] L. De Lathauwer, B. De Moor, J. Vandewalle: On the best rank-1 and 78 | rank-(R_1, R_2, \ldots, R_N) approximation of higher order tensors; 79 | IEEE Trans. Signal Process. 49 (2001), pp. 2262-2271 80 | """ 81 | # init options 82 | ainit = kwargs.pop('init', __DEF_INIT) 83 | maxIter = kwargs.pop('maxIter', __DEF_MAXITER) 84 | conv = kwargs.pop('conv', __DEF_CONV) 85 | dtype = kwargs.pop('dtype', X.dtype) 86 | if not len(kwargs) == 0: 87 | raise ValueError('Unknown keywords (%s)' % (kwargs.keys())) 88 | 89 | ndims = X.ndim 90 | if is_number(rank): 91 | rank = rank * ones(ndims) 92 | 93 | normX = norm(X) 94 | 95 | U = __init(ainit, X, ndims, rank, dtype) 96 | fit = 0 97 | exectimes = [] 98 | for itr in range(maxIter): 99 | tic = time.clock() 100 | fitold = fit 101 | 102 | for n in range(ndims): 103 | Utilde = ttm(X, U, n, transp=True, without=True) 104 | U[n] = nvecs(Utilde, n, rank[n]) 105 | 106 | # compute core tensor to get fit 107 | core = ttm(Utilde, U, n, transp=True) 108 | 109 | # since factors are orthonormal, compute fit on core tensor 110 | normresidual = sqrt(normX ** 2 - norm(core) ** 2) 111 | 112 | # fraction explained by model 113 | fit = 1 - (normresidual / normX) 114 | fitchange = abs(fitold - fit) 115 | exectimes.append(time.clock() - tic) 116 | 117 | _log.debug( 118 | '[%3d] fit: %.5f | delta: %7.1e | secs: %.5f' 119 | % (itr, fit, fitchange, exectimes[-1]) 120 | ) 121 | if itr > 1 and fitchange < conv: 122 | break 123 | return core, U 124 | 125 | def hosvd(X, rank, dims=None, dtype=None, compute_core=True): 126 | U = [None for _ in range(X.ndim)] 127 | if dims is None: 128 | dims = range(X.ndim) 129 | if dtype is None: 130 | dtype = X.dtype 131 | for d in dims: 132 | U[d] = array(nvecs(X, d, rank[d]), dtype=dtype) 133 | if compute_core: 134 | core = X.ttm(U, transp=True) 135 | return U, core 136 | else: 137 | return U 138 | 139 | def __init(init, X, N, rank, dtype): 140 | # Don't compute initial factor for first index, gets computed in 141 | # first iteration 142 | Uinit = [None] 143 | if isinstance(init, list): 144 | Uinit = init 145 | elif init == 'random': 146 | for n in range(1, N): 147 | Uinit.append(array(rand(X.shape[n], rank[n]), dtype=dtype)) 148 | elif init == 'nvecs': 149 | Uinit = hosvd(X, rank, range(1, N), dtype=dtype, compute_core=False) 150 | return Uinit 151 | -------------------------------------------------------------------------------- /sktensor/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy import cumprod, array, arange, zeros, floor, lexsort 3 | 4 | 5 | def accum(subs, vals, func=np.sum, issorted=False, with_subs=False): 6 | """ 7 | NumPy implementation for Matlab's accumarray 8 | """ 9 | # sort accmap for ediff if not sorted 10 | if not issorted: 11 | sidx = lexsort(subs, axis=0) 12 | subs = [sub[sidx] for sub in subs] 13 | vals = vals[sidx] 14 | idx = np.where(np.diff(subs).any(axis=0))[0] + 1 15 | idx = np.concatenate(([0], idx, [subs[0].shape[0]])) 16 | 17 | # create values array 18 | nvals = np.zeros(len(idx) - 1) 19 | for i in range(len(idx) - 1): 20 | nvals[i] = func(vals[idx[i]:idx[i + 1]]) 21 | 22 | # return results 23 | if with_subs: 24 | return nvals, tuple(sub[idx[:-1]] for sub in subs) 25 | else: 26 | return nvals 27 | 28 | 29 | def unravel_dimension(shape, idx): 30 | if isinstance(idx, type(1)): 31 | idx = array([idx]) 32 | k = [1] + list(cumprod(shape[:-1])) 33 | n = len(shape) 34 | subs = zeros((len(idx), n), dtype=np.int) 35 | for i in arange(n - 1, -1, -1): 36 | subs[:, i] = floor(idx / k[i]) 37 | idx = idx % k[i] 38 | return subs 39 | -------------------------------------------------------------------------------- /sktensor/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1' 2 | --------------------------------------------------------------------------------