conv.py 3.14 KB
Newer Older
rusty1s's avatar
rusty1s committed
1
from typing import Optional
rusty1s's avatar
linting  
rusty1s committed
2

rusty1s's avatar
rusty1s committed
3
import torch
rusty1s's avatar
linting  
rusty1s committed
4

rusty1s's avatar
rusty1s committed
5
6
from .basis import spline_basis
from .weighting import spline_weighting
rusty1s's avatar
rusty1s committed
7
8


rusty1s's avatar
rusty1s committed
9
10
11
12
13
14
15
@torch.jit.script
def spline_conv(x: torch.Tensor, edge_index: torch.Tensor,
                pseudo: torch.Tensor, weight: torch.Tensor,
                kernel_size: torch.Tensor, is_open_spline: torch.Tensor,
                degree: int = 1, norm: bool = True,
                root_weight: Optional[torch.Tensor] = None,
                bias: Optional[torch.Tensor] = None) -> torch.Tensor:
AntoinePrv's avatar
AntoinePrv committed
16
    r"""Applies the spline-based convolution operator :math:`(f \star g)(i) =
rusty1s's avatar
rusty1s committed
17
18
    \frac{1}{|\mathcal{N}(i)|} \sum_{l=1}^{M_{in}} \sum_{j \in \mathcal{N}(i)}
    f_l(j) \cdot g_l(u(i, j))` over several node features of an input graph.
rusty1s's avatar
typo  
rusty1s committed
19
20
    The kernel function :math:`g_l` is defined over the weighted B-spline
    tensor product basis for a single input feature map :math:`l`.
rusty1s's avatar
rusty1s committed
21
22

    Args:
rusty1s's avatar
rusty1s committed
23
        x (:class:`Tensor`): Input node features of shape
rusty1s's avatar
rusty1s committed
24
25
26
27
28
29
30
31
32
33
34
35
            (number_of_nodes x in_channels).
        edge_index (:class:`LongTensor`): Graph edges, given by source and
            target indices, of shape (2 x number_of_edges) in the fixed
            interval [0, 1].
        pseudo (:class:`Tensor`): Edge attributes, ie. pseudo coordinates,
            of shape (number_of_edges x number_of_edge_attributes).
        weight (:class:`Tensor`): Trainable weight parameters of shape
            (kernel_size x in_channels x out_channels).
        kernel_size (:class:`LongTensor`): Number of trainable weight
            parameters in each edge dimension.
        is_open_spline (:class:`ByteTensor`): Whether to use open or closed
            B-spline bases for each dimension.
rusty1s's avatar
rusty1s committed
36
        degree (int, optional): B-spline basis degree. (default: :obj:`1`)
rusty1s's avatar
rusty1s committed
37
38
        norm (bool, optional): Whether to normalize output by node degree.
            (default: :obj:`True`)
rusty1s's avatar
rusty1s committed
39
        root_weight (:class:`Tensor`, optional): Additional shared trainable
rusty1s's avatar
rusty1s committed
40
            parameters for each feature of the root node of shape
rusty1s's avatar
rusty1s committed
41
42
43
44
45
            (in_channels x out_channels). (default: :obj:`None`)
        bias (:class:`Tensor`, optional): Optional bias of shape
            (out_channels). (default: :obj:`None`)

    :rtype: :class:`Tensor`
rusty1s's avatar
rusty1s committed
46
    """
rusty1s's avatar
rusty1s committed
47

rusty1s's avatar
rusty1s committed
48
49
50
51
52
    x = x.unsqueeze(-1) if x.dim() == 1 else x
    pseudo = pseudo.unsqueeze(-1) if pseudo.dim() == 1 else pseudo

    row, col = edge_index
    N, E, M_out = x.size(0), row.size(0), weight.size(2)
rusty1s's avatar
rusty1s committed
53

rusty1s's avatar
rusty1s committed
54
55
56
    # Weight each node.
    basis, weight_index = spline_basis(pseudo, kernel_size, is_open_spline,
                                       degree)
rusty1s's avatar
rusty1s committed
57

rusty1s's avatar
rusty1s committed
58
    out = spline_weighting(x[col], weight, basis, weight_index)
rusty1s's avatar
rusty1s committed
59

rusty1s's avatar
rusty1s committed
60
61
62
    # Convert E x M_out to N x M_out features.
    row_expanded = row.unsqueeze(-1).expand_as(out)
    out = x.new_zeros((N, M_out)).scatter_add_(0, row_expanded, out)
rusty1s's avatar
rusty1s committed
63

rusty1s's avatar
rusty1s committed
64
65
66
67
    # Normalize out by node degree (if wished).
    if norm:
        deg = out.new_zeros(N).scatter_add_(0, row, out.new_ones(E))
        out = out / deg.unsqueeze(-1).clamp_(min=1)
rusty1s's avatar
rusty1s committed
68

rusty1s's avatar
rusty1s committed
69
70
71
    # Weight root node separately (if wished).
    if root_weight is not None:
        out = out + torch.matmul(x, root_weight)
rusty1s's avatar
rusty1s committed
72

rusty1s's avatar
rusty1s committed
73
74
75
    # Add bias (if wished).
    if bias is not None:
        out = out + bias
rusty1s's avatar
rusty1s committed
76

rusty1s's avatar
rusty1s committed
77
    return out