Graph Neural Networks
Learn how GNNs read graph-structured data — nodes, edges, and features — by passing messages between neighbours, and build one message-passing step by hand in plain Python before meeting GCN, GraphSAGE, and GAT in PyTorch Geometric.
Learn Graph Neural Networks in our free AI & Machine Learning course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a…
Part of the free AI & Machine Learning course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Want to guess someone's politics, music taste, or whether they will repay a loan? You learn a surprising amount just from who they spend time with . "You are the average of the five people you hang out with" is folk wisdom — and it is almost exactly how a GNN thinks.
Picture a friendship network. Each person holds a few facts about themselves (their features ). In one "round of gossip", everyone updates their own view by averaging in what their direct friends think . Do it again and your friends-of-friends start to influence you too. After a few rounds, your updated profile quietly encodes your whole local neighbourhood — not just you. That round of gossip is message passing , and stacking rounds is exactly what stacking GNN layers does.
A graph is data made of nodes (the things — people, atoms, products) joined by edges (the relationships — friendships, bonds, "bought together"). Unlike a table, where every row is independent, a graph's whole point is that the connections carry meaning.
🗂️ The simplest way to store a graph in code
An adjacency dict — every example below uses this:
Almost every GNN is a stack of message-passing layers, and each layer does the same three steps for every node:
After one layer, a node's new embedding reflects its 1-hop neighbourhood. Stack a second layer and it reaches 2 hops (friends of friends); k layers reach k hops. That is the entire trick — aggregate, then transform, then repeat.
The worked example below does step 1 and step 2 in plain Python: each node's new feature is simply the average of its neighbours' features . No NumPy, no PyTorch — just a dictionary and a loop. Read every comment, then run it.
Run the same averaging rule several times and watch a single signal spread across the graph — and start to flatten out. That flattening is a clue to a real GNN pitfall you will meet in Common Errors.
All three are message-passing layers. They differ in how they aggregate neighbours :
Takes a degree-normalised average of all neighbours. The simplest layer and a strong baseline — it is exactly the averaging you coded above, with a normalisation that stops high-degree nodes from dominating.
Samples a fixed number of neighbours instead of using all of them. That makes it scale to graphs with billions of edges and lets it handle nodes it never saw in training ( inductive learning).
Learns an attention weight for each neighbour, so important neighbours count more. Not every friend should count equally — GAT figures out who matters, the same idea that powers Transformers.
In practice you rarely hand-code these. PyTorch Geometric (PyG) ships GCNConv , SAGEConv , and GATConv as drop-in layers. The next example is read-only PyG showing a single GCNConv doing aggregate-then-transform for you.
Once message passing has given every node a rich embedding, you can predict at three different levels:
The read-only example below uses a GATConv (attention) layer and then produces all three: per-node logits, per-edge scores, and one graph-level vector from mean pooling .
GNNs shine wherever relationships matter as much as the items themselves :
Now you try. Fill in the blanks marked ___ . Gathering a node's neighbours and averaging them is message passing — get this and you have the heart of every GNN.
Your turn again. Real GNN layers include each node in its own neighbourhood — a self-loop — so a node never forgets its own features. Add it here.
These four mistakes trip up almost everyone building their first GNN:
Stack too many message-passing layers and every node keeps averaging in more of the graph until all embeddings collapse to nearly the same vector — the model can no longer tell nodes apart and accuracy crashes.
✅ Fix: keep it shallow (2-4 layers); add residual / jumping-knowledge connections only if you truly need depth.
If you aggregate only over neighbours, a node throws away its own features in every layer and loses its identity.
✅ Fix: add a self-loop — include the node in its own neighbourhood. PyG's GCNConv does this automatically.
Full-batch GCN on a graph with millions of nodes builds an enormous adjacency operation and runs out of memory.
✅ Fix: sample neighbourhoods in mini-batches (GraphSAGE-style) with a neighbour loader.
Mean aggregation throws away how many neighbours a node has — yet for tasks like counting substructures in molecules, degree is the signal. Mean can't distinguish "1 neighbour" from "100 identical neighbours".
✅ Fix: match the aggregator to the task — sum (GIN) preserves count and is most expressive; mean and max suit others.
Time to fly with less scaffolding. The starter gives you a line graph and a comment outline — write the step() function yourself and apply it twice. Remember the self-loop: average each node together with its neighbours.
Lesson 46 complete — you can now reason about graphs the way a GNN does!
You can describe a graph with nodes, edges, features, and an adjacency dict; you have hand-coded a message-passing step (average of neighbours) and watched information spread; you can explain how GCN, GraphSAGE, and GAT aggregate differently; you know the node-, edge-, and graph-level tasks; and you can spot the classic traps — over-smoothing, missing self-loops, scalability, and the wrong aggregator.
🚀 Up next: AutoML & NAS — let algorithms search for the best model and architecture for you.
Practice quiz
What kind of data does a graph neural network operate on?
- Fixed grids like images only
- Sequences of text only
- Nodes joined by edges, where nodes and edges can carry features
- Single independent rows in a table
Answer: Nodes joined by edges, where nodes and edges can carry features. A GNN works on graph-structured data: nodes (entities) joined by edges (relationships), each carrying features.
What are the three steps of message passing in a GNN layer?
- Gather neighbours' features, aggregate them, then transform
- Encode, attend, decode
- Split, shuffle, merge
- Normalise, dropout, activate
Answer: Gather neighbours' features, aggregate them, then transform. Each layer gathers neighbour messages, aggregates them (mean/sum/max), then transforms with a learnable weight and non-linearity.
After one message-passing layer, a node's embedding reflects information from how far away?
- The whole graph
- Only itself
- Exactly 5 hops
- Its 1-hop neighbours
Answer: Its 1-hop neighbours. After one layer a node knows its 1-hop neighbourhood; stacking k layers reaches k hops.
How does a GCN aggregate neighbours?
- By sampling a fixed number of neighbours
- A degree-normalised average of all neighbours
- With a learned attention weight per neighbour
- By taking the maximum only
Answer: A degree-normalised average of all neighbours. GCN takes a degree-normalised average of all neighbours — a simple, strong baseline.
What makes GraphSAGE different from GCN?
- It samples a fixed number of neighbours, so it scales and is inductive
- It uses attention weights
- It ignores node features
- It only works on small graphs
Answer: It samples a fixed number of neighbours, so it scales and is inductive. GraphSAGE samples a fixed number of neighbours, letting it scale to huge graphs and generalise to unseen nodes (inductive).
What does a GAT layer add over a GCN?
- Self-loops only
- A larger learning rate
- A learned attention weight for each neighbour
- Random neighbour dropout
Answer: A learned attention weight for each neighbour. GAT learns an attention weight per neighbour, so important neighbours count more than unimportant ones.
Which is an example of a node-level task?
- Deciding whether a whole molecule is toxic
- Flagging a fraudulent account
- Predicting whether two users will connect
- Pooling all nodes into one vector
Answer: Flagging a fraudulent account. Node-level tasks label individual nodes — for example flagging a fraudulent account in a transaction graph.
How do you get a single prediction for a whole graph (graph-level task)?
- Take the first node's embedding
- Use only the edges
- Ignore the node features
- Pool (read out) all node embeddings into one vector
Answer: Pool (read out) all node embeddings into one vector. Graph-level tasks pool all node embeddings into one vector (a readout), e.g. via global mean pooling.
What is over-smoothing in a GNN?
- Using too few features
- Stacking too many layers until all node embeddings collapse toward the same value
- Adding too many self-loops
- Sampling too few neighbours
Answer: Stacking too many layers until all node embeddings collapse toward the same value. Over-smoothing happens when too many layers make every node embedding converge, so nodes become indistinguishable.
Why add a self-loop in message passing?
- To remove the node's own features
- To make the graph directed
- So a node keeps its own features instead of losing its identity each layer
- To speed up training
Answer: So a node keeps its own features instead of losing its identity each layer. A self-loop includes the node in its own neighbourhood so its original features are preserved in the aggregate.