Purpose
Graphs break the assumptions behind both sequences and images. There is no fixed left-to-right order and no regular lattice. GNNs solve this by learning over neighborhoods.
Message Passing View
A generic message-passing layer updates node by aggregating over its neighbors:
Most named GNN architectures are concrete instantiations of MSG, AGG, and UPDATE.
flowchart TD j1["h_j1"] -- "MSG" --> agg["AGG over neighbors"] j2["h_j2"] -- "MSG" --> agg j3["h_j3"] -- "MSG" --> agg agg --> upd["UPDATE"] hi["h_i, current layer"] --> upd upd --> hnext["h_i, next layer"]
One layer moves information one hop. Stacking layers gives each node a receptive field of its -hop neighborhood, which is the graph analogue of stacking convolutions.
GCN and GAT differ only in how AGG weighs neighbors
GCN fixes the weights from graph structure: neighbor contributes with weight , determined entirely by degrees. GAT learns the weights: comes from an attention score over the node features themselves. Both fit the message-passing template above; the choice is whether neighbor importance should be structural or content-dependent.
GCN from Spectral Approximation
Kipf and Welling motivate GCN as a localized first-order approximation to spectral graph convolutions. The resulting propagation rule is:
where
adds self-loops and
is the corresponding degree matrix.
The paper explicitly introduces this as a renormalization trick to avoid numerical instability and exploding/vanishing gradients in deeper stacks.
What the GCN Layer Is Doing
The GCN update can be read in three steps:
- add each node to its own neighborhood
- average or normalize neighbor information
- apply a learned linear map
This makes GCN very simple, though the simplicity comes with a bias: all neighbors are treated symmetrically after normalization.
Datasets and Setting
The paper evaluates on citation-network benchmarks:
- Cora
- Citeseer
- Pubmed
and also on NELL. The problem is semi-supervised node classification with few labels.
This framing matters because it is one reason GCN became popular so quickly. The update rule is short, scalable, and effective on a benchmark family many people cared about.
Executable Cora Experiment
The companion notebook implements sparse graph convolution with PyTorch and uses the official LINQS Cora archive. It compares a feature-only MLP, a GCN over observed citations, and the same GCN after edge destinations are shuffled. Five initialization seeds expose run-to-run variation, while edge-label agreement and the rewired null show when neighborhood aggregation has useful structure to exploit.
GAT
Velickovic et al. replace fixed normalized averaging with learned attention over neighborhoods.
First compute unnormalized attention score
then normalize over the neighborhood:
and update:
The paper uses LeakyReLU with negative slope 0.2 inside the attention mechanism.
Multi-Head Attention on Graphs
Like transformers, GAT stabilizes learning with multi-head attention. The paper says explicitly that extending the mechanism to multi-head attention was beneficial.
This allows either:
- concatenation of head outputs in hidden layers
- averaging of head outputs at the final layer
depending on the stage of the network.
Why GAT Was Interesting
GAT addresses a limitation of GCN. In GCN, every neighbor contributes through the same normalized operator. In GAT, the model can assign different weights to different neighbors without expensive global matrix operations or requiring a fixed spectral basis.
The paper evaluates on:
- Cora
- Citeseer
- Pubmed
- PPI
The PPI result matters because it shows the method in an inductive setting where test graphs are unseen during training.
NumPy GCN Layer
import numpy as np
def gcn_layer(A, H, W):
A_hat = A + np.eye(A.shape[0], dtype=A.dtype)
D_hat = np.diag(np.sum(A_hat, axis=1))
D_inv_sqrt = np.linalg.inv(np.sqrt(D_hat))
A_norm = D_inv_sqrt @ A_hat @ D_inv_sqrt
return np.maximum(0.0, A_norm @ H @ W)PyTorch GCN Module
import torch
import torch.nn as nn
class GCNLayer(nn.Module):
def __init__(self, d_in: int, d_out: int):
super().__init__()
self.proj = nn.Linear(d_in, d_out, bias=False)
def forward(self, A: torch.Tensor, H: torch.Tensor) -> torch.Tensor:
A_hat = A + torch.eye(A.size(0), device=A.device, dtype=A.dtype)
D = A_hat.sum(dim=1)
D_inv_sqrt = torch.diag(torch.rsqrt(D))
A_norm = D_inv_sqrt @ A_hat @ D_inv_sqrt
return torch.relu(A_norm @ self.proj(H))What to Watch For
GNNs often run into:
- oversmoothing
- oversquashing
- noisy neighborhoods
- weak or missing node features
Deeper is not better by default
Because each layer averages over neighborhoods, stacking many layers drives node representations toward each other — oversmoothing. This is why the strong Cora baselines here are only two layers deep. Depth in a GNN widens the receptive field to more hops, and past the graph’s useful correlation length that mostly mixes in noise.
The papers here do not solve all of that. They just establish two key design points:
- fixed normalized neighborhood mixing
- learned attention-weighted neighborhood mixing