Skip to content

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

Merged
rgommers merged 6 commits into
numpy:masterfrom
ContinuumIO:mult-norm
Jan 4, 2017
Merged

rgommers merged 6 commits into
numpy:masterfrom
ContinuumIO:mult-norm

Conversation

@cowlicks

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

OK, sorry for rehashing the old discussion.

@charris

charris commented Mar 27, 2015

Copy link
Copy Markdown
Member

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
Copy Markdown
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

charris commented Mar 27, 2015

Copy link
Copy Markdown
Member

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

@charris

charris commented Mar 27, 2015

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor Author

ping @josef-pkt

Comment thread numpy/random/mtrand/mtrand.pyx Outdated

Copy link
Copy Markdown

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

homu commented Jan 24, 2016

Copy link
Copy Markdown
Contributor

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

@rgommers

rgommers commented Jan 3, 2017

Copy link
Copy Markdown
Member

@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
Copy Markdown

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

rgommers commented Jan 3, 2017

Copy link
Copy Markdown
Member

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

Comment thread numpy/random/mtrand/mtrand.pyx Outdated
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
Copy Markdown
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

Comment thread numpy/random/mtrand/mtrand.pyx Outdated
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
Copy Markdown
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

Comment thread numpy/random/mtrand/mtrand.pyx Outdated
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

Copy link
Copy Markdown

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.

Comment thread numpy/random/mtrand/mtrand.pyx Outdated
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
Copy Markdown
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?

Comment thread numpy/random/mtrand/mtrand.pyx Outdated
RuntimeWarning)

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

Copy link
Copy Markdown
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
Copy Markdown
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?

Copy link
Copy Markdown

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.

@rgommers rgommers Jan 3, 2017 •

Copy link
Copy Markdown
Member

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
Copy Markdown
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

rgommers commented Jan 3, 2017

Copy link
Copy Markdown
Member

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

@ovillellas

Copy link
Copy Markdown

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

@rgommers

rgommers commented Jan 3, 2017

Copy link
Copy Markdown
Member

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
Copy Markdown

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

@charris

charris commented Jan 3, 2017

Copy link
Copy Markdown
Member

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

@ovillellas

Copy link
Copy Markdown

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

rgommers commented Jan 4, 2017

Copy link
Copy Markdown
Member

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