←Home KnowML
TL;DR

Graph ML is for data where the connections carry the signal. A fraud ring is invisible in any single account's features. It is obvious in who pays whom.

One primitive does nearly all the work. Each node gathers vectors from its neighbours, aggregates them into one message, and updates itself. Stack $k$ layers and each node sees $k$ hops. GCN, GraphSAGE and GAT are that loop with three different aggregation rules.

What breaks is independence. Your samples touch each other, so a random split leaks, a minibatch has to be a subgraph, and stacking layers drags every node toward the same vector.

01Message passing

Look at the diagram first: three boxes, and everything else on this page changes the middle one and leaves the other two alone.

One layer of message passing — and the only box that differs between architectures
1 · GATHER read each neighbour's vector u₁ u₂ u₃ w 2 hops: silent v 2 · AGGREGATE any count, any order, one vector out GCN weight = 1 / √(dᵢ dⱼ) degree decides, nothing learned GraphSAGE mean / max of a sample fixed fan-out, so it scales GAT softmax attention αᵢⱼ content decides the weight 3 · UPDATE mix in your own state hᵥ ← σ( W · [hᵥ ‖ mᵥ] ) one vector per node the same W for every node stack another layer → one more hop of context Gather and update are shared. The architectures differ in the middle box only.
Watch the dashed node w. It is two hops from v, so one layer cannot reach it and a second layer can. That is the whole depth story: layers buy radius. Then read the middle box top to bottom. GCN, GraphSAGE and GAT are not three architectures to memorise. They are three answers to one question, which is how much a neighbour's message should count.

A graph is a set of nodes and a set of edges. Each node $i$ carries a feature vector $h_i$. That is the entire input.

A message passing layer updates every node at once, in three steps:

  • Gather. Each node reads the current vectors of the nodes it is connected to.
  • Aggregate. Those vectors collapse into a single message. This step must not care about ordering.
  • Update. The message is combined with the node's own vector and pushed through a learned transform.

Run the layer once and every node knows its immediate neighbours. Run it twice and information has travelled two hops. With $k$ layers, a node's output depends on everything within distance $k$. That is a receptive field, grown the same way a CNN grows one on page 05.

Why you cannot point a CNN at a graph

A 3×3 kernel has a dedicated weight for "the pixel above and to the left". That slot exists because a grid has directions and a fixed neighbour count.

A node has neither: one node has two neighbours, another has thirty thousand, and they arrive in no order. So the aggregation has to accept any number of inputs, in any order, and return the same answer. Sum, mean, max and attention all satisfy that. Concatenation does not.

Gilmer et al. named this framework MPNN in 2017 and showed that most published graph networks are instances of it. Learn the loop once, and a new architecture becomes one paragraph and no longer a whole paper.

02The graph as matrices

The loop above is a for-loop over edges. Written with matrices it becomes a single line, and that line is what you implement.

Three objects describe everything. The adjacency matrix $A$ is $N \times N$ with a 1 wherever an edge exists. The degree matrix $D$ is diagonal and holds each node's neighbour count. The feature matrix $X$ is $N \times F$, one row per node.

From those you get the graph Laplacian $L = D - A$. It is the discrete analogue of a second derivative on the graph, and its eigenvectors are the graph's Fourier basis. They reappear in section 11 as positional encodings.

Two adjustments turn $A$ into something you can propagate with. Add self-loops, $\hat{A} = A + I$, or a node discards its own state every layer. Then normalise by degree:

$$H^{(k+1)} = \sigma\!\left( \hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2} H^{(k)} W^{(k)} \right)$$

One matrix product does the gather and the aggregate for every node simultaneously. $W^{(k)}$ is the update, and it is the only learned tensor in the layer.

Normalisation does real work. Without it, a hub with 100,000 neighbours emits a message 100,000 times larger than a leaf node's, and the activations blow up in one layer. Symmetric normalisation gives each edge the weight $1/\sqrt{d_i d_j}$, so a message from a popular node counts for less.

In practice nobody stores $A$ densely, because $N^2$ floats is hopeless past about a hundred thousand nodes. Real graphs are sparse, with $E$ only a few times $N$, so implementations keep an edge list and use scatter-add. Cost is $O(E F)$ per layer, not $O(N^2 F)$.

03GCN: the aggregator with nothing learned in it

