Download model/nn/mlp.py from OneScience-Group/NequIP: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/OneScience-Group/NequIP/resolve/main/model/nn/mlp.py
- Command line
-
hf download hf://OneScience-Group/NequIP/model/nn/mlp.py
-
curl -L -o mlp.py https://huggingface.co/OneScience-Group/NequIP/resolve/main/model/nn/mlp.py
11.1 kB
| # This file is a part of the `nequip` package. Please see LICENSE and README at the root for information on using it. | |
| from math import sqrt, prod | |
| import torch | |
| from e3nn.o3._irreps import Irreps | |
| from e3nn.util.jit import compile_mode | |
| from onescience.datapipes.materials.nequip import AtomicDataDict | |
| from ._graph_mixin import GraphModuleMixin | |
| from .nonlinearities import ShiftedSoftplus | |
| from typing import Optional, Final, Dict | |
| _NONLINEARITY_MAP: Final[Dict[str, torch.nn.Module]] = { | |
| # NOTE: we include str options for `None` so that the parser always works | |
| None: torch.nn.Identity, | |
| "None": torch.nn.Identity, | |
| "null": torch.nn.Identity, | |
| "silu": torch.nn.SiLU, | |
| "mish": torch.nn.Mish, | |
| "gelu": torch.nn.GELU, | |
| "ssp": ShiftedSoftplus, | |
| "tanh": torch.nn.Tanh, | |
| # not 0 -> 0 | |
| "sigmoid": torch.nn.Sigmoid, | |
| "softplus": torch.nn.Softplus, | |
| } | |
| class ScalarMLP(GraphModuleMixin, torch.nn.Module): | |
| """Apply an MLP to some scalar field.""" | |
| field: str | |
| out_field: str | |
| def __init__( | |
| self, | |
| output_dim: int, | |
| hidden_layers_depth: int = 0, | |
| hidden_layers_width: Optional[int] = None, | |
| nonlinearity: Optional[str] = "silu", | |
| bias: bool = False, | |
| forward_weight_init: bool = True, | |
| init_mode: str = "uniform", | |
| parametrization: Optional[str] = None, | |
| field: str = AtomicDataDict.NODE_FEATURES_KEY, | |
| out_field: Optional[str] = None, | |
| irreps_in=None, | |
| ): | |
| super().__init__() | |
| self.field = field | |
| self.out_field = out_field if out_field is not None else field | |
| self._init_irreps( | |
| irreps_in=irreps_in, | |
| required_irreps_in=[self.field], | |
| ) | |
| assert len(self.irreps_in[self.field]) == 1 | |
| assert self.irreps_in[self.field][0].ir == (0, 1) # scalars | |
| self.mlp_module = ScalarMLPFunction( | |
| input_dim=self.irreps_in[self.field][0].mul, | |
| output_dim=output_dim, | |
| hidden_layers_depth=hidden_layers_depth, | |
| hidden_layers_width=hidden_layers_width, | |
| nonlinearity=nonlinearity, | |
| bias=bias, | |
| forward_weight_init=forward_weight_init, | |
| init_mode=init_mode, | |
| parametrization=parametrization, | |
| ) | |
| self.irreps_out[self.out_field] = Irreps([(self.mlp_module.dims[-1], (0, 1))]) | |
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: | |
| data[self.out_field] = self.mlp_module(data[self.field]) | |
| return data | |
| class ScalarMLPFunction(torch.nn.Module): | |
| """Module implementing an MLP according to provided options. | |
| ``input_dim`` and ``output_dim`` are mandatory arguments. | |
| If only ``input_dim`` and ``output_dim`` are specified, this module defaults to a linear layer (corresponding to the default of ``hidden_layers_depth=0``). | |
| If ``hidden_layers_depth!=0``, ``hidden_layers_width`` must be configured (an error will be raised if the default of ``hidden_layers_width=None`` is used). | |
| Args: | |
| nonlinearity (str): ``silu`` (default), ``mish``, ``gelu``, ``ssp``, ``tanh``, ``None``, ``null``, or ``"None"`` | |
| bias (bool): whether a bias is included (default ``False``) | |
| forward_weight_init (bool): whether to initialize weights to preserve forward activation variance (default ``True``) or initialize weights to preserve backward gradient variance | |
| """ | |
| num_layers: int | |
| bias: bool | |
| is_nonlinear: bool | |
| def __init__( | |
| self, | |
| input_dim: int, | |
| output_dim: int, | |
| hidden_layers_depth: int = 0, | |
| hidden_layers_width: Optional[int] = None, | |
| nonlinearity: Optional[str] = "silu", | |
| bias: bool = False, | |
| forward_weight_init: bool = True, | |
| init_mode: str = "uniform", | |
| parametrization: Optional[str] = None, | |
| ): | |
| super().__init__() | |
| self.bias = bias | |
| # === process MLP dims === | |
| if hidden_layers_depth != 0: | |
| assert hidden_layers_depth > 0 and hidden_layers_width > 0 | |
| hidden_layers_dims = hidden_layers_depth * [hidden_layers_width] | |
| self.dims = [input_dim] + hidden_layers_dims + [output_dim] | |
| self.num_layers = len(self.dims) - 1 | |
| assert self.num_layers >= 1 | |
| # NOTE: `input_dim` and `output_dim` are always mandatory, which default to at least a linear | |
| # a one-layer MLP is a linear layer | |
| # === handle nonlinearity === | |
| # TODO: maybe adapt gain to be nonlinearity dependent | |
| if nonlinearity not in _NONLINEARITY_MAP: | |
| available_options = list(_NONLINEARITY_MAP.keys()) | |
| raise ValueError( | |
| f"Unknown nonlinearity '{nonlinearity}'. Available options: {available_options}" | |
| ) | |
| nonlinearity_module = _NONLINEARITY_MAP[nonlinearity] | |
| self.is_nonlinear = False # updated below in loop | |
| # === build the MLP + weight init === | |
| mlp = torch.nn.Sequential() | |
| for layer, (h_in, h_out) in enumerate(zip(self.dims, self.dims[1:])): | |
| # === weight initialization === | |
| # normalize to preserve variance of forward activations or backward derivatives | |
| # we use "relu" gain (sqrt(2)) as a stand-in for the smooth nonlinearities we use, and only apply them if there is a nonlinearity | |
| # for forward (backward) norm, we don't include the gain for the first (last) layer | |
| # see https://pytorch.org/docs/stable/nn.init.html#torch.nn.init.kaiming_uniform_ | |
| if forward_weight_init: | |
| norm_dim = h_in | |
| gain = 1.0 if nonlinearity is None or (layer == 0) else sqrt(2) | |
| else: | |
| norm_dim = h_out | |
| gain = ( | |
| 1.0 | |
| if nonlinearity is None or (layer == self.num_layers - 1) | |
| else sqrt(2) | |
| ) | |
| # === instantiate `Linear` === | |
| linear_layer = ScalarLinearLayer( | |
| in_features=h_in, | |
| out_features=h_out, | |
| alpha=gain / sqrt(norm_dim), | |
| bias=bias, | |
| init_mode=init_mode, | |
| ) | |
| # apply parametrization if specified | |
| if parametrization == "spectral_norm": | |
| torch.nn.utils.parametrizations.spectral_norm( | |
| linear_layer, "weight", dim=1 | |
| ) | |
| elif parametrization == "weight_norm": | |
| torch.nn.utils.parametrizations.weight_norm( | |
| linear_layer, "weight", dim=1 | |
| ) | |
| elif parametrization == "orthogonal": | |
| torch.nn.utils.parametrizations.orthogonal(linear_layer, "weight") | |
| elif parametrization not in [None, "None", "null"]: | |
| raise ValueError( | |
| f"Unknown parametrization '{parametrization}'. " | |
| "Available options: None, 'weight_norm', 'orthogonal', 'spectral_norm'" | |
| ) | |
| mlp.append(linear_layer) | |
| del gain, norm_dim | |
| # === add nonlinearity (if any) except for last layer === | |
| if (layer != self.num_layers - 1) and (nonlinearity is not None): | |
| # only update `self.is_nonlinear` when a nonlinearity is applied | |
| mlp.append(nonlinearity_module()) | |
| self.is_nonlinear = True | |
| # use `multidot` based implementation for deep linear net (no nonlinearity, no bias, more than one layer) | |
| # otherwise use the `mlp` built in init | |
| if (not self.is_nonlinear) and (not self.bias) and (self.num_layers > 1): | |
| self.mlp = DeepLinearMLP(mlp) | |
| del mlp | |
| else: | |
| self.mlp = mlp | |
| def forward(self, x): | |
| return self.mlp(x) | |
| class DeepLinearMLP(torch.nn.Module): | |
| def __init__(self, mlp) -> None: | |
| super().__init__() | |
| self.weights = torch.nn.ParameterList() | |
| alphas = [] | |
| for this_idx, mlp_idx in enumerate(range(len(mlp))): | |
| new_weight = torch.clone(mlp[mlp_idx].weight) | |
| self.weights.append(new_weight) | |
| del new_weight | |
| alphas.append(mlp[mlp_idx].alpha) | |
| alpha = prod(alphas) | |
| # the constant has to be a buffer for constant-folding to happen with `torch.compile(...dynamic=True)` | |
| # `persistent=False` for backwards compatibility of checkpoint files | |
| # (and technically preserves the old behavior when using a float in that it's also not persistent) | |
| # `alpha` is already a torch.Tensor here | |
| self.register_buffer("alpha", alpha, persistent=False) | |
| del alphas | |
| def forward(self, input: torch.Tensor) -> torch.Tensor: | |
| weight = torch.mul( | |
| torch.linalg.multi_dot([weight for weight in self.weights]), self.alpha | |
| ) | |
| return torch.mm(input, weight) | |
| class ScalarLinearLayer(torch.nn.Module): | |
| """Module implementing a linear layer with a scaling factor `alpha` applied to the weights.""" | |
| in_features: int | |
| out_features: int | |
| def __init__( | |
| self, | |
| in_features: int, | |
| out_features: int, | |
| alpha: float = 1.0, | |
| bias: bool = False, | |
| init_mode: str = "uniform", | |
| ) -> None: | |
| super().__init__() | |
| self.in_features = in_features | |
| self.out_features = out_features | |
| # the constant has to be a buffer for constant-folding to happen with `torch.compile(...dynamic=True)` | |
| # `persistent=False` for backwards compatibility of checkpoint files | |
| # (and technically preserves the old behavior when using a float in that it's also not persistent) | |
| self.register_buffer("alpha", torch.tensor(alpha), persistent=False) | |
| self.weight = torch.nn.Parameter(torch.empty((in_features, out_features))) | |
| # initialize weights based on init_mode | |
| if init_mode == "uniform": | |
| # initialize weights to uniform distribution with mean 0 variance 1 | |
| torch.nn.init.uniform_(self.weight, -sqrt(3), sqrt(3)) | |
| elif init_mode == "normal": | |
| # initialize weights to normal distribution with mean 0 std 1 | |
| torch.nn.init.normal_(self.weight, mean=0.0, std=1.0) | |
| else: | |
| raise ValueError( | |
| f"Unknown init_mode: {init_mode}. Must be 'uniform' or 'normal'." | |
| ) | |
| # initialize bias (if any) to zeros | |
| if bias: | |
| self.bias = torch.nn.Parameter(torch.zeros(out_features)) | |
| else: | |
| self.register_parameter("bias", None) | |
| def forward(self, input: torch.Tensor) -> torch.Tensor: | |
| # compute scaled weights separately to be constant folded | |
| weight = self.weight * self.alpha | |
| if self.bias is None: | |
| return torch.mm(input, weight) | |
| else: | |
| return torch.addmm(self.bias, input, weight) | |
| def extra_repr(self) -> str: | |
| return f"in_features={self.in_features}, out_features={self.out_features}, bias={self.bias is not None}, alpha={self.alpha:.6f}" | |