Skip to content

BUG: Fix eigh and cholesky methods of numpy.random.multivariate_normal#15872

Merged
charris merged 1 commit intonumpy:masterfrom
Balandat:fix_eigh_mvn_sampling
Apr 5, 2020
Merged

BUG: Fix eigh and cholesky methods of numpy.random.multivariate_normal#15872
charris merged 1 commit intonumpy:masterfrom
Balandat:fix_eigh_mvn_sampling

Conversation

@Balandat
Copy link
Contributor

@Balandat Balandat commented Mar 31, 2020

Fixes #15871. The eigh and cholesky methods were not producing results with the correct covariance.

@eric-wieser
Copy link
Member

Needs a test. Would be worth checking why CI is failing too.

@seberg seberg self-requested a review March 31, 2020 14:43
@charris charris dismissed their stale review March 31, 2020 14:52

The explanation of the rest of the implementation was convincing.

@charris
Copy link
Member

charris commented Mar 31, 2020

@eric-wieser

Because the generators may change, the simplest test might be some rough statistics on the result. What do you suggest?

@Balandat
Copy link
Contributor Author

Re the test: We can just use the singular matrix from my example and then check that the first two samples are approx. equal for svd and eigh. Not sure what other things are tested already, but we should probably at least do some checks on the (marginal) variances and means if that is not done yet.

I do think that we should be consistent about the factor - typically the correlation is done as y = F @ x s.t. E[y y.T] = E[F @ x @ x.T @ F.T] = F @ E[x @ x.T] @ F.T = F @ F.T, so we should have F @ F.T = cov.

Note also that if many evals are zero, we could remove these evecs (or columns of U,V for svd) and work with sth like the following:

def eigh_sample(mean, cov, cutoff=1e-10):
    evals, evecs = np.linalg.eigh(cov)
    pos = evals > cutoff
    evecs_pos = evecs[:, pos]
    base_samples = rg.normal((np.sum(pos), 1))
    scale = np.sqrt(evals[pos])
    return mean + (scale * evecs_pos) @ base_samples

Not sure if it's worth doing this if we re-compute the eigendecomposition/svd every time though.

@charris
Copy link
Member

charris commented Mar 31, 2020

Not sure if it's worth doing this if we re-compute the eigendecomposition/svd every time though.

There was some discussion of adding the option of passing the left-hand factor, but that actually seems more appropriate to a class based implementation, that is, one could ask for a generator that returned multivariate normals with given covariance. If I needed a bunch of variates, that is how I would do it, but that might not be appropriate for NumPy.

@Balandat
Copy link
Contributor Author

I agree this probably doesn't belong into numpy. I'm surprised scipy's mvn for "frozen" rvs doesn't deal with this, looks like the matrix decomposition is recomputed for each sample here: https://github.com/scipy/scipy/blob/v1.4.1/scipy/stats/_multivariate.py#L660

Torch's MVN does store the factor (though it doesn't allow singular matrices): https://github.com/pytorch/pytorch/blob/master/torch/distributions/multivariate_normal.py#L146-L15

@charris
Copy link
Member

charris commented Mar 31, 2020

I'm surprised scipy's mvn for "frozen" rvs doesn't deal with this

Might be worth opening a SciPy issue.

@charris
Copy link
Member

charris commented Mar 31, 2020

but we should probably at least do some checks on the (marginal) variances and means if that is not done yet.

That is what I was thinking. It shouldn't require too large a sample to get sufficient accuracy to catch big errors like the current one. I would suggest starting with an L and passing L @ L.T

EDIT: Or even just generating a stream with L, then resetting the generator state and passing L @ L.T. The resulting streams should be pretty similar and it will avoid problems with changing generators.

@Balandat
Copy link
Contributor Author

Might be worth opening a SciPy issue.

scipy/scipy#11772

@charris charris changed the title Bug: Fix eigh method of multivariate_normal rng in numpy.random BUG: Fix eigh method of multivariate_normal rng in numpy.random Apr 2, 2020
@Balandat Balandat force-pushed the fix_eigh_mvn_sampling branch from 52fecae to 796d4a2 Compare April 4, 2020 15:25
@Balandat
Copy link
Contributor Author

Balandat commented Apr 4, 2020

Changed to transpose on trailing dims. Added generator-agnostic test for singular covariances. Maybe someone else can take on additional tests checking some basic statistics on the generated samples in a separate PR.

@charris
Copy link
Member

charris commented Apr 4, 2020

I think the test is too special cased. I think that a rough check the statistics all methods would be useful. For instance, looks like cholesky also yields wrong results.

In [36]: b = default_rng(123).multivariate_normal([0, 0], cov, size=(1000,), method='svd')                                              

In [37]: (b.T @ b)/1000                                                                                                                 
Out[37]: 
array([[1.06677611, 1.05240103],
       [1.05240103, 2.04555217]])

In [38]: b = default_rng(123).multivariate_normal([0, 0], cov, size=(1000,), method='eigh')                                             

In [39]: (b.T @ b)/1000                                                                                                                 
Out[39]: 
array([[ 0.38497443, -0.0150749 ],
       [-0.0150749 ,  2.79194006]])

In [40]: b = default_rng(123).multivariate_normal([0, 0], cov, size=(1000,), method='cholesky')                                         

In [41]: (b.T @ b)/1000                                                                                                                 
Out[41]: 
array([[2.14015452, 1.06968605],
       [1.06968605, 1.03675997]])

In [42]: cov                                                                                                                            
Out[42]: 
array([[1., 1.],
       [1., 2.]])