The simplest choice for the middle box. Each neighbour's weight is fixed by degree before training starts.

Kipf and Welling derived the graph convolutional network in 2016 as a first-order approximation to a spectral filter. The derivation matters less than the result. The result is the propagation matrix above, applied once per layer, with one shared weight matrix.

Stated plainly: average your neighbours and yourself, weight each by degree, then apply a linear layer and a nonlinearity. It is worth watching that happen on real numbers rather than trusting the equation.

Try it Build one GCN layer from an adjacency matrix and check the mixing by hand
import numpy as np

# 6 nodes: two triangles joined by the single edge 2-3.
E = [(0,1),(0,2),(1,2),(2,3),(3,4),(3,5),(4,5)]
A = np.zeros((6, 6))
for i, j in E: A[i, j] = A[j, i] = 1

A_hat = A + np.eye(6)         # self-loop, or a node forgets itself
deg = A_hat.sum(1)
Dm = np.diag(deg ** -0.5)
S = Dm @ A_hat @ Dm           # the aggregator: fixed, nothing learned

rng = np.random.default_rng(0)
X = rng.normal(size=(6, 4))   # 4 input features per node
W = rng.normal(size=(4, 2))   # the only learned tensor in the layer
H = np.maximum(S @ X @ W, 0)  # gather + aggregate, then update

print("A", A.shape, "X", X.shape, "W", W.shape, "-> H", H.shape)
print("degrees, self-loop included:", deg)
print("node 2 mixes with weights:  ", np.round(S[2], 3))
print("same mix, done by hand:", round(float(S[2] @ X[:, 0]), 4))
print("what the layer computed:", round(float((S @ X)[2, 0]), 4))
print("two layers stacked:         ", np.round((S @ S)[2], 3))
The last two numbers in node 2 mixes with weights are 0. and 0. Nodes 4 and 5 are two hops away, so one layer cannot see them at all. Stack two and their weight becomes 0.072. Inside the reachable set, node 2 gives its degree-3 neighbours 0.289 and its degree-4 neighbour 0.25: popular neighbours count for less. The hand-computed mix, -0.8755, is exactly what the layer produced.

Two properties are visible in that output and both matter later. A layer reaches exactly one hop. And the mixing weights depend only on how many edges a neighbour has, never on what the neighbour contains.

04GraphSAGE: sampling, and the inductive jump

Two changes to GCN: one makes it fit in memory, and the other lets it handle nodes it has never seen.

The first change is neighbour sampling. Instead of aggregating over every neighbour, draw a fixed number at random, say 25 in the first layer and 10 in the second. Work per node is now bounded, whatever the degree of the hub it happens to sit next to.

The second change is smaller and easy to skip past. GraphSAGE concatenates a node's own vector with the aggregated message rather than folding it in through a self-loop. The node's own state gets its own weights, so it cannot be averaged away by a crowd of neighbours.

The aggregator itself is a choice: mean, an elementwise max over a small MLP, or in the original paper an LSTM applied to a random permutation of the neighbours.

Transductive and inductive, in one sentence each

Methods that learn one embedding per node, such as node2vec or a GCN trained on a fixed graph, have no vector for a node that did not exist at training time. They are transductive.

GraphSAGE learns a function of features and neighbourhood, so a brand-new node gets a representation as soon as it has features and edges. That is inductive, and it is the difference between retraining nightly and serving in real time.

05GAT: let the weights be learned

GCN decides a neighbour's importance from degree alone. Attention makes it a function of what the neighbour says.

A graph attention network computes a coefficient per edge. Project both endpoints, concatenate, score with a shared vector, pass through a LeakyReLU, then softmax over the neighbourhood:

$$\alpha_{ij} = \frac{\exp\!\big(\mathrm{LeakyReLU}(\mathbf{a}^{\top}[W h_i \,\Vert\, W h_j])\big)}{\sum_{k \in \mathcal{N}(i)} \exp\!\big(\mathrm{LeakyReLU}(\mathbf{a}^{\top}[W h_i \,\Vert\, W h_k])\big)}$$

The denominator runs over neighbours only. The graph is the attention mask.

If that reads like the self-attention on page 08, it is. A transformer is a GNN over a complete graph, with position supplied by an encoding rather than by edges. GAT is the same operation with the mask set to the adjacency matrix. Hold on to that: section 11 becomes unsurprising.

