Source code for tgp.reduce.eigenpool_reduce

from typing import Optional, Tuple

import torch
from torch import Tensor
from torch_geometric.utils import unbatch

from tgp.reduce.base_reduce import Reduce
from tgp.select import SelectOutput
from tgp.utils.ops import is_multi_graph_batch
from tgp.utils.typing import ReduceType


[docs] class EigenPoolReduce(Reduce): r"""The :math:`\texttt{reduce}` operator for EigenPooling. It uses the pooling matrix :math:`\boldsymbol{\Theta}` computed by :class:`~tgp.select.EigenPoolSelect` and stored in ``so.theta``. For each graph: .. math:: \mathbf{X}_{\text{pool,raw}} = \boldsymbol{\Theta}^{\top}\mathbf{X}, then :math:`\mathbf{X}_{\text{pool,raw}}` is reshaped from mode-major layout :math:`[H \cdot K, F]` to :math:`[K, H \cdot F]`. ``return_batched`` is only used when returning multi-graph results (stack vs concatenate). EigenPool uses 2D inputs with a batch vector; there is no separate dense path. Args: num_modes (int, optional): Number of eigenvector modes :math:`H`. Kept for API symmetry with the EigenPool components. (default: ``5``) reduce_op (~tgp.utils.typing.ReduceType, optional): Kept for API compatibility with :class:`~tgp.reduce.Reduce`. (default: ``"sum"``) """ def __init__( self, num_modes: int = 5, reduce_op: ReduceType = "sum", ): super().__init__() self.num_modes = num_modes self.reduce_op = reduce_op @staticmethod def _pool_with_theta(theta: Tensor, x: Tensor) -> Tensor: if theta.is_sparse: return torch.sparse.mm(theta.t(), x) return theta.t().matmul(x) @staticmethod def _reshape_mode_major_to_feature_blocks( x_pool_raw: Tensor, num_clusters: int, ) -> Tensor: r"""Reshape pooled features from mode-major :math:`[H\cdot K, F]` to cluster-major feature blocks :math:`[K, H\cdot F]`. """ num_modes = x_pool_raw.size(0) // num_clusters feat_dim = x_pool_raw.size(-1) return ( x_pool_raw.view(num_modes, num_clusters, feat_dim) .permute(1, 0, 2) .reshape(num_clusters, num_modes * feat_dim) )
[docs] def forward( self, x: Tensor, so: SelectOutput, *, batch: Optional[Tensor] = None, edge_index: Optional[Tensor] = None, edge_weight: Optional[Tensor] = None, return_batched: bool = False, **kwargs, ) -> Tuple[Tensor, Optional[Tensor]]: r"""Forward pass. Args: x (~torch.Tensor): Node feature matrix :math:`\mathbf{X}` of shape :math:`[N, F]`. so (~tgp.select.SelectOutput): Output of the :math:`\texttt{select}` operator with dense assignment matrix ``so.s`` and pooling matrix ``so.theta``. batch (~torch.Tensor, optional): Batch vector for sparse multi-graph inputs. If :obj:`None`, this method uses ``so.batch`` when available. (default: :obj:`None`) edge_index (~torch.Tensor, optional): Unused by EigenPooling. (default: :obj:`None`) edge_weight (~torch.Tensor, optional): Unused by EigenPooling. (default: :obj:`None`) return_batched (bool, optional): Only used for multi-graph batches. If :obj:`True`, returns :math:`[B, K, H \cdot F]`; otherwise concatenated :math:`[B \cdot K, H \cdot F]`. For single graphs, if :obj:`True` returns :math:`[1, K, H \cdot F]`. (default: :obj:`False`) Returns: (~torch.Tensor, ~torch.Tensor or :obj:`None`): Pooled features and pooled batch vector. """ if batch is None and so.batch is not None: batch = so.batch num_clusters = so.s.size(-1) theta = so.theta is_multi_graph = is_multi_graph_batch(batch) # Single graph case: directly pool with theta and reshape if not is_multi_graph: x_pool = self._pool_with_theta(theta, x) x_pool = self._reshape_mode_major_to_feature_blocks(x_pool, num_clusters) batch_pool = super().reduce_batch(so, batch) if return_batched: x_pool = x_pool.unsqueeze(0) return x_pool, batch_pool # Multi-graph batch case: unbatch theta and x, pool each graph separately, then concatenate results. theta_list = theta if isinstance(theta, list) else unbatch(theta, batch=batch) x_list = unbatch(x, batch=batch) pooled_features = [] for theta_b, x_b in zip(theta_list, x_list): x_pool_b = self._pool_with_theta(theta_b, x_b) pooled_features.append( self._reshape_mode_major_to_feature_blocks(x_pool_b, num_clusters) ) x_pool = torch.cat(pooled_features, dim=0) batch_pool = super().reduce_batch(so, batch) if return_batched: x_pool = x_pool.view(len(theta_list), num_clusters, -1) return x_pool, batch_pool
def __repr__(self) -> str: return f"{self.__class__.__name__}(num_modes={self.num_modes})"