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. ]] """ 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 = {"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)])
class LRFreeFormCov(Function): """ General semi-definite positive matrix of low rank, K = LLᵀ. The covariance matrix K is given by LLᵀ, where L is a n×m matrix and n≥m. Therefore, K will have rank(K) ≤ m. Example ------- .. doctest:: >>> from glimix_core.cov import LRFreeFormCov >>> cov = LRFreeFormCov(3, 2) >>> print(cov.L) [[1. 1.] [1. 1.] [1. 1.]] >>> cov.L = [[1, 2], [0, 3], [1, 3]] >>> print(cov.L) [[1. 2.] [0. 3.] [1. 3.]] >>> cov.name = "F" >>> print(cov) LRFreeFormCov(n=3, m=2): F L: [[1. 2.] [0. 3.] [1. 3.]] """ def __init__(self, n, m): """ Constructor. Parameters ---------- n : int Covariance dimension. m : int Upper limit of the covariance matrix rank. """ self._L = ones((n, m)) self._Lu = Vector(self._L.ravel()) Function.__init__(self, "LRFreeFormCov", Lu=self._Lu) @property def nparams(self): """ Number of parameters. """ return self._L.size def listen(self, func): """ Listen to parameters change. Parameters ---------- func : callable Function to be called when a parameter changes. """ self._Lu.listen(func) def fix(self): """ Disable parameter optimisation. """ self._Lu.fix() def unfix(self): """ Enable parameter optimisation. """ self._Lu.unfix() @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): """ Matrix L from K = LLᵀ. Returns ------- L : (n, m) ndarray Parametric matrix. """ return self._L @L.setter def L(self, value): self._Lu.value = asarray(value, float).ravel() @property def shape(self): """ Array shape. """ n = self._L.shape[0] return (n, n) def value(self): """ Covariance matrix. Returns ------- K : (n, n) ndarray K = LLᵀ. """ return dot(self.L, self.L.T) def gradient(self): """ Derivative of the covariance matrix over the lower triangular, flat part of L. It is equal to ∂K/∂Lᵢⱼ = ALᵀ + LAᵀ, where Aᵢⱼ is an n×m matrix of zeros except at [Aᵢⱼ]ᵢⱼ=1. Returns ------- Lu : ndarray Derivative of K over the lower-triangular, flat part of L. """ L = self.L n = self.L.shape[0] grad = {"Lu": zeros((n, n, n * self._L.shape[1]))} for ii in range(self._L.shape[0] * self._L.shape[1]): row = ii // self._L.shape[1] col = ii % self._L.shape[1] grad["Lu"][row, :, ii] = L[:, col] grad["Lu"][:, row, ii] += L[:, col] return grad def __str__(self): return format_function( self, {"n": self._L.shape[0], "m": self._L.shape[1]}, [("L", self._L)] )