Try it Put GCN's degree weights and GAT's attention weights side by side
import numpy as np
rng = np.random.default_rng(0)

E = [(0,1),(0,2),(1,2),(2,3),(3,4),(3,5),(4,5)]  # same two triangles
A = np.zeros((6, 6))
for i, j in E: A[i, j] = A[j, i] = 1
M = A + np.eye(6)                    # who is allowed to send a message
deg = M.sum(1)
gcn = M / np.sqrt(np.outer(deg, deg))   # GCN: degree decides, full stop

X = rng.normal(size=(6, 4))
W = rng.normal(size=(4, 3)) * 0.5
a_i, a_j = rng.normal(size=3), rng.normal(size=3)
Z = X @ W                            # (6, 3) projected node features
e = (Z @ a_i)[:, None] + (Z @ a_j)[None, :]
e = np.where(e > 0, e, 0.2 * e)      # LeakyReLU, as in the GAT paper
e = np.where(M > 0, e, -np.inf)      # non-neighbours cannot compete
att = np.exp(e - e.max(1, keepdims=True))
att /= att.sum(1, keepdims=True)     # softmax over each node's neighbours

print("Z", Z.shape, "e", e.shape, "att", att.shape)
print("node 2, GCN weights:", np.round(gcn[2], 3))
print("node 2, GAT weights:", np.round(att[2], 3))
print("row sums:  GCN", round(float(gcn[2].sum()), 3),
      " GAT", round(float(att[2].sum()), 3))
GCN gives nodes 0 and 1 the identical weight 0.289. GAT gives them 0.195 and 0.188. Those two neighbours have the same degree, so GCN has no way to tell them apart, while attention reads their features and does. Note the row sums: GAT's is exactly 1.0 because softmax normalises, GCN's is 1.077 because symmetric normalisation does not. Both rows are still zero at positions 4 and 5. Attention re-weights the neighbourhood, it does not enlarge it.

Attention has a cost: you store a coefficient per edge per head, so memory is $O(E H)$. It pays off when neighbours differ in relevance, which is typical of heterogeneous graphs. On a regular graph where every neighbour is alike, GAT and GCN land in much the same place.

Where you are

You have the primitive and three aggregators. GCN weights by degree, GraphSAGE samples and keeps the self-vector separate, GAT learns a weight per edge. Nothing else about the loop changed.

The rest of the page is what happens when you try to use it: what you attach to the node vectors, why splitting the data is subtle, how to fit a graph that does not fit, and why depth hurts here.

06Nodes, edges, graphs: three levels of prediction

Message passing gives you one vector per node. What you attach to those vectors is what decides the task.

  • Node level. A classifier on each row of $H$. Is this account fraudulent, what topic is this paper.
  • Edge level. A decoder over a pair, either a dot product $h_u \cdot h_v$ or an MLP on the concatenation. Link prediction and recommendation live here.
  • Graph level. Pool every node vector into one, then predict. Is this molecule soluble, is this program malicious.

The pooling step is called readout, and the choice between sum and mean matters. Sum preserves size, so it can count. Mean is size-invariant, which generalises better when test graphs are much larger than training ones. Xu et al. showed sum followed by an MLP is the most expressive of the simple options.

Link prediction carries a quiet difficulty. There are $N^2$ possible edges and only $E$ real ones, so most of the training signal is a choice about which non-edges to sample. Uniform corruption of one endpoint is standard. Sampling hard negatives, such as nodes two hops away, makes the task harder and the reported numbers far more honest.

07Your samples are not independent

Every other page here assumes rows can be shuffled and split. On a graph they cannot be, and that changes what a validation set means.

There are two regimes, and interviewers ask you to name them.

Transductive: the entire graph is present during training, edges and features included. Only some labels are hidden. You split the labels and leave the graph intact. Most citation-network benchmarks work this way, which is worth remembering when a published number looks high.

Inductive: the test nodes or graphs were absent during training. Separate molecules in separate splits, or one evolving graph cut at a date. This is what production looks like, because tomorrow's users were not in last night's snapshot.

The leak that makes link prediction look solved

To predict an edge you must delete it from the adjacency matrix used for message passing. Leave it in and the model reads the answer straight off its input.

This produces an AUC near 1.0 and a model that is worthless in production. The undirected version catches people too: delete the reverse edge as well, or half the information is still there.

