Source code for tgp.select.kmis_select

from typing import Optional

import torch
from torch_geometric.nn.dense import Linear
from torch_geometric.typing import Adj, OptTensor, PairTensor, Tensor
from torch_geometric.utils import scatter, to_undirected
from torch_geometric.utils.num_nodes import maybe_num_nodes

from tgp.imports import HAS_TORCH_SCATTER

if HAS_TORCH_SCATTER:
    from torch_scatter import scatter_add, scatter_max, scatter_min
from tgp.select import Select, SelectOutput
from tgp.utils import (
    connectivity_to_edge_index,
    weighted_degree,
)
from tgp.utils.typing import SinvType


[docs] def degree_scorer( edge_index: Adj, edge_weight: Optional[Tensor] = None, num_nodes: Optional[int] = None, dim: int = 1, ): num_nodes = maybe_num_nodes(edge_index, num_nodes) edge_index, edge_weight = connectivity_to_edge_index(edge_index, edge_weight) neigh = edge_index[dim] deg = weighted_degree(neigh, edge_weight, num_nodes) return deg.float()
def maximal_independent_set( edge_index: Tensor, order_k: int = 1, perm: OptTensor = None, num_nodes: Optional[int] = None, ) -> Tensor: r"""Returns a Maximal :math:`k`-Independent Set of a graph, i.e., a set of nodes (as a :class:`ByteTensor`) such that none of them are :math:`k`-hop neighbors, and any node in the graph has a :math:`k`-hop neighbor in the returned set. The algorithm greedily selects the nodes in their canonical order. If a permutation ``perm`` is provided, the nodes are extracted following that permutation instead. This method follows `Blelloch's Alogirithm <https://arxiv.org/abs/1202.3205>`_ for :math:`k = 1`, and its generalization by `Bacciu et al. <https://arxiv.org/abs/2208.03523>`_ for higher values of :math:`k`. Args: edge_index (Tensor of shape [2, E]): The graph connectivity. order_k (int): The :math:`k`-th order (defaults to 1). perm (LongTensor, optional): Permutation vector. Must be of size :obj:`(n,)` (defaults to :obj:`None`). num_nodes (int, optional): The number of nodes (defaults to :obj:`None`). :rtype: :class:`ByteTensor` """ n = num_nodes if num_nodes is not None else maybe_num_nodes(edge_index) row, col = edge_index[0], edge_index[1] device = row.device if perm is None: rank = torch.arange(n, dtype=torch.long, device=device) else: rank = torch.zeros_like(perm) rank[perm] = torch.arange(n, dtype=torch.long, device=device) mis = torch.zeros(n, dtype=torch.bool, device=device) mask = mis.clone() min_rank = rank.clone() while not mask.all(): for _ in range(order_k): if HAS_TORCH_SCATTER: min_neigh = torch.full_like(min_rank, fill_value=n) scatter_min(min_rank[row], col, out=min_neigh) torch.minimum(min_neigh, min_rank, out=min_rank) # self-loops else: min_scatter = scatter( src=min_rank[row], index=col, dim=0, dim_size=n, reduce="min" ) # Compute a count for each node to detect which indices received no update: counts = scatter( src=torch.ones_like(min_rank[row]), index=col, dim=0, dim_size=n, reduce="sum", ) # For indices with no incoming message, assign the identity value (n): min_scatter[counts == 0] = n min_rank = torch.minimum(min_scatter, min_rank) # self-loops mis = mis | torch.eq(rank, min_rank) mask = mis.clone().byte() for _ in range(order_k): if HAS_TORCH_SCATTER: max_neigh = torch.full_like(mask, fill_value=0) scatter_max(mask[row], col, out=max_neigh) torch.maximum(max_neigh, mask, out=mask) # self-loops else: mask_int = mask.long() max_scatter = scatter( src=mask_int[row], index=col, dim=0, dim_size=n, reduce="max" ) mask_int = torch.maximum(mask_int, max_scatter) # self-loops mask = mask_int.bool() mask = mask.to(dtype=torch.bool) min_rank = rank.clone() min_rank[mask] = n return mis def maximal_independent_set_cluster( edge_index: Tensor, order_k: int = 1, perm: OptTensor = None, num_nodes: Optional[int] = None, ) -> PairTensor: r"""Computes the Maximal :math:`k`-Independent Set (:math:`k`-MIS) clustering of a graph, as defined in `"Generalizing Downsampling from Regular Data to Graphs" <https://arxiv.org/abs/2208.03523>`_. The algorithm greedily selects the nodes in their canonical order. If a permutation ``perm`` is provided, the nodes are extracted following that permutation instead. This method returns both the :math:`k`-MIS and the clustering, where the :math:`c`-th cluster refers to the :math:`c`-th element of the :math:`k`-MIS. Args: edge_index (Tensor of shape [2, E]): The graph connectivity. order_k (int): The :math:`k`-th order (defaults to 1). perm (LongTensor, optional): Permutation vector. Must be of size :obj:`(n,)` (defaults to :obj:`None`). num_nodes (int, optional): The number of nodes (defaults to :obj:`None`). :rtype: (:class:`ByteTensor`, :class:`LongTensor`) """ mis = maximal_independent_set( edge_index=edge_index, order_k=order_k, perm=perm, num_nodes=num_nodes ) n, device = mis.size(0), mis.device row, col = edge_index[0], edge_index[1] if perm is None: rank = torch.arange(n, dtype=torch.long, device=device) else: rank = torch.zeros_like(perm) rank[perm] = torch.arange(n, dtype=torch.long, device=device) min_rank = torch.full((n,), fill_value=n, dtype=torch.long, device=device) rank_mis = rank[mis] min_rank[mis] = rank_mis for _ in range(order_k): min_neigh = torch.full_like(min_rank, fill_value=n) scatter_min(min_rank[row], col, out=min_neigh) torch.minimum(min_neigh, min_rank, out=min_rank) _, clusters = torch.unique(min_rank, return_inverse=True) perm = torch.argsort(rank_mis) return mis, perm[clusters]
[docs] class KMISSelect(Select): r"""Computes the node assignments following the Maximal :math:`k`-Independent Set (:math:`k`-MIS) algorithm, as defined in the paper `"Generalizing Downsampling from Regular Data to Graphs" <https://arxiv.org/abs/2208.03523>`_ (Bacciu et al., AAAI 2023). To compute the :math:`k`-MIS, the algorithm greedily selects the nodes in their canonical order. If a permutation ``perm`` is provided, the nodes are extracted following that permutation instead. Args: in_channels (int, optional): Size of each input sample. Ignored if ``scorer`` is not ``"linear"``. (default: :obj:`None`) order_k (int): The :math:`k`-th order for the independent set. (default: ``1``) scorer (str): A function that computes a score for each node. Nodes with higher score have a higher chance of being selected for pooling. It can be one of: - ``"linear"`` (default): Uses a sigmoid-activated linear layer to compute the scores. ``in_channels`` must be set when using this option. - ``"random"``: Assigns a random score in :math:`[0, 1]` to each node. - ``"constant"``: Assigns a constant score of :math:`1` to each node. - ``"canonical"``: Assigns the score :math:`-i` to the :math:`i`-th node. - ``"degree"``: Uses the degree of each node as the score. score_heuristic (str, optional): Heuristic to increase the total score of selected nodes. Given an initial score vector :math:`\mathbf{s} \in \mathbb{R}^n`, options include: - :obj:`None`: No heuristic applied. - ``"greedy"`` (default): Computes the updated score :math:`\mathbf{s}'` as .. math:: \mathbf{s}' = \mathbf{s} \oslash (\mathbf{A} + \mathbf{I})^k \mathbf{1} where :math:`\oslash` is element-wise division. - ``"w-greedy"``: Computes the updated score :math:`\mathbf{s}'` as .. math:: \mathbf{s}' = \mathbf{s} \oslash (\mathbf{A} + \mathbf{I})^k \mathbf{s} force_undirected (bool, optional): Whether to force the input graph to be undirected. (default: :obj:`False`) 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}`. """ _heuristics = {None, "greedy", "w-greedy"} _scorers = {"linear", "degree", "random", "constant", "canonical"} def __init__( self, in_channels: Optional[int] = None, order_k: int = 1, scorer: str = "linear", score_heuristic: Optional[str] = "greedy", force_undirected: bool = False, s_inv_op: SinvType = "transpose", ): super(KMISSelect, self).__init__() assert score_heuristic in self._heuristics, ( f"Unrecognized `score_heuristic` value: {score_heuristic}" ) assert scorer in self._scorers, f"Unrecognized `scorer` value: {scorer}" self.order_k = order_k self.scorer = scorer self.score_heuristic = score_heuristic self.force_undirected = force_undirected self.s_inv_op = s_inv_op if scorer == "linear": if isinstance(in_channels, list): in_channels = in_channels[0] self.lin = Linear( in_channels=in_channels, out_channels=1, weight_initializer="uniform" ) def _apply_heuristic(self, x: Tensor, edge_index: Tensor) -> Tensor: if self.score_heuristic is None: return x row, col = edge_index[0], edge_index[1] x = x.view(-1) k_sums = torch.ones_like(x) if self.score_heuristic == "greedy" else x.clone() if HAS_TORCH_SCATTER: for _ in range(self.order_k): scatter_add(k_sums[row], col, out=k_sums) else: for _ in range(self.order_k): k_sums += scatter( k_sums[row], col, dim=0, dim_size=k_sums.size(0), reduce="add" ) return x / k_sums def _scorer( self, edge_index: Adj, edge_weight: Optional[Tensor] = None, x: Optional[Tensor] = None, num_nodes: Optional[int] = None, ) -> Tensor: device = edge_index.device if self.scorer == "linear": assert x is not None, "x must be provided when scorer is 'linear'" return self.lin(x).sigmoid() if self.scorer == "random": return torch.rand((num_nodes, 1), device=device) if self.scorer == "constant": return torch.ones((num_nodes, 1), device=device) if self.scorer == "canonical": return -torch.arange(num_nodes, device=device).view(-1, 1) if self.scorer == "degree": return degree_scorer( edge_index=edge_index, edge_weight=edge_weight, num_nodes=num_nodes ) raise ValueError(f"Unrecognized `scorer` value: {self.scorer}")
[docs] def forward( self, *, edge_index: Adj, edge_weight: Optional[Tensor] = None, x: Optional[Tensor] = None, batch: Optional[Tensor] = None, num_nodes: Optional[int] = None, **kwargs, ) -> SelectOutput: r"""Forward pass. Args: edge_index (~torch_geometric.typing.Adj): 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. edge_weight (~torch.Tensor, optional): A vector of shape :math:`[E]` or :math:`[E, 1]` containing the weights of the edges. (default: :obj:`None`) x (~torch.Tensor, optional): 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. (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`) num_nodes (int, optional): The total number of nodes of the graphs in the batch. (default: :obj:`None`) Returns: :class:`~tgp.select.SelectOutput`: The output of the :math:`\texttt{select}` operator. """ size_x = x.size(0) if x is not None else None num_nodes = ( num_nodes if num_nodes is not None else maybe_num_nodes(edge_index, size_x) ) edge_index, edge_weight = connectivity_to_edge_index(edge_index, edge_weight) if self.force_undirected: edge_index, edge_weight = to_undirected( edge_index, edge_weight, num_nodes, reduce="max" ) score = self._scorer(edge_index, edge_weight, x, num_nodes=num_nodes) updated_score = self._apply_heuristic(score, edge_index) perm = torch.argsort(updated_score.view(-1), 0, descending=True) mis, cluster = maximal_independent_set_cluster( edge_index, self.order_k, perm, num_nodes=num_nodes ) mis = mis.nonzero().view(-1) so = SelectOutput( cluster_index=cluster, num_nodes=num_nodes, num_supernodes=mis.size(0), weight=score.view(-1), s_inv_op=self.s_inv_op, mis=mis, ) return so
def __repr__(self) -> str: return ( f"{self.__class__.__name__}(" f"order_k={self.order_k}, " f"scorer={self.scorer}, " f"score_heuristic={self.score_heuristic}, " f"force_undirected={self.force_undirected}, " f"s_inv_op={self.s_inv_op})" )