""" Copyright 2018 Johns Hopkins University (Author: Jesus Villalba) Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) Some math functions. """ from typing import Callable, Optional, Tuple, Union import numpy as np import scipy.linalg as la from ..hyp_defs import float_cpu def logdet_pdmat(A: np.ndarray) -> float: """Log determinant of positive definite matrix.""" assert A.shape[0] == A.shape[1] R = la.cholesky(A) return 2 * np.sum(np.log(np.diag(R))) def invert_pdmat( A: np.ndarray, right_inv: bool = False, return_logdet: bool = False, return_inv: bool = False, ) -> Union[ Tuple[Callable[[np.ndarray], np.ndarray], np.ndarray], Tuple[Callable[[np.ndarray], np.ndarray], np.ndarray, float], Tuple[Callable[[np.ndarray], np.ndarray], np.ndarray, float, np.ndarray], ]: """Inversion of positive definite matrices. Returns lambda function f that multiplies the inverse of A times a vector. Args: A: Positive definite matrix right_inv: If False, f(v)=A^{-1}v; if True f(v)=v' A^{-1} return_logdet: If True, it also returns the log determinant of A. return_inv: If True, it also returns A^{-1} Returns: Lambda function that multiplies A^{-1} times vector. Cholesky transform of A upper triangular Log determinant of A A^{-1} """ assert A.shape[0] == A.shape[1] R = la.cholesky(A, lower=False) if right_inv: fh = lambda x: la.cho_solve((R, False), x.T).T else: fh = lambda x: la.cho_solve((R, False), x) # fh=lambda x: la.solve_triangular(R, la.solve_triangular(R.T, x, lower=True), lower=False) r = [fh, R] logdet = None invA = None if return_logdet: logdet = 2 * np.sum(np.log(np.diag(R))) r.append(logdet) if return_inv: invA = fh(np.eye(A.shape[0])) r.append(invA) return r def invert_trimat( A: np.ndarray, lower: bool = False, right_inv: bool = False, return_logdet: bool = False, return_inv: bool = False, ) -> Union[ Callable[[np.ndarray], np.ndarray], Tuple[Callable[[np.ndarray], np.ndarray], float], Tuple[Callable[[np.ndarray], np.ndarray], float, np.ndarray], ]: """Inversion of triangular matrices. Returns lambda function f that multiplies the inverse of A times a vector. Args: A: Triangular matrix. lower: if True A is lower triangular, else A is upper triangular. right_inv: If False, f(v)=A^{-1}v; if True f(v)=v' A^{-1} return_logdet: If True, it also returns the log determinant of A. return_inv: If True, it also returns A^{-1} Returns: Lambda function that multiplies A^{-1} times vector. Log determinant of A A^{-1} """ if right_inv: fh = lambda x: la.solve_triangular(A.T, x.T, lower=not (lower)).T else: fh = lambda x: la.solve_triangular(A, x, lower=lower) if return_logdet or return_inv: r = [fh] else: r = fh if return_logdet: logdet = np.sum(np.log(np.diag(A))) r.append(logdet) if return_inv: invA = fh(np.eye(A.shape[0])) r.append(invA) return r def softmax(r: np.ndarray, axis: int = -1) -> np.ndarray: """ Returns: y = \exp(r)/\sum(\exp(r)) """ max_r = np.max(r, axis=axis, keepdims=True) r = np.exp(r - max_r) r /= np.sum(r, axis=axis, keepdims=True) return r def logsumexp(r: np.ndarray, axis: int = -1) -> np.ndarray: """ Returns: y = \log \sum(\exp(r)) """ max_r = np.max(r, axis=axis, keepdims=True) r = np.exp(r - max_r) return np.log(np.sum(r, axis=axis) + 1e-20) + np.squeeze(max_r, axis=axis) def logsigmoid(x: np.ndarray) -> np.ndarray: """ Returns: y = \log(sigmoid(x)) """ e = np.exp(-x) f = x < -100 log_p = -np.log(1 + np.exp(-x)) log_p[f] = x[f] return log_p def neglogsigmoid(x: np.ndarray) -> np.ndarray: """ Returns: y = -\log(sigmoid(x)) """ e = np.exp(-x) f = x < -100 log_p = np.log(1 + np.exp(-x)) log_p[f] = -x[f] return log_p def sigmoid(x: np.ndarray) -> np.ndarray: """ Returns: y = sigmoid(x) """ e = np.exp(-x) f = x < -100 p = 1 / (1 + np.exp(-x)) p[f] = 0 return p def fisher_ratio( mu1: np.ndarray, Sigma1: np.ndarray, mu2: np.ndarray, Sigma2: np.ndarray ) -> float: """Computes the Fisher ratio between two classes from the class means and covariances. """ S = Sigma1 + Sigma2 L = invert_pdmat(S)[0] delta = mu1 - mu2 return np.inner(delta, L(delta)) def fisher_ratio_with_precs( mu1: np.ndarray, Lambda1: np.ndarray, mu2: np.ndarray, Lambda2: np.ndarray ) -> float: """Computes the Fisher ratio between two classes from the class means precisions. """ Sigma1 = invert_pdmat(Lambda1, return_inv=True)[-1] Sigma2 = invert_pdmat(Lambda2, return_inv=True)[-1] return fisher_ratio(mu1, Sigma1, mu2, Sigma2) def symmat2vec( A: np.ndarray, lower: bool = False, diag_factor: Optional[float] = None ) -> np.ndarray: """Puts a symmetric matrix into a vector. Args: A: Symmetric matrix. lower: If True, it uses the lower triangular part of the matrix. If False, it uses the upper triangular part of the matrix. diag_factor: It multiplies the diagonal of A by diag_factor. Returns: Vector with the upper or lower triangular part of A. """ if diag_factor is not None: A = np.copy(A) A[np.diag_indices(A.shape[0])] *= diag_factor if lower: return A[np.tril_indices(A.shape[0])] return A[np.triu_indices(A.shape[0])] def vec2symmat( v: np.ndarray, lower: bool = False, diag_factor: Optional[float] = None ) -> np.ndarray: """Puts a vector back into a symmetric matrix. Args: v: Vector with the upper or lower triangular part of A. lower: If True, v contains the lower triangular part of the matrix. If False, v contains the upper triangular part of the matrix. diag_factor: It multiplies the diagonal of A by diag_factor. Returns: Symmetric matrix. """ dim = int((-1 + np.sqrt(1 + 8 * v.shape[0])) / 2) idx_u = np.triu_indices(dim) idx_l = np.tril_indices(dim) A = np.zeros((dim, dim), dtype=float_cpu()) if lower: A[idx_l] = v A[idx_u] = A.T[idx_u] else: A[idx_u] = v A[idx_l] = A.T[idx_l] if diag_factor is not None: A[np.diag_indices(A.shape[0])] *= diag_factor return A def trimat2vec(A: np.ndarray, lower: bool = False) -> np.ndarray: """Puts a triangular matrix into a vector. Args: A: Triangular matrix. lower: If True, it uses the lower triangular part of the matrix. If False, it uses the upper triangular part of the matrix. Returns: Vector with the upper or lower triangular part of A. """ return symmat2vec(A, lower) def vec2trimat(v: np.ndarray, lower: bool = False) -> np.ndarray: """Puts a vector back into a triangular matrix. Args: v: Vector with the upper or lower triangular part of A. lower: If True, v contains the lower triangular part of the matrix. If False, v contains the upper triangular part of the matrix. Returns: Triangular matrix. """ dim = int((-1 + np.sqrt(1 + 8 * v.shape[0])) / 2) A = np.zeros((dim, dim), dtype=float_cpu()) if lower: A[np.tril_indices(dim)] = v return A A[np.triu_indices(dim)] = v return A def fullcov_varfloor( S: np.ndarray, F: Union[np.ndarray, float], F_is_chol: bool = False, lower: bool = False, ) -> np.ndarray: """Variance flooring for full covariance matrices. Args: S: Covariance. F: Minimum cov or Cholesqy decomposisition of it F_is_chol: If True F is Cholesqy decomposition lower: True if cholF is lower triangular, False otherwise Returns: Floored covariance """ if isinstance(F, np.ndarray): if not F_is_chol: cholF = la.cholesky(F, lower=False, overwrite_a=False) else: cholF = F if lower: cholF = cholF.T icholF = invert_trimat(cholF, return_inv=True)[-1] T = np.dot(np.dot(icholF.T, S), icholF) else: T = S / F u, d, _ = la.svd(T, full_matrices=False, overwrite_a=True) d[d < 1.0] = 1 T = np.dot(u * d, u.T) if isinstance(F, np.ndarray): S = np.dot(cholF.T, np.dot(T, cholF)) else: S = F * T return S def fullcov_varfloor_from_cholS( cholS: np.ndarray, cholF: Union[np.ndarray, float], lower: bool = False ) -> np.ndarray: """Variance flooring for full covariance matrices using Cholesky decomposition as input/output Args: cholS: Cholesqy decomposisition of the covariance. cholF: Cholesqy decomposisition of the minimum covariance. lower: True if matrices are lower triangular, False otherwise Returns: Cholesky decomposition of the floored covariance """ if isinstance(cholF, np.ndarray): if lower: cholS = cholS.T cholF = cholF.T T = np.dot(cholS, invert_trimat(cholF, return_inv=True)[-1]) else: if lower: cholS = cholS.T T = cholS / cholF T = np.dot(T.T, T) u, d, _ = la.svd(T, full_matrices=False, overwrite_a=True) d[d < 1.0] = 1 T = np.dot(u * d, u.T) if isinstance(cholF, np.ndarray): S = np.dot(cholF.T, np.dot(T, cholF)) else: S = (cholF**2) * T return la.cholesky(S, lower) def int2onehot(class_ids: np.ndarray, num_classes: Optional[int] = None) -> np.ndarray: """Integer to 1-hot vector. Args: class_ids: Numpy array of integers. num_classes: Maximum number of classes. Returns: 1-hot Numpy array. """ if num_classes is None: num_classes = np.max(class_ids) + 1 p = np.zeros((len(class_ids), num_classes), dtype=float_cpu()) p[np.arange(len(class_ids)), class_ids] = 1 return p def average_vectors(x: np.ndarray, ids: np.ndarray) -> np.ndarray: assert x.shape[0] == len(ids) num_ids = np.max(ids) + 1 x_avg = np.zeros((num_ids, x.shape[1]), dtype=x.dtype) for i in range(num_ids): mask = ids == i x_avg[i] = np.mean(x[mask], axis=0) return x_avg def cosine_scoring( x1: np.ndarray, x2: np.ndarray, ids1: Optional[np.ndarray] = None, ids2: Optional[np.ndarray] = None, ) -> np.ndarray: if ids1 is not None: x1 = average_vectors(x1, ids1) if ids2 is not None: x2 = average_vectors(x2, ids2) l2_1 = np.sqrt(np.sum(x1**2, axis=-1, keepdims=True) + 1e-10) l2_2 = np.sqrt(np.sum(x2**2, axis=-1, keepdims=True) + 1e-10) x1 = x1 / l2_1 x2 = x2 / l2_2 return np.dot(x1, x2.T)