Time makes it sharper: graphs grow, and a random split trains on next week to predict last week, so split by timestamp instead, with the same discipline page 17 applies to series data.

08Neighbour explosion, and why a minibatch is a subgraph

Full-batch training holds the whole graph and every layer's activations in memory. Past a few million nodes that stops working.

The arithmetic is unforgiving: with average degree $d$ and $k$ layers, one target node needs its $k$-hop neighbourhood: roughly $d^k$ nodes. At $d = 30$ and $k = 3$ that is 27,000 nodes to produce one prediction.

So you cannot take a random sample of rows. A minibatch on a graph is a subgraph, and three families of method build one:

  • Node-wise sampling (GraphSAGE). Fix a fan-out per layer, such as 25 then 10. Simple and still the default. Growth is exponential in depth, just with a small base.
  • Layer-wise sampling (FastGCN). Sample a fixed number of nodes per layer, importance-weighted, so total cost is linear in depth. Needs care or the sampled layers share too few edges.
  • Subgraph sampling (Cluster-GCN, GraphSAINT). Partition the graph, then run full-batch on one partition. Cheap and cache-friendly. Every edge crossing a partition boundary is dropped for that step.

Sampling has a cost you feel later. Training sees a random neighbourhood each epoch and evaluation usually uses the full one, so the two are not measuring the same model. Fix the seed, and report which regime a number came from.

Serving is the other half of the problem. A prediction needs the node's $k$-hop neighbourhood, which is a database query, not a tensor. Many production systems precompute embeddings in a nightly batch job and serve them as ordinary features, trading freshness for a latency budget they can meet.

Where you are

The primitive, the three aggregators, the three task levels, how to split without leaking, and how to fit a graph that does not fit in memory.

What is left is the part that surprises people. Making a GNN deeper usually makes it worse, and there are two separate reasons with two separate fixes.

09Over-smoothing and over-squashing

Depth helps every other architecture on this site. Here it is a budget you spend, and most GNNs in production are two or three layers deep.

Over-smoothing is the first reason: averaging over neighbours is a low-pass filter, and each layer pulls connected nodes closer together. Iterate enough and every node in a connected component converges to the same representation. There is then nothing left for a classifier to separate.

You do not have to take that on trust. It is four lines of numpy.

Try it Propagate repeatedly and watch every node collapse onto the same vector
import numpy as np

E = [(0,1),(0,2),(1,2),(2,3),(3,4),(3,5),(4,5)]  # same two triangles
A = np.zeros((6, 6))
for i, j in E: A[i, j] = A[j, i] = 1
A_hat = A + np.eye(6)
Dm = np.diag(A_hat.sum(1) ** -0.5)
S = Dm @ A_hat @ Dm

def spread(M):                 # mean distance between unit-norm rows
    U = M / np.linalg.norm(M, axis=1, keepdims=True)
    D = np.linalg.norm(U[:, None] - U[None, :], axis=-1)
    return D[np.triu_indices(6, 1)].mean()

rng = np.random.default_rng(0)
H = rng.normal(size=(6, 8))    # nodes start with unrelated features

print("layers  mean pairwise distance")
for k in range(65):
    if k in (0, 1, 2, 4, 8, 16, 32, 64):
        print(f"{k:6d}  {spread(H):.5f}")
    H = S @ H                  # one more round of message passing
lam = np.sort(np.abs(np.linalg.eigvalsh(S)))[::-1]
print("shrink factor per layer =", round(float(lam[1]), 3))
From 1.35136 to 0.00010. The six nodes start pointing in unrelated directions and end up indistinguishable. Look at the rate: 0.86 is the second-largest eigenvalue of the propagation matrix, and the spread shrinks by roughly that factor every layer. Over-smoothing is not a bug in anyone's code. It is a power iteration converging to the dominant eigenvector, which is what repeated averaging always does.

A real GNN puts a weight matrix and a ReLU between propagation steps, which slows the collapse. It does not prevent it. The standard defences all work by keeping earlier, sharper representations reachable:

  • Residual connections, exactly as on page 04, so each layer adds to rather than replaces the node state.
  • Jumping knowledge: concatenate every layer's output at the end, letting the model pick its own effective depth per node.
  • Decoupling propagation from transformation. SGC strips the nonlinearities and applies $S^k X$ once. APPNP propagates with a restart term, which keeps a fixed share of a node's own features in play forever.

