8000 BUG, API: np.random.multivariate_normal behavior with bad covariance matrix by cowlicks · Pull Request #5726 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

BUG, API: np.random.multivariate_normal behavior with bad covariance matrix #5726

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 6 commits into from
Jan 4, 2017

Conversation

cowlicks
Copy link
Contributor

Edit:

This now allows for PSD checking with a check_valid keyword arg that can take the values 'warn', 'raise', and 'ignore'.

It also allows the covariance matrix to be almost PSD up to some small errors.


There have been several old discussions about how np.random.multivariate_normal should behave when passed a "bad" covariance matrix. i.e. non-symmetric, or not Positive semi-definite (PSD). The reason people need this is because the covariance matrix might have roundoff error making it not strictly symmetric or PSD.

In the discussions there was some consensus to fix things the way I do in this PR. I added a keyword arg for ignoring the PSD check and silencing the warning. I also added a keyword arg that ignores the new symmetry check and silences the warning. Previously symmetry was not checked at all.

I'd like to go one step further and raise ValueError when the covariance matrix isn't symmetric or PSD, of course, with the keyword args there so this can be bypassed. What do y'all think about that?

old discussions:
#1821
http://mail.scipy.org/pipermail/numpy-discussion/2009-September/045255.html

@argriffing
Copy link
Contributor

Just as a technical comment, is there a reason to use svd instead of eigh here? The covariance matrix has already been constructed, and the line
neg = (np.sum(u.T * v, axis=1) < 0) & (s > 0)
looks weird to me. It seems like it should just directly check the approximate signs of eigenvalues instead, avoiding this matrix-matrix product.

@cowlicks
Copy link
Contributor Author

The was a discussion for switching to eigh, or cholesky. eigh seems better. here but that seemed out of scope of this PR, but it is on my todo list.

In the code there is a comment that says We continue to use the SVD rather than Cholesky in order to preserve current outputs.. I'm guessing using eigh or Cholesky would change the results to some number of sig figs.

@argriffing
Copy link
Contributor

OK, sorry for rehashing the old discussion.

@charris
Copy link
Member
charris commented Mar 27, 2015

Needs to be discussed on the list. Another option is to add a method keyword and an eigh method that checks for symmetry by default. The semidefinite property is not well defined for floating point for small eigenvalues due to roundoff, so there is the question of what to do in that case. It would also probably be easier to add a single check keyword with boolean values and either go as is or check both symmetry and positive semidefinite.

@cowlicks
Copy link
Contributor Author

@charris I'll ping the list.

On Travis-CI a RuntimeWarning isn't being caught even though it is gaurded by with warings.catch_warnings .... Failure here, and guarding code here.

This doesn't happen on my local machine, what is different on Travis-CI?

@charris
Copy link
Member
charris commented Mar 27, 2015

Try warnings.simplefilter("always") as the first line in the managed context.

@charris
Copy link
Member
charris commented Mar 27, 2015

Note that symettry and almost positive semidefinite can be checked with the svd. Something like

In [16]: a = eye(3)

In [17]: u, d, v = svd(a)

In [18]: b = dot(v.T*d, v)

In [19]: allclose(a, b)
Out[19]: True

The tolerances for allclose can be adjusted.

@cowlicks
Copy link
Contributor Author

@charris It didn't generate much discussion on list (link), but I think @josef-pkt's comments were helpful. To summarize, instead of seperate kwargs for checking symmetry, and PSDness, just one kwarg check_valid= that takes 'warn', 'raise', and 'ignore'. And it will only assert symmetry up to some reasonable tolerance (instead of exact).

@cowlicks
Copy link
Contributor Author

@charris Rehashed this. Basically we now allow for small floating point errors in the covariance matrix. And I added a check_valid key word that lets you choose to raise, warn, or ignore if the covariance matrix is PSD.

@cowlicks
Copy link
Contributor Author

ping @josef-pkt

8000
(u, s, v) = svd(cov)
neg = (np.sum(u.T * v, axis=1) < 0) & (s > 0)

Choose a reason for hiding this comment

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

I would be the entire next part under
if check_valid != 'ignore':

no reason to check if we 'ignore'

cowlicks added 2 commits May 19, 2015 16:21
In np.random.multivariate_normal allow the covariance matrix to have
small floating point errors. And allow control over what to do if the
PSD check fails.
Test that warnings are or are not raised when check_valid kwarg is used.
@homu
Copy link
Contributor
homu commented Jan 24, 2016

