Source code for tgp.poolers.sep

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 SelectOutput, SEPSelect
from tgp.src import BasePrecoarseningMixin, PoolingOutput, SRCPooling
from tgp.utils.typing import ConnectionType, LiftType, ReduceType, SinvType


[docs] class SEPPooling(BasePrecoarseningMixin, SRCPooling): r"""The SEPPooling operator from the paper `"Structural Entropy Guided Graph Hierarchical Pooling" <https://proceedings.mlr.press/v162/wu22b/wu22b.pdf>`_ (Wu et al., ICML 2022). SEP performs graph pooling by optimizing cluster assignments globally with the goal of minimizing structural entropy. SEP internally builds a coding tree. In standard pooling mode (:meth:`forward`), only the first partition above the original nodes is exposed, i.e., node-to-depth-1 clusters. Note: A single call to :meth:`forward` only returns the finest pooled partition (the bottom non-leaf level of the SEP tree). This corresponds to using only a depth-2 tree view (nodes -> first supernodes -> root). To use deeper SEP hierarchies (depth > 2) as intended by the original method, use pre-coarsening via :meth:`~tgp.poolers.SEPPooling.multi_level_precoarsening` (or :class:`~tgp.data.transforms.PreCoarsening` with repeated ``"sep"`` levels). .. admonition:: Example Standard one-level forward (returns only depth-1 assignments): .. code-block:: python pool = SEPPooling() out = pool( x=x, adj=edge_index, edge_weight=edge_weight, batch=batch, ) # out.so maps original nodes -> first-level SEP clusters only. Multi-level SEP pre-coarsening (returns hierarchy levels): .. code-block:: python pool = SEPPooling() levels = pool.multi_level_precoarsening( levels=3, edge_index=edge_index, edge_weight=edge_weight, batch=batch, num_nodes=x.size(0), ) # levels[0].so: nodes -> level-1 # levels[1].so: level-1 -> level-2 # levels[2].so: level-2 -> level-3 Equivalent transform-level usage: .. code-block:: python from tgp.data.transforms import PreCoarsening transform = PreCoarsening(poolers=["sep", "sep", "sep"]) data = transform(data) # data.pooled_data contains 3 pooled levels in order. Args: cached (bool, optional): If :obj:`True`, cache :class:`~tgp.select.SelectOutput`. (default: :obj:`False`) remove_self_loops (bool, optional): Whether to remove self-loops after coarsening. (default: :obj:`True`) degree_norm (bool, optional): If :obj:`True`, symmetrically normalize pooled adjacency. (default: :obj:`True`) edge_weight_norm (bool, optional): Whether to normalize pooled edge weights. (default: :obj:`False`) lift (~tgp.utils.typing.LiftType, optional): Operation used by :class:`~tgp.lift.BaseLift` to compute :math:`\mathbf{S}_\text{inv}` during lifting. (default: ``"precomputed"``) s_inv_op (~tgp.utils.typing.SinvType, optional): Operation used to compute :math:`\mathbf{S}_\text{inv}` in :class:`~tgp.select.SelectOutput`. (default: ``"transpose"``) """ def __init__( self, lift: LiftType = "precomputed", s_inv_op: SinvType = "transpose", connect_red_op: ConnectionType = "sum", lift_red_op: ReduceType = "sum", cached: bool = False, remove_self_loops: bool = True, degree_norm: bool = True, edge_weight_norm: bool = False, ): super().__init__( selector=SEPSelect(s_inv_op=s_inv_op), reducer=BaseReduce(), lifter=BaseLift(matrix_op=lift, reduce_op=lift_red_op), connector=SparseConnect( reduce_op=connect_red_op, remove_self_loops=remove_self_loops, degree_norm=degree_norm, edge_weight_norm=edge_weight_norm, ), cached=cached, )
[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, ) -> Union[PoolingOutput, Tensor]: r"""Forward pass. Args: x (~torch.Tensor): The node feature matrix of shape :math:`[N, F]`, where :math:`N` is the number of nodes in the batch and :math:`F` is the number of node features. adj (~torch_geometric.typing.Adj, optional): The connectivity matrix. It can either be a ``torch_sparse.SparseTensor`` of (sparse) shape :math:`[N, N]`, where :math:`N` is the number of nodes in the batch or a :obj:`~torch.Tensor` of shape :math:`[2, E]`, where :math:`E` is the number of edges in the batch. If ``lifting`` is :obj:`False`, it cannot be :obj:`None`. (default: :obj:`None`) edge_weight (~torch.Tensor, optional): A vector of shape :math:`[E]` or :math:`[E, 1]` containing the weights of the edges. (default: :obj:`None`) so (~tgp.select.SelectOutput, optional): The output of the :math:`\texttt{select}` operator. (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. (default: :obj:`None`) lifting (bool, optional): If set to :obj:`True`, the :math:`\texttt{lift}` operation is performed. (default: :obj:`False`) Returns: ~tgp.src.PoolingOutput or ~torch.Tensor: Pooled output if ``lifting=False``, otherwise lifted features. """ if lifting: # Lift x_lifted = self.lift(x_pool=x, so=so) return x_lifted # Select (if not precomputed) if so is None: # Select so = self.select( edge_index=adj, edge_weight=edge_weight, batch=batch, num_nodes=x.size(0), ) # Reduce x_pooled, batch_pooled = self.reduce(x=x, so=so, batch=batch) # Connect edge_index_pooled, edge_weight_pooled = self.connect( edge_index=adj, so=so, edge_weight=edge_weight, batch_pooled=batch_pooled, ) out = PoolingOutput( x=x_pooled, edge_index=edge_index_pooled, edge_weight=edge_weight_pooled, batch=batch_pooled, so=so, ) return out
[docs] def multi_level_precoarsening( self, levels: int, edge_index: Optional[Adj] = None, edge_weight: Optional[Tensor] = None, *, batch: Optional[Tensor] = None, num_nodes: Optional[int] = None, **kwargs, ) -> list[PoolingOutput]: """Compute multiple SEP pre-coarsening levels from a single tree hierarchy.""" if levels < 1: raise ValueError(f"'levels' must be >= 1, got {levels}.") if edge_index is None: raise ValueError("edge_index cannot be None for pre-coarsening.") clear_cache = getattr(self, "clear_cache", None) if callable(clear_cache): clear_cache() if levels == 1: pooled_levels = [ self.precoarsening( edge_index=edge_index, edge_weight=edge_weight, batch=batch, num_nodes=num_nodes, **kwargs, ) ] if callable(clear_cache): clear_cache() return pooled_levels so_levels = self.selector.multi_level_select( edge_index=edge_index, edge_weight=edge_weight, batch=batch, num_nodes=num_nodes, levels=levels, **kwargs, ) if len(so_levels) != levels: raise RuntimeError( f"SEPSelect returned {len(so_levels)} levels, expected {levels}." ) pooled_levels = [] current_edge_index = edge_index current_edge_weight = edge_weight current_batch = batch current_num_nodes = num_nodes for so in so_levels: if current_num_nodes is not None and int(current_num_nodes) != int( so.num_nodes ): raise RuntimeError( "Inconsistent hierarchy sizes in multi-level SEP pre-coarsening: " f"expected {int(current_num_nodes)} nodes, got {int(so.num_nodes)}." ) pooled = self._precoarsening_from_select_output( so=so, edge_index=current_edge_index, edge_weight=current_edge_weight, batch=current_batch, **kwargs, ) pooled_levels.append(pooled) pooled_data = pooled.as_data() current_edge_index = pooled_data.edge_index current_edge_weight = pooled_data.edge_weight current_batch = pooled_data.batch current_num_nodes = pooled_data.num_nodes if callable(clear_cache): clear_cache() return pooled_levels
def extra_repr_args(self) -> dict: return {"cached": self.cached}