Over-squashing is the second reason, and it is the opposite complaint. Information from distant nodes must funnel through a few bottleneck edges. The number of nodes at distance $k$ grows exponentially while the vector carrying them stays a fixed width.

Two problems that look identical from the outside

Both appear as "I added layers and it got worse". They are not the same failure. Over-smoothing is too much mixing, so representations collapse together. Over-squashing is too little bandwidth on the path, so distant information never arrives.

The test is cheap: measure the spread of your node representations, as the block above does. If it is shrinking, you have smoothing, and depth is the cause. If the spread is healthy but long-range tasks still fail, you have squashing, and more layers will not help because the bottleneck is an edge.

Squashing is fixed by changing the graph or bypassing it. Rewiring adds edges to shorten paths. A virtual node connected to everything gives every pair a two-hop route. Global attention removes the question entirely, which is section 11.

10Knowledge graphs and embeddings

A knowledge graph has typed edges, and the type is the point. The models look different because the task is different: score a fact.

The unit is a triple of head entity, relation and tail entity, such as (Paris, capital_of, France). A knowledge graph holds millions of them and is always incomplete, so the central task is link prediction. Given a head and a relation, rank every entity as the tail.

Each method is a scoring function, and each one's weakness is a fact it cannot represent:

  • TransE. A relation is a translation: $h + r \approx t$. Elegant and weak. It cannot express a symmetric relation without forcing $r = 0$, and one-to-many relations collapse.
  • DistMult. A bilinear score with a diagonal matrix. Cheap, but symmetric by construction, so it scores "A is the parent of B" and the reverse identically.
  • ComplEx. Move the embeddings into complex space and use the conjugate on the tail. Asymmetry becomes representable at almost no extra cost.
  • RotatE. A relation is a rotation in the complex plane. Symmetry, inversion and composition all fall out of one form, which is why it is the usual modern baseline.

Training corrupts one endpoint of a true triple to make negatives. Evaluation ranks the true tail against all candidates and reports MRR and Hits@k. Use the filtered version, which removes other known-true triples from the ranking. Otherwise you penalise the model for correct answers.

Message passing is still available here. R-GCN gives each relation type its own weight matrix, then shares parameters through a basis decomposition because the count explodes. The rough rule: use a GNN when node features carry information, use embeddings when the structure is all you have.

11Beyond message passing

Message passing has a ceiling, and unusually for deep learning it is a precise one.

A message-passing GNN cannot distinguish two graphs that the 1-dimensional Weisfeiler-Leman colour refinement test cannot distinguish. The reason is structural: both produce the same multiset of neighbour labels at every round, so both produce identical outputs forever.

Xu et al. proved that bound and showed GIN attains it by using sum aggregation followed by an MLP. The practical consequence is that a standard GNN cannot count triangles or tell a 6-cycle from two triangles. If your task needs that, compute it as a feature and feed it in.

Graph transformers take the other route. Let every node attend to every other node, and inject structure through positional encodings, usually Laplacian eigenvectors or random-walk landing probabilities.

Over-squashing disappears, because everything is one hop away. Cost returns to $O(N^2)$, which is fine for a molecule with 40 atoms and hopeless for a social network. GraphGPS is the common compromise: one local message-passing layer and one global attention layer per block.

Two more pieces complete the checklist.

Graph contrastive learning attacks the label scarcity. Build two views of a graph by dropping edges, masking features or sampling subgraphs, then pull the same node's two views together and push other nodes apart, using page 03's machinery. It works, but there is no ImageNet here. A model pretrained on molecules tells you nothing about a payment network.

Temporal graphs carry a timestamp on every edge. The usual design keeps a memory vector per node and updates it as events arrive, so a representation reflects the node's history rather than a static snapshot. Fraud and recommendation both need this, because the order of events is most of the signal.

12Build this

The claim worth testing yourself is that structure alone carries signal, and that depth destroys it. Both are visible in an afternoon on a laptop.

Project Classify nodes that have no informative features, then smooth it to death ~3 hours · numpy + PyTorch, CPU

