8000 MAINT: Fix the cutoff value inconsistency for pinv2 and pinvh by ilayn · Pull Request #10067 · scipy/scipy · GitHub
[go: up one dir, main page]

Skip to content

MAINT: Fix the cutoff value inconsistency for pinv2 and pinvh #10067

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

Merged
merged 7 commits into from
Apr 25, 2019

Conversation

ilayn
Copy link
Member
@ilayn ilayn commented Apr 16, 2019

Fixes #8861

The least square solver bound value is notoriously difficult to select due to the inherent trade-off between selecting the best minimizer for the ||Ax-b|| and the precision of x when b is in the image of the matrix A. Since pinv is a special least squares problem the usual bound of dtype eps is relaxed to have a factor of MAX(M, N). Otherwise the problem shown in the linked issue is hit more often than acceptable times.

On the other hand SVD based pseudo-inverses enjoy the convenience of having the singular values available for obtaining an upper bound on the error. In this PR we remove the hard-coded values of 1E3 and 1E6 factors for pinv2 and pinvh and tie them to the max(M,N)*sigma_max*eps. Probably for historical reasons the cutoff values were hardcoded.

Again, these values by no means provide a failsafe situation. However, they are kind of the standard values that serve well in most of the cases. Otherwise one can always provide a better cond value manually.

Here a possible backwards incompatibility can arise if a user relies on the resulting precision too heavily. But in my opinion, it repairs quite more than it might break.

@ilayn ilayn added scipy.linalg maintenance Items related to regular maintenance tasks labels Apr 16, 2019
@rgommers
Copy link
Member

Can you explain in the description in what cases this matters for backwards compatibility?

@tylerjereddy
Copy link
Contributor

@ilayn If you are comfortable assessing pinv related issues, note that NumPy has recently been plagued by sporadic / bizzare pinv failures: numpy/numpy#12862

Probably a little too optimistic to hope for any connection though.

@ilayn
Copy link
Member Author
ilayn commented Apr 16, 2019

@rgommers Done.

@tylerjereddy I'll have a look.

@ilayn
Copy link
Member Author
ilayn commented Apr 16, 2019

@tylerjereddy It looks like the same issue fixed here. For single precision (np.float32, np.complex64 etc.) pinv value is still 1e-15 by default and that causes mismatches almost every failed test very close the single precision eps (~1e-7).

If OpenBLAS folks touched these and their related LAPACK routines or number representations, they can flip to the left or to the right if they are sitting on the fence. Hence I am not so sure if it is compiler related.

Also, unfortunately, np.linalg.pinv is sp.linalg.pinv2 :(

@rgommers
Copy link
Member

Thanks @ilayn, clear description. Let's leave this open for a while to give people a chance to weigh in.

@rgommers rgommers added the needs-decision Items that need further discussion before they are merged or closed label Apr 16, 2019
@ilayn
Copy link
Member Author
ilayn commented Apr 21, 2019

If there are no more comments on this, I'd like to merge it tomorrow.

@rgommers rgommers removed the needs-decision Items that need further discussion before they are merged or closed label Apr 21, 2019
Copy link
Member
@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Looks good to me. Probably worth a note in the backward incompatible changes release notes section, too, though.

Also some tests that show things work better now than on master would help if it's not too much work. Maybe something simple like pinv, pinvh, and pinv2 (good case for parametrize) on a 2x2 double array with 1 and 1e-10 in the diagonal?

@ilayn
Copy link
Member Author
ilayn commented Apr 21, 2019

Ah yes. Good idea.

@ilayn
Copy link
Member Author
ilayn commented Apr 22, 2019

@larsoner pinvh is basically pinv2 with different solver so I only compared pinv and pinv2. I couldn't replicate a symmetric matrix that exhibits the issue but let me know if you have a better example to test with

@tylerjereddy If there is still enough time please consider this to add to 1.3.0

@ilayn
Copy link
Member Author
ilayn commented Apr 22, 2019

Sigh... pypy is not having a good day again. @mattip Any suggestions?

@mattip
Copy link
Contributor
mattip commented Apr 22, 2019

This is using NumPy 1.16.3 I assume, which was released yesterday and added a new C-API function. I will issue a PR to go back to using nightly builds rather than official releases, since PyPy fixed when it was merged to NumPy but it is not in a release yet..

@mattip
Copy link
Contributor
mattip commented Apr 22, 2019

Maybe better would be to pin the NumPy used in the PyPy CI run to the version used in the other CI runs.

Edit: travis runs seem to use a number of different NumPy versions, including latest.

@ilayn ilayn added this to the 1.3.0 milestone Apr 24, 2019
@ilayn
Copy link
Member Author
ilayn commented Apr 24, 2019

@tylerjereddy I forgot to add the milestone to this if @larsoner is happy with the changes it is ok to merge.

Copy link
Member
@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

pinvh is basically pinv2 with different solver so I only compared pinv and pinv2. I couldn't replicate a symmetric matrix that exhibits the issue but let me know if you have a better example to test with

What I meant above about "a 2x2 double array with 1 and 1e-10 in the diagonal" is something like the following, which should fail on master for pinvh and pinv2 (and succeed for pinv) but all pass on your PR (at least when I tested it as a script, now generalized to the pytest case, it did I think):

@pytest.mark.parametrize('scale', (1e-20, 1., 1e20))
@pytest.mark.parametrize('pinv_', (pinv, pinvh, pinv2))
def test_auto_rcond(scale, pinv_):
    x = np.array([[1, 0], [0, 1e-10]]) * scale
    expected = np.diag(1. / np.diag(x))
    x_inv = pinv_(x)
    assert_allclose(x_inv, expected)

@tylerjereddy
Copy link
Contributor

I think we should bump the milestone--looks like there's still some active discussion / revision going on here.

@tylerjereddy tylerjereddy modified the milestones: 1.3.0, 1.4.0 Apr 24, 2019
@ilayn 8000
Copy link
Member Author
ilayn commented Apr 24, 2019

Give me an hour and it will be done :)

