Skill Tracer
Skills › Scientific Agent Skills › torch-geometric › message_passing.md

message_passing.md

127 lines · skills/torch-geometric/references/message_passing.md at 5f2bc03

Open on GitHub
1# Custom Message Passing Layers
2
3Full reference for implementing custom GNN layers via the `MessagePassing` base class.
4

Custom Message Passing Layers

Full reference for implementing custom GNN layers via the MessagePassing base class.

Raw lines 1 to 4
5## MessagePassing API
6
7```python
8MessagePassing(aggr="add", flow="source_to_target", node_dim=-2)
9```
11- `aggr`: Aggregation scheme — `"add"`, `"mean"`, or `"max"`
12- `flow`: Message direction — `"source_to_target"` (default) or `"target_to_source"`
13- `node_dim`: Axis along which to propagate

MessagePassing API

MessagePassing(aggr="add", flow="source_to_target", node_dim=-2)
  • aggr: Aggregation scheme — "add", "mean", or "max"
  • flow: Message direction — "source_to_target" (default) or "target_to_source"
  • node_dim: Axis along which to propagate
Raw lines 5 to 14
15### Methods to override
17- `message(...)`: Constructs messages for each edge. Access source/target node features via `_j`/`_i` suffixes.
18- `aggregate(inputs, index)`: Aggregates messages (usually handled by `aggr` parameter).
19- `update(aggr_out, ...)`: Post-aggregation transform on each node.
20- `propagate(edge_index, size=None, **kwargs)`: Orchestrates the full pipeline. Call this from `forward()`.
22Any tensor passed to `propagate()` can be auto-indexed in `message()` by appending `_i` (target) or `_j` (source). E.g., passing `x=features` lets you use `x_i` and `x_j` in the message function.
24For bipartite graphs, pass `size=(N, M)` to `propagate()` and provide features as tuples: `x=(x_src, x_dst)`.

Methods to override

  • message(...): Constructs messages for each edge. Access source/target node features via _j/_i suffixes.
  • aggregate(inputs, index): Aggregates messages (usually handled by aggr parameter).
  • update(aggr_out, ...): Post-aggregation transform on each node.
  • propagate(edge_index, size=None, **kwargs): Orchestrates the full pipeline. Call this from forward().

Any tensor passed to propagate() can be auto-indexed in message() by appending _i (target) or _j (source). E.g., passing x=features lets you use x_i and x_j in the message function.

For bipartite graphs, pass size=(N, M) to propagate() and provide features as tuples: x=(x_src, x_dst).

Raw lines 15 to 25
26## Example: GCN Layer from Scratch
28```python
29import torch
30from torch.nn import Linear, Parameter
31from torch_geometric.nn import MessagePassing
32from torch_geometric.utils import add_remaining_self_loops, degree
34class GCNConv(MessagePassing):
35 def __init__(self, in_channels, out_channels):
36 super().__init__(aggr='add')
37 self.lin = Linear(in_channels, out_channels, bias=False)
38 self.bias = Parameter(torch.empty(out_channels))
39 self.reset_parameters()
41 def reset_parameters(self):
42 self.lin.reset_parameters()
43 torch.nn.init.zeros_(self.bias)
45 def forward(self, x, edge_index):
46 # 1. Add self-loops
47 edge_index, _ = add_remaining_self_loops(edge_index, num_nodes=x.size(0))
48 # 2. Linear transform
49 x = self.lin(x)
50 # 3. Compute normalization coefficients
51 row, col = edge_index
52 deg = degree(col, x.size(0), dtype=x.dtype)
53 deg_inv_sqrt = deg.pow(-0.5)
54 deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0
55 norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
56 # 4-5. Message passing
57 out = self.propagate(edge_index, x=x, norm=norm)
58 # 6. Add bias
59 return out + self.bias
61 def message(self, x_j, norm):
62 # x_j: source node features for each edge [num_edges, out_channels]
63 # norm: normalization coefficients [num_edges]
64 return norm.view(-1, 1) * x_j
65```

Example: GCN Layer from Scratch

import torch
from torch.nn import Linear, Parameter
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import add_remaining_self_loops, degree

class GCNConv(MessagePassing):
    def __init__(self, in_channels, out_channels):
        super().__init__(aggr='add')
        self.lin = Linear(in_channels, out_channels, bias=False)
        self.bias = Parameter(torch.empty(out_channels))
        self.reset_parameters()

    def reset_parameters(self):
        self.lin.reset_parameters()
        torch.nn.init.zeros_(self.bias)

    def forward(self, x, edge_index):
        # 1. Add self-loops
        edge_index, _ = add_remaining_self_loops(edge_index, num_nodes=x.size(0))
        # 2. Linear transform
        x = self.lin(x)
        # 3. Compute normalization coefficients
        row, col = edge_index
        deg = degree(col, x.size(0), dtype=x.dtype)
        deg_inv_sqrt = deg.pow(-0.5)
        deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0
        norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
        # 4-5. Message passing
        out = self.propagate(edge_index, x=x, norm=norm)
        # 6. Add bias
        return out + self.bias

    def message(self, x_j, norm):
        # x_j: source node features for each edge [num_edges, out_channels]
        # norm: normalization coefficients [num_edges]
        return norm.view(-1, 1) * x_j
