Source code for aepsych.likelihoods.ordinal

#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
# All rights reserved.

# This source code is licensed under the license found in the
# LICENSE file in the root directory of this source tree.

from typing import Callable, Optional

import gpytorch
import torch
from aepsych.config import Config
from gpytorch.likelihoods import Likelihood
from torch.distributions import Categorical, Normal


[docs]class OrdinalLikelihood(Likelihood): """ Ordinal likelihood, suitable for rating models (e.g. likert scales). Formally, .. math:: z_k(x\\mid f) := p(d_k < f(x) \\le d_{k+1}) = \\sigma(d_{k+1}-f(x)) - \\sigma(d_{k}-f(x)), where :math:`\\sigma()` is the link function (equivalent to the perceptual noise distribution in psychophysics terms), :math:`f(x)` is the latent GP evaluated at x, and :math:`d_k` is a learned cutpoint parameter for each level. """ def __init__(self, n_levels: int, link: Optional[Callable] = None) -> None: """Initialize OrdinalLikelihood. Args: n_levels (int): Number of levels in the ordinal scale. link (Callable, optional): Link function. Defaults to None. """ super().__init__() self.n_levels = n_levels self.register_parameter( name="raw_cutpoint_deltas", parameter=torch.nn.Parameter(torch.abs(torch.randn(n_levels - 2))), ) self.register_constraint("raw_cutpoint_deltas", gpytorch.constraints.Positive()) self.link = link or Normal(0, 1).cdf @property def cutpoints(self) -> torch.Tensor: cutpoint_deltas = self.raw_cutpoint_deltas_constraint.transform( self.raw_cutpoint_deltas ) # for identification, the first cutpoint is 0 return torch.cat((torch.tensor([0]), torch.cumsum(cutpoint_deltas, 0)))
[docs] def forward(self, function_samples: torch.Tensor, *params, **kwargs) -> Categorical: """Forward pass for Ordinal Args: function_samples (torch.Tensor): Function samples. Returns: Categorical: Categorical distribution object. """ # this whole thing can probably be some clever batched thing, meh probs = torch.zeros(*function_samples.size(), self.n_levels) probs[..., 0] = self.link(self.cutpoints[0] - function_samples) for i in range(1, self.n_levels - 1): probs[..., i] = self.link(self.cutpoints[i] - function_samples) - self.link( self.cutpoints[i - 1] - function_samples ) probs[..., -1] = 1 - self.link(self.cutpoints[-1] - function_samples) res = Categorical(probs=probs) return res
[docs] @classmethod def from_config(cls, config: Config) -> "OrdinalLikelihood": """Creates an instance fron configuration object Args: config (Config): Configuration object. Returns: OrdinalLikelihood: OrdinalLikelihood instance""" classname = cls.__name__ n_levels = config.getint(classname, "n_levels") link = config.getobj(classname, "link", fallback=None) return cls(n_levels=n_levels, link=link)