Source code for mlx_graphs.nn.norm.batch_norm

from typing import Optional

import mlx.core as mx
import mlx.nn as nn

from mlx_graphs.utils.scatter import scatter


[docs] class BatchNormalization(nn.Module): r"""Applies batch normalization over a batch of features as described in the `Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" <https://arxiv.org/abs/1502.03167>`_ paper. .. math:: \mathbf{x}^{\prime}_i = \frac{\mathbf{x} - \textrm{E}[\mathbf{x}]}{\sqrt{\textrm{Var}[\mathbf{x}] + \epsilon}} \odot \gamma + \beta The mean and standard-deviation are calculated per-dimension over all nodes inside the mini-batch. Args: in_channels : Size of each input sample. eps : A value added to the denominator for numerical stability. (default: :obj:`1e-5`) momentum : The value used for the running mean and running variance computation. (default: :obj:`0.1`) affine : If set to :obj:`True`, this module has learnable affine parameters :math:`\gamma` and :math:`\beta`. (default: :obj:`True`) track_running_stats : If set to :obj:`True`, this module tracks the running mean and variance, and when set to :obj:`False`, this module does not track such statistics and always uses batch statistics in both training and eval modes. (default: :obj:`True`) allow_single_element : If set to :obj:`True`, batches with only a single element will work as during in evaluation. That is the running mean and variance will be used. Requires :obj:`track_running_stats=True`. (default: :obj:`False`) """ def __init__( self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = True, track_running_stats: bool = True, allow_single_element: bool = False, ): super().__init__() if allow_single_element and not track_running_stats: raise ValueError( "'allow_single_element' requires " "'track_running_stats' to be set to `True`" ) self.module = nn.BatchNorm( num_features=num_features, eps=eps, momentum=momentum, affine=affine, track_running_stats=track_running_stats, ) self.num_features = num_features self.allow_single_element = allow_single_element
[docs] def __call__(self, x: mx.array): if self.allow_single_element and x.shape[0] <= 1: x = (x - self.module.running_mean) * mx.rsqrt( self.module.running_var + self.module.eps ) return self.module.weight * x + self.module.bias return self.module(x)
def __repr__(self): return f"{self.__class__.__name__}({self.module.num_features})"
[docs] class HeteroBatchNormalization(nn.Module): r"""Applies batch normalization over a batch of heterogeneous features, as described in the `"Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" <https://arxiv.org/abs/1502.03167>`_ paper. Compared to a standard batch norm, :class:`HeteroBatchNorm` applies normalization individually for each node or edge type. Args: in_channels (int): Size of each input sample. num_types (int): The number of types. eps (float, optional): A value added to the denominator for numerical stability. (default: :obj:`1e-5`) momentum (float, optional): The value used for the running mean and running variance computation. If set to :obj:`None`, a cumulative moving average is used instead. (default: :obj:`0.1`) affine (bool, optional): If set to :obj:`True`, this module has learnable affine parameters :math:`\gamma` and :math:`\beta`. (default: :obj:`True`) track_running_stats (bool, optional): If set to :obj:`True`, this module tracks the running mean and variance, and when set to :obj:`False`, this module does not track such statistics and always uses batch statistics in both training and eval modes. (default: :obj:`True`) """ def __init__( self, in_channels: int, num_types: int, eps: float = 1e-5, momentum: Optional[float] = 0.1, affine: bool = True, track_running_stats: bool = True, ): super().__init__() self.in_channels = in_channels self.num_types = num_types self.eps = eps self.momentum = momentum self.affine = affine self.track_running_stats = track_running_stats if self.affine: self.weight = mx.ones((num_types, in_channels)) self.bias = mx.zeros((num_types, in_channels)) if self.track_running_stats: self.running_mean = mx.zeros((num_types, in_channels)) self.running_var = mx.ones((num_types, in_channels)) self.num_batches_tracked = mx.array(0) # Running stats are not learnable: exclude them from gradients. self.freeze( keys=["running_mean", "running_var", "num_batches_tracked"], recurse=False, ) self.reset_parameters()
[docs] def reset_running_stats(self): r"""Resets all running statistics of the module.""" if self.track_running_stats: self.running_mean = mx.zeros_like(self.running_mean) self.running_var = mx.ones_like(self.running_var) self.num_batches_tracked = mx.array(0)
[docs] def reset_parameters(self): r"""Resets all learnable parameters of the module.""" self.reset_running_stats() if self.affine: self.weight = mx.ones_like(self.weight) self.bias = mx.zeros_like(self.bias)
[docs] def __call__(self, x: mx.array, type_vec: mx.array) -> mx.array: r"""Forward pass. Args: x (mx.array): The input features of shape :obj:`[num_items, in_channels]`. type_vec (mx.array): An integer vector of shape :obj:`[num_items]` that maps each entry to a type in :obj:`[0, num_types)`. """ if not self.training and self.track_running_stats: mean, var = self.running_mean, self.running_var else: mean = scatter(x, type_vec, out_size=self.num_types, aggr="mean") mean_sq = scatter(x * x, type_vec, out_size=self.num_types, aggr="mean") var = mx.maximum(mean_sq - mean * mean, 0.0) if self.training and self.track_running_stats: if self.momentum is None: self.num_batches_tracked = self.num_batches_tracked + 1 exp_avg_factor = 1.0 / float(self.num_batches_tracked.item()) else: exp_avg_factor = self.momentum # Only update stats for types actually present in this batch, # leaving stats for absent types untouched. ones = mx.ones((type_vec.shape[0], 1)) counts = scatter(ones, type_vec, out_size=self.num_types, aggr="add") mask = counts > 0 new_running_mean = ( 1.0 - exp_avg_factor ) * self.running_mean + exp_avg_factor * mean new_running_var = ( 1.0 - exp_avg_factor ) * self.running_var + exp_avg_factor * var self.running_mean = mx.where(mask, new_running_mean, self.running_mean) self.running_var = mx.where(mask, new_running_var, self.running_var) out = (x - mean[type_vec]) / mx.sqrt(mx.maximum(var, self.eps))[type_vec] if self.affine: out = out * self.weight[type_vec] + self.bias[type_vec] return out
def __repr__(self) -> str: return ( f"{self.__class__.__name__}({self.in_channels}, " f"num_types={self.num_types})" )