Source code for tgp.poolers.eigenpool

import warnings
from typing import Optional, Union

from torch import Tensor
from torch_geometric.typing import Adj

from tgp.connect import EigenPoolConnect
from tgp.lift import EigenPoolLift
from tgp.reduce import EigenPoolReduce
from tgp.select import EigenPoolSelect, SelectOutput
from tgp.src import BasePrecoarseningMixin, DenseSRCPooling, PoolingOutput
from tgp.utils.ops import build_pooled_batch
from tgp.utils.typing import LiftType, SinvType


[docs] class EigenPooling(BasePrecoarseningMixin, DenseSRCPooling): r"""The EigenPooling operator from `"Graph Convolutional Networks with EigenPooling" <https://arxiv.org/abs/1904.13107>`_ (Ma et al., KDD 2019). + The :math:`\texttt{select}` operator is implemented with :class:`~tgp.select.EigenPoolSelect`. + The :math:`\texttt{reduce}` operator is implemented with :class:`~tgp.reduce.EigenPoolReduce`. + The :math:`\texttt{connect}` operator is implemented with :class:`~tgp.connect.EigenPoolConnect`. + The :math:`\texttt{lift}` operator is implemented with :class:`~tgp.lift.EigenPoolLift`. Let: + :math:`\mathbf{X} \in \mathbb{R}^{N \times F}` be node features; + :math:`\mathbf{S} \in \{0,1\}^{N \times K}` be the hard assignment matrix produced by :class:`~tgp.select.EigenPoolSelect`; + :math:`\boldsymbol{\Omega} := \mathbf{S}` (same matrix, connectivity notation); + :math:`\mathbf{A}_{\text{ext}} \in \mathbb{R}^{N \times N}` be the input (possibly block-diagonal) adjacency used by the connector; + :math:`H` be the number of eigenvector modes. EigenPooling first partitions nodes into :math:`K` clusters via spectral clustering, then builds a multi-mode pooling matrix :math:`\boldsymbol{\Theta} \in \mathbb{R}^{N \times (K\cdot H)}` from Laplacian eigenvectors of each cluster-induced subgraph. Features are pooled as: .. math:: \mathbf{X}_{\text{pool,raw}} = \boldsymbol{\Theta}^{\top}\mathbf{X}, then reshaped from :math:`[H\!\cdot\!K, F]` to :math:`[K, H\!\cdot\!F]`. Connectivity is coarsened as: .. math:: \mathbf{A}_{\text{coar}} = \boldsymbol{\Omega}^{\top}\mathbf{A}_{\text{ext}}\boldsymbol{\Omega}. Notes: - This implementation supports sparse inputs and multi-graph batches via ``edge_index`` + ``batch``. - Dense padded batched inputs (:math:`[B, N, N]`) are **not** supported. Args: k (int): Number of clusters (supernodes) in the pooled graph. num_modes (int, optional): Number of eigenvector modes :math:`H`. (default: ``5``) normalized (bool, optional): If :obj:`True`, use the normalized Laplacian. (default: :obj:`True`) 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`) adj_transpose (bool, optional): Passed to the connector for adjacency post-processing. (default: :obj:`True`) lift (~tgp.utils.typing.LiftType, optional): Kept for API compatibility. EigenPooling always uses eigenvector-based lifting and ignores this option. (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"``) batched (bool, optional): Kept for API compatibility. Dense batched mode is unsupported and this option is ignored. Use sparse inputs with ``batch`` instead. (default: :obj:`False`) sparse_output (bool, optional): If :obj:`True`, return sparse pooled connectivity. (default: :obj:`False`) cache_preprocessing (bool, optional): Passed to :class:`~tgp.src.DenseSRCPooling`; has no practical effect for this sparse-oriented path. (default: :obj:`False`) """ def __init__( self, k: int, num_modes: int = 5, normalized: bool = True, cached: bool = False, remove_self_loops: bool = True, degree_norm: bool = True, edge_weight_norm: bool = False, adj_transpose: bool = True, lift: LiftType = "precomputed", s_inv_op: SinvType = "transpose", batched: bool = False, sparse_output: bool = False, cache_preprocessing: bool = False, ): if batched: warnings.warn( "EigenPooling does not support dense padded batched inputs. " "Use batched=False with a sparse edge_index and batch vector.", UserWarning, ) if lift != "precomputed": warnings.warn( "EigenPooling ignores the 'lift' argument and always uses " "eigenvector-based lifting.", UserWarning, ) # EigenPooling always uses unbatched mode # because spectral clustering operates on individual graphs super().__init__( selector=EigenPoolSelect( k=k, s_inv_op=s_inv_op, num_modes=num_modes, normalized=normalized, ), reducer=EigenPoolReduce( num_modes=num_modes, ), lifter=EigenPoolLift( num_modes=num_modes, ), connector=EigenPoolConnect( remove_self_loops=remove_self_loops, degree_norm=degree_norm, adj_transpose=adj_transpose, edge_weight_norm=edge_weight_norm, sparse_output=sparse_output, ), cached=cached, cache_preprocessing=cache_preprocessing, adj_transpose=adj_transpose, batched=False, # Always use unbatched mode sparse_output=sparse_output, ) self.k = k self.num_modes = num_modes self.normalized = normalized self.cached = cached # Connector for precoarsening (always sparse output) self.preconnector = EigenPoolConnect( remove_self_loops=remove_self_loops, degree_norm=degree_norm, edge_weight_norm=edge_weight_norm, sparse_output=True, )
[docs] def forward( self, x: Tensor, adj: Optional[Adj] = None, edge_weight: Optional[Tensor] = None, so: Optional[SelectOutput] = None, mask: Optional[Tensor] = None, batch: Optional[Tensor] = None, batch_pooled: Optional[Tensor] = None, lifting: bool = False, **kwargs, ) -> Union[PoolingOutput, Tensor]: r"""Forward pass. Args: x (~torch.Tensor): Node features :math:`\mathbf{X} \in \mathbb{R}^{N \times F}`. During lifting, accepts pooled features :math:`\mathbf{X}_{\text{pool}} \in \mathbb{R}^{K \times (H\cdot F)}`. adj (~torch_geometric.typing.Adj, optional): Sparse graph connectivity (edge index, :class:`~torch_sparse.SparseTensor`, or torch COO tensor). Internally interpreted as :math:`\mathbf{A}_{\text{ext}}`; required when ``lifting=False``. (default: :obj:`None`) edge_weight (~torch.Tensor, optional): Edge weights associated with ``adj``. (default: :obj:`None`) so (~tgp.select.SelectOutput, optional): Pre-computed selection output. (default: :obj:`None`) mask (~torch.Tensor, optional): Unused input-node validity mask. (default: :obj:`None`) batch (~torch.Tensor, optional): Batch vector for sparse multi-graph inputs. (default: :obj:`None`) batch_pooled (~torch.Tensor, optional): Batch vector for pooled nodes, used during lifting. (default: :obj:`None`) lifting (bool, optional): If :obj:`True`, apply :math:`\texttt{lift}` instead of pooling. (default: :obj:`False`) Returns: ~tgp.src.PoolingOutput or ~torch.Tensor: Pooled output if ``lifting=False``, otherwise lifted features. """ if lifting: # Lift expects 2D x_pool [N, F]; flatten if we got dense batched [B, K, F] x_pool = x if x.dim() == 3: B, K, F = x.shape x_pool = x.view(-1, F) if batch_pooled is None: batch_pooled = build_pooled_batch(B, K, x.device) return self.lift( x_pool=x_pool, so=so, batch=batch, batch_pooled=batch_pooled, ) # Select (if not precomputed) if so is None: so = self.select(edge_index=adj, edge_weight=edge_weight, batch=batch) # Reduce x_pooled, pooled_batch = self.reduce(x=x, so=so, batch=batch) # Connect adj_pooled, edge_weight_pooled = self.connect( edge_index=adj, so=so, edge_weight=edge_weight, batch=batch, batch_pooled=pooled_batch, ) # When dense output and multiple graphs: reshape x_pooled to [B, K, F] if ( not self.sparse_output and pooled_batch is not None and pooled_batch.numel() > 0 ): batch_size = int(pooled_batch.max().item()) + 1 num_clusters = so.s.size(-1) x_pooled = x_pooled.view(batch_size, num_clusters, -1) return PoolingOutput( x=x_pooled, edge_index=adj_pooled, edge_weight=edge_weight_pooled, batch=pooled_batch, so=so, )
[docs] def precoarsening( self, edge_index: Optional[Adj] = None, edge_weight: Optional[Tensor] = None, *, batch: Optional[Tensor] = None, num_nodes: Optional[int] = None, **kwargs, ) -> PoolingOutput: """Precompute pooling outputs with a fixed assignment width ``k``.""" # In pre-coarsening, fix the assignment width to k across samples so # batched collation can concatenate dense SelectOutput.s safely. return super().precoarsening( edge_index=edge_index, edge_weight=edge_weight, batch=batch, num_nodes=num_nodes, fixed_k=True, **kwargs, )
def extra_repr_args(self) -> dict: return { "batched": self.batched, "k": self.k, "num_modes": self.num_modes, "normalized": self.normalized, "cached": self.cached, }