Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added linalg.pinv #48399

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fc302ba
Added torch.linalg.pinv
IvanYashchuk Nov 23, 2020
3eb8552
Merge remote-tracking branch 'upstream/master' into linalg-pinv
IvanYashchuk Dec 9, 2020
7dcb934
Removed duplicated tests
IvanYashchuk Dec 9, 2020
14a0c71
Added overloading for Tensor rcond
IvanYashchuk Dec 9, 2020
dec1212
Added tests for Tensor rcond and fixed implementation for this case
IvanYashchuk Dec 9, 2020
3170b60
Removed fixed xfailing tests
IvanYashchuk Dec 9, 2020
492b697
Use linalg_eigh instead of symeig
IvanYashchuk Dec 9, 2020
cae7cb2
Added OpInfo-based tests for pinv and pinv hermitian=True
IvanYashchuk Dec 9, 2020
94325ac
Updated docs, rcond now can be a Tensor
IvanYashchuk Dec 9, 2020
44d86c5
flake8
IvanYashchuk Dec 9, 2020
727baff
fix variant_test_name
IvanYashchuk Dec 9, 2020
0bb266e
Merge remote-tracking branch 'upstream/master' into linalg-pinv
IvanYashchuk Dec 16, 2020
52d3694
Restrict dtypes of linalg_inv
IvanYashchuk Dec 16, 2020
7537fb6
Updated comment for input.numel() == 0 case
IvanYashchuk Dec 16, 2020
b7a3eb3
Removed code 'Tensor rcond_ = rcond' and conditional for dim > 0
IvanYashchuk Dec 16, 2020
4d08f1e
Added parantheses
IvanYashchuk Dec 16, 2020
e758afd
Removed test_pinv_autograd
IvanYashchuk Dec 16, 2020
0800641
Added device checks and test_pinv_errors_and_warnings
IvanYashchuk Dec 16, 2020
4de9786
Restrict rcond to non-complex types and add test
IvanYashchuk Dec 16, 2020
4ac1eaf
Added all_types rcond dtype test cases
IvanYashchuk Dec 16, 2020
ed067a8
Updated docs
IvanYashchuk Dec 16, 2020
b8714d9
Added more rcond examples
IvanYashchuk Dec 16, 2020
6eb2f86
Expanded docstring for HermitianOpInfo
IvanYashchuk Dec 16, 2020
f5fd672
Merge remote-tracking branch 'upstream/master' into linalg-pinv
IvanYashchuk Dec 18, 2020
80be298
Use 'use_c10_dispatcher: hacky_wrapper_for_legacy_signatures'
IvanYashchuk Dec 18, 2020
fa57cf3
Fix CI failure: Torch not compiled with CUDA enabled
IvanYashchuk Dec 18, 2020
76ec22b
Fix CI failure: Torch not compiled with CUDA enabled #2
IvanYashchuk Dec 18, 2020
c0045fc
Merge remote-tracking branch 'upstream/master' into linalg-pinv
IvanYashchuk Dec 22, 2020
42882c3
Replace sample_inputs_pinverse with sample_inputs_linalg_pinv
IvanYashchuk Dec 22, 2020
7c1672a
Merge remote-tracking branch 'upstream/master' into linalg-pinv
IvanYashchuk Dec 22, 2020
609e931
svd does support automatic differentiation for outputs with complex d…
IvanYashchuk Dec 22, 2020
73f9dce
Changed torch.linalg.svd -> torch.svd; added svd reference in rcond d…
IvanYashchuk Dec 22, 2020
0507670
Merge branch 'master' into linalg-pinv
IvanYashchuk Jan 6, 2021
dbd65d4
Restore removed code after merge
IvanYashchuk Jan 6, 2021
48ac5dd
Merge branch 'master' into linalg-pinv
IvanYashchuk Jan 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 61 additions & 13 deletions aten/src/ATen/native/LinearAlgebra.cpp
Expand Up @@ -95,22 +95,70 @@ std::tuple<Tensor, Tensor> slogdet(const Tensor& self) {
return std::make_tuple(det_sign, abslogdet_val);
}

