Source code for ghonn_models_pytorch.core.honu

"""Defines the Higher-Order Neural Units (HONU) model."""

import math
from itertools import combinations_with_replacement
from typing import Any

import torch
from torch import Tensor, nn

__version__ = "0.0.2"


[docs] class HONU(nn.Module): """Higher-Order Neural Units (HONU) model for polynomial regression. This model computes polynomial feature combinations of the input data and applies trainable weights to produce the output. It supports configurable polynomial orders and optional bias terms. Methods: __init__: Initializes the HONU model with the specified parameters. __repr__: Returns a string representation of the HONU model. forward: Performs a forward pass through the HONU model. _validate_setup: Validates the configuration of the model. _initialize_weights: Initializes the trainable weights of the model. _get_combinations: Precomputes index combinations for polynomial features. _get_colx: Computes the polynomial feature map for the input batch. """ _comb_idx: Tensor
[docs] def __init__( self, in_features: int, polynomial_order: int, *, activation: str = "identity", **kwargs: dict[str, Any], ) -> None: """Initialize the Higher-Order Neural Units model. Args: in_features (int): Number of input features. polynomial_order (int): Order of the HONU model. activation (str, optional): Activation function to be used, by default "identity". **kwargs: Additional keyword arguments: - weight_divisor (int or float, optional): Divisor for the randomly initialized weights, by default 100.0. - bias (bool, optional): Whether to include a bias term, by default True. Attributes: order (int): Polynomial order of the model. in_features (int): Number of input features. _weight_divisor (float): Divisor used to scale the randomly initialized weights. _weight_init_mode (str): Method for initializing weights, can be "random", "zeros", "ones", "xavier", "kaiming_normal", or "kaiming_uniform". _activation (str): Activation function to be used. _activation_function (callable): The actual activation function to apply. _bias (bool): Indicates whether a bias term is included in the model. weight (nn.Parameter): Trainable weights of the model. _num_combinations (int): Number of polynomial feature combinations. _comb_idx (Tensor): Precomputed index combinations for polynomial features. """ super().__init__() # Main model parameters self.order = polynomial_order self.in_features = in_features # Optional params weight_divisor = kwargs.get("weight_divisor", 100.0) if not isinstance(weight_divisor, (int, float, str)): msg = f"weight_divisor must be a number or string, got {type(weight_divisor)}" raise TypeError(msg) self._weight_divisor = float(weight_divisor) self._weight_init_mode = kwargs.get("weight_init_mode", "random") self._bias = kwargs.get("bias", True) self._activation = activation if self._activation in ["identity", "linear"]: self._activation_function = lambda x: x else: self._activation_function = getattr(torch.nn.functional, self._activation) self._validate_setup() # Initialize weights as trainable parameters self.weight = nn.Parameter(self._initialize_weights()) # Get all combinations of indices for the polynomial features self._num_combinations = self.weight.size(0) self.register_buffer("_comb_idx", self._get_combinations())
[docs] def __repr__(self) -> str: """Return a string representation of the HONU model.""" return ( f"HONU(in_features={self.in_features}, polynomial_order={self.order}, " f"bias={self._bias}, activation={self._activation})" )
[docs] def _validate_setup(self) -> None: """Validates the configuration of the model to ensure all parameters are correctly set. This method checks the following conditions: - The `polynomial_order` must be greater than 0. - The `in_features` must be greater than 0. - The `weight_divisor` must be greater than 0. Raises: ValueError: If any of the above conditions are not met. """ if self.order <= 0: msg = f"Polynomial order must be greater than 0. Got {self.order}." raise ValueError(msg) if self.in_features <= 0: msg = f"Input length must be greater than 0. Got {self.in_features}." raise ValueError(msg) if self._weight_divisor <= 0: msg = f"Weight divisor must be greater than 0. Got {self._weight_divisor}." raise ValueError(msg)
[docs] def _initialize_weights(self) -> Tensor: """Initialize weights for the HONU model. This method initializes the weights for the model based on the input length, polynomial order, and whether a bias term is included. The number of weights is calculated using the formula for combinations with repetition: Combinations with repetition = ((n + r - 1)! / (r! * (n - 1)!)) where: - n is the number of states, calculated as the input length + 1 if a bias is included. - r is the polynomial order of the neuron. The weights are initialized using various methods based on the `_weight_init_mode`: - "random": Uniformly distributed random values scaled by `_weight_divisor`. - "zeros": All weights initialized to zero. - "ones": All weights initialized to one. - "xavier": Xavier initialization for 1D tensors. - "kaiming_normal": Kaiming normal initialization for 1D tensors. - "kaiming_uniform": Kaiming uniform initialization for 1D tensors. Returns: Array of initialized weights. """ # Calculate the number of weights needed based on the order and input length n_weights = self.in_features + 1 if self._bias else self.in_features num_weights = int( math.factorial(n_weights + self.order - 1) / (math.factorial(self.order) * math.factorial(n_weights - 1)) ) # Initialize weights using PyTorch native methods weights = torch.empty(num_weights) if self._weight_init_mode == "random": torch.nn.init.uniform_(weights, 0, 1) weights /= self._weight_divisor elif self._weight_init_mode == "zeros": torch.nn.init.zeros_(weights) elif self._weight_init_mode == "ones": torch.nn.init.ones_(weights) elif self._weight_init_mode == "xavier": # For 1D tensors, manually compute Xavier initialization limit = math.sqrt(6 / (self.in_features + num_weights)) torch.nn.init.uniform_(weights, -limit, limit) elif self._weight_init_mode == "kaiming_normal": # For 1D tensors, manually compute Kaiming normal initialization std = math.sqrt(2 / self.in_features) torch.nn.init.normal_(weights, mean=0, std=std) elif self._weight_init_mode == "kaiming_uniform": # For 1D tensors, manually compute Kaiming uniform initialization limit = math.sqrt(6 / self.in_features) torch.nn.init.uniform_(weights, -limit, limit) else: msg = f"Unknown weight initialization mode: {self._weight_init_mode}" raise ValueError(msg) return weights
[docs] def _get_combinations(self) -> Tensor: """Precompute and return all index combinations for the given input length and order. This method generates combinations with replacement of indices based on the input length and polynomial order. If bias is included, an additional feature is accounted for in the combinations. The resulting combinations are stored as a tensor. Returns: Tensor: A tensor containing all index combinations with shape (num_combinations, order). """ # Precompute all index combinations once and store as buffer n_feat = self.in_features + (1 if self._bias else 0) return torch.tensor( list(combinations_with_replacement(range(n_feat), self.order)), dtype=torch.long, ) # shape: (num_combinations, order)
[docs] def _get_colx(self, x: Tensor) -> Tensor: """Compute polynomial feature map using precomputed index combinations. For each sample in the batch, generates all degree-`order` monomials (with replacement) of the input features. If `bias=True`, a constant term is prepended before forming combinations. Args: x : Input batch of shape (batch_size, input_length). Returns: Tensor[B, num_combinations]: Tensor of shape (batch_size, num_combinations) where each column is the product of one combination of input features. """ # Add bias column if needed if self._bias: ones = torch.ones((x.size(0), 1), device=x.device, dtype=x.dtype) x = torch.cat([ones, x], dim=1) # now x.shape = [B, n_feat] # x_exp expected shape [B, num_combinations, n_feat] x_exp = x.unsqueeze(1).expand(-1, self._comb_idx.size(0), -1) # idx expected shape [B, num_combinations, order] idx = self._comb_idx.unsqueeze(0).expand(x.size(0), -1, -1) # selected expected shape [B, num_combinations, order] selected = x_exp.gather(2, idx) # colx expected shape [B, num_combinations] return selected.prod(dim=2)
[docs] def forward(self, x: Tensor) -> Tensor: """Perform the forward pass of the HONU model. Args: x: Input tensor [B, input_length] with the data. Returns: Tensor[B, 1]: Output tensor from the model. """ # Check if input shape matches the expected input length if x.size(1) != self.in_features: msg = f"Input shape mismatch: expected {self.in_features} features, got {x.size(1)}." raise ValueError(msg) # Get the polynomial feature map colx = self._get_colx(x) # Compute the output by multiplying the feature map with the weights return self._activation_function(torch.matmul(colx, self.weight.view(-1, 1)))
if __name__ == "__main__": from pathlib import Path filename = Path(__file__).name MSG = f"The {filename} is not meant to be run as a script." raise OSError(MSG)