Source code for fastbnns.bnn.losses

"""Losses and helpers useful for Bayesian neural network training/evaluation."""

from abc import ABC, abstractmethod
from typing import Optional, Union

import torch
import torch.distributions as dist
from torch.nn.modules.loss import _Loss

from . import priors


[docs] def kl_divergence_sampled( dist0: dist.Distribution, dist1: dist.Distribution, n_samples: int = 1 ): """KL divergence KL(dist0||dist1) approximated by Monte Carlo sampling.""" kl_divergence = [] for _ in range(n_samples): sample = dist0.sample() kl_divergence.append(dist0.log_prob(sample) - dist1.log_prob(sample)) return torch.stack(kl_divergence).mean(dim=0)
[docs] class KLDivergence(_Loss): """KL divergence loss for Bayesian neural networks.""" def __init__(self, prior: Optional[Union[dict, priors.Distribution]] = None): """Initialize KL divergence loss. Args: prior: Prior distribution over parameters. This can be a single distribution for all parameters or a dictionary of dictionaries whose primary keys correspond to named modules and whose secondary keys correspond to parameters of that module. The list indices correspond to model.named_modules(). By default, None will use priors set within each Bayesian layer on initialization. """ super().__init__() self.prior = prior
[docs] def forward( self, model: torch.nn.Module, ) -> torch.Tensor: """Compute KL divergence for Bayesian sub-modules of `model`. Args: model: torch.nn.Module that may have some Bayesian layers as sub-modules, for which we'll compute the KL divergence w.r.t their prior. """ kl = [] for module in model.named_modules(): module_parameters = [p for p in module[1].parameters() if p.requires_grad] if ( hasattr(module[1], "compute_kl_divergence") and (len(module_parameters) > 0) and module[1].learn_var ): if isinstance(self.prior, dict): # Pass the input prior dictionary for this module. kl.append( module[1].compute_kl_divergence(priors=self.prior[module[0]]) ) else: kl.append(module[1].compute_kl_divergence(priors=self.prior)) return torch.stack(kl).sum()
[docs] class BNNLoss(ABC, _Loss): """Abstract class for ELBO-like losses used to train Bayesian Neural Networks.""" @property @abstractmethod def beta(self) -> torch.FloatTensor: """Scale factor for KL divergence loss term.""" pass @property @abstractmethod def neg_log_likelihood(self) -> _Loss: """_Loss to compute the negative log-likelihood term in the ELBO.""" pass @property @abstractmethod def kl_divergence(self) -> _Loss: """_Loss to compute the KL divergence of a model.""" pass
[docs] class ELBO(BNNLoss): """Evidence lower bound with scaled KL.""" def __init__( self, neg_log_likelihood: Optional[_Loss] = None, kl_divergence: _Loss = KLDivergence(), beta: float = 1.0, reduction: str = "sum", ) -> None: """Initialize ELBO loss. Args: neg_log_likelihood: Initialized neg_log_likelihood loss (e.g., torch.nn.GaussianNLLLoss()). This will be called in the forward pass of this loss as neg_log_likelihood(**kwargs) where **kwargs are the keyword arguments passed as ELBO()(**kwargs). kl_divergence: Initialized kl_divergence loss whose forward pass takes a torch.nn.Module `model` as input and returns a tensor corresponding to the KL divergence between parameters of `model` and their prior distribution. beta: Scaling parameter for KL loss term in the ELBO. """ super().__init__(reduction=reduction) # Set default log likelihood calculator. if neg_log_likelihood is None: neg_log_likelihood = torch.nn.GaussianNLLLoss( full=True, reduction=reduction ) self._neg_log_likelihood = neg_log_likelihood self._kl_divergence = kl_divergence self._beta = beta @property def beta(self) -> bool: """Return property `beta`. This property is written as an @property method for compatibility with the abstract parent class. """ return self._beta @property def neg_log_likelihood(self) -> bool: """Return property `neg_log_likelihood`. This property is written as an @property method for compatibility with the abstract parent class. """ return self._neg_log_likelihood @property def kl_divergence(self) -> bool: """Return property `kl_divergence`. This property is written as an @property method for compatibility with the abstract parent class. """ return self._kl_divergence
[docs] def forward( self, model: Optional[torch.nn.Module] = None, **kwargs ) -> torch.Tensor: """Compute the ELBO loss. Args: model: torch.nn.Module that may have some layers.BayesianLayers as sub-modules, for which we'll compute the KL divergence w.r.t their prior. Passing None is treated as no model, i.e., KL = 0.0 kwargs: Keyword arguments to pass to self.log_likelihood(**kwargs) """ if (model is None) or (self.beta == 0.0) or (self.kl_divergence is None): return self.neg_log_likelihood(**kwargs) else: return self.neg_log_likelihood(**kwargs) + self.beta * self.kl_divergence( model )