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.