Source code for tgp.poolers.asap

import inspect
from typing import Callable, Optional, Union

import torch
import torch.nn.functional as F
from torch import Tensor
from torch.nn import Linear
from torch_geometric.nn.conv import LEConv
from torch_geometric.typing import Adj
from torch_geometric.utils import scatter, softmax

from tgp.connect import SparseConnect
from tgp.lift import BaseLift
from tgp.reduce import BaseReduce
from tgp.select import SelectOutput, TopkSelect
from tgp.src import PoolingOutput, SRCPooling
from tgp.utils import add_remaining_self_loops, connectivity_to_edge_index
from tgp.utils.typing import LiftType, ReduceType, SinvType


[docs] class ASAPooling(SRCPooling): r"""The Adaptive Structure Aware Pooling operator from the paper `"ASAP: Adaptive Structure Aware Pooling for Learning Hierarchical Graph Representations" <https://arxiv.org/abs/1911.07979>`_ (Ranjan et al., AAAI 2020). + The :math:`\texttt{select}` operator is implemented by passing a special score to :class:`~tgp.select.TopkSelect`. + 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`. Args: in_channels (int): Size of each input sample. ratio (float or int): Graph pooling ratio, which is used to compute :math:`k = \lceil \mathrm{ratio} \cdot N \rceil`, or the value of :math:`k` itself, depending on whether the type of ``ratio`` is :obj:`float` or :obj:`int`. (default: ``0.5``) GNN (~torch.nn.Module, optional): A graph neural network layer for using intra-cluster properties. Especially helpful for graphs with higher degree of neighborhood (one of :class:`~torch_geometric.nn.conv.GraphConv`, :class:`~torch_geometric.nn.conv.GCNConv` or any GNN which supports the ``edge_weight`` parameter). (default: :obj:`None`) dropout (float, optional): Dropout probability of the normalized attention coefficients which exposes each node to a stochastically sampled neighborhood during training. (default: ``0``) negative_slope (float, optional): LeakyReLU angle of the negative slope. (default: ``0.2``) nonlinearity (str or callable, optional): The non-linearity to use when computing the score. (default: ``"tanh"``) 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:`False`) **kwargs (optional): Additional parameters for initializing the graph neural network layer. """ def __init__( self, in_channels: int, ratio: Union[float, int] = 0.5, GNN: Optional[torch.nn.Module] = None, dropout: float = 0.0, negative_slope: float = 0.2, add_self_loops: bool = False, nonlinearity: Union[str, Callable] = "sigmoid", lift: LiftType = "precomputed", s_inv_op: SinvType = "transpose", connect_red_op: ReduceType = "sum", lift_red_op: ReduceType = "sum", remove_self_loops: bool = True, degree_norm: bool = False, edge_weight_norm: bool = False, **kwargs, ): if remove_self_loops and add_self_loops: raise ValueError("remove_self_loops and add_self_loops cannot be both True") super().__init__( selector=TopkSelect(ratio=ratio, act=nonlinearity, s_inv_op=s_inv_op), reducer=BaseReduce(), lifter=BaseLift(matrix_op=lift, reduce_op=lift_red_op), connector=SparseConnect( remove_self_loops=remove_self_loops, reduce_op=connect_red_op, degree_norm=degree_norm, edge_weight_norm=edge_weight_norm, ), ) self.in_channels = in_channels self.ratio = ratio self.negative_slope = negative_slope self.dropout = dropout self.GNN = GNN self.select_scorer = LEConv(in_channels, 1) self.add_self_loops = add_self_loops self.lin = Linear(in_channels, in_channels) self.att = Linear(2 * in_channels, 1) if self.GNN is not None: # keep only the kwargs that are used in the GNN (signature works when __code__ is not available) try: _params = set(inspect.signature(GNN).parameters.keys()) except (ValueError, TypeError): _params = set() kwargs = {k: v for k, v in kwargs.items() if k in _params} self.gnn_intra_cluster = GNN(self.in_channels, self.in_channels, **kwargs) else: self.gnn_intra_cluster = None def reset_parameters(self): r"""Resets all learnable parameters of the module.""" self.lin.reset_parameters() self.att.reset_parameters() if self.gnn_intra_cluster is not None: self.gnn_intra_cluster.reset_parameters() super().reset_parameters()
[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"""The forward pass of the pooling operator. 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 :class:`~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: PoolingOutput: The output of the pooling operator. """ if lifting: # Lift x_lifted = self.lift(x_pool=x, so=so) return x_lifted else: N = x.size(0) x = x.unsqueeze(-1) if x.dim() == 1 else x # Convert to edge_index if needed edge_index, edge_weight = connectivity_to_edge_index(adj, edge_weight) edge_index, edge_weight = add_remaining_self_loops( edge_index, edge_weight, fill_value=1.0, num_nodes=N ) x_pool = x if self.gnn_intra_cluster is not None: x_pool = self.gnn_intra_cluster( x=x, edge_index=edge_index, edge_weight=edge_weight ) if batch is None: batch = edge_index.new_zeros(x.size(0)) x_pool_j = x_pool[edge_index[0]] x_q = scatter(x_pool_j, edge_index[1], dim=0, reduce="max") x_q = self.lin(x_q)[edge_index[1]] score = self.att(torch.cat([x_q, x_pool_j], dim=-1)).view(-1) score = F.leaky_relu(score, self.negative_slope) score = softmax(score, edge_index[1], num_nodes=N) # Sample attention coefficients stochastically. score = F.dropout(score, p=self.dropout, training=self.training) v_j = x[edge_index[0]] * score.view(-1, 1) x = scatter(v_j, edge_index[1], dim=0, reduce="sum") score = self.select_scorer( x, edge_index=edge_index, edge_weight=edge_weight ) # Select so = self.select(x=score, batch=batch) # Reduce x, batch_pooled = self.reduce(x=x, so=so, batch=batch) # Connect edge_index_pooled, pooled_edge_weight = self.connect( edge_index=edge_index, so=so, edge_weight=edge_weight, batch_pooled=batch_pooled, ) out = PoolingOutput( x=x, edge_index=edge_index_pooled, edge_weight=pooled_edge_weight, batch=batch_pooled, so=so, ) return out
def extra_repr_args(self) -> dict: return { "ratio": self.ratio, "GNN": self.GNN.__class__.__name__ if self.GNN is not None else "None", "add_self_loops": self.add_self_loops, }