Source code for tgp.reduce.base_reduce

from typing import Optional, Tuple

import torch
from torch import Tensor, nn
from torch_geometric.utils import scatter, unbatch

from tgp.select import SelectOutput
from tgp.utils.ops import build_pooled_batch, is_multi_graph_batch


[docs] class Reduce(nn.Module): r"""A template class for implementing the :math:`\texttt{reduce}` operator."""
[docs] @staticmethod def reduce_batch( select_output: SelectOutput, batch: Optional[Tensor], ) -> Optional[Tensor]: r"""Computes the batch vector of the coarsened graph. Args: select_output (~tgp.select.SelectOutput): The output of :class:`~tgp.select.Select`. 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`) Returns: :class:`~torch.Tensor` or :obj:`None`: The pooled batch. """ if batch is None: return None # Sparse assignment: each selected supernode inherits the graph id of # the node that maps to it. if select_output.s.is_sparse: out = torch.arange(select_output.num_supernodes, device=batch.device) return out.scatter_( 0, select_output.cluster_index, batch[select_output.node_index] ) # Dense assignment: each graph contributes exactly K pooled nodes. if batch.numel() == 0: return batch.new_empty((0,), dtype=batch.dtype) batch_size = int(batch.max().item()) + 1 return build_pooled_batch( batch_size, select_output.num_supernodes, batch.device, dtype=batch.dtype, )
def reset_parameters(self): r"""Resets all learnable parameters of the module.""" pass
[docs] def forward( self, x: Tensor, so: SelectOutput, *, batch: Optional[Tensor] = None, **kwargs, ) -> Tuple[Tensor, Optional[Tensor]]: r"""Forward pass. Args: x (~torch.Tensor): The node feature matrix. For a sparse pooler, ``x`` has shape :math:`[N, F]`, where :math:`N` is the number of nodes in the batch and :math:`F` is the number of node features. For a dense pooler, ``x`` has shape :math:`[B, N, F]`, where :math:`B` is the batch size. so (~tgp.select.SelectOutput): The output of the :math:`\texttt{select}` operator. 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`) Returns: ~torch.Tensor: The pooled features :math:`\mathbf{X}_{pool}` of the supernodes. """ raise NotImplementedError
def __repr__(self) -> str: return f"{self.__class__.__name__}()"
[docs] class BaseReduce(Reduce): r"""The basic :math:`\texttt{reduce}` operator that computes :math:`\mathbf{S}^\top \mathbf{X}`. For **sparse** assignment :math:`\mathbf{S}`, this is implemented as a sum over nodes in each cluster (with optional weighting by ``so.s.values()``). For **dense** assignment, it is a matrix multiply :math:`\mathbf{S}^\top \mathbf{X}`. For dense multi-graph batches (dense :math:`[N, K]` with a batch vector), each graph is processed separately (unbatch then per-graph matmul) for memory efficiency when using unbatched dense poolers (``batched=False``). For dense unbatched assignments :math:`[N, K]` with multi-graph batches, ``return_batched=True`` returns :math:`[B, K, F]`; otherwise :math:`[B \cdot K, F]`. For dense batched assignments :math:`[B, N, K]`, output is always :math:`[B, K, F]`. """ def __init__(self): super().__init__()
[docs] def forward( self, x: Tensor, so: SelectOutput, *, batch: Optional[Tensor] = None, return_batched: bool = False, **kwargs, ) -> Tuple[Tensor, Optional[Tensor]]: r"""Forward pass computing :math:`\mathbf{S}^\top \mathbf{X}`. Args: x (~torch.Tensor): The node feature matrix. For a sparse pooler, ``x`` has shape :math:`[N, F]`, where :math:`N` is the number of nodes in the batch and :math:`F` is the number of node features. For a dense pooler, ``x`` has shape :math:`[B, N, F]`, where :math:`B` is the batch size. so (~tgp.select.SelectOutput): The output of the :math:`\texttt{select}` operator. 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`) return_batched (bool, optional): For dense unbatched :math:`[N, K]` assignments with multi-graph batches, controls output shape: :obj:`True` gives :math:`[B, K, F]`, :obj:`False` gives :math:`[B \cdot K, F]`. For single-graph :math:`[N, K]`, :obj:`True` wraps output as :math:`[1, K, F]`. (default: :obj:`False`) """ if batch is None and so.batch is not None: batch = so.batch # Path 1: sparse assignment matrix (edge list style). Aggregate selected # node features into supernodes via scatter. if so.s.is_sparse: if return_batched: raise ValueError( "return_batched=True is only supported for dense assignment matrices." ) src = x[so.node_index] * so.weight.view(-1, 1) x_pool = scatter( src, so.cluster_index, dim=0, dim_size=so.num_supernodes, reduce="sum", ) batch_pool = self.reduce_batch(so, batch) return x_pool, batch_pool # Path 2: dense batched assignment [B, N, K] and dense features [B, N, F]. if so.s.dim() == 3: x_pool = so.s.transpose(-2, -1).matmul(x) # [B, K, F] batch_pool = self.reduce_batch(so, batch) return x_pool, batch_pool if so.s.dim() != 2: raise ValueError( "Dense SelectOutput.s must be 2D [N, K] or 3D [B, N, K], " f"got ndim={so.s.dim()}." ) # Path 3: dense unbatched assignment [N, K] with multi-graph batch. if is_multi_graph_batch(batch): unbatched_s = unbatch(so.s, batch) # list of [N_i, K] unbatched_x = unbatch(x, batch) # list of [N_i, F] x_pool_per_graph = [ s_i.t().matmul(x_i) for s_i, x_i in zip(unbatched_s, unbatched_x) ] x_pool = ( torch.stack(x_pool_per_graph, dim=0) if return_batched else torch.cat(x_pool_per_graph, dim=0) ) batch_pool = self.reduce_batch(so, batch) return x_pool, batch_pool # Path 4: dense unbatched assignment [N, K] for a single graph. x_pool = so.s.transpose(-2, -1).matmul(x) # [K, F] if return_batched: x_pool = x_pool.unsqueeze(0) # [1, K, F] batch_pool = self.reduce_batch(so, batch) return x_pool, batch_pool
def __repr__(self) -> str: return f"{self.__class__.__name__}()"