Source code for tgp.poolers.maxcut

from typing import Optional, Union

from torch import Tensor
from torch_geometric.typing import Adj

from tgp.connect import SparseConnect
from tgp.lift import BaseLift
from tgp.reduce import BaseReduce
from tgp.select import MaxCutSelect, SelectOutput
from tgp.src import PoolingOutput, SRCPooling
from tgp.utils.losses import maxcut_loss
from tgp.utils.ops import connectivity_to_edge_index
from tgp.utils.typing import ConnectionType, LiftType, ReduceType, SinvType


[docs] class MaxCutPooling(SRCPooling): r"""The MaxCut pooling operator from the paper `"MaxCutPool: differentiable feature-aware Maxcut for pooling in graph neural networks" <https://arxiv.org/abs/2409.05100>`_ (Abate & Bianchi, ICLR 2025). This pooling layer uses a differentiable MaxCut objective to learn node assignments. It is particularly effective for heterophilic graphs and provides robust pooling through graph topology-aware scoring. + The :math:`\texttt{select}` operator is implemented with :class:`~tgp.select.MaxCutSelect`, which computes MaxCut-aware node scores and performs top-k selection. + The :math:`\texttt{reduce}` operator is implemented with :class:`~tgp.reduce.BaseReduce`. + The :math:`\texttt{connect}` operator is implemented with :class:`~tgp.connect.SparseConnect`. + The :math:`\texttt{lift}` operator is implemented with :class:`~tgp.lift.BaseLift`. This layer provides one auxiliary loss: + the MaxCut loss (:class:`~tgp.utils.losses.maxcut_loss`). Args: in_channels (int): Size of each input sample. ratio (Union[float, int]): Graph pooling ratio for top-k selection. (default: ``0.5``) assign_all_nodes (bool, optional): Whether to create assignment matrices that map all nodes to the closest supernode (True) or perform standard top-k selection (False). (default: :obj:`True`) max_iter (int, optional): Maximum distance for the closest node assignment. (default: ``5``) loss_coeff (float, optional): Coefficient for the MaxCut auxiliary loss. (default: ``1.0``) mp_units (list, optional): List of hidden units for message passing layers. (default: ``[32, 32, 32, 32, 16, 16, 16, 16, 8, 8, 8, 8]``) mp_act (str, optional): Activation function for message passing layers. (default: ``"tanh"``) mlp_units (list, optional): List of hidden units for MLP layers. (default: ``[16, 16]``) mlp_act (str, optional): Activation function for MLP layers. (default: ``"relu"``) act (str, optional): Activation function for the final score. (default: ``"tanh"``) delta (float, optional): Delta parameter for propagation matrix computation. (default: ``2.0``) lift (~tgp.utils.typing.LiftType, optional): Defines how to compute the matrix :math:`\mathbf{S}_\text{inv}` to lift the pooled node features. - ``"precomputed"`` (default): Use as :math:`\mathbf{S}_\text{inv}` what is already stored in the ``"s_inv"`` attribute of the :class:`~tgp.select.SelectOutput`. - ``"transpose"``: Recomputes :math:`\mathbf{S}_\text{inv}` as :math:`\mathbf{S}^\top`, the transpose of :math:`\mathbf{S}`. - ``"inverse"``: Recomputes :math:`\mathbf{S}_\text{inv}` as :math:`\mathbf{S}^+`, the Moore-Penrose pseudoinverse of :math:`\mathbf{S}`. 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}`. connect_red_op (~tgp.utils.typing.ConnectionType, optional): The aggregation function to be applied to all edges connecting nodes assigned to supernodes :math:`i` and :math:`j`. Can be any string of class :class:`~tgp.utils.typing.ConnectionType` admitted by :obj:`~torch_geometric.utils.coalesce`, e.g., ``'sum'``, ``'mean'``, ``'max'``) (default: ``"sum"``) lift_red_op (~tgp.utils.typing.ReduceType, optional): The aggregation function to be applied to the lifted node features. Can be any string of class :class:`~tgp.utils.typing.ReduceType` admitted by :obj:`~torch_geometric.utils.scatter`, e.g., ``'sum'``, ``'mean'``, ``'max'``) (default: ``"sum"``) remove_self_loops (bool, optional): If :obj:`True`, the self-loops will be removed from the adjacency matrix. (default: :obj:`True`) degree_norm (bool, optional): If :obj:`True`, the adjacency matrix will be symmetrically normalized. (default: :obj:`False`) edge_weight_norm (bool, optional): Whether to normalize the edge weights by dividing by the maximum absolute value per graph. (default: :obj:`True`) """ def __init__( self, in_channels: int, ratio: Union[float, int] = 0.5, assign_all_nodes: bool = True, max_iter: int = 5, loss_coeff: float = 1.0, mp_units: list = [32, 32, 32, 32], mp_act: str = "tanh", mlp_units: list = [16, 16], mlp_act: str = "relu", act: str = "tanh", delta: float = 2.0, lift: LiftType = "precomputed", s_inv_op: SinvType = "transpose", connect_red_op: ConnectionType = "sum", lift_red_op: ReduceType = "sum", remove_self_loops: bool = True, degree_norm: bool = False, edge_weight_norm: bool = True, ): super().__init__( selector=MaxCutSelect( in_channels=in_channels, ratio=ratio, assign_all_nodes=assign_all_nodes, max_iter=max_iter, mp_units=mp_units, mp_act=mp_act, mlp_units=mlp_units, mlp_act=mlp_act, act=act, delta=delta, s_inv_op=s_inv_op, ), reducer=BaseReduce(), connector=SparseConnect( reduce_op=connect_red_op, edge_weight_norm=edge_weight_norm, degree_norm=degree_norm, remove_self_loops=remove_self_loops, ), lifter=BaseLift(matrix_op=lift, reduce_op=lift_red_op), ) self.in_channels = in_channels self.ratio = ratio self.assign_all_nodes = assign_all_nodes self.max_iter = max_iter self.loss_coeff = loss_coeff self.mp_units = mp_units self.mp_act = mp_act self.mlp_units = mlp_units self.mlp_act = mlp_act self.act = act self.delta = delta
[docs] def forward( self, x: Tensor, adj: Optional[Adj] = None, edge_weight: Optional[Tensor] = None, so: Optional[SelectOutput] = None, batch: Optional[Tensor] = None, lifting: bool = False, **kwargs, ) -> PoolingOutput: r"""Forward pass of the MaxCut pooling operator. Args: x (~torch.Tensor): Node features of shape :math:`(N, F)`. adj (~torch_geometric.typing.Adj, optional): Graph connectivity. Can be edge_index tensor of shape :math:`(2, E)` or SparseTensor. (default: :obj:`None`) edge_weight (~torch.Tensor, optional): Edge weights of shape :math:`(E,)`. (default: :obj:`None`) so (~tgp.select.SelectOutput, optional): The output of the select operator. (default: :obj:`None`) batch (~torch.Tensor, optional): Batch assignments of shape :math:`(N,)`. (default: :obj:`None`) lifting (bool, optional): If :obj:`True`, perform lift operation. (default: :obj:`False`) Returns: PoolingOutput: The output of the pooling operator. """ if lifting: # Lift if so is None: raise ValueError("SelectOutput (so) cannot be None for lifting") x_lifted = self.lift(x_pool=x, so=so) return x_lifted # Select so = self.select(x=x, edge_index=adj, edge_weight=edge_weight, batch=batch) loss = self.compute_loss(so.scores, adj, edge_weight, batch) # Reduce x_pooled, batch_pooled = self.reduce(x=x, so=so, batch=batch) # Connect (it is always based on the full assignment) if not self.assign_all_nodes: full_so = so.assign_all_nodes( adj=adj, weight=None, max_iter=self.max_iter, batch=batch, closest_node_assignment=True, ) else: full_so = so edge_index_pooled, edge_weight_pooled = self.connect( edge_index=adj, so=full_so, edge_weight=edge_weight, batch_pooled=batch_pooled, ) return PoolingOutput( x=x_pooled, edge_index=edge_index_pooled, edge_weight=edge_weight_pooled, batch=batch_pooled, so=so, loss=loss, )
def compute_loss( self, scores: Tensor, adj: Adj, edge_weight: Optional[Tensor] = None, batch: Optional[Tensor] = None, ) -> dict: """Compute the auxiliary MaxCut loss. Args: scores (~torch.Tensor): Node scores computed by the MaxCut selector. adj (~torch_geometric.typing.Adj): Graph connectivity. Can be edge_index tensor of shape :math:`(2, E)` or SparseTensor. edge_weight (~torch.Tensor, optional): Edge weights of shape :math:`(E,)`. (default: :obj:`None`) batch (~torch.Tensor, optional): Batch assignments of shape :math:`(N,)`. (default: :obj:`None`) Returns: dict: A dictionary with the MaxCut loss term. """ edge_index, edge_weight = connectivity_to_edge_index(adj, edge_weight) # Compute MaxCut loss maxcut_loss_val = maxcut_loss( scores=scores, edge_index=edge_index, edge_weight=edge_weight, batch=batch, batch_reduction="mean", ) return {"maxcut_loss": maxcut_loss_val * self.loss_coeff} @property def has_loss(self) -> bool: """Returns True if this pooler computes auxiliary losses.""" return True def extra_repr_args(self) -> dict: """Additional representation arguments for debugging.""" return { "loss_coeff": self.loss_coeff, }