Raw lines 26 to 66
67## Example: EdgeConv Layer
69```python
70import torch
71from torch.nn import Sequential as Seq, Linear, ReLU
72from torch_geometric.nn import MessagePassing
74class EdgeConv(MessagePassing):
75 def __init__(self, in_channels, out_channels):
76 super().__init__(aggr='max')
77 self.mlp = Seq(
78 Linear(2 * in_channels, out_channels),
79 ReLU(),
80 Linear(out_channels, out_channels),
81 )
83 def forward(self, x, edge_index):
84 return self.propagate(edge_index, x=x)
86 def message(self, x_i, x_j):
87 # x_i: target node features [num_edges, in_channels]
88 # x_j: source node features [num_edges, in_channels]
89 return self.mlp(torch.cat([x_i, x_j - x_i], dim=1))
90```
92This unweighted implementation assumes the default source-to-target flow and matches `GCNConv` normalization for the same graph. Existing self-loops are retained once. Choose directed/undirected semantics deliberately; do not reuse a cached normalization after changing edges.

Example: EdgeConv Layer

import torch
from torch.nn import Sequential as Seq, Linear, ReLU
from torch_geometric.nn import MessagePassing

class EdgeConv(MessagePassing):
    def __init__(self, in_channels, out_channels):
        super().__init__(aggr='max')
        self.mlp = Seq(
            Linear(2 * in_channels, out_channels),
            ReLU(),
            Linear(out_channels, out_channels),
        )

    def forward(self, x, edge_index):
        return self.propagate(edge_index, x=x)

    def message(self, x_i, x_j):
        # x_i: target node features [num_edges, in_channels]
        # x_j: source node features [num_edges, in_channels]
        return self.mlp(torch.cat([x_i, x_j - x_i], dim=1))

This unweighted implementation assumes the default source-to-target flow and matches GCNConv normalization for the same graph. Existing self-loops are retained once. Choose directed/undirected semantics deliberately; do not reuse a cached normalization after changing edges.

Raw lines 67 to 93
94## Example: Dynamic EdgeConv (recomputes graph each layer)
96Illustrative: requires a compatible `pyg-lib` build in PyG 2.8. Supply the node-to-graph `batch` vector to avoid connections between examples; each graph needs more than `k` nodes with `loop=False`.
98```python
99from torch_geometric.nn import knn_graph
101class DynamicEdgeConv(EdgeConv):
102 def __init__(self, in_channels, out_channels, k=6):
103 super().__init__(in_channels, out_channels)
104 self.k = k
106 def forward(self, x, batch=None):
107 edge_index = knn_graph(x, self.k, batch, loop=False, flow=self.flow)
108 return super().forward(x, edge_index)
109```

Example: Dynamic EdgeConv (recomputes graph each layer)

Illustrative: requires a compatible pyg-lib build in PyG 2.8. Supply the node-to-graph batch vector to avoid connections between examples; each graph needs more than k nodes with loop=False.

from torch_geometric.nn import knn_graph

class DynamicEdgeConv(EdgeConv):
    def __init__(self, in_channels, out_channels, k=6):
        super().__init__(in_channels, out_channels)
        self.k = k

    def forward(self, x, batch=None):
        edge_index = knn_graph(x, self.k, batch, loop=False, flow=self.flow)
        return super().forward(x, edge_index)
Raw lines 94 to 110
111## Utility Functions
113```python
114from torch_geometric.utils import (
115 add_self_loops, # Add self-loop edges
116 remove_self_loops, # Remove self-loop edges
117 degree, # Compute node degrees
118 softmax, # Sparse softmax over neighborhoods
119 to_dense_adj, # Convert edge_index to dense adjacency matrix
120 to_undirected, # Make edge_index undirected
121 contains_self_loops, # Check for self-loops
122 is_undirected, # Check if graph is undirected
123 scatter, # Scatter operations (sum, mean, max)
125```
127Sources: [MessagePassing](https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.nn.conv.MessagePassing.html), [custom layers](https://pytorch-geometric.readthedocs.io/en/latest/tutorial/create_gnn.html). For sparse message passing, use target rows/source columns (`adj_t`), the transpose of source-row COO adjacency.

Utility Functions

from torch_geometric.utils import (
    add_self_loops,      # Add self-loop edges
    remove_self_loops,   # Remove self-loop edges
    degree,              # Compute node degrees
    softmax,             # Sparse softmax over neighborhoods
    to_dense_adj,        # Convert edge_index to dense adjacency matrix
    to_undirected,       # Make edge_index undirected
    contains_self_loops, # Check for self-loops
    is_undirected,       # Check if graph is undirected
    scatter,             # Scatter operations (sum, mean, max)
)

Sources: MessagePassing, custom layers. For sparse message passing, use target rows/source columns (adj_t), the transpose of source-row COO adjacency.

Raw lines 111 to 127