import torch from torch import nn from src.nets.util import parse_functional class DeepSetsModule(nn.Module): def __init__(self, input_dim, phi_hidden_n, phi_hidden_dim, latent_dim, rho_hidden_n, rho_hidden_dim, output_dim): """ Args: input_dim (int): input size of one instance of the set; input size of phi phi_hidden_n (int): number of hidden layers in phi phi_hidden_dim (int): size of hidden layers in phi latent_dim (int): output size of phi network, where sum is taken over instances; input size of rho rho_hidden_n (int): number of hidden layers in rho rho_hidden_dim (int): size of hidden layers in rho output_dim (int): output size of rho """ super(DeepSetsModule, self).__init__() self.input_dim = input_dim self.latent_dim = latent_dim self.phi = Phi(self.input_dim, phi_hidden_n, phi_hidden_dim, self.latent_dim) self.rho = Phi(self.latent_dim, rho_hidden_n, rho_hidden_dim, output_dim) self.output_dim = self.rho.output_dim self.pooling = torch.sum @staticmethod def from_config(config): """ Args: config (dict): dictionary with network parameters in the form { "input_dim": 5, "phi": { "hidden_n": 1, "hidden_dim": 10, }, "latent_dim": 8, "rho": { "hidden_n": 1, "hidden_dim": 10, }, "output_dim" : 1, } Returns: m (nn.Module): deep sets module """ input_dim = config["input_dim"] phi = config["phi"] latent_dim = config["latent_dim"] rho = config["rho"] output_dim = config["output_dim"] m = DeepSetsModule(input_dim, phi["hidden_n"], phi["hidden_dim"], latent_dim, rho["hidden_n"], rho["hidden_dim"], output_dim) return m def forward(self, x): """ Args: x (torch.tensor): ([B, ]max_nv, d) Returns: y (torch.tensor): ([B, ]output_dim) """ # mask for selecting only those batches and vehicles where all relative states are not nan # shape (B, max_nv) notnan_mask = torch.all(~torch.isnan(x), dim=-1) # create zero tensor of shape (B, max_nv, latent_dim) to store phi evaluations in latent = torch.zeros([*x.shape[:-1], self.latent_dim], dtype=x.dtype) # evaluate phi for all not NaN entries # x[batch_dynamic_mask] has shape (notnan_mask.sum(), input_dim) latent[notnan_mask] = self.phi(x[notnan_mask]) # sum over relative state dimension latent = self.pooling(latent, dim=-2) # apply rho network y = self.rho(latent) return y class Phi(nn.Module): def __init__(self, input_dim, hidden_n, hidden_dim, output_dim, final_activation=None): """ Fully connected feedforward network with same size for all hidden layers and ReLU activation Args: input_dim (int): input dimension hidden_n (int): number of hidden layers hidden_dim (int): hidden layer dimension output_dim (int): output dimension """ super(Phi, self).__init__() self.input_dim = input_dim self.output_dim = output_dim if hidden_n > 0: self.layers = nn.ModuleList([nn.Linear(self.input_dim, hidden_dim)]) for _ in range(hidden_n - 1): self.layers.append(nn.Linear(hidden_dim, hidden_dim)) self.layers.append(nn.Linear(hidden_dim, self.output_dim)) else: self.layers = nn.ModuleList([nn.Identity()]) self.output_dim = self.input_dim self.activation = nn.functional.relu self.final_activation = final_activation if final_activation else lambda x: x def forward(self, x): for layer in self.layers[:-1]: x = self.activation(layer(x)) x = self.final_activation(self.layers[-1](x)) return x @staticmethod def from_config(config): args = (config["input_dim"], config["hidden_n"], config["hidden_dim"], config["output_dim"]) if "final_activation" in config: kwargs = {"final_activation": parse_functional(config["final_activation"])} else: kwargs = {} return Phi(*args, **kwargs)