In [43]: numpy.__version__                                                                                                              
Out[43]: '1.19.0.dev0+f1a247f'

@Balandat
Copy link
Contributor Author

Balandat commented Apr 4, 2020

Oh so the Cholesky samples are broken too. We may as well just go all in on the _factor @ x representation then and only transpose the svd factor

@charris
Copy link
Member

charris commented Apr 4, 2020

We will need a release note to go with this. You can see how to do this by looking at the current release note files in doc/release/upcoming_changes/.

@Balandat Balandat force-pushed the fix_eigh_mvn_sampling branch 2 times, most recently from 3a94a57 to 259e57b Compare April 4, 2020 17:28
@Balandat
Copy link
Contributor Author

Balandat commented Apr 4, 2020

Changed representation internally. Was easiest to use einsum for the output, hopefully that's ok. Added a test that is pretty loose and should only catch significant errors. At a later stage one could do some proper hypothesis testing with an extremely low significance level to avoid flaky tests.
Added upcoming_changes.

@Balandat Balandat force-pushed the fix_eigh_mvn_sampling branch from 259e57b to 0254bb3 Compare April 4, 2020 18:03
@Balandat Balandat changed the title BUG: Fix eigh method of multivariate_normal rng in numpy.random BUG: Fix eigh and cholesky methods of numpy.random.multivariate_normal Apr 4, 2020
@mattip
Copy link
Member

mattip commented Apr 4, 2020

Should the tests cover the new numpy.random.default_rng as well, or are there already similar tests elsewhere?

@Balandat
Copy link
Contributor Author

Balandat commented Apr 4, 2020

not sure about other rngs, maybe someone who knows the codebase better can clarify?

Copy link
Member

Choose a reason for hiding this comment

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

It would be better to use a separate test here, say test_multivariate_normal_stats or some such, that way you can use a new (seeded) rng and and the stats will be independent of possible changes in other parts of the test. This test will fail about 35 times out of a 1000 if the random stream is random. It will also allow marking it as @pytest.mark.slow if we want to do better statistics, currently it is around .6 seconds, which is already a bit slow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean using a fixed seed and checking the samples will do the trick for validating correctness of the formulation. The statistics test is really more and end-to-end integration test. That should really be done in a "statistical unit testing" setting where you run the test a bunch of times under a hypothesis test and then see that you get reasonably close to your selected significance level across many runs. But that shouldn't be part of the regular test suite.

Copy link
Member

Choose a reason for hiding this comment

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

But that shouldn't be part of the regular test suite.

I would support adding a validation test suite at some point. But for now we could keep this and move it later. It did uncover the Cholesky problem and would have caught the eigh problem.

The various random functions were tested for statistical accuracy many years ago, we should really have tests somewhere that we could use to revalidate the results since there have been changes since. Hmm, that could almost be a GSOC project next time that rolls around, not that statistical tests are the least bit trivial but the problem is well stated. Validating the generators themselves might be worth something just to make sure we didn't drop the ball somewhere, after all, there was the buggy generation of low precision integers that we should have caught earlier.

@bashtage @rkern Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed.

Copy link
Member

Choose a reason for hiding this comment

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

The problem here is that u is not necessarily vh.T for the svd, if there are negative eigenvalues from numerical error, there will be a sign change. Hmm... if the cov is valid and the negative eigenvalue is due to numerical error, i.e., it is really tiny, then it should have hardly any impact on the output, so this is probably OK. Might mention the possibility in the release note.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean the same is true for the previous formulation, right?

Copy link
Member

Choose a reason for hiding this comment

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

Yep, it would give incorrect results in that case, but different incorrect results :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok let's make sure our errors are correct then

@Balandat Balandat force-pushed the fix_eigh_mvn_sampling branch from 0254bb3 to f6e5fe2 Compare April 4, 2020 21:25
@Balandat
Copy link
Contributor Author

Balandat commented Apr 4, 2020

Using x @ _factor.T, splitting out the basic stats test & relaxing tolerances.

Copy link
Member

Choose a reason for hiding this comment

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

random = Generator(MT19937(self.seed))

Otherwise it uses the global unseeded generator defined at the top of the file. We should probably get rid of that at some point.

Copy link
Member

@charris charris Apr 4, 2020

Choose a reason for hiding this comment

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

This could be as tight as before if it works with the seeded generator, it should be repeatable.

@Balandat Balandat force-pushed the fix_eigh_mvn_sampling branch from f6e5fe2 to 72457f0 Compare April 5, 2020 01:27
@charris
Copy link
Member

charris commented Apr 5, 2020

Looks good, let's see if the tests pass.

@charris charris merged commit 569dfcd into numpy:master Apr 5, 2020
@charris
Copy link
Member

charris commented Apr 5, 2020

Thanks @Balandat. It is very nice to have those errors fixed and tested.

@bashtage
Copy link
Contributor

bashtage commented Apr 5, 2020

@charris It would be a good idea. @WarrenWeckesser discovered a casting issue using some Monte Carlo methods IIRC. It would make sense to start with the things that are "newer", e.g., the Ziggurat exponentials, gamma, normals, and the bounded integers. These are all widely used in other distributions.

@Balandat Balandat deleted the fix_eigh_mvn_sampling branch April 5, 2020 15:55
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Apr 6, 2020
@charris charris removed this from the 1.18.3 release milestone Apr 6, 2020
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.

eigh and cholesky methods in multivariate_normal produce incorrect samples

7 participants