The real world is not a spreadsheet. It is a web of relationships β proteins binding to proteins, users following users, transactions flowing between accounts. Before you can build a GNN, you need to understand the data structure it operates on: the graph.
1How Computers See a Network
A graph G is defined by a set of nodes V and a set of edges E. Every node represents an entity β a user, an atom, a word β and every edge represents a relationship between two entities. For a computer to process this, we need a numeric representation. The standard choice is the Adjacency Matrix (A), a square NΓN matrix where A[i][j] = 1 if an edge exists between node i and node j, and 0 otherwise.
This works well conceptually, but it does not scale. A social network with 1 million users requires a matrix with 1 trillion entries, the vast majority of which are zeros because most people are not directly connected to most other people. This is the Sparsity Problem. In practice, GNN libraries like PyTorch Geometric store graphs as Edge Lists β a flat list of (source, destination) pairs β which only stores the connections that actually exist. This reduces memory from O(NΒ²) to O(E), where E is the number of edges.
// Dense Matrix: O(NΒ²) memory
const adjMatrix = [
[0,1,0,1], // Node A β B,D
[1,0,1,0], // Node B β A,C
[0,1,0,1], // Node C β B,D
[1,0,1,0], // Node D β A,C
];
// 1M nodes β 1 TRILLION entries β
// Sparse Edge List: O(E) memory
const edgeIndex = [
[0,1],[0,3], // A's edges
[1,2],[2,3], // B,C edges
];
// 4 edges β 4 entries β2Why Standard Neural Networks Fail on Graphs
CNNs work because images are Euclidean: every pixel has exactly 8 neighbors, always arranged in the same spatial order. This regularity lets a convolutional filter slide across the grid in a predictable way. Graphs break this assumption entirely. A node might have 1 neighbor or 10,000 neighbors, and those neighbors have no inherent ordering. If you fed a node's neighbors into an MLP, the model would produce a different output depending on the arbitrary order you chose β which is meaningless and incorrect.
GNNs solve this with Permutation Invariant operations. Instead of processing neighbors in a fixed order, GNNs aggregate neighbor features using functions like Sum, Mean, or Max that produce the same result regardless of the input order. Alongside the structural connectivity, every node carries a Feature Vector β a numeric representation of its attributes (e.g., a user's age and activity count, or an atom's atomic number). These feature vectors are the signals that the GNN learns to transform through message passing.
// β MLP: Permutation-VARIANT
function mlpFail(neighbors) {
return mlp(neighbors.flat());
// [B,C,D] β output_A
// [D,C,B] β output_B β output_A β
}
// β GNN: Permutation-INVARIANT sum
function gnnAggregate(neighbors, dim) {
return neighbors.reduce(
(acc, h_j) =>
acc.map((v, i) => v + h_j[i]),
new Array(dim).fill(0)
);
// [B,C,D] OR [D,C,B] β same result β
}MLP([D,C,B]): [0.43, 0.57] β
SUM([D,C,B]): [1.5, 2.1] β
