Source code for tgp.select.dp_select

from typing import List, Optional, Union

import torch
import torch.nn.functional as F
from torch import Tensor
from torch.distributions import Beta

from tgp.select import SelectOutput
from tgp.select.mlp_select import MLPSelect
from tgp.utils.typing import SinvType


[docs] class DPSelect(MLPSelect): r"""The Dirichlet Process selection operator for the :class:`~tgp.poolers.BNPool` operator, as proposed in the paper `"BN-Pool: Bayesian Nonparametric Graph Pooling" <https://arxiv.org/abs/2501.09821>`_ (Castellana & Bianchi, 2025). DPSelect implements a Bayesian nonparametric selection mechanism to automatically learn both the number of clusters and their assignments through variational inference. The method uses a truncated stick-breaking representation of the Dirichlet Process to model cluster assignments: .. math:: v_{ik} \sim \text{Beta}(\alpha_{ik}, \beta_{ik}), \quad k = 1, \ldots, K-1, \quad i = 1, \ldots, N where :math:`v_{ik}` are the stick-breaking fractions. The assignment of node :math:`i` to cluster :math:`k` is computed as: .. math:: \pi_{ik} = v_{ik} \prod_{j=1}^{k-1} (1 - v_{ij}) \quad \text{for } k = 1, \ldots, K-1 The variational parameters :math:`\alpha_{ik}, \beta_{ik}` are computed by an MLP from node features: .. math:: [\alpha_{i,1}, \ldots, \alpha_{i,K-1}, \beta_{i,1}, \ldots, \beta_{i,K-1}] = \text{softplus}(\text{MLP}(\mathbf{x}_i)) The procedure can be summarized as follows: 1. **Feature Processing**: Node features are processed by an MLP to produce :math:`2(K-1)` outputs per node. 2. **Parameter Extraction**: The MLP output is split into :math:`\boldsymbol{\alpha}` and :math:`\boldsymbol{\beta}` parameters for the Beta distribution. 3. **Sampling**: Stick-breaking fractions are obtained from the sampling procedure implemented in :class:`~torch.distributions.beta.Beta`: :math:`v_{ik} = \text{Beta}(\alpha_{ik}, \beta_{ik}).rsample()`. 4. **Cluster Assignment**: The assignment matrix is computed via the stick-breaking construction. Args: in_channels (int, list of int): Number of hidden units for each hidden layer in the :class:`~torch_geometric.nn.models.mlp.MLP` used to compute cluster assignments. The first integer must match the size of the node features. k (int): Maximum number of clusters :math:`K`. The actual number of active clusters is learned through the stick-breaking process. batched_representation (bool, optional): If :obj:`True`, expects batched input :math:`\mathbf{X} \in \mathbb{R}^{B \times N \times F}` and returns assignment matrix :math:`\mathbf{S} \in \mathbb{R}^{B \times N \times K}`. If :obj:`False`, expects unbatched input :math:`\mathbf{X} \in \mathbb{R}^{N \times F}` where :math:`N` is the total number of nodes across all graphs, and returns assignment matrix :math:`\mathbf{S} \in \mathbb{R}^{N \times K}`. (default: :obj:`True`) act (str or Callable, optional): Activation function in the hidden layers of the :class:`~torch_geometric.nn.models.mlp.MLP`. dropout (float, optional): Dropout probability in the :class:`~torch_geometric.nn.models.mlp.MLP`. (default: ``0.0``) s_inv_op (~tgp.utils.typing.SinvType, optional): The operation used to compute :math:`\mathbf{S}_\text{inv}` from the select matrix :math:`\mathbf{S}`. :math:`\mathbf{S}_\text{inv}` is stored in the ``"s_inv"`` attribute of the :class:`~tgp.select.SelectOutput`. It can be one of: - ``"transpose"`` (default): Computes :math:`\mathbf{S}_\text{inv}` as :math:`\mathbf{S}^\top`, the transpose of :math:`\mathbf{S}`. - ``"inverse"``: Computes :math:`\mathbf{S}_\text{inv}` as :math:`\mathbf{S}^+`, the Moore-Penrose pseudoinverse of :math:`\mathbf{S}`. Note: This class extends :class:`~tgp.select.MLPSelect` but replaces the softmax assignment with the stick-breaking construction. The :class:`~tgp.select.SelectOutput` returned includes both the assignment matrix :math:`\mathbf{S}` and the posterior distributions :math:`q(v_{ik})` for computing KL divergence losses. """ def __init__( self, in_channels: Union[int, List[int]], k: int, batched_representation: bool = True, act: str = None, dropout: float = 0.0, s_inv_op: SinvType = "transpose", ): # 2*max_key needs to compute both alphas and betas of the posterior super(DPSelect, self).__init__( in_channels=in_channels, k=2 * (k - 1), act=act, dropout=dropout, s_inv_op=s_inv_op, ) self.k = k self.batched_representation = batched_representation @staticmethod def _compute_pi_given_sticks(stick_fractions): """Computes the stick-breaking proportions (pi) for a given set of stick fractions. This function implements the stick-breaking by multiplying the stick fractions. The multiplications are done in the logarithmic space to avoid numerical errors. Args: stick_fractions (torch.Tensor): A tensor representing the stick fractions with shape :math:`[..., K-1]`. Each value must be within the interval (0, 1). Returns: torch.Tensor: A tensor containing the cluster assignment probabilities with shape :math:`[..., K]`. """ out_size = stick_fractions.size() device = stick_fractions.device pi = torch.zeros(out_size[:-1] + (out_size[-1] + 1,), device=device) pi[..., :-1] = torch.log(stick_fractions) pi[..., 1:] += torch.cumsum(torch.log(1 - stick_fractions), dim=-1) return torch.exp(pi) def _inner_forward(self, x): out = torch.clamp(F.softplus(self.mlp(x)), min=1e-3, max=1e3) q_v_alpha, q_v_beta = torch.split(out, self.k - 1, dim=-1) q_z = Beta(q_v_alpha, q_v_beta) z = q_z.rsample() s = self._compute_pi_given_sticks(z) return s, q_z
[docs] def forward( self, x: Tensor, mask: Optional[Tensor] = None, batch: Optional[Tensor] = None, **kwargs, ) -> SelectOutput: r"""Applies the Dirichlet Process selection operator to compute cluster assignments. Args: x (~torch.Tensor): Node feature tensor. If ``batched_representation=True``, expected shape is :math:`\mathbb{R}^{B \times N \times F}`. If ``batched_representation=False``, expected shape is :math:`\mathbb{R}^{N \times F}`, where :math:`N` is the total number of nodes across all graphs in the batch. mask (~torch.Tensor, optional): Input-node validity mask :math:`\mathbf{M} \in {\{ 0, 1 \}}^{B \times N}` with :obj:`True` on real (non-padded) nodes. Only used when ``batched_representation=True``. (default: :obj:`None`) batch (~torch.Tensor, optional): The batch vector :math:`\mathbf{b} \in {\{ 0, \ldots, B-1\}}^N`, which indicates to which graph in the batch each node belongs. Only used when ``batched_representation=False``. (default: :obj:`None`) Returns: :class:`~tgp.select.SelectOutput`: The output of :math:`\texttt{select}` operator. If ``batched_representation=True``, the assignment matrix :math:`\mathbf{S}` has shape :math:`\mathbb{R}^{B \times N \times K}`. If ``batched_representation=False``, the assignment matrix :math:`\mathbf{S}` has shape :math:`\mathbb{R}^{N \times K}`. """ x = self._prepare_inputs(x) s, q_z = self._inner_forward(x) if self.batched_representation: s = self._apply_mask(s, mask) return self._build_output(s, mask=mask, q_z=q_z) return self._build_output(s, batch=batch, q_z=q_z)