Build a graph with two communities and give every node pure noise as its feature vector. No feature says anything about the label. The only way to be right is to use the edges, so any accuracy above chance is the graph talking.

  1. Generate 200 nodes in two communities. Connect pairs inside a community with probability 0.08 and pairs across communities with 0.005. Give every node a random 16-dimensional feature vector drawn from the same distribution, regardless of its community.
  2. Build the propagation matrix by hand from the adjacency: add self-loops, compute degrees, form $\hat{D}^{-1/2}\hat{A}\hat{D}^{-1/2}$. A layer is relu(S @ X @ W) and needs nothing else, including any graph library.
  3. Train a two-layer version with cross-entropy on 10 labelled nodes per community. Report accuracy on every unlabelled node.
  4. Ablate the graph. Delete S from the forward pass, keeping the same weights and training loop, so it becomes an MLP on noise, which is the baseline that proves the point.
  5. Now the breakage. Rebuild with 2, 4, 8 and 16 layers. At each depth record accuracy and the mean pairwise distance between node representations, the same quantity the third code block prints.
  6. For the second breakage, keep the labels and rewire the edges at random, destroying the community structure while preserving degrees.
You'll know it worked when the two-layer GCN separates the communities from features that contain no information, while the MLP ablation sits at chance. Then the depth sweep shows accuracy falling and the representation spread collapsing together, in the same run.
What the breakages teach. The MLP ablation isolates the contribution of structure: the difference between the two curves is the graph, measured. The random rewiring is the reverse control. The model keeps its capacity, its features and its labels, and loses only the community structure, which sends it back to chance. Together they rule out the two explanations people reach for first, which are that the features were secretly informative and that the model memorised the labelled nodes.

13What breaks

Graph failures are rarely the architecture. They are usually a split that leaked or an assumption about the graph that was never checked.

  • Link prediction scores 0.99 AUC. The edges being predicted were still present in the adjacency matrix used for message passing. The model was reading the answer.
  • A gradient-boosted tree on node features beats the GNN. This happens often and it is informative. It means the edges are not carrying signal for your task, and you have paid a large engineering cost for nothing.
  • The graph is heterophilous and nobody checked. GCN assumes connected nodes share labels. In fraud, a mule connects to victims rather than to other mules, so averaging over neighbours erases exactly the contrast you need.
  • Adding layers made it worse. The likely cause is over-smoothing, so measure representation spread before blaming the learning rate.
  • Results will not reproduce. Neighbour sampling is random, training saw one sample and evaluation used full neighbourhoods, and nobody fixed the seed or wrote down which was which.
  • One hub node blows up the batch. A celebrity account with a million edges makes a sampled subgraph enormous, and the out-of-memory error arrives hours into training.
  • It trains but cannot be served. Inference needs a $k$-hop neighbourhood fetched from a database within the latency budget. Page 24 has the rest of that story.
  • The model is transductive and the world is not. New nodes arrive every minute and the embedding table has no row for them.

14Where you meet this in the wild

The primitive is the same, but the graphs differ and so do the ways each one goes wrong.

Molecules and materials

Atoms are nodes, bonds are edges, and the label belongs to the whole graph. Graphs are tiny, so full-batch training is easy and global attention is affordable. Permutation invariance is free, which is the reason this was the field's first real win.

Graph-level readout, often a hybrid of message passing and attention.

Fraud and abuse

A ring is invisible per account and obvious in the transaction graph. Labels are rare, adversaries adapt weekly, and new accounts must be scored the moment they appear.

Inductive sampling models, temporal edges, and a heterophily check before anything else.

Recommendation at web scale

The user-item matrix is a bipartite graph, so recommendation is link prediction. PinSage, from Ying et al. in 2018, was the first published system to run this over billions of edges, and it did so with sampling rather than a cleverer layer.

Sampled neighbourhoods feeding the retrieval stage on page 16.

Knowledge graphs behind search

Entities and typed relations, used to answer factual queries and to ground a retrieval system in something checkable. Completion suggests facts nobody entered.

Embedding models such as RotatE, and graph-structured retrieval on page 11.

Not a message-passing GNN by default.

15Interview questions

BeginnerWalk me through one layer of a graph neural network.

Every node holds a feature vector, and a layer updates all of them at once in three steps. First gather: each node reads the current vectors of the nodes it is connected to. Second aggregate: those neighbour vectors are combined into a single message by a function that must be permutation invariant, because a node's neighbours have no ordering and no fixed count, which rules out concatenation and leaves sum, mean, max and attention. Third update: the message is combined with the node's own vector and passed through a shared learned transform and a nonlinearity. Written as matrices this is one product of a normalised adjacency matrix with the feature matrix and a weight matrix. Stacking k layers means each node's output depends on its k-hop neighbourhood, so depth buys receptive field exactly as it does in a convolutional network.

