Source code for glimix_core.cov._free
from typing import Any, Dict
from numpy import diag_indices_from, dot, exp, eye, inf, log, tril_indices_from, zeros
from optimix import Function, Vector
from .._util import format_function
[docs]class FreeFormCov(Function):
"""
General definite positive matrix, K = LLᵀ + ϵI.
A d×d covariance matrix K will have ((d+1)⋅d)/2 parameters defining the lower
triangular elements of a Cholesky matrix L such that:
K = LLᵀ + ϵI,
for a very small positive number ϵ. That additional term is necessary to avoid
singular and ill conditioned covariance matrices.
Example
-------
.. doctest::
>>> from glimix_core.cov import FreeFormCov
>>>
>>> cov = FreeFormCov(2)
>>> cov.L = [[1., .0], [0.5, 2.]]
>>> print(cov.gradient()["Lu"])
[[[0. 2. 0. ]
[1. 0.5 0. ]]
<BLANKLINE>
[[1. 0.5 0. ]
[1. 0. 8. ]]]
>>> cov.name = "K"
>>> print(cov)
FreeFormCov(dim=2): K
L: [[1. 0. ]
[0.5 2. ]]
"""
[docs] def __init__(self, dim):
"""
Constructor.
Parameters
----------
dim : int
Dimension d of the free-form covariance matrix.
"""
from numpy_sugar import epsilon
dim = int(dim)
tsize = ((dim + 1) * dim) // 2
self._L = zeros((dim, dim))
self._tril1 = tril_indices_from(self._L, k=-1)
self._diag = diag_indices_from(self._L)
self._L[self._tril1] = 1
self._L[self._diag] = 0
self._epsilon = epsilon.small * 1000
self._Lu = Vector(zeros(tsize))
self._Lu.value[: tsize - dim] = 1
n = self.L.shape[0]
self._grad_Lu = zeros((n, n, self._Lu.shape[0]))
Function.__init__(self, "FreeCov", Lu=self._Lu)
bounds = [(-inf, +inf)] * (tsize - dim)
bounds += [(log(epsilon.small * 1000), +11)] * dim
self._Lu.bounds = bounds
self._cache: Dict[str, Any] = {"eig": None}
self.listen(self._parameters_update)
self._nparams = tsize
def _parameters_update(self):
self._cache["eig"] = None
@property
def nparams(self):
"""
Number of parameters.
"""
return self._nparams
def listen(self, func):
"""
Listen to parameters change.
Parameters
----------
func : callable
Function to be called when a parameter changes.
"""
self._Lu.listen(func)
@property
def shape(self):
"""
Array shape.
"""
n = self._L.shape[0]
return (n, n)
def fix(self):
"""
Disable parameter optimisation.
"""
self._Lu.fix()
def unfix(self):
"""
Enable parameter optimisation.
"""
self._Lu.unfix()
def eigh(self):
"""
Eigen decomposition of K.
Returns
-------
S : ndarray
The eigenvalues in ascending order, each repeated according to its
multiplicity.
U : ndarray
Normalized eigenvectors.
"""
from numpy.linalg import svd
if self._cache["eig"] is not None:
return self._cache["eig"]
U, S = svd(self.L)[:2]
S *= S
S += self._epsilon
self._cache["eig"] = S, U
return self._cache["eig"]
@property
def Lu(self):
"""
Lower-triangular, flat part of L.
"""
return self._Lu.value
@Lu.setter
def Lu(self, v):
self._Lu.value = v
@property
def L(self):
"""
Lower-triangular matrix L such that K = LLᵀ + ϵI.
Returns
-------
L : (d, d) ndarray
Lower-triangular matrix.
"""
m = len(self._tril1[0])
self._L[self._tril1] = self._Lu.value[:m]
self._L[self._diag] = exp(self._Lu.value[m:])
return self._L
@L.setter
def L(self, value):
self._L[:] = value
m = len(self._tril1[0])
self._Lu.value[:m] = self._L[self._tril1]
self._Lu.value[m:] = log(self._L[self._diag])
def logdet(self):
"""
Log of |K|.
Returns
-------
float
Log-determinant of K.
"""
from numpy.linalg import slogdet
K = self.value()
sign, logdet = slogdet(K)
if sign != 1.0:
msg = "The estimated determinant of K is not positive: "
msg += f" ({sign}, {logdet})."
raise RuntimeError(msg)
return logdet
def value(self):
"""
Covariance matrix.
Returns
-------
K : ndarray
Matrix K = LLᵀ + ϵI, for a very small positive number ϵ.
"""
K = dot(self.L, self.L.T)
return K + self._epsilon * eye(K.shape[0])
def gradient(self):
"""
Derivative of the covariance matrix over the parameters of L.
Returns
-------
Lu : ndarray
Derivative of K over the lower triangular part of L.
"""
L = self.L
self._grad_Lu[:] = 0
for i in range(len(self._tril1[0])):
row = self._tril1[0][i]
col = self._tril1[1][i]
self._grad_Lu[row, :, i] = L[:, col]
self._grad_Lu[:, row, i] += L[:, col]
m = len(self._tril1[0])
for i in range(len(self._diag[0])):
row = self._diag[0][i]
col = self._diag[1][i]
self._grad_Lu[row, :, m + i] = L[row, col] * L[:, col]
self._grad_Lu[:, row, m + i] += L[row, col] * L[:, col]
return {"Lu": self._grad_Lu}
def __str__(self):
return format_function(self, {"dim": self._L.shape[0]}, [("L", self.L)])