Tensor pinverse(const Tensor& self, double rcond) {
TORCH_CHECK((at::isFloatingType(self.scalar_type()) || at::isComplexType(self.scalar_type())) && self.dim() >= 2,
"pinverse(", self.scalar_type(), "{", self.sizes(), "}): expected a tensor with 2 or more dimensions "
Tensor linalg_pinv(const Tensor& input, const Tensor& rcond, bool hermitian) {
TORCH_CHECK((at::isFloatingType(input.scalar_type()) || at::isComplexType(input.scalar_type())) && input.dim() >= 2,
"linalg_pinv(", input.scalar_type(), "{", input.sizes(), "}): expected a tensor with 2 or more dimensions "
"of floating types");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"of floating types" is misleading. The requirement is that the tensors have a floating point dtype or a complex dtype.

Also, is this the right place to valid that input and rcond have the same dtype?

Also also, what happens when the dtype is bfloat16 or half?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I'm mistaken. It should not be that the dtype of input and rcond are the same.

Shouldn't it be that rcond must always be the "value type" of input? That is, if input is double rcond should be double, but if input is complex double then rcond should be double, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the torch check to accept only float, double, cfloat or cdouble types.

As for the rcond, I don't know actually how we should restrict its types. When only a float is given from Python interface it always gets converted to double in C++ and then we always create a scalar tensor of type double.
If directly a tensor is passed, then all types should be valid that allow multiplication with max_val tensor (that can be only of type float or double) and that allow comparison with a tensor of type float or double. So I guess any floating and integer types (so everything that is not complex) should be valid here for any allowed input?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added restriction for rcond type not to accept complex types 4de9786
And added tests to show that it works for all other types 4ac1eaf

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your analysis makes sense. Restricting it to not be complex sounds good.

if (self.numel() == 0) {
if (input.numel() == 0) {
// Match NumPy
auto self_sizes = self.sizes().vec();
std::swap(self_sizes[self.dim() - 1], self_sizes[self.dim() - 2]);
return at::empty(self_sizes, self.options());
auto input_sizes = input.sizes().vec();
mruberry marked this conversation as resolved.
Show resolved Hide resolved
std::swap(input_sizes[input.dim() - 1], input_sizes[input.dim() - 2]);
return at::empty(input_sizes, input.options());
}

Tensor rcond_ = rcond;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on what's happening here, and add a comment elaborating

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not needed here and I removed it. b7a3eb3

if (rcond.dim() > 0) {
rcond_ = rcond.unsqueeze(-1);
}
Tensor U, S, V;
std::tie(U, S, V) = self.svd();
Tensor max_val = at::narrow(S, /*dim=*/-1, /*start=*/0, /*length=*/1);
Tensor S_pseudoinv = at::where(S > rcond * max_val, S.reciprocal(), at::zeros({}, S.options())).to(self.dtype());
// computes V.conj() @ diag(S_pseudoinv) @ U.T.conj()
return at::matmul(V.conj() * S_pseudoinv.unsqueeze(-2), U.transpose(-2, -1).conj());

// If not Hermitian use singular value decomposition, else use eigenvalue decomposition
if (!hermitian) {
// until https://github.com/pytorch/pytorch/issues/45821 is resolved
mruberry marked this conversation as resolved.
Show resolved Hide resolved
// svd() returns conjugated V for complex-valued input
Tensor U, S, V_conj;
// TODO: replace input.svd with linalg_svd
std::tie(U, S, V_conj) = input.svd();
Tensor max_val = at::narrow(S, /*dim=*/-1, /*start=*/0, /*length=*/1); // singular values are sorted in descending order
Tensor S_pseudoinv = at::where(S > rcond_ * max_val, S.reciprocal(), at::zeros({}, S.options())).to(input.dtype());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parens around rcond_ * max_val so people don't need to worry about the binding of the comparison operator

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the .to(input.dtype()) actually necessary here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, .to(input.dtype()) is necessary because S is real-valued and we need it to convert to complex type to be able to do the matmul. at::matmul, and at::where don't work with mixed real and complex types.
Same discussion: #45819 (comment)

// computes V @ diag(S_pseudoinv) @ U.T.conj()
// TODO: replace V_conj.conj() -> V once https://github.com/pytorch/pytorch/issues/45821 is resolved
return at::matmul(V_conj.conj() * S_pseudoinv.unsqueeze(-2), U.conj().transpose(-2, -1));
} else {
Tensor S, U;
std::tie(S, U) = at::linalg_eigh(input);
// For Hermitian matrices, singular values equal to abs(eigenvalues)
Tensor S_abs = S.abs();
// eigenvalues are sorted in ascending order starting with negative values, we need a maximum value of abs(eigenvalues)
Tensor max_val = S_abs.amax(/*dim=*/-1, /*keepdim=*/true);
Tensor S_pseudoinv = at::where(S_abs > rcond_ * max_val, S.reciprocal(), at::zeros({}, S.options())).to(input.dtype());
// computes U @ diag(S_pseudoinv) @ U.conj().T
return at::matmul(U * S_pseudoinv.unsqueeze(-2), U.conj().transpose(-2, -1));
}
}

Tensor linalg_pinv(const Tensor& input, double rcond, bool hermitian) {
Tensor rcond_tensor = at::full({}, rcond, input.options().dtype(ScalarType::Double));
mruberry marked this conversation as resolved.
Show resolved Hide resolved
return at::linalg_pinv(input, rcond_tensor, hermitian);
}

// TODO: implement _out variant avoiding copy and using already allocated storage directly
Tensor& linalg_pinv_out(Tensor& result, const Tensor& input, const Tensor& rcond, bool hermitian) {
TORCH_CHECK(result.scalar_type() == input.scalar_type(),
"result dtype ", result.scalar_type(), " does not match the expected dtype ", input.scalar_type());

Tensor result_tmp = at::linalg_pinv(input, rcond, hermitian);
at::native::resize_output(result, result_tmp.sizes());
result.copy_(result_tmp);
return result;
}

Tensor& linalg_pinv_out(Tensor& result, const Tensor& input, double rcond, bool hermitian) {
Tensor rcond_tensor = at::full({}, rcond, input.options().dtype(ScalarType::Double));
mruberry marked this conversation as resolved.
Show resolved Hide resolved
return at::linalg_pinv_out(result, input, rcond_tensor, hermitian);
}

Tensor pinverse(const Tensor& self, double rcond) {
return at::linalg_pinv(self, rcond, /*hermitian=*/false);
}

Tensor& linalg_matrix_rank_out(Tensor& result, const Tensor& self, optional<double> tol, bool hermitian) {
Expand Down
26 changes: 26 additions & 0 deletions aten/src/ATen/native/native_functions.yaml
Expand Up @@ -9560,6 +9560,32 @@
dispatch:
Math: linalg_cond_out

- func: linalg_pinv(Tensor self, float rcond=1e-15, bool hermitian=False) -> Tensor
python_module: linalg
use_c10_dispatcher: full
variants: function
dispatch:
Math: linalg_pinv

- func: linalg_pinv.rcond_tensor(Tensor self, Tensor rcond, bool hermitian=False) -> Tensor
python_module: linalg
use_c10_dispatcher: full
variants: function
dispatch:
Math: linalg_pinv

- func: linalg_pinv.out(Tensor self, float rcond=1e-15, bool hermitian=False, *, Tensor(a!) out) -> Tensor(a!)
python_module: linalg
variants: function
dispatch:
Math: linalg_pinv_out

- func: linalg_pinv.out_rcond_tensor(Tensor self, Tensor rcond, bool hermitian=False, *, Tensor(a!) out) -> Tensor(a!)
python_module: linalg
variants: function
dispatch:
Math: linalg_pinv_out

- func: linalg_tensorinv(Tensor self, int ind=2) -> Tensor
python_module: linalg
variants: function
Expand Down
1 change: 1 addition & 0 deletions docs/source/linalg.rst
Expand Up @@ -19,5 +19,6 @@ Functions
.. autofunction:: eigvalsh
.. autofunction:: matrix_rank
.. autofunction:: norm
.. autofunction:: pinv
.. autofunction:: tensorinv
.. autofunction:: tensorsolve
99 changes: 99 additions & 0 deletions test/test_linalg.py
Expand Up @@ -2064,6 +2064,105 @@ def run_test_singular_input(batch_dim, n):
for params in [(1, 0), (2, 0), (2, 1), (4, 0), (4, 2), (10, 2)]:
run_test_singular_input(*params)

@precisionOverride({torch.float32: 1e-3, torch.complex64: 1e-3, torch.float64: 1e-7, torch.complex128: 1e-7})
@skipCUDAIfNoMagma
@skipCPUIfNoLapack
@dtypes(torch.float32, torch.float64, torch.complex64, torch.complex128)
def test_pinv(self, device, dtype):
from torch.testing._internal.common_utils import random_hermitian_pd_matrix

def run_test_main(A, hermitian):
# Testing against definition for pseudo-inverses
A_pinv = torch.linalg.pinv(A, hermitian=hermitian)
if A.numel() > 0:
self.assertEqual(A, A @ A_pinv @ A, atol=self.precision, rtol=self.precision)
self.assertEqual(A_pinv, A_pinv @ A @ A_pinv, atol=self.precision, rtol=self.precision)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines probably want rtol = 0

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mathematically yes, but it doesn't work with rtol=0. It fails for fp32 and complex64 and a larger rtol is needed for this test to pass. We do two matrix-matrix multiplications (A @ A_pinv @ A), which's really bad for fp32 if we compare element-wise. I think if compared in matrix norm these two objects would be very close to each other.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, no worries.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: we should support setting rtol and atol with the PrecisionOverride decorator.

self.assertEqual(A @ A_pinv, (A @ A_pinv).conj().transpose(-2, -1))
self.assertEqual(A_pinv @ A, (A_pinv @ A).conj().transpose(-2, -1))
else:
self.assertEqual(A.shape, A_pinv.shape[:-2] + (A_pinv.shape[-1], A_pinv.shape[-2]))

# Check out= variant
Copy link
Collaborator

@mruberry mruberry Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too bad the OpInfo's won't test out= because we don't have multi-tensor out= testing (yet).

out = torch.empty_like(A_pinv)
ans = torch.linalg.pinv(A, hermitian=hermitian, out=out)
self.assertEqual(ans, out)
self.assertEqual(ans, A_pinv)

def run_test_numpy(A, hermitian):
# Check against NumPy output
# Test float rcond, and specific value for each matrix
rconds = [float(torch.rand(1)), torch.rand(A.shape[:-2], dtype=torch.double, device=device)]
# Test broadcasting of rcond
if A.ndim > 2:
rconds.append(torch.rand(A.shape[-3], device=device))
for rcond in rconds:
actual = torch.linalg.pinv(A, rcond=rcond, hermitian=hermitian)
numpy_rcond = rcond if isinstance(rcond, float) else rcond.cpu().numpy()
expected = np.linalg.pinv(A.cpu().numpy(), rcond=numpy_rcond, hermitian=hermitian)
self.assertEqual(actual, expected)

for sizes in [(5, 5), (3, 5, 5), (3, 2, 5, 5), # square matrices
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome enumeration over sizes.

(3, 2), (5, 3, 2), (2, 5, 3, 2), # fat matrices
(2, 3), (5, 2, 3), (2, 5, 2, 3), # thin matrices
(0, 0), (0, 2), (2, 0), (3, 0, 0), (0, 3, 0), (0, 0, 3)]: # zero numel matrices
A = torch.randn(*sizes, dtype=dtype, device=device)
hermitian = False
run_test_main(A, hermitian)
run_test_numpy(A, hermitian)

# Check hermitian = True
for sizes in [(5, 5), (3, 5, 5), (3, 2, 5, 5), # square matrices
(0, 0), (3, 0, 0), ]: # zero numel square matrices
A = random_hermitian_pd_matrix(sizes[-1], *sizes[:-2], dtype=dtype, device=device)
hermitian = True
run_test_main(A, hermitian)
run_test_numpy(A, hermitian)

@skipCUDAIfNoMagma
@skipCPUIfNoLapack
@dtypes(torch.float64)
def test_pinv_autograd(self, device, dtype):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this adds an OpInfo then I don't think a custom autograd test is needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's not needed. I'll remove it.

from torch.testing._internal.common_utils import random_fullrank_matrix_distinct_singular_value

n = 5
for batches in ([], [2], [2, 3]):
# using .to(device) instead of device=device because @xwang233 claims it's faster
a = random_fullrank_matrix_distinct_singular_value(n, *batches, dtype=dtype).to(device)
a.requires_grad_()

def func(a, hermitian):
if hermitian:
a = a + a.conj().transpose(-2, -1)
return torch.linalg.pinv(a, hermitian=hermitian)

for hermitian in [False, True]:
gradcheck(func, [a, hermitian])
gradgradcheck(func, [a, hermitian])

# TODO: RuntimeError: svd does not support automatic differentiation for outputs with complex dtype.
# See https://github.com/pytorch/pytorch/pull/47761
@unittest.expectedFailure
@skipCUDAIfNoMagma
@skipCPUIfNoLapack
@dtypes(torch.complex128)
def test_pinv_autograd_complex_xfailed(self, device, dtype):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This failure test will be needed.

There are other failure cases that need to be tested for, too. Like rcond having the wrong dtype or the sizes of the inputs being incorrect.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added test_pinv_errors_and_warnings 0800641

from torch.testing._internal.common_utils import random_fullrank_matrix_distinct_singular_value

n = 5
batches = (2, 3)
# using .to(device) instead of device=device because @xwang233 claims it's faster
a = random_fullrank_matrix_distinct_singular_value(n, *batches, dtype=dtype).to(device)
a.requires_grad_()

def func(a, hermitian):
if hermitian:
a = a + a.conj().transpose(-2, -1)
return torch.linalg.pinv(a, hermitian=hermitian)

for hermitian in [False, True]:
gradcheck(func, [a, hermitian])
gradgradcheck(func, [a, hermitian])

def solve_test_helper(self, A_dims, b_dims, device, dtype):
from torch.testing._internal.common_utils import random_fullrank_matrix_distinct_singular_value

Expand Down
14 changes: 8 additions & 6 deletions test/test_ops.py
Expand Up @@ -29,13 +29,15 @@ class TestOpInfo(TestCase):
@onlyOnCPUAndCUDA
@ops(op_db, dtypes=OpDTypes.unsupported)
def test_unsupported_dtypes(self, device, dtype, op):
samples = op.sample_inputs(device, dtype)
if len(samples) == 0:
self.skipTest("Skipped! No sample inputs!")

# NOTE: only tests on first sample
sample = samples[0]
# sample_inputs can have a function for generating the input that doesn't work for specified dtype
# https://github.com/pytorch/pytorch/issues/49024
with self.assertRaises(RuntimeError):
samples = op.sample_inputs(device, dtype)
if len(samples) == 0:
self.skipTest("Skipped! No sample inputs!")

# NOTE: only tests on first sample
sample = samples[0]
op(*sample.input, *sample.args, **sample.kwargs)

# Verifies that ops have their supported dtypes
Expand Down
19 changes: 19 additions & 0 deletions torch/csrc/api/include/torch/linalg.h
Expand Up @@ -60,6 +60,14 @@ inline Tensor& matrix_rank_out(Tensor& result, const Tensor input, optional<doub
return torch::linalg_matrix_rank_out(result, input, tol, hermitian);
}

inline Tensor pinv(const Tensor& input, double rcond, bool hermitian) {
return torch::linalg_pinv(input, rcond, hermitian);
}

inline Tensor& pinv_out(Tensor& result, const Tensor& input, double rcond, bool hermitian) {
return torch::linalg_pinv_out(result, input, rcond, hermitian);
}

inline Tensor tensorinv(const Tensor& self, int64_t ind) {
return torch::linalg_tensorinv(self, ind);
}
Expand Down Expand Up @@ -150,6 +158,17 @@ inline Tensor& matrix_rank_out(Tensor& result, const Tensor input, optional<doub
return detail::matrix_rank_out(result, input, tol, hermitian);
}

/// Computes pseudo-inverse
///
/// See https://pytorch.org/docs/master/linalg.html#torch.linalg.pinv
inline Tensor pinv(const Tensor& input, double rcond=1e-15, bool hermitian=false) {
return detail::pinv(input, rcond, hermitian);
}

inline Tensor& pinv_out(Tensor& result, const Tensor& input, double rcond=1e-15, bool hermitian=false) {
return detail::pinv_out(result, input, rcond, hermitian);
}

/// Computes the inverse of a tensor
///
/// See https://pytorch.org/docs/master/linalg.html#torch.linalg.tensorinv
Expand Down
73 changes: 73 additions & 0 deletions torch/linalg/__init__.py
Expand Up @@ -483,6 +483,79 @@
>>> tensor([ 11.6734+0.j, 105.1037+0.j, 10.1978+0.j])
""")

pinv = _add_docstr(_linalg.linalg_pinv, r"""
linalg.pinv(input, rcond=1e-15, hermitian=False) -> Tensor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for @heitorschueroff, we should be sure to docs deprecate the old pinverse before 1.8.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NumPy version of this operator can throw a Runtime error:

https://numpy.org/doc/1.18/reference/generated/numpy.linalg.pinv.html?highlight=pinv#numpy.linalg.pinv

The other linear algebra functions that we've recently added also explain when they may throw runtime errors. Is there something we can document here, too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a note that if svd or eigh do not converge then the runtime error will be thrown. It's difficult to test these situations though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes that hard to test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find how to generate matrices that would make svd or eigh fail. They're quite robust.


Computes the pseudo-inverse (also known as the Moore-Penrose inverse) of a matrix :attr:`input`,
or of each matrix in a batched :attr:`input`.
The pseudo-inverse is computed using singular value decomposition (see :func:`torch.linalg.svd`) by default.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately torch.linalg.svd doesn't exist yet. This can reference torch.svd for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can change that.
I just thought that we should start using torch.linalg even if the references are not working for now.

While we're not actually using the newer linear algebra functions internally we should still reference them here.
#48206 (comment)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is nice to dream... but we should probably keep all the references in the docs working ;)

If :attr:`hermitian` is ``True``, then :attr:`input` is assumed to be Hermitian (symmetric if real-valued),
and the computation of the pseudo-inverse is done by obtaining the eigenvalues and eigenvectors
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this paragraph is very good. In the future we may want to elaborate on the precise computation using the outputs of the singular value decomposition or the eigenvalues and eigenvectors.

(see :func:`torch.linalg.eigh`).
The singular values (or the absolute eigenvalues when :attr:`hermitian` is ``True``) that are below
the specified :attr:`rcond` threshold are treated to be zero and discarded in the computation.
IvanYashchuk marked this conversation as resolved.
Show resolved Hide resolved

Supports input of ``float``, ``double``, ``cfloat`` and ``cdouble`` datatypes.
Copy link
Collaborator

@mruberry mruberry Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should verify that float16 and bfloat16 are not supported and throw the proper error message when given. See comments above.


.. note:: When given inputs on a CUDA device, this function synchronizes that device with the CPU.

Args:
input (Tensor): the input matrix of size :math:`(m, n)` or the batch of matrices of size :math:`(*, m, n)`
where `*` is one or more batch dimensions.
rcond (float, Tensor, optional): the tolerance value to determine the cutoff for small singular values. Default: 1e-15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One challenge with clearly documenting rcond is explaining what different tensor shapes do. Do you think we can elaborate on this? In particular, the shape of rcond must be broadcastable to the singular value tensor returned from torch.svd, right?

Is it worth including an rcond example where rcond isn't a scalar? Possibly by inspecting the singular value decomposition and then determining a cutoff?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, rcond must be broadcastable to the singular values. I think it's a good enough condition to mention and doesn't need further explanation?
Changed in b8714d9

hermitian(bool, optional): indicates whether :attr:`input` is Hermitian. Default: ``False``

Examples::

>>> input = torch.randn(3, 5)
>>> input
tensor([[ 0.5495, 0.0979, -1.4092, -0.1128, 0.4132],
[-1.1143, -0.3662, 0.3042, 1.6374, -0.9294],
[-0.3269, -0.5745, -0.0382, -0.5922, -0.6759]])
>>> torch.linalg.pinv(input)
tensor([[ 0.0600, -0.1933, -0.2090],
[-0.0903, -0.0817, -0.4752],
[-0.7124, -0.1631, -0.2272],
[ 0.1356, 0.3933, -0.5023],
[-0.0308, -0.1725, -0.5216]])

Batched linalg.pinv example
>>> a = torch.randn(2, 6, 3)
>>> b = torch.linalg.pinv(a)
>>> torch.matmul(b, a)
tensor([[[ 1.0000e+00, 1.6391e-07, -1.1548e-07],
[ 8.3121e-08, 1.0000e+00, -2.7567e-07],
[ 3.5390e-08, 1.4901e-08, 1.0000e+00]],

[[ 1.0000e+00, -8.9407e-08, 2.9802e-08],
[-2.2352e-07, 1.0000e+00, 1.1921e-07],
[ 0.0000e+00, 8.9407e-08, 1.0000e+00]]])

Hermitian input example
>>> a = torch.randn(3, 3, dtype=torch.complex64)
>>> a = a + a.t().conj() # creates a Hermitian matrix
>>> b = torch.linalg.pinv(a, hermitian=True)
>>> torch.matmul(b, a)
tensor([[ 1.0000e+00+0.0000e+00j, -1.1921e-07-2.3842e-07j,
5.9605e-08-2.3842e-07j],
[ 5.9605e-08+2.3842e-07j, 1.0000e+00+2.3842e-07j,
-4.7684e-07+1.1921e-07j],
[-1.1921e-07+0.0000e+00j, -2.3842e-07-2.9802e-07j,
1.0000e+00-1.7897e-07j]])

Non-default rcond example
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is tricky. Because we don't see the intermediate steps it's hard to understand the effect that rcond is having. The next example has the same issue.

What are your thoughts? I think we can leave out these examples for now since this PR seems to be almost ready to merge, otherwise, and maybe return to add them later?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says that the pseudo-inverse is calculated using SVD and it says that rcond determines which singular values should be set to zero. The first example demonstrates that using the default and some other value for rcond gives different results as expected. Just using the facts from the docs I think it's understandable that different rconds in general give different results. I think we should keep this example because I think examples should contain the code snippets demonstrating each variable. That's studying math to really understand the effect of rcond (topic of Tikhonov regularization) and definitely not needed here. We're on the level of "something in the input changes, something in the output should change as well".

The purpose of the second rcond example (Matrix-wise rcond example) is to show what shapes of rcond are acceptable. I agree that the second example doesn't look transparent and it might be difficult to understand, but that's broadcasting. Smaller arrays are extended to higher-dimensional arrays. We have rcond must be broadcastable to singular values of input. in the docs. I don't see how to help the user to understand this bit better.

How about keeping these examples as is (that's working and valid code) and return to it later to improve them?
I don't mind removing it now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mruberry, so should we remove those rcond examples now and think about it later or keep it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's fine, if you like them let's keep them.

>>> rcond = 0.5
>>> a = torch.randn(3, 3)
>>> torch.linalg.pinv(a)
tensor([[ 0.2971, -0.4280, -2.0111],
[-0.0090, 0.6426, -0.1116],
[-0.7832, -0.2465, 1.0994]])
>>> torch.linalg.pinv(a, rcond)
tensor([[-0.2672, -0.2351, -0.0539],
[-0.0211, 0.6467, -0.0698],
[-0.4400, -0.3638, -0.0910]])
""")

tensorinv = _add_docstr(_linalg.linalg_tensorinv, r"""
linalg.tensorinv(input, ind=2, *, out=None) -> Tensor

Expand Down
1 change: 1 addition & 0 deletions torch/overrides.py
Expand Up @@ -701,6 +701,7 @@ def get_testing_overrides() -> Dict[Callable, Callable]:
torch.pca_lowrank: lambda input, q=None, center=True, niter=2: -1,
torch.pdist: lambda input, p=2: -1,
torch.pinverse: lambda input, rcond=1e-15: -1,
torch.linalg.pinv: lambda input, rcond=1e-15, hermitian=False: -1,
torch.pixel_shuffle: lambda input, upscale_factor: -1,
torch.poisson: lambda input, generator=None: -1,
torch.poisson_nll_loss: lambda input, target, log_input, full, eps, reduction: -1,
Expand Down
2 changes: 2 additions & 0 deletions torch/testing/_internal/common_device_type.py
Expand Up @@ -172,6 +172,8 @@
def _construct_test_name(test_name, op, device_type, dtype):
if op is not None:
test_name += "_" + op.name.replace('.', '_')
if op.variant_test_name:
test_name += "_" + op.variant_test_name

test_name += "_" + device_type

Expand Down