BeginnerWhy not flatten the adjacency matrix and feed it to an MLP?

Two reasons, and both are fatal. First, an MLP over a flattened adjacency matrix is not permutation invariant: relabelling the nodes of the same graph produces a different input and therefore a different prediction, even though nothing about the graph changed. The model would have to learn invariance from data, which wastes enormous capacity on a property you could have built in. Second, the input size is fixed, so a network trained on graphs with a thousand nodes cannot accept one with a thousand and one, and real graphs vary in size constantly. Message passing solves both structurally. It is defined per node in terms of an unordered neighbour set, so it gives the same answer under relabelling and accepts any graph size with the same weights.

IntermediateCompare GCN, GraphSAGE and GAT.

They are the same three-step loop with different aggregation. GCN weights each neighbour by one over the square root of the product of the two degrees, a fixed quantity computed from the graph before training, and adds self-loops so a node keeps its own state. GraphSAGE changes two things: it samples a fixed number of neighbours per layer rather than using all of them, which bounds compute regardless of hub degree, and it concatenates the node's own vector with the aggregated message instead of mixing it in, so the self representation has its own weights. It is also explicitly inductive, learning a function of features rather than an embedding per node. GAT replaces the fixed coefficient with a learned attention weight per edge, computed from both endpoints' features and normalised by a softmax over the neighbourhood, which is self-attention with the adjacency matrix as the mask. GCN is the cheapest, GraphSAGE is what scales, GAT helps when neighbours differ in relevance.

IntermediateWhat is the difference between transductive and inductive learning on graphs, and how do you split data for each?

Transductive means the entire graph, including all edges and all node features, is available during training and only some labels are hidden, so you split the labels rather than the graph. Most citation benchmarks are set up this way, which is why their numbers look strong. Inductive means the test nodes or graphs were genuinely unseen at training time, either because they are separate graphs as in molecular datasets or because the graph was cut at a timestamp. Production is almost always inductive, since the users and transactions you will score tomorrow are not in tonight's snapshot. The splitting rules follow: for node classification hide labels but keep the structure in the transductive case, and hold out whole subgraphs or a time range in the inductive case. For link prediction the held-out edges must be removed from the adjacency used for message passing, including their reverse direction in an undirected graph, or the model simply reads the target off its own input.

IntermediateWhy do deep GNNs usually perform worse than shallow ones?

Because a message-passing layer is a low-pass filter, and stacking them is a power iteration. Each layer averages every node with its neighbours, which pulls connected representations closer, and repeated application converges toward the dominant eigenvector of the propagation matrix, so every node in a connected component ends up with essentially the same vector. The rate is set by the second eigenvalue, and on a small well-connected graph the pairwise distance between node representations can fall by an order of magnitude within ten or so layers. A classifier then has nothing left to separate. This is over-smoothing, and it is why most deployed GNNs are two or three layers deep. Mitigations keep earlier representations reachable: residual connections, jumping knowledge that concatenates every layer's output, normalisation schemes such as PairNorm, and decoupling propagation from transformation as SGC and APPNP do, with APPNP's restart term permanently retaining a share of each node's own features.

DeepHow would you train a GNN on a graph with a billion edges?

You stop treating a minibatch as a set of rows and start treating it as a subgraph, because with average degree thirty and three layers a single target node already pulls in tens of thousands of others. Three families of sampling exist. Node-wise sampling, from GraphSAGE, fixes a fan-out per layer such as twenty-five then ten, which is simple and still the common default though it remains exponential in depth. Layer-wise sampling, as in FastGCN, draws a fixed number of nodes per layer with importance weights so total cost is linear in depth, at the price of needing care that sampled layers share enough edges. Subgraph sampling, as in Cluster-GCN and GraphSAINT, partitions the graph and runs full-batch on one partition, which is cache-friendly but discards edges that cross partition boundaries. Beyond sampling you cap the degree of hub nodes so one celebrity does not blow up a batch, and you decide early whether serving will do live k-hop lookups or precompute embeddings nightly, since that choice constrains the architecture more than accuracy does.

DeepDistinguish over-smoothing from over-squashing.