☔ The latest upstream changes (presumably #7082) made this pull request unmergeable. Please resolve the merge conflicts.

@rgommers
Copy link
Member
rgommers commented Jan 3, 2017

@ovillellas pushed 0 commits.

You managed to confuse GitHub, haven't seen that before:)

Have you taken over this PR, and is it ready to merge?

@ovillellas
Copy link

Yes, I just took over this PR. It should be ready to commit (it was just a matter of cleaning up a bit the test).

@rgommers
Copy link
Member
rgommers commented Jan 3, 2017

Okay thanks. I'll review it now then, before we forget about it again.

@@ -4380,6 +4381,10 @@ cdef class RandomState:
generated, and packed in an `m`-by-`n`-by-`k` arrangement. Because
each sample is `N`-dimensional, the output shape is ``(m,n,k,N)``.
If no shape is specified, a single (`N`-D) sample is returned.
check_valid : 'warn', 'raise', 'ignore'
Copy link
Member

Choose a reason for hiding this comment

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

formatting: change to {'warn', 'raise', 'ignore'}, optional

@@ -4380,6 +4381,10 @@ cdef class RandomState:
generated, and packed in an `m`-by-`n`-by-`k` arrangement. Because
each sample is `N`-dimensional, the output shape is ``(m,n,k,N)``.
If no shape is specified, a single (`N`-D) sample is returned.
check_valid : 'warn', 'raise', 'ignore'
Behavior when the covariance matrix is not Positive Semi-definite.
Copy link
Member

Choose a reason for hiding this comment

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

style nitpick: use lower case for positive semidefinite

< 8000 !-- -->

@@ -4380,6 +4381,10 @@ cdef class RandomState:
generated, and packed in an `m`-by-`n`-by-`k` arrangement. Because
each sample is `N`-dimensional, the output shape is ``(m,n,k,N)``.
If no shape is specified, a single (`N`-D) sample is returned.
check_valid : 'warn', 'raise', 'ignore'
Behavior when the covariance matrix is not Positive Semi-definite.
tol : float
Copy link
Member

Choose a reason for hiding this comment

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

float, optional

if mean.shape[0] != cov.shape[0]:
raise ValueError("mean and cov must have same length")
raise ValueError("mean and cov must have same length")

Copy link
Member

Choose a reason for hiding this comment

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

Can you add a check for check_valid being one of the 3 accepted strings?

Copy link
Member

Choose a reason for hiding this comment

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

actually, move it from line 4511 up here. It's misplaced there; only checked for when not psd.

Copy link
Member

Choose a reason for hiding this comment

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

that looks good now

Choose a reason for hiding this comment

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

note that the check is itself later in the code, when checking. However, it may go unchecked when the psd passes, so I have rearranged the logic so it is always checked.

check_valid : 'warn', 'raise', 'ignore'
Behavior when the covariance matrix is not Positive Semi-definite.
tol : float
Tolerance of the singular values in covariance matrix.
Copy link
Member

Choose a reason for hiding this comment

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

absolute or relative tolerance?

RuntimeWarning)

if check_valid != 'ignore':
psd = np.allclose(np.dot(v.T * s, v), cov)
Copy link
Member

Choose a reason for hiding this comment

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

tol should be used here. It's not used anywhere now.

Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test with non-default tol for good measure?

Choose a reason for hiding this comment

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

I realize. I wonder if tol should be changed to atol and rtol and just forward them in here.

Copy link
Member
@rgommers rgommers Jan 3, 2017

Choose a reason for hiding this comment

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

You'll have to specify both atol and rtol (here, not in the signature), because the defaults are both nonzero: rtol=1e-05, atol=1e-08

Copy link
Member

Choose a reason for hiding this comment

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

Sorry my comment wasn't a reply to yours, non-updating interface.

atol = rtol = tol seems okay. atol is the only one of interest near covariance values of 0, and rtol may do something useful for very large values.

@rgommers
Copy link
Member
rgommers commented Jan 3, 2017

Overall LGTM, let's get this merged once comments are addressed.

@ovillellas
Copy link

Ok, just pushed the documentation changes.

Also:

  • changed logic handling check_valid so it will always raise on invalid argument even if it cov is not psd.
  • changed tol parameter to atol and rtol, that are mapped straight into np.allclose in the code.

updated the docstring accordingly

10000

@rgommers
Copy link
Member
rgommers commented Jan 3, 2017

changed tol parameter to atol and rtol, that are mapped straight into np.allclose in the code.

I wonder if that's better. Realistically no one will use rtol for anything interesting, so likely adding only one keyword is better. What do you think about atol = rtol = tol?

@ovillellas
Copy link

Ok, I will change it back to the single tol and pass it as atol and rtol

@charris
Copy link
Member
charris commented Jan 3, 2017

Needs a mention in doc/release/1.13.0-notes.rst.

@ovillellas
Copy link

reverted to a single tol argument. I also added a mention in the release notes under "changes"

@rgommers rgommers merged commit b94c2b0 into numpy:master Jan 4, 2017
@rgommers
Copy link
Member
rgommers commented Jan 4, 2017

Okay, in it goes! Thanks @ovillellas, @cowlicks, all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants
0