Source code for analogvnn.nn.noise.LaplacianNoise

import math
from typing import Optional

import numpy as np
import torch
from torch import Tensor, nn

from analogvnn.backward.BackwardIdentity import BackwardIdentity
from analogvnn.nn.noise.Noise import Noise
from analogvnn.utils.common_types import TENSOR_OPERABLE
from analogvnn.utils.to_tensor_parameter import to_float_tensor, to_nongrad_parameter

__all__ = ['LaplacianNoise']


[docs]class LaplacianNoise(Noise, BackwardIdentity): """Implements the Laplacian noise function. Attributes: scale (nn.Parameter): the scale of the Laplacian noise. leakage (nn.Parameter): the leakage of the Laplacian noise. precision (nn.Parameter): the precision of the Laplacian noise. """
[docs] __constants__ = ['scale', 'leakage', 'precision']
[docs] scale: nn.Parameter
[docs] leakage: nn.Parameter
[docs] precision: nn.Parameter
def __init__( self, scale: Optional[float] = None, leakage: Optional[float] = None, precision: Optional[int] = None ): """Initialize the Laplacian noise function. Args: scale (float): the scale of the Laplacian noise. leakage (float): the leakage of the Laplacian noise. precision (int): the precision of the Laplacian noise. """ super().__init__() if (scale is None) + (leakage is None) + (precision is None) != 1: raise ValueError('only 2 out of 3 arguments are needed (scale, leakage, precision)') scale, leakage, precision = to_float_tensor(scale, leakage, precision) if scale is None and leakage is not None and precision is not None: scale = self.calc_scale(leakage, precision) if precision is None and scale is not None and leakage is not None: precision = self.calc_precision(scale, leakage) if leakage is None and scale is not None and precision is not None: leakage = self.calc_leakage(scale, precision) self.scale, self.leakage, self.precision = to_nongrad_parameter(scale, leakage, precision) @staticmethod
[docs] def calc_scale(leakage: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> TENSOR_OPERABLE: """Calculate the scale of the Laplacian noise. Args: leakage (float): the leakage of the Laplacian noise. precision (int): the precision of the Laplacian noise. Returns: float: the scale of the Laplacian noise. """ return - 1 / (2 * math.log(leakage) * precision)
@staticmethod
[docs] def calc_precision(scale: TENSOR_OPERABLE, leakage: TENSOR_OPERABLE) -> TENSOR_OPERABLE: """Calculate the precision of the Laplacian noise. Args: scale (float): the scale of the Laplacian noise. leakage (float): the leakage of the Laplacian noise. Returns: int: the precision of the Laplacian noise. """ return - 1 / (2 * math.log(leakage) * scale)
@staticmethod
[docs] def calc_leakage(scale: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> Tensor: """Calculate the leakage of the Laplacian noise. Args: scale (float): the scale of the Laplacian noise. precision (int): the precision of the Laplacian noise. Returns: float: the leakage of the Laplacian noise. """ return 2 * LaplacianNoise.static_cdf(x=-1 / (2 * precision), scale=scale)
@property
[docs] def stddev(self) -> Tensor: """The standard deviation of the Laplacian noise. Returns: Tensor: the standard deviation of the Laplacian noise. """ return (2 ** 0.5) * self.scale
@property
[docs] def variance(self) -> Tensor: """The variance of the Laplacian noise. Returns: Tensor: the variance of the Laplacian noise. """ return 2 * self.scale.pow(2)
[docs] def pdf(self, x: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0) -> Tensor: """The probability density function of the Laplacian noise. Args: x (TENSOR_OPERABLE): the input tensor. loc (TENSOR_OPERABLE): the mean of the Laplacian noise. Returns: Tensor: the probability density function of the Laplacian noise. """ return torch.exp(self.log_prob(x=x, loc=loc))
[docs] def log_prob(self, x: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0) -> Tensor: """The log probability density function of the Laplacian noise. Args: x (TENSOR_OPERABLE): the input tensor. loc (TENSOR_OPERABLE): the mean of the Laplacian noise. Returns: Tensor: the log probability density function of the Laplacian noise. """ x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) loc = loc if isinstance(loc, Tensor) else torch.tensor(loc, requires_grad=False) return -torch.log(2 * self.scale) - torch.abs(x - loc) / self.scale
@staticmethod
[docs] def static_cdf(x: TENSOR_OPERABLE, scale: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0.) -> TENSOR_OPERABLE: """The cumulative distribution function of the Laplacian noise. Args: x (TENSOR_OPERABLE): the input tensor. scale (TENSOR_OPERABLE): the scale of the Laplacian noise. loc (TENSOR_OPERABLE): the mean of the Laplacian noise. Returns: TENSOR_OPERABLE: the cumulative distribution function of the Laplacian noise. """ return 0.5 - 0.5 * np.sign(x - loc) * np.expm1(-abs(x - loc) / scale)
[docs] def cdf(self, x: Tensor, loc: Tensor = 0) -> Tensor: """The cumulative distribution function of the Laplacian noise. Args: x (Tensor): the input tensor. loc (Tensor): the mean of the Laplacian noise. Returns: Tensor: the cumulative distribution function of the Laplacian noise. """ x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) loc = loc if isinstance(loc, Tensor) else torch.tensor(loc, requires_grad=False) return self.static_cdf(x=x, scale=self.scale, loc=loc)
[docs] def forward(self, x: Tensor) -> Tensor: """Add Laplacian noise to the input tensor. Args: x (Tensor): the input tensor. Returns: Tensor: the output tensor with Laplacian noise. """ return torch.distributions.Laplace(loc=x, scale=self.scale).sample()
[docs] def extra_repr(self) -> str: """The extra representation of the Laplacian noise. Returns: str: the extra representation of the Laplacian noise. """ return f'std={float(self.std):.4f}, leakage={float(self.leakage):.4f}, precision={int(self.precision)}'