Skip to content

Bayesian Layers

Drop-in replacements for standard torch.nn layers that implement the Local Reparameterization Trick (LRT).

Bayesian Linear

bensemble.layers.linear.BayesianLinear

BayesianLinear(
    in_features: int,
    out_features: int,
    prior_sigma: float = 1.0,
    init_sigma: float = 0.1,
    weight_init: str = "kaiming",
)

Bases: BaseBayesianLayer

Bayesian Linear layer with Local Reparameterization Trick.

Weights and biases are parameterized as Gaussian distributions with learnable means and standard deviations (parametrized by rho).

Initializes the BayesianLinear layer.

Parameters:

Name Type Description Default
in_features int

Size of each input sample.

required
out_features int

Size of each output sample.

required
prior_sigma float

Standard deviation of the prior Gaussian distribution. Defaults to 1.0.

1.0
init_sigma float

Initial standard deviation for posterior distributions. Defaults to 0.1.

0.1
weight_init str

Weight initialization scheme ("kaiming", "xavier", or "normal"). Defaults to "kaiming".

'kaiming'
Source code in bensemble/layers/linear.py
def __init__(
    self,
    in_features: int,
    out_features: int,
    prior_sigma: float = 1.0,
    init_sigma: float = 0.1,
    weight_init: str = "kaiming",
):
    """Initializes the BayesianLinear layer.

    Args:
        in_features: Size of each input sample.
        out_features: Size of each output sample.
        prior_sigma: Standard deviation of the prior Gaussian distribution. Defaults to 1.0.
        init_sigma: Initial standard deviation for posterior distributions. Defaults to 0.1.
        weight_init: Weight initialization scheme ("kaiming", "xavier", or "normal"). Defaults to "kaiming".
    """
    super().__init__(prior_sigma=prior_sigma)
    self.in_features = in_features
    self.out_features = out_features
    self.init_sigma = init_sigma
    self.weight_init = weight_init

    self.w_mu = nn.Parameter(torch.empty(out_features, in_features))
    self.w_rho = nn.Parameter(torch.empty(out_features, in_features))

    self.b_mu = nn.Parameter(torch.empty(out_features))
    self.b_rho = nn.Parameter(torch.empty(out_features))

    self.reset_parameters()

forward

forward(x: Tensor) -> torch.Tensor

Applies linear transformation using deterministic means in eval mode or LRT sampling in train mode.

Parameters:

Name Type Description Default
x Tensor

Input tensor of shape (..., in_features).

required

Returns:

Type Description
Tensor

torch.Tensor: Output tensor of shape (..., out_features).

Source code in bensemble/layers/linear.py
def forward(self, x: torch.Tensor) -> torch.Tensor:
    """Applies linear transformation using deterministic means in eval mode or LRT sampling in train mode.

    Args:
        x: Input tensor of shape (..., in_features).

    Returns:
        torch.Tensor: Output tensor of shape (..., out_features).
    """
    if not self.training:
        return F.linear(x, self.w_mu, self.b_mu)

    w_sigma = F.softplus(self.w_rho)
    b_sigma = F.softplus(self.b_rho)

    gamma = F.linear(x, self.w_mu)
    delta = F.linear(x.pow(2), w_sigma.pow(2)) + b_sigma.pow(2)

    eps = torch.randn_like(gamma)
    return gamma + eps * torch.sqrt(delta + 1e-8) + self.b_mu

reset_parameters

reset_parameters() -> None

Initializes layer weights and bias parameters.

Source code in bensemble/layers/linear.py
def reset_parameters(self) -> None:
    """Initializes layer weights and bias parameters."""
    if self.weight_init == "kaiming":
        init.kaiming_normal_(self.w_mu, nonlinearity="relu")
    elif self.weight_init == "xavier":
        init.xavier_normal_(self.w_mu)
    else:
        init.kaiming_uniform_(self.w_mu, a=math.sqrt(5))

    init.zeros_(self.b_mu)

    rho_init_val = math.log(math.exp(self.init_sigma) - 1.0)
    self.w_rho.data.fill_(rho_init_val)
    self.b_rho.data.fill_(rho_init_val)

Bayesian Conv2d

bensemble.layers.conv.BayesianConv2d

BayesianConv2d(
    in_channels: int,
    out_channels: int,
    kernel_size: int | tuple[int, int],
    stride: int | tuple[int, int] = 1,
    padding: int | tuple[int, int] = 0,
    dilation: int | tuple[int, int] = 1,
    groups: int = 1,
    prior_sigma: float = 1.0,
    init_sigma: float = 0.1,
)

