Source code for pints.toy._gaussian
#
# Unimodal Normal/Gaussian toy log pdf.
#
# This file is part of PINTS (https://github.com/pints-team/pints/) which is
# released under the BSD 3-clause license. See accompanying LICENSE.md for
# copyright notice and full license details.
#
import numpy as np
import scipy.stats
from . import ToyLogPDF
from .. import vector
[docs]
class GaussianLogPDF(ToyLogPDF):
"""
Toy distribution based on a multivariate (unimodal) Normal/Gaussian
distribution.
Extends :class:`pints.toy.ToyLogPDF`.
Parameters
----------
mean
The distribution mean (specified as a vector).
sigma
The distribution's covariance matrix. Can be given as either a matrix
or a vector (in which case ``diag(sigma)`` will be used. Should be
symmetric and positive-semidefinite.
"""
def __init__(self, mean=[0, 0], sigma=[1, 1]):
# Copy and convert
mean = np.array(mean, copy=True)
sigma = np.array(sigma, copy=True)
# Check dimension
self._n_parameters = len(mean)
if sigma.shape == (self._n_parameters, ):
sigma = np.diag(sigma)
elif sigma.shape != (self._n_parameters, self._n_parameters):
raise ValueError(
'Sigma must have same dimension as mean, or be a square matrix'
' with the same dimension as the mean.')
# check whether covariance matrix is positive semidefinite
if not np.all(np.linalg.eigvals(sigma) >= 0):
raise ValueError('Covariance matrix must be positive ' +
'semidefinite.')
# check if matrix is symmetric
if not np.allclose(sigma, sigma.T, atol=1e-8):
raise ValueError('Covariance matrix must be symmetric.')
# Store
self._mean = mean
self._sigma = sigma
# Create scipy distribution
self._phi = scipy.stats.multivariate_normal(self._mean, self._sigma)
self._sigma_inv = np.linalg.inv(self._sigma)
def __call__(self, x):
y = vector(x).reshape(self._n_parameters)
return self._phi.logpdf(y)
[docs]
def distance(self, samples):
"""
Returns the :meth:`Kullback-Leibler divergence<kl_divergence>`.
See :meth:`pints.toy.ToyLogPDF.distance()`.
"""
return self.kl_divergence(samples)
[docs]
def evaluateS1(self, x):
""" See :meth:`pints.LogPDF.evaluateS1()`. """
L = self.__call__(x)
self._x_minus_mu = x - self._mean
# derivative wrt x: see https://stats.stackexchange.com/questions/27436/how-to-take-derivative-of-multivariate-normal-density # noqa
dL = -np.matmul(self._sigma_inv, self._x_minus_mu)
return L, dL
[docs]
def kl_divergence(self, samples):
"""
Calculates the Kullback-Leibler divergence between a given list of
samples and the distribution underlying this LogPDF.
The returned value is (near) zero for perfect sampling, and then
increases as the error gets larger.
See: https://en.wikipedia.org/wiki/Kullback-Leibler_divergence
"""
# Check size of input
if not len(samples.shape) == 2:
raise ValueError('Given samples list must be n x 2.')
if samples.shape[1] != self._n_parameters:
raise ValueError(
'Given samples must have length ' + str(self._n_parameters))
# From wikipedia:
#
# k = dimension of distribution
# dkl = 0.5 * (
# trace(s1^-1 * s0)
# + (m1 - m0)T * s1^-1 * (m1 - m0)
# + log( det(s1) / det(s0) )
# - k
# )
#
# using s1 = real sigma, as this needs to be inverted and the real one
# is more likely to be invertible than the sample one
m0 = np.mean(samples, axis=0)
m1 = self._mean
s0 = np.cov(samples.T)
s1 = self._sigma
cov_inv = np.linalg.inv(s1)
dkl1 = np.trace(cov_inv.dot(s0))
dkl2 = np.dot((m1 - m0).T, cov_inv).dot(m1 - m0)
dkl3 = np.log(np.linalg.det(s1) / np.linalg.det(s0))
return 0.5 * (dkl1 + dkl2 + dkl3 - self._n_parameters)
[docs]
def n_parameters(self):
""" See :meth:`pints.LogPDF.n_parameters()`. """
return self._n_parameters
[docs]
def sample(self, n):
""" See :meth:`pints.toy.ToyLogPDF.sample()`. """
if n < 0:
raise ValueError('Number of samples cannot be negative.')
return self._phi.rvs(n)