""" Utility nn components, in particular handling activations, initializations, and normalization layers """ import math from functools import partial import torch import torch.nn as nn from opt_einsum import contract class modrelu(nn.Module): def __init__(self, features): # For now we just support square layers super(modrelu, self).__init__() self.features = features self.b = nn.Parameter(torch.Tensor(self.features)) self.reset_parameters() def reset_parameters(self): self.b.data.uniform_(-0.01, 0.01) def forward(self, inputs): norm = torch.abs(inputs) biased_norm = norm + self.b magnitude = nn.functional.relu(biased_norm) phase = torch.sign(inputs) return phase * magnitude class Modrelu(modrelu): def reset_parameters(self): self.b.data.uniform_(-0.01, 0.01) def Activation(activation=None, size=None, dim=-1): if activation in [None, "id", "identity", "linear"]: return nn.Identity() elif activation == "tanh": return nn.Tanh() elif activation == "relu": return nn.ReLU() elif activation == "gelu": return nn.GELU() elif activation in ["swish", "silu"]: return nn.SiLU() elif activation == "glu": return nn.GLU(dim=dim) elif activation == "sigmoid": return nn.Sigmoid() elif activation == "modrelu": return Modrelu(size) else: raise NotImplementedError( "hidden activation '{}' is not implemented".format(activation) ) def get_initializer(name, activation=None): if activation in [None, "id", "identity", "linear", "modrelu"]: nonlinearity = "linear" elif activation in ["relu", "tanh", "sigmoid"]: nonlinearity = activation elif activation in ["gelu", "swish", "silu"]: nonlinearity = "relu" # Close to ReLU so approximate with ReLU's gain else: raise NotImplementedError( f"get_initializer: activation {activation} not supported" ) if name == "uniform": initializer = partial(torch.nn.init.kaiming_uniform_, nonlinearity=nonlinearity) elif name == "normal": initializer = partial(torch.nn.init.kaiming_normal_, nonlinearity=nonlinearity) elif name == "xavier": initializer = torch.nn.init.xavier_normal_ elif name == "zero": initializer = partial(torch.nn.init.constant_, val=0) elif name == "one": initializer = partial(torch.nn.init.constant_, val=1) else: raise NotImplementedError( f"get_initializer: initializer type {name} not supported" ) return initializer def LinearActivation( d_input, d_output, bias=True, zero_bias_init=False, transposed=False, initializer=None, activation=None, activate=False, # Apply activation as part of this module weight_norm=False, **kwargs, ): """Returns a linear nn.Module with control over axes order, initialization, and activation""" # Construct core module linear_cls = TransposedLinear if transposed else nn.Linear if activation == "glu": d_output *= 2 linear = linear_cls(d_input, d_output, bias=bias, **kwargs) # Initialize weight if initializer is not None: get_initializer(initializer, activation)(linear.weight) # Initialize bias if bias and zero_bias_init: nn.init.zeros_(linear.bias) # Weight norm if weight_norm: linear = nn.utils.weight_norm(linear) if activate and activation is not None: activation = Activation(activation, d_output, dim=-2 if transposed else -1) linear = nn.Sequential(linear, activation) return linear class TransposedLinear(nn.Module): """Linear module on the second-to-last dimension""" def __init__(self, d_input, d_output, bias=True): super().__init__() self.weight = nn.Parameter(torch.empty(d_output, d_input)) nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5)) # nn.Linear default init # nn.init.kaiming_uniform_(self.weight, nonlinearity='linear') # should be equivalent if bias: self.bias = nn.Parameter(torch.empty(d_output, 1)) bound = 1 / math.sqrt(d_input) nn.init.uniform_(self.bias, -bound, bound) else: self.bias = 0.0 def forward(self, x): return contract("... u l, v u -> ... v l", x, self.weight) + self.bias class TransposedLN(nn.Module): """LayerNorm module over second-to-last dimension This is slow and a dedicated CUDA/Triton implementation shuld provide substantial end-to-end speedup """ def __init__(self, d, scalar=True): super().__init__() self.scalar = scalar if self.scalar: self.m = nn.Parameter(torch.zeros(1)) self.s = nn.Parameter(torch.ones(1)) else: self.ln = nn.LayerNorm(d) def forward(self, x): if self.scalar: s, m = torch.std_mean(x, dim=-2, unbiased=False, keepdim=True) y = (self.s / s) * (x - m + self.m) else: y = self.ln(x.transpose(-1, -2)).transpose(-1, -2) return y class Normalization(nn.Module): def __init__( self, d, transposed=False, # Length dimension is -1 or -2 _name_="layer", **kwargs, ): super().__init__() self.transposed = transposed if _name_ == "layer": self.channel = True # Normalize over channel dimension if self.transposed: self.norm = TransposedLN(d, **kwargs) else: self.norm = nn.LayerNorm(d, **kwargs) elif _name_ == "instance": self.channel = False norm_args = {"affine": False, "track_running_stats": False} norm_args.update(kwargs) self.norm = nn.InstanceNorm1d( d, **norm_args ) # (True, True) performs very poorly elif _name_ == "batch": self.channel = False norm_args = {"affine": True, "track_running_stats": True} norm_args.update(kwargs) self.norm = nn.BatchNorm1d(d, **norm_args) elif _name_ == "none": self.channel = True self.norm = nn.Identity() else: raise NotImplementedError def forward(self, x): # The cases of LayerNorm / no normalization are automatically handled in all cases # Instance/Batch Norm work automatically with transposed axes if self.channel or self.transposed: return self.norm(x) else: x = x.transpose(-1, -2) x = self.norm(x) x = x.transpose(-1, -2) return x