Bases: BaseBayesianLayer

Bayesian 2D Convolutional layer with Local Reparameterization Trick.

Initializes the BayesianConv2d layer.

Parameters:

Name Type Description Default
in_channels int

Number of channels in the input image.

required
out_channels int

Number of channels produced by the convolution.

required
kernel_size int | tuple[int, int]

Size of the convolving kernel.

required
stride int | tuple[int, int]

Stride of the convolution. Defaults to 1.

1
padding int | tuple[int, int]

Zero-padding added to both sides of the input. Defaults to 0.

0
dilation int | tuple[int, int]

Spacing between kernel elements. Defaults to 1.

1
groups int

Number of blocked connections from input to output channels. Defaults to 1.

1
prior_sigma float

Standard deviation of the Gaussian prior distribution. Defaults to 1.0.

1.0
init_sigma float

Initial standard deviation for posterior parameters. Defaults to 0.1.

0.1
Source code in bensemble/layers/conv.py
def __init__(
    self,
    in_channels: int,
    out_channels: int,
    kernel_size: int | tuple[int, int],
    stride: int | tuple[int, int] = 1,
    padding: int | tuple[int, int] = 0,
    dilation: int | tuple[int, int] = 1,
    groups: int = 1,
    prior_sigma: float = 1.0,
    init_sigma: float = 0.1,
):
    """Initializes the BayesianConv2d layer.

    Args:
        in_channels: Number of channels in the input image.
        out_channels: Number of channels produced by the convolution.
        kernel_size: Size of the convolving kernel.
        stride: Stride of the convolution. Defaults to 1.
        padding: Zero-padding added to both sides of the input. Defaults to 0.
        dilation: Spacing between kernel elements. Defaults to 1.
        groups: Number of blocked connections from input to output channels. Defaults to 1.
        prior_sigma: Standard deviation of the Gaussian prior distribution. Defaults to 1.0.
        init_sigma: Initial standard deviation for posterior parameters. Defaults to 0.1.
    """
    super().__init__(prior_sigma=prior_sigma)
    self.in_channels = in_channels
    self.out_channels = out_channels
    self.init_sigma = init_sigma
    self.kernel_size = (
        kernel_size
        if isinstance(kernel_size, tuple)
        else (kernel_size, kernel_size)
    )
    self.stride = stride
    self.padding = padding
    self.dilation = dilation
    self.groups = groups

    weight_shape = (out_channels, in_channels // groups, *self.kernel_size)

    self.w_mu = nn.Parameter(torch.empty(weight_shape))
    self.w_rho = nn.Parameter(torch.empty(weight_shape))

    self.b_mu = nn.Parameter(torch.empty(out_channels))
    self.b_rho = nn.Parameter(torch.empty(out_channels))

    self.reset_parameters()

forward

forward(x: Tensor) -> torch.Tensor

Executes forward pass using deterministic means in eval mode or LRT sampling in train mode.

Parameters:

Name Type Description Default
x Tensor

Input tensor of shape (batch_size, in_channels, height, width).

required

Returns:

Type Description
Tensor

torch.Tensor: Convolved output tensor of shape (batch_size, out_channels, out_height, out_width).

Source code in bensemble/layers/conv.py
def forward(self, x: torch.Tensor) -> torch.Tensor:
    """Executes forward pass using deterministic means in eval mode or LRT sampling in train mode.

    Args:
        x: Input tensor of shape (batch_size, in_channels, height, width).

    Returns:
        torch.Tensor: Convolved output tensor of shape (batch_size, out_channels, out_height, out_width).
    """
    if not self.training:
        return F.conv2d(
            x,
            self.w_mu,
            self.b_mu,
            self.stride,
            self.padding,
            self.dilation,
            self.groups,
        )

    w_sigma = F.softplus(self.w_rho)
    b_sigma = F.softplus(self.b_rho)

    conv_mu = F.conv2d(
        x,
        self.w_mu,
        bias=None,
        stride=self.stride,
        padding=self.padding,
        dilation=self.dilation,
        groups=self.groups,
    )

    conv_var = F.conv2d(
        x.pow(2),
        w_sigma.pow(2),
        bias=None,
        stride=self.stride,
        padding=self.padding,
        dilation=self.dilation,
        groups=self.groups,
    )

    conv_var = conv_var + b_sigma.pow(2).view(1, -1, 1, 1)

    eps = torch.randn_like(conv_mu)
    out = conv_mu + eps * torch.sqrt(conv_var + 1e-8)

    return out + self.b_mu.view(1, -1, 1, 1)

reset_parameters

reset_parameters() -> None

Initializes layer weights and bias parameters.

Source code in bensemble/layers/conv.py
def reset_parameters(self) -> None:
    """Initializes layer weights and bias parameters."""
    init.kaiming_normal_(self.w_mu, mode="fan_in", nonlinearity="relu")
    init.zeros_(self.b_mu)

    rho_init = math.log(math.exp(self.init_sigma) - 1.0)
    self.w_rho.data.fill_(rho_init)
    self.b_rho.data.fill_(rho_init)

Base Class

bensemble.layers.base.BaseBayesianLayer

BaseBayesianLayer(prior_sigma: float = 1.0)

Bases: Module

Base class for all bayesian layers.

Computes KL-divergence automatically for all parameters ending with _mu and _rho.

Source code in bensemble/layers/base.py
def __init__(self, prior_sigma: float = 1.0):
    super().__init__()
    self.prior_sigma = prior_sigma

apply_pruning

apply_pruning(threshold: float = 0.83) -> float

Applies pruning in-place: zeros out the means and minimizes the variance for weights that fall below the SNR threshold.

Returns:

Name Type Description
float float

Sparsity of the layer (percentage of pruned weights, 0.0 to 1.0).

Source code in bensemble/layers/base.py
def apply_pruning(self, threshold: float = 0.83) -> float:
    """
    Applies pruning in-place: zeros out the means and minimizes the variance
    for weights that fall below the SNR threshold.

    Returns:
        float: Sparsity of the layer (percentage of pruned weights, 0.0 to 1.0).
    """
    masks = self.get_pruning_masks(threshold)

    total_weights = 0
    pruned_weights = 0

    with torch.no_grad():
        for name, param in self.named_parameters():
            if name.endswith("_mu"):
                mask = masks[name]

                total_weights += mask.numel()
                pruned_weights += (mask == 0.0).sum().item()
                param.data *= mask

                rho_name = name.replace("_mu", "_rho")
                if hasattr(self, rho_name):
                    rho = getattr(self, rho_name)
                    rho.data = torch.where(
                        mask.bool(),
                        rho.data,
                        torch.tensor(-1e8, device=rho.device, dtype=rho.dtype),
                    )

    sparsity = pruned_weights / total_weights if total_weights > 0 else 0.0
    return sparsity

get_pruning_masks

get_pruning_masks(threshold: float = 0.83) -> dict

Returns binary masks for parameters satisfying the SNR threshold.

Implements Graves' pruning heuristic where weights with low Signal-to-Noise Ratio are considered redundant and can be removed.

Parameters:

Name Type Description Default
threshold float

The SNR threshold (|mu|/sigma). Defaults to 0.83, the "safe" threshold suggested by Graves.

0.83

Returns:

Type Description
dict

dict[str, torch.Tensor]: A dictionary mapping parameter names to binary masks (1.0 for keeping, 0.0 for pruning).

Source code in bensemble/layers/base.py
def get_pruning_masks(self, threshold: float = 0.83) -> dict:
    """Returns binary masks for parameters satisfying the SNR threshold.

    Implements Graves' pruning heuristic where weights with low
    Signal-to-Noise Ratio are considered redundant and can be removed.

    Args:
        threshold (float, optional): The SNR threshold (|mu|/sigma).
            Defaults to 0.83, the "safe" threshold suggested by Graves.

    Returns:
        dict[str, torch.Tensor]: A dictionary mapping parameter names to
            binary masks (1.0 for keeping, 0.0 for pruning).
    """
    snr_dict = self._get_snr_dict()
    return {name: (val > threshold).float() for name, val in snr_dict.items()}

kl_divergence

kl_divergence() -> torch.Tensor

Computes KL-divergence KL(q || p) for all bayesian weights of the layer. p(w) = N(0, prior_sigma^2) q(w) = N(mu, sigma^2), where sigma = softplus(rho)

Source code in bensemble/layers/base.py
def kl_divergence(self) -> torch.Tensor:
    """
    Computes KL-divergence KL(q || p) for all bayesian weights of the layer.
    p(w) = N(0, prior_sigma^2)
    q(w) = N(mu, sigma^2), where sigma = softplus(rho)
    """
    total_kl = 0.0

    for name, param in self.named_parameters():
        if name.endswith("_mu"):
            rho_name = name.replace("_mu", "_rho")

            if hasattr(self, rho_name):
                mu = param
                rho = getattr(self, rho_name)

                total_kl += self._compute_kl_for_param(mu, rho)

    return total_kl