Skip to content

Commit 2cc7b48

Browse files
author
Aaron Meyer
committed
Merge branch 'main' into jt-diag
2 parents 2896e47 + acc439e commit 2cc7b48

33 files changed

+1542
-1090
lines changed

.github/workflows/deploy_pypi.yml

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Deploy to Pypi
1+
name: Deploy 📦 to Test-Pypi and Pypi 🐍
22

33
on:
44
push:
@@ -23,47 +23,87 @@ jobs:
2323
python -m pip install -r requirements.txt
2424
python -m pip install -r doc/requirements_doc.txt
2525
pip install setuptools wheel
26+
- name: Install package
27+
run: |
28+
python -m pip install -e .
2629
- name: Make doc
2730
run: |
2831
cd doc
2932
python minify.py
3033
make html
3134
cd ..
32-
- name: Install package
33-
run: |
34-
python -m pip install -e .
3535
- name: Build a binary wheel and a source tarball
3636
run: |
3737
python setup.py sdist bdist_wheel
38-
- name: Publish package to TestPyPI
39-
uses: pypa/gh-action-pypi-publish@master
40-
with:
41-
user: __token__
42-
password: ${{ secrets.TEST_PYPI_PASSWORD }}
43-
repository_url: https://test.pypi.org/legacy/
44-
- name: Publish package to PyPI
45-
uses: pypa/gh-action-pypi-publish@master
38+
- name: Store the distribution packages
39+
uses: actions/upload-artifact@v4
4640
with:
47-
user: __token__
48-
password: ${{ secrets.PYPI_PASSWORD }}
49-
- name: Push doc to stable
50-
if: success()
51-
run: |
52-
# Add deploy key and clone through ssh
53-
eval "$(ssh-agent -s)"
54-
mkdir ~/.ssh
55-
echo "${{ secrets.DEPLOY_DOC_KEY }}" > ~/.ssh/id_rsa
56-
chmod 600 ~/.ssh/id_rsa
57-
ssh-keyscan -t rsa github.com
58-
echo 'Documentation was successfully built, updating the website.'
59-
# See https://github.community/t/github-actions-bot-email-address/17204/5
60-
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
61-
git config --global user.name "github-actions"
62-
git clone "git@github.com:tensorly/tensorly.github.io.git" doc_folder
63-
echo "-- Updating the content"
64-
cd doc_folder
65-
cp -r dev/* stable/
66-
echo "Pushing to git"
67-
git add stable
68-
git commit -m "Github action: new release."
69-
git push --force origin main
41+
name: python-package-distributions
42+
path: dist/
43+
44+
publish-to-testpypi:
45+
name: Publish Python 🐍 distribution 📦 to TestPyPI
46+
needs:
47+
- build
48+
runs-on: ubuntu-latest
49+
50+
environment:
51+
name: testpypi
52+
url: https://test.pypi.org/p/tensorly
53+
54+
permissions:
55+
id-token: write
56+
57+
steps:
58+
- name: Download all the dists
59+
uses: actions/download-artifact@v4
60+
with:
61+
name: python-package-distributions
62+
path: dist/
63+
- name: Publish distribution 📦 to TestPyPI
64+
uses: pypa/gh-action-pypi-publish@release/v1
65+
with:
66+
repository-url: https://test.pypi.org/legacy/
67+
68+
publish-to-pypi:
69+
name: >-
70+
Publish Python 🐍 distribution 📦 to PyPI
71+
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
72+
needs:
73+
- build
74+
runs-on: ubuntu-latest
75+
environment:
76+
name: pypi
77+
url: https://pypi.org/p/tensorly # Replace <package-name> with your PyPI project name
78+
permissions:
79+
id-token: write # IMPORTANT: mandatory for trusted publishing
80+
81+
steps:
82+
- name: Download all the dists
83+
uses: actions/download-artifact@v4
84+
with:
85+
name: python-package-distributions
86+
path: dist/
87+
- name: Publish distribution 📦 to PyPI
88+
uses: pypa/gh-action-pypi-publish@release/v1
89+
- name: Push doc to stable if upload was successful
90+
if: success()
91+
run: |
92+
# Add deploy key and clone through ssh
93+
eval "$(ssh-agent -s)"
94+
mkdir ~/.ssh
95+
echo "${{ secrets.DEPLOY_DOC_KEY }}" > ~/.ssh/id_rsa
96+
chmod 600 ~/.ssh/id_rsa
97+
ssh-keyscan -t rsa github.com
98+
echo 'Documentation was successfully built, updating the website.'
99+
# See https://github.community/t/github-actions-bot-email-address/17204/5
100+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
101+
git config --global user.name "github-actions"
102+
git clone "git@github.com:tensorly/tensorly.github.io.git" doc_folder
103+
echo "-- Updating the content"
104+
cd doc_folder
105+
cp -r dev/* stable/
106+
echo "Pushing to git"
107+
git add stable
108+
git commit -m "Github action: new release."
109+
git push --force origin main

doc/modules/api.rst

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,14 @@ Other tensor algebraic functionalities:
290290
kronecker
291291
mode_dot
292292
multi_mode_dot
293-
proximal.soft_thresholding
294-
proximal.svd_thresholding
295-
proximal.procrustes
296293
inner
297294
outer
298295
batched_outer
299296
tensordot
300297
higher_order_moment
298+
proximal.soft_thresholding
299+
proximal.svd_thresholding
300+
proximal.procrustes
301301

302302
Tensor Algebra Backend
303303
----------------------
@@ -411,6 +411,26 @@ Tensor Regression (:mod:`tensorly.regression`)
411411
CP_PLSR
412412

413413

414+
Solvers (:mod:`tensorly.solvers`)
415+
=================================
416+
417+
Tensorly provides with efficient solvers for nonnegative least squares problems which are crucial to nonnegative tensor decomposition, as well as a generic admm solver useful for constrained decompositions. Several proximal (projection) operators are located in tenalg.
418+
419+
.. automodule:: tensorly.solvers
420+
:no-members:
421+
:no-inherited-members:
422+
423+
.. currentmodule:: tensorly.solvers
424+
425+
.. autosummary::
426+
:toctree: generated/
427+
:template: function.rst
428+
429+
nnls.hals_nnls
430+
nnls.fista
431+
nnls.active_set_nnls
432+
admm.admm
433+
414434
Performance measures (:mod:`tensorly.metrics`)
415435
==============================================
416436

examples/applications/plot_image_compression.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import matplotlib.pyplot as plt
99
import tensorly as tl
1010
import numpy as np
11-
from scipy.misc import face
11+
from scipy.datasets import face
1212
from scipy.ndimage import zoom
1313
from tensorly.decomposition import parafac
1414
from tensorly.decomposition import tucker

examples/decomposition/plot_guide_for_constrained_cp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
# Using one constraint for all modes
8181
# --------------------------------------------
8282
# Constraints are inputs of the constrained_parafac function, which itself uses the
83-
# ``tensorly.tenalg.proximal.validate_constraints`` function in order to process the input
83+
# ``tensorly.solver.proximal.validate_constraints`` function in order to process the input
8484
# of the user. If a user wants to use the same constraint for all modes, an
8585
# input (bool or a scalar value or list of scalar values) should be given to this constraint.
8686
# Assume, one wants to use unimodality constraint for all modes. Since it does not require

tensorly/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.8.1"
1+
__version__ = "0.9.0"
22

33
import sys
44

@@ -89,6 +89,7 @@
8989
norm,
9090
dot,
9191
kron,
92+
einsum,
9293
solve,
9394
lstsq,
9495
qr,
@@ -139,6 +140,7 @@
139140
from . import tenalg
140141
from . import random
141142
from . import datasets
143+
from . import solvers
142144

143145

144146
# Add Backend functions, dynamically dispatched

tensorly/backend/paddle_backend.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Sequence
33

44
from packaging.version import Version
5+
import warnings
56

67
try:
78
import paddle
@@ -23,14 +24,14 @@
2324
)
2425

2526

26-
if Version(paddle.__version__) < Version("2.6.0"):
27-
raise RuntimeError("TensorLy only supports paddle v2.6.0 and above.")
27+
if paddle.__version__ != "0.0.0" and Version(paddle.__version__) < Version("2.6.0"):
28+
raise RuntimeError(
29+
"TensorLy only supports paddle v2.6.0 and above, or the develop version, "
30+
f"but got {paddle.__version__}"
31+
)
2832

2933

3034
class PaddleBackend(Backend, backend_name="paddle"):
31-
# set default device to cpu
32-
place = paddle.device.set_device("cpu")
33-
3435
@staticmethod
3536
def context(tensor: paddle.Tensor):
3637
return {
@@ -67,15 +68,7 @@ def tensor(
6768
# If source is a tensor, use clone-detach as suggested by Paddle
6869
tensor = data.clone().detach()
6970
else:
70-
# Else, use Paddle's tensor constructor
71-
if place is None:
72-
# set default device to cpu when place is not specified
73-
# and gpu is avaiable
74-
current_device = paddle.device.get_device()
75-
if current_device.startswith("gpu"):
76-
place = "cpu"
77-
78-
tensor = paddle.to_tensor(data, place=place)
71+
tensor = paddle.to_tensor(data)
7972

8073
# Set dtype/place/stop_gradient if specified
8174
if dtype is not None:
@@ -249,9 +242,32 @@ def lstsq(a: paddle.Tensor, b: paddle.Tensor, rcond=None, driver="gelsd"):
249242
b = b.unsqueeze(-1)
250243

251244
m, n = a.shape[-2], a.shape[-1]
252-
sol, res, rank, single_value = paddle.linalg.lstsq(
253-
a, b, rcond=rcond, driver=driver
254-
)
245+
if driver != "gels" and paddle.device.get_device() != "cpu":
246+
fallback_device = "cpu"
247+
# NOTE: Paddle only support 'gels' on CUDA, so we use a.cpu() and b.cpu()
248+
# as input, then fall back to CPU lstsq implementation and show warnings.
249+
with paddle.base.dygraph.guard(fallback_device):
250+
sol, res, rank, single_value = paddle.linalg.lstsq(
251+
a.to(fallback_device),
252+
b.to(fallback_device),
253+
rcond=rcond,
254+
driver=driver,
255+
)
256+
# copy result back to current device
257+
current_device = paddle.device.get_device()
258+
sol = sol.to(current_device)
259+
res = res.to(current_device)
260+
rank = rank.to(current_device)
261+
single_value = single_value.to(current_device)
262+
warnings.warn(
263+
f"lstsq is falling back to {fallback_device} as the"
264+
f" specified driver '{driver}' is only supported on {fallback_device}, "
265+
"which may result in additional overhead."
266+
)
267+
else:
268+
sol, res, rank, single_value = paddle.linalg.lstsq(
269+
a, b, rcond=rcond, driver=driver
270+
)
255271
if m > n and driver != "gelsy":
256272
compute_residuals = True
257273
if driver in ["gelss", "gelsd"]:

tensorly/backend/tensorflow_backend.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def norm(tensor, order=2, axis=None):
8585
@staticmethod
8686
def solve(lhs, rhs):
8787
squeeze = False
88-
if rhs.ndim == 1:
88+
if tnp.ndim(rhs) == 1:
8989
squeeze = [-1]
9090
rhs = tf.reshape(rhs, (-1, 1))
9191
res = tf.linalg.solve(lhs, rhs)
@@ -117,9 +117,11 @@ def lstsq(a, b):
117117
return x, residuals if tf.linalg.matrix_rank(a) == n else tf.constant([])
118118

119119
def svd(self, matrix, full_matrices):
120-
"""Correct for the atypical return order of tf.linalg.svd."""
120+
"""Correct for the atypical return order of tf.linalg.svd.
121+
According to the https://www.tensorflow.org/api_docs/python/tf/linalg/svd#expandable-2
122+
not only the order is changed, but V is returned instead of V^H"""
121123
S, U, V = tf.linalg.svd(matrix, full_matrices=full_matrices)
122-
return U, S, tf.transpose(a=V)
124+
return U, S, tfm.conj(tf.transpose(a=V))
123125

124126
def index_update(self, tensor, indices, values):
125127
if not isinstance(tensor, tf.Variable):

tensorly/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def partial_unfold(tensor, mode=0, skip_begin=1, skip_end=0, ravel_tensors=False
113113
new_shape = [tensor.shape[i] for i in range(skip_begin)] + new_shape
114114

115115
if skip_end:
116-
new_shape += [tensor.shape[-i] for i in range(1, 1 + skip_end)]
116+
new_shape += [tensor.shape[-i] for i in range(skip_end, 0, -1)]
117117

118118
return tl.reshape(tl.moveaxis(tensor, mode + skip_begin, skip_begin), new_shape)
119119

tensorly/contrib/decomposition/_tt_cross.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -452,18 +452,15 @@ def maxvol(A):
452452

453453
# Find the row of max norm
454454
max_row_idx = tl.argmax(rows_norms, axis=0)
455-
max_row = A[rest_of_rows[max_row_idx], :]
455+
max_row = A_new[max_row_idx, :]
456456

457457
# Compute the projection of max_row to other rows
458-
# projection a to b is computed as: <a,b> / sqrt(|a|*|b|)
458+
# projection = <b, a>/|a|^2
459459
projection = tl.dot(A_new, tl.transpose(max_row))
460-
normalization = tl.sqrt(rows_norms[max_row_idx] * rows_norms)
461-
# make sure normalization vector is of the same shape of projection
462-
normalization = tl.reshape(normalization, tl.shape(projection))
463-
projection = projection / normalization
460+
projection = projection / (tl.sum(max_row**2))
464461

465462
# Subtract the projection from A_new: b <- b - a * projection
466-
A_new = A_new - A_new * tl.reshape(projection, (tl.shape(A_new)[0], 1))
463+
A_new = A_new - tl.tenalg.outer((projection, max_row))
467464

468465
# Delete the selected row
469466
mask.pop(tl.to_numpy(max_row_idx))

tensorly/decomposition/_constrained_cp.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from ._base_decomposition import DecompositionMixin
77
from ..base import unfold
88
from ..cp_tensor import CPTensor, cp_norm, validate_cp_rank
9-
from ..tenalg.proximal import admm, proximal_operator, validate_constraints
9+
from ..solvers.admm import admm
10+
from ..tenalg.proximal import proximal_operator, validate_constraints
1011
from ..tenalg.svd import svd_interface
1112
from ..tenalg import unfolding_dot_khatri_rao
1213

0 commit comments

Comments
 (0)