Both present as adding layers making results worse, but they are opposite problems. Over-smoothing is too much mixing: repeated neighbourhood averaging is a low-pass filter that drives node representations together until they are indistinguishable, and it gets worse with depth by construction. Over-squashing is too little bandwidth: the number of nodes within distance k grows exponentially while the fixed-width vector carrying their information does not, so a message from a distant node must funnel through a small number of bottleneck edges and arrives degraded. Depth does not fix squashing, because the constraint is the topology rather than the number of layers. The diagnosis differs too. If the mean pairwise distance between node representations is collapsing, it is smoothing, and the fixes are residuals, jumping knowledge or decoupled propagation. If the representations stay well spread but long-range tasks fail, it is squashing, and the fixes change the graph rather than the network: rewiring to shorten paths, adding a virtual node connected to everything, or replacing local propagation with global attention.

DeepYou are asked to detect fraud rings in a payment network. How do you approach it?

Start by checking whether the graph is worth using at all, with a gradient-boosted tree on node features as the baseline the GNN must beat, because that comparison is frequently lost and is cheap to run. Then check homophily, since fraud is often heterophilous: a mule connects to victims rather than to other mules, and plain neighbourhood averaging erases the contrast, which argues for architectures that keep self and neighbour representations separate, such as GraphSAGE with concatenation, or explicitly heterophily-aware layers. The model must be inductive, because accounts are created continuously and a transductive embedding table has no row for them, and it should be temporal, since the ordering and recency of transactions carry most of the signal. Train with neighbour sampling and cap hub degree so a high-volume merchant does not dominate a batch. Evaluate on a time-based split with precision at a realistic alert budget rather than AUC, since labels are extremely imbalanced and delayed. Finally, plan serving up front: a k-hop lookup inside a latency budget, or nightly precomputed embeddings served as features, which is a genuine freshness tradeoff against an adversary who adapts in days.

16Go deeper

📄
Paper
Neural Message Passing for Quantum Chemistry
Gilmer, Schoenholz, Riley, Vinyals & Dahl, 2017 — the framework this whole page is built on, and the name for it — arXiv:1704.01212
📄
Paper
Semi-Supervised Classification with Graph Convolutional Networks
Kipf & Welling, 2016 — the GCN layer in section 03, derived from a spectral filter — arXiv:1609.02907
📄
Paper
Inductive Representation Learning on Large Graphs
Hamilton, Ying & Leskovec, 2017 — GraphSAGE: neighbour sampling and the inductive setting — arXiv:1706.02216
📄
Paper
Graph Attention Networks
Veličković et al., 2017 — attention weights per edge, and the masked softmax in section 05 — arXiv:1710.10903
📄
Paper
Deeper Insights into Graph Convolutional Networks for Semi-Supervised Learning
Li, Han & Wu, 2018 — the analysis that named over-smoothing as Laplacian smoothing — arXiv:1801.07606
📄
Paper
On the Bottleneck of Graph Neural Networks and its Practical Implications
Alon & Yahav, 2020 — over-squashing, and why it is not the same problem as over-smoothing — arXiv:2006.05205
📄
Paper
How Powerful are Graph Neural Networks?
Xu, Hu, Leskovec & Jegelka, 2018 — the 1-WL expressivity bound and GIN, which reaches it — arXiv:1810.00826
📄
Paper
Recipe for a General, Powerful, Scalable Graph Transformer
Rampášek et al., 2022 — GraphGPS: local message passing plus global attention, with positional encodings — arXiv:2205.12454
📄
Paper
RotatE: Knowledge Graph Embedding by Relational Rotation in Complex Space
Sun, Deng, Nie & Tang, 2019 — the knowledge graph baseline in section 10 — arXiv:1902.10197
🔧
Tool
PyTorch Geometric
The library to build with once you have written a layer by hand. Its sampling loaders are the practical version of section 08.

●Now write it yourself

Reading the derivation and being able to produce it are different skills. These are Deep-ML problems that exercise what this page covers — each one is checked against real test cases, not multiple choice.

Matched to this page from Deep-ML's catalogue of 1,380 problems. More at deep-ml.com, and Where to practise covers the other platforms and what each one trains.

Other techniques for this problem

A scoped slice of the full Technique Map — every technique this page covers, grouped by what it solves.

My Notes — 18 Graph ML

Free notes

Highlights on this page