@ilayn
Copy link
Member Author
ilayn commented Apr 24, 2019

@larsoner After having sufficient coffee I can give a better response: Now if the cond arg is given it is respected and we only interfere if both are None which is what this PR is fixing.

Also thank you for tests indeed it fails on master and passes this PR. Hence please have a look.

But I think the terminology is wrong in the inversion context; just as an example https://nl.mathworks.com/help/matlab/ref/rcond.html

Copy link
Member
@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Apologies for being a pain here, but I'm not sure the docstring rewordings help the problem here.

@ilayn
Copy link
Member Author
ilayn commented Apr 25, 2019

OK I thought [ci skip] would stop these and not the CircleCI sorry about that,

Copy link
Member
@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Thanks for iterating, wording LGTM now

@ilayn
Copy link
Member Author
ilayn commented Apr 25, 2019

Thanks for keeping it sane. I really confused myself with this one 😃

@ilayn ilayn merged commit c42462a into scipy:master Apr 25, 2019
@ilayn ilayn deleted the pinv2_eps branch April 25, 2019 13:16
@tylerjereddy tylerjereddy modified the milestones: 1.4.0, 1.3.0 Apr 25, 2019
@tylerjereddy
Copy link
A3E2
Contributor

I restored the milestone to 1.3.0 & did the same for the cognate issue. If this deserves a release note please feel free to add one on the wiki page.

@ilayn
Copy link
Member Author
ilayn commented Apr 25, 2019

Thanks Tyler, wiki page edited.

@GillesOrban
Copy link

Dear member of scipy,

Using pinv2 and wanted to fix the relative conditioning number with rcond, I saw in the code that rcond and cond have the same behavior - as far as I can tell.
Not sure if this is indented.
My suggestion would be to replace in line 1372
cond = rcond
by
cond = rcond * np.max(s)

Thank you for supporting scipy,

Gilles

@ilayn
Copy link
Member Author
ilayn commented Nov 8, 2019

cond = rcond only if it is not None otherwise it has the max(s) by default

@ogrisel
Copy link
Contributor
ogrisel commented Mar 15, 2021

cond = rcond only if it is not None otherwise it has the max(s) by default

I agree with @GillesOrban. @ilayn by reading the lines: https://github.com/scipy/scipy/pull/10067/files#diff-8400a871af5e3ff38643aff36ff192650313e769cdfe80f57b02140def686ee1R1368-R1377 one can see that:

  • if rcond is not None and cond is None, then cond = rcond and rank = np.sum(s > cond) or equivalently rank = np.sum(s > rcond)
  • if rcond is None and cond is not None, then rank = np.sum(s > cond) directly.

Therefore those two arguments mean the same thing in practice.

Another way to frame it: there is no way to pass xcond and have rank = np.sum(s > xcond * np.max(s)) (I am not sure if such an xcond should actually be named cond or rcond).

Finally the docstring does not explain what is the (expected) difference between those two arguments.

@ilayn
Copy link
Member Author
ilayn commented Mar 16, 2021

Arrrgh, this issue still haunts me 🤦

@ogrisel I think these keywords were incorrect to start with, as you mentioned. I'll come back to that in a sec.

However I think there is a discrepancy in that rcond is not a relative condition number as in rtol to atol but the reciprocal condition number. So the functionality you want to pass is essentially a rtol, based on the largest singular value and makes sense (That's basically what the default value is trying to do in this PR). rcond and cond were placed as the condition number wrt inversion which is essentially an atol for cutoff and that's where the hardcoded numbers were placed.

Back to keywords, ideally we shouldn't have had these wrongly named keywords, shortcut together behind the scenes but I didn't touch it for backwards compatibility. However I do agree that the naming is confusing. And if you want to have the relative cutoff value I would really appreciate a new issue/feature request as here not many people will see it. And more people can chime in. Probably we should have gone for rtol and atol with better names.

Hopefully this time we can fix it for good and be done with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintenance Items related to regular maintenance tasks scipy.linalg
Projects
None yet
Development

Successfully merging this pull request may close these issues.

scipy.linalg.pinv gives wrong result while scipy.linalg.pinv2 works
7 participants
0