"""GKLR kernel_calcs module."""
from typing import Optional, Tuple
import numpy as np
from .logger import *
from .kernel_matrix import KernelMatrix
from .kernel_utils import *
from .calcs import Calcs
[docs]class KernelCalcs(Calcs):
"""Main calculations for the Kernel Logistic Regression (KLR) model."""
def __init__(self, K: KernelMatrix) -> None:
"""Constructor.
Args:
K: KernelMatrix object.
"""
super().__init__(K)
[docs] def calc_probabilities(self,
alpha: np.ndarray,
indices: Optional[np.ndarray] = None,
) -> np.ndarray:
"""Calculate the probabilities for each alternative.
Obtain the probabilities for each alternative for each row of the
dataset.
Args:
alpha: The vector of parameters. Shape: (num_cols_kernel_matrix, num_alternatives).
indices: The indices of the rows of the dataset for which the
probabilities are calculated. If None, the probabilities are
calculated for all rows of the dataset. Default: None.
Returns:
A matrix of probabilities for each alternative for each row of the
dataset. Each column corresponds to an alternative and each row
to a row of the dataset. The sum of the probabilities for each
row is 1. Shape: (n_samples, num_alternatives).
"""
f = self.calc_f(alpha, indices=indices)
Y = self.calc_Y(f)
G, G_j = self.calc_G(Y)
P = self.calc_P(Y, G, G_j)
return P
[docs] def log_likelihood(self,
alpha: np.ndarray,
P: Optional[np.ndarray] = None,
choice_indices: Optional[np.ndarray] = None,
pmle: Optional[str] = None,
pmle_lambda: float = 0,
indices: Optional[np.ndarray] = None,
) -> float:
"""Calculate the log-likelihood of the KLR model for the given parameters.
Args:
alpha: The vector of parameters. Shape: (num_cols_kernel_matrix, num_alternatives).
P: The matrix of probabilities of each alternative for each row of
the dataset. If None, the probabilities are calculated.
Shape: (n_samples, num_alternatives). Default: None.
choice_indices: The indices of the chosen alternatives for each row
of the dataset. If None, the indices are obtained from the
KernelMatrix object. Shape: (n_samples,). Default: None.
pmle: It specifies the type of penalization for performing a penalized
maximum likelihood estimation. Default: None.
pmle_lambda: The lambda parameter for the penalized maximum likelihood.
Default: 0.
indices: The indices of the rows of the dataset for which the
log-likelihood is calculated. If None, the log-likelihood is
calculated for all rows of the dataset. Default: None.
Returns:
The log-likelihood of the KLR model for the given parameters.
"""
if indices is None:
num_rows = self.K.get_num_rows()
else:
num_rows = indices.shape[0]
if P is None:
P = self.calc_probabilities(alpha, indices=indices)
else:
if P.shape != (num_rows, self.K.get_num_alternatives()):
m = (f"P has {P.shape} dimensions, but it should have "
f" dimensions: ({num_rows}, {self.K.get_num_alternatives()}).")
logger_error(m)
raise ValueError(m)
if choice_indices is None:
choice_indices = self.K.get_choices_indices()
if indices is not None:
indices = indices.tolist()
choice_indices = choice_indices[indices]
else:
if len(choice_indices) != P.shape[0]:
m = (f"choice_indices has {len(choice_indices)} elements, but P"
" has {P.shape[0]} rows.")
logger_error(m)
raise ValueError(m)
# Compute the log-likelihood from the matrix of probabilities
log_P = np.log(P)
log_likelihood = np.sum(log_P[np.arange(len(log_P)), choice_indices])
# Compute the penalty function
penalty = 0
if pmle is None:
pass
elif pmle == "Tikhonov":
penalty = self.tikhonov_penalty(alpha, pmle_lambda)
else:
msg = f"'pmle' = {pmle} is not a valid value for the penalization."
logger_error(msg)
raise ValueError(msg)
return log_likelihood + penalty
[docs] def gradient(self,
alpha: np.ndarray,
P: Optional[np.ndarray] = None,
pmle: Optional[str] = None,
pmle_lambda: float = 0,
indices: Optional[np.ndarray] = None,
) -> np.ndarray:
"""Calculate the gradient of the log-likelihood function of the KLR model
for the given parameters.
Args:
alpha: The vector of parameters. Shape: (num_cols_kernel_matrix, num_alternatives).
pmle: It specifies the type of penalization for performing a penalized
maximum likelihood estimation. Default: None.
pmle_lambda: The lambda parameter for the penalized maximum likelihood.
Default: 0.
P: The matrix of probabilities of each alternative for each row of
the dataset. If None, the probabilities are calculated.
Shape: (n_samples, num_alternatives). Default: None.
indices: The indices of the rows of the dataset for which the
log-likelihood is calculated. If None, the log-likelihood is
calculated for all rows of the dataset. Default: None.
Returns:
The gradient of the log-likelihood function of the KLR model for the
given parameters. Shape: (num_rows_kernel_matrix * num_alternatives, ).
"""
if indices is None:
num_rows = self.K.get_num_rows()
else:
num_rows = indices.shape[0]
if P is None:
P = self.calc_probabilities(alpha, indices=indices)
else:
if P.shape != (num_rows, self.K.get_num_alternatives()):
m = (f"P has {P.shape} dimensions, but it should have "
f" dimensions: ({num_rows}, {self.K.get_num_alternatives()}).")
logger_error(m)
raise ValueError(m)
# Compute the gradient of the log-likelihood function
Z = self.K.get_choices_matrix()
if indices is not None:
Z = Z[indices, :]
grad_penalization = 0
if pmle is None:
pass
elif pmle == "Tikhonov":
grad_penalization = self.tikhonov_penalty_gradient(alpha, pmle_lambda, indices=indices)
else:
msg = f"ERROR. {pmle} is not a valid value for the penalization method `pmle`."
logger_error(msg)
raise ValueError(msg)
H = grad_penalization + P - Z
n_alts = self.K.get_num_alternatives()
gradient = np.zeros((self.K.get_num_cols(), n_alts), dtype=DEFAULT_DTYPE)
for alt in range(0,n_alts):
gradient_alt = self.K.dot(H[:, alt], K_index=alt, col_indices=indices)
gradient_alt = (gradient_alt / H.shape[0]).reshape((self.K.get_num_cols(),))
gradient[:, alt] = gradient_alt
gradient = gradient.reshape(self.K.get_num_cols() * self.K.get_num_alternatives())
return gradient
[docs] def calc_f(self,
alpha: np.ndarray,
indices: Optional[np.ndarray] = None,
) -> np.ndarray:
"""Calculate the value of utility function for each alternative for each row
of the dataset.
Args:
alpha: The vector of parameters. Shape: (num_cols_kernel_matrix, num_alternatives).
indices: The indices of the rows of the dataset for which the utility
function is calculated. If None, all the rows are used. Default: None.
Returns:
A matrix where each row corresponds to the utility of each alternative
for each row of the dataset. Shape: (n_samples, num_alternatives).
"""
num_rows = self.K.get_num_rows()
if indices is not None:
if np.max(indices) >= self.K.get_num_rows() or np.min(indices) < 0:
msg = "Some of the indices provided to compute utility function are out of range."
logger_error(msg)
raise ValueError(msg)
else:
num_rows = indices.shape[0]
n_alts = self.K.get_num_alternatives()
f = np.zeros((num_rows, n_alts), dtype=DEFAULT_DTYPE)
for alt in range(0,n_alts):
alpha_alt = alpha[:, alt].reshape(self.K.get_num_cols(),1) # Get only the column for alt
f_alt = self.K.dot(alpha_alt, K_index=alt, row_indices=indices)
f[:, alt] = f_alt.reshape((num_rows,)) # Store the result in the corresponding column
return f
[docs] def calc_G(self, Y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Calculate the generating function `G` of a Generalized Extreme Value
(GEV) model and its derivative. For KLR model, the generating function
is the sum of the utilities of the alternatives for each row of the
dataset.
Args:
Y: The auxiliary matrix `Y` that contains the exponentiated values of the
matrix `f`. Shape: (n_samples, num_alternatives).
Returns:
A tuple with the auxiliary matrix `G` and its derivative.
The auxiliary matrix `G` is a numpy array of shape: (n_samples, 1)
and its derivative `G_j` is a numpy array of shape: (n_samples, num_alternatives).
"""
# Implementation for KLR
G = np.sum(Y, axis=1).reshape((Y.shape[0], 1))
# Compute G_j, the derivative of G with respecto to the variable Y_j
G_j = np.ones_like(Y)
return (G, G_j)
[docs] def tikhonov_penalty(self,
alpha: np.ndarray,
pmle_lambda: float
) -> float:
"""Calculate the Tikhonov penalty for the given parameters.
Args:
alpha: The vector of parameters. Shape: (num_cols_kernel_matrix, num_alternatives).
pmle_lambda: The lambda parameter for the penalized maximum likelihood.
Returns:
The Tikhonov penalty for the given parameters.
"""
penalty = 0
for alt in range(0,self.K.get_num_alternatives()):
alpha_alt = alpha[:, alt].reshape(self.K.get_num_cols(), 1) # Get only the column for alt
penalty += alpha_alt.T.dot(self.K.dot(alpha_alt, K_index=alt)).item()
penalty = 0.5 * pmle_lambda * penalty
return penalty # TODO: Check if this is correct or if a subset of the rows should be used for alpha
[docs] def tikhonov_penalty_gradient(self,
alpha: np.ndarray,
pmle_lambda: float,
indices: Optional[np.ndarray] = None,
) -> np.ndarray:
"""Calculate the gradient of the Tikhonov penalty for the given parameters.
Args:
alpha: The vector of parameters. Shape: (num_cols_kernel_matrix, num_alternatives).
pmle_lambda: The lambda parameter for the penalized maximum likelihood.
indices: The indices of the rows of the dataset for which the gradient
Returns:
The gradient of the Tikhonov penalty for the given parameters.
If indices is None, the shape is (num_cols_kernel_matrix, num_alternatives),
otherwise, the shape is (len(indices), num_alternatives).
"""
if indices is not None:
alpha = alpha[indices.tolist(), :] # Get only the rows for the indices
return alpha.shape[0] * pmle_lambda * alpha